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