<?php
/**
* SeekQuarry/Yioop --
* Open Source Pure PHP Search Engine, Crawler, and Indexer
*
* Copyright (C) 2009 - 2026 Chris Pollett chris@pollett.org
*
* LICENSE:
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* END LICENSE
*
* @author Chris Pollett chris@pollett.org
* @license https://www.gnu.org/licenses/ GPL3
* @link https://www.seekquarry.com/
* @copyright 2009 - 2026
* @filesource
*/
namespace seekquarry\yioop\library;
/**
* Holds and updates the record of a single issue tracked against a git
* repository wiki page. An issue record remembers who reported the issue,
* who it is assigned to, how urgent it is, whether it is still open, and a
* history of what happened to it and when. This class only shapes that
* record as plain data; saving the record and reading it back belong to the
* model layer, and turning ids or timestamps into readable words belongs to
* the view layer. Every method returns a fresh record rather than changing
* one in place, so a caller always works with the value it is handed back.
*
* @author Chris Pollett
*/
class WikiIssue
{
/**
* Marks an issue that still needs attention
*/
const STATUS_OPEN = "open";
/**
* Marks an issue that has been dealt with
*/
const STATUS_CLOSED = "closed";
/**
* Records that a closed issue was actually fixed
*/
const RESOLUTION_FIXED = "fixed";
/**
* Records that a closed issue was decided against rather than fixed
*/
const RESOLUTION_WONT_FIX = "wont_fix";
/**
* Display status of an open issue no one is working on yet
*/
const DISPLAY_REPORTED = "reported";
/**
* Display status of an open issue given to someone to work on
*/
const DISPLAY_ASSIGNED = "assigned";
/**
* Display status of a closed issue that was fixed
*/
const DISPLAY_FIXED = "marked_fixed";
/**
* Display status of a closed issue decided against rather than fixed
*/
const DISPLAY_WONT_FIX = "marked_wont_fix";
/**
* History entry name for when an issue was first reported
*/
const ACTION_OPENED = "opened";
/**
* History entry name for when an issue was given to someone
*/
const ACTION_ASSIGNED = "assigned";
/**
* History entry name for when an issue was closed
*/
const ACTION_CLOSED = "closed";
/**
* History entry name for when a closed issue was opened again
*/
const ACTION_REOPENED = "reopened";
/**
* History entry name for when an issue's urgency was changed
*/
const ACTION_PRIORITY = "priority";
/**
* Least urgent an issue can be
*/
const PRIORITY_LOW = 1;
/**
* Middle urgency, the level a newly reported issue starts at
*/
const PRIORITY_MEDIUM = 2;
/**
* Most urgent an issue can be
*/
const PRIORITY_HIGH = 3;
/**
* Makes the record of a newly reported issue: open, not yet assigned to
* anyone, at middle urgency because the reporter does not choose one,
* and carrying its title and the branch and version it was seen on. The
* first history entry says who reported it and when.
*
* @param int $reporter id of the person reporting the issue
* @param int $time when the issue was reported, as a Unix timestamp
* @param string $title short summary of the issue
* @param string $branch name of the branch the issue was seen on
* @param string $version the version the issue was seen on, either a
* release tag name or a commit name standing for the current code
* @return array the new issue record
*/
public static function open($reporter, $time, $title, $branch, $version)
{
return [
"reporter" => $reporter,
"assignee" => 0,
"priority" => self::PRIORITY_MEDIUM,
"status" => self::STATUS_OPEN,
"resolution" => "",
"title" => $title,
"branch" => $branch,
"version" => $version,
"history" => [[
"action" => self::ACTION_OPENED,
"by" => $reporter,
"time" => $time]]];
}
/**
* Gives an issue to a person to work on and notes the change in the
* issue's history.
*
* @param array $record the issue record to change
* @param int $assignee id of the person the issue is given to
* @param int $by id of the person making the change
* @param int $time when the change was made, as a Unix timestamp
* @return array the changed issue record
*/
public static function assign($record, $assignee, $by, $time)
{
$record["assignee"] = $assignee;
$record["history"][] = [
"action" => self::ACTION_ASSIGNED,
"by" => $by,
"to" => $assignee,
"time" => $time];
return $record;
}
/**
* Closes an issue, either as one that was fixed or as one that will not
* be acted on, and notes the change in the issue's history. An
* unrecognized reason is treated as fixed.
*
* @param array $record the issue record to change
* @param string $resolution how the issue was closed, either fixed or
* won't-fix
* @param int $by id of the person closing the issue
* @param int $time when the issue was closed, as a Unix timestamp
* @param string $commit name of the commit that fixed the issue, kept
* when there is one
* @return array the changed issue record
*/
public static function close($record, $resolution, $by, $time,
$commit = "")
{
$allowed = [self::RESOLUTION_FIXED, self::RESOLUTION_WONT_FIX];
if (!in_array($resolution, $allowed)) {
$resolution = self::RESOLUTION_FIXED;
}
$record["status"] = self::STATUS_CLOSED;
$record["resolution"] = $resolution;
$entry = [
"action" => self::ACTION_CLOSED,
"by" => $by,
"resolution" => $resolution,
"time" => $time];
if ($commit !== "") {
$record["fix_commit"] = $commit;
$entry["commit"] = $commit;
}
$record["history"][] = $entry;
return $record;
}
/**
* Opens a closed issue again so it can be worked on once more, clearing
* how it had been closed and noting the change in its history.
*
* @param array $record the issue record to change
* @param int $by id of the person reopening the issue
* @param int $time when the issue was reopened, as a Unix timestamp
* @return array the changed issue record
*/
public static function reopen($record, $by, $time)
{
$record["status"] = self::STATUS_OPEN;
$record["resolution"] = "";
$record["history"][] = [
"action" => self::ACTION_REOPENED,
"by" => $by,
"time" => $time];
return $record;
}
/**
* Puts an issue back to the reported state: open, held by no one, and
* with no closing reason, noting the change in its history. This is what
* choosing the reported status does when an issue was assigned or closed.
*
* @param array $record the issue record to change
* @param int $by id of the person making the change
* @param int $time when the change was made, as a Unix timestamp
* @return array the changed issue record
*/
public static function report($record, $by, $time)
{
$record["status"] = self::STATUS_OPEN;
$record["resolution"] = "";
$record["assignee"] = 0;
$record["history"][] = [
"action" => self::ACTION_OPENED,
"by" => $by,
"time" => $time];
return $record;
}
/**
* Changes how urgent an issue is and notes the change in its history.
*
* @param array $record the issue record to change
* @param int $priority the new urgency, a larger number meaning more
* urgent
* @param int $by id of the person making the change
* @param int $time when the change was made, as a Unix timestamp
* @return array the changed issue record
*/
public static function setPriority($record, $priority, $by, $time)
{
$record["priority"] = $priority;
$record["history"][] = [
"action" => self::ACTION_PRIORITY,
"by" => $by,
"priority" => $priority,
"time" => $time];
return $record;
}
/**
* Works out the single word that best describes where an issue stands,
* for the status column and the filter: a closed issue reads as marked
* fixed or marked won't fix by how it was closed, and an open issue reads
* as assigned when someone holds it or reported when no one does. A
* reopened issue is open again, so it reads as assigned or reported once
* more.
*
* @param array $record the issue record to weigh
* @return string one of the four display-status names
*/
public static function displayStatus($record)
{
if (($record["status"] ?? "") === self::STATUS_CLOSED) {
if (($record["resolution"] ?? "") === self::RESOLUTION_WONT_FIX) {
return self::DISPLAY_WONT_FIX;
}
return self::DISPLAY_FIXED;
}
if (!empty($record["assignee"])) {
return self::DISPLAY_ASSIGNED;
}
return self::DISPLAY_REPORTED;
}
/**
* Finds the user answerable for an issue's current status: the person
* it is assigned to while it is open and assigned, whoever last closed
* it once it is fixed or won't be fixed, and otherwise the person who
* reported it. This is the name the issue list shows next to the status
* so a reader can see at a glance who to look to.
*
* @param array $record the issue record
* @return int the user id answerable for the current status, 0 if none
*/
public static function statusUser($record)
{
if (($record["status"] ?? "") === self::STATUS_CLOSED) {
$history = $record["history"] ?? [];
for ($i = count($history) - 1; $i >= 0; $i--) {
if (($history[$i]["action"] ?? "") ===
self::ACTION_CLOSED) {
return (int)($history[$i]["by"] ?? 0);
}
}
return (int)($record["reporter"] ?? 0);
}
if (!empty($record["assignee"])) {
return (int)$record["assignee"];
}
return (int)($record["reporter"] ?? 0);
}
}