1#!/usr/bin/env php
2<?php
3
4use splitbrain\phpcli\CLI;
5use splitbrain\phpcli\Options;
6
7if (!defined('DOKU_INC')) define('DOKU_INC', realpath(__DIR__ . '/../') . '/');
8define('NOSESSION', 1);
9require_once(DOKU_INC . 'inc/init.php');
10
11/**
12 * Easily manage DokuWiki git repositories
13 *
14 * @author Andreas Gohr <andi@splitbrain.org>
15 */
16class 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