Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(essentials): add enableMergeConflictPrompt configuration setting #6126

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
32 changes: 32 additions & 0 deletions .yarn/versions/09ca2e27.yml
@@ -0,0 +1,32 @@
releases:
"@yarnpkg/builder": patch
"@yarnpkg/cli": patch
"@yarnpkg/core": minor
"@yarnpkg/doctor": patch
"@yarnpkg/extensions": patch
"@yarnpkg/nm": patch
"@yarnpkg/plugin-compat": patch
"@yarnpkg/plugin-constraints": patch
"@yarnpkg/plugin-dlx": patch
"@yarnpkg/plugin-essentials": minor
"@yarnpkg/plugin-exec": patch
"@yarnpkg/plugin-file": patch
"@yarnpkg/plugin-git": patch
"@yarnpkg/plugin-github": patch
"@yarnpkg/plugin-http": patch
"@yarnpkg/plugin-init": patch
"@yarnpkg/plugin-interactive-tools": patch
"@yarnpkg/plugin-link": patch
"@yarnpkg/plugin-nm": patch
"@yarnpkg/plugin-npm": patch
"@yarnpkg/plugin-npm-cli": patch
"@yarnpkg/plugin-pack": patch
"@yarnpkg/plugin-patch": patch
"@yarnpkg/plugin-pnp": patch
"@yarnpkg/plugin-pnpm": patch
"@yarnpkg/plugin-stage": patch
"@yarnpkg/plugin-typescript": patch
"@yarnpkg/plugin-version": patch
"@yarnpkg/plugin-workspace-tools": patch
"@yarnpkg/pnpify": patch
"@yarnpkg/sdks": patch
Expand Up @@ -227,5 +227,7 @@ describe(`Features`, () => {
},
),
);

