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 53 $options->registerCommand( 54 'install', 55 'The same as clone, but when no git source repository can be found, the extension is installed via ' . 56 'download' 57 ); 58 $options->registerArgument( 59 'extension', 60 'name of the extension to install, prefix with \'template:\' for templates', 61 true, 62 'install' 63 ); 64 65 $options->registerCommand( 66 'repos', 67 'Lists all git repositories found in this DokuWiki installation' 68 ); 69 70 $options->registerCommand( 71 '*', 72 'Any unknown commands are assumed to be arguments to git and will be executed in all repositories ' . 73 'found within this DokuWiki installation' 74 ); 75 } 76 77 /** 78 * Your main program 79 * 80 * Arguments and options have been parsed when this is run 81 * 82 * @param Options $options 83 * @return void 84 */ 85 protected function main(Options $options) 86 { 87 $command = $options->getCmd(); 88 $args = $options->getArgs(); 89 if (!$command) $command = array_shift($args); 90 91 switch ($command) { 92 case '': 93 echo $options->help(); 94 break; 95 case 'clone': 96 $this->cmdClone($args); 97 break; 98 case 'install': 99 $this->cmdInstall($args); 100 break; 101 case 'repo': 102 case 'repos': 103 $this->cmdRepos(); 104 break; 105 default: 106 $this->cmdGit($command, $args); 107 } 108 } 109 110 /** 111 * Tries to install the given extensions using git clone 112 * 113 * @param array $extensions 114 */ 115 public function cmdClone($extensions) 116 { 117 $errors = []; 118 $succeeded = []; 119 120 foreach ($extensions as $ext) { 121 $repo = $this->getSourceRepo($ext); 122 123 if (!$repo) { 124 $this->error("could not find a repository for $ext"); 125 $errors[] = $ext; 126 } elseif ($this->cloneExtension($ext, $repo)) { 127 $succeeded[] = $ext; 128 } else { 129 $errors[] = $ext; 130 } 131 } 132 133 echo "\n"; 134 if ($succeeded) $this->success('successfully cloned the following extensions: ' . implode(', ', $succeeded)); 135 if ($errors) $this->error('failed to clone the following extensions: ' . implode(', ', $errors)); 136 } 137 138 /** 139 * Tries to install the given extensions using git clone with fallback to install 140 * 141 * @param array $extensions 142 */ 143 public function cmdInstall($extensions) 144 { 145 $errors = []; 146 $succeeded = []; 147 148 foreach ($extensions as $ext) { 149 $repo = $this->getSourceRepo($ext); 150 151 if (!$repo) { 152 $this->info("could not find a repository for $ext"); 153 154 try { 155 $installer = new Installer(); 156 $this->info("installing $ext via download"); 157 $installer->installFromId($ext); 158 $this->success("installed $ext via download"); 159 $succeeded[] = $ext; 160 } catch (\Exception $e) { 161 $this->error("failed to install $ext via download"); 162 $errors[] = $ext; 163 } 164 } elseif ($this->cloneExtension($ext, $repo)) { 165 $succeeded[] = $ext; 166 } else { 167 $errors[] = $ext; 168 } 169 } 170 171 echo "\n"; 172 if ($succeeded) $this->success('successfully installed the following extensions: ' . implode(', ', $succeeded)); 173 if ($errors) $this->error('failed to install the following extensions: ' . implode(', ', $errors)); 174 } 175 176 /** 177 * Executes the given git command in every repository 178 * 179 * @param $cmd 180 * @param $arg 181 */ 182 public function cmdGit($cmd, $arg) 183 { 184 $repos = $this->findRepos(); 185 186 $shell = array_merge(['git', $cmd], $arg); 187 $shell = array_map('escapeshellarg', $shell); 188 $shell = implode(' ', $shell); 189 190 foreach ($repos as $repo) { 191 if (!@chdir($repo)) { 192 $this->error("Could not change into $repo"); 193 continue; 194 } 195 196 $this->info("executing $shell in $repo"); 197 $ret = 0; 198 system($shell, $ret); 199 200 if ($ret == 0) { 201 $this->success("git succeeded in $repo"); 202 } else { 203 $this->error("git failed in $repo"); 204 } 205 } 206 } 207 208 /** 209 * Simply lists the repositories 210 */ 211 public function cmdRepos() 212 { 213 $repos = $this->findRepos(); 214 foreach ($repos as $repo) { 215 echo "$repo\n"; 216 } 217 } 218 219 /** 220 * Clones the extension from the given repository 221 * 222 * @param string $ext 223 * @param string $repo 224 * @return bool 225 */ 226 private function cloneExtension($ext, $repo) 227 { 228 if (str_starts_with($ext, 'template:')) { 229 $target = fullpath(tpl_incdir() . '../' . substr($ext, 9)); 230 } else { 231 $target = DOKU_PLUGIN . $ext; 232 } 233 234 $this->info("cloning $ext from $repo to $target"); 235 $ret = 0; 236 system("git clone $repo $target", $ret); 237 if ($ret === 0) { 238 $this->success("cloning of $ext succeeded"); 239 return true; 240 } else { 241 $this->error("cloning of $ext failed"); 242 return false; 243 } 244 } 245 246 /** 247 * Returns all git repositories in this DokuWiki install 248 * 249 * Looks in root, template and plugin directories only. 250 * 251 * @return array 252 */ 253 private function findRepos() 254 { 255 $this->info('Looking for .git directories'); 256 $data = array_merge( 257 glob(DOKU_INC . '.git', GLOB_ONLYDIR), 258 glob(DOKU_PLUGIN . '*/.git', GLOB_ONLYDIR), 259 glob(fullpath(tpl_incdir() . '../') . '/*/.git', GLOB_ONLYDIR) 260 ); 261 262 if (!$data) { 263 $this->error('Found no .git directories'); 264 } else { 265 $this->success('Found ' . count($data) . ' .git directories'); 266 } 267 $data = array_map('fullpath', array_map('dirname', $data)); 268 return $data; 269 } 270 271 /** 272 * Returns the repository for the given extension 273 * 274 * @param string $extensionId 275 * @return false|string 276 */ 277 private function getSourceRepo($extensionId) 278 { 279 $extension = Extension::createFromId($extensionId); 280 281 $repourl = $extension->getSourcerepoURL(); 282 if (!$repourl) return false; 283 284 // match github repos 285 if (preg_match('/github\.com\/([^\/]+)\/([^\/]+)/i', $repourl, $m)) { 286 $user = $m[1]; 287 $repo = $m[2]; 288 return 'https://github.com/' . $user . '/' . $repo . '.git'; 289 } 290 291 // match gitorious repos 292 if (preg_match('/gitorious.org\/([^\/]+)\/([^\/]+)?/i', $repourl, $m)) { 293 $user = $m[1]; 294 $repo = $m[2]; 295 if (!$repo) $repo = $user; 296 297 return 'https://git.gitorious.org/' . $user . '/' . $repo . '.git'; 298 } 299 300 // match bitbucket repos - most people seem to use mercurial there though 301 if (preg_match('/bitbucket\.org\/([^\/]+)\/([^\/]+)/i', $repourl, $m)) { 302 $user = $m[1]; 303 $repo = $m[2]; 304 return 'https://bitbucket.org/' . $user . '/' . $repo . '.git'; 305 } 306 307 return false; 308 } 309} 310 311// Main 312$cli = new GitToolCLI(); 313$cli->run(); 314