1
0
Fork 0
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:
slawekbiel 2011-06-23 12:10:59 -07:00
parent 55353001d4
commit 5171ec161a
7 changed files with 422 additions and 0 deletions

View file

@ -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
View 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
View 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');

View file

@ -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);
}
}

View file

@ -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');

View 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";
}
}
}

View 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');