Skip to content

Commit

Permalink
auto detect and merge lockfile conflicts
Browse files Browse the repository at this point in the history
  • Loading branch information
Sebastian McKenzie committed May 31, 2017
1 parent db5edea commit b0c3929
Show file tree
Hide file tree
Showing 8 changed files with 164 additions and 19 deletions.
4 changes: 2 additions & 2 deletions __tests__/commands/add.js
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,7 @@ test.concurrent('add with offline mirror', (): Promise<void> => {
).toBeGreaterThanOrEqual(0);

const rawLockfile = await fs.readFile(path.join(config.cwd, constants.LOCKFILE_FILENAME));
const lockfile = parse(rawLockfile);
const {object: lockfile} = parse(rawLockfile);

expect(lockfile['is-array@^1.0.1']['resolved']).toEqual(
'https://registry.yarnpkg.com/is-array/-/is-array-1.0.1.tgz#e9850cc2cc860c3bc0977e84ccf0dd464584279a',
Expand Down Expand Up @@ -384,7 +384,7 @@ test.concurrent('install with --save and without offline mirror', (): Promise<vo
).toEqual(-1);

const rawLockfile = await fs.readFile(path.join(config.cwd, constants.LOCKFILE_FILENAME));
const lockfile = parse(rawLockfile);
const {object: lockfile} = parse(rawLockfile);

expect(lockfile['is-array@^1.0.1']['resolved']).toMatch(
/https:\/\/registry\.yarnpkg\.com\/is-array\/-\/is-array-1\.0\.1\.tgz#[a-f0-9]+/,
Expand Down
6 changes: 3 additions & 3 deletions __tests__/commands/install/integration.js
Original file line number Diff line number Diff line change
Expand Up @@ -549,7 +549,7 @@ test.concurrent('offline mirror can be enabled from parent dir', (): Promise<voi
};
return runInstall({}, fixture, async (config, reporter) => {
const rawLockfile = await fs.readFile(path.join(config.cwd, 'yarn.lock'));
const lockfile = parse(rawLockfile);
const {object: lockfile} = parse(rawLockfile);
expect(lockfile['mime-types@2.1.14'].resolved).toEqual(
'https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.14.tgz#f7ef7d97583fcaf3b7d282b6f8b5679dab1e94ee',
);
Expand All @@ -564,7 +564,7 @@ test.concurrent('offline mirror can be enabled from parent dir, with merging of
};
return runInstall({}, fixture, async (config, reporter) => {
const rawLockfile = await fs.readFile(path.join(config.cwd, 'yarn.lock'));
const lockfile = parse(rawLockfile);
const {object: lockfile} = parse(rawLockfile);
expect(lockfile['mime-types@2.1.14'].resolved).toEqual(
'https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.14.tgz#f7ef7d97583fcaf3b7d282b6f8b5679dab1e94ee',
);
Expand All @@ -579,7 +579,7 @@ test.concurrent('offline mirror can be disabled locally', (): Promise<void> => {
};
return runInstall({}, fixture, async (config, reporter) => {
const rawLockfile = await fs.readFile(path.join(config.cwd, 'yarn.lock'));
const lockfile = parse(rawLockfile);
const {object: lockfile} = parse(rawLockfile);
expect(lockfile['mime-types@2.1.14'].resolved).toEqual(
'https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.14.tgz#f7ef7d97583fcaf3b7d282b6f8b5679dab1e94ee',
);
Expand Down
59 changes: 50 additions & 9 deletions __tests__/lockfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,19 @@ const objs = [{foo: 'bar'}, {foo: {}}, {foo: 'foo', bar: 'bar'}, {foo: 5}];
let i = 0;
for (const obj of objs) {
test(`parse/stringify ${++i}`, () => {
expect(parse(stringify(obj))).toEqual(nullify(obj));
expect(parse(stringify(obj)).object).toEqual(nullify(obj));
});
}

test('parse', () => {
expect(parse('foo "bar"')).toEqual(nullify({foo: 'bar'}));
expect(parse('"foo" "bar"')).toEqual(nullify({foo: 'bar'}));
expect(parse('foo "bar"')).toEqual(nullify({foo: 'bar'}));

expect(parse(`foo:\n bar "bar"`)).toEqual(nullify({foo: {bar: 'bar'}}));
expect(parse(`foo:\n bar:\n foo "bar"`)).toEqual(nullify({foo: {bar: {}, foo: 'bar'}}));
expect(parse(`foo:\n bar:\n foo "bar"`)).toEqual(nullify({foo: {bar: {foo: 'bar'}}}));
expect(parse('foo:\n bar:\n yes no\nbar:\n yes no')).toEqual(
expect(parse('foo "bar"').object).toEqual(nullify({foo: 'bar'}));
expect(parse('"foo" "bar"').object).toEqual(nullify({foo: 'bar'}));
expect(parse('foo "bar"').object).toEqual(nullify({foo: 'bar'}));

expect(parse(`foo:\n bar "bar"`).object).toEqual(nullify({foo: {bar: 'bar'}}));
expect(parse(`foo:\n bar:\n foo "bar"`).object).toEqual(nullify({foo: {bar: {}, foo: 'bar'}}));
expect(parse(`foo:\n bar:\n foo "bar"`).object).toEqual(nullify({foo: {bar: {foo: 'bar'}}}));
expect(parse('foo:\n bar:\n yes no\nbar:\n yes no').object).toEqual(
nullify({
foo: {
bar: {
Expand Down Expand Up @@ -193,3 +193,44 @@ test('Lockfile.getLockfile (sorting)', () => {

expect(actual).toEqual(expected);
});

test('parse merge conflicts', () => {
const file = `
a:
no "yes"
<<<<<<< HEAD
b:
foo "bar"
=======
c:
bar "foo"
>>>>>>> branch-a
d:
yes "no"
`;

const {type, object} = parse(file);
expect(type).toEqual('merge');
expect(object.a.no).toEqual('yes');
expect(object.b.foo).toEqual('bar');
expect(object.c.bar).toEqual('foo');
expect(object.d.yes).toEqual('no');
});

test('parse merge conflict fail', () => {
const file = `
<<<<<<< HEAD
b:
foo: "bar"
=======
c:
bar "foo"
>>>>>>> branch-a
`;

const {type, object} = parse(file);
expect(type).toEqual('conflict');
expect(Object.keys(object).length).toEqual(0);
});
96 changes: 94 additions & 2 deletions src/lockfile/parse.js
Original file line number Diff line number Diff line change
Expand Up @@ -314,9 +314,101 @@ export class Parser {
}
}

export default function(str: string, fileLoc: string = 'lockfile'): Object {
str = stripBOM(str);
const MERGE_CONFLICT_START = '<<<<<<<';
const MERGE_CONFLICT_SEP = '=======';
const MERGE_CONFLICT_END = '>>>>>>>';

/**
* Extract the two versions of the lockfile from a merge conflict.
*/

export function extractConflictVariants(str: string): Array<string> {
const variants: Array<Array<string>> = [[], []];
const lines = str.split(/\n/g);

while (lines.length) {
const line = lines.shift();
if (line.startsWith(MERGE_CONFLICT_START)) {
// get the first variant
while (lines.length) {
const line = lines.shift();
if (line === MERGE_CONFLICT_SEP) {
break;
} else {
variants[0].push(line);
}
}

// get the second variant
while (lines.length) {
const line = lines.shift();
if (line.startsWith(MERGE_CONFLICT_END)) {
break;
} else {
variants[1].push(line);
}
}
} else {
variants[0].push(line);
variants[1].push(line);
}
}

return variants.map(lines => lines.join('\n'));
}

/**
* Check if a lockfile has merge conflicts.
*/

export function hasMergeConflicts(str: string): boolean {
return str.includes(MERGE_CONFLICT_START);
}

/**
* Parse the lockfile.
*/

function parse(str: string, fileLoc: string): Object {
const parser = new Parser(str, fileLoc);
parser.next();
return parser.parse();
}

type ParseResult = {
type: 'merge' | 'none' | 'conflict',
object: Object,
};

/**
* Parse and merge the two variants in a conflicted lockfile.
*/

function parseWithConflict(str: string, fileLoc: string): ParseResult {
const variants = extractConflictVariants(str);

try {
const obj = Object.assign(
{},
parse(variants[0], fileLoc),
parse(variants[1], fileLoc),
);
return {type: 'merge', object: obj};
} catch (err) {
if (err instanceof SyntaxError) {
return {type: 'conflict', object: {}};
} else {
throw err;
}
}
}

export default function(str: string, fileLoc: string = 'lockfile'): ParseResult {
str = stripBOM(str);

if (hasMergeConflicts(str)) {
return parseWithConflict(str, fileLoc);
} else {
return {type: 'none', object: parse(str, fileLoc)};
}
}
12 changes: 11 additions & 1 deletion src/lockfile/wrapper.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,17 @@ export default class Lockfile {

if (await fs.exists(lockfileLoc)) {
rawLockfile = await fs.readFile(lockfileLoc);
lockfile = parse(rawLockfile, lockfileLoc);
const lockResult = parse(rawLockfile, lockfileLoc);

if (reporter) {
if (lockResult.type === 'merge') {
reporter.info(reporter.lang('lockfileMerged'));
} else if (lockResult.type === 'conflict') {
reporter.warn(reporter.lang('lockfileConflict'));
}
}

lockfile = lockResult.object;
} else {
if (reporter) {
reporter.info(reporter.lang('noLockfileFound'));
Expand Down
2 changes: 1 addition & 1 deletion src/rc.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ let rcArgsCache;

const buildRcConf = () =>
rcUtil.findRc('yarn', (fileText, filePath) => {
const values = parse(fileText, 'yarnrc');
const {object: values} = parse(fileText, 'yarnrc');
const keys = Object.keys(values);

for (const key of keys) {
Expand Down
2 changes: 1 addition & 1 deletion src/registries/yarn-registry.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export default class YarnRegistry extends NpmRegistry {

async loadConfig(): Promise<void> {
for (const [isHome, loc, file] of await this.getPossibleConfigLocations('.yarnrc', this.reporter)) {
const config = parse(file, loc);
const {object: config} = parse(file, loc);

if (isHome) {
this.homeConfig = config;
Expand Down
2 changes: 2 additions & 0 deletions src/reporters/lang/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ const messages = {
couldntFindManifestIn: "Couldn't find manifest in $0.",
shrinkwrapWarning: 'npm-shrinkwrap.json found. This will not be updated or respected. See https://yarnpkg.com/en/docs/migrating-from-npm for more information.',
lockfileOutdated: 'Outdated lockfile. Please run `yarn install` and try again.',
lockfileMerged: 'Merge conflict detected in yarn.lock and successfully merged.',
lockfileConflict: 'A merge conflict was found in yarn.lock but it could not be successfully merged, regenerating yarn.lock from scratch.',
ignoredScripts: 'Ignored scripts due to flag.',
missingAddDependencies: 'Missing list of packages to add to your project.',
yesWarning: 'The yes flag has been set. This will automatically answer yes to all questions which may have security implications.',
Expand Down

0 comments on commit b0c3929

Please sign in to comment.