blob: a3faa78b6aa2c75425734cca4a1e766bad458588 [file] [log] [blame]
<?php
/*******************************************************************************
* Copyright (c) 2011 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 - initial API and implementation
* Wayne Beaton - Modified query to skip 'committers_only' bugs.
*******************************************************************************/
/*
* The Bug class is used by the contribution review tool to review
* bugs that either have been marked iplog+ or perhaps should be.
*
* TODO This class should be generalized.
*
* This file assumes that the $App variable has been defined.
*/
// TODO Generalize this
require_once($_SERVER['DOCUMENT_ROOT'] . "/projects/classes/Project.class.php");
require_once($_SERVER['DOCUMENT_ROOT'] . "/projects/classes/common.php");
require_once($_SERVER['DOCUMENT_ROOT'] . "/projects/classes/debug.php");
trace_file_info(__FILE__);
class Bug {
var $bug_id;
var $title;
var $product;
var $component;
var $target_milestone;
var $patches = array();
var $flags = array();
/**
* Cache the committers in case we need them.
* @see Bug::getComments()
*
* @var BugCommitter[]
*/
var $committers;
function getId() {
return $this->bug_id;
}
function getTitle() {
return $this->title;
}
function getProduct() {
return $this->product;
}
function getComponent() {
return $this->component;
}
function getTargetMilestone() {
return $this->target_milestone;
}
function getLink() {
return "https://bugs.eclipse.org/$this->bug_id";
}
function asHtml() {
$link = $this->getLink();
$text = "<a href=\"$link\">$this->bug_id</a> $this->product: $this->title";
if ($this->target_milestone) {
$text = "$text ($this->target_milestone)";
}
return $text;
}
function has_no_patch() {
return $this->count_patches() == 0;
}
function has_single_patch() {
return $this->count_patches() == 1;
}
function count_patches() {
return count($this->get_patches());
}
function get_patches() {
$patches = array();
foreach($this->patches as $patch) {
if ($patch->is_patch()) $patches[] = $patch;
}
return $patches;
}
function has_iplog_flag() {
foreach ($this->flags as $flag) {
if ($flag->is_iplog_flag()) return true;
}
return false;
}
function has_iplog_patch() {
foreach ($this->get_patches() as $patch) {
if ($patch->has_iplog_flag()) return true;
}
return false;
}
/*
* Answers true if all the patches come from a single contributor.
* Assumes that the receiver has at least one patch.
*/
function all_patches_from_same_contributor() {
$contributor = $this->patches[0]->contributor;
foreach($this->get_patches() as $patch) {
if (strcmp($patch->contributor, $contributor) != 0) return false;
}
return true;
}
function has_good_candidate_patch() {
if ($this->has_single_patch()) return true;
if ($this->all_patches_from_same_contributor()) return true;
return false;
}
/**
* Add recommendations to the given array. What do we recommend be
* done for this bug? Should the bug itself be marked iplog+, or
* one of the attachments? Perhaps the iplog flag should be
* cleared...
*/
function add_recommendations(&$recommendations) {
if ($this->has_no_patch()) return;
if ($this->has_iplog_flag()) $recommendations[] = new ClearBugFlagRecommendation($this);
foreach($this->get_patches() as $patch) {
$patch->addRecommendations($recommendations);
}
}
function get_link_string() {
return "<a target=\"_blank\" href=\"https://bugs.eclipse.org/$this->bug_id\">$this->bug_id</a>";
}
/**
* Get the comments for the bug. Note that this will issue an database request
* for the data. Note also that the results of this query are not cached.
*/
function getComments() {
global $App;
$sql = "select
longdescs.comment_id as id,
UNIX_TIMESTAMP(longdescs.bug_when) AS created,
LENGTH(longdescs.thetext) AS size,
profiles.login_name as login,
profiles.realname as realname
from longdescs
join profiles on (profiles.userid = longdescs.who)
where longdescs.bug_id = $this->bug_id";
debugMessage($sql);
$result = $App->bugzilla_sql( $sql );
$comments = array();
while( $row = mysql_fetch_assoc($result) ) {
$comment = new BugComment();
$comment->id = $row['id'];
$comment->creation = $row['created'];
$comment->size = $row['size'];
$comment->contributor = $row['login'];
$comment->realname = $row['realname'];
$comment->by_committer = is_project_committer($comment->contributor, $comment->creation, $this->committers);
$comments[] = $comment;
}
mysql_free_result($result);
return $comments;
}
}
class Patch {
var $attach_id;
var $description;
var $contributor;
var $realname;
var $size;
var $by_committer;
var $is_patch;
var $isObsolete;
var $creation;
var $flags = array();
/**
* We only consider an attachment a patch if it is marked as a patch and
* was contributed by a non-committer.
*/
function is_patch() {
if (!$this->is_patch) return false;
if ($this->by_committer) return false;
return true;
}
function isObsolete() {
return $this->isObsolete;
}
function has_iplog_flag($sign = '+') {
foreach ($this->flags as $flag) {
if ($flag->is_iplog_flag($sign)) return true;
}
return false;
}
function get_link_string() {
return "<a target=\"_blank\" href=\"https://bugs.eclipse.org/bugs/attachment.cgi?id=$this->attach_id&action=edit\">$this->attach_id</a>";
}
function addRecommendations(&$recommendations) {
if ($this->has_iplog_flag()) return;
if ($this->has_iplog_flag('-')) return;
if ($this->isObsolete()) return;
$recommendations[] = new MarkPatchFlagRecommendation($this);
}
}
class BugComment {
var $id;
var $creation;
var $size;
var $login;
var $realname;
var $by_committer;
}
class BugCommitter {
// TODO Consolidate this with the Committer class.
var $id;
var $email;
var $start;
var $end;
}
class Flag {
var $name;
var $value;
function is_iplog_flag($sign = '+') {
if ($this->name != 'iplog') return false;
if ($this->value != $sign) return false;
return true;
}
}
class Recommendation {
}
class ClearBugFlagRecommendation extends Recommendation {
var $bug;
function __construct($bug) {
$this->bug = $bug;
}
function get_html_string() {
return "Clear the iplog flag on bug " . $this->bug->get_link_string();
}
}
class MarkPatchFlagRecommendation extends Recommendation {
var $patch;
function __construct($patch) {
$this->patch = $patch;
}
function get_html_string() {
return "Set the iplog flag on attachment " . $this->patch->get_link_string() . " (or mark this attachment as obsolete).";
}
}
function find_bugs($projectid, &$committers = null){
global $App;
global $_GET;
if (!$committers) $committers = find_committers($projectid);
$project = get_project($projectid);
if (!$project) {
debugMessage("No project with id $projectid found!");
return array();
}
/*
* Obtain the product and components for the project. Note that
* the components are returned as an array of component names.
* We roll them into a single string with each component
* surrounded in single quites and separated from the previous
* component by a comma.
*/
$product = $project->getBugzillaProduct();
$components = $project->getBugzillaComponents();
if ($components) {
$components = "'" . implode("','", $components) . "'";
}
debugMessage('Bugzilla product is '
. ($product ? $product : 'not specified')
. ', components are '
. ($components ? $components : 'not specified')
. '.');
clear_project_cache();
// $keys = find_bugzilla_keys($projectid);
// $keys_list = '"' . implode('","', $keys) . '"';
$sql = "
SELECT
bugs.bug_id as bug_id,
bugs.short_desc as title,
products.name as product,
components.name as component,
bugflagtypes.name as bugflag_name,
bugflags.status as bugflag_value,
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 attach_contributor,
profiles.realname as attach_realname,
flagtypes.name as flag_name,
flags.status as flag_value
FROM
bugs
join products on (bugs.product_id = products.id)
join components on (bugs.component_id = components.id)
left join attachments on (bugs.bug_id = attachments.bug_id and attachments.isobsolete = 0)
left join attach_data on (attachments.attach_id = attach_data.id)
left join profiles on (attachments.submitter_id = profiles.userid)
left join flags on (attachments.attach_id = flags.attach_id)
left join flagtypes on (flags.type_id = flagtypes.id)
left join flags as bugflags on (bugs.bug_id = bugflags.bug_id and bugflags.attach_id is null)
left join flagtypes as bugflagtypes on (bugflags.type_id = bugflagtypes.id)
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 (flagtypes.name is null or flagtypes.name = 'iplog')
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))";
debugMessage($sql);
$result = $App->bugzilla_sql( $sql );
$bugs = array();
while( $row = mysql_fetch_assoc($result) ) {
$bug_id = $row['bug_id'];
$description = $row['description'];
if (strcmp($description, "mylyn/context/zip") == 0) continue;
if (strcmp($description, "mylar/context/zip") == 0) continue;
if (isset($bugs[$bug_id])) {
$bug = $bugs[$bug_id];
} else {
$bug = new Bug();
$bug->committers = $committers; // Cache the committers in case we need them.
$bug->bug_id = $bug_id;
$bug->title = $row['title'];
$bug->product = $row['product'];
$bug->component = $row['component'];
$bugs[$bug_id] = $bug;
}
$flag_name = $row['bugflag_name'];
if ($flag_name) {
$flag = new Flag();
$flag->name = $flag_name;
$flag->value = $row['bugflag_value'];
$bug->flags[$flag_name] = $flag;
//echo "Found bug flag $flag_name ($flag->value) on $bug_id<br/>";
}
$attach_id = $row['attach_id'];
if (!$attach_id) continue;
$patch = new Patch();
$patch->attach_id = $attach_id;
$patch->description = $row['description'];
$patch->contributor = $row['attach_contributor'];
$patch->realname = $row['attach_realname'];
$patch->size = $row['size'];
$patch->is_patch = $row['ispatch'];
$patch->isObsolete = $row['isobsolete'];
$creation = $row['creation'];
$patch->creation = strtotime($creation);
$flag_name = $row['flag_name'];
if ($flag_name) {
$flag = new Flag();
$flag->name = $flag_name;
$flag->value = $row['flag_value'];
$patch->flags[$flag_name] = $flag;
}
$patch->by_committer = (is_contributor_committer($patch, $committers));
$bug->patches[] = $patch;
}
mysql_free_result($result);
return $bugs;
}
function find_committers($projectid) {
global $App;
mustBeValidProjectId($projectid);
$sql = "SELECT
People.PersonId as id,
People.Email as email,
PeopleProjects.ActiveDate AS start,
PeopleProjects.InactiveDate AS end
FROM
People
join PeopleProjects on (People.PersonId = PeopleProjects.PersonId)
WHERE
PeopleProjects.Relation = 'CM'
AND PeopleProjects.ProjectId = '$projectid'";
//echo $sql;
$result = $App->foundation_sql( $sql );
$committers = array();
while( $row = mysql_fetch_assoc($result) ) {
$committer = new BugCommitter();
$committer->id = $row['id'];
$committer->email = $row['email'];
$committer->start = strtotime($row['start']);
$committer->end = strtotime($row['end']);
$committers[$committer->email] = $committer;
}
mysql_free_result($result);
return $committers;
}
function dump_committers() {
global $committers;
foreach ($committers as $committer) {
$start = date("Y.m.d", $committer->start);
$end = "present";
if ($committer->end) $end = date("Y.m.d", $committer->end);
echo "$committer->email ($start to $end)<br/>";
}
}
function dump_bugs(&$bugs) {
$count = count($bugs);
echo "<p>$count bugs</p>";
if ($count == 0) return;
$ids = array();
foreach ($bugs as $bug) $ids[] = $bug->bug_id;
$all = implode(',', $ids);
echo "<p>Review all in <a target=\"_blank\" href=\"https://bugs.eclipse.org/bugs/buglist.cgi?bug_id=$all\">Eclipse Bugzilla</a>.</p>";
echo "<ul>";
foreach ($bugs as $bug) {
echo "<li>";
dump_bug($bug);
echo "</li>";
$ids[] = $bug->bug_id;
}
echo "</ul>";
}
function dump_bug(&$bug) {
$product = htmlentities($bug->product);
$title = htmlentities($bug->title);
echo "$product <a target=\"_blank\" href=\"https://bugs.eclipse.org/$bug->bug_id\">$bug->bug_id</a> $title";
dump_flags($bug->flags);
if (count($bug->patches) == 0) {
echo " (no attachments)";
} else {
echo "<ul>";
foreach ($bug->patches as $patch) {
echo "<li>";
dump_patch($patch);
echo "</li>";
}
echo "</ul>";
}
}
function dump_patch(&$patch) {
$description = htmlentities($patch->description);
if ($patch->by_committer) echo "<strike>";
echo "<a target=\"_blank\" href=\"https://bugs.eclipse.org/bugs/attachment.cgi?id=$patch->attach_id&action=edit\">$patch->attach_id</a>";
//echo " ($patch->contributor)";
echo ": $description";
dump_flags($patch->flags);
if ($patch->is_patch) echo " (PATCH)";
echo " ";
echo date("Y.m.d", $patch->creation);
if ($patch->by_committer) echo "</strike>";
}
function dump_flags(&$flags) {
if (!$flags) return;
$collect = array();
foreach ($flags as $flag) {
$collect[] = $flag->name . $flag->value;
}
echo ' [' . implode(', ', $collect) . ']';
}
/*
* This function returns true if the contributor was a committer on the project
* at the time the patch was created.
*/
function is_contributor_committer(&$patch, &$committers) {
$contributor = $patch->contributor;
$creation = $patch->creation;
return is_project_committer($contributor, $creation, $committers);
}
/**
* @internal
* @param string $contributor email address
* @param unknown $creation
* @param unknown $committers
* @return boolean
*/
function is_project_committer($contributor, $creation, $committers) {
$trace = trace("Checking whether or not $contributor is a committer.");
if (!isset($committers[$contributor])) {
$trace->trace('No committer record found');
return false;
}
$trace->trace('Patch was created on ' . date('Y/m/d', $creation));
$committer = $committers[$contributor];
$trace->trace('Committer start date ' . date('Y/m/d', $committer->start));
if ($committer->start > $creation) {
$trace->trace('Committer status started after patch.');
return false;
}
if ($committer->end) {
$trace->trace('Committer end date ' . date('Y/m/d', $committer->end));
if ($committer->end < $creation) {
$trace->trace('Committer status ended before patch.');
return false;
}
}
$trace->trace ("$contributor was a committer at the time of contribution!");
return true;
}
/**
* This function returns an array containing Bug instances representing
* bugs that have been flagged as resolutions to 'security' issues.
*
* @return Bug[]
*/
function findResolvedSecurityBugs() {
// TODO Generalize this with #find_bugs
global $App;
$sql = "select
b.bug_id as bug_id,
p.name as product,
c.name as component,
b.short_desc as title,
b.target_milestone as target_milestone
from bugs as b
join products as p on (b.product_id=p.id)
join components as c on (b.component_id = c.id)
join keywords as k on (b.bug_id=k.bug_id)
join keyworddefs as kd on (k.keywordid=kd.id)
where
b.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 kd.name='security'
and b.bug_status IN ( 'RESOLVED', 'VERIFIED', 'CLOSED' )
and b.resolution IN ( 'FIXED' )";
$result = $App->bugzilla_sql($sql);
$bugs = array();
while( $row = mysql_fetch_assoc($result) ) {
$bug = new Bug();
$bug->bug_id = $row['bug_id'];
$bug->title = $row['title'];
$bug->product = $row['product'];
$bug->component = $row['component'];
$bug->target_milestone = $row['target_milestone'];
$bugs[] = $bug;
}
return $bugs;
}
?>