Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F15352043
D21324.id50787.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
14 KB
Referenced Files
None
Subscribers
None
D21324.id50787.diff
View Options
diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -220,6 +220,8 @@
'ArcanistGitCommitSymbolCommitHardpointQuery' => 'ref/commit/ArcanistGitCommitSymbolCommitHardpointQuery.php',
'ArcanistGitLandEngine' => 'land/engine/ArcanistGitLandEngine.php',
'ArcanistGitLocalState' => 'repository/state/ArcanistGitLocalState.php',
+ 'ArcanistGitRawCommit' => 'repository/raw/ArcanistGitRawCommit.php',
+ 'ArcanistGitRawCommitTestCase' => 'repository/raw/__tests__/ArcanistGitRawCommitTestCase.php',
'ArcanistGitUpstreamPath' => 'repository/api/ArcanistGitUpstreamPath.php',
'ArcanistGitWorkingCopy' => 'workingcopy/ArcanistGitWorkingCopy.php',
'ArcanistGitWorkingCopyRevisionHardpointQuery' => 'query/ArcanistGitWorkingCopyRevisionHardpointQuery.php',
@@ -1239,6 +1241,8 @@
'ArcanistGitCommitSymbolCommitHardpointQuery' => 'ArcanistWorkflowGitHardpointQuery',
'ArcanistGitLandEngine' => 'ArcanistLandEngine',
'ArcanistGitLocalState' => 'ArcanistRepositoryLocalState',
+ 'ArcanistGitRawCommit' => 'Phobject',
+ 'ArcanistGitRawCommitTestCase' => 'PhutilTestCase',
'ArcanistGitUpstreamPath' => 'Phobject',
'ArcanistGitWorkingCopy' => 'ArcanistWorkingCopy',
'ArcanistGitWorkingCopyRevisionHardpointQuery' => 'ArcanistWorkflowGitHardpointQuery',
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
@@ -226,20 +226,18 @@
return $this->getLandTargetLocalCommit($target);
}
- private function updateWorkingCopy($into_commit) {
- $api = $this->getRepositoryAPI();
- if ($into_commit === null) {
- throw new Exception('TODO: Author a new empty state.');
- } else {
- $api->execxLocal('checkout %s --', $into_commit);
- }
- }
-
protected function executeMerge(ArcanistLandCommitSet $set, $into_commit) {
$api = $this->getRepositoryAPI();
$log = $this->getLogEngine();
- $this->updateWorkingCopy($into_commit);
+ $is_empty = ($into_commit === null);
+
+ if ($is_empty) {
+ $empty_commit = ArcanistGitRawCommit::newEmptyCommit();
+ $into_commit = $api->writeRawCommit($empty_commit);
+ }
+
+ $api->execxLocal('checkout %s --', $into_commit);
$commits = $set->getCommits();
$max_commit = last($commits);
@@ -251,7 +249,8 @@
// as changes.
list($changes) = $api->execxLocal(
- 'diff --no-ext-diff HEAD..%s --',
+ 'diff --no-ext-diff %s..%s --',
+ $into_commit,
$source_commit);
$changes = trim($changes);
if (!strlen($changes)) {
@@ -274,20 +273,30 @@
$this->getDisplayHash($source_commit),
$max_commit->getDisplaySummary()));
- try {
+ $argv = array();
+ $argv[] = '--no-stat';
+ $argv[] = '--no-commit';
- if ($this->isSquashStrategy()) {
- // NOTE: We're explicitly specifying "--ff" to override the presence
- // of "merge.ff" options in user configuration.
+ // 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';
+ }
- $api->execxLocal(
- 'merge --no-stat --no-commit --ff --squash -- %s',
- $source_commit);
- } else {
- $api->execxLocal(
- 'merge --no-stat --no-commit --no-ff -- %s',
- $source_commit);
- }
+ 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[] = '--';
+ $argv[] = $source_commit;
+
+ try {
+ $api->execxLocal('merge %Ls', $argv);
} catch (CommandException $ex) {
// TODO: If we previously succeeded with at least one merge, we could
@@ -340,14 +349,23 @@
list($stdout) = $api->execxLocal('rev-parse --verify %s', 'HEAD');
$new_cursor = trim($stdout);
- if ($into_commit === null) {
+ if ($is_empty) {
+ // See T12876. If we're landing into the empty state, we just did a fake
+ // merge on top of an empty commit. We're now on a commit with all of the
+ // right details except that it has an extra empty commit as a parent.
+
+ // Create a new commit which is the same as the current HEAD, except that
+ // it doesn't have the extra parent.
+
+ $raw_commit = $api->readRawCommit($new_cursor);
if ($this->isSquashStrategy()) {
- throw new Exception(
- pht('TODO: Rewrite HEAD to have no parents.'));
+ $raw_commit->setParents(array());
} else {
- throw new Exception(
- pht('TODO: Rewrite HEAD to have only source as a parent.'));
+ $raw_commit->setParents(array($source_commit));
}
+ $new_cursor = $api->writeRawCommit($raw_commit);
+
+ $api->execxLocal('checkout %s --', $new_cursor);
}
return $new_cursor;
@@ -720,9 +738,14 @@
);
}
- private function didHoldChanges() {
+ protected function didHoldChanges(
+ ArcanistRepositoryLocalState $state) {
$log = $this->getLogEngine();
+ // TODO: This probably needs updates.
+
+ // TODO: We should refuse "--hold" if we stash.
+
if ($this->getIsGitPerforce()) {
$this->writeInfo(
pht('HOLD'),
@@ -738,16 +761,15 @@
pht(
'Holding change locally, it has not been pushed.'));
- $push_command = csprintf(
- '$ git push -- %R %R:%R',
- $this->getTargetRemote(),
- $this->mergedRef,
- $this->getTargetOnto());
+ $push_command = 'TODO: ...';
+ // csprintf(
+ // '$ git push -- %R %R:%R',
+ // $this->getOntoRemote(),
+ // $this->mergedRef,
+ // $this->getOnto());
}
- $restore_command = csprintf(
- '$ git checkout %R --',
- $this->localRef);
+ $restore_command = 'TODO: ...';
echo tsprintf(
"\n%s\n\n".
diff --git a/src/land/engine/ArcanistLandEngine.php b/src/land/engine/ArcanistLandEngine.php
--- a/src/land/engine/ArcanistLandEngine.php
+++ b/src/land/engine/ArcanistLandEngine.php
@@ -1266,8 +1266,8 @@
}
if ($is_hold) {
- $this->didHoldChanges();
- $this->discardLocalState();
+ $this->didHoldChanges($local_state);
+ $local_state->discardLocalState();
} else {
$this->reconcileLocalState($into_commit, $local_state);
}
@@ -1275,6 +1275,7 @@
// TODO: Restore this.
// $this->getWorkflow()->askForRepositoryUpdate();
+ // TODO: This is misleading under "--hold".
$log->writeSuccess(
pht('DONE'),
pht('Landed changes.'));
@@ -1287,7 +1288,6 @@
}
}
-
protected function validateArguments() {
$log = $this->getLogEngine();
diff --git a/src/repository/api/ArcanistGitAPI.php b/src/repository/api/ArcanistGitAPI.php
--- a/src/repository/api/ArcanistGitAPI.php
+++ b/src/repository/api/ArcanistGitAPI.php
@@ -1753,4 +1753,22 @@
->setRepositoryAPI($this);
}
+ public function readRawCommit($hash) {
+ list($stdout) = $this->execxLocal(
+ 'cat-file commit -- %s',
+ $hash);
+
+ return ArcanistGitRawCommit::newFromRawBlob($stdout);
+ }
+
+ public function writeRawCommit(ArcanistGitRawCommit $commit) {
+ $blob = $commit->getRawBlob();
+
+ $future = $this->execFutureLocal('hash-object -t commit --stdin -w');
+ $future->write($blob);
+ list($stdout) = $future->resolvex();
+
+ return trim($stdout);
+ }
+
}
diff --git a/src/repository/raw/ArcanistGitRawCommit.php b/src/repository/raw/ArcanistGitRawCommit.php
new file mode 100644
--- /dev/null
+++ b/src/repository/raw/ArcanistGitRawCommit.php
@@ -0,0 +1,183 @@
+<?php
+
+final class ArcanistGitRawCommit
+ extends Phobject {
+
+ private $treeHash;
+ private $parents = array();
+ private $rawAuthor;
+ private $rawCommitter;
+ private $message;
+
+ const GIT_EMPTY_TREE_HASH = '4b825dc642cb6eb9a060e54bf8d69288fbee4904';
+
+ public static function newEmptyCommit() {
+ $raw = new self();
+ $raw->setTreeHash(self::GIT_EMPTY_TREE_HASH);
+ return $raw;
+ }
+
+ public static function newFromRawBlob($blob) {
+ $lines = phutil_split_lines($blob);
+
+ $seen = array();
+ $raw = new self();
+
+ $pattern = '(^(\w+) ([^\n]+)\n?\z)';
+ foreach ($lines as $key => $line) {
+ unset($lines[$key]);
+
+ $is_divider = ($line === "\n");
+ if ($is_divider) {
+ break;
+ }
+
+ $matches = null;
+ $ok = preg_match($pattern, $line, $matches);
+ if (!$ok) {
+ throw new Exception(
+ pht(
+ 'Expected to match pattern "%s" against line "%s" in raw commit '.
+ 'blob: %s',
+ $pattern,
+ $line,
+ $blob));
+ }
+
+ $label = $matches[1];
+ $value = $matches[2];
+
+ // Detect unexpected repeated lines.
+
+ if (isset($seen[$label])) {
+ switch ($label) {
+ case 'parent':
+ break;
+ default:
+ throw new Exception(
+ pht(
+ 'Encountered two "%s" lines ("%s", "%s") while parsing raw '.
+ 'commit blob, expected at most one: %s',
+ $label,
+ $seen[$label],
+ $line,
+ $blob));
+ }
+ } else {
+ $seen[$label] = $line;
+ }
+
+ switch ($label) {
+ case 'tree':
+ $raw->setTreeHash($value);
+ break;
+ case 'parent':
+ $raw->addParent($value);
+ break;
+ case 'author':
+ $raw->setRawAuthor($value);
+ break;
+ case 'committer':
+ $raw->setRawCommitter($value);
+ break;
+ default:
+ throw new Exception(
+ pht(
+ 'Unknown attribute label "%s" in line "%s" while parsing raw '.
+ 'commit blob: %s',
+ $label,
+ $line,
+ $blob));
+ }
+ }
+
+ $message = implode('', $lines);
+ $raw->setMessage($message);
+
+ return $raw;
+ }
+
+ public function getRawBlob() {
+ $out = array();
+
+ $tree = $this->getTreeHash();
+ if ($tree !== null) {
+ $out[] = sprintf("tree %s\n", $tree);
+ }
+
+ $parents = $this->getParents();
+ foreach ($parents as $parent) {
+ $out[] = sprintf("parent %s\n", $parent);
+ }
+
+ $raw_author = $this->getRawAuthor();
+ if ($raw_author !== null) {
+ $out[] = sprintf("author %s\n", $raw_author);
+ }
+
+ $raw_committer = $this->getRawCommitter();
+ if ($raw_committer !== null) {
+ $out[] = sprintf("committer %s\n", $raw_committer);
+ }
+
+ $out[] = "\n";
+
+ $message = $this->getMessage();
+ if ($message !== null) {
+ $out[] = $message;
+ }
+
+ return implode('', $out);
+ }
+
+ public function setTreeHash($tree_hash) {
+ $this->treeHash = $tree_hash;
+ return $this;
+ }
+
+ public function getTreeHash() {
+ return $this->treeHash;
+ }
+
+ public function setRawAuthor($raw_author) {
+ $this->rawAuthor = $raw_author;
+ return $this;
+ }
+
+ public function getRawAuthor() {
+ return $this->rawAuthor;
+ }
+
+ public function setRawCommitter($raw_committer) {
+ $this->rawCommitter = $raw_committer;
+ return $this;
+ }
+
+ public function getRawCommitter() {
+ return $this->rawCommitter;
+ }
+
+ public function setParents(array $parents) {
+ $this->parents = $parents;
+ return $this;
+ }
+
+ public function getParents() {
+ return $this->parents;
+ }
+
+ public function addParent($hash) {
+ $this->parents[] = $hash;
+ return $this;
+ }
+
+ public function setMessage($message) {
+ $this->message = $message;
+ return $this;
+ }
+
+ public function getMessage() {
+ return $this->message;
+ }
+
+}
diff --git a/src/repository/raw/__tests__/ArcanistGitRawCommitTestCase.php b/src/repository/raw/__tests__/ArcanistGitRawCommitTestCase.php
new file mode 100644
--- /dev/null
+++ b/src/repository/raw/__tests__/ArcanistGitRawCommitTestCase.php
@@ -0,0 +1,91 @@
+<?php
+
+final class ArcanistGitRawCommitTestCase
+ extends PhutilTestCase {
+
+ public function testGitRawCommitParser() {
+ $cases = array(
+ array(
+ 'name' => 'empty',
+ 'blob' => array(
+ 'tree fcfd0454eac6a28c729aa6bf7d38a5f1efc5cc5d',
+ '',
+ '',
+ ),
+ 'tree' => 'fcfd0454eac6a28c729aa6bf7d38a5f1efc5cc5d',
+ ),
+ array(
+ 'name' => 'parents',
+ 'blob' => array(
+ 'tree 63ece8fd5a8283f1da2c14735d059669a09ba628',
+ 'parent 4aebaaf60895c3f3dd32a8cadff00db2c8f74899',
+ 'parent 0da1a2e17d921dc27ce9afa76b123cb4c8b73b17',
+ 'author alice',
+ 'committer alice',
+ '',
+ 'Quack quack quack.',
+ '',
+ ),
+ 'tree' => '63ece8fd5a8283f1da2c14735d059669a09ba628',
+ 'parents' => array(
+ '4aebaaf60895c3f3dd32a8cadff00db2c8f74899',
+ '0da1a2e17d921dc27ce9afa76b123cb4c8b73b17',
+ ),
+ 'author' => 'alice',
+ 'committer' => 'alice',
+ 'message' => "Quack quack quack.\n",
+ ),
+ );
+
+ foreach ($cases as $case) {
+ $name = $case['name'];
+ $blob = $case['blob'];
+
+ if (is_array($blob)) {
+ $blob = implode("\n", $blob);
+ }
+
+ $raw = ArcanistGitRawCommit::newFromRawBlob($blob);
+ $out = $raw->getRawBlob();
+
+ $this->assertEqual(
+ $blob,
+ $out,
+ pht(
+ 'Expected read + write to produce the original raw Git commit '.
+ 'blob in case "%s".',
+ $name));
+
+ $tree = idx($case, 'tree');
+ $this->assertEqual(
+ $tree,
+ $raw->getTreeHash(),
+ pht('Tree hashes in case "%s".', $name));
+
+ $parents = idx($case, 'parents', array());
+ $this->assertEqual(
+ $parents,
+ $raw->getParents(),
+ pht('Parents in case "%s".', $name));
+
+ $author = idx($case, 'author');
+ $this->assertEqual(
+ $author,
+ $raw->getRawAuthor(),
+ pht('Authors in case "%s".', $name));
+
+ $committer = idx($case, 'committer');
+ $this->assertEqual(
+ $committer,
+ $raw->getRawCommitter(),
+ pht('Committer in case "%s".', $name));
+
+ $message = idx($case, 'message', '');
+ $this->assertEqual(
+ $message,
+ $raw->getMessage(),
+ pht('Message in case "%s".', $name));
+ }
+ }
+
+}
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Tue, Mar 11, 4:08 PM (1 w, 5 d ago)
Storage Engine
blob
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
7535655
Default Alt Text
D21324.id50787.diff (14 KB)
Attached To
Mode
D21324: When landing changes in an empty repository, merge cleanly in Git
Attached
Detach File
Event Timeline
Log In to Comment