563 lines
16 KiB
PHP
563 lines
16 KiB
PHP
<?php
|
|
|
|
require_once 'Logger.php';
|
|
require_once 'Models.php';
|
|
require_once 'Utils.php';
|
|
|
|
class EGGDatabase
|
|
{
|
|
const DB_NAME = "";
|
|
|
|
/** @var string */
|
|
private $path;
|
|
|
|
/* @var PDO */
|
|
private $pdo = null;
|
|
|
|
/* @var ILogger */
|
|
private $logger = null;
|
|
|
|
/**
|
|
* @param $path string
|
|
* @param $log ILogger
|
|
*/
|
|
public function __construct(string $path, ILogger $log)
|
|
{
|
|
$this->path = $path;
|
|
$this->logger = $log;
|
|
}
|
|
|
|
public function open()
|
|
{
|
|
$exists = file_exists($this->path);
|
|
|
|
if (!$exists) $this->logger->proclog("No database file found. Creating new at '" . $this->path . "'");
|
|
|
|
$dsn = "sqlite:" . $this->path;
|
|
$opt =
|
|
[
|
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
|
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
|
];
|
|
|
|
$this->pdo = new PDO($dsn, null, null, $opt);
|
|
|
|
if(!$exists) $this->init();
|
|
}
|
|
|
|
public function openReadOnly()
|
|
{
|
|
$this->open();
|
|
}
|
|
|
|
public function beginTransaction()
|
|
{
|
|
$this->pdo->beginTransaction();
|
|
}
|
|
|
|
public function commitTransaction()
|
|
{
|
|
$this->pdo->commit();
|
|
}
|
|
|
|
public function abortTransactionIfExists()
|
|
{
|
|
if ($this->pdo !== null && $this->pdo->inTransaction()) $this->pdo->rollBack();
|
|
}
|
|
|
|
private function init()
|
|
{
|
|
$this->logger->proclog("Initialize new database '" . $this->path . "'");
|
|
|
|
$this->query_from_file(__DIR__."/db_init.sql");
|
|
}
|
|
|
|
public function close()
|
|
{
|
|
$this->pdo = null; // https://stackoverflow.com/questions/18277233
|
|
}
|
|
|
|
/** @param $path string */
|
|
public function query_from_file(string $path)
|
|
{
|
|
$sql = file_get_contents($path);
|
|
|
|
$cmds = explode("/*----SPLIT----*/", $sql);
|
|
|
|
foreach ($cmds as $cmd) $this->pdo->exec($cmd);
|
|
}
|
|
|
|
public function sql_query_assoc(string $query)
|
|
{
|
|
$r = $this->pdo->query($query)->fetchAll(PDO::FETCH_ASSOC);
|
|
|
|
return $r;
|
|
}
|
|
|
|
public function sql_query_assoc_prep(string $query, array $params)
|
|
{
|
|
$stmt = $this->pdo->prepare($query);
|
|
|
|
foreach ($params as $p)
|
|
{
|
|
if (strpos($query, $p[0]) !== FALSE) $stmt->bindValue($p[0], $p[1], $p[2]);
|
|
}
|
|
|
|
$stmt->execute();
|
|
|
|
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
}
|
|
|
|
public function sql_query_assoc_pre_prep(PDOStatement $stmt, array $params)
|
|
{
|
|
$stmt->closeCursor();
|
|
|
|
foreach ($params as $p)
|
|
{
|
|
if (strpos($stmt->queryString, $p[0]) !== FALSE) $stmt->bindValue($p[0], $p[1], $p[2]);
|
|
}
|
|
|
|
$stmt->execute();
|
|
|
|
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
}
|
|
|
|
public function sql_exec_prep(string $query, array $params)
|
|
{
|
|
$stmt = $this->pdo->prepare($query);
|
|
|
|
foreach ($params as $p)
|
|
{
|
|
if (strpos($query, $p[0]) !== FALSE) $stmt->bindValue($p[0], $p[1], $p[2]);
|
|
}
|
|
|
|
$stmt->execute();
|
|
|
|
return $stmt->rowCount();
|
|
}
|
|
|
|
public function sql_exec_pre_prep(PDOStatement $stmt, array $params)
|
|
{
|
|
$stmt->closeCursor();
|
|
|
|
foreach ($params as $p)
|
|
{
|
|
if (str_contains($stmt->queryString, $p[0])) $stmt->bindValue($p[0], $p[1], $p[2]);
|
|
}
|
|
|
|
$stmt->execute();
|
|
|
|
return $stmt->rowCount();
|
|
}
|
|
|
|
/**
|
|
* @param string $url
|
|
* @param string $name
|
|
* @param string $source
|
|
* @return Repository
|
|
* @throws Exception
|
|
*/
|
|
public function getOrCreateRepository(string $url, string $name, string $source)
|
|
{
|
|
$repo = $this->sql_query_assoc_prep("SELECT * FROM repositories WHERE url = :url",
|
|
[
|
|
[":url", $url, PDO::PARAM_STR],
|
|
]);
|
|
|
|
if (count($repo) === 0)
|
|
{
|
|
$this->sql_exec_prep("INSERT INTO repositories (url, name, source, last_update, last_change) VALUES (:url, :nam, :src, :lu, :lc)",
|
|
[
|
|
[":url", $url, PDO::PARAM_STR],
|
|
[":nam", $name, PDO::PARAM_STR],
|
|
[":src", $source, PDO::PARAM_STR],
|
|
[":lu", Utils::sqlnow(), PDO::PARAM_STR],
|
|
[":lc", Utils::sqlnow(), PDO::PARAM_STR],
|
|
]);
|
|
|
|
$repo = $this->sql_query_assoc_prep("SELECT * FROM repositories WHERE url = :url",
|
|
[
|
|
[":url", $url, PDO::PARAM_STR],
|
|
]);
|
|
|
|
if (count($repo) === 0) throw new EGGException("No repo after insert [" . $source . "|" . $name . "]");
|
|
|
|
$this->logger->proclog("Inserted (new) repository [" . $source . "|" . $name . "] into database");
|
|
}
|
|
|
|
$r = new Repository();
|
|
$r->ID = $repo[0]['id'];
|
|
$r->URL = $repo[0]['url'];
|
|
$r->Name = $repo[0]['name'];
|
|
$r->Source = $repo[0]['source'];
|
|
$r->LastUpdate = $repo[0]['last_update'];
|
|
$r->LastChange = $repo[0]['last_change'];
|
|
return $r;
|
|
}
|
|
|
|
/**
|
|
* @param string $source
|
|
* @param Repository $repo
|
|
* @param string $name
|
|
* @return Branch
|
|
* @throws Exception
|
|
*/
|
|
public function getOrCreateBranch(string $source, Repository $repo, string $name)
|
|
{
|
|
$branch = $this->sql_query_assoc_prep("SELECT * FROM branches WHERE repo_id = :rid AND name = :nam",
|
|
[
|
|
[":rid", $repo->ID, PDO::PARAM_INT],
|
|
[":nam", $name, PDO::PARAM_STR],
|
|
]);
|
|
|
|
if (count($branch) === 0)
|
|
{
|
|
$this->sql_exec_prep("INSERT INTO branches (repo_id, name, head, last_update, last_change) VALUES (:rid, :nam, :sha, :lu, :lc)",
|
|
[
|
|
[":rid", $repo->ID, PDO::PARAM_INT],
|
|
[":nam", $name, PDO::PARAM_STR],
|
|
[":sha", null, PDO::PARAM_STR],
|
|
[":lu", Utils::sqlnow(), PDO::PARAM_STR],
|
|
[":lc", Utils::sqlnow(), PDO::PARAM_STR],
|
|
]);
|
|
|
|
$branch = $this->sql_query_assoc_prep("SELECT * FROM branches WHERE repo_id = :rid AND name = :nam",
|
|
[
|
|
[":rid", $repo->ID, PDO::PARAM_INT],
|
|
[":nam", $name, PDO::PARAM_STR],
|
|
]);
|
|
|
|
if (count($branch) === 0) throw new EGGException("No branch after insert [" . $source . "|" . $repo->Name . "|" . $name . "]");
|
|
|
|
$this->logger->proclog("Inserted (new) branch [" . $source . "|" . $repo->Name . "|" . $name . "] into database");
|
|
}
|
|
|
|
$r = new Branch();
|
|
$r->ID = $branch[0]['id'];
|
|
$r->Name = $branch[0]['name'];
|
|
$r->Repo = $repo;
|
|
$r->Head = $branch[0]['head'];
|
|
$r->LastUpdate = $branch[0]['last_update'];
|
|
$r->LastChange = $branch[0]['last_change'];
|
|
return $r;
|
|
}
|
|
|
|
/**
|
|
* @param string $source
|
|
* @param Repository $repo
|
|
* @param Branch $branch
|
|
* @param Commit[] $commits
|
|
*/
|
|
public function insertNewCommits(string $source, Repository $repo, Branch $branch, array $commits) {
|
|
$this->logger->proclog("Inserting " . count($commits) . " (new) commits into [" . $source . "|" . $repo->Name . "|" . $branch->Name . "]");
|
|
|
|
|
|
$stmtAddCommit = $this->pdo->prepare("INSERT INTO commits ([branch_id], [hash]) VALUES (:brid, :sha)");
|
|
$stmtAddMD = $this->pdo->prepare("INSERT OR IGNORE INTO metadata ([hash], [author_name], [author_email], [committer_name], [committer_email], [message], [date], [parent_commits]) VALUES (:sha, :an, :am, :cn, :cm, :msg, :dat, :prt)");
|
|
$stmtGetID = $this->pdo->prepare("SELECT id FROM commits WHERE [branch_id] = :brid AND [Hash] = :sha");
|
|
|
|
foreach ($commits as $commit)
|
|
{
|
|
$strparents = implode(";", $commit->Parents);
|
|
|
|
$this->sql_exec_pre_prep($stmtAddCommit,
|
|
[
|
|
[":brid", $branch->ID, PDO::PARAM_INT],
|
|
[":sha", $commit->Hash, PDO::PARAM_STR],
|
|
]);
|
|
|
|
$this->sql_exec_pre_prep($stmtAddMD,
|
|
[
|
|
[":sha", $commit->Hash, PDO::PARAM_STR],
|
|
[":an", $commit->AuthorName, PDO::PARAM_STR],
|
|
[":am", $commit->AuthorEmail, PDO::PARAM_STR],
|
|
[":cn", $commit->CommitterName, PDO::PARAM_STR],
|
|
[":cm", $commit->CommitterEmail, PDO::PARAM_STR],
|
|
[":msg", $commit->Message, PDO::PARAM_STR],
|
|
[":dat", $commit->Date, PDO::PARAM_STR],
|
|
[":prt", $strparents, PDO::PARAM_STR],
|
|
]);
|
|
|
|
$dbid = $this->sql_query_assoc_pre_prep($stmtGetID,
|
|
[
|
|
[":brid", $branch->ID, PDO::PARAM_INT],
|
|
[":sha", $commit->Hash, PDO::PARAM_STR],
|
|
]);
|
|
|
|
$commit->ID = $dbid[0]['id'];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param Branch $branch
|
|
* @param string $head
|
|
*/
|
|
public function setBranchHead(Branch $branch, string $head) {
|
|
$this->logger->proclog("Set HEAD of branch [" . $branch->Repo->Source . "|" . $branch->Repo->Name . "|" . $branch->Name . "] to {".substr($head, 0, 8)."}");
|
|
|
|
$this->sql_exec_prep("UPDATE branches SET head = :head WHERE id = :id",
|
|
[
|
|
[":id", $branch->ID, PDO::PARAM_INT],
|
|
[":head", $head, PDO::PARAM_STR],
|
|
]);
|
|
|
|
$branch->Head = $head;
|
|
}
|
|
|
|
public function setUpdateDateOnRepository(Repository $repo) {
|
|
$now = Utils::sqlnow();
|
|
$this->sql_exec_prep("UPDATE repositories SET last_update = :now WHERE id = :id",
|
|
[
|
|
[":id", $repo->ID, PDO::PARAM_INT],
|
|
[":now", $now, PDO::PARAM_STR],
|
|
]);
|
|
$repo->LastUpdate = $now;
|
|
}
|
|
|
|
public function setChangeDateOnRepository(Repository $repo) {
|
|
$now = Utils::sqlnow();
|
|
$this->sql_exec_prep("UPDATE repositories SET last_change = :now WHERE id = :id",
|
|
[
|
|
[":id", $repo->ID, PDO::PARAM_INT],
|
|
[":now", $now, PDO::PARAM_STR],
|
|
]);
|
|
$repo->LastChange = $now;
|
|
}
|
|
|
|
public function setUpdateDateOnBranch(Branch $branch) {
|
|
$now = Utils::sqlnow();
|
|
$this->sql_exec_prep("UPDATE branches SET last_update = :now WHERE id = :id",
|
|
[
|
|
[":id", $branch->ID, PDO::PARAM_INT],
|
|
[":now", $now, PDO::PARAM_STR],
|
|
]);
|
|
$branch->LastUpdate = $now;
|
|
}
|
|
|
|
public function setChangeDateOnBranch(Branch $branch) {
|
|
$now = Utils::sqlnow();
|
|
$this->sql_exec_prep("UPDATE branches SET last_change = :now WHERE id = :id",
|
|
[
|
|
[":id", $branch->ID, PDO::PARAM_INT],
|
|
[":now", $now, PDO::PARAM_STR],
|
|
]);
|
|
$branch->LastChange = $now;
|
|
}
|
|
|
|
/**
|
|
* @param string $source
|
|
* @param Repository $repo
|
|
* @param Branch[] $branches
|
|
*/
|
|
public function deleteOtherBranches(string $source, Repository $repo, array $branches)
|
|
{
|
|
$db = $this->sql_query_assoc_prep("SELECT id, repo_id, name FROM branches WHERE repo_id = :rid",
|
|
[
|
|
[":rid", $repo->ID, PDO::PARAM_STR]
|
|
]);
|
|
|
|
foreach ($db as $dbname)
|
|
{
|
|
$exist = false;
|
|
foreach ($branches as $brnch) if ($brnch->ID === $dbname['id']) $exist = true;
|
|
|
|
if (!$exist) $this->deleteBranchRecursive($source, $repo->Name, $dbname['name'], $dbname['id']);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param string $source
|
|
* @param Repository[] $repos
|
|
*/
|
|
public function deleteOtherRepositories(string $source, array $repos)
|
|
{
|
|
$db = $this->sql_query_assoc_prep("SELECT id, url, name FROM repositories WHERE source = :src",
|
|
[
|
|
[":src", $source, PDO::PARAM_STR]
|
|
]);
|
|
|
|
foreach ($db as $dbname)
|
|
{
|
|
$exist = false;
|
|
foreach ($repos as $rep) if ($rep->ID === $dbname['id']) $exist = true;
|
|
|
|
if (!$exist) $this->deleteRepositoryRecursive($source, $dbname['name'], $dbname['id']);
|
|
}
|
|
}
|
|
|
|
public function deleteDanglingCommitdata(string $name)
|
|
{
|
|
$hashes = $this->sql_query_assoc_prep("SELECT metadata.hash AS mdh FROM metadata LEFT JOIN commits ON metadata.hash = commits.hash WHERE commits.hash IS NULL", []);
|
|
|
|
if (count($hashes) === 0) return;
|
|
|
|
$this->logger->proclog("Deleting ".count($hashes)." dangling commits [" . $name . "] from database (no longer linked)");
|
|
|
|
$this->beginTransaction();
|
|
foreach ($hashes as $hash) {
|
|
$this->sql_query_assoc_prep("DELETE FROM metadata WHERE hash = :hash", [ [":hash", $hash['mdh'], PDO::PARAM_STR] ]);
|
|
}
|
|
$this->commitTransaction();
|
|
|
|
$this->logger->proclog("Succesfully deleted ".count($hashes)." dangling commits");
|
|
}
|
|
|
|
/**
|
|
* @param string $source
|
|
* @param string $name
|
|
* @param int $id
|
|
*/
|
|
private function deleteRepositoryRecursive(string $source, string $name, int $id)
|
|
{
|
|
$this->logger->proclog("Delete repository [".$source."|" . $name . "] from database (no longer exists)");
|
|
|
|
$branches = $this->sql_query_assoc_prep("SELECT id FROM branches WHERE repo_id = :rid", [ [":rid", $id, PDO::PARAM_INT] ]);
|
|
|
|
$this->sql_exec_prep("DELETE FROM repositories WHERE id = :id", [[":id", $id, PDO::PARAM_INT]]);
|
|
$this->sql_exec_prep("DELETE FROM branches WHERE repo_id = :rid", [[":rid", $id, PDO::PARAM_INT]]);
|
|
foreach ($branches as $branch) $this->sql_exec_prep("DELETE FROM commits WHERE branch_id = :bid", [[":bid", $branch["id"], PDO::PARAM_INT]]);
|
|
}
|
|
|
|
/**
|
|
* @param string $source
|
|
* @param string $reponame
|
|
* @param string $name
|
|
* @param int $id
|
|
*/
|
|
private function deleteBranchRecursive(string $source, string $reponame, string $name, int $id)
|
|
{
|
|
$this->logger->proclog("Delete branch [" . $source . "|" . $reponame . "|" . $name . "] from database (no longer exists)");
|
|
|
|
$this->sql_exec_prep("DELETE FROM branches WHERE id = :bid", [[":bid", $id, PDO::PARAM_INT]]);
|
|
$this->sql_exec_prep("DELETE FROM commits WHERE branch_id = :bid", [[":bid", $id, PDO::PARAM_INT]]);
|
|
}
|
|
|
|
/**
|
|
* @param Branch $branch
|
|
*/
|
|
public function deleteAllCommits(Branch $branch) {
|
|
$this->sql_exec_prep("DELETE FROM commits WHERE branch_id = :bid", [[":bid", $branch->ID, PDO::PARAM_INT]]);
|
|
}
|
|
|
|
public function deleteOldSources(array $sources)
|
|
{
|
|
$dbsources = $this->sql_query_assoc_prep("SELECT source FROM repositories GROUP BY source", []);
|
|
|
|
foreach ($dbsources as $dbsrc)
|
|
{
|
|
$exist = false;
|
|
foreach ($sources as $src) if ($src === $dbsrc['source']) $exist=true;
|
|
|
|
if (!$exist)
|
|
{
|
|
$this->logger->proclog("Delete source [" . $dbsrc['source'] . "] from database (no longer exists)");
|
|
$repos = $this->sql_query_assoc_prep("SELECT source,name,id FROM repositories WHERE source = :src", [ [":src", $dbsrc['source'], PDO::PARAM_STR] ]);
|
|
|
|
foreach ($repos as $r) $this->deleteRepositoryRecursive($r['source'], $r['name'], $r['id']);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param int $year
|
|
* @param string[] $identities
|
|
* @return array
|
|
*/
|
|
public function getCommitCountOfYearByDate(int $year, array $identities): array
|
|
{
|
|
$sql = file_get_contents(__DIR__ . "/db_queryyear.sql");
|
|
|
|
$cond = "(1=0)";
|
|
$prep =
|
|
[
|
|
[":year", "".$year, PDO::PARAM_STR]
|
|
];
|
|
$i=0;
|
|
foreach ($identities as $ident)
|
|
{
|
|
$cond .= " OR (mail1 = :_".$i."_)";
|
|
$prep []= [":_".$i."_", $ident, PDO::PARAM_STR];
|
|
$i++;
|
|
$cond .= " OR (mail2 = :_".$i."_)";
|
|
$prep []= [":_".$i."_", $ident, PDO::PARAM_STR];
|
|
$i++;
|
|
}
|
|
|
|
$sql = str_replace("/*{INDETITY_COND}*/", $cond, $sql);
|
|
|
|
$rows = $this->sql_query_assoc_prep($sql, $prep);
|
|
|
|
$r = [];
|
|
foreach ($rows as $row) $r[$row['commitdate']] = $row['count'];
|
|
|
|
return $r;
|
|
}
|
|
|
|
/**
|
|
* @return int[]
|
|
*/
|
|
public function getAllYears(): array
|
|
{
|
|
$rows = $this->sql_query_assoc("SELECT d FROM (SELECT cast(strftime('%Y', metadata.date) as decimal) AS d FROM commits LEFT JOIN metadata ON commits.hash = metadata.hash) GROUP BY d ORDER BY d");
|
|
$r = [];
|
|
foreach ($rows as $row) $r []= $row['d'];
|
|
return $r;
|
|
}
|
|
|
|
/**
|
|
* @return Commit[]
|
|
*/
|
|
public function getCommitsForBranch(Branch $branch): array
|
|
{
|
|
$rows = $this->sql_query_assoc_prep("SELECT metadata.*, commits.id AS commitid FROM commits LEFT JOIN metadata ON metadata.hash = commits.hash WHERE commits.branch_id = :bid",
|
|
[
|
|
[":bid", $branch->ID, PDO::PARAM_INT]
|
|
]);
|
|
|
|
$r = [];
|
|
foreach ($rows as $row)
|
|
{
|
|
$c = new Commit();
|
|
$c->ID = $row['commitid'];
|
|
$c->Branch = $branch;
|
|
$c->Hash = $row['hash'];
|
|
$c->AuthorName = $row['author_name'];
|
|
$c->AuthorEmail = $row['author_email'];
|
|
$c->CommitterName = $row['committer_name'];
|
|
$c->CommitterEmail = $row['committer_email'];
|
|
$c->Message = $row['message'];
|
|
$c->Date = $row['date'];
|
|
$c->Parents = array_filter(explode(';', $row['parent_commits']), fn($p) => $p !== '');
|
|
$r []= $c;
|
|
}
|
|
return $r;
|
|
}
|
|
|
|
/**
|
|
* @return Commit[]
|
|
*/
|
|
public function getCommitsForRepo(Repository $repo, Branch $branchValue): array
|
|
{
|
|
$rows = $this->sql_query_assoc_prep("SELECT DISTINCT metadata.* FROM branches INNER JOIN commits ON branches.id = commits.branch_id LEFT JOIN metadata ON metadata.hash = commits.hash WHERE branches.repo_id = :rid",
|
|
[
|
|
[":rid", $repo->ID, PDO::PARAM_INT]
|
|
]);
|
|
|
|
$r = [];
|
|
foreach ($rows as $row)
|
|
{
|
|
$c = new Commit();
|
|
$c->Branch = $branchValue;
|
|
$c->Hash = $row['hash'];
|
|
$c->AuthorName = $row['author_name'];
|
|
$c->AuthorEmail = $row['author_email'];
|
|
$c->CommitterName = $row['committer_name'];
|
|
$c->CommitterEmail = $row['committer_email'];
|
|
$c->Message = $row['message'];
|
|
$c->Date = $row['date'];
|
|
$c->Parents = array_filter(explode(';', $row['parent_commits']), fn($p) => $p !== '');
|
|
$r []= $c;
|
|
}
|
|
return $r;
|
|
}
|
|
} |