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