xref: /dokuwiki/bin/gittool.php (revision ddc6a58bcb9dd34a28f586d1e6f0dbe6bfbe6524)
1cbfa4829SPhy#!/usr/bin/env php
2c39ae2c9SAndreas Gohr<?php
3cbeaa4a0SAndreas Gohr
442469e71SAnna Dabrowskause dokuwiki\plugin\extension\Extension;
51a7e82cdSAnna Dabrowskause dokuwiki\plugin\extension\Installer;
6cbeaa4a0SAndreas Gohruse splitbrain\phpcli\CLI;
7cbeaa4a0SAndreas Gohruse splitbrain\phpcli\Options;
8cbeaa4a0SAndreas Gohr
9b1f206e1SAndreas Gohrif (!defined('DOKU_INC')) define('DOKU_INC', realpath(__DIR__ . '/../') . '/');
10c39ae2c9SAndreas Gohrdefine('NOSESSION', 1);
11c39ae2c9SAndreas Gohrrequire_once(DOKU_INC . 'inc/init.php');
12c39ae2c9SAndreas Gohr
13c39ae2c9SAndreas Gohr/**
14c39ae2c9SAndreas Gohr * Easily manage DokuWiki git repositories
15c39ae2c9SAndreas Gohr *
16c39ae2c9SAndreas Gohr * @author Andreas Gohr <andi@splitbrain.org>
17c39ae2c9SAndreas Gohr */
188c7c53b0SAndreas Gohrclass GitToolCLI extends CLI
198c7c53b0SAndreas Gohr{
209fb66494SAndreas Gohr    /**
219fb66494SAndreas Gohr     * Register options and arguments on the given $options object
229fb66494SAndreas Gohr     *
23cbeaa4a0SAndreas Gohr     * @param Options $options
249fb66494SAndreas Gohr     * @return void
259fb66494SAndreas Gohr     */
26d868eb89SAndreas Gohr    protected function setup(Options $options)
27d868eb89SAndreas Gohr    {
289fb66494SAndreas Gohr        $options->setHelp(
299fb66494SAndreas Gohr            "Manage git repositories for DokuWiki and its plugins and templates.\n\n" .
309fb66494SAndreas Gohr            "$> ./bin/gittool.php clone gallery template:ach\n" .
319fb66494SAndreas Gohr            "$> ./bin/gittool.php repos\n" .
32ae1ce4a6SAndreas Gohr            "$> ./bin/gittool.php origin -v"
339fb66494SAndreas Gohr        );
34c39ae2c9SAndreas Gohr
359fb66494SAndreas Gohr        $options->registerArgument(
369fb66494SAndreas Gohr            'command',
379fb66494SAndreas Gohr            'Command to execute. See below',
389fb66494SAndreas Gohr            true
399fb66494SAndreas Gohr        );
40c39ae2c9SAndreas Gohr
419fb66494SAndreas Gohr        $options->registerCommand(
429fb66494SAndreas Gohr            'clone',
439fb66494SAndreas Gohr            'Tries to install a known plugin or template (prefix with template:) via git. Uses the DokuWiki.org ' .
449fb66494SAndreas Gohr            'plugin repository to find the proper git repository. Multiple extensions can be given as parameters'
459fb66494SAndreas Gohr        );
469fb66494SAndreas Gohr        $options->registerArgument(
479fb66494SAndreas Gohr            'extension',
489fb66494SAndreas Gohr            'name of the extension to install, prefix with \'template:\' for templates',
499fb66494SAndreas Gohr            true,
509fb66494SAndreas Gohr            'clone'
519fb66494SAndreas Gohr        );
52*ddc6a58bSAndreas Gohr        $options->registerOption(
53*ddc6a58bSAndreas Gohr            'prefer-https',
54*ddc6a58bSAndreas Gohr            'Prefer HTTPS over SSH for cloning (default: try SSH first, fallback to HTTPS)',
55*ddc6a58bSAndreas Gohr            false,
56*ddc6a58bSAndreas Gohr            false,
57*ddc6a58bSAndreas Gohr            'clone'
58*ddc6a58bSAndreas Gohr        );
59c39ae2c9SAndreas Gohr
609fb66494SAndreas Gohr        $options->registerCommand(
619fb66494SAndreas Gohr            'install',
629fb66494SAndreas Gohr            'The same as clone, but when no git source repository can be found, the extension is installed via ' .
639fb66494SAndreas Gohr            'download'
649fb66494SAndreas Gohr        );
659fb66494SAndreas Gohr        $options->registerArgument(
669fb66494SAndreas Gohr            'extension',
679fb66494SAndreas Gohr            'name of the extension to install, prefix with \'template:\' for templates',
689fb66494SAndreas Gohr            true,
699fb66494SAndreas Gohr            'install'
709fb66494SAndreas Gohr        );
71*ddc6a58bSAndreas Gohr        $options->registerOption(
72*ddc6a58bSAndreas Gohr            'prefer-https',
73*ddc6a58bSAndreas Gohr            'Prefer HTTPS over SSH for cloning (default: try SSH first, fallback to HTTPS)',
74*ddc6a58bSAndreas Gohr            false,
75*ddc6a58bSAndreas Gohr            false,
76*ddc6a58bSAndreas Gohr            'install'
77*ddc6a58bSAndreas Gohr        );
78c39ae2c9SAndreas Gohr
799fb66494SAndreas Gohr        $options->registerCommand(
809fb66494SAndreas Gohr            'repos',
819fb66494SAndreas Gohr            'Lists all git repositories found in this DokuWiki installation'
829fb66494SAndreas Gohr        );
83c39ae2c9SAndreas Gohr
849fb66494SAndreas Gohr        $options->registerCommand(
859fb66494SAndreas Gohr            '*',
869fb66494SAndreas Gohr            'Any unknown commands are assumed to be arguments to git and will be executed in all repositories ' .
879fb66494SAndreas Gohr            'found within this DokuWiki installation'
889fb66494SAndreas Gohr        );
89c39ae2c9SAndreas Gohr    }
90c39ae2c9SAndreas Gohr
91c39ae2c9SAndreas Gohr    /**
929fb66494SAndreas Gohr     * Your main program
939fb66494SAndreas Gohr     *
949fb66494SAndreas Gohr     * Arguments and options have been parsed when this is run
959fb66494SAndreas Gohr     *
96cbeaa4a0SAndreas Gohr     * @param Options $options
979fb66494SAndreas Gohr     * @return void
989fb66494SAndreas Gohr     */
99d868eb89SAndreas Gohr    protected function main(Options $options)
100d868eb89SAndreas Gohr    {
1019fb66494SAndreas Gohr        $command = $options->getCmd();
102cbeaa4a0SAndreas Gohr        $args = $options->getArgs();
103cbeaa4a0SAndreas Gohr        if (!$command) $command = array_shift($args);
1049fb66494SAndreas Gohr
1059fb66494SAndreas Gohr        switch ($command) {
1069fb66494SAndreas Gohr            case '':
1071c36b3d8SAndreas Gohr                echo $options->help();
1089fb66494SAndreas Gohr                break;
1099fb66494SAndreas Gohr            case 'clone':
110*ddc6a58bSAndreas Gohr                $this->cmdClone($args, $options->getOpt('prefer-https'));
1119fb66494SAndreas Gohr                break;
1129fb66494SAndreas Gohr            case 'install':
113*ddc6a58bSAndreas Gohr                $this->cmdInstall($args, $options->getOpt('prefer-https'));
1149fb66494SAndreas Gohr                break;
1159fb66494SAndreas Gohr            case 'repo':
1169fb66494SAndreas Gohr            case 'repos':
1172b2d0ba9SAndreas Gohr                $this->cmdRepos();
1189fb66494SAndreas Gohr                break;
1199fb66494SAndreas Gohr            default:
1202b2d0ba9SAndreas Gohr                $this->cmdGit($command, $args);
1219fb66494SAndreas Gohr        }
1229fb66494SAndreas Gohr    }
1239fb66494SAndreas Gohr
1249fb66494SAndreas Gohr    /**
125c39ae2c9SAndreas Gohr     * Tries to install the given extensions using git clone
126c39ae2c9SAndreas Gohr     *
12742ea7f44SGerrit Uitslag     * @param array $extensions
128*ddc6a58bSAndreas Gohr     * @param bool $preferHttps
129c39ae2c9SAndreas Gohr     */
130*ddc6a58bSAndreas Gohr    public function cmdClone($extensions, $preferHttps = false)
131d868eb89SAndreas Gohr    {
132b1f206e1SAndreas Gohr        $errors = [];
133b1f206e1SAndreas Gohr        $succeeded = [];
134c39ae2c9SAndreas Gohr
135c39ae2c9SAndreas Gohr        foreach ($extensions as $ext) {
136c39ae2c9SAndreas Gohr            $repo = $this->getSourceRepo($ext);
137c39ae2c9SAndreas Gohr
138c39ae2c9SAndreas Gohr            if (!$repo) {
1399fb66494SAndreas Gohr                $this->error("could not find a repository for $ext");
140c39ae2c9SAndreas Gohr                $errors[] = $ext;
141*ddc6a58bSAndreas Gohr            } elseif ($this->cloneExtension($ext, $repo, $preferHttps)) {
142c39ae2c9SAndreas Gohr                $succeeded[] = $ext;
143c39ae2c9SAndreas Gohr            } else {
144c39ae2c9SAndreas Gohr                $errors[] = $ext;
145c39ae2c9SAndreas Gohr            }
146c39ae2c9SAndreas Gohr        }
147c39ae2c9SAndreas Gohr
148c39ae2c9SAndreas Gohr        echo "\n";
149b1f206e1SAndreas Gohr        if ($succeeded) $this->success('successfully cloned the following extensions: ' . implode(', ', $succeeded));
150b1f206e1SAndreas Gohr        if ($errors) $this->error('failed to clone the following extensions: ' . implode(', ', $errors));
151c39ae2c9SAndreas Gohr    }
152c39ae2c9SAndreas Gohr
153c39ae2c9SAndreas Gohr    /**
154c39ae2c9SAndreas Gohr     * Tries to install the given extensions using git clone with fallback to install
155c39ae2c9SAndreas Gohr     *
15642ea7f44SGerrit Uitslag     * @param array $extensions
157*ddc6a58bSAndreas Gohr     * @param bool $preferHttps
158c39ae2c9SAndreas Gohr     */
159*ddc6a58bSAndreas Gohr    public function cmdInstall($extensions, $preferHttps = false)
160d868eb89SAndreas Gohr    {
161b1f206e1SAndreas Gohr        $errors = [];
162b1f206e1SAndreas Gohr        $succeeded = [];
163c39ae2c9SAndreas Gohr
164c39ae2c9SAndreas Gohr        foreach ($extensions as $ext) {
165c39ae2c9SAndreas Gohr            $repo = $this->getSourceRepo($ext);
166c39ae2c9SAndreas Gohr
167c39ae2c9SAndreas Gohr            if (!$repo) {
1689fb66494SAndreas Gohr                $this->info("could not find a repository for $ext");
1691a7e82cdSAnna Dabrowska
1701a7e82cdSAnna Dabrowska                try {
1711a7e82cdSAnna Dabrowska                    $installer = new Installer();
1721a7e82cdSAnna Dabrowska                    $this->info("installing $ext via download");
1731a7e82cdSAnna Dabrowska                    $installer->installFromId($ext);
1741a7e82cdSAnna Dabrowska                    $this->success("installed $ext via download");
175c39ae2c9SAndreas Gohr                    $succeeded[] = $ext;
176093fe67eSAndreas Gohr                } catch (\Exception) {
1771a7e82cdSAnna Dabrowska                    $this->error("failed to install $ext via download");
178c39ae2c9SAndreas Gohr                    $errors[] = $ext;
179c39ae2c9SAndreas Gohr                }
180*ddc6a58bSAndreas Gohr            } elseif ($this->cloneExtension($ext, $repo, $preferHttps)) {
181c39ae2c9SAndreas Gohr                $succeeded[] = $ext;
182c39ae2c9SAndreas Gohr            } else {
183c39ae2c9SAndreas Gohr                $errors[] = $ext;
184c39ae2c9SAndreas Gohr            }
185c39ae2c9SAndreas Gohr        }
186c39ae2c9SAndreas Gohr
187c39ae2c9SAndreas Gohr        echo "\n";
188b1f206e1SAndreas Gohr        if ($succeeded) $this->success('successfully installed the following extensions: ' . implode(', ', $succeeded));
189b1f206e1SAndreas Gohr        if ($errors) $this->error('failed to install the following extensions: ' . implode(', ', $errors));
190c39ae2c9SAndreas Gohr    }
191c39ae2c9SAndreas Gohr
192c39ae2c9SAndreas Gohr    /**
193c39ae2c9SAndreas Gohr     * Executes the given git command in every repository
194c39ae2c9SAndreas Gohr     *
195c39ae2c9SAndreas Gohr     * @param $cmd
196c39ae2c9SAndreas Gohr     * @param $arg
197c39ae2c9SAndreas Gohr     */
198d868eb89SAndreas Gohr    public function cmdGit($cmd, $arg)
199d868eb89SAndreas Gohr    {
200c39ae2c9SAndreas Gohr        $repos = $this->findRepos();
201c39ae2c9SAndreas Gohr
202b1f206e1SAndreas Gohr        $shell = array_merge(['git', $cmd], $arg);
203093fe67eSAndreas Gohr        $shell = array_map(escapeshellarg(...), $shell);
204b1f206e1SAndreas Gohr        $shell = implode(' ', $shell);
205c39ae2c9SAndreas Gohr
206c39ae2c9SAndreas Gohr        foreach ($repos as $repo) {
207c39ae2c9SAndreas Gohr            if (!@chdir($repo)) {
2089fb66494SAndreas Gohr                $this->error("Could not change into $repo");
209c39ae2c9SAndreas Gohr                continue;
210c39ae2c9SAndreas Gohr            }
211c39ae2c9SAndreas Gohr
2129fb66494SAndreas Gohr            $this->info("executing $shell in $repo");
213c39ae2c9SAndreas Gohr            $ret = 0;
214c39ae2c9SAndreas Gohr            system($shell, $ret);
215c39ae2c9SAndreas Gohr
216c39ae2c9SAndreas Gohr            if ($ret == 0) {
2179fb66494SAndreas Gohr                $this->success("git succeeded in $repo");
218c39ae2c9SAndreas Gohr            } else {
2199fb66494SAndreas Gohr                $this->error("git failed in $repo");
220c39ae2c9SAndreas Gohr            }
221c39ae2c9SAndreas Gohr        }
222c39ae2c9SAndreas Gohr    }
223c39ae2c9SAndreas Gohr
224c39ae2c9SAndreas Gohr    /**
225c39ae2c9SAndreas Gohr     * Simply lists the repositories
226c39ae2c9SAndreas Gohr     */
227d868eb89SAndreas Gohr    public function cmdRepos()
228d868eb89SAndreas Gohr    {
229c39ae2c9SAndreas Gohr        $repos = $this->findRepos();
230c39ae2c9SAndreas Gohr        foreach ($repos as $repo) {
231c39ae2c9SAndreas Gohr            echo "$repo\n";
232c39ae2c9SAndreas Gohr        }
233c39ae2c9SAndreas Gohr    }
234c39ae2c9SAndreas Gohr
235c39ae2c9SAndreas Gohr    /**
236c39ae2c9SAndreas Gohr     * Clones the extension from the given repository
237c39ae2c9SAndreas Gohr     *
238c39ae2c9SAndreas Gohr     * @param string $ext
239c39ae2c9SAndreas Gohr     * @param string $repo
240*ddc6a58bSAndreas Gohr     * @param bool $preferHttps
241c39ae2c9SAndreas Gohr     * @return bool
242c39ae2c9SAndreas Gohr     */
243*ddc6a58bSAndreas Gohr    private function cloneExtension($ext, $repo, $preferHttps = false)
244d868eb89SAndreas Gohr    {
2451b2deed9Sfiwswe        if (str_starts_with($ext, 'template:')) {
246c39ae2c9SAndreas Gohr            $target = fullpath(tpl_incdir() . '../' . substr($ext, 9));
247c39ae2c9SAndreas Gohr        } else {
248c39ae2c9SAndreas Gohr            $target = DOKU_PLUGIN . $ext;
249c39ae2c9SAndreas Gohr        }
250c39ae2c9SAndreas Gohr
251*ddc6a58bSAndreas Gohr        $ret = -1;
252*ddc6a58bSAndreas Gohr
253*ddc6a58bSAndreas Gohr        // try SSH clone first, unless the user prefers HTTPS
254*ddc6a58bSAndreas Gohr        if (!$preferHttps) {
255*ddc6a58bSAndreas Gohr            $sshUrl = $this->httpsToSshUrl($repo);
256*ddc6a58bSAndreas Gohr            $this->info("cloning $ext from $sshUrl");
257*ddc6a58bSAndreas Gohr            system("git clone $sshUrl $target", $ret);
258*ddc6a58bSAndreas Gohr            if ($ret !== 0) $this->info("SSH clone failed, trying HTTPS: $repo");
259*ddc6a58bSAndreas Gohr        }
260*ddc6a58bSAndreas Gohr
261*ddc6a58bSAndreas Gohr        // try HTTPS clone
262*ddc6a58bSAndreas Gohr        if ($ret !== 0) {
263*ddc6a58bSAndreas Gohr            $this->info("cloning $ext from $repo");
264c39ae2c9SAndreas Gohr            system("git clone $repo $target", $ret);
265*ddc6a58bSAndreas Gohr        }
266*ddc6a58bSAndreas Gohr
267c39ae2c9SAndreas Gohr        if ($ret === 0) {
2689fb66494SAndreas Gohr            $this->success("cloning of $ext succeeded");
269c39ae2c9SAndreas Gohr            return true;
270c39ae2c9SAndreas Gohr        } else {
2719fb66494SAndreas Gohr            $this->error("cloning of $ext failed");
272c39ae2c9SAndreas Gohr            return false;
273c39ae2c9SAndreas Gohr        }
274c39ae2c9SAndreas Gohr    }
275c39ae2c9SAndreas Gohr
276c39ae2c9SAndreas Gohr    /**
277*ddc6a58bSAndreas Gohr     * Convert a HTTPS repo URL to an SSH URL if possible
278*ddc6a58bSAndreas Gohr     *
279*ddc6a58bSAndreas Gohr     * @return string
280*ddc6a58bSAndreas Gohr     */
281*ddc6a58bSAndreas Gohr    private function httpsToSshUrl($url)
282*ddc6a58bSAndreas Gohr    {
283*ddc6a58bSAndreas Gohr        if (preg_match('/(github\.com|bitbucket\.org|gitorious\.org)\/([^\/]+)\/([^\/]+)/i', $url, $m)) {
284*ddc6a58bSAndreas Gohr            $host = $m[1];
285*ddc6a58bSAndreas Gohr            $user = $m[2];
286*ddc6a58bSAndreas Gohr            $repo = $m[3];
287*ddc6a58bSAndreas Gohr            return "git@$host:$user/$repo";
288*ddc6a58bSAndreas Gohr        }
289*ddc6a58bSAndreas Gohr        return $url;
290*ddc6a58bSAndreas Gohr    }
291*ddc6a58bSAndreas Gohr
292*ddc6a58bSAndreas Gohr    /**
293c39ae2c9SAndreas Gohr     * Returns all git repositories in this DokuWiki install
294c39ae2c9SAndreas Gohr     *
295c39ae2c9SAndreas Gohr     * Looks in root, template and plugin directories only.
296c39ae2c9SAndreas Gohr     *
297c39ae2c9SAndreas Gohr     * @return array
298c39ae2c9SAndreas Gohr     */
299d868eb89SAndreas Gohr    private function findRepos()
300d868eb89SAndreas Gohr    {
3019fb66494SAndreas Gohr        $this->info('Looking for .git directories');
302c39ae2c9SAndreas Gohr        $data = array_merge(
303c39ae2c9SAndreas Gohr            glob(DOKU_INC . '.git', GLOB_ONLYDIR),
304c39ae2c9SAndreas Gohr            glob(DOKU_PLUGIN . '*/.git', GLOB_ONLYDIR),
305c39ae2c9SAndreas Gohr            glob(fullpath(tpl_incdir() . '../') . '/*/.git', GLOB_ONLYDIR)
306c39ae2c9SAndreas Gohr        );
307c39ae2c9SAndreas Gohr
308c39ae2c9SAndreas Gohr        if (!$data) {
3099fb66494SAndreas Gohr            $this->error('Found no .git directories');
310c39ae2c9SAndreas Gohr        } else {
3119fb66494SAndreas Gohr            $this->success('Found ' . count($data) . ' .git directories');
312c39ae2c9SAndreas Gohr        }
313093fe67eSAndreas Gohr        $data = array_map(fullpath(...), array_map(dirname(...), $data));
314c39ae2c9SAndreas Gohr        return $data;
315c39ae2c9SAndreas Gohr    }
316c39ae2c9SAndreas Gohr
317c39ae2c9SAndreas Gohr    /**
318c39ae2c9SAndreas Gohr     * Returns the repository for the given extension
319c39ae2c9SAndreas Gohr     *
3201a7e82cdSAnna Dabrowska     * @param string $extensionId
32142ea7f44SGerrit Uitslag     * @return false|string
322c39ae2c9SAndreas Gohr     */
3231a7e82cdSAnna Dabrowska    private function getSourceRepo($extensionId)
324d868eb89SAndreas Gohr    {
3251a7e82cdSAnna Dabrowska        $extension = Extension::createFromId($extensionId);
3267e8500eeSGerrit Uitslag
3271a7e82cdSAnna Dabrowska        $repourl = $extension->getSourcerepoURL();
32868cf024bSAndreas Gohr        if (!$repourl) return false;
32968cf024bSAndreas Gohr
330c39ae2c9SAndreas Gohr        // match github repos
33168cf024bSAndreas Gohr        if (preg_match('/github\.com\/([^\/]+)\/([^\/]+)/i', $repourl, $m)) {
332c39ae2c9SAndreas Gohr            $user = $m[1];
333c39ae2c9SAndreas Gohr            $repo = $m[2];
334c39ae2c9SAndreas Gohr            return 'https://github.com/' . $user . '/' . $repo . '.git';
335c39ae2c9SAndreas Gohr        }
336c39ae2c9SAndreas Gohr
337c39ae2c9SAndreas Gohr        // match gitorious repos
33868cf024bSAndreas Gohr        if (preg_match('/gitorious.org\/([^\/]+)\/([^\/]+)?/i', $repourl, $m)) {
339c39ae2c9SAndreas Gohr            $user = $m[1];
340c39ae2c9SAndreas Gohr            $repo = $m[2];
341c39ae2c9SAndreas Gohr            if (!$repo) $repo = $user;
342c39ae2c9SAndreas Gohr
343c39ae2c9SAndreas Gohr            return 'https://git.gitorious.org/' . $user . '/' . $repo . '.git';
344c39ae2c9SAndreas Gohr        }
345c39ae2c9SAndreas Gohr
346c39ae2c9SAndreas Gohr        // match bitbucket repos - most people seem to use mercurial there though
34768cf024bSAndreas Gohr        if (preg_match('/bitbucket\.org\/([^\/]+)\/([^\/]+)/i', $repourl, $m)) {
348c39ae2c9SAndreas Gohr            $user = $m[1];
349c39ae2c9SAndreas Gohr            $repo = $m[2];
350c39ae2c9SAndreas Gohr            return 'https://bitbucket.org/' . $user . '/' . $repo . '.git';
351c39ae2c9SAndreas Gohr        }
352c39ae2c9SAndreas Gohr
353c39ae2c9SAndreas Gohr        return false;
354c39ae2c9SAndreas Gohr    }
355c39ae2c9SAndreas Gohr}
356c39ae2c9SAndreas Gohr
357b0b7909bSAndreas Gohr// Main
358b0b7909bSAndreas Gohr$cli = new GitToolCLI();
359b0b7909bSAndreas Gohr$cli->run();
360