xref: /dokuwiki/bin/gittool.php (revision ddc6a58bcb9dd34a28f586d1e6f0dbe6bfbe6524)
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