mirror of
https://we.phorge.it/source/arcanist.git
synced 2024-12-23 14:00:55 +01:00
arc branch
Summary: Appending differential status, sorting, filtering and coloring git branches. I think it turned out rather nicely. On my repository with 70 branches it takes 1.6s, not terrible, though 1.2s is in the conduit call - seems like there is potential for optimization. I didn't end up changing 'arc list', as their semmantics are slightly different, but I'm open to ideas of consolidating them Test Plan: - Tested on both facebook www and arcanist repositories. - Validated that view-all flag works - Validated that the ordering is correct - Validated that the statuses match the differential status. Reviewed By: epriestley Reviewers: epriestley CC: aran, epriestley, slawekbiel Revert Plan: sure Other Notes: Differential Revision: 497
This commit is contained in:
parent
55353001d4
commit
5171ec161a
7 changed files with 422 additions and 0 deletions
|
@ -13,6 +13,7 @@ phutil_register_library_map(array(
|
|||
'ArcanistApacheLicenseLinterTestCase' => 'lint/linter/apachelicense/__tests__',
|
||||
'ArcanistBaseUnitTestEngine' => 'unit/engine/base',
|
||||
'ArcanistBaseWorkflow' => 'workflow/base',
|
||||
'ArcanistBranchWorkflow' => 'workflow/branch',
|
||||
'ArcanistBundle' => 'parser/bundle',
|
||||
'ArcanistCallConduitWorkflow' => 'workflow/call-conduit',
|
||||
'ArcanistChooseInvalidRevisionException' => 'exception',
|
||||
|
@ -77,6 +78,7 @@ phutil_register_library_map(array(
|
|||
'ArcanistWorkingCopyIdentity' => 'workingcopyidentity',
|
||||
'ArcanistXHPASTLinter' => 'lint/linter/xhpast',
|
||||
'ArcanistXHPASTLinterTestCase' => 'lint/linter/xhpast/__tests__',
|
||||
'BranchInfo' => 'branch',
|
||||
'PhutilLintEngine' => 'lint/engine/phutil',
|
||||
'PhutilModuleRequirements' => 'parser/phutilmodule',
|
||||
'PhutilUnitTestEngine' => 'unit/engine/phutil',
|
||||
|
@ -91,6 +93,7 @@ phutil_register_library_map(array(
|
|||
'ArcanistAmendWorkflow' => 'ArcanistBaseWorkflow',
|
||||
'ArcanistApacheLicenseLinter' => 'ArcanistLicenseLinter',
|
||||
'ArcanistApacheLicenseLinterTestCase' => 'ArcanistLinterTestCase',
|
||||
'ArcanistBranchWorkflow' => 'ArcanistBaseWorkflow',
|
||||
'ArcanistCallConduitWorkflow' => 'ArcanistBaseWorkflow',
|
||||
'ArcanistCommitWorkflow' => 'ArcanistBaseWorkflow',
|
||||
'ArcanistCoverWorkflow' => 'ArcanistBaseWorkflow',
|
||||
|
|
166
src/branch/BranchInfo.php
Normal file
166
src/branch/BranchInfo.php
Normal file
|
@ -0,0 +1,166 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* Copyright 2011 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.
|
||||
*/
|
||||
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"); //don't ask
|
||||
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;
|
||||
}
|
||||
|
||||
public function isCurrentHead() {
|
||||
return $this->currentHead;
|
||||
}
|
||||
|
||||
|
||||
public function setStatus($status) {
|
||||
$this->status = $status;
|
||||
}
|
||||
|
||||
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 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 phutil_console_format(
|
||||
'<fg:'.$this->getColorForStatus().'>%s</fg>',
|
||||
$this->status);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigns a pretty color based on the status
|
||||
*/
|
||||
private function getColorForStatus() {
|
||||
static $status_to_color = array(
|
||||
'Committed' => 'cyan',
|
||||
'Needs Review' => 'magenta',
|
||||
'Needs Revision' => 'red',
|
||||
'Accepted' => 'green',
|
||||
'No Revision' => 'blue',
|
||||
'Abandoned' => 'default',
|
||||
);
|
||||
return idx($status_to_color, $this->status, 'default');
|
||||
}
|
||||
|
||||
}
|
15
src/branch/__init__.php
Normal file
15
src/branch/__init__.php
Normal file
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
/**
|
||||
* This file is automatically generated. Lint this module to rebuild it.
|
||||
* @generated
|
||||
*/
|
||||
|
||||
|
||||
|
||||
phutil_require_module('arcanist', 'differential/commitmessage');
|
||||
|
||||
phutil_require_module('phutil', 'console');
|
||||
phutil_require_module('phutil', 'utils');
|
||||
|
||||
|
||||
phutil_require_source('BranchInfo.php');
|
|
@ -398,4 +398,71 @@ class ArcanistGitAPI extends ArcanistRepositoryAPI {
|
|||
return $stdout;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns names of all the branches in the current repository.
|
||||
*
|
||||
* @return array where each element is a triple ('name', 'sha1', 'current')
|
||||
*/
|
||||
public function getAllBranches() {
|
||||
list($branch_info) = execx(
|
||||
'cd %s && git branch --no-color', $this->getPath());
|
||||
$lines = explode("\n", trim($branch_info));
|
||||
$result = array();
|
||||
foreach ($lines as $line) {
|
||||
$match = array();
|
||||
$branch = array();
|
||||
preg_match('/^(\*?)\s*(\S+)/', $line, $match);
|
||||
$branch['current'] = !empty($match[1]);
|
||||
$branch['name'] = $match[2];
|
||||
$result[] = $branch;
|
||||
}
|
||||
$all_names = ipull($result, 'name');
|
||||
$names_list = implode(' ', $all_names);
|
||||
// Calling 'git branch' first and then 'git rev-parse' is way faster than
|
||||
// 'git branch -v' for some reason.
|
||||
list($sha1s_string) = execx(
|
||||
"cd %s && git rev-parse $names_list",
|
||||
$this->path);
|
||||
$sha1_map = array_combine($all_names, explode("\n", trim($sha1s_string)));
|
||||
foreach ($result as &$branch) {
|
||||
$branch['sha1'] = $sha1_map[$branch['name']];
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function multigetRevForBranch($branch_names) {
|
||||
$names_list = implode(' ', $branch_names);
|
||||
list($sha1s_string) = execx(
|
||||
"cd %s && git rev-parse $names_list",
|
||||
$this->path);
|
||||
return array_combine($branch_names, explode("\n", trim($sha1s_string)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
$delimiter = "%%x00";
|
||||
$revs_list = implode(' ', $revs);
|
||||
$show_command =
|
||||
"git show -s --pretty=\"format:$format$delimiter\" $revs_list";
|
||||
list($commits_string) = execx(
|
||||
"cd %s && $show_command",
|
||||
$this->getPath());
|
||||
$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) = execx(
|
||||
'cd %s && git config --get user.name',
|
||||
$this->getPath());
|
||||
return trim($owner);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
phutil_require_module('arcanist', 'repository/api/base');
|
||||
|
||||
phutil_require_module('phutil', 'future/exec');
|
||||
phutil_require_module('phutil', 'utils');
|
||||
|
||||
|
||||
phutil_require_source('ArcanistGitAPI.php');
|
||||
|
|
152
src/workflow/branch/ArcanistBranchWorkflow.php
Normal file
152
src/workflow/branch/ArcanistBranchWorkflow.php
Normal file
|
@ -0,0 +1,152 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* Copyright 2011 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
|
||||
*/
|
||||
class ArcanistBranchWorkflow extends ArcanistBaseWorkflow {
|
||||
|
||||
private $branches;
|
||||
|
||||
public function getCommandHelp() {
|
||||
return phutil_console_format(<<<EOTEXT
|
||||
**branch**
|
||||
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 committed/abandoned revisions
|
||||
are not displayed.
|
||||
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 committed and abandoned revisions",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
public function run() {
|
||||
$repository_api = $this->getRepositoryAPI();
|
||||
if (!($repository_api instanceof ArcanistGitAPI)) {
|
||||
throw new ArcanistUsageException(
|
||||
"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');
|
||||
}
|
||||
}
|
||||
if (!$this->getArgument('view-all')) {
|
||||
$this->filterOutFinished();
|
||||
}
|
||||
$this->printInColumns();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Makes a conduit call to differential to find out revision statuses
|
||||
* based on their IDs
|
||||
*/
|
||||
private function loadDifferentialStatuses($rev_ids) {
|
||||
$conduit = $this->getConduit();
|
||||
$revision_future = $conduit->callMethod(
|
||||
'differential.find',
|
||||
array(
|
||||
'guids' => $rev_ids,
|
||||
'query' => 'revision-ids',
|
||||
));
|
||||
$revisions = array();
|
||||
foreach ($revision_future->resolve() as $revision_dict) {
|
||||
$revisions[] = ArcanistDifferentialRevisionRef::newFromDictionary(
|
||||
$revision_dict);
|
||||
}
|
||||
$statuses = mpull($revisions, 'getStatusName', 'getId');
|
||||
return $statuses;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the branches with status either committed 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 == 'Committed' || $status == 'Abandoned') {
|
||||
unset($this->branches[$id]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
foreach ($this->branches as $branch) {
|
||||
$name_markdown = $branch->getFormattedName();
|
||||
$status_markdown = $branch->getFormattedStatus();
|
||||
$subject = $branch->getCommitSubject();
|
||||
$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";
|
||||
}
|
||||
}
|
||||
}
|
18
src/workflow/branch/__init__.php
Normal file
18
src/workflow/branch/__init__.php
Normal file
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
/**
|
||||
* This file is automatically generated. Lint this module to rebuild it.
|
||||
* @generated
|
||||
*/
|
||||
|
||||
|
||||
|
||||
phutil_require_module('arcanist', 'branch');
|
||||
phutil_require_module('arcanist', 'differential/revision');
|
||||
phutil_require_module('arcanist', 'exception/usage');
|
||||
phutil_require_module('arcanist', 'workflow/base');
|
||||
|
||||
phutil_require_module('phutil', 'console');
|
||||
phutil_require_module('phutil', 'utils');
|
||||
|
||||
|
||||
phutil_require_source('ArcanistBranchWorkflow.php');
|
Loading…
Reference in a new issue