mirror of
https://we.phorge.it/source/arcanist.git
synced 2024-11-10 00:42:40 +01:00
(stable) Promote 2021 Week 10
This commit is contained in:
commit
e745e770fe
3 changed files with 281 additions and 65 deletions
|
@ -39,7 +39,20 @@ abstract class ArcanistFilesystemConfigurationSource
|
||||||
$content = id(new PhutilJSON())
|
$content = id(new PhutilJSON())
|
||||||
->encodeFormatted($values);
|
->encodeFormatted($values);
|
||||||
|
|
||||||
Filesystem::writeFile($this->path, $content);
|
$path = $this->path;
|
||||||
|
|
||||||
|
// If the containing directory does not exist yet, create it.
|
||||||
|
//
|
||||||
|
// This is expected when (for example) you first write to project
|
||||||
|
// configuration in a Git working copy: the ".git/arc" directory will
|
||||||
|
// not exist yet.
|
||||||
|
|
||||||
|
$dir = dirname($path);
|
||||||
|
if (!Filesystem::pathExists($dir)) {
|
||||||
|
Filesystem::createDirectory($dir, 0755, $recursive = true);
|
||||||
|
}
|
||||||
|
|
||||||
|
Filesystem::writeFile($path, $content);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -300,78 +300,186 @@ final class ArcanistGitLandEngine
|
||||||
$api->getDisplayHash($max_hash),
|
$api->getDisplayHash($max_hash),
|
||||||
$max_commit->getDisplaySummary()));
|
$max_commit->getDisplaySummary()));
|
||||||
|
|
||||||
$argv = array();
|
// See T13576. We have several different approaches for performing the
|
||||||
$argv[] = '--no-stat';
|
// actual merge.
|
||||||
$argv[] = '--no-commit';
|
//
|
||||||
|
// In the simplest case, we're using the "merge" strategy. This means
|
||||||
|
// we always want to merge the entire history, and we can just use a
|
||||||
|
// "git merge" to accomplish our goal. No other approach is permissible
|
||||||
|
// here, so if that doesn't work we're all done and just tell the user
|
||||||
|
// to go resolve conflicts.
|
||||||
|
//
|
||||||
|
// If we're using the "squash" strategy, we may be merging a range of
|
||||||
|
// commits that aren't direct descendants of any ancestor of the state
|
||||||
|
// we're merging into. That is, there may be some ancestors of this
|
||||||
|
// range that we do NOT want to merge. A simple way to get into this
|
||||||
|
// state is to use "--pick". We need to slice off only the commits we
|
||||||
|
// want to merge to ensure we don't bring anything extra along.
|
||||||
|
//
|
||||||
|
// If history is simple and linear, we can select this slice with
|
||||||
|
// "git rebase". However, if history includes merge commits, it seems
|
||||||
|
// as though there are many cases where a (non-interactive) rebase is
|
||||||
|
// doomed to failure.
|
||||||
|
//
|
||||||
|
// If a "git rebase" fails, try to "reduce" the slice first, by using
|
||||||
|
// a "git merge --squash" to collapse the whole slice on top of its
|
||||||
|
// parent. This produces a single non-merge commit with all the changes,
|
||||||
|
// which we can then rebase and merge.
|
||||||
|
|
||||||
// When we're merging into the empty state, Git refuses to perform the
|
$try = array();
|
||||||
// merge until we tell it explicitly that we're doing something unusual.
|
|
||||||
if ($is_empty) {
|
|
||||||
$argv[] = '--allow-unrelated-histories';
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($this->isSquashStrategy()) {
|
|
||||||
// NOTE: We're explicitly specifying "--ff" to override the presence
|
|
||||||
// of "merge.ff" options in user configuration.
|
|
||||||
$argv[] = '--ff';
|
|
||||||
$argv[] = '--squash';
|
|
||||||
} else {
|
|
||||||
$argv[] = '--no-ff';
|
|
||||||
}
|
|
||||||
|
|
||||||
$argv[] = '--';
|
|
||||||
|
|
||||||
$is_rebasing = false;
|
|
||||||
$is_merging = false;
|
|
||||||
try {
|
|
||||||
if ($this->isSquashStrategy() && !$is_empty) {
|
if ($this->isSquashStrategy() && !$is_empty) {
|
||||||
// If we're performing a squash merge, we're going to rebase the
|
$try[] = 'rebase-merge';
|
||||||
// commit range first. We only want to merge the specific commits
|
$try[] = 'reduce-rebase-merge';
|
||||||
// in the range, and merging too much can create conflicts.
|
|
||||||
|
|
||||||
$api->execxLocal('checkout %s --', $max_hash);
|
|
||||||
|
|
||||||
$is_rebasing = true;
|
|
||||||
$api->execxLocal(
|
|
||||||
'rebase --onto %s -- %s',
|
|
||||||
$into_commit,
|
|
||||||
$min_hash.'^');
|
|
||||||
$is_rebasing = false;
|
|
||||||
|
|
||||||
$merge_hash = $api->getCanonicalRevisionName('HEAD');
|
|
||||||
} else {
|
} else {
|
||||||
$merge_hash = $max_hash;
|
$try[] = 'merge';
|
||||||
}
|
}
|
||||||
|
|
||||||
$api->execxLocal('checkout %s --', $into_commit);
|
$merge_exceptions = array();
|
||||||
|
$merge_complete = false;
|
||||||
|
foreach ($try as $approach) {
|
||||||
|
$reduce_min = null;
|
||||||
|
$reduce_max = null;
|
||||||
|
|
||||||
$argv[] = $merge_hash;
|
$rebase_min = null;
|
||||||
|
$rebase_max = null;
|
||||||
|
|
||||||
$is_merging = true;
|
$merge_hash = null;
|
||||||
$api->execxLocal('merge %Ls', $argv);
|
$force_resolve = false;
|
||||||
$is_merging = false;
|
|
||||||
|
switch ($approach) {
|
||||||
|
case 'reduce-rebase-merge':
|
||||||
|
$reduce_min = $min_hash;
|
||||||
|
$reduce_max = $max_hash;
|
||||||
|
|
||||||
|
$log->writeStatus(
|
||||||
|
pht('MERGE'),
|
||||||
|
pht('Attempting to reduce and rebase changes.'));
|
||||||
|
break;
|
||||||
|
case 'rebase-merge':
|
||||||
|
$rebase_min = $min_hash;
|
||||||
|
$rebase_max = $max_hash;
|
||||||
|
|
||||||
|
$log->writeStatus(
|
||||||
|
pht('MERGE'),
|
||||||
|
pht('Attempting to rebase changes.'));
|
||||||
|
break;
|
||||||
|
case 'merge':
|
||||||
|
$merge_hash = $max_hash;
|
||||||
|
|
||||||
|
$log->writeStatus(
|
||||||
|
pht('MERGE'),
|
||||||
|
pht('Attempting to merge changes.'));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Exception(
|
||||||
|
pht(
|
||||||
|
'Unknown merge approach "%s".',
|
||||||
|
$approach));
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if ($reduce_max) {
|
||||||
|
$reduce_dst = $reduce_min.'^';
|
||||||
|
|
||||||
|
// Squash the "into" state on top of the range. The goal is to
|
||||||
|
// guarantee that there are no unresolved conflicts between the
|
||||||
|
// maximum commit and the "into" state, because we're going to
|
||||||
|
// force conflicts to resolve in our favor later.
|
||||||
|
|
||||||
|
$this->applyMergeOperation(
|
||||||
|
$into_commit,
|
||||||
|
$reduce_max,
|
||||||
|
true,
|
||||||
|
$is_empty);
|
||||||
|
|
||||||
|
$join_hash = $this->applyCommitOperation(
|
||||||
|
sprintf(
|
||||||
|
'arc land: join (%s -> %s)',
|
||||||
|
$api->getDisplayHash($into_commit),
|
||||||
|
$api->getDisplayHash($reduce_max)),
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
$allow_empty = true);
|
||||||
|
|
||||||
|
// Squash the range, including the new merge, into a single
|
||||||
|
// commit. The goal here is to produce a new range with no merge
|
||||||
|
// commits so we can rebase it (we'll produce a sequence exactly
|
||||||
|
// one commit long).
|
||||||
|
|
||||||
|
$this->applyMergeOperation(
|
||||||
|
$join_hash,
|
||||||
|
$reduce_dst,
|
||||||
|
true,
|
||||||
|
$is_empty);
|
||||||
|
|
||||||
|
$reduce_hash = $this->applyCommitOperation(
|
||||||
|
sprintf(
|
||||||
|
'arc land: reduce (%s..%s -> %s)',
|
||||||
|
$api->getDisplayHash($reduce_min),
|
||||||
|
$api->getDisplayHash($reduce_max),
|
||||||
|
$reduce_dst));
|
||||||
|
|
||||||
|
// We've reduced the range into a new range that is one commit
|
||||||
|
// long, has no merge commits, and has no conflicts against the
|
||||||
|
// "into" state.
|
||||||
|
|
||||||
|
// We'll rebase it and force conflicts to resolve in favor of the
|
||||||
|
// reduced state. The hope is that we've taken sufficient steps to
|
||||||
|
// guarantee this resolution is always reasonable.
|
||||||
|
|
||||||
|
$rebase_min = $reduce_hash;
|
||||||
|
$rebase_max = $reduce_hash;
|
||||||
|
$force_resolve = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($rebase_max) {
|
||||||
|
$merge_hash = $this->applyRebaseOperation(
|
||||||
|
$rebase_min,
|
||||||
|
$rebase_max,
|
||||||
|
$into_commit,
|
||||||
|
$force_resolve);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->applyMergeOperation(
|
||||||
|
$merge_hash,
|
||||||
|
$into_commit,
|
||||||
|
$this->isSquashStrategy(),
|
||||||
|
$is_empty);
|
||||||
|
|
||||||
|
$log->writeStatus(pht('DONE'), pht('Merge succeeded.'));
|
||||||
|
|
||||||
|
$merge_complete = true;
|
||||||
} catch (CommandException $ex) {
|
} catch (CommandException $ex) {
|
||||||
|
$merge_exceptions[] = $ex;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($merge_complete) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$merge_complete) {
|
||||||
$direct_symbols = $max_commit->getDirectSymbols();
|
$direct_symbols = $max_commit->getDirectSymbols();
|
||||||
$indirect_symbols = $max_commit->getIndirectSymbols();
|
$indirect_symbols = $max_commit->getIndirectSymbols();
|
||||||
if ($direct_symbols) {
|
if ($direct_symbols) {
|
||||||
$message = pht(
|
$message = pht(
|
||||||
'Local commit "%s" (%s) does not merge cleanly into "%s". '.
|
'Local commit "%s" (%s) does not merge cleanly into "%s". '.
|
||||||
'Merge or rebase local changes so they can merge cleanly.',
|
'Rebase or merge local changes so they can merge cleanly.',
|
||||||
$api->getDisplayHash($max_hash),
|
$api->getDisplayHash($max_hash),
|
||||||
$this->getDisplaySymbols($direct_symbols),
|
$this->getDisplaySymbols($direct_symbols),
|
||||||
$api->getDisplayHash($into_commit));
|
$api->getDisplayHash($into_commit));
|
||||||
} else if ($indirect_symbols) {
|
} else if ($indirect_symbols) {
|
||||||
$message = pht(
|
$message = pht(
|
||||||
'Local commit "%s" (reachable from: %s) does not merge cleanly '.
|
'Local commit "%s" (reachable from: %s) does not merge cleanly '.
|
||||||
'into "%s". Merge or rebase local changes so they can merge '.
|
'into "%s". Rebase or merge local changes so they can merge '.
|
||||||
'cleanly.',
|
'cleanly.',
|
||||||
$api->getDisplayHash($max_hash),
|
$api->getDisplayHash($max_hash),
|
||||||
$this->getDisplaySymbols($indirect_symbols),
|
$this->getDisplaySymbols($indirect_symbols),
|
||||||
$api->getDisplayHash($into_commit));
|
$api->getDisplayHash($into_commit));
|
||||||
} else {
|
} else {
|
||||||
$message = pht(
|
$message = pht(
|
||||||
'Local commit "%s" does not merge cleanly into "%s". Merge or '.
|
'Local commit "%s" does not merge cleanly into "%s". Rebase or '.
|
||||||
'rebase local changes so they can merge cleanly.',
|
'merge local changes so they can merge cleanly.',
|
||||||
$api->getDisplayHash($max_hash),
|
$api->getDisplayHash($max_hash),
|
||||||
$api->getDisplayHash($into_commit));
|
$api->getDisplayHash($into_commit));
|
||||||
}
|
}
|
||||||
|
@ -388,18 +496,6 @@ final class ArcanistGitLandEngine
|
||||||
'Use "--incremental" to merge and push changes one by one.'));
|
'Use "--incremental" to merge and push changes one by one.'));
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($is_rebasing) {
|
|
||||||
$api->execManualLocal('rebase --abort');
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($is_merging) {
|
|
||||||
$api->execManualLocal('merge --abort');
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($is_merging || $is_rebasing) {
|
|
||||||
$api->execManualLocal('reset --hard HEAD --');
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new PhutilArgumentUsageException(
|
throw new PhutilArgumentUsageException(
|
||||||
pht('Encountered a merge conflict.'));
|
pht('Encountered a merge conflict.'));
|
||||||
}
|
}
|
||||||
|
@ -410,15 +506,10 @@ final class ArcanistGitLandEngine
|
||||||
$revision_ref = $set->getRevisionRef();
|
$revision_ref = $set->getRevisionRef();
|
||||||
$commit_message = $revision_ref->getCommitMessage();
|
$commit_message = $revision_ref->getCommitMessage();
|
||||||
|
|
||||||
$future = $api->execFutureLocal(
|
$new_cursor = $this->applyCommitOperation(
|
||||||
'commit --author %s --date %s -F - --',
|
$commit_message,
|
||||||
$original_author,
|
$original_author,
|
||||||
$original_date);
|
$original_date);
|
||||||
$future->write($commit_message);
|
|
||||||
$future->resolvex();
|
|
||||||
|
|
||||||
list($stdout) = $api->execxLocal('rev-parse --verify %s', 'HEAD');
|
|
||||||
$new_cursor = trim($stdout);
|
|
||||||
|
|
||||||
if ($is_empty) {
|
if ($is_empty) {
|
||||||
// See T12876. If we're landing into the empty state, we just did a fake
|
// See T12876. If we're landing into the empty state, we just did a fake
|
||||||
|
@ -1601,4 +1692,114 @@ final class ArcanistGitLandEngine
|
||||||
'Desired merge strategy is ambiguous, choose an explicit strategy.'));
|
'Desired merge strategy is ambiguous, choose an explicit strategy.'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function applyRebaseOperation(
|
||||||
|
$src_min,
|
||||||
|
$src_max,
|
||||||
|
$dst,
|
||||||
|
$force_resolve) {
|
||||||
|
|
||||||
|
$api = $this->getRepositoryAPI();
|
||||||
|
|
||||||
|
$api->execxLocal('checkout %s --', $src_max);
|
||||||
|
|
||||||
|
$argv = array();
|
||||||
|
$argv[] = '--onto';
|
||||||
|
$argv[] = gitsprintf('%s', $dst);
|
||||||
|
|
||||||
|
if ($force_resolve) {
|
||||||
|
$argv[] = '--strategy';
|
||||||
|
$argv[] = 'recursive';
|
||||||
|
$argv[] = '--strategy-option';
|
||||||
|
$argv[] = 'theirs';
|
||||||
|
}
|
||||||
|
|
||||||
|
$argv[] = '--';
|
||||||
|
$argv[] = gitsprintf('%s', $src_min.'^');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$api->execxLocal('rebase %Ls', $argv);
|
||||||
|
} catch (CommandException $ex) {
|
||||||
|
$api->execManualLocal('rebase --abort');
|
||||||
|
$api->execManualLocal('reset --hard HEAD --');
|
||||||
|
throw $ex;
|
||||||
|
}
|
||||||
|
|
||||||
|
$merge_hash = $api->getCanonicalRevisionName('HEAD');
|
||||||
|
|
||||||
|
return $merge_hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function applyMergeOperation($src, $dst, $is_squash, $is_empty) {
|
||||||
|
$api = $this->getRepositoryAPI();
|
||||||
|
|
||||||
|
$api->execxLocal('checkout %s --', $dst);
|
||||||
|
|
||||||
|
$argv = array();
|
||||||
|
$argv[] = '--no-stat';
|
||||||
|
$argv[] = '--no-commit';
|
||||||
|
|
||||||
|
// When we're merging into the empty state, Git refuses to perform the
|
||||||
|
// merge until we tell it explicitly that we're doing something unusual.
|
||||||
|
if ($is_empty) {
|
||||||
|
$argv[] = '--allow-unrelated-histories';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($is_squash) {
|
||||||
|
// NOTE: We're explicitly specifying "--ff" to override the presence
|
||||||
|
// of "merge.ff" options in user configuration.
|
||||||
|
$argv[] = '--ff';
|
||||||
|
$argv[] = '--squash';
|
||||||
|
} else {
|
||||||
|
$argv[] = '--no-ff';
|
||||||
|
}
|
||||||
|
|
||||||
|
$argv[] = '--';
|
||||||
|
|
||||||
|
$argv[] = $src;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$api->execxLocal('merge %Ls', $argv);
|
||||||
|
} catch (CommandException $ex) {
|
||||||
|
$api->execManualLocal('merge --abort');
|
||||||
|
$api->execManualLocal('reset --hard HEAD');
|
||||||
|
throw $ex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function applyCommitOperation(
|
||||||
|
$message,
|
||||||
|
$author = null,
|
||||||
|
$date = null,
|
||||||
|
$allow_empty = false) {
|
||||||
|
|
||||||
|
$api = $this->getRepositoryAPI();
|
||||||
|
|
||||||
|
$argv = array();
|
||||||
|
if ($author !== null) {
|
||||||
|
$argv[] = '--author';
|
||||||
|
$argv[] = $author;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($date !== null) {
|
||||||
|
$argv[] = '--date';
|
||||||
|
$argv[] = $date;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($allow_empty) {
|
||||||
|
$argv[] = '--allow-empty';
|
||||||
|
}
|
||||||
|
|
||||||
|
$future = $api->execFutureLocal(
|
||||||
|
'commit %Ls -F - --',
|
||||||
|
$argv);
|
||||||
|
$future->write($message);
|
||||||
|
$future->resolvex();
|
||||||
|
|
||||||
|
list($stdout) = $api->execxLocal('rev-parse --verify %s', 'HEAD');
|
||||||
|
$new_commit = trim($stdout);
|
||||||
|
|
||||||
|
return $new_commit;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -780,8 +780,10 @@ abstract class ArcanistLandEngine
|
||||||
$display_summary = $commit->getDisplaySummary();
|
$display_summary = $commit->getDisplaySummary();
|
||||||
|
|
||||||
if ($is_implicit) {
|
if ($is_implicit) {
|
||||||
|
// NOTE: Mark commits with both a color and a character so the marking
|
||||||
|
// survives copy/paste.
|
||||||
echo tsprintf(
|
echo tsprintf(
|
||||||
" <bg:yellow> %s </bg> %s\n",
|
" ! <bg:yellow> %s </bg> %s\n",
|
||||||
$display_hash,
|
$display_hash,
|
||||||
$display_summary);
|
$display_summary);
|
||||||
} else {
|
} else {
|
||||||
|
|
Loading…
Reference in a new issue