Skip to content

Instantly share code, notes, and snippets.

@ohader
Last active December 10, 2015 11:38
Show Gist options
  • Save ohader/4428305 to your computer and use it in GitHub Desktop.
Save ohader/4428305 to your computer and use it in GitHub Desktop.
Forge Issue Analysis
<?php
/**
* Usage:
* php forge-analyse.php <date-from> [<date-until>]
* php forge-analyse.php 2012-12-24 2012-12-31
*
* @author Oliver Hader <[email protected]>
* @license GPL v2 or any later version
* @see http://forge.typo3.org/projects/typo3v4-core/wiki/FriendlyGhost
*/
class AnalyseIssues {
const ISSUES_Limit = 50;
const ISSUES_AllUrl = 'http://forge.typo3.org/projects/typo3v4-core/issues.json?status_id=*&limit=%d&offset=%d&sort=updated_on:desc';
const ISSUES_DetailUrl = 'http://forge.typo3.org/projects/typo3v4-core/issues/%d.json?include=journals,changesets';
const DATETIME_Addition = 'T00:00:00+01:00';
const STATUS_Patched = '8';
const STATUS_Resolved = '3';
/**
* @var DateTime
*/
protected $from;
/**
* @var DateTime
*/
protected $until;
/**
* @var boolean
*/
protected $verbose = FALSE;
/**
* @var array
*/
protected $issues = array(
'created' => array(),
'patched' => array(),
'resolved' => array(),
);
/**
* @var array
*/
protected $projects = array();
public function __construct(DateTime $from, DateTime $until = NULL) {
$this->from = $from;
$this->until = $until;
}
public function setVerbose($verbose) {
$this->verbose = (bool) $verbose;
}
public function execute() {
$this->getIssues();
$this->render();
}
protected function render() {
$issuesInProjects = array();
foreach ($this->issues as $status => $issues) {
foreach ($issues as $issue) {
$projectId = $issue['project']['id'];
if (!isset($this->projects[$projectId])) {
$this->projects[$projectId] = $issue['project']['name'];
$issuesInProjects[$projectId] = array(
'created' => array(),
'patched' => array(),
'resolved' => array(),
);
}
$issuesInProjects[$projectId][$status][] = $issue;
}
}
ksort($issuesInProjects);
echo PHP_EOL;
echo str_repeat('=', 70) . PHP_EOL;
echo 'Issues from ' . $this->from->format('Y-m-d') . ($this->until ? ' until ' . $this->until->format('Y-m-d') : '') . PHP_EOL;
echo str_repeat('=', 70) . PHP_EOL;
foreach ($issuesInProjects as $projectId => $issuesByStatus) {
echo PHP_EOL;
echo $this->projects[$projectId] . PHP_EOL;
echo str_repeat('-', 70) . PHP_EOL;
foreach ($issuesByStatus as $status => $issues) {
echo ' * ' . $status . ': ' . count($issues) . PHP_EOL;
}
}
}
protected function getIssues() {
$offset = 0;
do {
$continue = FALSE;
$json = file_get_contents($this->getIssuesAllUrl(self::ISSUES_Limit, $offset));
$data = json_decode($json, TRUE);
if (empty($data['issues']) || !is_array($data['issues'])) {
break;
}
foreach ($data['issues'] as $issue) {
$id = $issue['id'];
$createdOn = new DateTime($issue['created_on']);
$updatedOn = new DateTime($issue['updated_on']);
$issue['updated_on'] = $updatedOn;
$issue['created_on'] = $createdOn;
// Collect if created in date range
if ($createdOn >= $this->from && ($this->until == NULL || $createdOn <= $this->until)) {
$this->issues['created'][$id] = $issue;
$continue = TRUE;
}
// Don't limit to range, since update might be ongoing,
// thus only use recent status on the accordant range
if ($updatedOn >= $this->from) {
$recentStatus = $this->getRecentStatus($id);
if ($recentStatus === self::STATUS_Resolved) {
$this->issues['resolved'][$id] = $issue;
$continue = TRUE;
}
if ($recentStatus === self::STATUS_Patched) {
$this->issues['patched'][$id] = $issue;
$continue = TRUE;
}
}
}
$offset += self::ISSUES_Limit;
} while ($continue);
}
/**
* @param integer $id
* @return NULL|array
*/
protected function getRecentStatus($id) {
$recentStatus = NULL;
$json = file_get_contents($this->getIssuesDetailUrl($id));
$data = json_decode($json, TRUE);
if (!empty($data['issue']['journals']) && is_array($data['issue']['journals'])) {
foreach ($data['issue']['journals'] as $journal) {
$createdOn = new DateTime($journal['created_on']);
if ($createdOn >= $this->from && ($this->until == NULL || $createdOn <= $this->until)) {
$statusDetail = $this->findInJournalDetails($journal['details'], 'status_id');
if ($statusDetail !== NULL && !empty($statusDetail['new_value'])) {
$recentStatus = $statusDetail['new_value'];
}
}
}
}
return $recentStatus;
}
protected function findInJournalDetails(array $details, $name) {
$result = NULL;
foreach ($details as $detail) {
if ($detail['name'] === $name) {
$result = $detail;
break;
}
}
return $result;
}
protected function getIssuesAllUrl($limit, $offset) {
$url = sprintf(self::ISSUES_AllUrl, $limit, $offset);
if ($this->verbose) {
echo '[FETCH] ' . $url . PHP_EOL;
}
return $url;
}
protected function getIssuesDetailUrl($id) {
$url = sprintf(self::ISSUES_DetailUrl, $id);
if ($this->verbose) {
echo '[FETCH] ' . $url . PHP_EOL;
}
return $url;
}
}
if (empty($argv[1])) {
echo 'Usage:' . PHP_EOL;
echo $argv[0] . ' <date-from> [<date-until>]' . PHP_EOL;
echo 'Example:' . PHP_EOL;
echo $argv[0] . ' 2012-12-24' . PHP_EOL;
} else {
$from = new DateTime($argv[1] . AnalyseIssues::DATETIME_Addition);
if (!empty($argv[2])) {
$until = new DateTime($argv[2] . AnalyseIssues::DATETIME_Addition);
} else {
$until = NULL;
}
$analyse = new AnalyseIssues($from, $until);
$analyse->setVerbose(FALSE);
$analyse->execute();
}
?>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment