diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index e5c35c6903..d1123b3ead 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -258,6 +258,8 @@ phutil_register_library_map(array( 'DifferentialRevisionListData' => 'applications/differential/data/revisionlist', 'DifferentialRevisionListView' => 'applications/differential/view/revisionlist', 'DifferentialRevisionQuery' => 'applications/differential/query/revision', + 'DifferentialRevisionStatsController' => 'applications/differential/controller/revisionstats', + 'DifferentialRevisionStatsView' => 'applications/differential/view/revisionstats', 'DifferentialRevisionStatusFieldSpecification' => 'applications/differential/field/specification/revisionstatus', 'DifferentialRevisionUpdateHistoryView' => 'applications/differential/view/revisionupdatehistory', 'DifferentialRevisionViewController' => 'applications/differential/controller/revisionview', @@ -1076,6 +1078,8 @@ phutil_register_library_map(array( 'DifferentialRevisionIDFieldSpecification' => 'DifferentialFieldSpecification', 'DifferentialRevisionListController' => 'DifferentialController', 'DifferentialRevisionListView' => 'AphrontView', + 'DifferentialRevisionStatsController' => 'DifferentialController', + 'DifferentialRevisionStatsView' => 'AphrontView', 'DifferentialRevisionStatusFieldSpecification' => 'DifferentialFieldSpecification', 'DifferentialRevisionUpdateHistoryView' => 'AphrontView', 'DifferentialRevisionViewController' => 'DifferentialController', diff --git a/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php b/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php index 09be17df02..c6aacc4146 100644 --- a/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php +++ b/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php @@ -96,6 +96,7 @@ class AphrontDefaultApplicationConfiguration '/differential/' => array( '$' => 'DifferentialRevisionListController', 'filter/(?P\w+)/$' => 'DifferentialRevisionListController', + 'stats/(?P\w+)/$' => 'DifferentialRevisionStatsController', 'diff/' => array( '(?P\d+)/$' => 'DifferentialDiffViewController', 'create/$' => 'DifferentialDiffCreateController', diff --git a/src/applications/differential/controller/revisionstats/DifferentialRevisionStatsController.php b/src/applications/differential/controller/revisionstats/DifferentialRevisionStatsController.php new file mode 100644 index 0000000000..d3661512a5 --- /dev/null +++ b/src/applications/differential/controller/revisionstats/DifferentialRevisionStatsController.php @@ -0,0 +1,170 @@ +establishConnection('r'); + $rows = queryfx_all( + $conn_r, + 'SELECT revisions.* FROM %T revisions ' . + 'JOIN %T comments ON comments.revisionID = revisions.id ' . + 'JOIN (' . + ' SELECT revisionID FROM %T WHERE objectPHID = %s ' . + ' UNION ALL ' . + ' SELECT id from differential_revision WHERE authorPHID = %s) rel ' . + 'ON (comments.revisionID = rel.revisionID)' . + 'WHERE comments.action = %s' . + 'AND comments.authorPHID = %s', + $table->getTableName(), + id(new DifferentialComment())->getTableName(), + DifferentialRevision::RELATIONSHIP_TABLE, + $phid, + $phid, + $this->filter, + $phid + ); + return $table->loadAllFromArray($rows); + } + + private function loadComments($phid) { + $table = new DifferentialComment(); + $conn_r = $table->establishConnection('r'); + $rows = queryfx_all( + $conn_r, + 'SELECT comments.* FROM %T comments ' . + 'JOIN (' . + ' SELECT revisionID FROM %T WHERE objectPHID = %s ' . + ' UNION ALL ' . + ' SELECT id from differential_revision WHERE authorPHID = %s) rel ' . + 'ON (comments.revisionID = rel.revisionID)' . + 'WHERE comments.action = %s' . + 'AND comments.authorPHID = %s', + $table->getTableName(), + DifferentialRevision::RELATIONSHIP_TABLE, + $phid, + $phid, + $this->filter, + $phid + ); + + return $table->loadAllFromArray($rows); + } + public function willProcessRequest(array $data) { + $this->filter = idx($data, 'filter'); + } + + public function processRequest() { + $request = $this->getRequest(); + $user = $request->getUser(); + + if ($request->isFormPost()) { + $phid_arr = $request->getArr('view_user'); + $view_target = head($phid_arr); + return id(new AphrontRedirectResponse()) + ->setURI($request->getRequestURI()->alter('phid', $view_target)); + } + + $params = array_filter( + array( + 'phid' => $request->getStr('phid'), + )); + + // Fill in the defaults we'll actually use for calculations if any + // parameters are missing. + $params += array( + 'phid' => $user->getPHID(), + ); + + $side_nav = new AphrontSideNavFilterView(); + $side_nav->setBaseURI(id(new PhutilURI('/differential/stats/')) + ->alter('phid', $params['phid'])); + foreach (array( + DifferentialAction::ACTION_COMMIT, + DifferentialAction::ACTION_ACCEPT, + DifferentialAction::ACTION_REJECT, + DifferentialAction::ACTION_UPDATE, + DifferentialAction::ACTION_COMMENT, + ) as $action) { + $verb = ucfirst(DifferentialAction::getActionPastTenseVerb($action)); + $side_nav->addFilter($action, $verb); + } + $this->filter = + $side_nav->selectFilter($this->filter, + DifferentialAction::ACTION_COMMIT); + + $panels = array(); + $handles = id(new PhabricatorObjectHandleData(array($params['phid']))) + ->loadHandles(); + + $filter_form = id(new AphrontFormView()) + ->setAction('/differential/stats/'.$this->filter.'/') + ->setUser($user); + + $filter_form->appendChild( + $this->renderControl($params['phid'], $handles)); + $filter_form->appendChild(id(new AphrontFormSubmitControl()) + ->setValue('Filter Revisions')); + + $side_nav->appendChild($filter_form); + + $comments = $this->loadComments($params['phid']); + $revisions = $this->loadRevisions($params['phid']); + + $panel = new AphrontPanelView(); + $panel->setHeader('Differential rate analysis'); + $panel->appendChild( + id(new DifferentialRevisionStatsView()) + ->setComments($comments) + ->setRevisions($revisions) + ->setUser($user)); + $panels[] = $panel; + + foreach ($panels as $panel) { + $side_nav->appendChild($panel); + } + + return $this->buildStandardPageResponse( + $side_nav, + array( + 'title' => 'Differential statistics', + )); + } + + private function renderControl($view_phid, $handles) { + $value = array(); + if ($view_phid) { + $value = array( + $view_phid => $handles[$view_phid]->getFullName(), + ); + } + return id(new AphrontFormTokenizerControl()) + ->setDatasource('/typeahead/common/users/') + ->setLabel('View User') + ->setName('view_user') + ->setValue($value) + ->setLimit(1); + } + +} diff --git a/src/applications/differential/controller/revisionstats/__init__.php b/src/applications/differential/controller/revisionstats/__init__.php new file mode 100644 index 0000000000..6f6c662d0f --- /dev/null +++ b/src/applications/differential/controller/revisionstats/__init__.php @@ -0,0 +1,27 @@ +revisions = $revisions; + return $this; + } + + public function setComments(array $comments) { + $this->comments = $comments; + return $this; + } + public function setUser($user) { + $this->user = $user; + return $this; + } + + public function render() { + $user = $this->user; + if (!$user) { + throw new Exception("Call setUser() before render()!"); + } + + $id_to_revision_map = array(); + foreach ($this->revisions as $rev) { + $id_to_revision_map[$rev->getID()] = $rev; + } + $revisions_seen = array(); + + $dates = array(); + $counts = array(); + $lines = array(); + $boosts = array(); + $days_with_diffs = array(); + $count_active = array(); + $now = time(); + $row_array = array(); + + foreach (array( + '1 week', '2 weeks', '3 weeks', + '1 month', '2 months', '3 months', '6 months', '9 months', + '1 year', '18 months', + '2 years', '3 years', '4 years', '5 years', + ) as $age) { + $dates[$age] = strtotime($age . ' ago'); + $counts[$age] = 0; + $lines[$age] = 0; + $count_active[$age] = 0; + } + + foreach ($this->comments as $comment) { + $rev_date = $comment->getDateCreated(); + + $day = phabricator_date($rev_date, $user); + $old_daycount = idx($days_with_diffs, $day, 0); + $days_with_diffs[$day] = $old_daycount + 1; + + $rev_id = $comment->getRevisionID(); + + if (idx($revisions_seen, $rev_id)) { + continue; + } + $rev = $id_to_revision_map[$rev_id]; + $revisions_seen[$rev_id] = true; + + foreach ($dates as $age => $cutoff) { + if ($cutoff > $rev_date) { + continue; + } + if ($rev) { + $lines[$age] += $rev->getLineCount(); + } + $counts[$age]++; + if (!$old_daycount) { + $count_active[$age]++; + } + } + } + + $old_count = 0; + foreach ($dates as $age => $cutoff) { + $weeks = ($now - $cutoff + 0.) / (7 * 60 * 60 * 24); + if ($old_count == $counts[$age]) { + continue; + } + $old_count = $counts[$age]; + + $row_array[$age] = array( + 'Revisions per week' => number_format($counts[$age] / $weeks, 2), + 'Lines per week' => number_format($lines[$age] / $weeks, 1), + 'Active days per week' => + number_format($count_active[$age] / $weeks, 1), + 'Revisions' => number_format($counts[$age]), + 'Lines' => number_format($lines[$age]), + 'Lines per diff' => number_format($lines[$age] / + ($counts[$age] + 0.0001)), + 'Active days' => number_format($count_active[$age]), + ); + } + + $rows = array(); + $row_names = array_keys(head($row_array)); + foreach ($row_names as $row_name) { + $rows[] = array($row_name); + } + foreach (array_keys($dates) as $age) { + $i = 0; + foreach ($row_names as $row_name) { + $rows[$i][] = idx(idx($row_array, $age), $row_name, '-'); + ++$i; + } + } + + $table = new AphrontTableView($rows); + $table->setColumnClasses( + array( + 'wide pri', + )); + + $table->setHeaders( + array_merge( + array( + 'Metric', + ), + array_keys($dates))); + + return $table->render(); + } +} diff --git a/src/applications/differential/view/revisionstats/__init__.php b/src/applications/differential/view/revisionstats/__init__.php new file mode 100644 index 0000000000..77d6adde1d --- /dev/null +++ b/src/applications/differential/view/revisionstats/__init__.php @@ -0,0 +1,16 @@ +