1
0
Fork 0
mirror of https://we.phorge.it/source/arcanist.git synced 2024-11-25 16:22:42 +01:00

Arcanist - make Patch workflow automagically apply dependencies

Summary: Nice title. Ref T479.

Test Plan:
Actually, help on that? I want to make sure I properly build up the "depends" on data. Is it as simple as

  -- observe at some commit hash RAZZMATAZZ
  -- git checkout -B "foo"
  -- <work>
  -- git commit -m "stash"
  -- arc diff -> yields DX
  -- git checkout -B "foo_prime"
  -- <work>
  -- git commit -m "stash"
  -- arc diff -> yield DY
  -- git checkout RAZZMATAZZ
  -- arc patch DY
  -- get prompted in workflow, agree
  -- git log and observe DX and DY applied

Reviewers: epriestley

Reviewed By: epriestley

CC: Korvin, aran, davidressman

Maniphest Tasks: T479

Differential Revision: https://secure.phabricator.com/D6790
This commit is contained in:
Bob Trahan 2013-10-17 14:59:04 -07:00
parent 79be59863e
commit b2021586d4
3 changed files with 204 additions and 49 deletions

View file

@ -57,6 +57,7 @@ phutil_register_library_map(array(
'ArcanistDiffWorkflow' => 'workflow/ArcanistDiffWorkflow.php', 'ArcanistDiffWorkflow' => 'workflow/ArcanistDiffWorkflow.php',
'ArcanistDifferentialCommitMessage' => 'differential/ArcanistDifferentialCommitMessage.php', 'ArcanistDifferentialCommitMessage' => 'differential/ArcanistDifferentialCommitMessage.php',
'ArcanistDifferentialCommitMessageParserException' => 'differential/ArcanistDifferentialCommitMessageParserException.php', 'ArcanistDifferentialCommitMessageParserException' => 'differential/ArcanistDifferentialCommitMessageParserException.php',
'ArcanistDifferentialDependencyGraph' => 'differential/ArcanistDifferentialDependencyGraph.php',
'ArcanistDifferentialRevisionHash' => 'differential/constants/ArcanistDifferentialRevisionHash.php', 'ArcanistDifferentialRevisionHash' => 'differential/constants/ArcanistDifferentialRevisionHash.php',
'ArcanistDifferentialRevisionStatus' => 'differential/constants/ArcanistDifferentialRevisionStatus.php', 'ArcanistDifferentialRevisionStatus' => 'differential/constants/ArcanistDifferentialRevisionStatus.php',
'ArcanistDownloadWorkflow' => 'workflow/ArcanistDownloadWorkflow.php', 'ArcanistDownloadWorkflow' => 'workflow/ArcanistDownloadWorkflow.php',
@ -222,6 +223,7 @@ phutil_register_library_map(array(
'ArcanistDiffUtilsTestCase' => 'ArcanistTestCase', 'ArcanistDiffUtilsTestCase' => 'ArcanistTestCase',
'ArcanistDiffWorkflow' => 'ArcanistBaseWorkflow', 'ArcanistDiffWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistDifferentialCommitMessageParserException' => 'Exception', 'ArcanistDifferentialCommitMessageParserException' => 'Exception',
'ArcanistDifferentialDependencyGraph' => 'AbstractDirectedGraph',
'ArcanistDownloadWorkflow' => 'ArcanistBaseWorkflow', 'ArcanistDownloadWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistEventType' => 'PhutilEventType', 'ArcanistEventType' => 'PhutilEventType',
'ArcanistExportWorkflow' => 'ArcanistBaseWorkflow', 'ArcanistExportWorkflow' => 'ArcanistBaseWorkflow',

View file

@ -0,0 +1,72 @@
<?php
final class ArcanistDifferentialDependencyGraph extends AbstractDirectedGraph {
private $repositoryAPI;
private $conduit;
private $startPHID;
public function setStartPHID($start_phid) {
$this->startPHID = $start_phid;
return $this;
}
public function getStartPHID() {
return $this->startPHID;
}
public function setRepositoryAPI(ArcanistRepositoryAPI $repository_api) {
$this->repositoryAPI = $repository_api;
return $this;
}
public function getRepositoryAPI() {
return $this->repositoryAPI;
}
public function setConduit(ConduitClient $conduit) {
$this->conduit = $conduit;
return $this;
}
public function getConduit() {
return $this->conduit;
}
protected function loadEdges(array $nodes) {
$repository_api = $this->getRepositoryAPI();
$dependencies = $this->getConduit()->callMethodSynchronous(
'differential.query',
array(
'phids' => $nodes,
));
$edges = array();
foreach ($dependencies as $dependency) {
$dependency_revision = $this->getCommitHashFromDict($dependency);
if ($repository_api->hasLocalCommit($dependency_revision)) {
$edges[$dependency['phid']] = array();
continue;
}
$auxillary = idx($dependency, 'auxiliary', array());
$edges[$dependency['phid']] = idx(
$auxillary,
'phabricator:depends-on',
array());
}
return $edges;
}
private function getCommitHashFromDict($dict) {
$api = $this->getRepositoryAPI();
$hashes = idx($dict, 'hashes', array());
if ($api instanceof ArcanistGitAPI) {
$key = ArcanistDifferentialRevisionHash::HASH_GIT_COMMIT;
} else if ($api instanceof ArcanistMercurialAPI) {
$key = ArcanistDifferentialRevisionHash::HASH_MERCURIAL_COMMIT;
} else {
$key = null;
}
return idx($hashes, $key);
}
}

View file

@ -93,6 +93,15 @@ EOTEXT
"Normally under git/hg, if the patch is successful, the changes ". "Normally under git/hg, if the patch is successful, the changes ".
"are committed to the working copy. This flag prevents the commit.", "are committed to the working copy. This flag prevents the commit.",
), ),
'skip-dependencies' => array(
'supports' => array(
'git', 'hg'
),
'help' =>
'Normally, if a patch has dependencies that are not present in the '.
'working copy, arc tries to apply them as well. This flag prevents '.
'such work.',
),
'nobranch' => array( 'nobranch' => array(
'supports' => array( 'supports' => array(
'git', 'hg' 'git', 'hg'
@ -186,12 +195,7 @@ EOTEXT
} }
private function shouldCommit() { private function shouldCommit() {
$no_commit = $this->getArgument('nocommit', false); return !$this->getArgument('nocommit', false);
if ($no_commit) {
return false;
}
return true;
} }
private function canBranch() { private function canBranch() {
@ -285,29 +289,6 @@ EOTEXT
return $bookmark_name; return $bookmark_name;
} }
private function hasBaseRevision(ArcanistBundle $bundle) {
$base_revision = $bundle->getBaseRevision();
$repository_api = $this->getRepositoryAPI();
// verify the base revision is valid
if ($repository_api instanceof ArcanistGitAPI) {
// in a working copy that uses the git-svn bridge, the base revision might
// be a svn uri instead of a git ref
// NOTE: Use 'cat-file', not 'rev-parse --verify', because 'rev-parse'
// always "verifies" any properly-formatted commit even if it does not
// exist.
list($err) = $repository_api->execManualLocal(
'cat-file -t %s',
$base_revision);
return !$err;
} else if ($repository_api instanceof ArcanistMercurialAPI) {
return $repository_api->hasLocalCommit($base_revision);
}
return false;
}
private function createBranch(ArcanistBundle $bundle, $has_base_revision) { private function createBranch(ArcanistBundle $bundle, $has_base_revision) {
$repository_api = $this->getRepositoryAPI(); $repository_api = $this->getRepositoryAPI();
if ($repository_api instanceof ArcanistGitAPI) { if ($repository_api instanceof ArcanistGitAPI) {
@ -354,6 +335,10 @@ EOTEXT
return $branch_name; return $branch_name;
} }
private function shouldApplyDependencies() {
return !$this->getArgument('skip-dependencies', false);
}
private function shouldUpdateWorkingCopy() { private function shouldUpdateWorkingCopy() {
return $this->getArgument('update', false); return $this->getArgument('update', false);
} }
@ -423,24 +408,24 @@ EOTEXT
$bundle->setEncoding($try_encoding); $bundle->setEncoding($try_encoding);
} }
$force = $this->getArgument('force', false); $sanity_check = !$this->getArgument('force', false);
if ($force) {
// force means don't do any sanity checks about the patch
} else {
$this->sanityCheck($bundle);
}
// we should update the working copy before we do ANYTHING else // we should update the working copy before we do ANYTHING else to
// the working copy
if ($this->shouldUpdateWorkingCopy()) { if ($this->shouldUpdateWorkingCopy()) {
$this->updateWorkingCopy(); $this->updateWorkingCopy();
} }
$repository_api = $this->getRepositoryAPI(); if ($sanity_check) {
$this->requireCleanWorkingCopy();
}
$has_base_revision = $this->hasBaseRevision($bundle); $repository_api = $this->getRepositoryAPI();
if ($this->shouldCommit() && $has_base_revision = $repository_api->hasLocalCommit(
$this->canBranch() && $bundle->getBaseRevision());
($this->shouldBranch() || $has_base_revision)) { if ($this->canBranch() &&
($this->shouldBranch() ||
($this->shouldCommit() && $has_base_revision))) {
if ($repository_api instanceof ArcanistGitAPI) { if ($repository_api instanceof ArcanistGitAPI) {
$original_branch = $repository_api->getBranchName(); $original_branch = $repository_api->getBranchName();
@ -460,6 +445,13 @@ EOTEXT
$new_branch = $this->createBranch($bundle, $has_base_revision); $new_branch = $this->createBranch($bundle, $has_base_revision);
} }
if (!$has_base_revision && $this->shouldApplyDependencies()) {
$this->applyDependencies($bundle);
}
if ($sanity_check) {
$this->sanityCheck($bundle);
}
if ($repository_api instanceof ArcanistSubversionAPI) { if ($repository_api instanceof ArcanistSubversionAPI) {
$patch_err = 0; $patch_err = 0;
@ -716,13 +708,16 @@ EOTEXT
$verb = 'applied'; $verb = 'applied';
} }
if ($this->shouldCommit() && $this->canBranch() && if ($this->canBranch() &&
!$this->shouldBranch() && $has_base_revision) { !$this->shouldBranch() &&
$this->shouldCommit() && $has_base_revision) {
$repository_api->execxLocal('checkout %s', $original_branch); $repository_api->execxLocal('checkout %s', $original_branch);
$ex = null; $ex = null;
try { try {
$repository_api->execxLocal('cherry-pick %s', $new_branch); $repository_api->execxLocal('cherry-pick %s', $new_branch);
} catch (Exception $ex) {} } catch (Exception $ex) {
// do nothing
}
$repository_api->execxLocal('branch -D %s', $new_branch); $repository_api->execxLocal('branch -D %s', $new_branch);
if ($ex) { if ($ex) {
echo phutil_console_format( echo phutil_console_format(
@ -858,15 +853,72 @@ EOTEXT
return array('ARGUMENT'); return array('ARGUMENT');
} }
private function applyDependencies(ArcanistBundle $bundle) {
// check for (and automagically apply on the user's be-hest) any revisions
// this patch depends on
$graph = $this->buildDependencyGraph($bundle);
if ($graph) {
$start_phid = $graph->getStartPHID();
$cycle_phids = $graph->detectCycles($start_phid);
if ($cycle_phids) {
$phids = array_keys($graph->getNodes());
$issue = 'The dependencies for this patch have a cycle. Applying them '.
'is not guaranteed to work. Continue anyway?';
$okay = phutil_console_confirm($issue, true);
} else {
$phids = $graph->getTopographicallySortedNodes();
$phids = array_reverse($phids);
$okay = true;
}
if (!$okay) {
return;
}
$dep_on_revs = $this->getConduit()->callMethodSynchronous(
'differential.query',
array(
'phids' => $phids,
'arcanistProjects' => array($bundle->getProjectID())
));
$revs = array();
foreach ($dep_on_revs as $dep_on_rev) {
$revs[$dep_on_rev['phid']] = 'D'.$dep_on_rev['id'];
}
// order them in case we got a topological sort earlier
$revs = array_select_keys($revs, $phids);
if (!empty($revs)) {
$base_args = array(
'--force',
'--skip-dependencies',
'--nobranch');
if (!$this->shouldCommit()) {
$base_args[] = '--nocommit';
}
foreach ($revs as $phid => $diff_id) {
// we'll apply this, the actual patch, later
// this should be the last in the list
if ($phid == $start_phid) {
continue;
}
$args = $base_args;
$args[] = $diff_id;
$apply_workflow = $this->buildChildWorkflow(
'patch',
$args);
$apply_workflow->run();
}
}
}
}
/** /**
* Do the best we can to prevent PEBKAC and id10t issues. * Do the best we can to prevent PEBKAC and id10t issues.
*/ */
private function sanityCheck(ArcanistBundle $bundle) { private function sanityCheck(ArcanistBundle $bundle) {
$repository_api = $this->getRepositoryAPI(); $repository_api = $this->getRepositoryAPI();
// Require clean working copy
$this->requireCleanWorkingCopy();
// Check to see if the bundle's project id matches the working copy // Check to see if the bundle's project id matches the working copy
// project id // project id
$bundle_project_id = $bundle->getProjectID(); $bundle_project_id = $bundle->getProjectID();
@ -953,8 +1005,6 @@ EOTEXT
} }
} }
} }
// TODO -- more sanity checks here
} }
/** /**
@ -1007,4 +1057,35 @@ EOTEXT
} }
return $found_revision; return $found_revision;
} }
private function buildDependencyGraph(ArcanistBundle $bundle) {
$graph = null;
if ($this->getRepositoryAPI() instanceof ArcanistSubversionAPI) {
return $graph;
}
$revision_id = $bundle->getRevisionID();
if ($revision_id) {
$revisions = $this->getConduit()->callMethodSynchronous(
'differential.query',
array(
'ids' => array($revision_id),
));
if ($revisions) {
$revision = head($revisions);
$rev_auxiliary = idx($revision, 'auxiliary', array());
$phids = idx($rev_auxiliary, 'phabricator:depends-on', array());
if ($phids) {
$revision_phid = $revision['phid'];
$graph = id(new ArcanistDifferentialDependencyGraph())
->setConduit($this->getConduit())
->setRepositoryAPI($this->getRepositoryAPI())
->setStartPHID($revision_phid)
->addNodes(array($revision_phid => $phids))
->loadGraph();
}
}
}
return $graph;
}
} }