mirror of
https://we.phorge.it/source/phorge.git
synced 2024-12-19 03:50:54 +01:00
Build that thing someone posted a screenshot of on Facebook
Summary: Seemed kinda cool. Test Plan: {F1707244} Reviewers: chad Reviewed By: chad Differential Revision: https://secure.phabricator.com/D16210
This commit is contained in:
parent
6c7e392f89
commit
dc37789d53
10 changed files with 450 additions and 225 deletions
|
@ -556,6 +556,7 @@ phutil_register_library_map(array(
|
|||
'DifferentialRevisionViewController' => 'applications/differential/controller/DifferentialRevisionViewController.php',
|
||||
'DifferentialSchemaSpec' => 'applications/differential/storage/DifferentialSchemaSpec.php',
|
||||
'DifferentialSetDiffPropertyConduitAPIMethod' => 'applications/differential/conduit/DifferentialSetDiffPropertyConduitAPIMethod.php',
|
||||
'DifferentialStackGraph' => 'applications/differential/edge/DifferentialStackGraph.php',
|
||||
'DifferentialStoredCustomField' => 'applications/differential/customfield/DifferentialStoredCustomField.php',
|
||||
'DifferentialSubscribersField' => 'applications/differential/customfield/DifferentialSubscribersField.php',
|
||||
'DifferentialSummaryField' => 'applications/differential/customfield/DifferentialSummaryField.php',
|
||||
|
@ -1599,6 +1600,7 @@ phutil_register_library_map(array(
|
|||
'PHUICurtainExtension' => 'view/extension/PHUICurtainExtension.php',
|
||||
'PHUICurtainPanelView' => 'view/layout/PHUICurtainPanelView.php',
|
||||
'PHUICurtainView' => 'view/layout/PHUICurtainView.php',
|
||||
'PHUIDiffGraphView' => 'infrastructure/diff/view/PHUIDiffGraphView.php',
|
||||
'PHUIDiffInlineCommentDetailView' => 'infrastructure/diff/view/PHUIDiffInlineCommentDetailView.php',
|
||||
'PHUIDiffInlineCommentEditView' => 'infrastructure/diff/view/PHUIDiffInlineCommentEditView.php',
|
||||
'PHUIDiffInlineCommentRowScaffold' => 'infrastructure/diff/view/PHUIDiffInlineCommentRowScaffold.php',
|
||||
|
@ -4928,6 +4930,7 @@ phutil_register_library_map(array(
|
|||
'DifferentialRevisionViewController' => 'DifferentialController',
|
||||
'DifferentialSchemaSpec' => 'PhabricatorConfigSchemaSpec',
|
||||
'DifferentialSetDiffPropertyConduitAPIMethod' => 'DifferentialConduitAPIMethod',
|
||||
'DifferentialStackGraph' => 'AbstractDirectedGraph',
|
||||
'DifferentialStoredCustomField' => 'DifferentialCustomField',
|
||||
'DifferentialSubscribersField' => 'DifferentialCoreCustomField',
|
||||
'DifferentialSummaryField' => 'DifferentialCoreCustomField',
|
||||
|
@ -6132,6 +6135,7 @@ phutil_register_library_map(array(
|
|||
'PHUICurtainExtension' => 'Phobject',
|
||||
'PHUICurtainPanelView' => 'AphrontTagView',
|
||||
'PHUICurtainView' => 'AphrontTagView',
|
||||
'PHUIDiffGraphView' => 'Phobject',
|
||||
'PHUIDiffInlineCommentDetailView' => 'PHUIDiffInlineCommentView',
|
||||
'PHUIDiffInlineCommentEditView' => 'PHUIDiffInlineCommentView',
|
||||
'PHUIDiffInlineCommentRowScaffold' => 'AphrontView',
|
||||
|
|
|
@ -341,6 +341,21 @@ final class DifferentialRevisionViewController extends DifferentialController {
|
|||
->setKey('commits')
|
||||
->appendChild($local_table));
|
||||
|
||||
$stack_graph = id(new DifferentialStackGraph())
|
||||
->setSeedRevision($revision)
|
||||
->loadGraph();
|
||||
if (!$stack_graph->isEmpty()) {
|
||||
$stack_view = $this->renderStackView($revision, $stack_graph);
|
||||
list($stack_name, $stack_color, $stack_table) = $stack_view;
|
||||
|
||||
$tab_group->addTab(
|
||||
id(new PHUITabView())
|
||||
->setName($stack_name)
|
||||
->setKey('stack')
|
||||
->setColor($stack_color)
|
||||
->appendChild($stack_table));
|
||||
}
|
||||
|
||||
if ($other_view) {
|
||||
$tab_group->addTab(
|
||||
id(new PHUITabView())
|
||||
|
@ -1198,4 +1213,149 @@ final class DifferentialRevisionViewController extends DifferentialController {
|
|||
}
|
||||
|
||||
|
||||
private function renderStackView(
|
||||
DifferentialRevision $current,
|
||||
DifferentialStackGraph $graph) {
|
||||
|
||||
$ancestry = $graph->getParentEdges();
|
||||
$viewer = $this->getViewer();
|
||||
|
||||
$revisions = id(new DifferentialRevisionQuery())
|
||||
->setViewer($viewer)
|
||||
->withPHIDs(array_keys($ancestry))
|
||||
->execute();
|
||||
$revisions = mpull($revisions, null, 'getPHID');
|
||||
|
||||
$order = id(new PhutilDirectedScalarGraph())
|
||||
->addNodes($ancestry)
|
||||
->getTopographicallySortedNodes();
|
||||
|
||||
$ancestry = array_select_keys($ancestry, $order);
|
||||
|
||||
$traces = id(new PHUIDiffGraphView())
|
||||
->renderGraph($ancestry);
|
||||
|
||||
// Load author handles, and also revision handles for any revisions which
|
||||
// we failed to load (they might be policy restricted).
|
||||
$handle_phids = mpull($revisions, 'getAuthorPHID');
|
||||
foreach ($order as $phid) {
|
||||
if (empty($revisions[$phid])) {
|
||||
$handle_phids[] = $phid;
|
||||
}
|
||||
}
|
||||
$handles = $viewer->loadHandles($handle_phids);
|
||||
|
||||
$rows = array();
|
||||
$rowc = array();
|
||||
|
||||
$ii = 0;
|
||||
$seen = false;
|
||||
foreach ($ancestry as $phid => $ignored) {
|
||||
$revision = idx($revisions, $phid);
|
||||
if ($revision) {
|
||||
$status_icon = $revision->getStatusIcon();
|
||||
$status_name = $revision->getStatusDisplayName();
|
||||
|
||||
$status = array(
|
||||
id(new PHUIIconView())->setIcon($status_icon),
|
||||
' ',
|
||||
$status_name,
|
||||
);
|
||||
|
||||
$author = $viewer->renderHandle($revision->getAuthorPHID());
|
||||
$title = phutil_tag(
|
||||
'a',
|
||||
array(
|
||||
'href' => $revision->getURI(),
|
||||
),
|
||||
array(
|
||||
$revision->getMonogram(),
|
||||
' ',
|
||||
$revision->getTitle(),
|
||||
));
|
||||
} else {
|
||||
$status = null;
|
||||
$author = null;
|
||||
$title = $viewer->renderHandle($phid);
|
||||
}
|
||||
|
||||
$rows[] = array(
|
||||
$traces[$ii++],
|
||||
$status,
|
||||
$author,
|
||||
$title,
|
||||
);
|
||||
|
||||
if ($phid == $current->getPHID()) {
|
||||
$rowc[] = 'highlighted';
|
||||
} else {
|
||||
$rowc[] = null;
|
||||
}
|
||||
}
|
||||
|
||||
$stack_table = id(new AphrontTableView($rows))
|
||||
->setHeaders(
|
||||
array(
|
||||
null,
|
||||
pht('Status'),
|
||||
pht('Author'),
|
||||
pht('Revision'),
|
||||
))
|
||||
->setRowClasses($rowc)
|
||||
->setColumnClasses(
|
||||
array(
|
||||
'threads',
|
||||
null,
|
||||
null,
|
||||
'wide',
|
||||
));
|
||||
|
||||
// Count how many revisions this one depends on that are not yet closed.
|
||||
$seen = array();
|
||||
$look = array($current->getPHID());
|
||||
while ($look) {
|
||||
$phid = array_pop($look);
|
||||
|
||||
$parents = idx($ancestry, $phid, array());
|
||||
foreach ($parents as $parent) {
|
||||
if (isset($seen[$parent])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$seen[$parent] = $parent;
|
||||
$look[] = $parent;
|
||||
}
|
||||
}
|
||||
|
||||
$blocking_count = 0;
|
||||
foreach ($seen as $parent) {
|
||||
if ($parent == $current->getPHID()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$revision = idx($revisions, $parent);
|
||||
if (!$revision) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($revision->isClosed()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$blocking_count++;
|
||||
}
|
||||
|
||||
if (!$blocking_count) {
|
||||
$stack_name = pht('Stack');
|
||||
$stack_color = null;
|
||||
} else {
|
||||
$stack_name = pht(
|
||||
'Stack (%s Open)',
|
||||
new PhutilNumber($blocking_count));
|
||||
$stack_color = PHUIListItemView::STATUS_FAIL;
|
||||
}
|
||||
|
||||
return array($stack_name, $stack_color, $stack_table);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -19,22 +19,4 @@ final class DifferentialChildRevisionsField
|
|||
return pht('Lists revisions this one is depended on by.');
|
||||
}
|
||||
|
||||
public function shouldAppearInPropertyView() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function renderPropertyViewLabel() {
|
||||
return $this->getFieldName();
|
||||
}
|
||||
|
||||
public function getRequiredHandlePHIDsForPropertyView() {
|
||||
return PhabricatorEdgeQuery::loadDestinationPHIDs(
|
||||
$this->getObject()->getPHID(),
|
||||
DifferentialRevisionDependedOnByRevisionEdgeType::EDGECONST);
|
||||
}
|
||||
|
||||
public function renderPropertyViewValue(array $handles) {
|
||||
return $this->renderHandleList($handles);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -23,24 +23,6 @@ final class DifferentialParentRevisionsField
|
|||
return pht('Lists revisions this one depends on.');
|
||||
}
|
||||
|
||||
public function shouldAppearInPropertyView() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function renderPropertyViewLabel() {
|
||||
return $this->getFieldName();
|
||||
}
|
||||
|
||||
public function getRequiredHandlePHIDsForPropertyView() {
|
||||
return PhabricatorEdgeQuery::loadDestinationPHIDs(
|
||||
$this->getObject()->getPHID(),
|
||||
DifferentialRevisionDependsOnRevisionEdgeType::EDGECONST);
|
||||
}
|
||||
|
||||
public function renderPropertyViewValue(array $handles) {
|
||||
return $this->renderHandleList($handles);
|
||||
}
|
||||
|
||||
public function getProTips() {
|
||||
return array(
|
||||
pht(
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
<?php
|
||||
|
||||
final class DifferentialStackGraph
|
||||
extends AbstractDirectedGraph {
|
||||
|
||||
private $parentEdges = array();
|
||||
private $childEdges = array();
|
||||
|
||||
public function setSeedRevision(DifferentialRevision $revision) {
|
||||
return $this->addNodes(
|
||||
array(
|
||||
'<seed>' => array($revision->getPHID()),
|
||||
));
|
||||
}
|
||||
|
||||
public function isEmpty() {
|
||||
return (count($this->getNodes()) <= 2);
|
||||
}
|
||||
|
||||
public function getParentEdges() {
|
||||
return $this->parentEdges;
|
||||
}
|
||||
|
||||
protected function loadEdges(array $nodes) {
|
||||
$query = id(new PhabricatorEdgeQuery())
|
||||
->withSourcePHIDs($nodes)
|
||||
->withEdgeTypes(
|
||||
array(
|
||||
DifferentialRevisionDependsOnRevisionEdgeType::EDGECONST,
|
||||
DifferentialRevisionDependedOnByRevisionEdgeType::EDGECONST,
|
||||
));
|
||||
|
||||
$query->execute();
|
||||
|
||||
$map = array();
|
||||
foreach ($nodes as $node) {
|
||||
$parents = $query->getDestinationPHIDs(
|
||||
array($node),
|
||||
array(
|
||||
DifferentialRevisionDependsOnRevisionEdgeType::EDGECONST,
|
||||
));
|
||||
|
||||
$children = $query->getDestinationPHIDs(
|
||||
array($node),
|
||||
array(
|
||||
DifferentialRevisionDependedOnByRevisionEdgeType::EDGECONST,
|
||||
));
|
||||
|
||||
$this->parentEdges[$node] = $parents;
|
||||
$this->childEdges[$node] = $children;
|
||||
|
||||
$map[$node] = array_values(array_fuse($parents) + array_fuse($children));
|
||||
}
|
||||
|
||||
return $map;
|
||||
}
|
||||
|
||||
}
|
|
@ -136,6 +136,10 @@ final class DifferentialRevision extends DifferentialDAO
|
|||
return "D{$id}";
|
||||
}
|
||||
|
||||
public function getURI() {
|
||||
return '/'.$this->getMonogram();
|
||||
}
|
||||
|
||||
public function setTitle($title) {
|
||||
$this->title = $title;
|
||||
if (!$this->getID()) {
|
||||
|
@ -426,6 +430,31 @@ final class DifferentialRevision extends DifferentialDAO
|
|||
return DifferentialRevisionStatus::isClosedStatus($this->getStatus());
|
||||
}
|
||||
|
||||
public function getStatusIcon() {
|
||||
$map = array(
|
||||
ArcanistDifferentialRevisionStatus::NEEDS_REVIEW
|
||||
=> 'fa-code grey',
|
||||
ArcanistDifferentialRevisionStatus::NEEDS_REVISION
|
||||
=> 'fa-refresh red',
|
||||
ArcanistDifferentialRevisionStatus::CHANGES_PLANNED
|
||||
=> 'fa-headphones red',
|
||||
ArcanistDifferentialRevisionStatus::ACCEPTED
|
||||
=> 'fa-check green',
|
||||
ArcanistDifferentialRevisionStatus::CLOSED
|
||||
=> 'fa-check-square-o black',
|
||||
ArcanistDifferentialRevisionStatus::ABANDONED
|
||||
=> 'fa-plane black',
|
||||
);
|
||||
|
||||
return idx($map, $this->getStatus());
|
||||
}
|
||||
|
||||
public function getStatusDisplayName() {
|
||||
$status = $this->getStatus();
|
||||
return ArcanistDifferentialRevisionStatus::getNameForRevisionStatus(
|
||||
$status);
|
||||
}
|
||||
|
||||
public function getFlag(PhabricatorUser $viewer) {
|
||||
return $this->assertAttachedKey($this->flags, $viewer->getPHID());
|
||||
}
|
||||
|
|
|
@ -104,10 +104,6 @@ final class DifferentialRevisionListView extends AphrontView {
|
|||
|
||||
$modified = $revision->getDateModified();
|
||||
|
||||
$status = $revision->getStatus();
|
||||
$status_name =
|
||||
ArcanistDifferentialRevisionStatus::getNameForRevisionStatus($status);
|
||||
|
||||
if (isset($icons['flag'])) {
|
||||
$item->addHeadIcon($icons['flag']);
|
||||
}
|
||||
|
@ -155,29 +151,14 @@ final class DifferentialRevisionListView extends AphrontView {
|
|||
$item->addAttribute(pht('Reviewers: %s', $reviewers));
|
||||
$item->setEpoch($revision->getDateModified());
|
||||
|
||||
switch ($status) {
|
||||
case ArcanistDifferentialRevisionStatus::NEEDS_REVIEW:
|
||||
$item->setStatusIcon('fa-code grey', pht('Needs Review'));
|
||||
break;
|
||||
case ArcanistDifferentialRevisionStatus::NEEDS_REVISION:
|
||||
$item->setStatusIcon('fa-refresh red', pht('Needs Revision'));
|
||||
break;
|
||||
case ArcanistDifferentialRevisionStatus::CHANGES_PLANNED:
|
||||
$item->setStatusIcon('fa-headphones red', pht('Changes Planned'));
|
||||
break;
|
||||
case ArcanistDifferentialRevisionStatus::ACCEPTED:
|
||||
$item->setStatusIcon('fa-check green', pht('Accepted'));
|
||||
break;
|
||||
case ArcanistDifferentialRevisionStatus::CLOSED:
|
||||
$item->setDisabled(true);
|
||||
$item->setStatusIcon('fa-check-square-o black', pht('Closed'));
|
||||
break;
|
||||
case ArcanistDifferentialRevisionStatus::ABANDONED:
|
||||
$item->setDisabled(true);
|
||||
$item->setStatusIcon('fa-plane black', pht('Abandoned'));
|
||||
break;
|
||||
if ($revision->isClosed()) {
|
||||
$item->setDisabled(true);
|
||||
}
|
||||
|
||||
$item->setStatusIcon(
|
||||
$revision->getStatusIcon(),
|
||||
$revision->getStatusDisplayName());
|
||||
|
||||
$list->addItem($item);
|
||||
}
|
||||
|
||||
|
|
|
@ -82,7 +82,10 @@ final class DiffusionHistoryTableView extends DiffusionView {
|
|||
|
||||
$graph = null;
|
||||
if ($this->parents) {
|
||||
$graph = $this->renderGraph();
|
||||
$graph = id(new PHUIDiffGraphView())
|
||||
->setIsHead($this->isHead)
|
||||
->setIsTail($this->isTail)
|
||||
->renderGraph($this->parents);
|
||||
}
|
||||
|
||||
$show_builds = PhabricatorApplication::isClassInstalledForViewer(
|
||||
|
@ -219,166 +222,4 @@ final class DiffusionHistoryTableView extends DiffusionView {
|
|||
return $view->render();
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw a merge/branch graph from the parent revision data. We're basically
|
||||
* building up a bunch of strings like this:
|
||||
*
|
||||
* ^
|
||||
* |^
|
||||
* o|
|
||||
* |o
|
||||
* o
|
||||
*
|
||||
* ...which form an ASCII representation of the graph we eventually want to
|
||||
* draw.
|
||||
*
|
||||
* NOTE: The actual implementation is black magic.
|
||||
*/
|
||||
private function renderGraph() {
|
||||
// This keeps our accumulated information about each line of the
|
||||
// merge/branch graph.
|
||||
$graph = array();
|
||||
|
||||
// This holds the next commit we're looking for in each column of the
|
||||
// graph.
|
||||
$threads = array();
|
||||
|
||||
// This is the largest number of columns any row has, i.e. the width of
|
||||
// the graph.
|
||||
$count = 0;
|
||||
|
||||
foreach ($this->history as $key => $history) {
|
||||
$joins = array();
|
||||
$splits = array();
|
||||
|
||||
$parent_list = $this->parents[$history->getCommitIdentifier()];
|
||||
|
||||
// Look for some thread which has this commit as the next commit. If
|
||||
// we find one, this commit goes on that thread. Otherwise, this commit
|
||||
// goes on a new thread.
|
||||
|
||||
$line = '';
|
||||
$found = false;
|
||||
$pos = count($threads);
|
||||
for ($n = 0; $n < $count; $n++) {
|
||||
if (empty($threads[$n])) {
|
||||
$line .= ' ';
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($threads[$n] == $history->getCommitIdentifier()) {
|
||||
if ($found) {
|
||||
$line .= ' ';
|
||||
$joins[] = $n;
|
||||
unset($threads[$n]);
|
||||
} else {
|
||||
$line .= 'o';
|
||||
$found = true;
|
||||
$pos = $n;
|
||||
}
|
||||
} else {
|
||||
|
||||
// We render a "|" for any threads which have a commit that we haven't
|
||||
// seen yet, this is later drawn as a vertical line.
|
||||
$line .= '|';
|
||||
}
|
||||
}
|
||||
|
||||
// If we didn't find the thread this commit goes on, start a new thread.
|
||||
// We use "o" to mark the commit for the rendering engine, or "^" to
|
||||
// indicate that there's nothing after it so the line from the commit
|
||||
// upward should not be drawn.
|
||||
|
||||
if (!$found) {
|
||||
if ($this->isHead) {
|
||||
$line .= '^';
|
||||
} else {
|
||||
$line .= 'o';
|
||||
foreach ($graph as $k => $meta) {
|
||||
// Go back across all the lines we've already drawn and add a
|
||||
// "|" to the end, since this is connected to some future commit
|
||||
// we don't know about.
|
||||
for ($jj = strlen($meta['line']); $jj <= $count; $jj++) {
|
||||
$graph[$k]['line'] .= '|';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update the next commit on this thread to the commit's first parent.
|
||||
// This might have the effect of making a new thread.
|
||||
$threads[$pos] = head($parent_list);
|
||||
|
||||
// If we made a new thread, increase the thread count.
|
||||
$count = max($pos + 1, $count);
|
||||
|
||||
// Now, deal with splits (merges). I picked this terms opposite to the
|
||||
// underlying repository term to confuse you.
|
||||
foreach (array_slice($parent_list, 1) as $parent) {
|
||||
$found = false;
|
||||
|
||||
// Try to find the other parent(s) in our existing threads. If we find
|
||||
// them, split to that thread.
|
||||
|
||||
foreach ($threads as $idx => $thread_commit) {
|
||||
if ($thread_commit == $parent) {
|
||||
$found = true;
|
||||
$splits[] = $idx;
|
||||
}
|
||||
}
|
||||
|
||||
// If we didn't find the parent, we don't know about it yet. Find the
|
||||
// first free thread and add it as the "next" commit in that thread.
|
||||
// This might create a new thread.
|
||||
|
||||
if (!$found) {
|
||||
for ($n = 0; $n < $count; $n++) {
|
||||
if (empty($threads[$n])) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
$threads[$n] = $parent;
|
||||
$splits[] = $n;
|
||||
$count = max($n + 1, $count);
|
||||
}
|
||||
}
|
||||
|
||||
$graph[] = array(
|
||||
'line' => $line,
|
||||
'split' => $splits,
|
||||
'join' => $joins,
|
||||
);
|
||||
}
|
||||
|
||||
// If this is the last page in history, replace the "o" with an "x" so we
|
||||
// do not draw a connecting line downward, and replace "^" with an "X" for
|
||||
// repositories with exactly one commit.
|
||||
if ($this->isTail && $graph) {
|
||||
$last = array_pop($graph);
|
||||
$last['line'] = str_replace('o', 'x', $last['line']);
|
||||
$last['line'] = str_replace('^', 'X', $last['line']);
|
||||
$graph[] = $last;
|
||||
}
|
||||
|
||||
// Render into tags for the behavior.
|
||||
|
||||
foreach ($graph as $k => $meta) {
|
||||
$graph[$k] = javelin_tag(
|
||||
'div',
|
||||
array(
|
||||
'sigil' => 'commit-graph',
|
||||
'meta' => $meta,
|
||||
),
|
||||
'');
|
||||
}
|
||||
|
||||
Javelin::initBehavior(
|
||||
'diffusion-commit-graph',
|
||||
array(
|
||||
'count' => $count,
|
||||
));
|
||||
|
||||
return $graph;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
171
src/infrastructure/diff/view/PHUIDiffGraphView.php
Normal file
171
src/infrastructure/diff/view/PHUIDiffGraphView.php
Normal file
|
@ -0,0 +1,171 @@
|
|||
<?php
|
||||
|
||||
final class PHUIDiffGraphView extends Phobject {
|
||||
|
||||
private $isHead = true;
|
||||
private $isTail = true;
|
||||
|
||||
public function setIsHead($is_head) {
|
||||
$this->isHead = $is_head;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getIsHead() {
|
||||
return $this->isHead;
|
||||
}
|
||||
|
||||
public function setIsTail($is_tail) {
|
||||
$this->isTail = $is_tail;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getIsTail() {
|
||||
return $this->isTail;
|
||||
}
|
||||
|
||||
public function renderGraph(array $parents) {
|
||||
// This keeps our accumulated information about each line of the
|
||||
// merge/branch graph.
|
||||
$graph = array();
|
||||
|
||||
// This holds the next commit we're looking for in each column of the
|
||||
// graph.
|
||||
$threads = array();
|
||||
|
||||
// This is the largest number of columns any row has, i.e. the width of
|
||||
// the graph.
|
||||
$count = 0;
|
||||
|
||||
foreach ($parents as $cursor => $parent_list) {
|
||||
$joins = array();
|
||||
$splits = array();
|
||||
|
||||
// Look for some thread which has this commit as the next commit. If
|
||||
// we find one, this commit goes on that thread. Otherwise, this commit
|
||||
// goes on a new thread.
|
||||
|
||||
$line = '';
|
||||
$found = false;
|
||||
$pos = count($threads);
|
||||
for ($n = 0; $n < $count; $n++) {
|
||||
if (empty($threads[$n])) {
|
||||
$line .= ' ';
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($threads[$n] == $cursor) {
|
||||
if ($found) {
|
||||
$line .= ' ';
|
||||
$joins[] = $n;
|
||||
unset($threads[$n]);
|
||||
} else {
|
||||
$line .= 'o';
|
||||
$found = true;
|
||||
$pos = $n;
|
||||
}
|
||||
} else {
|
||||
|
||||
// We render a "|" for any threads which have a commit that we haven't
|
||||
// seen yet, this is later drawn as a vertical line.
|
||||
$line .= '|';
|
||||
}
|
||||
}
|
||||
|
||||
// If we didn't find the thread this commit goes on, start a new thread.
|
||||
// We use "o" to mark the commit for the rendering engine, or "^" to
|
||||
// indicate that there's nothing after it so the line from the commit
|
||||
// upward should not be drawn.
|
||||
|
||||
if (!$found) {
|
||||
if ($this->getIsHead()) {
|
||||
$line .= '^';
|
||||
} else {
|
||||
$line .= 'o';
|
||||
foreach ($graph as $k => $meta) {
|
||||
// Go back across all the lines we've already drawn and add a
|
||||
// "|" to the end, since this is connected to some future commit
|
||||
// we don't know about.
|
||||
for ($jj = strlen($meta['line']); $jj <= $count; $jj++) {
|
||||
$graph[$k]['line'] .= '|';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update the next commit on this thread to the commit's first parent.
|
||||
// This might have the effect of making a new thread.
|
||||
$threads[$pos] = head($parent_list);
|
||||
|
||||
// If we made a new thread, increase the thread count.
|
||||
$count = max($pos + 1, $count);
|
||||
|
||||
// Now, deal with splits (merges). I picked this terms opposite to the
|
||||
// underlying repository term to confuse you.
|
||||
foreach (array_slice($parent_list, 1) as $parent) {
|
||||
$found = false;
|
||||
|
||||
// Try to find the other parent(s) in our existing threads. If we find
|
||||
// them, split to that thread.
|
||||
|
||||
foreach ($threads as $idx => $thread_commit) {
|
||||
if ($thread_commit == $parent) {
|
||||
$found = true;
|
||||
$splits[] = $idx;
|
||||
}
|
||||
}
|
||||
|
||||
// If we didn't find the parent, we don't know about it yet. Find the
|
||||
// first free thread and add it as the "next" commit in that thread.
|
||||
// This might create a new thread.
|
||||
|
||||
if (!$found) {
|
||||
for ($n = 0; $n < $count; $n++) {
|
||||
if (empty($threads[$n])) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
$threads[$n] = $parent;
|
||||
$splits[] = $n;
|
||||
$count = max($n + 1, $count);
|
||||
}
|
||||
}
|
||||
|
||||
$graph[] = array(
|
||||
'line' => $line,
|
||||
'split' => $splits,
|
||||
'join' => $joins,
|
||||
);
|
||||
}
|
||||
|
||||
// If this is the last page in history, replace the "o" with an "x" so we
|
||||
// do not draw a connecting line downward, and replace "^" with an "X" for
|
||||
// repositories with exactly one commit.
|
||||
if ($this->getIsTail() && $graph) {
|
||||
$last = array_pop($graph);
|
||||
$last['line'] = str_replace('o', 'x', $last['line']);
|
||||
$last['line'] = str_replace('^', 'X', $last['line']);
|
||||
$graph[] = $last;
|
||||
}
|
||||
|
||||
// Render into tags for the behavior.
|
||||
|
||||
foreach ($graph as $k => $meta) {
|
||||
$graph[$k] = javelin_tag(
|
||||
'div',
|
||||
array(
|
||||
'sigil' => 'commit-graph',
|
||||
'meta' => $meta,
|
||||
),
|
||||
'');
|
||||
}
|
||||
|
||||
Javelin::initBehavior(
|
||||
'diffusion-commit-graph',
|
||||
array(
|
||||
'count' => $count,
|
||||
));
|
||||
|
||||
return $graph;
|
||||
}
|
||||
|
||||
}
|
|
@ -6,6 +6,7 @@ final class PHUITabView extends AphrontTagView {
|
|||
private $key;
|
||||
private $keyLocked;
|
||||
private $contentID;
|
||||
private $color;
|
||||
|
||||
public function setKey($key) {
|
||||
if ($this->keyLocked) {
|
||||
|
@ -58,8 +59,17 @@ final class PHUITabView extends AphrontTagView {
|
|||
return $this->contentID;
|
||||
}
|
||||
|
||||
public function setColor($color) {
|
||||
$this->color = $color;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getColor() {
|
||||
return $this->color;
|
||||
}
|
||||
|
||||
public function newMenuItem() {
|
||||
return id(new PHUIListItemView())
|
||||
$item = id(new PHUIListItemView())
|
||||
->setName($this->getName())
|
||||
->setKey($this->getKey())
|
||||
->setType(PHUIListItemView::TYPE_LINK)
|
||||
|
@ -69,6 +79,13 @@ final class PHUITabView extends AphrontTagView {
|
|||
array(
|
||||
'tabKey' => $this->getKey(),
|
||||
));
|
||||
|
||||
$color = $this->getColor();
|
||||
if ($color !== null) {
|
||||
$item->setStatusColor($color);
|
||||
}
|
||||
|
||||
return $item;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue