This repository has been archived by the owner. It is now read-only.
Permalink
Browse files

pkglock: automatically resolve conflicts

  • Loading branch information...
zkat authored and iarna committed Sep 30, 2017
1 parent 8279515 commit e27674c221dc17473f23bffa50123e49a021ae34
@@ -136,6 +136,25 @@ on. Additionally, the diffs from these changes are human-readable and will
inform you of any changes npm has made to your `node_modules`, so you can notice
if any transitive dependencies were updated, hoisted, etc.
### Resolving lockfile conflicts
Occasionally, two separate npm install will create package locks that cause
merge conflicts in source control systems. As of `npm@5.7.0`, these conflicts
can be resolved by manually fixing any `package.json` conflicts, and then
running `npm install [--package-lock-only]` again. npm will automatically
resolve any conflicts for you and write a merged package lock that includes all
the dependencies from both branches in a reasonable tree. If
`--package-lock-only` is provided, it will do this without also modifying your
local `node_modules/`.
To make this process seamless on git, consider installing
[`npm-merge-driver`](https://npm.im/npm-merge-driver), which will teach git how
to do this itself without any user interaction. In short: `$ npx
npm-merge-driver install -g` will let you do this, and even works with
pre-`npm@5.7.0` versions of npm 5, albeit a bit more noisily. Note that if
`package.json` itself conflicts, you will have to resolve that by hand and run
`npm install` manually, even with the merge driver.
## SEE ALSO
* https://medium.com/@sdboyer/so-you-want-to-write-a-package-manager-4ae9c17d9527

Some generated files are not rendered by default. Learn more.

Oops, something went wrong.
@@ -25,14 +25,7 @@ function readShrinkwrap (child, next) {
log.warn('read-shrinkwrap', 'Ignoring package-lock.json because there is already an npm-shrinkwrap.json. Please use only one of the two.')
}
const name = shrinkwrap ? 'npm-shrinkwrap.json' : 'package-lock.json'
let parsed = null
if (shrinkwrap || lockfile) {
try {
parsed = parseJSON(shrinkwrap || lockfile)
} catch (ex) {
throw ex
}
}
const parsed = parsePkgLock(shrinkwrap || lockfile, name)
if (parsed && parsed.lockfileVersion !== PKGLOCK_VERSION) {
log.warn('read-shrinkwrap', `This version of npm is compatible with lockfileVersion@${PKGLOCK_VERSION}, but ${name} was generated for lockfileVersion@${parsed.lockfileVersion || 0}. I'll try to do my best with it!`)
}
@@ -43,7 +36,8 @@ function readShrinkwrap (child, next) {
function maybeReadFile (name, child) {
return readFileAsync(
path.join(child.path, name)
path.join(child.path, name),
'utf8'
).catch({code: 'ENOENT'}, () => null)
}
@@ -56,3 +50,57 @@ module.exports.andInflate = function (child, next) {
}
}))
}
const PARENT_RE = /\|{7,}/g
const OURS_RE = /\<{7,}/g
const THEIRS_RE = /\={7,}/g
const END_RE = /\>{7,}/g
module.exports._isDiff = isDiff
function isDiff (str) {
return str.match(OURS_RE) && str.match(THEIRS_RE) && str.match(END_RE)
}
module.exports._parsePkgLock = parsePkgLock
function parsePkgLock (str, filename) {
if (!str) { return null }
try {
return parseJSON(str)
} catch (e) {
if (isDiff(str)) {
log.warn('conflict', `A git conflict was detected in ${filename}. Attempting to auto-resolve.`)
const pieces = str.split(/[\n\r]+/g).reduce((acc, line) => {
if (line.match(PARENT_RE)) acc.state = 'parent'
else if (line.match(OURS_RE)) acc.state = 'ours'
else if (line.match(THEIRS_RE)) acc.state = 'theirs'
else if (line.match(END_RE)) acc.state = 'top'
else {
if (acc.state === 'top' || acc.state === 'ours') acc.ours += line
if (acc.state === 'top' || acc.state === 'theirs') acc.theirs += line
if (acc.state === 'top' || acc.state === 'parent') acc.parent += line
}
return acc
}, {
state: 'top',
ours: '',
theirs: '',
parent: ''
})
try {
const ours = parseJSON(pieces.ours)
const theirs = parseJSON(pieces.theirs)
return reconcileLockfiles(ours, theirs)
} catch (_e) {
log.error('conflict', `Automatic conflict resolution failed. Please manually resolve conflicts in ${filename} and try again.`)
log.silly('conflict', `Error during resolution: ${_e}`)
throw e
}
} else {
throw e
}
}
}
function reconcileLockfiles (parent, ours, theirs) {
return Object.assign({}, ours, theirs)
}
@@ -182,7 +182,7 @@ function save (dir, pkginfo, opts, cb) {
indent: (pkg && pkg.indent) || 2
}
)
const updated = updateLockfileMetadata(pkginfo, pkg && pkg.data)
const updated = updateLockfileMetadata(pkginfo, pkg && JSON.parse(pkg.raw))
const swdata = JSON.stringify(updated, null, info.indent) + '\n'
if (swdata === info.raw) {
// skip writing if file is identical
@@ -244,9 +244,7 @@ function checkPackageFile (dir, name) {
return {
path: file,
raw: data,
data: JSON.parse(data),
indent: detectIndent(data).indent || 2
}
}).catch({code: 'ENOENT'}, () => {})
}
@@ -52,6 +52,25 @@ function errorMessage (er) {
break
case 'EJSONPARSE':
const path = require('path')
// Check whether we ran into a conflict in our own package.json
if (er.file === path.join(npm.prefix, 'package.json')) {
const isDiff = require('../install/read-shrinkwrap.js')._isDiff
const txt = require('fs').readFileSync(er.file, 'utf8')
if (isDiff(txt)) {
detail.push([
'',
[
'Merge conflict detected in your package.json.',
'',
'Please resolve the package.json conflict and retry the command:',
'',
`$ ${process.argv.join(' ')}`
].join('\n')
])
break
}
}
short.push(['', er.message])
short.push(['', 'File: ' + er.file])
detail.push([
@@ -0,0 +1,117 @@
'use strict'
const BB = require('bluebird')
const common = require('../common-tap.js')
const fs = BB.promisifyAll(require('fs'))
const path = require('path')
const rimraf = BB.promisify(require('rimraf'))
const test = require('tap').test
const Tacks = require('tacks')
const File = Tacks.File
const Dir = Tacks.Dir
const testDir = path.resolve(__dirname, path.basename(__filename, '.js'))
const modAdir = path.resolve(testDir, 'modA')
const modBdir = path.resolve(testDir, 'modB')
const modCdir = path.resolve(testDir, 'modC')
test('conflicts in shrinkwrap are auto-resolved on install', (t) => {
const fixture = new Tacks(Dir({
'package.json': File({
name: 'foo',
dependencies: {
modA: 'file://' + modAdir,
modB: 'file://' + modBdir
},
devDependencies: {
modC: 'file://' + modCdir
}
}),
'npm-shrinkwrap.json': File(
`
{
"name": "foo",
"requires": true,
"lockfileVersion": 1,
"dependencies": {
<<<<<<< HEAD
"modA": {
"version": "file:modA"
||||||| merged common ancestors
"modB": {
"version": "file:modB"
=======
"modC": {
"version": "file:modC",
"dev": true
>>>>>>> branch
}
}
}
`),
'modA': Dir({
'package.json': File({
name: 'modA',
version: '1.0.0'
})
}),
'modB': Dir({
'package.json': File({
name: 'modB',
version: '1.0.0'
})
}),
'modC': Dir({
'package.json': File({
name: 'modC',
version: '1.0.0'
})
})
}))
fixture.create(testDir)
function readJson (file) {
return fs.readFileAsync(path.join(testDir, file)).then(JSON.parse)
}
return BB.fromNode((cb) => {
common.npm([
'install',
'--loglevel', 'warn'
], {cwd: testDir}, (err, code, out, stderr) => {
t.comment(stderr)
t.match(stderr, /warn.*conflict/gi, 'warns about a conflict')
cb(err || (code && new Error('non-zero exit code')) || null, out)
})
})
.then(() => BB.join(
readJson('npm-shrinkwrap.json'),
readJson('node_modules/modA/package.json'),
readJson('node_modules/modB/package.json'),
readJson('node_modules/modC/package.json'),
(lockfile, A, B, C) => {
t.deepEqual(lockfile, {
name: 'foo',
requires: true,
lockfileVersion: 1,
dependencies: {
modA: {
version: 'file:modA'
},
modB: {
version: 'file:modB'
},
modC: {
version: 'file:modC',
dev: true
}
}
}, 'resolved lockfile matches expectations')
t.equal(A.name, 'modA', 'installed modA')
t.equal(B.name, 'modB', 'installed modB')
t.equal(C.name, 'modC', 'installed modC')
}
))
})
test('cleanup', () => rimraf(testDir))

0 comments on commit e27674c

Please sign in to comment.