Index: .arcconfig =================================================================== --- .arcconfig +++ .arcconfig @@ -1,5 +1,5 @@ { "phabricator.uri" : "https://code.wildfiregames.com/", "repository.callsign" : "P", - "load": ["build/arclint/pyrolint"] + "load": ["build/arclint/pyrolint", "build/arcpatch/ci-patch"] } Index: build/arcpatch/ci-patch/__phutil_library_init__.php =================================================================== --- /dev/null +++ build/arcpatch/ci-patch/__phutil_library_init__.php @@ -0,0 +1,3 @@ + 2, + 'class' => array( + 'CIPatchWorkflow' => 'src/CIPatchWorkflow.php', + ), + 'function' => array(), + 'xmap' => array( + 'CIPatchWorkflow' => 'ArcanistWorkflow', + ), +)); Index: build/arcpatch/ci-patch/src/CIPatchWorkflow.php =================================================================== --- /dev/null +++ build/arcpatch/ci-patch/src/CIPatchWorkflow.php @@ -0,0 +1,372 @@ + array( + 'param' => 'revision_id', + 'paramtype' => 'complete', + 'help' => pht( + "Apply changes from a Differential revision, using the most recent ". + "diff that has been attached to it. You can run '%s' as a shorthand.", + 'arc patch D12345'), + ), + 'diff' => array( + 'param' => 'diff_id', + 'help' => pht( + 'Apply changes from a specific Differential diff. The diff must be '. + 'attached to a revision.'), + ), + 'allow-abandoned' => array( + 'help' => pht( + 'Allow specifying an abandoned revision (or diff from an abandoned '. + 'revision).'), + ), + 'allow-abandoned-dependencies' => array( + 'help' => pht('Apply any abandoned dependencies found.'), + ), + 'allow-untracked' => array( + 'help' => pht('Skip checks for untracked files in the working copy.'), + ), + '*' => 'name', + ); + } + + // Certain error message texts depend on whether it's a revision + // or a diff being processed. + private function getErrorMessage($source, $reason) { + return array( + ArcanistPatchWorkflow::SOURCE_REVISION => array( + 'is_closed' => pht( + "This revision has been closed, and is likely already merged ". + "into the source tree."), + 'is_abandoned_fatal' => pht( + "This revision has been abandoned. If you really want to apply it, ". + "pass '%s'.", + '--allow-abandoned'), + 'is_abandoned_continuing' => pht( + "This revision has been abandoned."), + ), + ArcanistPatchWorkflow::SOURCE_DIFF => array( + 'is_closed' => pht( + "The revision this diff is attached to is closed. Any changes ". + "contained within are likely already merged into the source tree."), + 'is_abandoned_fatal' => pht( + "The revision this diff is attached to has been abandoned. If you ". + "really want to apply this diff, pass '%s'.", + '--allow-abandoned'), + 'is_abandoned_continuing' => pht( + "The revision this diff is attached to has been abandoned."), + ), + )[$source][$reason]; + } + + public function requiresAuthentication() { + return true; + } + + public function requiresConduit() { + return true; + } + + public function requiresRepositoryAPI() { + return true; + } + + public function requiresWorkingCopy() { + return true; + } + + public function run() { + $this->requireCleanWorkingCopy(); + + $source = $this->source; + $param = $this->sourceParam; + $argument = ""; + $revisionID = null; + switch ($source) { + case ArcanistPatchWorkflow::SOURCE_REVISION: + $bundle = $this->loadRevisionBundleFromConduit( + $this->getConduit(), + $param); + $argument = '--revision=' . $param; + $revisionID = $param; + break; + case ArcanistPatchWorkflow::SOURCE_DIFF: + $bundle = $this->loadDiffBundleFromConduit( + $this->getConduit(), + $param); + $argument = '--diff=' . $param; + $revisionID = $bundle->getRevisionID(); + if (!$revisionID) { + $this->throwError( + pht('This diff is not attached to a revision!')); + } + break; + } + + // Check that the revision (or the diff that the revision is attached to) is still active. + $result = $this->getConduit()->callMethodSynchronous( + 'differential.query', + array( + 'ids' => array($revisionID), + )); + if ($result[0]['status'] == ArcanistDifferentialRevisionStatus::CLOSED) { + $this->throwError($this->getErrorMessage($source, 'is_closed')); + } else if ($result[0]['status'] == ArcanistDifferentialRevisionStatus::ABANDONED) { + if ($this->getArgument('allow-abandoned')) { + $this->printWarning($this->getErrorMessage($source, 'is_abandoned_continuing')); + } else { + $this->throwError($this->getErrorMessage($source, 'is_abandoned_fatal')); + } + } + + $this->updateWorkingCopy(); + $this->applyDependencies($bundle); + + $this->printInfo(pht('Applying %s %s.', $source, $param)); + $childWorkflow = $this->buildChildWorkflow( + 'patch', + array_merge( + self::PATCH_ARGUMENTS, + array($argument) + )); + $errorCode = $childWorkflow->run(); + + // We expect the child workflow to display a suitable message if something + // has gone wrong. + if (!$errorCode) { + echo phutil_console_format( + "** %s **\n", + pht('DONE')); + } + return $errorCode; + } + + private function applyDependencies(ArcanistBundle $bundle) { + $this->printInfo(pht('Detecting dependencies...'), false); + $graph = $this->buildDependencyGraph($bundle); + if (!$graph) { + echo pht(' none found.')."\n"; + return; + } + echo "\n"; + + $revisionID = $bundle->getRevisionID(); + $start_phid = $graph->getStartPHID(); + $cycle_phids = $graph->detectCycles($start_phid); + if ($cycle_phids) { + $phids = array_keys($graph->getNodes()); + $this->printWarning( + pht('The dependencies for this patch are cyclic. Applying '. + 'them might not be successful.')); + } else { + $phids = $graph->getNodesInTopologicalOrder(); + $phids = array_reverse($phids); + } + + $dep_on_revs = $this->getConduit()->callMethodSynchronous( + 'differential.query', + array( + 'phids' => $phids, + )); + $revs = array(); + foreach ($dep_on_revs as $dep_on_rev) { + // Skip the revision we've requested the dependencies of + if ($dep_on_rev['id'] == $revisionID) + continue; + + // Conditionally skip dependencies that are abandoned + if ($dep_on_rev['status'] == ArcanistDifferentialRevisionStatus::ABANDONED) { + $this->printWarning( + pht('Dependency %s has been abandoned.', + 'D'.$dep_on_rev['id'])); + if (!$this->getArgument('allow-abandoned')) { + continue; + } + } + + // Skip dependencies that are closed + if ($dep_on_rev['status'] == ArcanistDifferentialRevisionStatus::CLOSED) { + continue; + } + + $revs[$dep_on_rev['phid']] = 'D'.$dep_on_rev['id']; + } + + if (!count($revs)) { + $this->printInfo(pht('No active dependencies found.')); + return; + } + $this->printInfo(pht('Found %s active dependencies.', count($revs))); + + // Retain the topological sort order from earlier + $revs = array_select_keys($revs, $phids); + + foreach ($revs as $phid => $id) { + $this->printInfo(pht('Applying dependancy: %s.', $id)); + $childWorkflow = $this->buildChildWorkflow( + 'patch', + array_merge( + self::PATCH_ARGUMENTS, + array('--revision=' . $id) + )); + $errorCode = $childWorkflow->run(); + + // We expect the child workflow to display a suitable message if something + // has gone wrong. + if ($errorCode) { + exit($errorCode); + } + } + } + + private function printInfo($msg, $newline=true) { + $format = "** %s ** %s"; + if ($newline) + $format .= "\n"; + echo phutil_console_format( + $format, + pht('INFO'), + $msg); + } + + private function printWarning($msg) { + echo phutil_console_format( + "** %s ** %s\n", + pht('WARNING'), + $msg); + } + + private function throwError($msg) { + echo phutil_console_format( + "** %s ** %s\n", + pht('ERROR'), + $msg); + exit(1); + } + + private function updateWorkingCopy() { + $this->printInfo(pht('Updating working copy...')); + $this->getRepositoryAPI()->updateWorkingCopy(); + } + + protected function didParseArguments() { + $arguments = array( + 'revision' => ArcanistPatchWorkflow::SOURCE_REVISION, + 'diff' => ArcanistPatchWorkflow::SOURCE_DIFF, + 'name' => ArcanistPatchWorkflow::SOURCE_REVISION, + ); + + $sources = array(); + foreach ($arguments as $key => $source_type) { + $value = $this->getArgument($key); + if (!$value) { + continue; + } + + switch ($key) { + case 'revision': + $value = $this->normalizeRevisionID($value); + break; + case 'name': + if (count($value) > 1) { + throw new ArcanistUsageException( + pht('Specify at most one revision name.')); + } + $value = $this->normalizeRevisionID(head($value)); + break; + } + + $sources[] = array( + $source_type, + $value, + ); + } + + if (!$sources) { + throw new ArcanistUsageException( + pht( + 'You must specify changes to apply to the working copy with '. + '"D12345", "--revision", or "--diff".')); + } + + if (count($sources) > 1) { + throw new ArcanistUsageException( + pht( + 'Options "D12345", "--revision", and "--diff" are mutually '. + 'exclusive. Choose exactly one patch source.')); + } + + $source = head($sources); + + $this->source = $source[0]; + $this->sourceParam = $source[1]; + } + + private function buildDependencyGraph(ArcanistBundle $bundle) { + $graph = null; + $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; + } +} Index: build/jenkins/pipelines/docker-custom.Jenkinsfile =================================================================== --- build/jenkins/pipelines/docker-custom.Jenkinsfile +++ build/jenkins/pipelines/docker-custom.Jenkinsfile @@ -40,7 +40,7 @@ } steps { ws("/zpool0/custom") { - sh "arc patch --diff ${params.DIFF_ID} --force" + sh "arc ci-patch --diff ${params.DIFF_ID}" } } } Index: build/jenkins/pipelines/docker-differential.Jenkinsfile =================================================================== --- build/jenkins/pipelines/docker-differential.Jenkinsfile +++ build/jenkins/pipelines/docker-differential.Jenkinsfile @@ -27,7 +27,7 @@ stage("Patch: ${compiler}") { try { ws("/zpool0/${compiler}") { - sh "arc patch --diff ${params.DIFF_ID} --force" + sh "arc ci-patch --diff ${params.DIFF_ID}" } } catch(e) { sh "sudo zfs rollback zpool0/${compiler}@latest" @@ -121,7 +121,7 @@ stages { stage("Patch") { steps { - sh "arc patch --diff ${params.DIFF_ID} --force" + sh "arc ci-patch --diff ${params.DIFF_ID}" script { parallel patchesMap } } } Index: build/jenkins/pipelines/macos-differential.Jenkinsfile =================================================================== --- build/jenkins/pipelines/macos-differential.Jenkinsfile +++ build/jenkins/pipelines/macos-differential.Jenkinsfile @@ -49,11 +49,11 @@ steps { script { try { - sh "arc patch --diff ${params.DIFF_ID} --force" + sh "arc ci-patch --diff ${params.DIFF_ID}" } catch (e) { sh "svn revert -R ." sh "svn st | cut -c 9- | xargs rm -rf" - sh "arc patch --diff ${params.DIFF_ID} --force" + sh "arc ci-patch --diff ${params.DIFF_ID}" } } } Index: build/jenkins/pipelines/vs2015-differential.Jenkinsfile =================================================================== --- build/jenkins/pipelines/vs2015-differential.Jenkinsfile +++ build/jenkins/pipelines/vs2015-differential.Jenkinsfile @@ -51,11 +51,11 @@ steps { script { try { - bat "arc patch --diff ${params.DIFF_ID} --force" + bat "arc ci-patch --diff ${params.DIFF_ID}" } catch (e) { bat 'svn revert -R .' bat 'powershell.exe "svn st --no-ignore | %% {$_.substring(8)} | del -r" ' - bat "arc patch --diff ${params.DIFF_ID} --force" + bat "arc ci-patch --diff ${params.DIFF_ID}" } } }