Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F15301759
D21590.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
12 KB
Referenced Files
None
Subscribers
None
D21590.diff
View Options
diff --git a/src/land/engine/ArcanistGitLandEngine.php b/src/land/engine/ArcanistGitLandEngine.php
--- a/src/land/engine/ArcanistGitLandEngine.php
+++ b/src/land/engine/ArcanistGitLandEngine.php
@@ -300,78 +300,186 @@
$api->getDisplayHash($max_hash),
$max_commit->getDisplaySummary()));
- $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 ($this->isSquashStrategy()) {
- // NOTE: We're explicitly specifying "--ff" to override the presence
- // of "merge.ff" options in user configuration.
- $argv[] = '--ff';
- $argv[] = '--squash';
+ // See T13576. We have several different approaches for performing the
+ // actual merge.
+ //
+ // 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.
+
+ $try = array();
+ if ($this->isSquashStrategy() && !$is_empty) {
+ $try[] = 'rebase-merge';
+ $try[] = 'reduce-rebase-merge';
} else {
- $argv[] = '--no-ff';
+ $try[] = 'merge';
}
- $argv[] = '--';
+ $merge_exceptions = array();
+ $merge_complete = false;
+ foreach ($try as $approach) {
+ $reduce_min = null;
+ $reduce_max = null;
- $is_rebasing = false;
- $is_merging = false;
- try {
- if ($this->isSquashStrategy() && !$is_empty) {
- // If we're performing a squash merge, we're going to rebase the
- // commit range first. We only want to merge the specific commits
- // in the range, and merging too much can create conflicts.
+ $rebase_min = null;
+ $rebase_max = null;
- $api->execxLocal('checkout %s --', $max_hash);
+ $merge_hash = null;
+ $force_resolve = false;
- $is_rebasing = true;
- $api->execxLocal(
- 'rebase --onto %s -- %s',
- $into_commit,
- $min_hash.'^');
- $is_rebasing = false;
+ switch ($approach) {
+ case 'reduce-rebase-merge':
+ $reduce_min = $min_hash;
+ $reduce_max = $max_hash;
- $merge_hash = $api->getCanonicalRevisionName('HEAD');
- } else {
- $merge_hash = $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));
}
- $api->execxLocal('checkout %s --', $into_commit);
+ 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;
+ }
- $argv[] = $merge_hash;
+ if ($rebase_max) {
+ $merge_hash = $this->applyRebaseOperation(
+ $rebase_min,
+ $rebase_max,
+ $into_commit,
+ $force_resolve);
+ }
- $is_merging = true;
- $api->execxLocal('merge %Ls', $argv);
- $is_merging = false;
- } catch (CommandException $ex) {
+ $this->applyMergeOperation(
+ $merge_hash,
+ $into_commit,
+ $this->isSquashStrategy(),
+ $is_empty);
+
+ $log->writeStatus(pht('DONE'), pht('Merge succeeded.'));
+
+ $merge_complete = true;
+ } catch (CommandException $ex) {
+ $merge_exceptions[] = $ex;
+ }
+
+ if ($merge_complete) {
+ break;
+ }
+ }
+
+ if (!$merge_complete) {
$direct_symbols = $max_commit->getDirectSymbols();
$indirect_symbols = $max_commit->getIndirectSymbols();
if ($direct_symbols) {
$message = pht(
'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),
$this->getDisplaySymbols($direct_symbols),
$api->getDisplayHash($into_commit));
} else if ($indirect_symbols) {
$message = pht(
'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.',
$api->getDisplayHash($max_hash),
$this->getDisplaySymbols($indirect_symbols),
$api->getDisplayHash($into_commit));
} else {
$message = pht(
- 'Local commit "%s" does not merge cleanly into "%s". Merge or '.
- 'rebase local changes so they can merge cleanly.',
+ 'Local commit "%s" does not merge cleanly into "%s". Rebase or '.
+ 'merge local changes so they can merge cleanly.',
$api->getDisplayHash($max_hash),
$api->getDisplayHash($into_commit));
}
@@ -388,18 +496,6 @@
'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(
pht('Encountered a merge conflict.'));
}
@@ -410,15 +506,10 @@
$revision_ref = $set->getRevisionRef();
$commit_message = $revision_ref->getCommitMessage();
- $future = $api->execFutureLocal(
- 'commit --author %s --date %s -F - --',
+ $new_cursor = $this->applyCommitOperation(
+ $commit_message,
$original_author,
$original_date);
- $future->write($commit_message);
- $future->resolvex();
-
- list($stdout) = $api->execxLocal('rev-parse --verify %s', 'HEAD');
- $new_cursor = trim($stdout);
if ($is_empty) {
// See T12876. If we're landing into the empty state, we just did a fake
@@ -1601,4 +1692,114 @@
'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;
+ }
+
+
}
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Thu, Mar 6, 6:11 PM (4 d, 4 h ago)
Storage Engine
blob
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
7301059
Default Alt Text
D21590.diff (12 KB)
Attached To
Mode
D21590: In "arc land", if rebasing a range fails, attempt to "reduce" it
Attached
Detach File
Event Timeline
Log In to Comment