1cbfa4829SPhy#!/usr/bin/env php 2c39ae2c9SAndreas Gohr<?php 3cbeaa4a0SAndreas Gohr 442469e71SAnna Dabrowskause dokuwiki\plugin\extension\Extension; 51a7e82cdSAnna Dabrowskause dokuwiki\plugin\extension\Installer; 6cbeaa4a0SAndreas Gohruse splitbrain\phpcli\CLI; 7cbeaa4a0SAndreas Gohruse splitbrain\phpcli\Options; 8cbeaa4a0SAndreas Gohr 9b1f206e1SAndreas Gohrif (!defined('DOKU_INC')) define('DOKU_INC', realpath(__DIR__ . '/../') . '/'); 10c39ae2c9SAndreas Gohrdefine('NOSESSION', 1); 11c39ae2c9SAndreas Gohrrequire_once(DOKU_INC . 'inc/init.php'); 12c39ae2c9SAndreas Gohr 13c39ae2c9SAndreas Gohr/** 14c39ae2c9SAndreas Gohr * Easily manage DokuWiki git repositories 15c39ae2c9SAndreas Gohr * 16c39ae2c9SAndreas Gohr * @author Andreas Gohr <andi@splitbrain.org> 17c39ae2c9SAndreas Gohr */ 188c7c53b0SAndreas Gohrclass GitToolCLI extends CLI 198c7c53b0SAndreas Gohr{ 209fb66494SAndreas Gohr /** 219fb66494SAndreas Gohr * Register options and arguments on the given $options object 229fb66494SAndreas Gohr * 23cbeaa4a0SAndreas Gohr * @param Options $options 249fb66494SAndreas Gohr * @return void 259fb66494SAndreas Gohr */ 26d868eb89SAndreas Gohr protected function setup(Options $options) 27d868eb89SAndreas Gohr { 289fb66494SAndreas Gohr $options->setHelp( 299fb66494SAndreas Gohr "Manage git repositories for DokuWiki and its plugins and templates.\n\n" . 309fb66494SAndreas Gohr "$> ./bin/gittool.php clone gallery template:ach\n" . 319fb66494SAndreas Gohr "$> ./bin/gittool.php repos\n" . 32ae1ce4a6SAndreas Gohr "$> ./bin/gittool.php origin -v" 339fb66494SAndreas Gohr ); 34c39ae2c9SAndreas Gohr 359fb66494SAndreas Gohr $options->registerArgument( 369fb66494SAndreas Gohr 'command', 379fb66494SAndreas Gohr 'Command to execute. See below', 389fb66494SAndreas Gohr true 399fb66494SAndreas Gohr ); 40c39ae2c9SAndreas Gohr 419fb66494SAndreas Gohr $options->registerCommand( 429fb66494SAndreas Gohr 'clone', 439fb66494SAndreas Gohr 'Tries to install a known plugin or template (prefix with template:) via git. Uses the DokuWiki.org ' . 449fb66494SAndreas Gohr 'plugin repository to find the proper git repository. Multiple extensions can be given as parameters' 459fb66494SAndreas Gohr ); 469fb66494SAndreas Gohr $options->registerArgument( 479fb66494SAndreas Gohr 'extension', 489fb66494SAndreas Gohr 'name of the extension to install, prefix with \'template:\' for templates', 499fb66494SAndreas Gohr true, 509fb66494SAndreas Gohr 'clone' 519fb66494SAndreas Gohr ); 52*ddc6a58bSAndreas Gohr $options->registerOption( 53*ddc6a58bSAndreas Gohr 'prefer-https', 54*ddc6a58bSAndreas Gohr 'Prefer HTTPS over SSH for cloning (default: try SSH first, fallback to HTTPS)', 55*ddc6a58bSAndreas Gohr false, 56*ddc6a58bSAndreas Gohr false, 57*ddc6a58bSAndreas Gohr 'clone' 58*ddc6a58bSAndreas Gohr ); 59c39ae2c9SAndreas Gohr 609fb66494SAndreas Gohr $options->registerCommand( 619fb66494SAndreas Gohr 'install', 629fb66494SAndreas Gohr 'The same as clone, but when no git source repository can be found, the extension is installed via ' . 639fb66494SAndreas Gohr 'download' 649fb66494SAndreas Gohr ); 659fb66494SAndreas Gohr $options->registerArgument( 669fb66494SAndreas Gohr 'extension', 679fb66494SAndreas Gohr 'name of the extension to install, prefix with \'template:\' for templates', 689fb66494SAndreas Gohr true, 699fb66494SAndreas Gohr 'install' 709fb66494SAndreas Gohr ); 71*ddc6a58bSAndreas Gohr $options->registerOption( 72*ddc6a58bSAndreas Gohr 'prefer-https', 73*ddc6a58bSAndreas Gohr 'Prefer HTTPS over SSH for cloning (default: try SSH first, fallback to HTTPS)', 74*ddc6a58bSAndreas Gohr false, 75*ddc6a58bSAndreas Gohr false, 76*ddc6a58bSAndreas Gohr 'install' 77*ddc6a58bSAndreas Gohr ); 78c39ae2c9SAndreas Gohr 799fb66494SAndreas Gohr $options->registerCommand( 809fb66494SAndreas Gohr 'repos', 819fb66494SAndreas Gohr 'Lists all git repositories found in this DokuWiki installation' 829fb66494SAndreas Gohr ); 83c39ae2c9SAndreas Gohr 849fb66494SAndreas Gohr $options->registerCommand( 859fb66494SAndreas Gohr '*', 869fb66494SAndreas Gohr 'Any unknown commands are assumed to be arguments to git and will be executed in all repositories ' . 879fb66494SAndreas Gohr 'found within this DokuWiki installation' 889fb66494SAndreas Gohr ); 89c39ae2c9SAndreas Gohr } 90c39ae2c9SAndreas Gohr 91c39ae2c9SAndreas Gohr /** 929fb66494SAndreas Gohr * Your main program 939fb66494SAndreas Gohr * 949fb66494SAndreas Gohr * Arguments and options have been parsed when this is run 959fb66494SAndreas Gohr * 96cbeaa4a0SAndreas Gohr * @param Options $options 979fb66494SAndreas Gohr * @return void 989fb66494SAndreas Gohr */ 99d868eb89SAndreas Gohr protected function main(Options $options) 100d868eb89SAndreas Gohr { 1019fb66494SAndreas Gohr $command = $options->getCmd(); 102cbeaa4a0SAndreas Gohr $args = $options->getArgs(); 103cbeaa4a0SAndreas Gohr if (!$command) $command = array_shift($args); 1049fb66494SAndreas Gohr 1059fb66494SAndreas Gohr switch ($command) { 1069fb66494SAndreas Gohr case '': 1071c36b3d8SAndreas Gohr echo $options->help(); 1089fb66494SAndreas Gohr break; 1099fb66494SAndreas Gohr case 'clone': 110*ddc6a58bSAndreas Gohr $this->cmdClone($args, $options->getOpt('prefer-https')); 1119fb66494SAndreas Gohr break; 1129fb66494SAndreas Gohr case 'install': 113*ddc6a58bSAndreas Gohr $this->cmdInstall($args, $options->getOpt('prefer-https')); 1149fb66494SAndreas Gohr break; 1159fb66494SAndreas Gohr case 'repo': 1169fb66494SAndreas Gohr case 'repos': 1172b2d0ba9SAndreas Gohr $this->cmdRepos(); 1189fb66494SAndreas Gohr break; 1199fb66494SAndreas Gohr default: 1202b2d0ba9SAndreas Gohr $this->cmdGit($command, $args); 1219fb66494SAndreas Gohr } 1229fb66494SAndreas Gohr } 1239fb66494SAndreas Gohr 1249fb66494SAndreas Gohr /** 125c39ae2c9SAndreas Gohr * Tries to install the given extensions using git clone 126c39ae2c9SAndreas Gohr * 12742ea7f44SGerrit Uitslag * @param array $extensions 128*ddc6a58bSAndreas Gohr * @param bool $preferHttps 129c39ae2c9SAndreas Gohr */ 130*ddc6a58bSAndreas Gohr public function cmdClone($extensions, $preferHttps = false) 131d868eb89SAndreas Gohr { 132b1f206e1SAndreas Gohr $errors = []; 133b1f206e1SAndreas Gohr $succeeded = []; 134c39ae2c9SAndreas Gohr 135c39ae2c9SAndreas Gohr foreach ($extensions as $ext) { 136c39ae2c9SAndreas Gohr $repo = $this->getSourceRepo($ext); 137c39ae2c9SAndreas Gohr 138c39ae2c9SAndreas Gohr if (!$repo) { 1399fb66494SAndreas Gohr $this->error("could not find a repository for $ext"); 140c39ae2c9SAndreas Gohr $errors[] = $ext; 141*ddc6a58bSAndreas Gohr } elseif ($this->cloneExtension($ext, $repo, $preferHttps)) { 142c39ae2c9SAndreas Gohr $succeeded[] = $ext; 143c39ae2c9SAndreas Gohr } else { 144c39ae2c9SAndreas Gohr $errors[] = $ext; 145c39ae2c9SAndreas Gohr } 146c39ae2c9SAndreas Gohr } 147c39ae2c9SAndreas Gohr 148c39ae2c9SAndreas Gohr echo "\n"; 149b1f206e1SAndreas Gohr if ($succeeded) $this->success('successfully cloned the following extensions: ' . implode(', ', $succeeded)); 150b1f206e1SAndreas Gohr if ($errors) $this->error('failed to clone the following extensions: ' . implode(', ', $errors)); 151c39ae2c9SAndreas Gohr } 152c39ae2c9SAndreas Gohr 153c39ae2c9SAndreas Gohr /** 154c39ae2c9SAndreas Gohr * Tries to install the given extensions using git clone with fallback to install 155c39ae2c9SAndreas Gohr * 15642ea7f44SGerrit Uitslag * @param array $extensions 157*ddc6a58bSAndreas Gohr * @param bool $preferHttps 158c39ae2c9SAndreas Gohr */ 159*ddc6a58bSAndreas Gohr public function cmdInstall($extensions, $preferHttps = false) 160d868eb89SAndreas Gohr { 161b1f206e1SAndreas Gohr $errors = []; 162b1f206e1SAndreas Gohr $succeeded = []; 163c39ae2c9SAndreas Gohr 164c39ae2c9SAndreas Gohr foreach ($extensions as $ext) { 165c39ae2c9SAndreas Gohr $repo = $this->getSourceRepo($ext); 166c39ae2c9SAndreas Gohr 167c39ae2c9SAndreas Gohr if (!$repo) { 1689fb66494SAndreas Gohr $this->info("could not find a repository for $ext"); 1691a7e82cdSAnna Dabrowska 1701a7e82cdSAnna Dabrowska try { 1711a7e82cdSAnna Dabrowska $installer = new Installer(); 1721a7e82cdSAnna Dabrowska $this->info("installing $ext via download"); 1731a7e82cdSAnna Dabrowska $installer->installFromId($ext); 1741a7e82cdSAnna Dabrowska $this->success("installed $ext via download"); 175c39ae2c9SAndreas Gohr $succeeded[] = $ext; 176093fe67eSAndreas Gohr } catch (\Exception) { 1771a7e82cdSAnna Dabrowska $this->error("failed to install $ext via download"); 178c39ae2c9SAndreas Gohr $errors[] = $ext; 179c39ae2c9SAndreas Gohr } 180*ddc6a58bSAndreas Gohr } elseif ($this->cloneExtension($ext, $repo, $preferHttps)) { 181c39ae2c9SAndreas Gohr $succeeded[] = $ext; 182c39ae2c9SAndreas Gohr } else { 183c39ae2c9SAndreas Gohr $errors[] = $ext; 184c39ae2c9SAndreas Gohr } 185c39ae2c9SAndreas Gohr } 186c39ae2c9SAndreas Gohr 187c39ae2c9SAndreas Gohr echo "\n"; 188b1f206e1SAndreas Gohr if ($succeeded) $this->success('successfully installed the following extensions: ' . implode(', ', $succeeded)); 189b1f206e1SAndreas Gohr if ($errors) $this->error('failed to install the following extensions: ' . implode(', ', $errors)); 190c39ae2c9SAndreas Gohr } 191c39ae2c9SAndreas Gohr 192c39ae2c9SAndreas Gohr /** 193c39ae2c9SAndreas Gohr * Executes the given git command in every repository 194c39ae2c9SAndreas Gohr * 195c39ae2c9SAndreas Gohr * @param $cmd 196c39ae2c9SAndreas Gohr * @param $arg 197c39ae2c9SAndreas Gohr */ 198d868eb89SAndreas Gohr public function cmdGit($cmd, $arg) 199d868eb89SAndreas Gohr { 200c39ae2c9SAndreas Gohr $repos = $this->findRepos(); 201c39ae2c9SAndreas Gohr 202b1f206e1SAndreas Gohr $shell = array_merge(['git', $cmd], $arg); 203093fe67eSAndreas Gohr $shell = array_map(escapeshellarg(...), $shell); 204b1f206e1SAndreas Gohr $shell = implode(' ', $shell); 205c39ae2c9SAndreas Gohr 206c39ae2c9SAndreas Gohr foreach ($repos as $repo) { 207c39ae2c9SAndreas Gohr if (!@chdir($repo)) { 2089fb66494SAndreas Gohr $this->error("Could not change into $repo"); 209c39ae2c9SAndreas Gohr continue; 210c39ae2c9SAndreas Gohr } 211c39ae2c9SAndreas Gohr 2129fb66494SAndreas Gohr $this->info("executing $shell in $repo"); 213c39ae2c9SAndreas Gohr $ret = 0; 214c39ae2c9SAndreas Gohr system($shell, $ret); 215c39ae2c9SAndreas Gohr 216c39ae2c9SAndreas Gohr if ($ret == 0) { 2179fb66494SAndreas Gohr $this->success("git succeeded in $repo"); 218c39ae2c9SAndreas Gohr } else { 2199fb66494SAndreas Gohr $this->error("git failed in $repo"); 220c39ae2c9SAndreas Gohr } 221c39ae2c9SAndreas Gohr } 222c39ae2c9SAndreas Gohr } 223c39ae2c9SAndreas Gohr 224c39ae2c9SAndreas Gohr /** 225c39ae2c9SAndreas Gohr * Simply lists the repositories 226c39ae2c9SAndreas Gohr */ 227d868eb89SAndreas Gohr public function cmdRepos() 228d868eb89SAndreas Gohr { 229c39ae2c9SAndreas Gohr $repos = $this->findRepos(); 230c39ae2c9SAndreas Gohr foreach ($repos as $repo) { 231c39ae2c9SAndreas Gohr echo "$repo\n"; 232c39ae2c9SAndreas Gohr } 233c39ae2c9SAndreas Gohr } 234c39ae2c9SAndreas Gohr 235c39ae2c9SAndreas Gohr /** 236c39ae2c9SAndreas Gohr * Clones the extension from the given repository 237c39ae2c9SAndreas Gohr * 238c39ae2c9SAndreas Gohr * @param string $ext 239c39ae2c9SAndreas Gohr * @param string $repo 240*ddc6a58bSAndreas Gohr * @param bool $preferHttps 241c39ae2c9SAndreas Gohr * @return bool 242c39ae2c9SAndreas Gohr */ 243*ddc6a58bSAndreas Gohr private function cloneExtension($ext, $repo, $preferHttps = false) 244d868eb89SAndreas Gohr { 2451b2deed9Sfiwswe if (str_starts_with($ext, 'template:')) { 246c39ae2c9SAndreas Gohr $target = fullpath(tpl_incdir() . '../' . substr($ext, 9)); 247c39ae2c9SAndreas Gohr } else { 248c39ae2c9SAndreas Gohr $target = DOKU_PLUGIN . $ext; 249c39ae2c9SAndreas Gohr } 250c39ae2c9SAndreas Gohr 251*ddc6a58bSAndreas Gohr $ret = -1; 252*ddc6a58bSAndreas Gohr 253*ddc6a58bSAndreas Gohr // try SSH clone first, unless the user prefers HTTPS 254*ddc6a58bSAndreas Gohr if (!$preferHttps) { 255*ddc6a58bSAndreas Gohr $sshUrl = $this->httpsToSshUrl($repo); 256*ddc6a58bSAndreas Gohr $this->info("cloning $ext from $sshUrl"); 257*ddc6a58bSAndreas Gohr system("git clone $sshUrl $target", $ret); 258*ddc6a58bSAndreas Gohr if ($ret !== 0) $this->info("SSH clone failed, trying HTTPS: $repo"); 259*ddc6a58bSAndreas Gohr } 260*ddc6a58bSAndreas Gohr 261*ddc6a58bSAndreas Gohr // try HTTPS clone 262*ddc6a58bSAndreas Gohr if ($ret !== 0) { 263*ddc6a58bSAndreas Gohr $this->info("cloning $ext from $repo"); 264c39ae2c9SAndreas Gohr system("git clone $repo $target", $ret); 265*ddc6a58bSAndreas Gohr } 266*ddc6a58bSAndreas Gohr 267c39ae2c9SAndreas Gohr if ($ret === 0) { 2689fb66494SAndreas Gohr $this->success("cloning of $ext succeeded"); 269c39ae2c9SAndreas Gohr return true; 270c39ae2c9SAndreas Gohr } else { 2719fb66494SAndreas Gohr $this->error("cloning of $ext failed"); 272c39ae2c9SAndreas Gohr return false; 273c39ae2c9SAndreas Gohr } 274c39ae2c9SAndreas Gohr } 275c39ae2c9SAndreas Gohr 276c39ae2c9SAndreas Gohr /** 277*ddc6a58bSAndreas Gohr * Convert a HTTPS repo URL to an SSH URL if possible 278*ddc6a58bSAndreas Gohr * 279*ddc6a58bSAndreas Gohr * @return string 280*ddc6a58bSAndreas Gohr */ 281*ddc6a58bSAndreas Gohr private function httpsToSshUrl($url) 282*ddc6a58bSAndreas Gohr { 283*ddc6a58bSAndreas Gohr if (preg_match('/(github\.com|bitbucket\.org|gitorious\.org)\/([^\/]+)\/([^\/]+)/i', $url, $m)) { 284*ddc6a58bSAndreas Gohr $host = $m[1]; 285*ddc6a58bSAndreas Gohr $user = $m[2]; 286*ddc6a58bSAndreas Gohr $repo = $m[3]; 287*ddc6a58bSAndreas Gohr return "git@$host:$user/$repo"; 288*ddc6a58bSAndreas Gohr } 289*ddc6a58bSAndreas Gohr return $url; 290*ddc6a58bSAndreas Gohr } 291*ddc6a58bSAndreas Gohr 292*ddc6a58bSAndreas Gohr /** 293c39ae2c9SAndreas Gohr * Returns all git repositories in this DokuWiki install 294c39ae2c9SAndreas Gohr * 295c39ae2c9SAndreas Gohr * Looks in root, template and plugin directories only. 296c39ae2c9SAndreas Gohr * 297c39ae2c9SAndreas Gohr * @return array 298c39ae2c9SAndreas Gohr */ 299d868eb89SAndreas Gohr private function findRepos() 300d868eb89SAndreas Gohr { 3019fb66494SAndreas Gohr $this->info('Looking for .git directories'); 302c39ae2c9SAndreas Gohr $data = array_merge( 303c39ae2c9SAndreas Gohr glob(DOKU_INC . '.git', GLOB_ONLYDIR), 304c39ae2c9SAndreas Gohr glob(DOKU_PLUGIN . '*/.git', GLOB_ONLYDIR), 305c39ae2c9SAndreas Gohr glob(fullpath(tpl_incdir() . '../') . '/*/.git', GLOB_ONLYDIR) 306c39ae2c9SAndreas Gohr ); 307c39ae2c9SAndreas Gohr 308c39ae2c9SAndreas Gohr if (!$data) { 3099fb66494SAndreas Gohr $this->error('Found no .git directories'); 310c39ae2c9SAndreas Gohr } else { 3119fb66494SAndreas Gohr $this->success('Found ' . count($data) . ' .git directories'); 312c39ae2c9SAndreas Gohr } 313093fe67eSAndreas Gohr $data = array_map(fullpath(...), array_map(dirname(...), $data)); 314c39ae2c9SAndreas Gohr return $data; 315c39ae2c9SAndreas Gohr } 316c39ae2c9SAndreas Gohr 317c39ae2c9SAndreas Gohr /** 318c39ae2c9SAndreas Gohr * Returns the repository for the given extension 319c39ae2c9SAndreas Gohr * 3201a7e82cdSAnna Dabrowska * @param string $extensionId 32142ea7f44SGerrit Uitslag * @return false|string 322c39ae2c9SAndreas Gohr */ 3231a7e82cdSAnna Dabrowska private function getSourceRepo($extensionId) 324d868eb89SAndreas Gohr { 3251a7e82cdSAnna Dabrowska $extension = Extension::createFromId($extensionId); 3267e8500eeSGerrit Uitslag 3271a7e82cdSAnna Dabrowska $repourl = $extension->getSourcerepoURL(); 32868cf024bSAndreas Gohr if (!$repourl) return false; 32968cf024bSAndreas Gohr 330c39ae2c9SAndreas Gohr // match github repos 33168cf024bSAndreas Gohr if (preg_match('/github\.com\/([^\/]+)\/([^\/]+)/i', $repourl, $m)) { 332c39ae2c9SAndreas Gohr $user = $m[1]; 333c39ae2c9SAndreas Gohr $repo = $m[2]; 334c39ae2c9SAndreas Gohr return 'https://github.com/' . $user . '/' . $repo . '.git'; 335c39ae2c9SAndreas Gohr } 336c39ae2c9SAndreas Gohr 337c39ae2c9SAndreas Gohr // match gitorious repos 33868cf024bSAndreas Gohr if (preg_match('/gitorious.org\/([^\/]+)\/([^\/]+)?/i', $repourl, $m)) { 339c39ae2c9SAndreas Gohr $user = $m[1]; 340c39ae2c9SAndreas Gohr $repo = $m[2]; 341c39ae2c9SAndreas Gohr if (!$repo) $repo = $user; 342c39ae2c9SAndreas Gohr 343c39ae2c9SAndreas Gohr return 'https://git.gitorious.org/' . $user . '/' . $repo . '.git'; 344c39ae2c9SAndreas Gohr } 345c39ae2c9SAndreas Gohr 346c39ae2c9SAndreas Gohr // match bitbucket repos - most people seem to use mercurial there though 34768cf024bSAndreas Gohr if (preg_match('/bitbucket\.org\/([^\/]+)\/([^\/]+)/i', $repourl, $m)) { 348c39ae2c9SAndreas Gohr $user = $m[1]; 349c39ae2c9SAndreas Gohr $repo = $m[2]; 350c39ae2c9SAndreas Gohr return 'https://bitbucket.org/' . $user . '/' . $repo . '.git'; 351c39ae2c9SAndreas Gohr } 352c39ae2c9SAndreas Gohr 353c39ae2c9SAndreas Gohr return false; 354c39ae2c9SAndreas Gohr } 355c39ae2c9SAndreas Gohr} 356c39ae2c9SAndreas Gohr 357b0b7909bSAndreas Gohr// Main 358b0b7909bSAndreas Gohr$cli = new GitToolCLI(); 359b0b7909bSAndreas Gohr$cli->run(); 360