| <?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; |
| } |
| ?> |