Skip to content

Commit cc904c0

Browse files
[1.x] Adds --diff option (#327)
* add [diff] option to default command * add path resolver for git diff * add [diff] method to PathsRepository contract. (Breaking?) * extract method for future re-use * implement new [diff] method * lint * skip duplicates * include untracked files in diff list * add tests * fixes * make stan happy
1 parent 0d8f830 commit cc904c0

File tree

5 files changed

+161
-2
lines changed

5 files changed

+161
-2
lines changed

app/Commands/DefaultCommand.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ protected function configure()
4141
new InputOption('test', '', InputOption::VALUE_NONE, 'Test for code style errors without fixing them'),
4242
new InputOption('bail', '', InputOption::VALUE_NONE, 'Test for code style errors without fixing them and stop on first error'),
4343
new InputOption('repair', '', InputOption::VALUE_NONE, 'Fix code style errors but exit with status 1 if there were any changes made'),
44+
new InputOption('diff', '', InputOption::VALUE_REQUIRED, 'Only fix files that have changed since branching off from the given branch', null, ['main', 'master', 'origin/main', 'origin/master']),
4445
new InputOption('dirty', '', InputOption::VALUE_NONE, 'Only fix files that have uncommitted changes'),
4546
new InputOption('format', '', InputOption::VALUE_REQUIRED, 'The output format that should be used'),
4647
new InputOption('cache-file', '', InputArgument::OPTIONAL, 'The path to the cache file'),

app/Contracts/PathsRepository.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,12 @@ interface PathsRepository
1010
* @return array<int, string>
1111
*/
1212
public function dirty();
13+
14+
/**
15+
* Determine the files that have changed since branching off from the given branch.
16+
*
17+
* @param string $branch
18+
* @return array<int, string>
19+
*/
20+
public function diff($branch);
1321
}

app/Project.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ public static function paths($input)
1818
return static::resolveDirtyPaths();
1919
}
2020

21+
if ($diff = $input->getOption('diff')) {
22+
return static::resolveDiffPaths($diff);
23+
}
24+
2125
return $input->getArgument('path');
2226
}
2327

@@ -46,4 +50,21 @@ public static function resolveDirtyPaths()
4650

4751
return $files;
4852
}
53+
54+
/**
55+
* Resolves the paths that have changed since branching off from the given branch, if any.
56+
*
57+
* @param string $branch
58+
* @return array<int, string>
59+
*/
60+
public static function resolveDiffPaths($branch)
61+
{
62+
$files = app(PathsRepository::class)->diff($branch);
63+
64+
if (empty($files)) {
65+
abort(0, "No files have changed since branching off of {$branch}.");
66+
}
67+
68+
return $files;
69+
}
4970
}

app/Repositories/GitPathsRepository.php

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use App\Contracts\PathsRepository;
66
use App\Factories\ConfigurationFactory;
7+
use Illuminate\Support\Collection;
78
use Illuminate\Support\Str;
89
use Symfony\Component\Process\Process;
910

