Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | n/a |
0 / 0 |
n/a |
0 / 0 |
CRAP | n/a |
0 / 0 |
|||
Repository | n/a |
0 / 0 |
n/a |
0 / 0 |
110 | n/a |
0 / 0 |
|||
__construct | n/a |
0 / 0 |
n/a |
0 / 0 |
2 | |||||
setPath | n/a |
0 / 0 |
n/a |
0 / 0 |
7 | |||||
getPath | n/a |
0 / 0 |
n/a |
0 / 0 |
1 | |||||
getActiveBranch | n/a |
0 / 0 |
n/a |
0 / 0 |
2 | |||||
getBranches | n/a |
0 / 0 |
n/a |
0 / 0 |
3 | |||||
run | n/a |
0 / 0 |
n/a |
0 / 0 |
6 | |||||
parseLines | n/a |
0 / 0 |
n/a |
0 / 0 |
4 | |||||
create | n/a |
0 / 0 |
n/a |
0 / 0 |
5 | |||||
status | n/a |
0 / 0 |
n/a |
0 / 0 |
1 | |||||
add | n/a |
0 / 0 |
n/a |
0 / 0 |
1 | |||||
rm | n/a |
0 / 0 |
n/a |
0 / 0 |
2 | |||||
parseFileList | n/a |
0 / 0 |
n/a |
0 / 0 |
2 | |||||
commit | n/a |
0 / 0 |
n/a |
0 / 0 |
2 | |||||
cloneTo | n/a |
0 / 0 |
n/a |
0 / 0 |
2 | |||||
cloneFrom | n/a |
0 / 0 |
n/a |
0 / 0 |
2 | |||||
cloneRemote | n/a |
0 / 0 |
n/a |
0 / 0 |
1 | |||||
clean | n/a |
0 / 0 |
n/a |
0 / 0 |
3 | |||||
createBranch | n/a |
0 / 0 |
n/a |
0 / 0 |
2 | |||||
getName | n/a |
0 / 0 |
n/a |
0 / 0 |
3 | |||||
getDirectoryPath | n/a |
0 / 0 |
n/a |
0 / 0 |
2 | |||||
getBranchesRemote | n/a |
0 / 0 |
n/a |
0 / 0 |
3 | |||||
checkout | n/a |
0 / 0 |
n/a |
0 / 0 |
1 | |||||
merge | n/a |
0 / 0 |
n/a |
0 / 0 |
1 | |||||
fetch | n/a |
0 / 0 |
n/a |
0 / 0 |
1 | |||||
createTag | n/a |
0 / 0 |
n/a |
0 / 0 |
1 | |||||
getTags | n/a |
0 / 0 |
n/a |
0 / 0 |
3 | |||||
push | n/a |
0 / 0 |
n/a |
0 / 0 |
1 | |||||
pull | n/a |
0 / 0 |
n/a |
0 / 0 |
1 | |||||
setDescription | n/a |
0 / 0 |
n/a |
0 / 0 |
1 | |||||
getDescription | n/a |
0 / 0 |
n/a |
0 / 0 |
1 | |||||
countFiles | n/a |
0 / 0 |
n/a |
0 / 0 |
1 | |||||
getLoc | n/a |
0 / 0 |
n/a |
0 / 0 |
8 | |||||
getContributors | n/a |
0 / 0 |
n/a |
0 / 0 |
5 | |||||
getCommitsCount | n/a |
0 / 0 |
n/a |
0 / 0 |
5 | |||||
getAdditionsRemovalsByContributor | n/a |
0 / 0 |
n/a |
0 / 0 |
4 | |||||
getRemote | n/a |
0 / 0 |
n/a |
0 / 0 |
1 | |||||
getCommitsBy | n/a |
0 / 0 |
n/a |
0 / 0 |
7 | |||||
getCommit | n/a |
0 / 0 |
n/a |
0 / 0 |
8 | |||||
getNewest | n/a |
0 / 0 |
n/a |
0 / 0 |
4 |
1 | <?php |
2 | /** |
3 | * Jingga |
4 | * |
5 | * PHP Version 8.1 |
6 | * |
7 | * @package phpOMS\Utils\Git |
8 | * @copyright Dennis Eichhorn |
9 | * @license OMS License 2.0 |
10 | * @version 1.0.0 |
11 | * @link https://jingga.app |
12 | */ |
13 | declare(strict_types=1); |
14 | |
15 | namespace phpOMS\Utils\Git; |
16 | |
17 | use phpOMS\System\File\PathException; |
18 | use phpOMS\Utils\StringUtils; |
19 | |
20 | /** |
21 | * Repository class |
22 | * |
23 | * @package phpOMS\Utils\Git |
24 | * @license OMS License 2.0 |
25 | * @link https://jingga.app |
26 | * @since 1.0.0 |
27 | * @codeCoverageIgnore |
28 | */ |
29 | class Repository |
30 | { |
31 | /** |
32 | * Repository path. |
33 | * |
34 | * @var string |
35 | * @since 1.0.0 |
36 | */ |
37 | private string $path = ''; |
38 | |
39 | /** |
40 | * Repository name. |
41 | * |
42 | * @var string |
43 | * @since 1.0.0 |
44 | */ |
45 | public string $name = ''; |
46 | |
47 | /** |
48 | * Bare repository. |
49 | * |
50 | * @var bool |
51 | * @since 1.0.0 |
52 | */ |
53 | private bool $bare = false; |
54 | |
55 | /** |
56 | * Current branch. |
57 | * |
58 | * @var Branch |
59 | * @since 1.0.0 |
60 | */ |
61 | private Branch $branch; |
62 | |
63 | /** |
64 | * Constructor |
65 | * |
66 | * @param string $path Repository path |
67 | * |
68 | * @since 1.0.0 |
69 | */ |
70 | public function __construct(string $path = '') |
71 | { |
72 | if (\is_dir($path)) { |
73 | $this->setPath($path); |
74 | } |
75 | |
76 | $this->branch = new Branch(); |
77 | } |
78 | |
79 | /** |
80 | * Set repository path. |
81 | * |
82 | * @param string $path Path to repository |
83 | * |
84 | * @return void |
85 | * |
86 | * @throws PathException |
87 | * |
88 | * @since 1.0.0 |
89 | */ |
90 | private function setPath(string $path) : void |
91 | { |
92 | if (!\is_dir($path) || \realpath($path) === false) { |
93 | throw new PathException($path); |
94 | } |
95 | |
96 | $this->path = \realpath($path); |
97 | |
98 | if (\is_dir($this->path . '/.git')) { |
99 | $this->bare = false; |
100 | } elseif (\is_file($this->path . '/config')) { // Is this a bare repo? |
101 | $parseIni = \parse_ini_file($this->path . '/config'); |
102 | |
103 | if ($parseIni !== false && $parseIni['bare']) { |
104 | $this->bare = true; |
105 | } |
106 | } |
107 | } |
108 | |
109 | /** |
110 | * Get repository path. |
111 | * |
112 | * @return string |
113 | * |
114 | * @since 1.0.0 |
115 | */ |
116 | public function getPath() : string |
117 | { |
118 | return $this->path; |
119 | } |
120 | |
121 | /** |
122 | * Get active Branch. |
123 | * |
124 | * @return Branch |
125 | * |
126 | * @since 1.0.0 |
127 | */ |
128 | public function getActiveBranch() : Branch |
129 | { |
130 | $branches = $this->getBranches(); |
131 | $active = \preg_grep('/^\*/', $branches); |
132 | |
133 | if (!\is_array($active)) { |
134 | return new Branch(); |
135 | } |
136 | |
137 | \reset($active); |
138 | |
139 | return new Branch(\current($active)); |
140 | } |
141 | |
142 | /** |
143 | * Get all branches. |
144 | * |
145 | * @return string[] |
146 | * |
147 | * @since 1.0.0 |
148 | */ |
149 | public function getBranches() : array |
150 | { |
151 | $branches = $this->run('branch'); |
152 | $result = []; |
153 | |
154 | foreach ($branches as $branch) { |
155 | $branch = \trim($branch, '* '); |
156 | |
157 | if ($branch !== '') { |
158 | $result[] = $branch; |
159 | } |
160 | } |
161 | |
162 | return $result; |
163 | } |
164 | |
165 | /** |
166 | * Run git command. |
167 | * |
168 | * @param string $cmd Command to run |
169 | * |
170 | * @return string[] |
171 | * |
172 | * @throws \Exception |
173 | * |
174 | * @since 1.0.0 |
175 | */ |
176 | private function run(string $cmd) : array |
177 | { |
178 | if (\strtolower((string) \substr(\PHP_OS, 0, 3)) === 'win') { |
179 | $cmd = 'cd ' . \escapeshellarg(\dirname(Git::getBin())) |
180 | . ' && ' . \basename(Git::getBin()) |
181 | . ' -C ' . \escapeshellarg($this->path) . ' ' |
182 | . $cmd; |
183 | } else { |
184 | $cmd = \escapeshellarg(Git::getBin()) |
185 | . ' -C ' . \escapeshellarg($this->path) . ' ' |
186 | . $cmd; |
187 | } |
188 | |
189 | $pipes = []; |
190 | $desc = [ |
191 | 1 => ['pipe', 'w'], |
192 | 2 => ['pipe', 'w'], |
193 | ]; |
194 | |
195 | $resource = \proc_open($cmd, $desc, $pipes, $this->path, null); |
196 | |
197 | if ($resource === false) { |
198 | throw new \Exception(); |
199 | } |
200 | |
201 | $stdout = \stream_get_contents($pipes[1]); |
202 | $stderr = \stream_get_contents($pipes[2]); |
203 | |
204 | foreach ($pipes as $pipe) { |
205 | \fclose($pipe); |
206 | } |
207 | |
208 | $status = \proc_close($resource); |
209 | |
210 | if ($status == -1) { |
211 | throw new \Exception((string) $stderr); |
212 | } |
213 | |
214 | return $this->parseLines(\trim($stdout === false ? '' : $stdout)); |
215 | } |
216 | |
217 | /** |
218 | * Parse lines. |
219 | * |
220 | * @param string $lines Result of git command |
221 | * |
222 | * @return string[] |
223 | * |
224 | * @since 1.0.0 |
225 | */ |
226 | private function parseLines(string $lines) : array |
227 | { |
228 | $lineArray = \preg_split('/\r\n|\n|\r/', $lines); |
229 | $lines = []; |
230 | |
231 | if ($lineArray === false) { |
232 | return $lines; |
233 | } |
234 | |
235 | foreach ($lineArray as $line) { |
236 | $temp = \preg_replace('/\s+/', ' ', \trim($line, ' ')); |
237 | |
238 | if (!empty($temp)) { |
239 | $lines[] = $temp; |
240 | } |
241 | } |
242 | |
243 | return $lines; |
244 | } |
245 | |
246 | /** |
247 | * Create repository |
248 | * |
249 | * @param string $source Create repository from source (optional, can be remote) |
250 | * |
251 | * @return void |
252 | * |
253 | * @throws \Exception |
254 | * |
255 | * @since 1.0.0 |
256 | */ |
257 | public function create(string $source = null) : void |
258 | { |
259 | if (!\is_dir($this->path) || \is_dir($this->path . '/.git')) { |
260 | throw new \Exception('Already repository'); |
261 | } |
262 | |
263 | if ($source !== null) { |
264 | \stripos($source, '//') !== false ? $this->cloneRemote($source) : $this->cloneFrom($source); |
265 | |
266 | return; |
267 | } |
268 | |
269 | $this->run('init'); |
270 | } |
271 | |
272 | /** |
273 | * Get status. |
274 | * |
275 | * @return string |
276 | * |
277 | * @since 1.0.0 |
278 | */ |
279 | public function status() : string |
280 | { |
281 | return \implode("\n", $this->run('status')); |
282 | } |
283 | |
284 | /** |
285 | * Files to add to commit. |
286 | * |
287 | * @param array|string $files Files to commit |
288 | * |
289 | * @return string |
290 | * |
291 | * @since 1.0.0 |
292 | */ |
293 | public function add(string | array $files = '*') : string |
294 | { |
295 | $files = $this->parseFileList($files); |
296 | |
297 | return \implode("\n", $this->run('add ' . $files . ' -v')); |
298 | } |
299 | |
300 | /** |
301 | * Remove file(s) from repository |
302 | * |
303 | * @param array|string $files Files to remove |
304 | * @param bool $cached ? |
305 | * |
306 | * @return string |
307 | * |
308 | * @since 1.0.0 |
309 | */ |
310 | public function rm(string | array $files = '*', bool $cached = false) : string |
311 | { |
312 | $files = $this->parseFileList($files); |
313 | |
314 | return \implode("\n", $this->run('rm ' . ($cached ? '--cached ' : '') . $files)); |
315 | } |
316 | |
317 | /** |
318 | * Remove file(s) from repository |
319 | * |
320 | * @param array|string $files Files to remove |
321 | * |
322 | * @return string |
323 | * |
324 | * @throws \InvalidArgumentException |
325 | * |
326 | * @since 1.0.0 |
327 | */ |
328 | private function parseFileList(string | array $files) : string |
329 | { |
330 | if (\is_array($files)) { |
331 | return '"' . \implode('" "', $files) . '"'; |
332 | } |
333 | |
334 | return $files; |
335 | } |
336 | |
337 | /** |
338 | * Commit files. |
339 | * |
340 | * @param Commit $commit Commit to commit |
341 | * @param bool $all Commit all |
342 | * |
343 | * @return string |
344 | * |
345 | * @since 1.0.0 |
346 | */ |
347 | public function commit(Commit $commit, bool $all = true) : string |
348 | { |
349 | return \implode("\n", $this->run('commit ' . ($all ? '-av' : '-v') . ' -m ' . \escapeshellarg($commit->getMessage()))); |
350 | } |
351 | |
352 | /** |
353 | * Clone repository to different directory |
354 | * |
355 | * @param string $target Target clone directory |
356 | * |
357 | * @return string |
358 | * |
359 | * @throws PathException in case the target is not a valid directory |
360 | * |
361 | * @since 1.0.0 |
362 | */ |
363 | public function cloneTo(string $target) : string |
364 | { |
365 | if (!\is_dir($target)) { |
366 | throw new PathException($target); |
367 | } |
368 | |
369 | return \implode("\n", $this->run('clone --local ' . $this->path . ' ' . $target)); |
370 | } |
371 | |
372 | /** |
373 | * Clone repository to current directory |
374 | * |
375 | * @param string $source Source repository to clone |
376 | * |
377 | * @return string |
378 | * |
379 | * @throws PathException in case the source repository is not valid |
380 | * |
381 | * @since 1.0.0 |
382 | */ |
383 | public function cloneFrom(string $source) : string |
384 | { |
385 | if (!\is_dir($source)) { |
386 | throw new PathException($source); |
387 | } |
388 | |
389 | return \implode("\n", $this->run('clone --local ' . $source . ' ' . $this->path)); |
390 | } |
391 | |
392 | /** |
393 | * Clone remote repository to current directory |
394 | * |
395 | * @param string $source Source repository to clone |
396 | * |
397 | * @return string |
398 | * |
399 | * @since 1.0.0 |
400 | */ |
401 | public function cloneRemote(string $source) : string |
402 | { |
403 | return \implode("\n", $this->run('clone ' . $source . ' ' . $this->path)); |
404 | } |
405 | |
406 | /** |
407 | * Clean. |
408 | * |
409 | * @param bool $dirs Directories? |
410 | * @param bool $force Force? |
411 | * |
412 | * @return string |
413 | * |
414 | * @since 1.0.0 |
415 | */ |
416 | public function clean(bool $dirs = false, bool $force = false) : string |
417 | { |
418 | return \implode("\n", $this->run('clean' . ($force ? ' -f' : '') . ($dirs ? ' -d' : ''))); |
419 | } |
420 | |
421 | /** |
422 | * Create local branch. |
423 | * |
424 | * @param Branch $branch Branch |
425 | * @param bool $force Force? |
426 | * |
427 | * @return string |
428 | * |
429 | * @since 1.0.0 |
430 | */ |
431 | public function createBranch(Branch $branch, bool $force = false) : string |
432 | { |
433 | return \implode("\n", $this->run('branch ' . ($force ? '-D' : '-d') . ' ' . $branch->name)); |
434 | } |
435 | |
436 | /** |
437 | * Get repository name. |
438 | * |
439 | * @return string |
440 | * |
441 | * @since 1.0.0 |
442 | */ |
443 | public function getName() : string |
444 | { |
445 | if (empty($this->name)) { |
446 | $path = $this->getDirectoryPath(); |
447 | $path = \strtr($path, '\\', '/'); |
448 | $path = \explode('/', $path); |
449 | $this->name = $path[\count($path) - ($this->bare ? 1 : 2)]; |
450 | } |
451 | |
452 | return $this->name; |
453 | } |
454 | |
455 | /** |
456 | * Get directory path. |
457 | * |
458 | * @return string |
459 | * |
460 | * @since 1.0.0 |
461 | */ |
462 | public function getDirectoryPath() : string |
463 | { |
464 | return $this->bare ? $this->path : $this->path . '/.git'; |
465 | } |
466 | |
467 | /** |
468 | * Get all remote branches. |
469 | * |
470 | * @return string[] |
471 | * |
472 | * @since 1.0.0 |
473 | */ |
474 | public function getBranchesRemote() : array |
475 | { |
476 | $branches = $this->run('branch -r'); |
477 | $result = []; |
478 | |
479 | foreach ($branches as $key => $branch) { |
480 | $branch = \trim($branch, '* '); |
481 | |
482 | if ($branch !== '') { |
483 | $result[] = $branch; |
484 | } |
485 | } |
486 | |
487 | return $result; |
488 | } |
489 | |
490 | /** |
491 | * Checkout. |
492 | * |
493 | * @param Branch $branch Branch to checkout |
494 | * |
495 | * @return string |
496 | * |
497 | * @since 1.0.0 |
498 | */ |
499 | public function checkout(Branch $branch) : string |
500 | { |
501 | $result = \implode("\n", $this->run('checkout ' . $branch->name)); |
502 | $this->branch = $branch; |
503 | |
504 | return $result; |
505 | } |
506 | |
507 | /** |
508 | * Merge with branch. |
509 | * |
510 | * @param Branch $branch Branch to merge from |
511 | * |
512 | * @return string |
513 | * |
514 | * @since 1.0.0 |
515 | */ |
516 | public function merge(Branch $branch) : string |
517 | { |
518 | return \implode("\n", $this->run('merge ' . $branch->name . ' --no-ff')); |
519 | } |
520 | |
521 | /** |
522 | * Fetch. |
523 | * |
524 | * @return string |
525 | * |
526 | * @since 1.0.0 |
527 | */ |
528 | public function fetch() : string |
529 | { |
530 | return \implode("\n", $this->run('fetch')); |
531 | } |
532 | |
533 | /** |
534 | * Create tag. |
535 | * |
536 | * @param Tag $tag Tag to create |
537 | * |
538 | * @return string |
539 | * |
540 | * @since 1.0.0 |
541 | */ |
542 | public function createTag(Tag $tag) : string |
543 | { |
544 | return \implode("\n", $this->run('tag -a ' . $tag->getName() . ' -m ' . \escapeshellarg($tag->getMessage()))); |
545 | } |
546 | |
547 | /** |
548 | * Get all tags. |
549 | * |
550 | * @param string $pattern Tag pattern |
551 | * |
552 | * @return Tag[] |
553 | * |
554 | * @since 1.0.0 |
555 | */ |
556 | public function getTags(string $pattern = '') : array |
557 | { |
558 | $pattern = empty($pattern) ? ' -l ' . $pattern : ''; |
559 | $lines = $this->run('tag' . $pattern); |
560 | $tags = []; |
561 | |
562 | foreach ($lines as $key => $tag) { |
563 | $tags[$tag] = new Tag($tag); |
564 | } |
565 | |
566 | return $tags; |
567 | } |
568 | |
569 | /** |
570 | * Push. |
571 | * |
572 | * @param string $remote Remote repository |
573 | * @param Branch $branch Branch to pull |
574 | * |
575 | * @return string |
576 | * |
577 | * @since 1.0.0 |
578 | */ |
579 | public function push(string $remote, Branch $branch) : string |
580 | { |
581 | $remote = \escapeshellarg($remote); |
582 | |
583 | return \implode("\n", $this->run('push --tags ' . $remote . ' ' . $branch->name)); |
584 | } |
585 | |
586 | /** |
587 | * Pull. |
588 | * |
589 | * @param string $remote Remote repository |
590 | * @param Branch $branch Branch to pull |
591 | * |
592 | * @return string |
593 | * |
594 | * @since 1.0.0 |
595 | */ |
596 | public function pull(string $remote, Branch $branch) : string |
597 | { |
598 | $remote = \escapeshellarg($remote); |
599 | |
600 | return \implode("\n", $this->run('pull ' . $remote . ' ' . $branch->name)); |
601 | } |
602 | |
603 | /** |
604 | * Set repository description. |
605 | * |
606 | * @param string $description Repository description |
607 | * |
608 | * @return void |
609 | * |
610 | * @since 1.0.0 |
611 | */ |
612 | public function setDescription(string $description) : void |
613 | { |
614 | \file_put_contents($this->getDirectoryPath(), $description); |
615 | } |
616 | |
617 | /** |
618 | * Get repository description. |
619 | * |
620 | * @return string |
621 | * |
622 | * @since 1.0.0 |
623 | */ |
624 | public function getDescription() : string |
625 | { |
626 | return (string) \file_get_contents($this->getDirectoryPath() . '/description'); |
627 | } |
628 | |
629 | /** |
630 | * Count files in repository. |
631 | * |
632 | * @return int |
633 | * |
634 | * @since 1.0.0 |
635 | */ |
636 | public function countFiles() : int |
637 | { |
638 | $lines = $this->run('ls-files'); |
639 | |
640 | return \count($lines); |
641 | } |
642 | |
643 | /** |
644 | * Get LOC. |
645 | * |
646 | * @param string[] $extensions Extensions whitelist |
647 | * |
648 | * @return int |
649 | * |
650 | * @since 1.0.0 |
651 | */ |
652 | public function getLoc(array $extensions = ['*']) : int |
653 | { |
654 | $lines = $this->run('ls-files'); |
655 | $loc = 0; |
656 | |
657 | foreach ($lines as $line) { |
658 | if ($extensions[0] !== '*' && !StringUtils::endsWith($line, $extensions)) { |
659 | continue; |
660 | } |
661 | |
662 | if (!\is_dir($path = $this->getDirectoryPath() . ($this->bare ? '/' : '/../') . $line)) { |
663 | return 0; |
664 | } |
665 | |
666 | $fh = \fopen($path, 'r'); |
667 | |
668 | if (!$fh) { |
669 | return 0; |
670 | } |
671 | |
672 | while (!\feof($fh)) { |
673 | \fgets($fh); |
674 | ++$loc; |
675 | } |
676 | |
677 | \fclose($fh); |
678 | } |
679 | |
680 | return $loc; |
681 | } |
682 | |
683 | /** |
684 | * Get contributors. |
685 | * |
686 | * @param \DateTime $start Start date |
687 | * @param \DateTime $end End date |
688 | * |
689 | * @return array<Author> |
690 | * |
691 | * @since 1.0.0 |
692 | */ |
693 | public function getContributors(\DateTime $start = null, \DateTime $end = null) : array |
694 | { |
695 | if ($start === null) { |
696 | $start = new \DateTime('1970-12-31'); |
697 | } |
698 | |
699 | if ($end === null) { |
700 | $end = new \DateTime('now'); |
701 | } |
702 | |
703 | $lines = $this->run('shortlog -s -n --since="' . $start->format('Y-m-d') . '" --before="' . $end->format('Y-m-d') . '" --all'); |
704 | $contributors = []; |
705 | |
706 | foreach ($lines as $line) { |
707 | \preg_match('/^[0-9]*/', $line, $matches); |
708 | |
709 | $author = \substr($line, \strlen($matches[0]) + 1); |
710 | $contributor = new Author($author === false ? '' : $author); |
711 | $contributor->setCommitCount($this->getCommitsCount($start, $end)[$contributor->name]); |
712 | |
713 | $addremove = $this->getAdditionsRemovalsByContributor($contributor, $start, $end); |
714 | $contributor->setAdditionCount($addremove['added']); |
715 | $contributor->setRemovalCount($addremove['removed']); |
716 | |
717 | $contributors[] = $contributor; |
718 | } |
719 | |
720 | return $contributors; |
721 | } |
722 | |
723 | /** |
724 | * Count commits. |
725 | * |
726 | * @param \DateTime $start Start date |
727 | * @param \DateTime $end End date |
728 | * |
729 | * @return array<string, int> |
730 | * |
731 | * @since 1.0.0 |
732 | */ |
733 | public function getCommitsCount(\DateTime $start = null, \DateTime $end = null) : array |
734 | { |
735 | if ($start === null) { |
736 | $start = new \DateTime('1970-12-31'); |
737 | } |
738 | |
739 | if ($end === null) { |
740 | $end = new \DateTime('now'); |
741 | } |
742 | |
743 | $lines = $this->run('shortlog -s -n --since="' . $start->format('Y-m-d') . '" --before="' . $end->format('Y-m-d') . '" --all'); |
744 | $commits = []; |
745 | |
746 | foreach ($lines as $line) { |
747 | \preg_match('/^[0-9]*/', $line, $matches); |
748 | |
749 | $temp = \substr($line, \strlen($matches[0]) + 1); |
750 | if ($temp !== false) { |
751 | $commits[$temp] = (int) $matches[0]; |
752 | } |
753 | } |
754 | |
755 | return $commits; |
756 | } |
757 | |
758 | /** |
759 | * Get additions and removals from contributor. |
760 | * |
761 | * @param Author $author Author |
762 | * @param \DateTime $start Start date |
763 | * @param \DateTime $end End date |
764 | * |
765 | * @return array ['added' => ?, 'removed'=> ?] |
766 | * |
767 | * @since 1.0.0 |
768 | */ |
769 | public function getAdditionsRemovalsByContributor(Author $author, \DateTime $start = null, \DateTime $end = null) : array |
770 | { |
771 | if ($start === null) { |
772 | $start = new \DateTime('1900-01-01'); |
773 | } |
774 | |
775 | if ($end === null) { |
776 | $end = new \DateTime('now'); |
777 | } |
778 | |
779 | $addremove = ['added' => 0, 'removed' => 0]; |
780 | $lines = $this->run( |
781 | 'log --author=' . \escapeshellarg($author->name) |
782 | . ' --since="' . $start->format('Y-m-d') |
783 | . '" --before="' . $end->format('Y-m-d') |
784 | . '" --pretty=tformat: --numstat' |
785 | ); |
786 | |
787 | foreach ($lines as $line) { |
788 | $nums = \explode(' ', $line); |
789 | |
790 | $addremove['added'] += $nums[0]; |
791 | $addremove['removed'] += $nums[1]; |
792 | } |
793 | |
794 | return $addremove; |
795 | } |
796 | |
797 | /** |
798 | * Get remote. |
799 | * |
800 | * @return string |
801 | * |
802 | * @since 1.0.0 |
803 | */ |
804 | public function getRemote() : string |
805 | { |
806 | return \implode("\n", $this->run('config --get remote.origin.url')); |
807 | } |
808 | |
809 | /** |
810 | * Get commits by author. |
811 | * |
812 | * @param \DateTime $start Commits from |
813 | * @param \DateTime $end Commits to |
814 | * @param Author $author Commits by author |
815 | * |
816 | * @return Commit[] |
817 | * |
818 | * @since 1.0.0 |
819 | */ |
820 | public function getCommitsBy(\DateTime $start = null, \DateTime $end = null, Author $author = null) : array |
821 | { |
822 | if ($start === null) { |
823 | $start = new \DateTime('1970-12-31'); |
824 | } |
825 | |
826 | if ($end === null) { |
827 | $end = new \DateTime('now'); |
828 | } |
829 | |
830 | $author = $author === null ? '' : ' --author=' . \escapeshellarg($author->name) . ''; |
831 | |
832 | $lines = $this->run( |
833 | 'git log --before="' . $end->format('Y-m-d') |
834 | . '" --after="' . $start->format('Y-m-d') . '"' |
835 | . $author . ' --reverse --date=short'); |
836 | |
837 | $count = \count($lines); |
838 | $commits = []; |
839 | |
840 | for ($i = 0; $i < $count; ++$i) { |
841 | $match = \preg_match('/[0-9ABCDEFabcdef]{40}/', $lines[$i], $matches); |
842 | |
843 | if ($match !== false && $match !== 0) { |
844 | $commit = $this->getCommit($matches[0]); |
845 | $commits[$commit->getId()] = $commit; |
846 | } |
847 | } |
848 | |
849 | return $commits; |
850 | } |
851 | |
852 | /** |
853 | * Get commit by id. |
854 | * |
855 | * @param string $commit Commit id |
856 | * |
857 | * @return Commit |
858 | * |
859 | * @throws \Exception |
860 | * |
861 | * @since 1.0.0 |
862 | */ |
863 | public function getCommit(string $commit) : Commit |
864 | { |
865 | $lines = $this->run('show --name-only ' . \escapeshellarg($commit)); |
866 | $count = \count($lines); |
867 | |
868 | if (empty($lines)) { |
869 | return new NullCommit(); |
870 | } |
871 | |
872 | \preg_match('/[0-9ABCDEFabcdef]{40}/', $lines[0], $matches); |
873 | |
874 | if (!isset($matches[0]) || \strlen($matches[0]) !== 40) { |
875 | throw new \Exception('Invalid commit id'); |
876 | } |
877 | |
878 | if (StringUtils::startsWith($lines[1], 'Merge')) { |
879 | return new Commit(); |
880 | } |
881 | |
882 | $author = \explode(':', $lines[1] ?? ''); |
883 | $author = \count($author) < 2 ? ['none', 'none'] : \explode('<', \trim($author[1] ?? '')); |
884 | |
885 | $date = \substr($lines[2] ?? '', 6); |
886 | if ($date === false) { |
887 | $date = 'now'; |
888 | } |
889 | |
890 | $commit = new Commit($matches[0]); |
891 | $commit->setAuthor(new Author(\trim($author[0] ?? ''), \rtrim($author[1] ?? '', '>'))); |
892 | $commit->setDate(new \DateTime(\trim($date))); |
893 | $commit->setMessage($lines[3]); |
894 | $commit->setTag(new Tag()); |
895 | $commit->setRepository($this); |
896 | $commit->setBranch($this->branch); |
897 | |
898 | for ($i = 4; $i < $count; ++$i) { |
899 | $commit->addFile($lines[$i]); |
900 | } |
901 | |
902 | return $commit; |
903 | } |
904 | |
905 | /** |
906 | * Get newest commit. |
907 | * |
908 | * @param int $limit Limit of commits |
909 | * |
910 | * @return Commit |
911 | * |
912 | * @throws \Exception |
913 | * |
914 | * @since 1.0.0 |
915 | */ |
916 | public function getNewest(int $limit = 1) : Commit |
917 | { |
918 | $lines = $this->run('log -n ' . $limit); |
919 | |
920 | if (empty($lines)) { |
921 | return new NullCommit(); |
922 | } |
923 | |
924 | \preg_match('/[0-9ABCDEFabcdef]{40}/', $lines[0], $matches); |
925 | |
926 | if (!isset($matches[0]) || \strlen($matches[0]) !== 40) { |
927 | throw new \Exception('Invalid commit id'); |
928 | } |
929 | |
930 | return $this->getCommit($matches[0]); |
931 | } |
932 | } |