blob: 414628c8a6077672f0149660e31204c8598d812c [file] [log] [blame]
<?php
/*******************************************************************************
* Copyright (c) 2011, 2012 Eclipse Foundation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Wayne Beaton (Eclipse Foundation)- initial API and implementation
*******************************************************************************/
/*
* This script assumes that it is being included by another script. We
* assume that the $App variable has already been defined.
*/
require_once(dirname(__FILE__) ."/Project.class.php");
require_once(dirname(__FILE__) ."/Committer.class.php");
require_once(dirname(__FILE__) . "/debug.php");
require_once(dirname(__FILE__) . "/common.php");
trace_file_info(__FILE__);
// require_once($_SERVER['DOCUMENT_ROOT'] . "/eclipse.org-common/system/app.class.php");
// $App = new App();
// $log = new IPLog(array(getProject('eclipse.orion')));
// print_r($log->getContributions());
//
//echo get_trace_html();
class IPLog {
var $projects;
var $committers = array();
var $contributors = null;
var $trace;
function __construct($projects) {
$this->projects = $projects;
$this->trace = trace("IPLog");
}
function getLicenses(&$projectids) {
global $App;
$sql = "SELECT distinct l.LicenseID as Id, l.Description as License FROM ProjectLicenses as p join (SYS_Licenses as l) on (p.LicenseId = l.LicenseId) where p.ProjectId IN ( '" . implode( "','", $projectids ) . "')";
log_query($sql);
$result = $App->foundation_sql( $sql );
//mysql_error_check();
$licenses = array();
while( $row = mysql_fetch_assoc($result) ) {
$id = $row['Id'];
$license = $row['License'];
$licenses[$id] = $license;
}
return $licenses;
}
function getContributions() {
if ($this->contributors === null) {
$this->gatherGitContributions($this->projects);
$this->gatherBugzillaContributions($this->projects);
}
uasort($this->contributors, 'contributors_sort_by_name');
return $this->contributors;
}
/**
* Gather any contributions provided through Git for $project. Contributions
* are added to the provided array. This function makes a call to the dash
* server for each Git repository listed by the project. The script running
* on the server provides the data in CSV form.
*
* @internal
* @param Project $project
* @param Contribution[] $contributions
*/
function gatherGitContributions(&$projects) {
global $App;
// $repositories = array();
// foreach($projects as $project) {
// foreach($project->getSourceRepositories() as $repository) {
// if ($repository->getType() != 'git') continue;
// $path = preg_replace('/^\/gitroot\//', '/home/data/git/', $repository->getPath());
// $repositories[$path] = $repository;
// }
// }
$ids = array();
foreach($projects as $project) {
$ids[] = "'" . $project->getId() . "'";
}
$ids = join(',', $ids);
/*
* Select records from the repository.
*
* Note that we only want those records that could possibly be contributed
* by a non-committer. Candidate records either have an author that is
* different from the committer, or--if they are from projects that use
* Gerrit--have a changeId. Note that these records are not necessarily
* from non-committers, but they could be.
*/
// $sql = "
// select
// repo, ref, committer, author, authorName, size, comment, commitDate as date
// from gitlogcache
// where
// repo in ('" . implode("','", array_keys($repositories)) . "')
// and author not in ('cvs', 'cvs2git')
// and authorName not in ('cvs2svn', 'cvs2git migration')";
$sql = "select distinct
r.project as project, r.path as repo, c.ref as ref, a.name as authorName, a.email as author,
sum(f.added)+sum(f.removed) as size, c.date as date, c.comment
from GitRepo as r
join GitCommit as c on r.path=c.path
join GitCommitAuthor as a on c.path=a.path and c.ref=a.ref
join GitCommitFile as f on c.path=f.path and c.ref=f.ref
where project in ($ids)
and a.email not in ('cvs', 'cvs2git')
and a.name not in ('cvs2svn', 'cvs2git migration')
group by c.ref";
$result = $App->dashboard_sql($sql);
while ($row = mysql_fetch_assoc($result)) {
$project = $row['project'];
$repo = $row['repo'];
$ref = $row['ref'];
// $committer = strtolower($row['committer']);
$authorName = $row['authorName'];
$author = $row['author'];
// HACK: When importing from CVS, some older projects
// ended up with many commits done by then-committers
// having just the committer id in both the author and
// author name fields. Some of the commits pre-date our
// record keeping and so do not have committer records
// in the database.
// FIXME: Fix the committer records in the database.
if ($authorName == $author)
if (preg_match('/^[\w]*$/', $author)) continue;
if (preg_match('/^[[:alnum:]]+$/', $author)) continue;
$author = strtolower($author);
$size = $row['size'] . ' lines';
$comment = $this->getAbbreviatedGitComment($row['comment']);
$date = $row['date'];
if ($this->isProjectCommitter($projects, $author, $date)) continue;
$link = $this->getRefLink($project, $repo, $ref);
// $repository = $repositories[$repo];
// $path = $repository->getLink();
// $link = "$path/commit?id=$ref";
// TODO Determine the link.
$this->addContribution($author, $authorName, $ref, substr($ref,0,6), $link, $size, $comment);
}
}
function getRefLink($project, $repo, $ref) {
// FIXME Handle LocationTech and PolarSys
if (preg_match('/\/gitroot(\/.*)$/', $repo, $matches)) {
$path = $matches[1];
return "http://git.eclipse.org/c$path/commit?id=$ref";
} elseif (preg_match('/^\/home\/data\/gitmirrors\/github\/([^\/]*)\/(.+)\.git$/', $repo, $matches)) {
$path = $matches[2];
return "https://github.com/eclipse/$path/commit/$ref";
}
return null;
}
function getAbbreviatedGitComment($string) {
$lines = split("\n", $string);
return $lines[0];
}
/**
* @internal
* @param unknown_type $author
* @param unknown_type $name
* @param unknown_type $ref
* @param unknown_type $id
* @param unknown_type $link
* @param unknown_type $size
* @param unknown_type $comment
*/
function addContribution($author, $name, $ref, $id, $link, $size, $comment) {
$contributor = $this->addContributor($author, $name);
if (isset($contributor->contributions[$ref]))
$contributor->contributions[$ref]->size += $size;
else
$contributor->contributions[$ref] = new Contribution($id, $link, $author, $size, $comment);
}
function addContributor($email, $name) {
if (!isset($this->contributors[$email])) {
$this->contributors[$email] = new Contributor($email, $name);
}
return $this->contributors[$email];
}
/**
* @internal
* @param unknown_type $projects
*/
function gatherBugzillaContributions(&$projects) {
foreach($projects as $project) {
IPLogBugzillaBuilder::assemble($this, $project);
}
}
function isProjectCommitter(&$projects, $author, $date) {
foreach($projects as $project) {
$committer = $this->findProjectCommitter($project, $author);
if (!$committer) continue;
if ($committer->isCommitter($project->getId(), $date)) return true;
}
return false;
}
/**
* Attempt to find a committer on a particular project. The value of
* $author may be a committer id, or it could be an email address.
*
* @param Project $project
* @param string $author
* @return Committer|NULL
*/
function findProjectCommitter(&$project, $author) {
if (!isset($this->committers[$project->getId()]))
$this->committers[$project->getId()] = getCommittersForProject($project->getId(), false);
foreach($this->committers[$project->getId()] as $committer) {
if ($committer->id == $author) return $committer;
if ($committer->hasEmailAddress($author)) return $committer;
}
return null;
}
function getSourceRepositories() {
$repositories = array();
foreach($this->projects as $project)
foreach($project->getSourceRepositories() as $repository)
$repositories[] = $repository;
return $repositories;
}
}
class Contributor {
var $name;
var $email;
var $contributions = array();
function __construct($email, $name) {
$this->email = $email;
$this->name = $name;
}
}
class Contribution {
var $ref;
var $link;
var $author;
var $size;
var $comments;
function __construct($ref, $link, $author, $size, $comments) {
$this->ref = $ref;
$this->link = $link;
$this->author = $author;
$this->size = $size;
$this->comments = $comments;
}
}
function contributors_sort_by_name($a, $b) {
return strcasecmp($a->name, $b->name);
}
/**
* The IPLogBugzillaBuilder queries Bugzilla to populate the log.
*
* @internal
*/
class IPLogBugzillaBuilder {
/* @var IPLog */
var $iplog;
var $project;
var $corrections = array();
static function assemble(&$iplog, &$project) {
$builder = new self($iplog, $project);
$trace = trace("Assemble Bugzilla records for " . $project->getName());
$builder->loadCorrections();
$builder->assembleAttachmentContributions($trace);
$builder->assembleCommentContributions($trace);
}
function __construct(&$iplog, &$project) {
$this->iplog = $iplog;
$this->project = $project;
$this->committers = array();
foreach(getCommittersForProject($project->getId()) as $committer)
$this->committers[$committer->email] = $committer;
}
/**
* We no longer support corrections, but there are a mess of them in the database already,
* so we should honour what's there. At least for comments.
*
* @internal
*/
function loadCorrections() {
global $App;
$id = $this->project->getId();
$sql = "SELECT ItemType as type, Action as action, ItemID as id FROM IPLogCorrections where ProjectId = '$id'";
$result = $App->foundation_sql( $sql );
while($row = mysql_fetch_assoc($result)) {
$this->corrections[$row['type']][$row['action']][$row['id']] = 'remove';
}
}
/**
* Query the Bugzilla database for iplog+ flagged attachments in the project.
* Assemble contributions from the results.
*
* Skip attachments made by committers.
*
* @internal
* @param Tracer $trace
*/
function assembleAttachmentContributions($trace) {
global $App;
$product = $this->project->getBugzillaProduct();
$components = $this->project->getBugzillaComponents();
if ($components)
$components = "'" . implode("','", $components) . "'";
$sql = "
SELECT
bugs.bug_id as bug_id,
bugs.short_desc as title,
products.name as product,
components.name as component,
attachments.attach_id as attach_id,
attachments.description as description,
attachments.isobsolete as isobsolete,
attachments.ispatch as ispatch,
attachments.creation_ts as creation,
LENGTH(attach_data.thedata) AS size,
profiles.login_name as contributor,
profiles.realname as realname
FROM
bugs
join products on (bugs.product_id = products.id)
join components on (bugs.component_id = components.id)
join attachments on (bugs.bug_id = attachments.bug_id and attachments.isobsolete = 0)
join attach_data on (attachments.attach_id = attach_data.id)
join profiles on (attachments.submitter_id = profiles.userid)
join flags on (attachments.attach_id = flags.attach_id and flags.status = '+')
join flagtypes on (flags.type_id = flagtypes.id and flagtypes.name = 'iplog')
WHERE
products.name = '$product'
and bugs.bug_id not in (select bg.bug_id from bug_group_map as bg join groups as g on (bg.group_id=g.id and g.name = 'Security_Advisories'))
and bugs.bug_status IN ( 'RESOLVED', 'VERIFIED', 'CLOSED' )
and bugs.resolution IN ( 'FIXED' )";
if ($components)
$sql .= " and bugs.component_id in (SELECT id FROM components WHERE product_id=products.id and name in ($components))";
$trace->trace($sql);
$result = $App->bugzilla_sql( $sql );
while( $row = mysql_fetch_assoc($result) ) {
$bugId = $row['bug_id'];
$author = $row['contributor'];
$creation = $row['creation'];
$attachId = $row['attach_id'];
// if (isset($this->corrections['CONTRIB']['RMA'][$attachId])) continue;
$trace->trace("Reviewing attachment by $author bug $bugId attachment $attachId");
if ($this->isCommitter($author, $creation)) continue;
$name = $row['realname'];
$ref = "$bugId#a$attachId";
$size = ($attachId == 104976 ? 1543319 : $row['size']) . ' bytes';
$link = "https://bugs.eclipse.org/$bugId";
$comment = $row['title'] . "\nAttachment $attachId: " . $row['description'];
$trace->trace("Attachment contribution from $author bug $bugId attachment $attachId");
$this->iplog->addContribution($author, $name, $ref, $bugId, $link, $size, $comment);
}
}
/**
* Query the Bugzilla database for iplog+ flagged bugs in the project.
* Assemble contributions from the comments in those bugs.
*
* Skip comments made by committers.
* Skip comments indicated in the corrections table (Foundation database).
*
* @internal
* @param Tracer $trace
*/
function assembleCommentContributions($trace) {
global $App;
$product = $this->project->getBugzillaProduct();
$components = $this->project->getBugzillaComponents();
if ($components)
$components = "'" . implode("','", $components) . "'";
$sql = "
SELECT
bugs.bug_id as bug_id,
bugs.short_desc as title,
products.name as product,
components.name as component,
flagtypes.name as bugflag_name,
flags.status as bugflag_value,
longdescs.comment_id as comment_id,
longdescs.bug_when as creation,
LENGTH(longdescs.thetext) as size,
profiles.login_name as contributor,
profiles.realname as realname
FROM
bugs
join products on (bugs.product_id = products.id)
join components on (bugs.component_id = components.id)
join flags on (bugs.bug_id = flags.bug_id and flags.attach_id is null)
join flagtypes on (flags.type_id = flagtypes.id)
join longdescs on (bugs.bug_id = longdescs.bug_id)
join profiles on (profiles.userid = longdescs.who)
WHERE
products.name = '$product'
and flagtypes.name = 'iplog'
and flags.status = '+'
and bugs.bug_id not in (select bg.bug_id from bug_group_map as bg join groups as g on (bg.group_id=g.id and g.name = 'Security_Advisories'))
and bugs.bug_status IN ( 'RESOLVED', 'VERIFIED', 'CLOSED' )
and bugs.resolution IN ( 'FIXED' )";
if ($components)
$sql .= " and bugs.component_id in (SELECT id FROM components WHERE product_id=products.id and name in ($components))";
$trace->trace($sql);
$result = $App->bugzilla_sql( $sql );
$bugs = array();
while( $row = mysql_fetch_assoc($result) ) {
$bugId = $row['bug_id'];
$author = $row['contributor'];
$creation = $row['creation'];
$commentId = $row['comment_id'];
$trace->trace("Reviewing comment by $author bug $bugId comment $commentId");
if (isset($this->corrections['CONTRIB']['RMC'][$commentId])) continue;
if ($this->isCommitter($author, $creation)) continue;
$name = $row['realname'];
$ref = "$bugId#c";
$size = $row['size'] . ' bytes';
$link = "https://bugs.eclipse.org/$bugId";
$comment = $row['title'] . "\n" . 'Comment';
$trace->trace("Comment contribution from $author bug $bugId comment $commentId");
$this->iplog->addContribution($author, $name, $ref, $bugId, $link, $size, $comment);
}
}
/**
* Answers true if the author was/is a committer on the given date.
*
* @internal
* @param string $author email address
* @param string $when date in a strtotime-parseable format
* @return boolean
*/
private function isCommitter($author, $when) {
if (!isset($this->committers[$author])) return false;
return $this->committers[$author]->isCommitter($this->project->getId(), $when);
}
}
?>