Page Menu
Home
Sealhub
Search
Configure Global Search
Log In
Files
F9583590
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
62 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
index 1250ef8d..7c63e511 100644
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -1,241 +1,240 @@
<?php
/**
* This file is automatically generated. Use 'arc liberate' to rebuild it.
* @generated
* @phutil-library-version 2
*/
phutil_register_library_map(array(
'__library_version__' => 2,
'class' =>
array(
'ArcanistAliasWorkflow' => 'workflow/ArcanistAliasWorkflow.php',
'ArcanistAmendWorkflow' => 'workflow/ArcanistAmendWorkflow.php',
'ArcanistApacheLicenseLinter' => 'lint/linter/ArcanistApacheLicenseLinter.php',
'ArcanistApacheLicenseLinterTestCase' => 'lint/linter/__tests__/ArcanistApacheLicenseLinterTestCase.php',
'ArcanistBaseCommitParser' => 'parser/ArcanistBaseCommitParser.php',
'ArcanistBaseCommitParserTestCase' => 'parser/__tests__/ArcanistBaseCommitParserTestCase.php',
'ArcanistBaseUnitTestEngine' => 'unit/engine/ArcanistBaseUnitTestEngine.php',
'ArcanistBaseWorkflow' => 'workflow/ArcanistBaseWorkflow.php',
'ArcanistBranchWorkflow' => 'workflow/ArcanistBranchWorkflow.php',
'ArcanistBundle' => 'parser/ArcanistBundle.php',
'ArcanistBundleTestCase' => 'parser/__tests__/ArcanistBundleTestCase.php',
'ArcanistCallConduitWorkflow' => 'workflow/ArcanistCallConduitWorkflow.php',
'ArcanistCapabilityNotSupportedException' => 'workflow/exception/ArcanistCapabilityNotSupportedException.php',
'ArcanistChooseInvalidRevisionException' => 'exception/ArcanistChooseInvalidRevisionException.php',
'ArcanistChooseNoRevisionsException' => 'exception/ArcanistChooseNoRevisionsException.php',
'ArcanistCloseRevisionWorkflow' => 'workflow/ArcanistCloseRevisionWorkflow.php',
'ArcanistCloseWorkflow' => 'workflow/ArcanistCloseWorkflow.php',
'ArcanistCommentRemover' => 'parser/ArcanistCommentRemover.php',
'ArcanistCommentRemoverTestCase' => 'parser/__tests__/ArcanistCommentRemoverTestCase.php',
'ArcanistCommitWorkflow' => 'workflow/ArcanistCommitWorkflow.php',
'ArcanistConduitLinter' => 'lint/linter/ArcanistConduitLinter.php',
'ArcanistConfiguration' => 'configuration/ArcanistConfiguration.php',
'ArcanistCoverWorkflow' => 'workflow/ArcanistCoverWorkflow.php',
'ArcanistDiffChange' => 'parser/diff/ArcanistDiffChange.php',
'ArcanistDiffChangeType' => 'parser/diff/ArcanistDiffChangeType.php',
'ArcanistDiffHunk' => 'parser/diff/ArcanistDiffHunk.php',
'ArcanistDiffParser' => 'parser/ArcanistDiffParser.php',
'ArcanistDiffParserTestCase' => 'parser/__tests__/ArcanistDiffParserTestCase.php',
'ArcanistDiffUtils' => 'difference/ArcanistDiffUtils.php',
'ArcanistDiffUtilsTestCase' => 'difference/__tests__/ArcanistDiffUtilsTestCase.php',
'ArcanistDiffWorkflow' => 'workflow/ArcanistDiffWorkflow.php',
'ArcanistDifferentialCommitMessage' => 'differential/ArcanistDifferentialCommitMessage.php',
'ArcanistDifferentialCommitMessageParserException' => 'differential/ArcanistDifferentialCommitMessageParserException.php',
'ArcanistDifferentialRevisionHash' => 'differential/constants/ArcanistDifferentialRevisionHash.php',
'ArcanistDifferentialRevisionStatus' => 'differential/constants/ArcanistDifferentialRevisionStatus.php',
'ArcanistDownloadWorkflow' => 'workflow/ArcanistDownloadWorkflow.php',
'ArcanistEventType' => 'events/constant/ArcanistEventType.php',
'ArcanistExportWorkflow' => 'workflow/ArcanistExportWorkflow.php',
'ArcanistFilenameLinter' => 'lint/linter/ArcanistFilenameLinter.php',
'ArcanistGeneratedLinter' => 'lint/linter/ArcanistGeneratedLinter.php',
'ArcanistGetConfigWorkflow' => 'workflow/ArcanistGetConfigWorkflow.php',
'ArcanistGitAPI' => 'repository/api/ArcanistGitAPI.php',
'ArcanistGitHookPreReceiveWorkflow' => 'workflow/ArcanistGitHookPreReceiveWorkflow.php',
'ArcanistHelpWorkflow' => 'workflow/ArcanistHelpWorkflow.php',
'ArcanistHgClientChannel' => 'hgdaemon/ArcanistHgClientChannel.php',
'ArcanistHgProxyClient' => 'hgdaemon/ArcanistHgProxyClient.php',
'ArcanistHgProxyServer' => 'hgdaemon/ArcanistHgProxyServer.php',
'ArcanistHgServerChannel' => 'hgdaemon/ArcanistHgServerChannel.php',
'ArcanistHookAPI' => 'repository/hookapi/ArcanistHookAPI.php',
'ArcanistInlinesWorkflow' => 'workflow/ArcanistInlinesWorkflow.php',
'ArcanistInstallCertificateWorkflow' => 'workflow/ArcanistInstallCertificateWorkflow.php',
'ArcanistJSHintLinter' => 'lint/linter/ArcanistJSHintLinter.php',
'ArcanistLandWorkflow' => 'workflow/ArcanistLandWorkflow.php',
'ArcanistLiberateLintEngine' => 'lint/engine/ArcanistLiberateLintEngine.php',
'ArcanistLiberateWorkflow' => 'workflow/ArcanistLiberateWorkflow.php',
'ArcanistLicenseLinter' => 'lint/linter/ArcanistLicenseLinter.php',
'ArcanistLintConsoleRenderer' => 'lint/renderer/ArcanistLintConsoleRenderer.php',
'ArcanistLintEngine' => 'lint/engine/ArcanistLintEngine.php',
'ArcanistLintJSONRenderer' => 'lint/renderer/ArcanistLintJSONRenderer.php',
'ArcanistLintLikeCompilerRenderer' => 'lint/renderer/ArcanistLintLikeCompilerRenderer.php',
'ArcanistLintMessage' => 'lint/ArcanistLintMessage.php',
'ArcanistLintPatcher' => 'lint/ArcanistLintPatcher.php',
'ArcanistLintRenderer' => 'lint/renderer/ArcanistLintRenderer.php',
'ArcanistLintResult' => 'lint/ArcanistLintResult.php',
'ArcanistLintSeverity' => 'lint/ArcanistLintSeverity.php',
'ArcanistLintSummaryRenderer' => 'lint/renderer/ArcanistLintSummaryRenderer.php',
'ArcanistLintWorkflow' => 'workflow/ArcanistLintWorkflow.php',
'ArcanistLinter' => 'lint/linter/ArcanistLinter.php',
'ArcanistLinterTestCase' => 'lint/linter/__tests__/ArcanistLinterTestCase.php',
'ArcanistListWorkflow' => 'workflow/ArcanistListWorkflow.php',
'ArcanistMarkCommittedWorkflow' => 'workflow/ArcanistMarkCommittedWorkflow.php',
'ArcanistMercurialAPI' => 'repository/api/ArcanistMercurialAPI.php',
'ArcanistMercurialParser' => 'repository/parser/ArcanistMercurialParser.php',
'ArcanistMercurialParserTestCase' => 'repository/parser/__tests__/ArcanistMercurialParserTestCase.php',
'ArcanistNoEffectException' => 'exception/usage/ArcanistNoEffectException.php',
'ArcanistNoEngineException' => 'exception/usage/ArcanistNoEngineException.php',
'ArcanistNoLintLinter' => 'lint/linter/ArcanistNoLintLinter.php',
'ArcanistNoLintTestCaseMisnamed' => 'lint/linter/__tests__/ArcanistNoLintTestCase.php',
'ArcanistPEP8Linter' => 'lint/linter/ArcanistPEP8Linter.php',
'ArcanistPasteWorkflow' => 'workflow/ArcanistPasteWorkflow.php',
'ArcanistPatchWorkflow' => 'workflow/ArcanistPatchWorkflow.php',
'ArcanistPhpcsLinter' => 'lint/linter/ArcanistPhpcsLinter.php',
'ArcanistPhutilLibraryLinter' => 'lint/linter/ArcanistPhutilLibraryLinter.php',
'ArcanistPhutilModuleLinter' => 'lint/linter/ArcanistPhutilModuleLinter.php',
'ArcanistPhutilTestCase' => 'unit/engine/phutil/ArcanistPhutilTestCase.php',
'ArcanistPhutilTestCaseTestCase' => 'unit/engine/phutil/testcase/ArcanistPhutilTestCaseTestCase.php',
'ArcanistPhutilTestSkippedException' => 'unit/engine/phutil/testcase/ArcanistPhutilTestSkippedException.php',
'ArcanistPhutilTestTerminatedException' => 'unit/engine/phutil/testcase/ArcanistPhutilTestTerminatedException.php',
'ArcanistPyFlakesLinter' => 'lint/linter/ArcanistPyFlakesLinter.php',
'ArcanistPyLintLinter' => 'lint/linter/ArcanistPyLintLinter.php',
'ArcanistRepositoryAPI' => 'repository/api/ArcanistRepositoryAPI.php',
'ArcanistScriptAndRegexLinter' => 'lint/linter/ArcanistScriptAndRegexLinter.php',
'ArcanistSetConfigWorkflow' => 'workflow/ArcanistSetConfigWorkflow.php',
'ArcanistShellCompleteWorkflow' => 'workflow/ArcanistShellCompleteWorkflow.php',
'ArcanistSingleLintEngine' => 'lint/engine/ArcanistSingleLintEngine.php',
'ArcanistSpellingDefaultData' => 'lint/linter/ArcanistSpellingDefaultData.php',
'ArcanistSpellingLinter' => 'lint/linter/ArcanistSpellingLinter.php',
'ArcanistSpellingLinterTestCase' => 'lint/linter/__tests__/ArcanistSpellingLinterTestCase.php',
'ArcanistSubversionAPI' => 'repository/api/ArcanistSubversionAPI.php',
'ArcanistSubversionHookAPI' => 'repository/hookapi/ArcanistSubversionHookAPI.php',
'ArcanistSvnHookPreCommitWorkflow' => 'workflow/ArcanistSvnHookPreCommitWorkflow.php',
'ArcanistTasksWorkflow' => 'workflow/ArcanistTasksWorkflow.php',
'ArcanistTextLinter' => 'lint/linter/ArcanistTextLinter.php',
'ArcanistTextLinterTestCase' => 'lint/linter/__tests__/ArcanistTextLinterTestCase.php',
'ArcanistUncommittedChangesException' => 'exception/usage/ArcanistUncommittedChangesException.php',
'ArcanistUnitTestResult' => 'unit/ArcanistUnitTestResult.php',
'ArcanistUnitWorkflow' => 'workflow/ArcanistUnitWorkflow.php',
'ArcanistUpgradeWorkflow' => 'workflow/ArcanistUpgradeWorkflow.php',
'ArcanistUploadWorkflow' => 'workflow/ArcanistUploadWorkflow.php',
'ArcanistUsageException' => 'exception/ArcanistUsageException.php',
'ArcanistUserAbortException' => 'exception/usage/ArcanistUserAbortException.php',
'ArcanistWhichWorkflow' => 'workflow/ArcanistWhichWorkflow.php',
'ArcanistWorkingCopyIdentity' => 'workingcopyidentity/ArcanistWorkingCopyIdentity.php',
'ArcanistXHPASTLintNamingHook' => 'lint/linter/xhpast/ArcanistXHPASTLintNamingHook.php',
'ArcanistXHPASTLintNamingHookTestCase' => 'lint/linter/xhpast/__tests__/ArcanistXHPASTLintNamingHookTestCase.php',
'ArcanistXHPASTLinter' => 'lint/linter/ArcanistXHPASTLinter.php',
'ArcanistXHPASTLinterTestCase' => 'lint/linter/__tests__/ArcanistXHPASTLinterTestCase.php',
- 'BranchInfo' => 'branch/BranchInfo.php',
'ComprehensiveLintEngine' => 'lint/engine/ComprehensiveLintEngine.php',
'ExampleLintEngine' => 'lint/engine/ExampleLintEngine.php',
'NoseTestEngine' => 'unit/engine/NoseTestEngine.php',
'PhpunitTestEngine' => 'unit/engine/PhpunitTestEngine.php',
'PhutilLintEngine' => 'lint/engine/PhutilLintEngine.php',
'PhutilModuleRequirements' => 'parser/PhutilModuleRequirements.php',
'PhutilUnitTestEngine' => 'unit/engine/PhutilUnitTestEngine.php',
'PhutilUnitTestEngineTestCase' => 'unit/engine/__tests__/PhutilUnitTestEngineTestCase.php',
'UnitTestableArcanistLintEngine' => 'lint/engine/UnitTestableArcanistLintEngine.php',
),
'function' =>
array(
),
'xmap' =>
array(
'ArcanistAliasWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistAmendWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistApacheLicenseLinter' => 'ArcanistLicenseLinter',
'ArcanistApacheLicenseLinterTestCase' => 'ArcanistLinterTestCase',
'ArcanistBaseCommitParserTestCase' => 'ArcanistPhutilTestCase',
'ArcanistBranchWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistBundleTestCase' => 'ArcanistPhutilTestCase',
'ArcanistCallConduitWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistCapabilityNotSupportedException' => 'Exception',
'ArcanistChooseInvalidRevisionException' => 'Exception',
'ArcanistChooseNoRevisionsException' => 'Exception',
'ArcanistCloseRevisionWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistCloseWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistCommentRemoverTestCase' => 'ArcanistPhutilTestCase',
'ArcanistCommitWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistConduitLinter' => 'ArcanistLinter',
'ArcanistCoverWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistDiffParserTestCase' => 'ArcanistPhutilTestCase',
'ArcanistDiffUtilsTestCase' => 'ArcanistPhutilTestCase',
'ArcanistDiffWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistDifferentialCommitMessageParserException' => 'Exception',
'ArcanistDownloadWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistEventType' => 'PhutilEventType',
'ArcanistExportWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistFilenameLinter' => 'ArcanistLinter',
'ArcanistGeneratedLinter' => 'ArcanistLinter',
'ArcanistGetConfigWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistGitAPI' => 'ArcanistRepositoryAPI',
'ArcanistGitHookPreReceiveWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistHelpWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistHgClientChannel' => 'PhutilProtocolChannel',
'ArcanistHgServerChannel' => 'PhutilProtocolChannel',
'ArcanistInlinesWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistInstallCertificateWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistJSHintLinter' => 'ArcanistLinter',
'ArcanistLandWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistLiberateLintEngine' => 'ArcanistLintEngine',
'ArcanistLiberateWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistLicenseLinter' => 'ArcanistLinter',
'ArcanistLintConsoleRenderer' => 'ArcanistLintRenderer',
'ArcanistLintJSONRenderer' => 'ArcanistLintRenderer',
'ArcanistLintLikeCompilerRenderer' => 'ArcanistLintRenderer',
'ArcanistLintSummaryRenderer' => 'ArcanistLintRenderer',
'ArcanistLintWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistLinterTestCase' => 'ArcanistPhutilTestCase',
'ArcanistListWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistMarkCommittedWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistMercurialAPI' => 'ArcanistRepositoryAPI',
'ArcanistMercurialParserTestCase' => 'ArcanistPhutilTestCase',
'ArcanistNoEffectException' => 'ArcanistUsageException',
'ArcanistNoEngineException' => 'ArcanistUsageException',
'ArcanistNoLintLinter' => 'ArcanistLinter',
'ArcanistNoLintTestCaseMisnamed' => 'ArcanistLinterTestCase',
'ArcanistPEP8Linter' => 'ArcanistLinter',
'ArcanistPasteWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistPatchWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistPhpcsLinter' => 'ArcanistLinter',
'ArcanistPhutilLibraryLinter' => 'ArcanistLinter',
'ArcanistPhutilModuleLinter' => 'ArcanistLinter',
'ArcanistPhutilTestCaseTestCase' => 'ArcanistPhutilTestCase',
'ArcanistPhutilTestSkippedException' => 'Exception',
'ArcanistPhutilTestTerminatedException' => 'Exception',
'ArcanistPyFlakesLinter' => 'ArcanistLinter',
'ArcanistPyLintLinter' => 'ArcanistLinter',
'ArcanistScriptAndRegexLinter' => 'ArcanistLinter',
'ArcanistSetConfigWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistShellCompleteWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistSingleLintEngine' => 'ArcanistLintEngine',
'ArcanistSpellingLinter' => 'ArcanistLinter',
'ArcanistSpellingLinterTestCase' => 'ArcanistLinterTestCase',
'ArcanistSubversionAPI' => 'ArcanistRepositoryAPI',
'ArcanistSubversionHookAPI' => 'ArcanistHookAPI',
'ArcanistSvnHookPreCommitWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistTasksWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistTextLinter' => 'ArcanistLinter',
'ArcanistTextLinterTestCase' => 'ArcanistLinterTestCase',
'ArcanistUncommittedChangesException' => 'ArcanistUsageException',
'ArcanistUnitWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistUpgradeWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistUploadWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistUsageException' => 'Exception',
'ArcanistUserAbortException' => 'ArcanistUsageException',
'ArcanistWhichWorkflow' => 'ArcanistBaseWorkflow',
'ArcanistXHPASTLintNamingHookTestCase' => 'ArcanistPhutilTestCase',
'ArcanistXHPASTLinter' => 'ArcanistLinter',
'ArcanistXHPASTLinterTestCase' => 'ArcanistLinterTestCase',
'ComprehensiveLintEngine' => 'ArcanistLintEngine',
'ExampleLintEngine' => 'ArcanistLintEngine',
'NoseTestEngine' => 'ArcanistBaseUnitTestEngine',
'PhpunitTestEngine' => 'ArcanistBaseUnitTestEngine',
'PhutilLintEngine' => 'ArcanistLintEngine',
'PhutilUnitTestEngine' => 'ArcanistBaseUnitTestEngine',
'PhutilUnitTestEngineTestCase' => 'ArcanistPhutilTestCase',
'UnitTestableArcanistLintEngine' => 'ArcanistLintEngine',
),
));
diff --git a/src/branch/BranchInfo.php b/src/branch/BranchInfo.php
deleted file mode 100644
index 41bc7e7a..00000000
--- a/src/branch/BranchInfo.php
+++ /dev/null
@@ -1,180 +0,0 @@
-<?php
-
-/*
- * Copyright 2012 Facebook, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-/**
- * Holds information about a single git branch, and provides methods
- * for loading and display.
- */
-final class BranchInfo {
-
- private $branchName;
- private $currentHead = false;
- private $revisionID = null;
- private $sha1;
- private $status;
- private $commitAuthor;
- private $commitTime;
- private $commitSubject;
-
- /**
- * Retrives all the branches from the current git repository,
- * and parses their commit messages.
- *
- * @return array a list of BranchInfo objects, one per branch.
- */
- public static function loadAll(ArcanistGitAPI $api) {
- $branches_raw = $api->getAllBranches();
- $branches = array();
- foreach ($branches_raw as $branch_raw) {
- $branch_info = new BranchInfo($branch_raw['name']);
- $branch_info->setSha1($branch_raw['sha1']);
- if ($branch_raw['current']) {
- $branch_info->setCurrent();
- }
- $branches[] = $branch_info;
- }
-
- $name_sha1_map = mpull($branches, 'getSha1', 'getName');
- $commits_list = $api->multigetCommitMessages(
- array_unique(array_values($name_sha1_map)),
- "%ct%n%an%n%s%n%b");
- foreach ($branches as $branch) {
- $sha1 = $name_sha1_map[$branch->getName()];
- $branch->setSha1($sha1);
- $branch->parseCommitMessage($commits_list[$sha1]);
- }
- $branches = msort($branches, 'getCommitTime');
- return $branches;
- }
-
- public function __construct($branch_name) {
- $this->branchName = $branch_name;
- }
-
- public function setSha1($sha1) {
- $this->sha1 = $sha1;
- return $this;
- }
-
- public function getSha1() {
- return $this->sha1;
- }
-
- public function setCurrent() {
- $this->currentHead = true;
- return $this;
- }
-
- public function isCurrentHead() {
- return $this->currentHead;
- }
-
-
- public function setStatus($status) {
- $this->status = $status;
- return $this;
- }
-
- public function getStatus() {
- return $this->status;
- }
-
- public function getRevisionID() {
- return $this->revisionID;
- }
-
- public function getCommitTime() {
- return $this->commitTime;
- }
-
- public function getCommitSubject() {
- return $this->commitSubject;
- }
-
- public function getCommitDisplayName() {
- if ($this->revisionID) {
- return 'D'.$this->revisionID.': '.$this->commitSubject;
- } else {
- return $this->commitSubject;
- }
- }
-
- public function getCommitAuthor() {
- return $this->commitAuthor;
- }
-
- public function getName() {
- return $this->branchName;
- }
-
- /**
- * Based on the 'git show' output extracts the commit date, author,
- * subject nad Differential revision .
- * 'Differential Revision:'
- *
- * @param string message output of git show -s --format="format:%ct%n%cn%n%b"
- */
- public function parseCommitMessage($message) {
- $message_lines = explode("\n", trim($message));
- $this->commitTime = $message_lines[0];
- $this->commitAuthor = $message_lines[1];
- $this->commitSubject = trim($message_lines[2]);
- $this->revisionID =
- ArcanistDifferentialCommitMessage::newFromRawCorpus($message)
- ->getRevisionID();
- }
-
- public function getFormattedName() {
- $res = "";
- if ($this->currentHead) {
- $res = '* ';
- }
- $res .= $this->branchName;
- return phutil_console_format('**%s**', $res);
-
- }
-
- /**
- * Generates a colored status name
- */
- public function getFormattedStatus() {
- return self::renderColorizedRevisionStatus($this->status);
- }
-
- /**
- * Assigns a pretty color based on the status
- */
- private static function getColorForStatus($status) {
- static $status_to_color = array(
- 'Closed' => 'cyan',
- 'Needs Review' => 'magenta',
- 'Needs Revision' => 'red',
- 'Accepted' => 'green',
- 'No Revision' => 'blue',
- 'Abandoned' => 'default',
- );
- return idx($status_to_color, $status, 'default');
- }
-
- public static function renderColorizedRevisionStatus($status) {
- return phutil_console_format(
- '<fg:'.self::getColorForStatus($status).'>%s</fg>',
- $status);
- }
-
-}
diff --git a/src/repository/api/ArcanistGitAPI.php b/src/repository/api/ArcanistGitAPI.php
index b0e1b570..c71a0253 100644
--- a/src/repository/api/ArcanistGitAPI.php
+++ b/src/repository/api/ArcanistGitAPI.php
@@ -1,929 +1,900 @@
<?php
/*
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Interfaces with Git working copies.
*
* @group workingcopy
*/
final class ArcanistGitAPI extends ArcanistRepositoryAPI {
private $status;
private $relativeCommit = null;
private $repositoryHasNoCommits = false;
const SEARCH_LENGTH_FOR_PARENT_REVISIONS = 16;
/**
* For the repository's initial commit, 'git diff HEAD^' and similar do
* not work. Using this instead does work.
*/
const GIT_MAGIC_ROOT_COMMIT = '4b825dc642cb6eb9a060e54bf8d69288fbee4904';
public static function newHookAPI($root) {
return new ArcanistGitAPI($root);
}
protected function buildLocalFuture(array $argv) {
$argv[0] = 'git '.$argv[0];
$future = newv('ExecFuture', $argv);
$future->setCWD($this->getPath());
return $future;
}
public function getSourceControlSystemName() {
return 'git';
}
public function getMetadataPath() {
return $this->getPath('.git');
}
public function getHasCommits() {
return !$this->repositoryHasNoCommits;
}
public function setRelativeCommit($relative_commit) {
$this->relativeCommit = $relative_commit;
return $this;
}
public function getLocalCommitInformation() {
if ($this->repositoryHasNoCommits) {
// Zero commits.
throw new Exception(
"You can't get local commit information for a repository with no ".
"commits.");
} else if ($this->relativeCommit == self::GIT_MAGIC_ROOT_COMMIT) {
// One commit.
$against = 'HEAD';
} else {
// 2..N commits. We include commits reachable from HEAD which are
// not reachable from the relative commit; this is consistent with
// user expectations even though it is not actually the diff range.
// Particularly:
//
// |
// D <----- master branch
// |
// C Y <- feature branch
// | /|
// B X
// | /
// A
// |
//
// If "A, B, C, D" are master, and the user is at Y, when they run
// "arc diff B" they want (and get) a diff of B vs Y, but they think about
// this as being the commits X and Y. If we log "B..Y", we only show
// Y. With "Y --not B", we show X and Y.
$against = csprintf('%s --not %s', 'HEAD', $this->getRelativeCommit());
}
// NOTE: Windows escaping of "%" symbols apparently is inherently broken;
// when passed throuhgh escapeshellarg() they are replaced with spaces.
// TODO: Learn how cmd.exe works and find some clever workaround?
// NOTE: If we use "%x00", output is truncated in Windows.
list($info) = $this->execxLocal(
phutil_is_windows()
? 'log %C --format=%C --'
: 'log %C --format=%s --',
$against,
// NOTE: "%B" is somewhat new, use "%s%n%n%b" instead.
'%H%x01%T%x01%P%x01%at%x01%an%x01%s%x01%s%n%n%b%x02');
$commits = array();
$info = trim($info, " \n\2");
if (!strlen($info)) {
return array();
}
$info = explode("\2", $info);
foreach ($info as $line) {
list($commit, $tree, $parents, $time, $author, $title, $message)
= explode("\1", trim($line), 7);
$message = rtrim($message);
$commits[$commit] = array(
'commit' => $commit,
'tree' => $tree,
'parents' => array_filter(explode(' ', $parents)),
'time' => $time,
'author' => $author,
'summary' => $title,
'message' => $message,
);
}
return $commits;
}
public function getRelativeCommit() {
if ($this->relativeCommit === null) {
// Detect zero-commit or one-commit repositories. There is only one
// relative-commit value that makes any sense in these repositories: the
// empty tree.
list($err) = $this->execManualLocal('rev-parse --verify HEAD^');
if ($err) {
list($err) = $this->execManualLocal('rev-parse --verify HEAD');
if ($err) {
$this->repositoryHasNoCommits = true;
}
$this->relativeCommit = self::GIT_MAGIC_ROOT_COMMIT;
if ($this->repositoryHasNoCommits) {
$this->setBaseCommitExplanation(
"the repository has no commits.");
} else {
$this->setBaseCommitExplanation(
"the repository has only one commit.");
}
return $this->relativeCommit;
}
if ($this->getBaseCommitArgumentRules() ||
$this->getWorkingCopyIdentity()->getConfigFromAnySource('base')) {
$base = $this->resolveBaseCommit();
if (!$base) {
throw new ArcanistUsageException(
"None of the rules in your 'base' configuration matched a valid ".
"commit. Adjust rules or specify which commit you want to use ".
"explicitly.");
}
$this->relativeCommit = $base;
return $this->relativeCommit;
}
$do_write = false;
$default_relative = null;
$working_copy = $this->getWorkingCopyIdentity();
if ($working_copy) {
$default_relative = $working_copy->getConfig(
'git.default-relative-commit');
$this->setBaseCommitExplanation(
"it is the merge-base of '{$default_relative}' and HEAD, as ".
"specified in 'git.default-relative-commit' in '.arcconfig'. This ".
"setting overrides other settings.");
}
if (!$default_relative) {
list($err, $upstream) = $this->execManualLocal(
"rev-parse --abbrev-ref --symbolic-full-name '@{upstream}'");
if (!$err) {
$default_relative = trim($upstream);
$this->setBaseCommitExplanation(
"it is the merge-base of '{$default_relative}' (the Git upstream ".
"of the current branch) HEAD.");
}
}
if (!$default_relative) {
$default_relative = $this->readScratchFile('default-relative-commit');
$default_relative = trim($default_relative);
if ($default_relative) {
$this->setBaseCommitExplanation(
"it is the merge-base of '{$default_relative}' and HEAD, as ".
"specified in '.git/arc/default-relative-commit'.");
}
}
if (!$default_relative) {
// TODO: Remove the history lesson soon.
echo phutil_console_format(
"<bg:green>** Select a Default Commit Range **</bg>\n\n");
echo phutil_console_wrap(
"You're running a command which operates on a range of revisions ".
"(usually, from some revision to HEAD) but have not specified the ".
"revision that should determine the start of the range.\n\n".
"Previously, arc assumed you meant 'HEAD^' when you did not specify ".
"a start revision, but this behavior does not make much sense in ".
"most workflows outside of Facebook's historic git-svn workflow.\n\n".
"arc no longer assumes 'HEAD^'. You must specify a relative commit ".
"explicitly when you invoke a command (e.g., `arc diff HEAD^`, not ".
"just `arc diff`) or select a default for this working copy.\n\n".
"In most cases, the best default is 'origin/master'. You can also ".
"select 'HEAD^' to preserve the old behavior, or some other remote ".
"or branch. But you almost certainly want to select ".
"'origin/master'.\n\n".
"(Technically: the merge-base of the selected revision and HEAD is ".
"used to determine the start of the commit range.)");
$prompt = "What default do you want to use? [origin/master]";
$default = phutil_console_prompt($prompt);
if (!strlen(trim($default))) {
$default = 'origin/master';
}
$default_relative = $default;
$do_write = true;
}
list($object_type) = $this->execxLocal(
'cat-file -t %s',
$default_relative);
if (trim($object_type) !== 'commit') {
throw new Exception(
"Relative commit '{$default_relative}' is not the name of a commit!");
}
if ($do_write) {
// Don't perform this write until we've verified that the object is a
// valid commit name.
$this->writeScratchFile('default-relative-commit', $default_relative);
$this->setBaseCommitExplanation(
"it is the merge-base of '{$default_relative}' and HEAD, as you ".
"just specified.");
}
list($merge_base) = $this->execxLocal(
'merge-base %s HEAD',
$default_relative);
$this->relativeCommit = trim($merge_base);
}
return $this->relativeCommit;
}
private function getDiffFullOptions($detect_moves_and_renames = true) {
$options = array(
self::getDiffBaseOptions(),
'--no-color',
'--src-prefix=a/',
'--dst-prefix=b/',
'-U'.$this->getDiffLinesOfContext(),
);
if ($detect_moves_and_renames) {
$options[] = '-M';
$options[] = '-C';
}
return implode(' ', $options);
}
private function getDiffBaseOptions() {
$options = array(
// Disable external diff drivers, like graphical differs, since Arcanist
// needs to capture the diff text.
'--no-ext-diff',
// Disable textconv so we treat binary files as binary, even if they have
// an alternative textual representation. TODO: Ideally, Differential
// would ship up the binaries for 'arc patch' but display the textconv
// output in the visual diff.
'--no-textconv',
);
return implode(' ', $options);
}
public function getFullGitDiff() {
$options = $this->getDiffFullOptions();
list($stdout) = $this->execxLocal(
"diff {$options} %s --",
$this->getRelativeCommit());
return $stdout;
}
/**
* @param string Path to generate a diff for.
* @param bool If true, detect moves and renames. Otherwise, ignore
* moves/renames; this is useful because it prompts git to
* generate real diff text.
*/
public function getRawDiffText($path, $detect_moves_and_renames = true) {
$options = $this->getDiffFullOptions($detect_moves_and_renames);
list($stdout) = $this->execxLocal(
"diff {$options} %s -- %s",
$this->getRelativeCommit(),
$path);
return $stdout;
}
public function getBranchName() {
// TODO: consider:
//
// $ git rev-parse --abbrev-ref `git symbolic-ref HEAD`
//
// But that may fail if you're not on a branch.
list($stdout) = $this->execxLocal('branch');
$matches = null;
if (preg_match('/^\* (.+)$/m', $stdout, $matches)) {
return $matches[1];
}
return null;
}
public function getSourceControlPath() {
// TODO: Try to get something useful here.
return null;
}
public function getGitCommitLog() {
$relative = $this->getRelativeCommit();
if ($this->repositoryHasNoCommits) {
// No commits yet.
return '';
} else if ($relative == self::GIT_MAGIC_ROOT_COMMIT) {
// First commit.
list($stdout) = $this->execxLocal(
'log --format=medium HEAD');
} else {
// 2..N commits.
list($stdout) = $this->execxLocal(
'log --first-parent --format=medium %s..HEAD',
$this->getRelativeCommit());
}
return $stdout;
}
public function getGitHistoryLog() {
list($stdout) = $this->execxLocal(
'log --format=medium -n%d %s',
self::SEARCH_LENGTH_FOR_PARENT_REVISIONS,
$this->getRelativeCommit());
return $stdout;
}
public function getSourceControlBaseRevision() {
list($stdout) = $this->execxLocal(
'rev-parse %s',
$this->getRelativeCommit());
return rtrim($stdout, "\n");
}
public function getCanonicalRevisionName($string) {
list($stdout) = $this->execxLocal('show -s --format=%C %s',
'%H', $string);
return rtrim($stdout);
}
public function getWorkingCopyStatus() {
if (!isset($this->status)) {
$options = $this->getDiffBaseOptions();
// -- parallelize these slow cpu bound git calls.
// Find committed changes.
$committed_future = $this->buildLocalFuture(
array(
"diff {$options} --raw %s --",
$this->getRelativeCommit(),
));
// Find uncommitted changes.
$uncommitted_future = $this->buildLocalFuture(
array(
"diff {$options} --raw %s --",
$this->repositoryHasNoCommits
? self::GIT_MAGIC_ROOT_COMMIT
: 'HEAD',
));
// Untracked files
$untracked_future = $this->buildLocalFuture(
array(
'ls-files --others --exclude-standard',
));
// TODO: This doesn't list unstaged adds. It's not clear how to get that
// list other than "git status --porcelain" and then parsing it. :/
// Unstaged changes
$unstaged_future = $this->buildLocalFuture(
array(
'ls-files -m',
));
$futures = array(
$committed_future,
$uncommitted_future,
$untracked_future,
$unstaged_future
);
Futures($futures)->resolveAll();
// -- read back and process the results
list($stdout, $stderr) = $committed_future->resolvex();
$files = $this->parseGitStatus($stdout);
list($stdout, $stderr) = $uncommitted_future->resolvex();
$uncommitted_files = $this->parseGitStatus($stdout);
foreach ($uncommitted_files as $path => $mask) {
$mask |= self::FLAG_UNCOMMITTED;
if (!isset($files[$path])) {
$files[$path] = 0;
}
$files[$path] |= $mask;
}
list($stdout, $stderr) = $untracked_future->resolvex();
$stdout = rtrim($stdout, "\n");
if (strlen($stdout)) {
$stdout = explode("\n", $stdout);
foreach ($stdout as $file) {
$files[$file] = self::FLAG_UNTRACKED;
}
}
list($stdout, $stderr) = $unstaged_future->resolvex();
$stdout = rtrim($stdout, "\n");
if (strlen($stdout)) {
$stdout = explode("\n", $stdout);
foreach ($stdout as $file) {
$files[$file] = isset($files[$file])
? ($files[$file] | self::FLAG_UNSTAGED)
: self::FLAG_UNSTAGED;
}
}
$this->status = $files;
}
return $this->status;
}
public function amendCommit($message) {
$tmp_file = new TempFile();
Filesystem::writeFile($tmp_file, $message);
$this->execxLocal(
'commit --amend --allow-empty -F %s',
$tmp_file);
}
public function getPreReceiveHookStatus($old_ref, $new_ref) {
$options = $this->getDiffBaseOptions();
list($stdout) = $this->execxLocal(
"diff {$options} --raw %s %s --",
$old_ref,
$new_ref);
return $this->parseGitStatus($stdout, $full = true);
}
private function parseGitStatus($status, $full = false) {
static $flags = array(
'A' => self::FLAG_ADDED,
'M' => self::FLAG_MODIFIED,
'D' => self::FLAG_DELETED,
);
$status = trim($status);
$lines = array();
foreach (explode("\n", $status) as $line) {
if ($line) {
$lines[] = preg_split("/[ \t]/", $line);
}
}
$files = array();
foreach ($lines as $line) {
$mask = 0;
$flag = $line[4];
$file = $line[5];
foreach ($flags as $key => $bits) {
if ($flag == $key) {
$mask |= $bits;
}
}
if ($full) {
$files[$file] = array(
'mask' => $mask,
'ref' => rtrim($line[3], '.'),
);
} else {
$files[$file] = $mask;
}
}
return $files;
}
public function getBlame($path) {
// TODO: 'git blame' supports --porcelain and we should probably use it.
list($stdout) = $this->execxLocal(
'blame --date=iso -w -M %s -- %s',
$this->getRelativeCommit(),
$path);
$blame = array();
foreach (explode("\n", trim($stdout)) as $line) {
if (!strlen($line)) {
continue;
}
// lines predating a git repo's history are blamed to the oldest revision,
// with the commit hash prepended by a ^. we shouldn't count these lines
// as blaming to the oldest diff's unfortunate author
if ($line[0] == '^') {
continue;
}
$matches = null;
$ok = preg_match(
'/^([0-9a-f]+)[^(]+?[(](.*?) +\d\d\d\d-\d\d-\d\d/',
$line,
$matches);
if (!$ok) {
throw new Exception("Bad blame? `{$line}'");
}
$revision = $matches[1];
$author = $matches[2];
$blame[] = array($author, $revision);
}
return $blame;
}
public function getOriginalFileData($path) {
return $this->getFileDataAtRevision($path, $this->getRelativeCommit());
}
public function getCurrentFileData($path) {
return $this->getFileDataAtRevision($path, 'HEAD');
}
private function parseGitTree($stdout) {
$result = array();
$stdout = trim($stdout);
if (!strlen($stdout)) {
return $result;
}
$lines = explode("\n", $stdout);
foreach ($lines as $line) {
$matches = array();
$ok = preg_match(
'/^(\d{6}) (blob|tree) ([a-z0-9]{40})[\t](.*)$/',
$line,
$matches);
if (!$ok) {
throw new Exception("Failed to parse git ls-tree output!");
}
$result[$matches[4]] = array(
'mode' => $matches[1],
'type' => $matches[2],
'ref' => $matches[3],
);
}
return $result;
}
private function getFileDataAtRevision($path, $revision) {
// NOTE: We don't want to just "git show {$revision}:{$path}" since if the
// path was a directory at the given revision we'll get a list of its files
// and treat it as though it as a file containing a list of other files,
// which is silly.
list($stdout) = $this->execxLocal(
'ls-tree %s -- %s',
$revision,
$path);
$info = $this->parseGitTree($stdout);
if (empty($info[$path])) {
// No such path, or the path is a directory and we executed 'ls-tree dir/'
// and got a list of its contents back.
return null;
}
if ($info[$path]['type'] != 'blob') {
// Path is or was a directory, not a file.
return null;
}
list($stdout) = $this->execxLocal(
'cat-file blob %s',
$info[$path]['ref']);
return $stdout;
}
/**
* Returns names of all the branches in the current repository.
*
- * @return array where each element is a triple ('name', 'sha1', 'current')
+ * @return list<dict<string, string>> Dictionary of branch information.
*/
public function getAllBranches() {
- list($branch_info) = $this->execxLocal('branch --no-color');
- $lines = explode("\n", trim($branch_info));
+ list($branch_info) = $this->execxLocal(
+ 'branch --verbose --abbrev=40 --no-color');
+ $lines = explode("\n", rtrim($branch_info));
+
$result = array();
foreach ($lines as $line) {
- $match = array();
- preg_match('/^(\*?)\s*(.*)$/', $line, $match);
- $name = $match[2];
- if ($name == '(no branch)') {
- // Just ignore this, we could theoretically try to figure out the ref
- // and treat it like a real branch but that's sort of ridiculous.
+
+ if (preg_match('/^[* ]+\(no branch\)/', $line)) {
+ // This is indicating that the working copy is in a detached state;
+ // just ignore it.
continue;
}
+
+ list($current, $name, $hash, $desc) = preg_split('/\s+/', $line, 4);
$result[] = array(
- 'current' => !empty($match[1]),
+ 'current' => !empty($current),
'name' => $name,
+ 'hash' => $hash,
+ 'desc' => $desc,
);
}
- $all_names = ipull($result, 'name');
- // Calling 'git branch' first and then 'git rev-parse' is way faster than
- // 'git branch -v' for some reason.
- list($sha1s_string) = $this->execxLocal('rev-parse %Ls', $all_names);
-
- $sha1_map = array_combine($all_names, explode("\n", trim($sha1s_string)));
- foreach ($result as &$branch) {
- $branch['sha1'] = $sha1_map[$branch['name']];
- }
- return $result;
- }
-
- /**
- * Returns git commit messages for the given revisions,
- * in the specified format (see git show --help for options).
- *
- * @param array $revs a list of commit hashes
- * @param string $format the format to show messages in
- */
- public function multigetCommitMessages($revs, $format) {
-
- list($commits_string) = $this->execxLocal(
- "show -s --pretty='format:'%s%s %Ls",
- $format,
- '%x00',
- $revs);
- $commits_list = array_slice(explode("\0", $commits_string), 0, -1);
- $commits_list = array_combine($revs, $commits_list);
- return $commits_list;
- }
-
- public function getRepositoryOwner() {
- list($owner) = $this->execxLocal('config --get user.name');
- return trim($owner);
+ return $result;
}
public function getWorkingCopyRevision() {
list($stdout) = $this->execxLocal('rev-parse HEAD');
return rtrim($stdout, "\n");
}
public function isHistoryDefaultImmutable() {
return false;
}
public function supportsAmend() {
return true;
}
public function supportsRelativeLocalCommits() {
return true;
}
public function hasLocalCommit($commit) {
try {
$this->getCanonicalRevisionName($commit);
} catch (CommandException $exception) {
return false;
}
return true;
}
public function parseRelativeLocalCommit(array $argv) {
if (count($argv) == 0) {
return;
}
if (count($argv) != 1) {
throw new ArcanistUsageException("Specify only one commit.");
}
$base = reset($argv);
if ($base == ArcanistGitAPI::GIT_MAGIC_ROOT_COMMIT) {
$merge_base = $base;
$this->setBaseCommitExplanation(
"you explicitly specified the empty tree.");
} else {
list($err, $merge_base) = $this->execManualLocal(
'merge-base %s HEAD',
$base);
if ($err) {
throw new ArcanistUsageException(
"Unable to find any git commit named '{$base}' in this repository.");
}
$this->setBaseCommitExplanation(
"it is the merge-base of '{$base}' and HEAD, as you explicitly ".
"specified.");
}
$this->setRelativeCommit(trim($merge_base));
}
public function getAllLocalChanges() {
$diff = $this->getFullGitDiff();
if (!strlen(trim($diff))) {
return array();
}
$parser = new ArcanistDiffParser();
return $parser->parseDiff($diff);
}
public function supportsLocalBranchMerge() {
return true;
}
public function performLocalBranchMerge($branch, $message) {
if (!$branch) {
throw new ArcanistUsageException(
"Under git, you must specify the branch you want to merge.");
}
$err = phutil_passthru(
'(cd %s && git merge --no-ff -m %s %s)',
$this->getPath(),
$message,
$branch);
if ($err) {
throw new ArcanistUsageException("Merge failed!");
}
}
public function getFinalizedRevisionMessage() {
return "You may now push this commit upstream, as appropriate (e.g. with ".
"'git push', or 'git svn dcommit', or by printing and faxing it).";
}
public function getCommitMessageForRevision($rev) {
list($message) = $this->execxLocal(
'log -n1 %s',
$rev);
$parser = new ArcanistDiffParser();
return head($parser->parseDiff($message));
}
public function loadWorkingCopyDifferentialRevisions(
ConduitClient $conduit,
array $query) {
$messages = $this->getGitCommitLog();
if (!strlen($messages)) {
return array();
}
$parser = new ArcanistDiffParser();
$messages = $parser->parseDiff($messages);
// First, try to find revisions by explicit revision IDs in commit messages.
$reason_map = array();
$revision_ids = array();
foreach ($messages as $message) {
$object = ArcanistDifferentialCommitMessage::newFromRawCorpus(
$message->getMetadata('message'));
if ($object->getRevisionID()) {
$revision_ids[] = $object->getRevisionID();
$reason_map[$object->getRevisionID()] = $message->getCommitHash();
}
}
if ($revision_ids) {
$results = $conduit->callMethodSynchronous(
'differential.query',
$query + array(
'ids' => $revision_ids,
));
foreach ($results as $key => $result) {
$hash = substr($reason_map[$result['id']], 0, 16);
$results[$key]['why'] =
"Commit message for '{$hash}' has explicit 'Differential Revision'.";
}
return $results;
}
// If we didn't succeed, try to find revisions by hash.
$hashes = array();
foreach ($this->getLocalCommitInformation() as $commit) {
$hashes[] = array('gtcm', $commit['commit']);
$hashes[] = array('gttr', $commit['tree']);
}
$results = $conduit->callMethodSynchronous(
'differential.query',
$query + array(
'commitHashes' => $hashes,
));
foreach ($results as $key => $result) {
$results[$key]['why'] =
"A git commit or tree hash in the commit range is already attached ".
"to the Differential revision.";
}
return $results;
}
public function updateWorkingCopy() {
$this->execxLocal('pull');
}
public function getCommitSummary($commit) {
if ($commit == self::GIT_MAGIC_ROOT_COMMIT) {
return '(The Empty Tree)';
}
list($summary) = $this->execxLocal(
'log -n 1 --format=%C %s',
'%s',
$commit);
return trim($summary);
}
public function resolveBaseCommitRule($rule, $source) {
list($type, $name) = explode(':', $rule, 2);
switch ($type) {
case 'git':
$matches = null;
if (preg_match('/^merge-base\((.+)\)$/', $name, $matches)) {
list($err, $merge_base) = $this->execManualLocal(
'merge-base %s HEAD',
$matches[1]);
if (!$err) {
$this->setBaseCommitExplanation(
"it is the merge-base of '{$matches[1]}' and HEAD, as ".
"specified by '{$rule}' in your {$source} 'base' ".
"configuration.");
return trim($merge_base);
}
} else {
list($err) = $this->execManualLocal(
'cat-file -t %s',
$name);
if (!$err) {
$this->setBaseCommitExplanation(
"it is specified by '{$rule}' in your {$source} 'base' ".
"configuration.");
return $name;
}
}
break;
case 'arc':
switch ($name) {
case 'empty':
$this->setBaseCommitExplanation(
"you specified '{$rule}' in your {$source} 'base' ".
"configuration.");
return self::GIT_MAGIC_ROOT_COMMIT;
case 'upstream':
list($err, $upstream) = $this->execManualLocal(
"rev-parse --abbrev-ref --symbolic-full-name '@{upstream}'");
if (!$err) {
list($upstream_merge_base) = $this->execxLocal(
'merge-base %s HEAD',
$upstream);
$this->setBaseCommitExplanation(
"it is the merge-base of the upstream of the current branch ".
"and HEAD, and matched the rule '{$rule}' in your {$source} ".
"'base' configuration.");
return $upstream_merge_base;
}
break;
}
default:
return null;
}
return null;
}
}
diff --git a/src/workflow/ArcanistBranchWorkflow.php b/src/workflow/ArcanistBranchWorkflow.php
index 88566e8a..030915ed 100644
--- a/src/workflow/ArcanistBranchWorkflow.php
+++ b/src/workflow/ArcanistBranchWorkflow.php
@@ -1,175 +1,253 @@
<?php
/*
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Displays user's git branches
*
* @group workflow
*/
final class ArcanistBranchWorkflow extends ArcanistBaseWorkflow {
private $branches;
public function getCommandSynopses() {
return phutil_console_format(<<<EOTEXT
- **branch**
+ **branch** [__options__]
EOTEXT
);
}
public function getCommandHelp() {
return phutil_console_format(<<<EOTEXT
Supports: git
A wrapper on 'git branch'. It pulls data from Differential and
displays the revision status next to the branch name.
- Branches are sorted in ascending order by the last commit time.
- By default branches with closed/abandoned revisions
- are not displayed.
+
+ By default, branches are sorted chronologically. You can sort them
+ by status instead with __--by-status__.
+
+ By default, branches that are "Closed" or "Abandoned" are not
+ displayed. You can show them with __--view-all__.
EOTEXT
);
}
public function requiresConduit() {
return true;
}
public function requiresRepositoryAPI() {
return true;
}
public function requiresAuthentication() {
return true;
}
public function getArguments() {
return array(
'view-all' => array(
- 'help' =>
- "Include closed and abandoned revisions",
+ 'help' => 'Include closed and abandoned revisions',
),
'by-status' => array(
- 'help' => 'Group output by revision status.',
+ 'help' => 'Sort branches by status instead of time.',
),
);
}
public function run() {
$repository_api = $this->getRepositoryAPI();
if (!($repository_api instanceof ArcanistGitAPI)) {
throw new ArcanistUsageException(
- "arc branch is only supported under git."
- );
+ 'arc branch is only supported under git.');
}
- $this->branches = BranchInfo::loadAll($repository_api);
- $all_revisions = array_unique(
- array_filter(mpull($this->branches, 'getRevisionId')));
- $revision_status = $this->loadDifferentialStatuses($all_revisions);
- $owner = $repository_api->getRepositoryOwner();
- foreach ($this->branches as $branch) {
- if ($branch->getCommitAuthor() != $owner) {
- $branch->setStatus('Not Yours');
- continue;
- }
-
- $rev_id = $branch->getRevisionID();
- if ($rev_id) {
- $status = idx($revision_status, $rev_id, 'Unknown Status');
- $branch->setStatus($status);
- } else {
- $branch->setStatus('No Revision');
- }
+ $branches = $repository_api->getAllBranches();
+ if (!$branches) {
+ throw new ArcanistUsageException('No branches in this working copy.');
}
- if (!$this->getArgument('view-all')) {
- $this->filterOutFinished();
+
+ $commit_map = $this->loadCommitInfo($branches, $repository_api);
+ foreach ($branches as $key => $branch) {
+ $branches[$key] += $commit_map[$branch['hash']];
}
- $this->printInColumns();
+
+ $revisions = $this->loadRevisions($branches);
+
+ $this->printBranches($branches, $revisions);
+
+ return 0;
}
+ private function loadCommitInfo(
+ array $branches,
+ ArcanistRepositoryAPI $repository_api) {
+
+ $commits = ipull($branches, 'hash');
+ list($info) = $repository_api->execxLocal(
+ 'log --format=%C %Ls --',
+ '%H%x01%ct%x01%T%x01%s%n%b%x02',
+ $commits);
+
+ $commit_map = array();
+ $info = array_filter(explode("\2", trim($info)));
+ foreach ($info as $line) {
+ list($hash, $epoch, $tree, $text) = explode("\1", trim($line), 4);
+ $message = ArcanistDifferentialCommitMessage::newFromRawCorpus($text);
+ $id = $message->getRevisionID();
- /**
- * Makes a conduit call to differential to find out revision statuses
- * based on their IDs
- */
- private function loadDifferentialStatuses($rev_ids) {
- $conduit = $this->getConduit();
- $revisions = $conduit->callMethodSynchronous(
- 'differential.query',
- array(
- 'ids' => $rev_ids,
- ));
- $statuses = ipull($revisions, 'statusName', 'id');
- return $statuses;
+ $commit_map[$hash] = array(
+ 'epoch' => (int)$epoch,
+ 'tree' => $tree,
+ 'revisionID' => $id,
+ );
+ }
+
+ return $commit_map;
}
- /**
- * Removes the branches with status either closed or abandoned.
- */
- private function filterOutFinished() {
- foreach ($this->branches as $id => $branch) {
- if ($branch->isCurrentHead() ) {
- continue; //never filter the current branch
- }
- $status = $branch->getStatus();
- if ($status == 'Closed' || $status == 'Abandoned') {
- unset($this->branches[$id]);
+ private function loadRevisions(array $branches) {
+ $ids = array();
+ $hashes = array();
+
+ foreach ($branches as $branch) {
+ if ($branch['revisionID']) {
+ $ids[] = $branch['revisionID'];
}
+ $hashes[] = array('gtcm', $branch['hash']);
+ $hashes[] = array('gttr', $branch['tree']);
}
- }
- public function printInColumns() {
- $longest_name = 0;
- $longest_status = 0;
- foreach ($this->branches as $branch) {
- $longest_name = max(strlen($branch->getFormattedName()), $longest_name);
- $longest_status = max(strlen($branch->getStatus()), $longest_status);
+ $calls = array();
+
+ if ($ids) {
+ $calls[] = $this->getConduit()->callMethod(
+ 'differential.query',
+ array(
+ 'ids' => $ids,
+ ));
}
- if ($this->getArgument('by-status')) {
- $by_status = mgroup($this->branches, 'getStatus');
- foreach (array('Accepted', 'Needs Revision',
- 'Needs Review', 'No Revision') as $status) {
- $branches = idx($by_status, $status);
- if (!$branches) {
- continue;
+ if ($hashes) {
+ $calls[] = $this->getConduit()->callMethod(
+ 'differential.query',
+ array(
+ 'commitHashes' => $hashes,
+ ));
+ }
+
+ $results = array();
+ foreach (Futures($calls) as $call) {
+ $results[] = $call->resolve();
+ }
+
+ return array_mergev($results);
+ }
+
+ private function printBranches(array $branches, array $revisions) {
+ $revisions = ipull($revisions, null, 'id');
+
+ static $color_map = array(
+ 'Closed' => 'cyan',
+ 'Needs Review' => 'magenta',
+ 'Needs Revision' => 'red',
+ 'Accepted' => 'green',
+ 'No Revision' => 'blue',
+ 'Abandoned' => 'default',
+ );
+
+ static $ssort_map = array(
+ 'Closed' => 1,
+ 'No Revision' => 2,
+ 'Needs Review' => 3,
+ 'Needs Revision' => 4,
+ 'Accepted' => 5,
+ );
+
+ $out = array();
+ foreach ($branches as $branch) {
+ $revision = idx($revisions, idx($branch, 'revisionID'));
+
+ // If we haven't identified a revision by ID, try to identify it by hash.
+ if (!$revision) {
+ foreach ($revisions as $rev) {
+ $hashes = idx($rev, 'hashes', array());
+ foreach ($hashes as $hash) {
+ if (($hash[0] == 'gtcm' && $hash[1] == $branch['hash']) ||
+ ($hash[0] == 'gttr' && $hash[1] == $branch['tree'])) {
+ $revision = $rev;
+ break;
+ }
+ }
}
- echo reset($branches)->getFormattedStatus()."\n";
- foreach ($branches as $branch) {
- $name_markdown = $branch->getFormattedName();
- $subject = $branch->getCommitDisplayName();
- $name_markdown = str_pad($name_markdown, $longest_name + 4, ' ');
- echo " $name_markdown $subject\n";
+ }
+
+ if ($revision) {
+ $desc = 'D'.$revision['id'].': '.$revision['title'];
+ $status = $revision['statusName'];
+ } else {
+ $desc = $branch['desc'];
+ $status = 'No Revision';
+ }
+
+ if (!$this->getArgument('view-all')) {
+ if ($status == 'Closed' || $status == 'Abandoned') {
+ continue;
}
}
+
+ $epoch = $branch['epoch'];
+
+ $color = idx($color_map, $status, 'default');
+ $ssort = sprintf('%d%012d', idx($ssort_map, $status, 0), $epoch);
+
+ $out[] = array(
+ 'name' => $branch['name'],
+ 'current' => $branch['current'],
+ 'status' => $status,
+ 'desc' => $desc,
+ 'color' => $color,
+ 'esort' => $epoch,
+ 'ssort' => $ssort,
+ );
+ }
+
+ $len_name = max(array_map('strlen', ipull($out, 'name'))) + 2;
+ $len_status = max(array_map('strlen', ipull($out, 'status'))) + 2;
+
+ if ($this->getArgument('by-status')) {
+ $out = isort($out, 'ssort');
} else {
- foreach ($this->branches as $branch) {
- $name_markdown = $branch->getFormattedName();
- $status_markdown = $branch->getFormattedStatus();
- $subject = $branch->getCommitDisplayName();
- $subject_pad = $longest_status - strlen($branch->getStatus()) + 4;
- $name_markdown =
- str_pad($name_markdown, $longest_name + 4, ' ');
- $subject =
- str_pad($subject, strlen($subject) + $subject_pad, ' ', STR_PAD_LEFT);
- echo "$name_markdown $status_markdown $subject\n";
- }
+ $out = isort($out, 'esort');
+ }
+
+ $console = PhutilConsole::getConsole();
+ foreach ($out as $line) {
+ $color = $line['color'];
+ $console->writeOut(
+ "%s **%s** <fg:{$color}>%s</fg> %s\n",
+ $line['current'] ? '* ' : ' ',
+ str_pad($line['name'], $len_name),
+ str_pad($line['status'], $len_status),
+ $line['desc']);
}
}
+
}
diff --git a/src/workflow/ArcanistListWorkflow.php b/src/workflow/ArcanistListWorkflow.php
index d165156d..5c5d8bff 100644
--- a/src/workflow/ArcanistListWorkflow.php
+++ b/src/workflow/ArcanistListWorkflow.php
@@ -1,107 +1,105 @@
<?php
/*
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Lists open revisions in Differential.
*
* @group workflow
*/
final class ArcanistListWorkflow extends ArcanistBaseWorkflow {
public function getCommandSynopses() {
return phutil_console_format(<<<EOTEXT
**list**
EOTEXT
);
}
public function getCommandHelp() {
return phutil_console_format(<<<EOTEXT
Supports: git, svn, hg
List your open Differential revisions.
EOTEXT
);
}
public function requiresConduit() {
return true;
}
public function requiresRepositoryAPI() {
return true;
}
public function requiresAuthentication() {
return true;
}
public function run() {
$revisions = $this->getConduit()->callMethodSynchronous(
'differential.query',
array(
'authors' => array($this->getUserPHID()),
'status' => 'status-open',
));
if (!$revisions) {
echo "You have no open Differential revisions.\n";
return 0;
}
$repository_api = $this->getRepositoryAPI();
$info = array();
$status_len = 0;
foreach ($revisions as $key => $revision) {
$revision_path = Filesystem::resolvePath($revision['sourcePath']);
$current_path = Filesystem::resolvePath($repository_api->getPath());
if ($revision_path == $current_path) {
$info[$key]['here'] = 1;
} else {
$info[$key]['here'] = 0;
}
$info[$key]['sort'] = sprintf(
'%d%04d%08d',
$info[$key]['here'],
$revision['status'],
$revision['id']);
- $info[$key]['statusColorized'] =
- BranchInfo::renderColorizedRevisionStatus(
- $revision['statusName']);
+ $info[$key]['statusName'] = $revision['statusName'];
$status_len = max(
$status_len,
- strlen($info[$key]['statusColorized']));
+ strlen($info[$key]['statusName']));
}
$info = isort($info, 'sort');
foreach ($info as $key => $spec) {
$revision = $revisions[$key];
printf(
"%s %-".($status_len + 4)."s D%d: %s\n",
$spec['here']
? phutil_console_format('**%s**', '*')
: ' ',
- $spec['statusColorized'],
+ $spec['statusName'],
$revision['id'],
$revision['title']);
}
return 0;
}
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sat, Oct 11, 10:11 (17 h, 2 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
984188
Default Alt Text
(62 KB)
Attached To
Mode
R118 Arcanist - fork
Attached
Detach File
Event Timeline
Log In to Comment