@@ -41,14 +42,56 @@ public function dirty()
4142
->mapWithKeys(fn ($file) => [substr($file, 3) => trim(substr($file, 0, 3))])
4243
->reject(fn ($status) => $status === 'D')
4344
->map(fn ($status, $file) => $status === 'R' ? Str::after($file, ' -> ') : $file)
45+
->values();
46+
47+
return $this->processFileNames($dirtyFiles);
48+
}
49+
50+
/**
51+
* {@inheritDoc}
52+
*/
53+
public function diff($branch)
54+
{
55+
$files = [
56+
'committed' => tap(new Process(['git', 'diff', '--name-only', '--diff-filter=AM', "{$branch}...HEAD", '--', '**.php']))->run(),
57+
'staged' => tap(new Process(['git', 'diff', '--name-only', '--diff-filter=AM', '--cached', '--', '**.php']))->run(),
58+
'unstaged' => tap(new Process(['git', 'diff', '--name-only', '--diff-filter=AM', '--', '**.php']))->run(),
59+
'untracked' => tap(new Process(['git', 'ls-files', '--others', '--exclude-standard', '--', '**.php']))->run(),
60+
];
61+
62+
$files = collect($files)
63+
->each(fn ($process) => abort_if(
64+
boolean: ! $process->isSuccessful(),
65+
code: 1,
66+
message: 'The [--diff] option is only available when using Git.',
67+
))
68+
->map(fn ($process) => $process->getOutput())
69+
->map(fn ($output) => explode(PHP_EOL, $output))
70+
->flatten()
71+
->filter()
72+
->unique()
73+
->values()
74+
->map(fn ($s) => (string) $s);
75+
76+
return $this->processFileNames($files);
77+
}
78+
79+
/**
80+
* Process the files.
81+
*
82+
* @param \Illuminate\Support\Collection<int, string> $fileNames
83+
* @return array<int, string>
84+
*/
85+
protected function processFileNames(Collection $fileNames)
86+
{
87+
$processedFileNames = $fileNames
4488
->map(function ($file) {
4589
if (PHP_OS_FAMILY === 'Windows') {
4690
$file = str_replace('/', DIRECTORY_SEPARATOR, $file);
4791
}
4892

4993
return $this->path.DIRECTORY_SEPARATOR.$file;
5094
})
51-
->values()
5295
->all();
5396

5497
$files = array_values(array_map(function ($splFile) {
@@ -58,6 +101,6 @@ public function dirty()
58101
->files()
59102
)));
60103

61-
return array_values(array_intersect($files, $dirtyFiles));
104+
return array_values(array_intersect($files, $processedFileNames));
62105
}
63106
}

tests/Feature/DiffTest.php

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
<?php
2+
3+
use App\Contracts\PathsRepository;
4+
5+
it('determines diff files', function () {
6+
$paths = Mockery::mock(PathsRepository::class);
7+
8+
$paths
9+
->shouldReceive('diff')
10+
->with('main')
11+
->once()
12+
->andReturn([
13+
base_path('tests/Fixtures/without-issues-laravel/file.php'),
14+
]);
15+
16+
$this->swap(PathsRepository::class, $paths);
17+
18+
[$statusCode, $output] = run('default', ['--diff' => 'main']);
19+
20+
expect($statusCode)->toBe(0)
21+
->and($output)
22+
->toContain('── Laravel', ' 1 file');
23+
});
24+
25+
it('ignores the path argument', function () {
26+
$paths = Mockery::mock(PathsRepository::class);
27+
28+
$paths
29+
->shouldReceive('diff')
30+
->once()
31+
->andReturn([
32+
base_path('tests/Fixtures/without-issues-laravel/file.php'),
33+
]);
34+
35+
$this->swap(PathsRepository::class, $paths);
36+
37+
[$statusCode, $output] = run('default', [
38+
'--diff' => 'main',
39+
'path' => base_path(),
40+
]);
41+
42+
expect($statusCode)->toBe(0)
43+
->and($output)
44+
->toContain('── Laravel', ' 1 file');
45+
});
46+
47+
it('does not abort when there are no diff files', function () {
48+
$paths = Mockery::mock(PathsRepository::class);
49+
50+
$paths
51+
->shouldReceive('diff')
52+
->once()
53+
->andReturn([]);
54+
55+
$this->swap(PathsRepository::class, $paths);
56+
57+
[$statusCode, $output] = run('default', [
58+
'--diff' => 'main',
59+
]);
60+
61+
expect($statusCode)->toBe(0)
62+
->and($output)
63+
->toContain('── Laravel', ' 0 files');
64+
});
65+
66+
it('parses nested branch names', function () {
67+
$paths = Mockery::mock(PathsRepository::class);
68+
69+
$paths
70+
->shouldReceive('diff')
71+
->with('origin/main')
72+
->once()
73+
->andReturn([
74+
base_path('tests/Fixtures/without-issues-laravel/file.php'),
75+
]);
76+
77+
$this->swap(PathsRepository::class, $paths);
78+
79+
[$statusCode, $output] = run('default', [
80+
'--diff' => 'origin/main',
81+
]);
82+
83+
expect($statusCode)->toBe(0)
84+
->and($output)
85+
->toContain('── Laravel', ' 1 file');
86+
});

0 commit comments

Comments
 (0)