// TODO: Add tests for `enableMergeConflictPrompt`
});
});
7 changes: 7 additions & 0 deletions packages/docusaurus/static/configuration/yarnrc.json
Expand Up @@ -186,6 +186,13 @@
"type": "boolean",
"examples": [false]
},
"enableMergeConflictPrompt": {
"_package": "@yarnpkg/plugin-essentials",
"title": "Define whether to prompt the user about what to do when Yarn detects merge conflicts in the lockfile.",
"description": "If true, Yarn will prompt the user about what to do when it detects merge conflicts in the lockfile.",
"type": "boolean",
"examples": [false]
},
"enableMessageNames": {
"_package": "@yarnpkg/core",
"title": "Define whether to prepend a message name before each printed line or not.",
Expand Down
73 changes: 70 additions & 3 deletions packages/plugin-essentials/sources/commands/install.ts
Expand Up @@ -2,8 +2,10 @@ import {BaseCommand, WorkspaceRequiredError}
import {Configuration, Cache, MessageName, Project, ReportError, StreamReport, formatUtils, InstallMode, execUtils, structUtils, LEGACY_PLUGINS, ConfigurationValueMap, YarnVersion, httpUtils, reportOptionDeprecations} from '@yarnpkg/core';
import {xfs, ppath, Filename, PortablePath} from '@yarnpkg/fslib';
import {parseSyml, stringifySyml} from '@yarnpkg/parsers';
import {gitUtils} from '@yarnpkg/plugin-git';
import CI from 'ci-info';
import {Command, Option, Usage, UsageError} from 'clipanion';
import {prompt} from 'enquirer';
import semver from 'semver';
import * as t from 'typanion';

Expand Down Expand Up @@ -215,7 +217,7 @@ export default class YarnCommand extends BaseCommand {
changed = true;
}

if (await autofixMergeConflicts(configuration, immutable)) {
if (await autofixMergeConflicts(configuration, immutable, this.context)) {
report.reportInfo(MessageName.AUTOMERGE_SUCCESS, `Automatically fixed merge conflicts 👍`);
changed = true;
}
Expand Down Expand Up @@ -374,7 +376,10 @@ export default class YarnCommand extends BaseCommand {

const MERGE_CONFLICT_START = `<<<<<<<`;

async function autofixMergeConflicts(configuration: Configuration, immutable: boolean) {
type MergeType = `MERGE` | `REBASE` | `CHERRY_PICK`;
type MergeConflictPlan = `combine` | `current` | `incoming`;

async function autofixMergeConflicts(configuration: Configuration, immutable: boolean, context: YarnCommand['context']) {
if (!configuration.projectCwd)
return false;

Expand All @@ -389,17 +394,20 @@ async function autofixMergeConflicts(configuration: Configuration, immutable: bo
if (immutable)
throw new ReportError(MessageName.AUTOMERGE_IMMUTABLE, `Cannot autofix a lockfile when running an immutable install`);

let mergeType: MergeType = `MERGE`;
let commits = await execUtils.execvp(`git`, [`rev-parse`, `MERGE_HEAD`, `HEAD`], {
cwd: configuration.projectCwd,
});

if (commits.code !== 0) {
mergeType = `REBASE`;
commits = await execUtils.execvp(`git`, [`rev-parse`, `REBASE_HEAD`, `HEAD`], {
cwd: configuration.projectCwd,
});
}

if (commits.code !== 0) {
mergeType = `CHERRY_PICK`;
commits = await execUtils.execvp(`git`, [`rev-parse`, `CHERRY_PICK_HEAD`, `HEAD`], {
cwd: configuration.projectCwd,
});
Expand All @@ -408,7 +416,9 @@ async function autofixMergeConflicts(configuration: Configuration, immutable: bo
if (commits.code !== 0)
throw new ReportError(MessageName.AUTOMERGE_GIT_ERROR, `Git returned an error when trying to find the commits pertaining to the conflict`);

let variants = await Promise.all(commits.stdout.trim().split(/\n/).map(async hash => {
const commitHashes = await getDesiredCommitHashes(configuration, commits.stdout.trim().split(/\n/), mergeType, context);

let variants = await Promise.all(commitHashes.map(async hash => {
const content = await execUtils.execvp(`git`, [`show`, `${hash}:./${Filename.lockfile}`], {
cwd: configuration.projectCwd!,
});
Expand Down Expand Up @@ -486,6 +496,63 @@ async function autofixMergeConflicts(configuration: Configuration, immutable: bo
return true;
}

async function getDesiredCommitHashes(configuration: Configuration, commitHashes: Array<string>, mergeType: MergeType, context: YarnCommand['context']) {
if (!configuration.get(`enableMergeConflictPrompt`))
return commitHashes;

const gitRoot = await gitUtils.fetchRoot(configuration.projectCwd!);

if (!gitRoot)
throw new ReportError(MessageName.AUTOMERGE_GIT_ERROR, `Failed to find the Git root to find more information about the conflict`);

const mergeMsgContent = await xfs.readFilePromise(ppath.join(gitRoot, `.git`, `MERGE_MSG`), `utf8`);
const mergeMsgFirstLine = mergeMsgContent.split(/\r?\n/)[0];
let incomingName = mergeMsgFirstLine;
if (mergeType === `MERGE`) {
const match = mergeMsgFirstLine.match(/^Merge branch '(.+?)'/);
if (match) {
incomingName = match[1];
}
}

const {mergeConflictPlan} = await prompt<{mergeConflictPlan: MergeConflictPlan}>({
type: `select`,
name: `mergeConflictPlan`,
message: `How do you want to resolve merge conflicts in the lockfile?`,
choices: [
{
name: `combine`,
message: `Combine all changes (default)`,
},
{
name: `current`,
message: `Prefer current changes (HEAD)`,
},
{
name: `incoming`,
message: `Prefer incoming changes (${incomingName})`,
},
],
onCancel: () => process.exit(130),
result(name: MergeConflictPlan) {
return name;
},
stdin: context.stdin as NodeJS.ReadStream,
stdout: context.stdout as NodeJS.WriteStream,
});

switch (mergeConflictPlan) {
case `combine`:
return commitHashes;
case `current`:
return [commitHashes[commitHashes.length - 1]];
case `incoming`:
return [commitHashes[0]];
default:
return commitHashes;
}
}

async function autofixLegacyPlugins(configuration: Configuration, immutable: boolean) {
if (!configuration.projectCwd)
return false;
Expand Down
6 changes: 6 additions & 0 deletions packages/yarnpkg-core/sources/Configuration.ts
Expand Up @@ -576,6 +576,11 @@ export const coreDefinitions: {[coreSettingName: string]: SettingsDefinition} =
default: [`.env.yarn?`],
isArray: true,
},
enableMergeConflictPrompt: {
description: `If true, prompt the user about what to do when Yarn detects merge conflicts in the lockfile`,
type: SettingsType.BOOLEAN,
default: false,
},

// Package patching - to fix incorrect definitions
packageExtensions: {
Expand Down Expand Up @@ -693,6 +698,7 @@ export interface ConfigurationValueMap {

// Miscellaneous settings
injectEnvironmentFiles: Array<PortablePath>;
enableMergeConflictPrompt: boolean;

// Package patching - to fix incorrect definitions
packageExtensions: Map<string, miscUtils.ToMapValue<{
Expand Down