1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/** |
4
|
|
|
* GitElephant - An abstraction layer for git written in PHP |
5
|
|
|
* Copyright (C) 2013 Matteo Giachino |
6
|
|
|
* |
7
|
|
|
* This program is free software: you can redistribute it and/or modify |
8
|
|
|
* it under the terms of the GNU General Public License as published by |
9
|
|
|
* the Free Software Foundation, either version 3 of the License, or |
10
|
|
|
* (at your option) any later version. |
11
|
|
|
* |
12
|
|
|
* This program is distributed in the hope that it will be useful, |
13
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
14
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
15
|
|
|
* GNU General Public License for more details. |
16
|
|
|
* |
17
|
|
|
* You should have received a copy of the GNU General Public License |
18
|
|
|
* along with this program. If not, see [http://www.gnu.org/licenses/]. |
19
|
|
|
*/ |
20
|
|
|
|
21
|
|
|
namespace GitElephant\Objects; |
22
|
|
|
|
23
|
|
|
use GitElephant\Command\BranchCommand; |
24
|
|
|
use GitElephant\Command\Caller\CallerInterface; |
25
|
|
|
use GitElephant\Command\MainCommand; |
26
|
|
|
use GitElephant\Command\RevListCommand; |
27
|
|
|
use GitElephant\Command\RevParseCommand; |
28
|
|
|
use GitElephant\Command\ShowCommand; |
29
|
|
|
use GitElephant\Objects\Commit\Message; |
30
|
|
|
use GitElephant\Repository; |
31
|
|
|
|
32
|
|
|
/** |
33
|
|
|
* The Commit object represent a commit |
34
|
|
|
* |
35
|
|
|
* @author Matteo Giachino <[email protected]> |
36
|
|
|
*/ |
37
|
|
|
class Commit implements TreeishInterface, \Countable |
38
|
|
|
{ |
39
|
|
|
/** |
40
|
|
|
* @var \GitElephant\Repository |
41
|
|
|
*/ |
42
|
|
|
private $repository; |
43
|
|
|
|
44
|
|
|
/** |
45
|
|
|
* @var string |
46
|
|
|
*/ |
47
|
|
|
private $ref; |
48
|
|
|
|
49
|
|
|
/** |
50
|
|
|
* sha |
51
|
|
|
* |
52
|
|
|
* @var string |
53
|
|
|
*/ |
54
|
|
|
private $sha; |
55
|
|
|
|
56
|
|
|
/** |
57
|
|
|
* tree |
58
|
|
|
* |
59
|
|
|
* @var string |
60
|
|
|
*/ |
61
|
|
|
private $tree; |
62
|
|
|
|
63
|
|
|
/** |
64
|
|
|
* the commit parents |
65
|
|
|
* |
66
|
|
|
* @var array |
67
|
|
|
*/ |
68
|
|
|
private $parents = []; |
69
|
|
|
|
70
|
|
|
/** |
71
|
|
|
* the Author instance for author |
72
|
|
|
* |
73
|
|
|
* @var Author |
74
|
|
|
*/ |
75
|
|
|
private $author; |
76
|
|
|
|
77
|
|
|
/** |
78
|
|
|
* the Author instance for committer |
79
|
|
|
* |
80
|
|
|
* @var Author |
81
|
|
|
*/ |
82
|
|
|
private $committer; |
83
|
|
|
|
84
|
|
|
/** |
85
|
|
|
* the Message instance |
86
|
|
|
* |
87
|
|
|
* @var Commit\Message |
88
|
|
|
*/ |
89
|
|
|
private $message; |
90
|
|
|
|
91
|
|
|
/** |
92
|
|
|
* the date for author |
93
|
|
|
* |
94
|
|
|
* @var \DateTime |
95
|
|
|
*/ |
96
|
|
|
private $datetimeAuthor; |
97
|
|
|
|
98
|
|
|
/** |
99
|
|
|
* the date for committer |
100
|
|
|
* |
101
|
|
|
* @var \Datetime |
102
|
|
|
*/ |
103
|
|
|
private $datetimeCommitter; |
104
|
|
|
|
105
|
|
|
/** |
106
|
|
|
* Class constructor |
107
|
|
|
* |
108
|
|
|
* @param \GitElephant\Repository $repository the repository |
109
|
|
|
* @param TreeishInterface|string $treeish a treeish reference |
110
|
|
|
*/ |
111
|
37 |
|
private function __construct(Repository $repository, $treeish = 'HEAD') |
112
|
|
|
{ |
113
|
37 |
|
$this->repository = $repository; |
114
|
37 |
|
$this->ref = $treeish; |
|
|
|
|
115
|
37 |
|
$this->parents = []; |
116
|
37 |
|
} |
117
|
|
|
|
118
|
|
|
/** |
119
|
|
|
* factory method to create a commit |
120
|
|
|
* |
121
|
|
|
* @param Repository $repository repository instance |
122
|
|
|
* @param string $message commit message |
123
|
|
|
* @param bool $stageAll automatically stage the dirty working tree. Alternatively call stage() on the repo |
124
|
|
|
* @param string|Author $author override the author for this commit |
125
|
|
|
* |
126
|
|
|
* @throws \RuntimeException |
127
|
|
|
* @throws \Symfony\Component\Process\Exception\LogicException |
128
|
|
|
* @throws \InvalidArgumentException |
129
|
|
|
* @throws \Symfony\Component\Process\Exception\InvalidArgumentException |
130
|
|
|
* @throws \Symfony\Component\Process\Exception\RuntimeException |
131
|
|
|
* @return Commit |
132
|
|
|
*/ |
133
|
5 |
|
public static function create( |
134
|
|
|
Repository $repository, |
135
|
|
|
string $message, |
136
|
|
|
bool $stageAll = false, |
137
|
|
|
$author = null |
138
|
|
|
): Commit { |
139
|
5 |
|
$repository->getCaller()->execute(MainCommand::getInstance($repository)->commit($message, $stageAll, $author)); |
140
|
|
|
|
141
|
5 |
|
return $repository->getCommit(); |
142
|
|
|
} |
143
|
|
|
|
144
|
|
|
/** |
145
|
|
|
* pick an existing commit |
146
|
|
|
* |
147
|
|
|
* @param Repository $repository repository |
148
|
|
|
* @param TreeishInterface|string $treeish treeish |
149
|
|
|
* |
150
|
|
|
* @throws \RuntimeException |
151
|
|
|
* @throws \Symfony\Component\Process\Exception\RuntimeException |
152
|
|
|
* @return Commit |
153
|
|
|
*/ |
154
|
18 |
|
public static function pick(Repository $repository, $treeish = null): Commit |
155
|
|
|
{ |
156
|
18 |
|
$commit = new self($repository, $treeish); |
|
|
|
|
157
|
18 |
|
$commit->createFromCommand(); |
158
|
|
|
|
159
|
18 |
|
return $commit; |
160
|
|
|
} |
161
|
|
|
|
162
|
|
|
/** |
163
|
|
|
* static generator to generate a single commit from output of command.show service |
164
|
|
|
* |
165
|
|
|
* @param \GitElephant\Repository $repository repository |
166
|
|
|
* @param array $outputLines output lines |
167
|
|
|
* |
168
|
|
|
* @return Commit |
169
|
|
|
*/ |
170
|
20 |
|
public static function createFromOutputLines(Repository $repository, array $outputLines): Commit |
171
|
|
|
{ |
172
|
20 |
|
$commit = new self($repository); |
173
|
20 |
|
$commit->parseOutputLines($outputLines); |
|
|
|
|
174
|
|
|
|
175
|
20 |
|
return $commit; |
176
|
|
|
} |
177
|
|
|
|
178
|
|
|
/** |
179
|
|
|
* get the commit properties from command |
180
|
|
|
* |
181
|
|
|
* @see ShowCommand::commitInfo |
182
|
|
|
*/ |
183
|
18 |
|
public function createFromCommand(): void |
184
|
|
|
{ |
185
|
18 |
|
$command = ShowCommand::getInstance($this->getRepository())->showCommit($this->ref); |
186
|
18 |
|
$outputLines = $this->getCaller()->execute($command, true, $this->getRepository()->getPath())->getOutputLines(); |
187
|
18 |
|
$this->parseOutputLines($outputLines); |
|
|
|
|
188
|
18 |
|
} |
189
|
|
|
|
190
|
|
|
/** |
191
|
|
|
* get the branches this commit is contained in |
192
|
|
|
* |
193
|
|
|
* @see BranchCommand::contains |
194
|
|
|
*/ |
195
|
|
|
public function getContainedIn(): array |
196
|
|
|
{ |
197
|
|
|
$command = BranchCommand::getInstance($this->getRepository())->contains($this->getSha()); |
198
|
|
|
|
199
|
|
|
return array_map('trim', (array) $this->getCaller()->execute($command)->getOutputLines(true)); |
200
|
|
|
} |
201
|
|
|
|
202
|
|
|
/** |
203
|
|
|
* number of commits that lead to this one |
204
|
|
|
* |
205
|
|
|
* @throws \RuntimeException |
206
|
|
|
* @throws \Symfony\Component\Process\Exception\LogicException |
207
|
|
|
* @throws \Symfony\Component\Process\Exception\InvalidArgumentException |
208
|
|
|
* @throws \Symfony\Component\Process\Exception\RuntimeException |
209
|
|
|
* @return int |
210
|
|
|
*/ |
211
|
3 |
|
public function count(): int |
212
|
|
|
{ |
213
|
3 |
|
$command = RevListCommand::getInstance($this->getRepository())->commitPath($this); |
214
|
|
|
|
215
|
3 |
|
return count($this->getCaller()->execute($command)->getOutputLines(true)); |
216
|
|
|
} |
217
|
|
|
|
218
|
1 |
|
public function getDiff(): \GitElephant\Objects\Diff\Diff |
219
|
|
|
{ |
220
|
1 |
|
return $this->getRepository()->getDiff($this); |
221
|
|
|
} |
222
|
|
|
|
223
|
|
|
/** |
224
|
|
|
* parse the output of a git command showing a commit |
225
|
|
|
* |
226
|
|
|
* @param Iterable $outputLines output lines |
227
|
|
|
*/ |
228
|
37 |
|
private function parseOutputLines($outputLines): void |
229
|
|
|
{ |
230
|
37 |
|
$message = []; |
231
|
37 |
|
foreach ($outputLines as $line) { |
232
|
37 |
|
$matches = []; |
233
|
37 |
|
if (preg_match('/^commit (\w+)$/', $line, $matches) > 0) { |
234
|
37 |
|
$this->sha = $matches[1]; |
235
|
|
|
} |
236
|
|
|
|
237
|
37 |
|
if (preg_match('/^tree (\w+)$/', $line, $matches) > 0) { |
238
|
37 |
|
$this->tree = $matches[1]; |
239
|
|
|
} |
240
|
|
|
|
241
|
37 |
|
if (preg_match('/^parent (\w+)$/', $line, $matches) > 0) { |
242
|
26 |
|
$this->parents[] = $matches[1]; |
243
|
|
|
} |
244
|
|
|
|
245
|
37 |
|
if (preg_match('/^author (.*) <(.*)> (\d+) (.*)$/', $line, $matches) > 0) { |
246
|
37 |
|
$author = new Author(); |
247
|
37 |
|
$author->setName($matches[1]); |
248
|
37 |
|
$author->setEmail($matches[2]); |
249
|
37 |
|
$this->author = $author; |
250
|
37 |
|
$date = \DateTime::createFromFormat('U O', $matches[3] . ' ' . $matches[4]); |
251
|
37 |
|
$date->modify($date->getOffset() . ' seconds'); |
252
|
37 |
|
$this->datetimeAuthor = $date; |
|
|
|
|
253
|
|
|
} |
254
|
|
|
|
255
|
37 |
|
if (preg_match('/^committer (.*) <(.*)> (\d+) (.*)$/', $line, $matches) > 0) { |
256
|
37 |
|
$committer = new Author(); |
257
|
37 |
|
$committer->setName($matches[1]); |
258
|
37 |
|
$committer->setEmail($matches[2]); |
259
|
37 |
|
$this->committer = $committer; |
260
|
37 |
|
$date = \DateTime::createFromFormat('U O', $matches[3] . ' ' . $matches[4]); |
261
|
37 |
|
$date->modify($date->getOffset() . ' seconds'); |
262
|
37 |
|
$this->datetimeCommitter = $date; |
|
|
|
|
263
|
|
|
} |
264
|
|
|
|
265
|
37 |
|
if (preg_match('/^ (.*)$/', $line, $matches)) { |
266
|
37 |
|
$message[] = $matches[1]; |
267
|
|
|
} |
268
|
|
|
} |
269
|
|
|
|
270
|
37 |
|
$this->message = new Message($message); |
271
|
37 |
|
} |
272
|
|
|
|
273
|
|
|
/** |
274
|
|
|
* Returns true if the commit is a root commit. Usually the first of the repository |
275
|
|
|
* |
276
|
|
|
* @return bool |
277
|
|
|
*/ |
278
|
3 |
|
public function isRoot(): bool |
279
|
|
|
{ |
280
|
3 |
|
return empty($this->parents); |
281
|
|
|
} |
282
|
|
|
|
283
|
|
|
/** |
284
|
|
|
* toString magic method |
285
|
|
|
* |
286
|
|
|
* @return string the sha |
287
|
|
|
*/ |
288
|
8 |
|
public function __toString(): string |
289
|
|
|
{ |
290
|
8 |
|
return $this->sha; |
291
|
|
|
} |
292
|
|
|
|
293
|
|
|
/** |
294
|
|
|
* @return CallerInterface |
295
|
|
|
*/ |
296
|
18 |
|
private function getCaller(): CallerInterface |
297
|
|
|
{ |
298
|
18 |
|
return $this->getRepository()->getCaller(); |
299
|
|
|
} |
300
|
|
|
|
301
|
|
|
/** |
302
|
|
|
* Repository setter |
303
|
|
|
* |
304
|
|
|
* @param \GitElephant\Repository $repository repository variable |
305
|
|
|
*/ |
306
|
|
|
public function setRepository(Repository $repository): void |
307
|
|
|
{ |
308
|
|
|
$this->repository = $repository; |
309
|
|
|
} |
310
|
|
|
|
311
|
|
|
/** |
312
|
|
|
* Repository getter |
313
|
|
|
* |
314
|
|
|
* @return \GitElephant\Repository |
315
|
|
|
*/ |
316
|
18 |
|
public function getRepository(): \GitElephant\Repository |
317
|
|
|
{ |
318
|
18 |
|
return $this->repository; |
319
|
|
|
} |
320
|
|
|
|
321
|
|
|
/** |
322
|
|
|
* author getter |
323
|
|
|
* |
324
|
|
|
* @return Author |
325
|
|
|
*/ |
326
|
4 |
|
public function getAuthor(): ?Author |
327
|
|
|
{ |
328
|
4 |
|
return $this->author; |
329
|
|
|
} |
330
|
|
|
|
331
|
|
|
/** |
332
|
|
|
* committer getter |
333
|
|
|
* |
334
|
|
|
* @return Author |
335
|
|
|
*/ |
336
|
4 |
|
public function getCommitter(): ?Author |
337
|
|
|
{ |
338
|
4 |
|
return $this->committer; |
339
|
|
|
} |
340
|
|
|
|
341
|
|
|
/** |
342
|
|
|
* message getter |
343
|
|
|
* |
344
|
|
|
* @return Message |
345
|
|
|
*/ |
346
|
11 |
|
public function getMessage(): ?Message |
347
|
|
|
{ |
348
|
11 |
|
return $this->message; |
349
|
|
|
} |
350
|
|
|
|
351
|
|
|
/** |
352
|
|
|
* parent getter |
353
|
|
|
* |
354
|
|
|
* @return array |
355
|
|
|
*/ |
356
|
2 |
|
public function getParents(): array |
357
|
|
|
{ |
358
|
2 |
|
return $this->parents; |
359
|
|
|
} |
360
|
|
|
|
361
|
|
|
/** |
362
|
|
|
* sha getter |
363
|
|
|
* |
364
|
|
|
* @param bool $short short version |
365
|
|
|
* |
366
|
|
|
* @return string |
367
|
|
|
*/ |
368
|
23 |
|
public function getSha(bool $short = false): ?string |
369
|
|
|
{ |
370
|
23 |
|
return $short ? substr($this->sha, 0, 7) : $this->sha; |
371
|
|
|
} |
372
|
|
|
|
373
|
|
|
/** |
374
|
|
|
* tree getter |
375
|
|
|
* |
376
|
|
|
* @return string |
377
|
|
|
*/ |
378
|
3 |
|
public function getTree(): ?string |
379
|
|
|
{ |
380
|
3 |
|
return $this->tree; |
381
|
|
|
} |
382
|
|
|
|
383
|
|
|
/** |
384
|
|
|
* datetimeAuthor getter |
385
|
|
|
* |
386
|
|
|
* @return \DateTime |
387
|
|
|
*/ |
388
|
4 |
|
public function getDatetimeAuthor(): ?\DateTime |
389
|
|
|
{ |
390
|
4 |
|
return $this->datetimeAuthor; |
391
|
|
|
} |
392
|
|
|
|
393
|
|
|
/** |
394
|
|
|
* datetimeCommitter getter |
395
|
|
|
* |
396
|
|
|
* @return \DateTime |
397
|
|
|
*/ |
398
|
4 |
|
public function getDatetimeCommitter(): ?\DateTime |
399
|
|
|
{ |
400
|
4 |
|
return $this->datetimeCommitter; |
401
|
|
|
} |
402
|
|
|
|
403
|
|
|
/** |
404
|
|
|
* rev-parse command - often used to return a commit tag. |
405
|
|
|
* |
406
|
|
|
* @param array $options the options to apply to rev-parse |
407
|
|
|
* |
408
|
|
|
* @throws \RuntimeException |
409
|
|
|
* @throws \InvalidArgumentException |
410
|
|
|
* @throws \Symfony\Component\Process\Exception\RuntimeException |
411
|
|
|
* @return array |
412
|
|
|
*/ |
413
|
1 |
|
public function revParse(array $options = []): array |
414
|
|
|
{ |
415
|
1 |
|
$c = RevParseCommand::getInstance()->revParse($this, $options); |
416
|
1 |
|
$caller = $this->repository->getCaller(); |
417
|
1 |
|
$caller->execute($c); |
418
|
|
|
|
419
|
1 |
|
return array_map('trim', $caller->getOutputLines(true)); |
420
|
|
|
} |
421
|
|
|
|
422
|
|
|
/** |
423
|
|
|
* Is the commit tagged? |
424
|
|
|
* |
425
|
|
|
* return true if some tag of repository point to this commit |
426
|
|
|
* return false otherwise |
427
|
|
|
* |
428
|
|
|
* @return bool |
429
|
|
|
*/ |
430
|
2 |
|
public function tagged(): bool |
431
|
|
|
{ |
432
|
2 |
|
$result = false; |
433
|
|
|
|
434
|
|
|
/** @var Tag $tag */ |
435
|
2 |
|
foreach ($this->repository->getTags() as $tag) { |
436
|
1 |
|
if ($tag->getSha() === $this->getSha()) { |
437
|
1 |
|
$result = true; |
438
|
1 |
|
break; |
439
|
|
|
} |
440
|
|
|
} |
441
|
|
|
|
442
|
2 |
|
return $result; |
443
|
|
|
} |
444
|
|
|
|
445
|
|
|
/** |
446
|
|
|
* Return Tags that point to this commit |
447
|
|
|
* |
448
|
|
|
* @return Tag[] |
449
|
|
|
*/ |
450
|
1 |
|
public function getTags(): array |
451
|
|
|
{ |
452
|
1 |
|
$currentCommitTags = []; |
453
|
|
|
|
454
|
|
|
/** @var Tag $tag */ |
455
|
1 |
|
foreach ($this->repository->getTags() as $tag) { |
456
|
1 |
|
if ($tag->getSha() === $this->getSha()) { |
457
|
1 |
|
$currentCommitTags[] = $tag; |
458
|
|
|
} |
459
|
|
|
} |
460
|
|
|
|
461
|
1 |
|
return $currentCommitTags; |
462
|
|
|
} |
463
|
|
|
} |
464
|
|
|
|
Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.
For example, imagine you have a variable
$accountId
that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to theid
property of an instance of theAccount
class. This class holds a proper account, so the id value must no longer be false.Either this assignment is in error or a type check should be added for that assignment.