#!/usr/bin/php 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']);