This repository has been archived on 2020-05-24. You can view files and clone it, but cannot push or open issues or pull requests.
codesite/codesite2git

335 lines
12 KiB
PHP
Executable File

#!/usr/bin/php
<?php
require __DIR__.'/lib/bootstrap.php';
class CProjectGitExporter extends CProject {
function __construct(string $projdirname, string $projname) {
CProject::__construct($projdirname, $projname);
$this->allowText2Html = false; // Don't produce HTML when processing the download block
}
function ExportAsGit(string $dest) {
// Create destination directory
if (is_dir($dest)) {
throw new Exception("Destination path '{$dest}' already exists");
}
mkdir($dest);
shell_exec('cd '. escapeshellarg($dest).' && git init');
// Parse download information blocks
$this->filterLongDescArea();
// Prepare to start modifying the generated README.md file
$this->longdesc = preg_replace('~\\[url=([^\\]]+?)\\](.+?)\\[/url\\]~m', '[\\2](\\1)', $this->longdesc);
$this->longdesc = str_replace("\xEF\xBB\xBF", "", $this->longdesc); // Remove interior UTF-8 BOM markers
$this->longdesc = str_replace("\r", "", $this->longdesc); // dos2unix
// Add main header and "written in" badge
$lines = explode("\n", $this->longdesc);
$header = ["# ".$this->projname, ""];
if (strlen($this->subtag) > 0) {
$header[] = "![](https://img.shields.io/badge/written%20in-" . rawurlencode($this->subtag) . "-blue)";
$header[] = "";
}
$lines = array_merge($header, $lines);
// Modify some lines to standard markdown format:
foreach($lines as $i => $line) {
// Convert ==HEADERS== to ## Headers
if (strlen($line) > 0 && $line[0] == '=') {
$tmp = rtrim($line, '=');
$indent = substr_count($tmp, '=');
// Normalise the capitalisation for header text
$header_text = substr($tmp, $indent);
if ($header_text != "TODO" && $header_text != "FIXME" && $header_text != "WARNING" && $header_text != "API") {
$header_text = ucwords(strtolower($header_text));
}
// raise markdown-equivalent header level by 1 to compensate for new header
$lines[$i] = $line = str_repeat('#', $indent + 1).' '.$header_text;
}
// Convert multiline single-backtick to multi-backtick
// Heuristic: if there is only a single backtick in the line
if (substr_count($line, '`') == 1) {
$lines[$i] = $line = str_replace('`', "\n```\n", $line);
}
// If the previous line started with a hyphen, and this line didn't, we need an
// extra line separator
if ($i > 0 && strlen($line) > 0 &&
$line[0] != '-' && strlen($lines[$i-1]) > 0 && $lines[$i-1][0] == '-'
) {
$lines[$i] = $line = "\n".$line;
}
}
// Copy all images to a doc/ subdirectory
if (count($this->images) > 0) {
mkdir($dest.'/doc');
foreach($this->images as $img) {
// Normalise some legacy file extension variants while we're here
copy($this->dir.$img, $dest.'/doc/'.str_replace(['.jpeg', '.JPG', '.JPEG'], ['.jpg', '.jpg', '.jpg'], $img));
}
}
// Copy all downloads to a dist-archive/ subdirectory
if (count($this->downloads) > 0) {
mkdir($dest.'/dist-archive');
foreach($this->downloads as $file) {
copy($this->dir.$file, $dest.'/dist-archive/'.$file);
}
if (! $this->downloads_section_was_replaced) {
// Add our own downloads section
$lines[] = $this->renderDownloadsBlock($this->downloads, true);
}
}
// Save final README
file_put_contents($dest.'/README.md', implode("\n", $lines));
// Extra properties file
$ctime = $this->lastupdate - ($this->lifespan * 3600); // Lifespans are measured in hours
if (strlen($this->shortdesc) > 0) {
$this->shortdesc = rtrim($this->shortdesc, '.').'.';
$this->shortdesc[0] = strtoupper($this->shortdesc[0]);
}
file_put_contents($dest.'/.legacy-codesite.toml', "# Converted with codesite2git
project_name=".json_encode($this->projname)."
short_description=".json_encode($this->shortdesc)."
written_in_lang=".json_encode($this->subtag)."
topics=".json_encode($this->tags)."
ctime=".$ctime."
mtime=".$this->lastupdate."
");
// Git commit everything
// Once for the meta with ctime; once for all files with the mtime
$command = (
'cd '. escapeshellarg($dest).' && '.
'git add .legacy-codesite.toml ; '.
'GIT_COMMITTER_DATE="'.date(DATE_ISO8601, $ctime).'" git commit -m "initial meta commit" --date '.escapeshellarg(date(DATE_ISO8601, $ctime)).' ; '.
'git add -A ; '.
'GIT_COMMITTER_DATE="'.date(DATE_ISO8601, $this->lastupdate).'" git commit -m "commit all archived files" --date '.escapeshellarg(date(DATE_ISO8601, $this->lastupdate))
);
//echo $command."\n";
shell_exec($command);
}
public function renderDownloadsBlock($render_downloads, $include_header=false) { // override
if (! count($render_downloads)) {
return;
}
$ret = "\n";
if ($include_header) {
$ret .= "## Download\n\n";
}
foreach($render_downloads as $filename) {
// It's possible to make a general URL that links directly to the raw download endpoint
// But because our markdown is rendered in context of the repo, we can't properly make a relative link
// We could make an absolute link, but that's probably a bad idea w.r.t. any future Git migrations
// Just link to the file in-repo although it won't be a RAW download link
$ret .= "- [⬇️ {$filename}](dist-archive/".rawurlencode($filename).") "; // raw/branch/master/dist-archive/{$filename}
$ret .= "*(".fbytes(filesize($this->dir.$filename)).")*\n";
}
return $ret;
}
}
class Gitea {
protected $url;
protected $org;
protected $token;
public function __construct(string $url, string $org, string $token) {
$this->url = $url;
$this->org = $org;
$this->token = $token;
}
public function repoExists(string $repo): bool {
$resp = @file_get_contents($this->url.'api/v1/repos/'.$this->org.'/'.$repo);
if ($resp === false) {
return false; // not conclusive but it will suffice
}
if (strpos($resp, 'does not exist')) {
return false;
}
$inf = json_decode($resp, true);
if (is_array($inf) && $inf['name'] == $repo) {
return true; // definitely yes
}
throw new \Exception("Couldn't decide whether repo {$repo} exists\n\n".var_export($resp, true));
}
public function createRepo(string $repo) {
$command = ('curl -sS -X POST ' .
escapeshellarg($this->url.'api/v1/admin/users/'.$this->org.'/repos?access_token='.urlencode($this->token)) . ' '.
'-H "accept: application/json" -H "Content-Type: application/json" -d ' . escapeshellarg(json_encode(['name' => $repo]))
);
$code = 0;
$output = [];
exec($command, $output, $code);
if ($code == 0) {
return; // success
}
throw new \Exception("failed to create repository {$repo} (exit code {$code})\n\n".var_export($output, true));
}
public function updateRepoProperties(string $repo, string $description) {
$json_update = [
'archived' => true,
'description' => $description,
// SPECIAL https://code.ivysaur.me/ - don't leave in commit {{{
'has_wiki' => false,
'website' => 'https://code.ivysaur.me/'.$repo,
// }}}
];
$command = ('curl -sS -X PATCH ' .
escapeshellarg($this->url.'api/v1/repos/'.$this->org.'/'.$repo.'?access_token='.urlencode($this->token)) . ' '.
'-H "accept: application/json" -H "Content-Type: application/json" -d ' . escapeshellarg(json_encode($json_update))
);
$code = 0;
$output = [];
exec($command, $output, $code);
if ($code == 0) {
return; // success
}
throw new \Exception("failed to update repository {$repo} properties: (exit code {$code})\n\n".var_export($output, true));
}
public function setRepoTopics(string $repo, array $topics) {
$command = ('curl -sS -X PUT ' .
escapeshellarg($this->url.'api/v1/repos/'.$this->org.'/'.$repo.'/topics?access_token='.urlencode($this->token)) . ' '.
'-H "accept: application/json" -H "Content-Type: application/json" -d ' . escapeshellarg(json_encode(['topics' => $topics]))
);
$code = 0;
$output = [];
exec($command, $output, $code);
if ($code == 0) {
return; // success
}
throw new \Exception("failed to update repository {$repo} topics: (exit code {$code})\n\n".var_export($output, true));
}
public function gitRemoteUrlFor(string $repo): string {
return $this->url.$this->org.'/'.$repo.'.git';
}
}
function codesite2git(string $projdirname, string $projname, string $dest): CProjectGitExporter {
// Parse existing project
$c = new CProjectGitExporter($projdirname, $projname);
// var_dump($c);
$c->ExportAsGit($dest);
return $c;
}
function usage() {
die("Usage:
codesite2git [FLAGS...]
Options:
--single CODESITE_ROOT_PATH PROJECT_DIR_NAME PROJECT_REAL_NAME DEST_DIR
--all CODESITE_ROOT_PATH TEMP_DIR GITEA_URL GITEA_ORG GITEA_AUTH_TOKEN
");
die(1);
}
function main(array $argv) {
if (count($argv) < 2) {
usage();
}
if ($argv[1] == '--single') {
if (count($argv) != 6) {
usage();
}
$config = setup_vars($argv[2]);
codesite2git($argv[3], $argv[4], $argv[5]);
} else if ($argv[1] == "--all") {
if (count($argv) != 7) {
usage();
}
$config = setup_vars($argv[2]);
$repos = glob(BASEDIR.'data/*');
$temp_dir = $argv[3];
$gitea = new Gitea($argv[4], $argv[5], $argv[6]);
foreach($repos as $i => $path) {
$projdir = basename($path);
$reponame = explode('-', $projdir, 2)[1];
echo sprintf("[%3d/%3d] Processing '%s'... ", $i+1, count($repos), $reponame);
// Convert to git
$git_repo_dir = $temp_dir.'/'.$projdir.'-archive.git';
$c = codesite2git($projdir, $reponame, $git_repo_dir);
// Check if Gitea repo already exists
if ($gitea->repoExists($reponame)) {
throw new \Exception("Abandoning process since repo {$reponame} already exists in Gitea - move it out of the source directory first");
}
// Create new Gitea repo
$gitea->createRepo($reponame);
if (! $gitea->repoExists($reponame)) {
throw new \Exception("Created repo {$reponame} successfully but it doesn't seem to exist?");
}
// Add git remote and push
shell_exec(
'cd '.escapeshellarg($git_repo_dir) .' && '.
'git remote add origin '.escapeshellarg($gitea->gitRemoteUrlFor($reponame)).' && '.
'git push origin master'
);
// Set Gitea topics + description
$gitea->updateRepoProperties($reponame, $c->shortdesc);
$gitea->setRepoTopics($reponame, $c->tags);
echo " Done\n";
}
} else {
usage();
}
}
main($_SERVER['argv']);