diff --git a/src/__celerity_resource_map__.php b/src/__celerity_resource_map__.php index 652e0d697f..9129bbf39d 100644 --- a/src/__celerity_resource_map__.php +++ b/src/__celerity_resource_map__.php @@ -82,7 +82,23 @@ celerity_register_resource_map(array( ), 'differential-core-view-css' => array( - 'path' => '/res/f750b85d/rsrc/css/application/differential/core.css', + 'path' => '/res/525d1a12/rsrc/css/application/differential/core.css', + 'type' => 'css', + 'requires' => + array( + ), + ), + 'differential-revision-detail-css' => + array( + 'path' => '/res/11a36dad/rsrc/css/application/differential/revision-detail.css', + 'type' => 'css', + 'requires' => + array( + ), + ), + 'differential-revision-history-css' => + array( + 'path' => '/res/755f3da3/rsrc/css/application/differential/revision-history.css', 'type' => 'css', 'requires' => array( @@ -90,7 +106,7 @@ celerity_register_resource_map(array( ), 'differential-table-of-contents-css' => array( - 'path' => '/res/ebf6641c/rsrc/css/application/differential/table-of-contents.css', + 'path' => '/res/a4a7b2b5/rsrc/css/application/differential/table-of-contents.css', 'type' => 'css', 'requires' => array( diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index cca9234785..94cf87f8f7 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -86,11 +86,14 @@ phutil_register_library_map(array( 'DifferentialReviewRequestMail' => 'applications/differential/mail/reviewrequest', 'DifferentialRevision' => 'applications/differential/storage/revision', 'DifferentialRevisionControlSystem' => 'applications/differential/constants/revisioncontrolsystem', + 'DifferentialRevisionDetailView' => 'applications/differential/view/revisiondetail', 'DifferentialRevisionEditController' => 'applications/differential/controller/revisionedit', 'DifferentialRevisionEditor' => 'applications/differential/editor/revision', 'DifferentialRevisionListController' => 'applications/differential/controller/revisionlist', 'DifferentialRevisionListData' => 'applications/differential/data/revisionlist', 'DifferentialRevisionStatus' => 'applications/differential/constants/revisionstatus', + 'DifferentialRevisionUpdateHistoryView' => 'applications/differential/view/revisionupdatehistory', + 'DifferentialRevisionViewController' => 'applications/differential/controller/revisionview', 'DifferentialUnitStatus' => 'applications/differential/constants/unitstatus', 'Javelin' => 'infratructure/javelin/api', 'LiskDAO' => 'storage/lisk/dao', @@ -232,8 +235,11 @@ phutil_register_library_map(array( 'DifferentialNewDiffMail' => 'DifferentialReviewRequestMail', 'DifferentialReviewRequestMail' => 'DifferentialMail', 'DifferentialRevision' => 'DifferentialDAO', + 'DifferentialRevisionDetailView' => 'AphrontView', 'DifferentialRevisionEditController' => 'DifferentialController', 'DifferentialRevisionListController' => 'DifferentialController', + 'DifferentialRevisionUpdateHistoryView' => 'AphrontView', + 'DifferentialRevisionViewController' => 'DifferentialController', 'PhabricatorAuthController' => 'PhabricatorController', 'PhabricatorConduitAPIController' => 'PhabricatorConduitController', 'PhabricatorConduitConnectionLog' => 'PhabricatorConduitDAO', diff --git a/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php b/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php index 3c76065a09..9f482deb13 100644 --- a/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php +++ b/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php @@ -77,6 +77,8 @@ class AphrontDefaultApplicationConfiguration ), '/api/(?[^/]+)$' => 'PhabricatorConduitAPIController', + + '/D(?\d+)' => 'DifferentialRevisionViewController', '/differential/' => array( '$' => 'DifferentialRevisionListController', 'filter/(?\w+)/$' => 'DifferentialRevisionListController', diff --git a/src/applications/differential/controller/revisionview/DifferentialRevisionViewController.php b/src/applications/differential/controller/revisionview/DifferentialRevisionViewController.php new file mode 100644 index 0000000000..cb1bcdff7c --- /dev/null +++ b/src/applications/differential/controller/revisionview/DifferentialRevisionViewController.php @@ -0,0 +1,1709 @@ +revisionID = $data['id']; + } + + public function processRequest() { + + $request = $this->getRequest(); + + $revision = id(new DifferentialRevision())->load($this->revisionID); + if (!$revision) { + return new Aphront404Response(); + } + + $revision->loadRelationships(); + + $diffs = $revision->loadDiffs(); + + $target = end($diffs); + + $changesets = $target->loadChangesets(); + + $object_phids = array_merge( + $revision->getReviewers(), + $revision->getCCPHIDs(), + array( + $revision->getOwnerPHID(), + $request->getUser()->getPHID(), + )); + + $handles = id(new PhabricatorObjectHandleData($object_phids)) + ->loadHandles(); + + $diff_history = new DifferentialRevisionUpdateHistoryView(); + $diff_history->setDiffs($diffs); + + $toc_view = new DifferentialDiffTableOfContentsView(); + $toc_view->setChangesets($changesets); + + $changeset_view = new DifferentialChangesetListView(); + $changeset_view->setChangesets($changesets); + + $revision_detail = new DifferentialRevisionDetailView(); + $revision_detail->setRevision($revision); + + $properties = $this->getRevisionProperties($revision, $target, $handles); + $revision_detail->setProperties($properties); + + $actions = $this->getRevisionActions($revision); + $revision_detail->setActions($actions); + + return $this->buildStandardPageResponse( + '
'. + $revision_detail->render(). + $diff_history->render(). + $toc_view->render(). + $changeset_view->render(). + '
', + array( + 'title' => $revision->getTitle(), + )); + } + + private function getRevisionProperties( + DifferentialRevision $revision, + DifferentialDiff $diff, + array $handles) { + + $properties = array(); + + $status = $revision->getStatus(); + $status = DifferentialRevisionStatus::getNameForRevisionStatus($status); + $properties['Revision Status'] = ''.$status.''; + + $author = $handles[$revision->getOwnerPHID()]; + $properties['Author'] = $author->renderLink(); + + $properties['Reviewers'] = $this->renderHandleLinkList( + array_select_keys( + $handles, + $revision->getReviewers())); + + $properties['CCs'] = $this->renderHandleLinkList( + array_select_keys( + $handles, + $revision->getCCPHIDs())); + + $path = $diff->getSourcePath(); + if ($path) { + $branch = $diff->getBranch() ? ' (' . $diff->getBranch() . ')' : ''; + $host = $diff->getSourceMachine(); + if ($host) { + $host .= ':'; + } + $properties['Path'] = phutil_escape_html("{$host}{$path} {$branch}"); + } + + + $properties['Lint'] = 'TODO'; + $properties['Unit'] = 'TODO'; + + return $properties; + } + + private function getRevisionActions(DifferentialRevision $revision) { + $viewer_phid = $this->getRequest()->getUser()->getPHID(); + $viewer_is_owner = ($revision->getOwnerPHID() == $viewer_phid); + $viewer_is_reviewer = in_array($viewer_phid, $revision->getReviewers()); + $viewer_is_cc = in_array($viewer_phid, $revision->getCCPHIDs()); + $status = $revision->getStatus(); + $revision_id = $revision->getID(); + $revision_phid = $revision->getPHID(); + + $links = array(); + + if ($viewer_is_owner) { + $links[] = array( + 'class' => 'revision-edit', + 'href' => "/differential/revision/edit/{$revision_id}/", + 'name' => 'Edit Revision', + ); + } + + if (!$viewer_is_owner && !$viewer_is_reviewer) { + $action = $viewer_is_cc ? 'rem' : 'add'; + $links[] = array( + 'class' => $viewer_is_cc ? 'subscribe-rem' : 'subscribe-add', + 'href' => "/differential/subscribe/{$action}/{$revision_id}/", + 'name' => $viewer_is_cc ? 'Unsubscribe' : 'Subscribe', + ); + } else { + $links[] = array( + 'class' => 'subscribe-rem unavailable', + 'name' => 'Automatically Subscribed', + ); + } + + $links[] = array( + 'class' => 'transcripts-metamta', + 'name' => 'MetaMTA Transcripts', + 'href' => "/mail/?phid={$revision_phid}", + ); + + return $links; + } + + + private function renderHandleLinkList(array $list) { + if (empty($list)) { + return 'None'; + } + return implode(', ', mpull($list, 'renderLink')); + } + +} +/* + + + $viewer_id = $this->getRequest()->getViewerContext()->getUserID(); + $viewer_is_owner = ($viewer_id == $revision->getOwnerID()); + $viewer_is_reviewer = + ((array_search($viewer_id, $revision->getReviewers())) !== false); + $viewer_is_cc = + ((array_search($viewer_id, $revision->getCCFBIDs())) !== false); + $status = $revision->getStatus(); + + $links = array(); + + if (!$viewer_is_owner && !$viewer_is_reviewer) { + $action = $viewer_is_cc + ? 'rem' + : 'add'; + $revision_id = $revision->getID(); + $href = "/differential/subscribe/{$action}/{$revision_id}"; + $links[] = array( + $viewer_is_cc ? 'subscribe-disabled' : 'subscribe-enabled', + {$viewer_is_cc ? 'Unsubscribe' : 'Subscribe'}, + ); + } else { + $links[] = array( + 'subscribe-disabled unavailable', + Automatically Subscribed, + ); + } + + $blast_uri = RedirectURI( + '/intern/differential/?action=tasks&fbid='.$revision->getFBID()) + ->setTier('intern'); + $links[] = array( + 'tasks', + Edit Tasks, + ); + + $engineering_repository_id = RepositoryRef::getByCallsign('E')->getID(); + $svn_revision = $revision->getSVNRevision(); + if ($status == DifferentialConstants::COMMITTED && + $svn_revision && + $revision->getRepositoryID() == $engineering_repository_id) { + $href = '/intern/push/request.php?rev='.$svn_revision; + $href = RedirectURI($href)->setTier('intern'); + $links[] = array( + 'merge', + Ask for Merge, + ); + } + + $links[] = array( + 'herald-transcript', + getFBID()} + >Herald Transcripts, + ); + $links[] = array( + 'metamta-transcript', + getFBID()} + >MetaMTA Transcripts, + ); + + + $list =
    ; + foreach ($links as $link) { + list($class, $tag) = $link; + $list->appendChild(
  • {$tag}
  • ); + } + + return $list; + + + +/* +// TODO +// $sandcastle = $this->getSandcastleURI($diff); +// if ($sandcastle) { +// $fields['Sandcastle'] = {$sandcastle}; +// } + + $path = $diff->getSourcePath(); + if ($path) { + $host = $diff->getSourceMachine(); + $branch = $diff->getGitBranch() ? ' (' . $diff->getGitBranch() . ')' : ''; + + if ($host) { +// TODO +// $user = $handles[$this->getRequest()->getViewerContext()->getUserID()] +// ->getName(); + $user = 'TODO'; + $fields['Path'] = + + {$host}:{$path}{$branch} + ; + } else { + $fields['Path'] = $path; + } + } + + $reviewer_links = array(); + foreach ($revision->getReviewers() as $reviewer) { + $reviewer_links[] = ; + } + if ($reviewer_links) { + $fields['Reviewers'] = array_implode(', ', $reviewer_links); + } else { + $fields['Reviewers'] = None; + } + + $ccs = $revision->getCCFBIDs(); + if ($ccs) { + $links = array(); + foreach ($ccs as $cc) { + $links[] = ; + } + $fields['CCs'] = array_implode(', ', $links); + } + + $blame_rev = $revision->getSvnBlameRevision(); + if ($blame_rev) { + if ($revision->getRepositoryRef() && is_numeric($blame_rev)) { + $ref = new RevisionRef($revision->getRepositoryRef(), $blame_rev); + $fields['Blame Revision'] = + getDetailURL())}> + {$ref->getName()} + ; + } else { + $fields['Blame Revision'] = $blame_rev; + } + } + + $tasks = $revision->getTaskHandles(); + + if ($tasks) { + $links = array(); + foreach ($tasks as $task) { + $links[] = ; + } + $fields['Tasks'] = array_implode(
    , $links); + } + + $bugzilla_id = $revision->getBugzillaID(); + if ($bugzilla_id) { + $href = 'http://bugs.developers.facebook.com/show_bug.cgi?id='. + $bugzilla_id; + $fields['Bugzilla'] = {'#'.$bugzilla_id}; + } + + $fields['Apply Patch'] = arc patch --revision {$revision->getID()}; + + if ($diff->getParentRevisionID()) { + $parent = id(new DifferentialRevision())->load( + $diff->getParentRevisionID()); + if ($parent) { + $fields['Depends On'] = + getURI()}> + D{$parent->getID()}: {$parent->getName()} + ; + } + } + + $star = {"\xE2\x98\x85"}; + + Javelin::initBehavior('differential-star-more'); + + switch ($diff->getLinted()) { + case Diff::LINT_FAIL: + $more = $this->renderDiffPropertyMoreLink($diff, 'lint'); + $fields['Lint'] = + + {$star} Lint Failures + {$more} + ; + break; + case Diff::LINT_WARNINGS: + $more = $this->renderDiffPropertyMoreLink($diff, 'lint'); + $fields['Lint'] = + + {$star} Lint Warnings + {$more} + ; + break; + case Diff::LINT_OKAY: + $fields['Lint'] = + {$star} Lint Free; + break; + default: + case Diff::LINT_NO: + $fields['Lint'] = + {$star} Not Linted; + break; + } + + $unit_details = false; + switch ($diff->getUnitTested()) { + case Diff::UNIT_FAIL: + $fields['Unit Tests'] = + {$star} Unit Test Failures; + $unit_details = true; + break; + case Diff::UNIT_WARN: + $fields['Unit Tests'] = + {$star} Unit Test Warnings; + $unit_details = true; + break; + case Diff::UNIT_OKAY: + $fields['Unit Tests'] = + {$star} Unit Tests Passed; + $unit_details = true; + break; + case Diff::UNIT_NO_TESTS: + $fields['Unit Tests'] = + {$star} No Test Coverage; + break; + case Diff::UNIT_NO: + default: + $fields['Unit Tests'] = + {$star} Not Unit Tested; + break; + } + + if ($unit_details) { + $fields['Unit Tests'] = + + {$fields['Unit Tests']} + {$this->renderDiffPropertyMoreLink($diff, 'unit')} + ; + } + + $platform_impact = $revision->getPlatformImpact(); + if ($platform_impact) { + $fields['Platform Impact'] = + {$platform_impact}; + } + + return $fields; + } + + +} + +/* + + + + protected function getSandcastleURI(Diff $diff) { + $uri = $this->getDiffProperty($diff, 'facebook:sandcastle_uri'); + if (!$uri) { + $uri = $diff->getSandboxURL(); + } + return $uri; + } + + protected function getDiffProperty(Diff $diff, $property, $default = null) { + $diff_id = $diff->getID(); + if (empty($this->diffProperties[$diff_id])) { + $props = id(new DifferentialDiffProperty()) + ->loadAllWhere('diffID = %s', $diff_id); + $dict = array_pull($props, 'getData', 'getName'); + $this->diffProperties[$diff_id] = $dict; + } + return idx($this->diffProperties[$diff_id], $property, $default); + } + + public function process() { + $uri = $this->getRequest()->getPath(); + if (starts_with($uri, '/d')) { + return ; + } + + $revision = id(new DifferentialRevision())->load($this->revisionID); + if (!$revision) { + throw new Exception("Bad revision ID."); + } + + $diffs = id(new Diff())->loadAllWhere( + 'revisionID = %d', + $revision->getID()); + $diffs = array_psort($diffs, 'getID'); + + $request = $this->getRequest(); + $new = $request->getInt('new'); + $old = $request->getInt('old'); + + if (($new || $old) && $new <= $old) { + throw new Exception( + "You can only view the diff of an older update relative to a newer ". + "update."); + } + + if ($new && empty($diffs[$new])) { + throw new Exception( + "The 'new' diff does not exist."); + } else if ($new) { + $diff = $diffs[$new]; + } else { + $diff = end($diffs); + if (!$diff) { + throw new Exception("No diff attached to this revision?"); + } + $new = $diff->getID(); + } + + $target_diff = $diff; + + if ($old && empty($diffs[$old])) { + throw new Exception( + "The 'old' diff does not exist."); + } + + $rows = array(array('Base', '', true, false, null, + $diff->getSourceControlBaseRevision() + ? $diff->getSourceControlBaseRevision() + : Master)); + $idx = 0; + foreach ($diffs as $cdiff) { + $rows[] = array( + 'Diff '.(++$idx), + $cdiff->getID(), + $cdiff->getID() != max(array_pull($diffs, 'getID')), + true, + $cdiff->getDateCreated(), + $cdiff->getDescription() + ? $cdiff->getDescription() + : No description available., + $cdiff->getUnitTested(), + $cdiff->getLinted()); + } + + $diff_table = + + + + + + + + + +
    DiffDiff IDDescriptionAgeLintUnit
    ; + $ii = 0; + + $old_ids = array(); + foreach ($rows as $row) { + $xold = null; + if ($row[2]) { + $lradio = = $new} + checked={$old == $row[1]} />; + if ($old == $row[1]) { + $xold = 'old-now'; + } + $old_ids[] = $lradio->requireUniqueID(); + } else { + $lradio = null; + } + $xnew = null; + if ($row[3]) { + $rradio = ; + if ($new == $row[1]) { + $xnew = 'new-now'; + } + } else { + $rradio = null; + } + + if ($row[3]) { + $unit_star = 'star-none'; + switch ($row[6]) { + case Diff::UNIT_FAIL: + case Diff::UNIT_WARN: $unit_star = 'star-warn'; break; + case Diff::UNIT_OKAY: $unit_star = 'star-okay'; break; + } + + $lint_star = 'star-none'; + switch ($row[7]) { + case Diff::LINT_FAIL: + case Diff::LINT_WARNINGS: $lint_star = 'star-warn'; break; + case Diff::LINT_OKAY: $lint_star = 'star-okay'; break; + } + + $star = "\xE2\x98\x85"; + + $unit_star = + + {$star} + ; + + $lint_star = + + {$star} + ; + } else { + $unit_star = null; + $lint_star = null; + } + + $diff_table->appendChild( + + {$row[0]} + {$row[1]} + {$row[5]} + {$row[4] ? ago(time() - $row[4]) : null} + {$lint_star} + {$unit_star} + {$lradio} + {$rradio} + ); + } + + Javelin::initBehavior('differential-diff-radios', array( + 'radios' => $old_ids, + )); + + $diff_table->appendChild( + + + + {id() + ->setOptions($actions)} + + + + + {$content} + + + + + + + + + {$preview} + ; + + $notice = null; + if ($this->getRequest()->getBool('diff_changed')) { + $notice = + + This revision was updated with a new diff while you + were providing feedback. Your inline comments appear on the + old diff. + ; + } + + return + getName()}> +
    + {$warning} + {$notice} + {$info} + + {$diff_table} + {$table_of_contents} + {$against_warn} + {$detail_view} + {$feedback_form} +
    +
    ; + } + + protected function getQuickLinks(DifferentialRevision $revision) { + + $viewer_id = $this->getRequest()->getViewerContext()->getUserID(); + $viewer_is_owner = ($viewer_id == $revision->getOwnerID()); + $viewer_is_reviewer = + ((array_search($viewer_id, $revision->getReviewers())) !== false); + $viewer_is_cc = + ((array_search($viewer_id, $revision->getCCFBIDs())) !== false); + $status = $revision->getStatus(); + + $links = array(); + + if (!$viewer_is_owner && !$viewer_is_reviewer) { + $action = $viewer_is_cc + ? 'rem' + : 'add'; + $revision_id = $revision->getID(); + $href = "/differential/subscribe/{$action}/{$revision_id}"; + $links[] = array( + $viewer_is_cc ? 'subscribe-disabled' : 'subscribe-enabled', + {$viewer_is_cc ? 'Unsubscribe' : 'Subscribe'}, + ); + } else { + $links[] = array( + 'subscribe-disabled unavailable', + Automatically Subscribed, + ); + } + + $blast_uri = RedirectURI( + '/intern/differential/?action=blast&fbid='.$revision->getFBID()) + ->setTier('intern'); + $links[] = array( + 'blast', + Blast Revision, + ); + + $blast_uri = RedirectURI( + '/intern/differential/?action=tasks&fbid='.$revision->getFBID()) + ->setTier('intern'); + $links[] = array( + 'tasks', + Edit Tasks, + ); + + if ($viewer_is_owner && false) { + $perflab_uri = RedirectURI( + '/intern/differential/?action=perflab&fbid='.$revision->getFBID()) + ->setTier('intern'); + $links[] = array( + 'perflab', + Run in Perflab, + ); + } + + $engineering_repository_id = RepositoryRef::getByCallsign('E')->getID(); + $svn_revision = $revision->getSVNRevision(); + if ($status == DifferentialConstants::COMMITTED && + $svn_revision && + $revision->getRepositoryID() == $engineering_repository_id) { + $href = '/intern/push/request.php?rev='.$svn_revision; + $href = RedirectURI($href)->setTier('intern'); + $links[] = array( + 'merge', + Ask for Merge, + ); + } + + $links[] = array( + 'herald-transcript', + getFBID()} + >Herald Transcripts, + ); + $links[] = array( + 'metamta-transcript', + getFBID()} + >MetaMTA Transcripts, + ); + + + $list =
      ; + foreach ($links as $link) { + list($class, $tag) = $link; + $list->appendChild(
    • {$tag}
    • ); + } + + return $list; + } + + protected function getDetailFields( + DifferentialRevision $revision, + Diff $diff, + array $handles) { + + $fields = array(); + $fields['Revision Status'] = $this->getRevisionStatusDisplay($revision); + + $author = $revision->getOwnerID(); + $fields['Author'] = ; + + $sandcastle = $this->getSandcastleURI($diff); + if ($sandcastle) { + $fields['Sandcastle'] = {$sandcastle}; + } + + $path = $diff->getSourcePath(); + if ($path) { + $host = $diff->getSourceMachine(); + $branch = $diff->getGitBranch() ? ' (' . $diff->getGitBranch() . ')' : ''; + + if ($host) { + $user = $handles[$this->getRequest()->getViewerContext()->getUserID()] + ->getName(); + $fields['Path'] = + + {$host}:{$path}{$branch} + ; + } else { + $fields['Path'] = $path; + } + } + + $reviewer_links = array(); + foreach ($revision->getReviewers() as $reviewer) { + $reviewer_links[] = ; + } + if ($reviewer_links) { + $fields['Reviewers'] = array_implode(', ', $reviewer_links); + } else { + $fields['Reviewers'] = None; + } + + $ccs = $revision->getCCFBIDs(); + if ($ccs) { + $links = array(); + foreach ($ccs as $cc) { + $links[] = ; + } + $fields['CCs'] = array_implode(', ', $links); + } + + $blame_rev = $revision->getSvnBlameRevision(); + if ($blame_rev) { + if ($revision->getRepositoryRef() && is_numeric($blame_rev)) { + $ref = new RevisionRef($revision->getRepositoryRef(), $blame_rev); + $fields['Blame Revision'] = + getDetailURL())}> + {$ref->getName()} + ; + } else { + $fields['Blame Revision'] = $blame_rev; + } + } + + $tasks = $revision->getTaskHandles(); + + if ($tasks) { + $links = array(); + foreach ($tasks as $task) { + $links[] = ; + } + $fields['Tasks'] = array_implode(
      , $links); + } + + $bugzilla_id = $revision->getBugzillaID(); + if ($bugzilla_id) { + $href = 'http://bugs.developers.facebook.com/show_bug.cgi?id='. + $bugzilla_id; + $fields['Bugzilla'] = {'#'.$bugzilla_id}; + } + + $fields['Apply Patch'] = arc patch --revision {$revision->getID()}; + + if ($diff->getParentRevisionID()) { + $parent = id(new DifferentialRevision())->load( + $diff->getParentRevisionID()); + if ($parent) { + $fields['Depends On'] = + getURI()}> + D{$parent->getID()}: {$parent->getName()} + ; + } + } + + $star = {"\xE2\x98\x85"}; + + Javelin::initBehavior('differential-star-more'); + + switch ($diff->getLinted()) { + case Diff::LINT_FAIL: + $more = $this->renderDiffPropertyMoreLink($diff, 'lint'); + $fields['Lint'] = + + {$star} Lint Failures + {$more} + ; + break; + case Diff::LINT_WARNINGS: + $more = $this->renderDiffPropertyMoreLink($diff, 'lint'); + $fields['Lint'] = + + {$star} Lint Warnings + {$more} + ; + break; + case Diff::LINT_OKAY: + $fields['Lint'] = + {$star} Lint Free; + break; + default: + case Diff::LINT_NO: + $fields['Lint'] = + {$star} Not Linted; + break; + } + + $unit_details = false; + switch ($diff->getUnitTested()) { + case Diff::UNIT_FAIL: + $fields['Unit Tests'] = + {$star} Unit Test Failures; + $unit_details = true; + break; + case Diff::UNIT_WARN: + $fields['Unit Tests'] = + {$star} Unit Test Warnings; + $unit_details = true; + break; + case Diff::UNIT_OKAY: + $fields['Unit Tests'] = + {$star} Unit Tests Passed; + $unit_details = true; + break; + case Diff::UNIT_NO_TESTS: + $fields['Unit Tests'] = + {$star} No Test Coverage; + break; + case Diff::UNIT_NO: + default: + $fields['Unit Tests'] = + {$star} Not Unit Tested; + break; + } + + if ($unit_details) { + $fields['Unit Tests'] = + + {$fields['Unit Tests']} + {$this->renderDiffPropertyMoreLink($diff, 'unit')} + ; + } + + $platform_impact = $revision->getPlatformImpact(); + if ($platform_impact) { + $fields['Platform Impact'] = + {$platform_impact}; + } + + return $fields; + } + + protected function renderDiffPropertyMoreLink(Diff $diff, $name) { + $target = ; + $meta = array( + 'target' => $target->requireUniqueID(), + 'uri' => '/differential/diffprop/'.$diff->getID().'/'.$name.'/', + ); + $more = + + · + Show Details + ; + return {$more}{$target}; + } + + + + protected function loadInlineComments(array $feedback, array &$changesets) { + + $inline_comments = array(); + $feedback_ids = array_filter(array_pull($feedback, 'getID')); + if (!$feedback_ids) { + return $inline_comments; + } + + $inline_comments = id(new DifferentialInlineComment()) + ->loadAllWhere('feedbackID in (%Ld)', $feedback_ids); + + $load_changesets = array(); + $load_hunks = array(); + foreach ($inline_comments as $inline) { + $changeset_id = $inline->getChangesetID(); + if (isset($changesets[$changeset_id])) { + continue; + } + $load_changesets[$changeset_id] = true; + } + + $more_changesets = array(); + if ($load_changesets) { + $changeset_ids = array_keys($load_changesets); + $more_changesets += id(new DifferentialChangeset()) + ->loadAllWithIDs($changeset_ids); + } + + if ($more_changesets) { + $changesets += $more_changesets; + $changesets = array_psort($changesets, 'getSortKey'); + } + + return $inline_comments; + } + + protected function getRevisionActions(DifferentialRevision $revision) { + $actions = array( + 'none' => true, + ); + + $viewer = $this->getRequest()->getViewerContext(); + + $viewer_is_owner = ($viewer->getUserID() == $revision->getOwnerID()); + if ($viewer_is_owner) { + switch ($revision->getStatus()) { + case DifferentialConstants::NEEDS_REVIEW: + $actions['abandon'] = true; + break; + case DifferentialConstants::NEEDS_REVISION: + $actions['abandon'] = true; + $actions['request_review'] = true; + break; + case DifferentialConstants::ACCEPTED: + $actions['abandon'] = true; + $actions['request_review'] = true; + break; + case DifferentialConstants::COMMITTED: + break; + case DifferentialConstants::ABANDONED: + $actions['reclaim'] = true; + break; + default: + throw new Exception('Unknown DifferentialRevision status.'); + } + } else { + switch ($revision->getStatus()) { + case DifferentialConstants::NEEDS_REVIEW: + $actions['accept'] = true; + $actions['reject'] = true; + break; + case DifferentialConstants::NEEDS_REVISION: + $actions['accept'] = true; + break; + case DifferentialConstants::ACCEPTED: + $actions['reject'] = true; + break; + case DifferentialConstants::COMMITTED: + break; + case DifferentialConstants::ABANDONED: + break; + default: + throw new Exception('Unknown DifferentialRevision status.'); + } + + if (in_array($viewer->getUserID(), $revision->getReviewers())) { + $actions['resign'] = true; + } + } + + // Put add reviewers at the bottom since it's rare relative to other + // actions, notably accept and reject + $actions['add_reviewers'] = true; + + static $action_names = array( + 'none' => 'Comment', + 'abandon' => 'Abandon Revision', + 'request_review' => 'Request Review', + 'reclaim' => 'Reclaim Revision', + 'accept' => "Accept Revision \xE2\x9C\x94", + 'reject' => "Request Changes \xE2\x9C\x98", + 'resign' => "Resign as Reviewer", + 'add_reviewers' => "Add Reviewers", + ); + + foreach ($actions as $key => $value) { + $actions[$key] = $action_names[$key]; + } + + return $actions; + } + + protected function getRevisionStatusDisplay(DifferentialRevision $revision) { + $viewer_id = $this->getRequest()->getViewerContext()->getUserID(); + $viewer_is_owner = ($viewer_id == $revision->getOwnerID()); + $status = $revision->getStatus(); + + $more = null; + switch ($status) { + case DifferentialConstants::NEEDS_REVIEW: + $message = 'Pending Review'; + break; + case DifferentialConstants::NEEDS_REVISION: + $message = 'Awaiting Revision'; + if ($viewer_is_owner) { + $more = 'Make the requested changes and update the revision.'; + } + break; + case DifferentialConstants::ACCEPTED: + $message = 'Ready for Commit'; + if ($viewer_is_owner) { + $more = + + Run arc commit (svn) or arc amend (git) to + proceed. + ; + } + break; + case DifferentialConstants::COMMITTED: + $message = 'Committed'; + $ref = $revision->getRevisionRef(); + $more = $ref + ? (getDetailURL())}> + {$ref->getName()} + ) + : null; + + $engineering_repository_id = RepositoryRef::getByCallsign('E')->getID(); + if ($revision->getSVNRevision() && + $revision->getRepositoryID() == $engineering_repository_id) { + Javelin::initBehavior( + 'differential-revtracker-status', + array( + 'uri' => '/differential/revtracker/'.$revision->getID().'/', + 'statusId' => 'revtracker_status', + 'mergeLinkId' => 'ask_for_merge_link', + )); + } + break; + case DifferentialConstants::ABANDONED: + $message = 'Abandoned'; + break; + default: + throw new Exception("Unknown revision status."); + } + + if ($more) { + $message = + + {$message} + · {$more} + ; + } else { + $message = {$message}; + } + + return $message; + } + + protected function renderFeedbackList(array $xhp, array $obj, $viewer_id) { + + // Use magical heuristics to try to hide older comments. + + $obj = array_reverse($obj); + $obj = array_values($obj); + $xhp = array_reverse($xhp); + $xhp = array_values($xhp); + + $last_comment = null; + foreach ($obj as $position => $feedback) { + if ($feedback->getUserID() == $viewer_id) { + if ($last_comment === null) { + $last_comment = $position; + } else if ($last_comment == $position - 1) { + // If you made consecuitive comments, show them all. This is a spaz + // rule for epriestley comments. + $last_comment = $position; + } + } + } + + $header = array(); + + $hide = array(); + if ($last_comment !== null) { + foreach ($obj as $position => $feedback) { + $action = $feedback->getAction(); + if ($action == 'testplan' || $action == 'summarize') { + // Always show summary and test plan. + $header[] = $xhp[$position]; + unset($xhp[$position]); + continue; + } + + if ($position <= $last_comment) { + // Always show comments after your last comment. + continue; + } + + if ($position < 3) { + // Always show the most recent 3 comments. + continue; + } + + // Hide everything else. + $hide[] = $position; + } + } + + if (count($hide) <= 3) { + // Don't hide if there's not much to hide. + $hide = array(); + } + + $header = array_reverse($header); + + $hidden = array_select_keys($xhp, $hide); + $visible = array_diff_key($xhp, $hidden); + + $visible = array_reverse($visible); + $hidden = array_reverse($hidden); + + if ($hidden) { + Javelin::initBehavior( + 'differential-show-all-feedback', + array( + 'markup' => id({$hidden})->toString(), + )); + $hidden = +
      +
      + {number_format(count($hidden))} older replies are hidden. + Show all feedback. +
      +
      ; + } else { + $hidden = null; + } + + return + + {$header} + {$hidden} + {$visible} + ; + } + +} + protected function getDetailFields( + DifferentialRevision $revision, + Diff $diff, + array $handles) { + + $fields = array(); + $fields['Revision Status'] = $this->getRevisionStatusDisplay($revision); + + $author = $revision->getOwnerID(); + $fields['Author'] = ; + + $sandcastle = $this->getSandcastleURI($diff); + if ($sandcastle) { + $fields['Sandcastle'] = {$sandcastle}; + } + + $path = $diff->getSourcePath(); + if ($path) { + $host = $diff->getSourceMachine(); + $branch = $diff->getGitBranch() ? ' (' . $diff->getGitBranch() . ')' : ''; + + if ($host) { + $user = $handles[$this->getRequest()->getViewerContext()->getUserID()] + ->getName(); + $fields['Path'] = + + {$host}:{$path}{$branch} + ; + } else { + $fields['Path'] = $path; + } + } + + $reviewer_links = array(); + foreach ($revision->getReviewers() as $reviewer) { + $reviewer_links[] = ; + } + if ($reviewer_links) { + $fields['Reviewers'] = array_implode(', ', $reviewer_links); + } else { + $fields['Reviewers'] = None; + } + + $ccs = $revision->getCCFBIDs(); + if ($ccs) { + $links = array(); + foreach ($ccs as $cc) { + $links[] = ; + } + $fields['CCs'] = array_implode(', ', $links); + } + + $blame_rev = $revision->getSvnBlameRevision(); + if ($blame_rev) { + if ($revision->getRepositoryRef() && is_numeric($blame_rev)) { + $ref = new RevisionRef($revision->getRepositoryRef(), $blame_rev); + $fields['Blame Revision'] = + getDetailURL())}> + {$ref->getName()} + ; + } else { + $fields['Blame Revision'] = $blame_rev; + } + } + + $tasks = $revision->getTaskHandles(); + + if ($tasks) { + $links = array(); + foreach ($tasks as $task) { + $links[] = ; + } + $fields['Tasks'] = array_implode(
      , $links); + } + + $bugzilla_id = $revision->getBugzillaID(); + if ($bugzilla_id) { + $href = 'http://bugs.developers.facebook.com/show_bug.cgi?id='. + $bugzilla_id; + $fields['Bugzilla'] = {'#'.$bugzilla_id}; + } + + $fields['Apply Patch'] = arc patch --revision {$revision->getID()}; + + if ($diff->getParentRevisionID()) { + $parent = id(new DifferentialRevision())->load( + $diff->getParentRevisionID()); + if ($parent) { + $fields['Depends On'] = + getURI()}> + D{$parent->getID()}: {$parent->getName()} + ; + } + } + + $star = {"\xE2\x98\x85"}; + + Javelin::initBehavior('differential-star-more'); + + switch ($diff->getLinted()) { + case Diff::LINT_FAIL: + $more = $this->renderDiffPropertyMoreLink($diff, 'lint'); + $fields['Lint'] = + + {$star} Lint Failures + {$more} + ; + break; + case Diff::LINT_WARNINGS: + $more = $this->renderDiffPropertyMoreLink($diff, 'lint'); + $fields['Lint'] = + + {$star} Lint Warnings + {$more} + ; + break; + case Diff::LINT_OKAY: + $fields['Lint'] = + {$star} Lint Free; + break; + default: + case Diff::LINT_NO: + $fields['Lint'] = + {$star} Not Linted; + break; + } + + $unit_details = false; + switch ($diff->getUnitTested()) { + case Diff::UNIT_FAIL: + $fields['Unit Tests'] = + {$star} Unit Test Failures; + $unit_details = true; + break; + case Diff::UNIT_WARN: + $fields['Unit Tests'] = + {$star} Unit Test Warnings; + $unit_details = true; + break; + case Diff::UNIT_OKAY: + $fields['Unit Tests'] = + {$star} Unit Tests Passed; + $unit_details = true; + break; + case Diff::UNIT_NO_TESTS: + $fields['Unit Tests'] = + {$star} No Test Coverage; + break; + case Diff::UNIT_NO: + default: + $fields['Unit Tests'] = + {$star} Not Unit Tested; + break; + } + + if ($unit_details) { + $fields['Unit Tests'] = + + {$fields['Unit Tests']} + {$this->renderDiffPropertyMoreLink($diff, 'unit')} + ; + } + + $platform_impact = $revision->getPlatformImpact(); + if ($platform_impact) { + $fields['Platform Impact'] = + {$platform_impact}; + } + + return $fields; + } + + +*/ diff --git a/src/applications/differential/controller/revisionview/__init__.php b/src/applications/differential/controller/revisionview/__init__.php new file mode 100644 index 0000000000..d724343cd1 --- /dev/null +++ b/src/applications/differential/controller/revisionview/__init__.php @@ -0,0 +1,23 @@ +getID()) { + return array(); + } + return id(new DifferentialDiff())->loadAllWhere( + 'revisionID = %d', + $this->getID()); + } + public function loadRelationships() { if (!$this->getID()) { $this->relationships = array(); diff --git a/src/applications/differential/storage/revision/__init__.php b/src/applications/differential/storage/revision/__init__.php index 641a9e397b..651e976961 100644 --- a/src/applications/differential/storage/revision/__init__.php +++ b/src/applications/differential/storage/revision/__init__.php @@ -7,6 +7,7 @@ phutil_require_module('phabricator', 'applications/differential/storage/base'); +phutil_require_module('phabricator', 'applications/differential/storage/diff'); phutil_require_module('phabricator', 'applications/phid/storage/phid'); phutil_require_module('phabricator', 'storage/qsprintf'); phutil_require_module('phabricator', 'storage/queryfx'); diff --git a/src/applications/differential/view/difftableofcontents/DifferentialDiffTableOfContentsView.php b/src/applications/differential/view/difftableofcontents/DifferentialDiffTableOfContentsView.php index 3130159981..f755c0571f 100644 --- a/src/applications/differential/view/difftableofcontents/DifferentialDiffTableOfContentsView.php +++ b/src/applications/differential/view/difftableofcontents/DifferentialDiffTableOfContentsView.php @@ -26,6 +26,8 @@ final class DifferentialDiffTableOfContentsView extends AphrontView { } public function render() { + + require_celerity_resource('differential-core-view-css'); require_celerity_resource('differential-table-of-contents-css'); $rows = array(); @@ -112,7 +114,7 @@ final class DifferentialDiffTableOfContentsView extends AphrontView { } return - '
      '. + '
      '. '

      Table of Contents

      '. ''. implode("\n", $rows). diff --git a/src/applications/differential/view/revisiondetail/DifferentialRevisionDetailView.php b/src/applications/differential/view/revisiondetail/DifferentialRevisionDetailView.php new file mode 100644 index 0000000000..6faf3a62de --- /dev/null +++ b/src/applications/differential/view/revisiondetail/DifferentialRevisionDetailView.php @@ -0,0 +1,90 @@ +revision = $revision; + return $this; + } + + public function setProperties(array $properties) { + $this->properties = $properties; + return $this; + } + + public function setActions(array $actions) { + $this->actions = $actions; + return $this; + } + + public function render() { + + require_celerity_resource('differential-core-view-css'); + require_celerity_resource('differential-revision-detail-css'); + + $revision = $this->revision; + + $rows = array(); + foreach ($this->properties as $key => $field) { + $rows[] = + ''. + ''. + ''. + ''; + } + + $properties = + '
      '.phutil_escape_html($key).':'.$field.'
      '. + implode("\n", $rows). + '
      '; + + $actions = array(); + foreach ($this->actions as $action) { + if (empty($action['href'])) { + $tag = 'span'; + } else { + $tag = 'a'; + } + $actions[] = phutil_render_tag( + $tag, + array( + 'href' => idx($action, 'href'), + 'class' => idx($action, 'class'), + ), + phutil_escape_html($action['name'])); + } + $actions = implode("\n", $actions); + + return + '
      '. + '
      '. + $actions. + '
      '. + '
      '. + '

      '.phutil_escape_html($revision->getTitle()).'

      '. + $properties. + '
      '. + '
      '. + '
      '; + } +} diff --git a/src/applications/differential/view/revisiondetail/__init__.php b/src/applications/differential/view/revisiondetail/__init__.php new file mode 100644 index 0000000000..9eb662528a --- /dev/null +++ b/src/applications/differential/view/revisiondetail/__init__.php @@ -0,0 +1,16 @@ +diffs = $diffs; + return $this; + } + + public function render() { + + require_celerity_resource('differential-core-view-css'); + require_celerity_resource('differential-revision-history-css'); + + $data = array( + array( + 'name' => 'Base', + 'id' => null, + 'desc' => 'Base', + 'old' => false, + 'new' => false, + 'age' => null, + 'lint' => null, + 'unit' => null, + ), + ); + + $seq = 0; + foreach ($this->diffs as $diff) { + $data[] = array( + 'name' => 'Diff '.(++$seq), + 'id' => $diff->getID(), + 'desc' => 'TODO',//$diff->getDescription(), + 'old' => false, + 'new' => false, + 'age' => $diff->getDateCreated(), + 'lint' => $diff->getLintStatus(), + 'unit' => $diff->getUnitStatus(), + ); + } + + $idx = 0; + $rows = array(); + foreach ($data as $row) { + + $name = phutil_escape_html($row['name']); + $id = phutil_escape_html($row['id']); + + $lint = '*'; + $unit = '*'; + $old = ''; + $new = ''; + + $desc = 'TODO'; + $age = '-'; + + if (++$idx % 2) { + $class = ' class="alt"'; + } else { + $class = null; + } + + $rows[] = + ''. + ''.$name.''. + ''.$id.''. + ''.$desc.''. + ''.$age.''. + ''.$lint.''. + ''.$unit.''. + ''.$old.''. + ''.$new.''. + ''; + } + + $select = ''; + + return + '
      '. + '

      Revision Update History

      '. + '
      '. + ''. + ''. + ''. + ''. + ''. + ''. + ''. + ''. + ''. + implode("\n", $rows). + ''. + ''. + ''. + '
      DiffIDDescriptionAgeLintUnit
      '. + ''. + ''. + '
      '. + '
      '. + '
      '; + } +} diff --git a/src/applications/differential/view/revisionupdatehistory/__init__.php b/src/applications/differential/view/revisionupdatehistory/__init__.php new file mode 100644 index 0000000000..6e88bf893d --- /dev/null +++ b/src/applications/differential/view/revisionupdatehistory/__init__.php @@ -0,0 +1,15 @@ +email; } - + public function renderLink() { + return phutil_render_tag( + 'a', + array( + 'href' => $this->getURI(), + ), + phutil_escape_html($this->getName())); + } } diff --git a/src/applications/phid/handle/__init__.php b/src/applications/phid/handle/__init__.php index e66e9c70a0..781f961e90 100644 --- a/src/applications/phid/handle/__init__.php +++ b/src/applications/phid/handle/__init__.php @@ -6,5 +6,7 @@ +phutil_require_module('phutil', 'markup'); + phutil_require_source('PhabricatorObjectHandle.php'); diff --git a/webroot/rsrc/css/application/differential/core.css b/webroot/rsrc/css/application/differential/core.css index f2aad4918a..fdc49a1959 100644 --- a/webroot/rsrc/css/application/differential/core.css +++ b/webroot/rsrc/css/application/differential/core.css @@ -7,3 +7,17 @@ max-width: 1162px; } +.differential-panel { + margin: 25px 0; + max-width: 1118px; + border: 1px solid #666622; + background: #efefdf; + padding: 15px 20px; + font-size: 13px; +} + +.differential-panel h1 { + border-bottom: 1px solid #aaaa99; + padding-bottom: 8px; + margin-bottom: 8px; +} diff --git a/webroot/rsrc/css/application/differential/revision-detail.css b/webroot/rsrc/css/application/differential/revision-detail.css new file mode 100644 index 0000000000..0e91516ae9 --- /dev/null +++ b/webroot/rsrc/css/application/differential/revision-detail.css @@ -0,0 +1,68 @@ +/** + * @provides differential-revision-detail-css + */ + +.differential-revision-properties { + font-size: 12px; + width: 100%; +} + +.differential-revision-properties tt { + letter-spacing: 1.1px; +} + +.differential-revision-properties th { + font-weight: bold; + width: 100px; + text-align: right; + padding: 3px 4px 3px 3px; + color: #333333; + white-space: nowrap; +} + +.differential-revision-properties td { + padding: 3px 2px; +} + +.differential-revision-actions { + float: right; + width: 250px; + background: #cfcfbf; + border: 1px solid #666622; + border-width: 0px 0px 1px 1px; + margin: -15px -20px 1em 0; + font-size: 11px; +} + +.differential-revision-detail-core { + margin-right: 265px; +} + +.differential-revision-actions a, +.differential-revision-actions span { + background-position: 8px center; + background-repeat: no-repeat; + display: block; + padding: 4px 4px 4px 32px; +} + +.differential-revision-actions span.unavailable { + color: #666666; + font-style: italic; +} + +.differential-revision-actions .subscribe-rem { + background-image: url(/rsrc/image/icon/unsubscribe.png); +} + +.differential-revision-actions .revision-edit { + background-image: url(/rsrc/image/icon/tango/edit.png); +} + +.differential-revision-actions .revision-edit { + background-image: url(/rsrc/image/icon/tango/edit.png); +} + +.differential-revision-actions .transcripts-metamta { + background-image: url(/rsrc/image/icon/tango/log.png); +} diff --git a/webroot/rsrc/css/application/differential/revision-history.css b/webroot/rsrc/css/application/differential/revision-history.css new file mode 100644 index 0000000000..8b20e8a6f3 --- /dev/null +++ b/webroot/rsrc/css/application/differential/revision-history.css @@ -0,0 +1,87 @@ +/** + * @provides differential-revision-history-css + */ + +.differential-revision-history-table { + width: 100%; + border-collapse: separate; + border-spacing: 1px; +} + +.differential-revision-history-table th { + padding: 0 .5em; + color: #666666; +} + +.differential-revision-history-table td { + padding: 4px; +} + +.differential-revision-history-table td { + white-space: nowrap; +} + +.differential-revision-history-table tr.alt { + background: #ddddcc; +} + + +.differential-revision-history-table td.revhistory-desc { + width: 100%; + white-space: normal; + padding-left: .5em; +} + +.differential-revision-history-table td.revhistory-name { + font-weight: bold; + padding-right: .5em; + padding-left: .5em; +} + +.differential-revision-history-table td.revhistory-age { + text-align: right; +} + +.differential-revision-history-table td.revhistory-old, +.differential-revision-history-table td.revhistory-new { + padding: 0em 1.5em; + text-align: center; +} + +.differential-revision-history-table td.revhistory-old { + background: #f9d0d0; +} + +.differential-revision-history-table td.revhistory-old-now { + background: #ffaaaa; +} + +.differential-revision-history-table td.revhistory-new { + background: #d0ffd0; +} + +.differential-revision-history-table td.revhistory-new-now { + background: #aaffaa; +} + +.differential-revision-history-table td.revhistory-star { + text-align: center; +} + + + + +.differential-revision-history-table td.diff-differ-submit { + text-align: right; + border-bottom: none; +} + +.differential-revision-history-table td.diff-differ-submit button { + margin-left: 1em; +} + +.differential-revision-history-table td.diff-differ-submit label { + font-weight: bold; + padding-right: .25em; + color: #444444; +} diff --git a/webroot/rsrc/css/application/differential/table-of-contents.css b/webroot/rsrc/css/application/differential/table-of-contents.css index a50c91705c..c79d38a955 100644 --- a/webroot/rsrc/css/application/differential/table-of-contents.css +++ b/webroot/rsrc/css/application/differential/table-of-contents.css @@ -2,15 +2,6 @@ * @provides differential-table-of-contents-css */ -.differential-toc { - margin: 25px 0; - max-width: 1118px; - border: 1px solid #666622; - background: #efefdf; - padding: 15px 20px; - font-size: 13px; -} - .differential-toc-meta { color: #666666; padding-left: 1em; @@ -32,9 +23,3 @@ .differential-toc-file { color: #444444; } - -.differential-toc h1 { - border-bottom: 1px solid #aaaa99; - padding-bottom: 8px; - margin-bottom: 8px; -} diff --git a/webroot/rsrc/image/icon/subscribe.png b/webroot/rsrc/image/icon/subscribe.png new file mode 100644 index 0000000000..34ec92ee5e Binary files /dev/null and b/webroot/rsrc/image/icon/subscribe.png differ diff --git a/webroot/rsrc/image/icon/tango/README b/webroot/rsrc/image/icon/tango/README new file mode 100644 index 0000000000..f18ea4955d --- /dev/null +++ b/webroot/rsrc/image/icon/tango/README @@ -0,0 +1,3 @@ +These icons come from the Tango Desktop Project: + + http://tango.freedesktop.org/ diff --git a/webroot/rsrc/image/icon/tango/edit.png b/webroot/rsrc/image/icon/tango/edit.png new file mode 100644 index 0000000000..188e1c12bd Binary files /dev/null and b/webroot/rsrc/image/icon/tango/edit.png differ diff --git a/webroot/rsrc/image/icon/tango/log.png b/webroot/rsrc/image/icon/tango/log.png new file mode 100644 index 0000000000..2d7f2d6017 Binary files /dev/null and b/webroot/rsrc/image/icon/tango/log.png differ diff --git a/webroot/rsrc/image/icon/unsubscribe.png b/webroot/rsrc/image/icon/unsubscribe.png new file mode 100644 index 0000000000..cd725e6e5c Binary files /dev/null and b/webroot/rsrc/image/icon/unsubscribe.png differ