1#!/usr/bin/env php 2<?php 3 4use dokuwiki\plugin\extension\Extension; 5use dokuwiki\plugin\extension\Installer; 6use splitbrain\phpcli\CLI; 7use splitbrain\phpcli\Options; 8 9if (!defined('DOKU_INC')) define('DOKU_INC', realpath(__DIR__ . '/../') . '/'); 10define('NOSESSION', 1); 11require_once(DOKU_INC . 'inc/init.php'); 12 13/** 14 * Easily manage DokuWiki git repositories 15 * 16 * @author Andreas Gohr <andi@splitbrain.org> 17 */ 18class GitToolCLI extends CLI 19{ 20 /** 21 * Register options and arguments on the given $options object 22 * 23 * @param Options $options 24 * @return void 25 */ 26 protected function setup(Options $options) 27 { 28 $options->setHelp( 29 "Manage git repositories for DokuWiki and its plugins and templates.\n\n" . 30 "$> ./bin/gittool.php clone gallery template:ach\n" . 31 "$> ./bin/gittool.php repos\n" . 32 "$> ./bin/gittool.php origin -v" 33 ); 34 35 $options->registerArgument( 36 'command', 37 'Command to execute. See below', 38 true 39 ); 40 41 $options->registerCommand( 42 'clone', 43 'Tries to install a known plugin or template (prefix with template:) via git. Uses the DokuWiki.org ' . 44 'plugin repository to find the proper git repository. Multiple extensions can be given as parameters' 45 ); 46 $options->registerArgument( 47 'extension', 48 'name of the extension to install, prefix with \'template:\' for templates', 49 true, 50 'clone' 51 ); 52 $options->registerOption( 53 'prefer-https', 54 'Prefer HTTPS over SSH for cloning (default: try SSH first, fallback to HTTPS)', 55 false, 56 false, 57 'clone' 58 ); 59 60 $options->registerCommand( 61 'install', 62 'The same as clone, but when no git source repository can be found, the extension is installed via ' . 63 'download' 64 ); 65 $options->registerArgument( 66 'extension', 67 'name of the extension to install, prefix with \'template:\' for templates', 68 true, 69 'install' 70 ); 71 $options->registerOption( 72 'prefer-https', 73 'Prefer HTTPS over SSH for cloning (default: try SSH first, fallback to HTTPS)', 74 false, 75 false, 76 'install' 77 ); 78 79 $options->registerCommand( 80 'repos', 81 'Lists all git repositories found in this DokuWiki installation' 82 ); 83 84 $options->registerCommand( 85 '*', 86 'Any unknown commands are assumed to be arguments to git and will be executed in all repositories ' . 87 'found within this DokuWiki installation' 88 ); 89 } 90 91 /** 92 * Your main program 93 * 94 * Arguments and options have been parsed when this is run 95 * 96 * @param Options $options 97 * @return void 98 */ 99 protected function main(Options $options) 100 { 101 $command = $options->getCmd(); 102 $args = $options->getArgs(); 103 if (!$command) $command = array_shift($args); 104 105 switch ($command) { 106 case '': 107 echo $options->help(); 108 break; 109 case 'clone': 110 $this->cmdClone($args, $options->getOpt('prefer-https')); 111 break; 112 case 'install': 113 $this->cmdInstall($args, $options->getOpt('prefer-https')); 114 break; 115 case 'repo': 116 case 'repos': 117 $this->cmdRepos(); 118 break; 119 default: 120 $this->cmdGit($command, $args); 121 } 122 } 123 124 /** 125 * Tries to install the given extensions using git clone 126 * 127 * @param array $extensions 128 * @param bool $preferHttps 129 */ 130 public function cmdClone($extensions, $preferHttps = false) 131 { 132 $errors = []; 133 $succeeded = []; 134 135 foreach ($extensions as $ext) { 136 $repo = $this->getSourceRepo($ext); 137 138 if (!$repo) { 139 $this->error("could not find a repository for $ext"); 140 $errors[] = $ext; 141 } elseif ($this->cloneExtension($ext, $repo, $preferHttps)) { 142 $succeeded[] = $ext; 143 } else { 144 $errors[] = $ext; 145 } 146 } 147 148 echo "\n"; 149 if ($succeeded) $this->success('successfully cloned the following extensions: ' . implode(', ', $succeeded)); 150 if ($errors) $this->error('failed to clone the following extensions: ' . implode(', ', $errors)); 151 } 152 153 /** 154 * Tries to install the given extensions using git clone with fallback to install 155 * 156 * @param array $extensions 157 * @param bool $preferHttps 158 */ 159 public function cmdInstall($extensions, $preferHttps = false) 160 { 161 $errors = []; 162 $succeeded = []; 163 164 foreach ($extensions as $ext) { 165 $repo = $this->getSourceRepo($ext); 166 167 if (!$repo) { 168 $this->info("could not find a repository for $ext"); 169 170 try { 171 $installer = new Installer(); 172 $this->info("installing $ext via download"); 173 $installer->installFromId($ext); 174 $this->success("installed $ext via download"); 175 $succeeded[] = $ext; 176 } catch (\Exception) { 177 $this->error("failed to install $ext via download"); 178 $errors[] = $ext; 179 } 180 } elseif ($this->cloneExtension($ext, $repo, $preferHttps)) { 181 $succeeded[] = $ext; 182 } else { 183 $errors[] = $ext; 184 } 185 } 186 187 echo "\n"; 188 if ($succeeded) $this->success('successfully installed the following extensions: ' . implode(', ', $succeeded)); 189 if ($errors) $this->error('failed to install the following extensions: ' . implode(', ', $errors)); 190 } 191 192 /** 193 * Executes the given git command in every repository 194 * 195 * @param $cmd 196 * @param $arg 197 */ 198 public function cmdGit($cmd, $arg) 199 { 200 $repos = $this->findRepos(); 201 202 $shell = array_merge(['git', $cmd], $arg); 203 $shell = array_map(escapeshellarg(...), $shell); 204 $shell = implode(' ', $shell); 205 206 foreach ($repos as $repo) { 207 if (!@chdir($repo)) { 208 $this->error("Could not change into $repo"); 209 continue; 210 } 211 212 $this->info("executing $shell in $repo"); 213 $ret = 0; 214 system($shell, $ret); 215 216 if ($ret == 0) { 217 $this->success("git succeeded in $repo"); 218 } else { 219 $this->error("git failed in $repo"); 220 } 221 } 222 } 223 224 /** 225 * Simply lists the repositories 226 */ 227 public function cmdRepos() 228 { 229 $repos = $this->findRepos(); 230 foreach ($repos as $repo) { 231 echo "$repo\n"; 232 } 233 } 234 235 /** 236 * Clones the extension from the given repository 237 * 238 * @param string $ext 239 * @param string $repo 240 * @param bool $preferHttps 241 * @return bool 242 */ 243 private function cloneExtension($ext, $repo, $preferHttps = false) 244 { 245 if (str_starts_with($ext, 'template:')) { 246 $target = fullpath(tpl_incdir() . '../' . substr($ext, 9)); 247 } else { 248 $target = DOKU_PLUGIN . $ext; 249 } 250 251 $ret = -1; 252 253 // try SSH clone first, unless the user prefers HTTPS 254 if (!$preferHttps) { 255 $sshUrl = $this->httpsToSshUrl($repo); 256 $this->info("cloning $ext from $sshUrl"); 257 system("git clone $sshUrl $target", $ret); 258 if ($ret !== 0) $this->info("SSH clone failed, trying HTTPS: $repo"); 259 } 260 261 // try HTTPS clone 262 if ($ret !== 0) { 263 $this->info("cloning $ext from $repo"); 264 system("git clone $repo $target", $ret); 265 } 266 267 if ($ret === 0) { 268 $this->success("cloning of $ext succeeded"); 269 return true; 270 } else { 271 $this->error("cloning of $ext failed"); 272 return false; 273 } 274 } 275 276 /** 277 * Convert a HTTPS repo URL to an SSH URL if possible 278 * 279 * @return string 280 */ 281 private function httpsToSshUrl($url) 282 { 283 if (preg_match('/(github\.com|bitbucket\.org|gitorious\.org)\/([^\/]+)\/([^\/]+)/i', $url, $m)) { 284 $host = $m[1]; 285 $user = $m[2]; 286 $repo = $m[3]; 287 return "git@$host:$user/$repo"; 288 } 289 return $url; 290 } 291 292 /** 293 * Returns all git repositories in this DokuWiki install 294 * 295 * Looks in root, template and plugin directories only. 296 * 297 * @return array 298 */ 299 private function findRepos() 300 { 301 $this->info('Looking for .git directories'); 302 $data = array_merge( 303 glob(DOKU_INC . '.git', GLOB_ONLYDIR), 304 glob(DOKU_PLUGIN . '*/.git', GLOB_ONLYDIR), 305 glob(fullpath(tpl_incdir() . '../') . '/*/.git', GLOB_ONLYDIR) 306 ); 307 308 if (!$data) { 309 $this->error('Found no .git directories'); 310 } else { 311 $this->success('Found ' . count($data) . ' .git directories'); 312 } 313 $data = array_map(fullpath(...), array_map(dirname(...), $data)); 314 return $data; 315 } 316 317 /** 318 * Returns the repository for the given extension 319 * 320 * @param string $extensionId 321 * @return false|string 322 */ 323 private function getSourceRepo($extensionId) 324 { 325 $extension = Extension::createFromId($extensionId); 326 327 $repourl = $extension->getSourcerepoURL(); 328 if (!$repourl) return false; 329 330 // match github repos 331 if (preg_match('/github\.com\/([^\/]+)\/([^\/]+)/i', $repourl, $m)) { 332 $user = $m[1]; 333 $repo = $m[2]; 334 return 'https://github.com/' . $user . '/' . $repo . '.git'; 335 } 336 337 // match gitorious repos 338 if (preg_match('/gitorious.org\/([^\/]+)\/([^\/]+)?/i', $repourl, $m)) { 339 $user = $m[1]; 340 $repo = $m[2]; 341 if (!$repo) $repo = $user; 342 343 return 'https://git.gitorious.org/' . $user . '/' . $repo . '.git'; 344 } 345 346 // match bitbucket repos - most people seem to use mercurial there though 347 if (preg_match('/bitbucket\.org\/([^\/]+)\/([^\/]+)/i', $repourl, $m)) { 348 $user = $m[1]; 349 $repo = $m[2]; 350 return 'https://bitbucket.org/' . $user . '/' . $repo . '.git'; 351 } 352 353 return false; 354 } 355} 356 357// Main 358$cli = new GitToolCLI(); 359$cli->run(); 360