1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-12-23 05:50:55 +01:00

Render pretty graphical traces for commit branches, etc

Summary: I AM A WIZARD

Test Plan: BEHOLD

Reviewers: btrahan

Reviewed By: btrahan

CC: aran, epriestley

Maniphest Tasks: T961

Differential Revision: https://secure.phabricator.com/D2007
This commit is contained in:
epriestley 2012-03-23 17:11:15 -07:00
parent bee69f9ce2
commit c2f50e258a
11 changed files with 456 additions and 37 deletions

View file

@ -135,7 +135,7 @@ celerity_register_resource_map(array(
),
'aphront-table-view-css' =>
array(
'uri' => '/res/3ff30c4f/rsrc/css/aphront/table-view.css',
'uri' => '/res/cbc7ab3a/rsrc/css/aphront/table-view.css',
'type' => 'css',
'requires' =>
array(
@ -642,6 +642,18 @@ celerity_register_resource_map(array(
),
'disk' => '/rsrc/js/application/differential/behavior-show-more.js',
),
'javelin-behavior-diffusion-commit-graph' =>
array(
'uri' => '/res/cfe336e8/rsrc/js/application/diffusion/behavior-commit-graph.js',
'type' => 'js',
'requires' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-dom',
2 => 'javelin-stratcom',
),
'disk' => '/rsrc/js/application/diffusion/behavior-commit-graph.js',
),
'javelin-behavior-diffusion-jump-to' =>
array(
'uri' => '/res/7c42e1ba/rsrc/js/application/diffusion/behavior-jump-to.js',
@ -1692,7 +1704,7 @@ celerity_register_resource_map(array(
),
'phabricator-remarkup-css' =>
array(
'uri' => '/res/11f89984/rsrc/css/core/remarkup.css',
'uri' => '/res/ff60c68a/rsrc/css/core/remarkup.css',
'type' => 'css',
'requires' =>
array(
@ -1968,7 +1980,7 @@ celerity_register_resource_map(array(
), array(
'packages' =>
array(
'66f447f1' =>
'c02b2ef0' =>
array(
'name' => 'core.pkg.css',
'symbols' =>
@ -1993,7 +2005,7 @@ celerity_register_resource_map(array(
17 => 'aphront-pager-view-css',
18 => 'phabricator-transaction-view-css',
),
'uri' => '/res/pkg/66f447f1/core.pkg.css',
'uri' => '/res/pkg/c02b2ef0/core.pkg.css',
'type' => 'css',
),
'21d01ed8' =>
@ -2140,17 +2152,17 @@ celerity_register_resource_map(array(
'reverse' =>
array(
'aphront-attached-file-view-css' => '31583232',
'aphront-crumbs-view-css' => '66f447f1',
'aphront-dialog-view-css' => '66f447f1',
'aphront-form-view-css' => '66f447f1',
'aphront-crumbs-view-css' => 'c02b2ef0',
'aphront-dialog-view-css' => 'c02b2ef0',
'aphront-form-view-css' => 'c02b2ef0',
'aphront-headsup-action-list-view-css' => '551249fc',
'aphront-list-filter-view-css' => '66f447f1',
'aphront-pager-view-css' => '66f447f1',
'aphront-panel-view-css' => '66f447f1',
'aphront-side-nav-view-css' => '66f447f1',
'aphront-table-view-css' => '66f447f1',
'aphront-tokenizer-control-css' => '66f447f1',
'aphront-typeahead-control-css' => '66f447f1',
'aphront-list-filter-view-css' => 'c02b2ef0',
'aphront-pager-view-css' => 'c02b2ef0',
'aphront-panel-view-css' => 'c02b2ef0',
'aphront-side-nav-view-css' => 'c02b2ef0',
'aphront-table-view-css' => 'c02b2ef0',
'aphront-tokenizer-control-css' => 'c02b2ef0',
'aphront-typeahead-control-css' => 'c02b2ef0',
'differential-changeset-view-css' => '551249fc',
'differential-core-view-css' => '551249fc',
'differential-inline-comment-editor' => '9b256876',
@ -2208,23 +2220,23 @@ celerity_register_resource_map(array(
'maniphest-task-detail-css' => '31583232',
'maniphest-task-summary-css' => '31583232',
'maniphest-transaction-detail-css' => '31583232',
'phabricator-app-buttons-css' => '66f447f1',
'phabricator-app-buttons-css' => 'c02b2ef0',
'phabricator-content-source-view-css' => '551249fc',
'phabricator-core-buttons-css' => '66f447f1',
'phabricator-core-css' => '66f447f1',
'phabricator-directory-css' => '66f447f1',
'phabricator-core-buttons-css' => 'c02b2ef0',
'phabricator-core-css' => 'c02b2ef0',
'phabricator-directory-css' => 'c02b2ef0',
'phabricator-drag-and-drop-file-upload' => '9b256876',
'phabricator-dropdown-menu' => '21d01ed8',
'phabricator-jump-nav' => '66f447f1',
'phabricator-jump-nav' => 'c02b2ef0',
'phabricator-keyboard-shortcut' => '21d01ed8',
'phabricator-keyboard-shortcut-manager' => '21d01ed8',
'phabricator-menu-item' => '21d01ed8',
'phabricator-object-selector-css' => '551249fc',
'phabricator-paste-file-upload' => '21d01ed8',
'phabricator-remarkup-css' => '66f447f1',
'phabricator-remarkup-css' => 'c02b2ef0',
'phabricator-shaped-request' => '9b256876',
'phabricator-standard-page-view' => '66f447f1',
'phabricator-transaction-view-css' => '66f447f1',
'syntax-highlighting-css' => '66f447f1',
'phabricator-standard-page-view' => 'c02b2ef0',
'phabricator-transaction-view-css' => 'c02b2ef0',
'syntax-highlighting-css' => 'c02b2ef0',
),
));

View file

@ -35,6 +35,11 @@ final class DiffusionHistoryController extends DiffusionController {
$history_query->needChildChanges(true);
}
$show_graph = !strlen($drequest->getPath());
if ($show_graph) {
$history_query->needParents(true);
}
$history = $history_query->loadHistory();
$pager = new AphrontPagerView();
@ -81,6 +86,11 @@ final class DiffusionHistoryController extends DiffusionController {
$handles = id(new PhabricatorObjectHandleData($phids))->loadHandles();
$history_table->setHandles($handles);
if ($show_graph) {
$history_table->setParents($history_query->getParents());
$history_table->setIsHead($offset == 0);
}
$history_panel = new AphrontPanelView();
$history_panel->setHeader('History');
$history_panel->addButton($button);

View file

@ -31,6 +31,7 @@ final class DiffusionRepositoryController extends DiffusionController {
$history_query = DiffusionHistoryQuery::newFromDiffusionRequest(
$drequest);
$history_query->setLimit(15);
$history_query->needParents(true);
$history = $history_query->loadHistory();
$browse_query = DiffusionBrowseQuery::newFromDiffusionRequest($drequest);
@ -63,6 +64,8 @@ final class DiffusionRepositoryController extends DiffusionController {
$history_table->setDiffusionRequest($drequest);
$history_table->setHandles($handles);
$history_table->setHistory($history);
$history_table->setParents($history_query->getParents());
$history_table->setIsHead(true);
$callsign = $drequest->getRepository()->getCallsign();
$all = phutil_render_tag(

View file

@ -23,6 +23,9 @@ abstract class DiffusionHistoryQuery extends DiffusionQuery {
protected $needDirectChanges;
protected $needChildChanges;
protected $needParents;
protected $parents = array();
final public static function newFromDiffusionRequest(
DiffusionRequest $request) {
@ -40,6 +43,18 @@ abstract class DiffusionHistoryQuery extends DiffusionQuery {
return $this;
}
final public function needParents($parents) {
$this->needParents = $parents;
return $this;
}
final public function getParents() {
if (!$this->needParents) {
throw new Exception('Specify needParents() before calling getParents()!');
}
return $this->parents;
}
final public function loadHistory() {
return $this->executeQuery();
}

View file

@ -1,7 +1,7 @@
<?php
/*
* Copyright 2011 Facebook, Inc.
* 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.
@ -29,18 +29,28 @@ final class DiffusionGitHistoryQuery extends DiffusionHistoryQuery {
'log '.
'--skip=%d '.
'-n %d '.
'--abbrev=40 '.
'--pretty=format:%%H '.
'%s -- %s',
'--pretty=format:%s '.
'%s -- %C',
$this->getOffset(),
$this->getLimit(),
'%H:%P',
$commit_hash,
$path);
// Git omits merge commits if the path is provided, even if it is empty.
(strlen($path) ? csprintf('%s', $path) : ''));
$hashes = explode("\n", $stdout);
$hashes = array_filter($hashes);
$hash_list = array();
$parent_map = array();
return $this->loadHistoryForCommitIdentifiers($hashes);
$lines = explode("\n", trim($stdout));
foreach ($lines as $line) {
list($hash, $parents) = explode(":", $line);
$hash_list[] = $hash;
$parent_map[$hash] = preg_split('/\s+/', $parents);
}
$this->parents = $parent_map;
return $this->loadHistoryForCommitIdentifiers($hash_list);
}
}

View file

@ -8,5 +8,7 @@
phutil_require_module('phabricator', 'applications/diffusion/query/history/base');
phutil_require_module('phutil', 'xsprintf/csprintf');
phutil_require_source('DiffusionGitHistoryQuery.php');

View file

@ -33,18 +33,56 @@ final class DiffusionMercurialHistoryQuery extends DiffusionHistoryQuery {
$default_path = '';
list($stdout) = $repository->execxLocalCommand(
'log --template %s --limit %d --branch %s --rev %s:0 -- %s',
'{node}\\n',
'log --debug --template %s --limit %d --branch %s --rev %s:0 -- %s',
'{node};{parents}\\n',
($this->getOffset() + $this->getLimit()), // No '--skip' in Mercurial.
$drequest->getBranch(),
$commit_hash,
nonempty(ltrim($path, '/'), $default_path));
$hashes = explode("\n", $stdout);
$hashes = array_filter($hashes);
$hashes = array_slice($hashes, $this->getOffset());
$lines = explode("\n", trim($stdout));
$lines = array_slice($lines, $this->getOffset());
return $this->loadHistoryForCommitIdentifiers($hashes);
$hash_list = array();
$parent_map = array();
$last = null;
foreach (array_reverse($lines) as $line) {
list($hash, $parents) = explode(';', $line);
$parents = trim($parents);
if (!$parents) {
if ($last === null) {
$parent_map[$hash] = array('...');
} else {
$parent_map[$hash] = array($last);
}
} else {
$parents = preg_split('/\s+/', $parents);
foreach ($parents as $parent) {
list($plocal, $phash) = explode(':', $parent);
if (!preg_match('/^0+$/', $phash)) {
$parent_map[$hash][] = $phash;
}
}
// This may happen for the zeroth commit in repository, both hashes
// are "000000000...".
if (empty($parent_map[$hash])) {
$parent_map[$hash] = array('...');
}
}
// The rendering code expects the first commit to be "mainline", like
// Git. Flip the order so it does the right thing.
$parent_map[$hash] = array_reverse($parent_map[$hash]);
$hash_list[] = $hash;
$last = $hash;
}
$hash_list = array_reverse($hash_list);
$this->parents = $parent_map;
return $this->loadHistoryForCommitIdentifiers($hash_list);
}
}

View file

@ -20,6 +20,8 @@ final class DiffusionHistoryTableView extends DiffusionView {
private $history;
private $handles = array();
private $isHead;
private $parents;
public function setHistory(array $history) {
$this->history = $history;
@ -44,12 +46,28 @@ final class DiffusionHistoryTableView extends DiffusionView {
return array_keys($phids);
}
public function setParents(array $parents) {
$this->parents = $parents;
return $this;
}
public function setIsHead($is_head) {
$this->isHead = $is_head;
return $this;
}
public function render() {
$drequest = $this->getDiffusionRequest();
$handles = $this->handles;
$graph = null;
if ($this->parents) {
$graph = $this->renderGraph();
}
$rows = array();
$ii = 0;
foreach ($this->history as $history) {
$epoch = $history->getEpoch();
@ -79,6 +97,7 @@ final class DiffusionHistoryTableView extends DiffusionView {
array(
'commit' => $history->getCommitIdentifier(),
)),
$graph ? $graph[$ii++] : null,
self::linkCommit(
$drequest->getRepository(),
$history->getCommitIdentifier()),
@ -100,6 +119,7 @@ final class DiffusionHistoryTableView extends DiffusionView {
$view->setHeaders(
array(
'Browse',
'',
'Commit',
'Change',
'Date',
@ -110,6 +130,7 @@ final class DiffusionHistoryTableView extends DiffusionView {
$view->setColumnClasses(
array(
'',
'threads',
'n',
'',
'',
@ -117,7 +138,165 @@ final class DiffusionHistoryTableView extends DiffusionView {
'',
'wide',
));
$view->setColumnVisibility(
array(
true,
$graph ? true : false,
));
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 eventaully 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 $n => $thread_commit) {
if ($thread_commit == $parent) {
$found = true;
$splits[] = $n;
}
}
// 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,
);
}
// Render into tags for the behavior.
foreach ($graph as $k => $meta) {
$graph[$k] = javelin_render_tag(
'div',
array(
'sigil' => 'commit-graph',
'meta' => $meta,
),
'');
}
Javelin::initBehavior(
'diffusion-commit-graph',
array(
'count' => $count,
));
return $graph;
}
}

View file

@ -7,9 +7,12 @@
phutil_require_module('phabricator', 'applications/diffusion/view/base');
phutil_require_module('phabricator', 'infrastructure/javelin/api');
phutil_require_module('phabricator', 'infrastructure/javelin/markup');
phutil_require_module('phabricator', 'view/control/table');
phutil_require_module('phutil', 'markup');
phutil_require_module('phutil', 'utils');
phutil_require_source('DiffusionHistoryTableView.php');

View file

@ -127,6 +127,16 @@ span.single-display-line-content {
max-height: 64px;
}
.aphront-table-view td.threads {
font-family: monospace;
white-space: pre;
padding: 0;
}
.aphront-table-view td.threads canvas {
display: block;
}
.aphront-table-view th.aphront-table-view-sortable {
padding: 0;
}

View file

@ -0,0 +1,137 @@
/**
* @provides javelin-behavior-diffusion-commit-graph
* @requires javelin-behavior
* javelin-dom
* javelin-stratcom
*/
JX.behavior('diffusion-commit-graph', function(config) {
var nodes = JX.DOM.scry(document.body, 'div', 'commit-graph');
var cxt;
// Pick the color for column 'c'.
function color(c) {
var colors = [
'#cc0000',
'#cc0099',
'#6600cc',
'#0033cc',
'#00cccc',
'#00cc33',
'#66cc00',
'#cc9900',
];
return colors[c % colors.length];
}
// Stroke a line (for lines between commits).
function lstroke(c) {
cxt.lineWidth = 3;
cxt.strokeStyle = '#ffffff';
cxt.stroke();
cxt.lineWidth = 1;
cxt.strokeStyle = color(c);
cxt.stroke();
}
// Stroke with fill (for commit circles).
function fstroke(c) {
cxt.fillStyle = color(c);
cxt.strokeStyle = '#ffffff';
cxt.fill();
cxt.stroke();
}
for (var ii = 0; ii < nodes.length; ii++) {
var data = JX.Stratcom.getData(nodes[ii]);
var cell = 12; // Width of each thread.
function xpos(col) {
return (col * cell) + (cell / 2);
}
var h = 24;
var w = cell * config.count;
var canvas = JX.$N('canvas', {width: w, height: h});
cxt = canvas.getContext('2d');
cxt.lineWidth = 3;
// This gives us sharper lines, since lines drawn on an integer (like 5)
// are drawn from 4.5 to 5.5.
cxt.translate(0.5, 0.5);
cxt.strokeStyle = '#ffffff';
cxt.fillStyle = '#ffffff';
// First, figure out which column this commit appears in. It is marked by
// "o" (if it has a commit after it) or "^" (if no other commit has it as
// a parent). We use this to figure out where to draw the join/split lines.
var origin = null;
for (var jj = 0; jj < data.line.length; jj++) {
var c = data.line.charAt(jj);
switch (c) {
case 'o':
case '^':
origin = xpos(jj);
break;
}
}
// Draw all the join lines. These start at some column at the top of the
// canvas and join the commit's column. They indicate branching.
for (var jj = 0; jj < data.join.length; jj++) {
var join = data.join[jj];
var x = xpos(join);
cxt.beginPath();
cxt.moveTo(x, 0);
cxt.bezierCurveTo(x, h/4, origin, h/4, origin, h/2);
lstroke(join);
}
// Draw all the split lines. These start at the commit and end at some
// column on the bottom of the canvas. They indicate merging.
for (var jj = 0; jj < data.split.length; jj++) {
var split = data.split[jj];
var x = xpos(split);
cxt.beginPath();
cxt.moveTo(origin, h/2);
cxt.bezierCurveTo(origin, 3*h/4, x, 3*h/4, x, h);
lstroke(split);
}
// Draw the vertical lines (a branch with no activity at this commit) and
// the commit circles.
for (var jj = 0; jj < data.line.length; jj++) {
var c = data.line.charAt(jj);
switch (c) {
case 'o':
case '^':
origin = xpos(jj);
case '|':
cxt.beginPath();
cxt.moveTo(xpos(jj), (c == '^' ? h/2 : 0));
cxt.lineTo(xpos(jj), h);
lstroke(jj);
if (c == 'o' || c == '^') {
cxt.beginPath();
cxt.arc(xpos(jj), h/2, 3, 0, 2 * Math.PI, true);
fstroke(jj);
}
break;
}
}
JX.DOM.setContent(nodes[ii], canvas);
}
});