blob: aeebb12409a1007869ac678f976a9bde6f0bccc1 [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 getCommitters() {
global $App;
$committers = array();
$ids = array();
foreach($this->projects as $project) {
$ids[] = "'" . $project->getForgeId() . "'";
}
$ids = join(',', $ids);
$sql = "
select
cm.id as PID,
cm.first as FName, cm.last as LName,
min(cp.activeDate) as activeDate, max(cp.inactiveDate) as inactiveDate,
ca.orgId, ca.orgName as Name1, '' as Name2
from Committer as cm
join CommitterProject as cp on cp.id=cm.id and cp.project in ($ids)
left join CommitterAffiliation as ca on ca.id=cm.id
group by cm.id";
$this->trace->trace($sql);
$result = $App->dashboard_sql( $sql );
while( $row = mysql_fetch_assoc($result) ) {
$row['Comment'] = '';
$committers[ $row['PID'] ] = $row;
}
uasort( $committers, 'sortcms' );
return $committers;
}
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->getForgeId() . "'";
}
$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')";
/*
* HACK
* Projects that predate our record keeping of committers (e.g.
* Eclipse Platform, CDT) may have commits that appear to be
* contributions from committers that we do not have proper
* records for. For every project that once used CVS or SVN
* any commits that have been migrated from those systems
* must have been made by authorized committers. With this in
* mind, we just ignore any commits that predate an arbitrarily-
* selected date after which we're pretty confident that the
* database is correct ('2008-06-25').
*/
$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', 'webmaster@eclipse.org')
and a.name not in ('cvs2svn', 'cvs2git migration', 'e4Build', 'SDK Build')
and c.date > date('2008-06-25')
and not (r.project='tools.cdt' and c.date < date('2011-06-25'))
group by c.ref";
$this->trace->trace($sql);
$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->getForgeId(), $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->getForgeId()]))
$this->committers[$project->getForgeId()] = getCommittersForProject($project->getForgeId(), false);
foreach($this->committers[$project->getForgeId()] 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->getForgeId()) 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->getForgeId();
$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->getForgeId(), $when);
}
}
?>