1 | <?php |
||||
2 | |||||
3 | namespace Bdelespierre\GitStats\Commands; |
||||
4 | |||||
5 | use Bdelespierre\GitStats\Interfaces\GitServiceInterface; |
||||
6 | use Bdelespierre\GitStats\Interfaces\ProcessServiceInterface; |
||||
7 | use Bdelespierre\GitStats\Task; |
||||
8 | use Symfony\Component\Console\Command\Command; |
||||
9 | use Symfony\Component\Console\Input\InputArgument; |
||||
10 | use Symfony\Component\Console\Input\InputInterface; |
||||
11 | use Symfony\Component\Console\Input\InputOption; |
||||
12 | use Symfony\Component\Console\Output\OutputInterface; |
||||
13 | |||||
14 | class Run extends Command |
||||
15 | { |
||||
16 | protected GitServiceInterface $git; |
||||
17 | protected ProcessServiceInterface $process; |
||||
18 | |||||
19 | public function __construct(GitServiceInterface $git, ProcessServiceInterface $process) |
||||
20 | { |
||||
21 | $this->git = $git; |
||||
22 | $this->process = $process; |
||||
23 | |||||
24 | parent::__construct(); |
||||
25 | } |
||||
26 | |||||
27 | protected function configure() |
||||
28 | { |
||||
29 | $this->setName('run') |
||||
30 | ->setDescription('Iterate through git commits to gather statistics') |
||||
31 | ->addArgument('branch', InputArgument::OPTIONAL, "The branch to use.", "master") |
||||
32 | ->addArgument('tasks', InputArgument::OPTIONAL, "The task file to use.", ".gitstats.php"); |
||||
33 | } |
||||
34 | |||||
35 | protected function execute(InputInterface $input, OutputInterface $output) |
||||
36 | { |
||||
37 | try { |
||||
38 | $this->checkEnvironment(); |
||||
39 | |||||
40 | $branch = $this->getBranch($input); |
||||
41 | $data = $this->runTasks( |
||||
42 | $this->getTasks($input), |
||||
43 | $this->git->getCommits($branch) |
||||
44 | ); |
||||
45 | |||||
46 | $output->write($this->format($data)); |
||||
47 | } catch (\Exception $e) { |
||||
48 | $output->writeln("<error>{$e->getMessage()}</error>"); |
||||
49 | return self::FAILURE; |
||||
50 | } finally { |
||||
51 | if (isset($branch)) { |
||||
52 | $this->git->checkout($branch); |
||||
53 | } |
||||
54 | } |
||||
55 | |||||
56 | return self::SUCCESS; |
||||
57 | } |
||||
58 | |||||
59 | private function checkEnvironment(): void |
||||
60 | { |
||||
61 | if (! $this->git->isGitAvailable()) { |
||||
62 | throw new \RuntimeException("Cannot proceed: unable to find git command."); |
||||
63 | } |
||||
64 | |||||
65 | if (! $this->git->isGitRepository()) { |
||||
66 | throw new \RuntimeException(sprintf("Cannot proceed: %s is not a git repository.", getcwd())); |
||||
67 | } |
||||
68 | |||||
69 | if (! $this->git->updateIndex()) { |
||||
70 | throw new \RuntimeException("Cannot proceed: unable to update index."); |
||||
71 | } |
||||
72 | |||||
73 | if ($this->git->hasUnstagedChanges()) { |
||||
74 | throw new \RuntimeException("Cannot proceed: you have unstaged changes. " . |
||||
75 | "Please commit or stash them."); |
||||
76 | } |
||||
77 | |||||
78 | if ($this->git->hasUncommittedChanges()) { |
||||
79 | throw new \RuntimeException("Cannot proceed: your index contains uncommitted changes. " . |
||||
80 | "Please commit or stash them."); |
||||
81 | } |
||||
82 | } |
||||
83 | |||||
84 | private function getBranch(InputInterface $input): string |
||||
85 | { |
||||
86 | $branch = $input->getArgument('branch'); |
||||
87 | |||||
88 | if (! $this->git->isValidBranch($branch)) { |
||||
0 ignored issues
–
show
Bug
introduced
by
![]() |
|||||
89 | throw new \RuntimeException("Branch {$branch} is not valid."); |
||||
90 | } |
||||
91 | |||||
92 | return $branch; |
||||
93 | } |
||||
94 | |||||
95 | private function getTasks(InputInterface $input): array |
||||
96 | { |
||||
97 | $file = $input->getArgument('tasks'); |
||||
98 | |||||
99 | if (! is_readable($file)) { |
||||
0 ignored issues
–
show
It seems like
$file can also be of type null and string[] ; however, parameter $filename of is_readable() does only seem to accept string , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
100 | throw new \RuntimeException("File {$file} does not exists."); |
||||
101 | } |
||||
102 | |||||
103 | $tasks = []; |
||||
104 | |||||
105 | foreach ((require $file)['tasks'] ?? [] as $name => $task) { |
||||
106 | if (! is_array($task) || ! isset($task['command'])) { |
||||
107 | $task = ['command' => $task]; |
||||
108 | } |
||||
109 | |||||
110 | $tasks[] = new Task($this->process, $name, $task['command'], $task['patterns'] ?? []); |
||||
111 | } |
||||
112 | |||||
113 | return $tasks; |
||||
114 | } |
||||
115 | |||||
116 | private function runTasks(iterable $tasks, iterable $commits): \Generator |
||||
117 | { |
||||
118 | // headers |
||||
119 | yield array_merge(['commit', 'date'], ...array_map(fn($task) => $task->getHeaders(), $tasks)); |
||||
0 ignored issues
–
show
$tasks of type iterable is incompatible with the type array expected by parameter $array of array_map() .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
120 | |||||
121 | foreach ($commits as $commit) { |
||||
122 | $this->git->checkout($commit); |
||||
123 | |||||
124 | $timestamp = $this->git->getCommitTimestamp($commit); |
||||
125 | $data = [ |
||||
126 | 'commit' => $commit, |
||||
127 | 'date' => date('Y-m-d H:i:s', $timestamp), |
||||
128 | ]; |
||||
129 | |||||
130 | foreach ($tasks as $task) { |
||||
131 | $data += $task->run($commit)->toArray(); |
||||
132 | } |
||||
133 | |||||
134 | yield $data; |
||||
135 | } |
||||
136 | } |
||||
137 | |||||
138 | private function format(iterable $data): \Generator |
||||
139 | { |
||||
140 | $buffer = fopen('php://temp', 'r+'); |
||||
141 | |||||
142 | foreach ($data as $row) { |
||||
143 | $length = fputcsv($buffer, $row, ',', '"', '\\'); |
||||
144 | fseek($buffer, ftell($buffer) - $length); |
||||
145 | yield fgets($buffer); |
||||
146 | } |
||||
147 | |||||
148 | fclose($buffer); |
||||
149 | } |
||||
150 | } |
||||
151 |