Passed
Push — master ( 28761b...b907cb )
by Arne
03:35
created

Git::ensureGitVersionSupported()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 9
c 0
b 0
f 0
ccs 0
cts 8
cp 0
rs 9.6666
cc 2
eloc 7
nc 2
nop 1
crap 6
1
<?php
2
namespace TheSeer\phpDox\Generator\Enricher {
3
4
    use TheSeer\fDOM\fDOMDocument;
5
    use TheSeer\fDOM\fDOMElement;
6
    use TheSeer\phpDox\Generator\ClassStartEvent;
7
    use TheSeer\phpDox\Generator\InterfaceStartEvent;
8
    use TheSeer\phpDox\Generator\PHPDoxEndEvent;
9
    use TheSeer\phpDox\Generator\PHPDoxStartEvent;
10
    use TheSeer\phpDox\Generator\TraitStartEvent;
11
12
    class Git extends AbstractEnricher implements FullEnricherInterface {
13
14
        const GITNS = 'http://xml.phpdox.net/gitlog';
15
        /**
16
         * @var bool
17
         */
18
        private $noGitAvailable = false;
19
20
        /**
21
         * Array of tokens for git log
22
         * see git log --help for more details
23
         *
24
         * @var array
25
         */
26
        private $tokens = array('H','aE','aN','cE','cN','at','ct');
27
28
        /**
29
         * @var GitConfig
30
         */
31
        private $config;
32
33
        /**
34
         * @var fDOMDocument
35
         */
36
        private $cacheDom;
37
38
        /**
39
         * @var bool
40
         */
41
        private $cacheDirty = false;
42
43
        /**
44
         * @var string
45
         */
46
        private $commitSha1;
47
48
        public function __construct(GitConfig $config) {
49
            $this->ensureExecFunctionEnabled();
50
            $this->ensureGitVersionSupported($config->getGitBinary());
51
            $this->config = $config;
52
        }
53
54
        /**
55
         * @return string
56
         */
57
        public function getName() {
58
            return 'GIT information';
59
        }
60
61
        public function enrichStart(PHPDoxStartEvent $event) {
62
            $dom = $event->getIndex()->asDom();
63
            /** @var fDOMElement $enrichtment */
64
            $enrichtment = $this->getEnrichtmentContainer($dom->documentElement, 'git');
65
66
            $binary = $this->config->getGitBinary();
67
68
            $devNull = mb_strtolower(mb_substr(PHP_OS, 0, 3)) == 'win' ? 'nul' : '/dev/null';
69
70
            $cwd = getcwd();
71
            chdir($this->config->getSourceDirectory());
72
            $describe = exec($binary . ' describe --always --dirty 2>'.$devNull, $foo, $rc);
73
            if ($rc !== 0) {
74
                $enrichtment->appendChild(
75
                    $dom->createComment('Not a git repository or no git binary available')
76
                );
77
                chdir($cwd);
78
                $this->noGitAvailable = true;
79
                return;
80
            }
81
82
            exec($binary . ' tag 2>'.$devNull, $tags, $rc);
83
            if (count($tags)) {
84
                $tagsNode = $enrichtment->appendElementNS(self::GITNS, 'tags');
85
                foreach($tags as $tagName) {
86
                    $tag = $tagsNode->appendElementNS(self::GITNS, 'tag');
87
                    $tag->setAttribute('name', $tagName);
88
                }
89
            }
90
91
            $currentBranch = 'master';
92
            exec($binary . ' branch --no-color 2>'.$devNull, $branches, $rc);
93
            if (count($branches)) {
94
                $branchesNode = $enrichtment->appendElementNS(self::GITNS, 'branches');
95
                foreach($branches as $branchName) {
96
                    $branch = $branchesNode->appendElementNS(self::GITNS, 'branch');
97
                    if ($branchName[0] == '*') {
98
                        $branchName = trim(mb_substr($branchName, 1));
99
                        $currentBranch = $branchName;
100
                    } else {
101
                        $branchName = trim($branchName);
102
                    }
103
                    $branch->setAttribute('name', $branchName);
104
                }
105
            }
106
107
            $current = $enrichtment->appendElementNS(self::GITNS, 'current');
108
            $current->setAttribute('describe', $describe);
109
            $current->setAttribute('branch', $currentBranch);
110
111
            $this->commitSha1 = exec($binary . " rev-parse HEAD 2>".$devNull);
112
            $current->setAttribute('commit', $this->commitSha1);
113
114
            chdir($cwd);
115
        }
116
117
        public function enrichClass(ClassStartEvent $event) {
118
            $this->enrichByFile($event->getClass()->asDom());
119
        }
120
121
        public function enrichInterface(InterfaceStartEvent $event) {
122
            $this->enrichByFile($event->getInterface()->asDom());
123
        }
124
125
        public function enrichTrait(TraitStartEvent $event) {
126
            $this->enrichByFile($event->getTrait()->asDom());
127
        }
128
129
        public function enrichEnd(PHPDoxEndEvent $event) {
130
            if ($this->cacheDirty) {
131
                $path = dirname($this->config->getLogfilePath());
132
                if (!file_exists($path)) {
133
                    mkdir($path, 0777, true);
134
                }
135
                $this->cacheDom->save($this->config->getLogfilePath());
136
            }
137
        }
138
139
        private function enrichByFile(fDOMDocument $dom) {
140
            if ($this->noGitAvailable) {
141
                return;
142
            }
143
            $fileNode = $dom->queryOne('//phpdox:file');
144
            if (!$fileNode) {
0 ignored issues
show
introduced by Arne Blankerts
$fileNode is of type TheSeer\fDOM\fDOMNode, thus it always evaluated to true.
Loading history...
145
                return;
146
            }
147
148
            /** @var fDOMElement $enrichtment */
149
            $enrichtment = $this->getEnrichtmentContainer($dom->documentElement, 'git');
150
            if (!$this->config->doLogProcessing()) {
151
                $enrichtment->appendChild(
152
                    $dom->createComment('GitEnricher: Log processing disabled in configuration ')
153
                );
154
                return;
155
            }
156
157
            if ($this->loadFromCache($fileNode, $enrichtment)) {
0 ignored issues
show
Bug introduced by Arne Blankerts
$fileNode of type TheSeer\fDOM\fDOMNode is incompatible with the type TheSeer\fDOM\fDOMElement expected by parameter $fileNode of TheSeer\phpDox\Generator...er\Git::loadFromCache(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

157
            if ($this->loadFromCache(/** @scrutinizer ignore-type */ $fileNode, $enrichtment)) {
Loading history...
158
                return;
159
            }
160
161
            try {
162
                $count = 0;
163
                $limit = $this->config->getLogLimit();
164
                $log = $this->getLogHistory($fileNode->getAttribute('realpath'));
0 ignored issues
show
Bug introduced by Arne Blankerts
The method getAttribute() does not exist on TheSeer\fDOM\fDOMNode. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

164
                $log = $this->getLogHistory($fileNode->/** @scrutinizer ignore-call */ getAttribute('realpath'));

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
165
                $block = array();
166
167
                foreach($log as $line) {
168
                    if ($line == '[EOF]') {
169
                        $this->addCommit($enrichtment, $this->tokens, $block);
170
                        $block = array();
171
                        $count++;
172
                        if ($count > $limit) {
173
                            break;
174
                        }
175
                        continue;
176
                    }
177
                    $block[] = $line;
178
                }
179
180
                $this->addToCache($fileNode, $enrichtment);
0 ignored issues
show
Bug introduced by Arne Blankerts
$fileNode of type TheSeer\fDOM\fDOMNode is incompatible with the type TheSeer\fDOM\fDOMElement expected by parameter $fileNode of TheSeer\phpDox\Generator...icher\Git::addToCache(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

180
                $this->addToCache(/** @scrutinizer ignore-type */ $fileNode, $enrichtment);
Loading history...
181
182
            } catch (GitEnricherException $e) {
183
                $enrichtment->appendChild(
184
                    $dom->createComment('GitEnricher Error: ' . $e->getMessage())
185
                );
186
            }
187
188
        }
189
190
        private function addCommit(fDOMElement $enrichment, array $tokens, array $block) {
191
            list($data, $text) = array_chunk($block, count($tokens));
192
193
            $data = array_combine($tokens, $data);
194
195
            $commit = $enrichment->appendElementNS(self::GITNS, 'commit');
196
            $commit->setAttribute('sha1', $data['H']);
197
198
            $author = $commit->appendElementNS(self::GITNS, 'author');
199
            $author->setAttribute('email', $data['aE']);
200
            $author->setAttribute('name', $data['aN']);
201
            $author->setAttribute('time', date('c', $data['at']));
202
            $author->setAttribute('unixtime', $data['at']);
203
204
            $commiter = $commit->appendElementNS(self::GITNS, 'commiter');
205
            $commiter->setAttribute('email', $data['cE']);
206
            $commiter->setAttribute('name', $data['cN']);
207
            $commiter->setAttribute('time', date('c', $data['ct']));
208
            $commiter->setAttribute('unixtime', $data['ct']);
209
210
            $message = $commit->appendElementNS(self::GITNS, 'message');
211
            $message->appendTextNode(trim(join("\n", $text)));
212
        }
213
214
215
        private function getLogHistory($filename) {
216
            /*
217
             * H:8283723b40725a91c684e27c0c0449b959a48740
218
             * aE:[email protected]
219
             * aN:Arne Blankerts
220
             * cE:[email protected]
221
             * cN:Arne Blankerts
222
             * at:1375611883
223
             * ct:1375836305
224
             * {commit message text}
225
             * [EOF]
226
             *
227
             * see git log --help for more details
228
             *
229
             * The logic of addCommit assumes the commit message to be last
230
             */
231
            $format = '%' . join('%n%', $this->tokens) . '%n%B%n[EOF]';
232
233
            $cwd = getcwd();
234
            if (!file_exists($filename)) {
235
                throw new GitEnricherException('Error getting log history for file ' . $filename . ' (file not found)', GitEnricherException::FetchingHistoryFailed);
236
            }
237
            chdir(dirname($filename));
238
            $fname = escapeshellarg(basename($filename));
239
            exec(sprintf($this->config->getGitBinary() . ' log --no-color --follow --pretty=format:"%s" %s', $format, $fname), $log, $rc);
240
            chdir($cwd);
241
            if ($rc !== 0) {
242
                throw new GitEnricherException('Error getting log history for file ' . $filename, GitEnricherException::FetchingHistoryFailed);
243
            }
244
            return $log;
245
        }
246
247
        private function loadFromCache(fDOMElement $fileNode, fDOMElement $enrichment) {
248
            $dom = $this->getCacheDom();
249
            $fields = array(
250
                'path' => $fileNode->getAttribute('path'),
251
                'file' => $fileNode->getAttribute('file')
252
            );
253
            $query = $dom->prepareQuery('//*[@path = :path and @file = :file]', $fields);
254
            $cacheNode = $dom->queryOne($query);
255
            if (!$cacheNode) {
0 ignored issues
show
introduced by Arne Blankerts
$cacheNode is of type TheSeer\fDOM\fDOMNode, thus it always evaluated to true.
Loading history...
256
                return false;
257
            }
258
            foreach($cacheNode->childNodes as $child) {
259
                $enrichment->appendChild(
260
                    $enrichment->ownerDocument->importNode($child, true)
261
                );
262
            }
263
            return true;
264
        }
265
266
        private function addToCache(fDOMElement $fileNode, fDOMElement $enrichment) {
267
            $dom = $this->getCacheDom();
268
            $import = $dom->createElementNS(self::GITNS, 'file');
269
            foreach($fileNode->attributes as $attr) {
270
                $import->appendChild(
271
                    $dom->importNode($attr)
272
                );
273
            }
274
            foreach($enrichment->childNodes as $node) {
275
                $import->appendChild(
276
                    $dom->importNode($node, true)
277
                );
278
            }
279
            $dom->documentElement->appendChild($import);
280
            $this->cacheDirty = true;
281
        }
282
283
        private function getCacheDom() {
284
            if ($this->cacheDom === NULL) {
285
                $this->cacheDom = new fDOMDocument();
286
                $cacheFile = $this->config->getLogfilePath();
287
                if (file_exists($cacheFile)) {
288
                    $this->cacheDom->load($cacheFile);
289
290
                    $sha1 = $this->cacheDom->documentElement->getAttribute('sha1');
291
                    $cwd = getcwd();
292
                    chdir($this->config->getSourceDirectory());
293
                    exec($this->config->getGitBinary() . ' diff --name-only ' . $sha1, $files, $rc);
294
                    foreach($files as $file) {
295
                        $fields = array(
296
                            'path' => dirname($file),
297
                            'file' => basename($file)
298
                        );
299
                        $query = $this->cacheDom->prepareQuery('//*[@path = :path and @file = :file]', $fields);
300
                        $node = $this->cacheDom->queryOne($query);
301
                        if (!$node) {
302
                            continue;
303
                        }
304
                        $node->parentNode->removeChild($node);
305
                    }
306
                    chdir($cwd);
307
                } else {
308
                    $this->cacheDom->loadXML('<?xml version="1.0" ?><gitlog xmlns="' . self::GITNS . '" />');
309
                    $this->cacheDom->documentElement->setAttribute('sha1', $this->commitSha1);
310
                }
311
            }
312
            return $this->cacheDom;
313
        }
314
315
        /**
316
         * @throws GitEnricherException
317
         */
318
        private function ensureExecFunctionEnabled() {
319
            if (strpos(ini_get('disable_functions'), 'exec') !== FALSE) {
320
                throw new GitEnricherException(
321
                    'The use of "exec" has been disabled in php.ini but is required for this enricher',
322
                    GitEnricherException::ExecDisabled
323
                );
324
            }
325
        }
326
327
        private function ensureGitVersionSupported($binary) {
328
            $output = exec(sprintf('%s --version', $binary));
329
            $parts = explode(' ', $output);
330
            $version = array_pop($parts);
331
332
            if (version_compare($version, '1.7.2', '<=')) {
333
                throw new GitEnricherException(
334
                    sprintf('Your version of GIT is too old. Please upgrade to at least version 1.7.2 (Found: %s)', $version),
335
                    GitEnricherException::GitVersionTooOld
336
                );
337
338
            }
339
        }
340
341
    }
342
343
}
344