Skip to content

Commit 7116992

Browse files
authored
Merge pull request #163 from buildkite-plugins/SUP-6353-cleanup-diff-logic
Improve diff() logic, extend tests
2 parents e910528 + eb4c7de commit 7116992

5 files changed

Lines changed: 82 additions & 5 deletions

File tree

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,11 +236,16 @@ Depending on your use case, you may want to determine the point where the branch
236236

237237
Default: `git diff --name-only HEAD~1`
238238

239+
The diff command must produce **newline-delimited output** with one file path per line. This is the format that `git diff --name-only` produces by default. Newline-delimited output is required for filenames containing spaces to be parsed correctly.
240+
241+
Custom diff scripts should follow the same convention — print one path per line to standard output.
242+
239243
#### Sample output
240244

241245
```
242246
README.md
243247
lib/trigger.bash
248+
directory/File Name With Spaces.md
244249
```
245250
246251
#### Example scripts

e2e/diff-output-realistic

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
normal/path.go
2+
"path/with\tescape.go"
3+
directory/File Name With Spaces.md
4+
"projects/17_\360\237\252\201_emoji.py"
5+
another dir/some file.txt

e2e/multiple-paths

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
user-service/infrastructure/cloudfront.yaml
2+
user-service/my config/settings.yaml
23
user-service/serverless.yaml

pipeline.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -105,21 +105,23 @@ func diff(command string) ([]string, error) {
105105
return nil, fmt.Errorf("diff command failed: %v", err)
106106
}
107107

108-
trimmed := strings.Trim(output, " \t\r\v\f")
109-
if trimmed == "" {
108+
hasNewlines := strings.ContainsRune(output, '\n')
109+
output = strings.TrimRight(output, "\n")
110+
if output == "" {
110111
return []string{}, nil
111112
}
112113

113114
var fields []string
114-
if strings.Contains(trimmed, "\n") {
115-
for _, line := range strings.Split(trimmed, "\n") {
115+
if hasNewlines {
116+
for _, line := range strings.Split(output, "\n") {
116117
line = strings.TrimSpace(line)
117118
if line != "" {
118119
fields = append(fields, line)
119120
}
120121
}
121122
} else {
122-
fields = strings.Fields(trimmed)
123+
// Single line without newline — legacy compat for custom diff commands
124+
fields = strings.Fields(output)
123125
}
124126

125127
paths := make([]string, 0, len(fields))

pipeline_test.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,13 +108,29 @@ func TestDiff(t *testing.T) {
108108
func TestDiffWithSubshell(t *testing.T) {
109109
want := []string{
110110
"user-service/infrastructure/cloudfront.yaml",
111+
"user-service/my config/settings.yaml",
111112
"user-service/serverless.yaml",
112113
}
113114
got, err := diff("cat e2e/multiple-paths")
114115
assert.NoError(t, err)
115116
assert.Equal(t, want, got)
116117
}
117118

119+
func TestDiffRealisticGitOutput(t *testing.T) {
120+
// Fixture mirrors git diff --name-only output: plain paths, C-style
121+
// quoted paths (tab, octal emoji), and paths with spaces.
122+
want := []string{
123+
"normal/path.go",
124+
"path/with\tescape.go",
125+
"directory/File Name With Spaces.md",
126+
"projects/17_🪁_emoji.py",
127+
"another dir/some file.txt",
128+
}
129+
got, err := diff("cat e2e/diff-output-realistic")
130+
assert.NoError(t, err)
131+
assert.Equal(t, want, got)
132+
}
133+
118134
func TestDiffWithQuotedPaths(t *testing.T) {
119135
want := []string{
120136
"projects/test/pages/17_🪁_testfile.py",
@@ -161,6 +177,54 @@ func TestDiffWithSpacesInFilenamesSingleFile(t *testing.T) {
161177
assert.Equal(t, want, got)
162178
}
163179

180+
func TestDiffEmptyOutput(t *testing.T) {
181+
got, err := diff(`printf ''`)
182+
assert.NoError(t, err)
183+
assert.Equal(t, []string{}, got)
184+
}
185+
186+
func TestDiffWhitespaceOnlyOutput(t *testing.T) {
187+
got, err := diff(`printf '\n\n\n'`)
188+
assert.NoError(t, err)
189+
assert.Equal(t, []string{}, got)
190+
}
191+
192+
func TestDiffWhitespaceOnlyNoNewlines(t *testing.T) {
193+
got, err := diff(`printf ' \t '`)
194+
assert.NoError(t, err)
195+
assert.Equal(t, []string{}, got)
196+
}
197+
198+
func TestDiffSingleFileNoTrailingNewline(t *testing.T) {
199+
// Legacy compat: custom diff commands may not emit a trailing newline
200+
want := []string{"services/foo/serverless.yml"}
201+
got, err := diff(`printf 'services/foo/serverless.yml'`)
202+
assert.NoError(t, err)
203+
assert.Equal(t, want, got)
204+
}
205+
206+
func TestDiffWindowsLineEndings(t *testing.T) {
207+
want := []string{
208+
"services/foo/file.go",
209+
"services/bar/file.go",
210+
}
211+
got, err := diff(`printf 'services/foo/file.go\r\nservices/bar/file.go\r\n'`)
212+
assert.NoError(t, err)
213+
assert.Equal(t, want, got)
214+
}
215+
216+
func TestDiffQuotedPathsWithSpaces(t *testing.T) {
217+
// Git C-style quotes paths with special chars; spaces alone don't trigger quoting,
218+
// but paths with both spaces and special chars will be quoted.
219+
want := []string{
220+
"projects/my docs/17_🪁_file.py",
221+
"normal/file.txt",
222+
}
223+
got, err := diff(`printf '"projects/my docs/17_\360\237\252\201_file.py"\nnormal/file.txt\n'`)
224+
assert.NoError(t, err)
225+
assert.Equal(t, want, got)
226+
}
227+
164228
func TestStepsToTriggerWithEmojiPaths(t *testing.T) {
165229
watch := []WatchConfig{
166230
{

0 commit comments

Comments
 (0)