Skip to content

Commit 7c411c4

Browse files
authored
Merge pull request #1496 from merico-dev/new-dtm
Update CI Workflow and Fix Mixed Tab and Space Indentation in Patch File
2 parents 782ee3c + aac178c commit 7c411c4

File tree

3 files changed

+249
-16
lines changed

3 files changed

+249
-16
lines changed

.github/workflows/ci.yaml

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
pull_request:
8+
branches:
9+
- main
10+
11+
jobs:
12+
build:
13+
runs-on: ubuntu-latest
14+
steps:
15+
- name: Set up Go
16+
uses: actions/setup-go@v4
17+
with:
18+
go-version: 1.20
19+
cache: true
20+
21+
- name: Check out code
22+
uses: actions/checkout@v3
23+
24+
- name: Run tests
25+
run: go test -v ./...
26+
27+
- name: Build project
28+
run: make build

internal/pkg/patch/patch.go

+150-1
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,36 @@
11
package patch
22

33
import (
4+
"bufio"
45
"fmt"
56
"os"
67
"os/exec"
8+
"path/filepath"
9+
"regexp"
10+
"strings"
711

812
"github.com/devstream-io/devstream/internal/log"
913
)
1014

15+
const (
16+
processOptionTabToSpace ProcessOption = "tabToSpace"
17+
processOptionSpaceToTab ProcessOption = "spaceToTab"
18+
)
19+
20+
type ProcessOption string
21+
1122
// Patch calls the patch command to apply a diff file to an original
1223
func Patch(workDir, patchFile string) error {
1324
log.Infof("Patching file: %s", patchFile)
1425

26+
// Fix patch file if it mixed tab and space indentation
27+
err := fixPatchFile(workDir, patchFile)
28+
if err != nil {
29+
return fmt.Errorf("patch file fix failed: %w", err)
30+
}
31+
1532
// Check if the patch command exists and is executable
16-
err := checkPatchCommand()
33+
err = checkPatchCommand()
1734
if err != nil {
1835
return fmt.Errorf("patch command check failed: %w", err)
1936
}
@@ -50,3 +67,135 @@ func checkPatchCommand() error {
5067

5168
return nil
5269
}
70+
71+
// fixPatchFile fixes the patch file if it mixed tab and space indentation.
72+
// The patch file is generated by GPT4, and it may have different indentation with the original file.
73+
// The original file path is contained in the patch file, so we can use the fix the patch file by using the original file.
74+
// If the original file uses tab indentation, we replace all spaces with tabs in the patch file.
75+
// If the original file uses space indentation, we replace all tabs with spaces in the patch file.
76+
func fixPatchFile(workDir, patchFile string) error {
77+
// Read the original file path from the patch file
78+
originalFilePath, err := extractOriginalFilePathFromPatchFile(patchFile)
79+
originalFilePath = filepath.Join(workDir, originalFilePath)
80+
81+
if err != nil {
82+
return fmt.Errorf("failed to extract original file path from patch string: %w", err)
83+
}
84+
85+
// Check if the original file contain tabs in the indentation
86+
original, err := os.Open(originalFilePath)
87+
if err != nil {
88+
return fmt.Errorf("failed to open original file: %w", err)
89+
}
90+
defer original.Close()
91+
92+
hasTab := false
93+
scanner := bufio.NewScanner(original)
94+
for scanner.Scan() {
95+
line := scanner.Text()
96+
if strings.HasPrefix(line, "\t") {
97+
hasTab = true
98+
break
99+
}
100+
}
101+
102+
if err = scanner.Err(); err != nil {
103+
return fmt.Errorf("failed to read original file: %w", err)
104+
}
105+
106+
// The original file uses tab indentation
107+
if hasTab {
108+
// Replace all space indentation with tabs in the patch file
109+
if err = processTabSpaceSwitch(patchFile, processOptionSpaceToTab); err != nil {
110+
return fmt.Errorf("failed to process tab to space: %w", err)
111+
}
112+
// The original file uses space indentation
113+
} else {
114+
// Replace all tab indentation with spaces in the patch file
115+
if err = processTabSpaceSwitch(patchFile, processOptionTabToSpace); err != nil {
116+
return fmt.Errorf("failed to process space to tab: %w", err)
117+
}
118+
}
119+
120+
return nil
121+
122+
}
123+
124+
// ExtractOriginalFilePathFromPatchString extracts the original file path from a patch string
125+
// e.g. --- pkg/patch/patch.go 2021-08-15 16:00:00.000000000 +0900 -> pkg/patch/patch.go
126+
func extractOriginalFilePathFromPatchFile(patchFile string) (string, error) {
127+
// Read content from the patch file
128+
fileContent, err := os.ReadFile(patchFile)
129+
if err != nil {
130+
return "", fmt.Errorf("failed to read patch file: %w", err)
131+
}
132+
133+
lines := strings.Split(string(fileContent), "\n")
134+
135+
for _, line := range lines {
136+
if strings.HasPrefix(line, "--- ") {
137+
fields := strings.Fields(line)
138+
if len(fields) > 1 {
139+
return fields[1], nil
140+
}
141+
}
142+
}
143+
144+
return "", fmt.Errorf("original file path not found in patch string")
145+
}
146+
147+
// processTabSpaceSwitch processes the tab/space indentation switch in a file
148+
// If the option is processOptionTabToSpace, it replaces all tabs with spaces
149+
// If the option is processOptionSpaceToTab, it replaces all spaces with tabs
150+
func processTabSpaceSwitch(filePath string, option ProcessOption) error {
151+
file, err := os.Open(filePath)
152+
if err != nil {
153+
return fmt.Errorf("failed to open file: %w", err)
154+
}
155+
defer file.Close()
156+
157+
scanner := bufio.NewScanner(file)
158+
var processedLines []string
159+
160+
// Matches the start of the string (^) followed by an optional + or - sign, followed by one or more groups of 4 spaces ( {4})+
161+
spaceRegex := regexp.MustCompile(`^(\+|\-)?( {4})+`)
162+
// Matches the start of the string (^) followed by an optional + or - sign, followed by one or more tabs (\t)+
163+
tabRegex := regexp.MustCompile(`^(\+|\-)?\t+`)
164+
165+
for scanner.Scan() {
166+
line := scanner.Text()
167+
if option == processOptionTabToSpace {
168+
line = tabRegex.ReplaceAllStringFunc(line, func(s string) string {
169+
prefix := ""
170+
if s[0] == '+' || s[0] == '-' {
171+
prefix = string(s[0])
172+
s = s[1:]
173+
}
174+
return prefix + strings.Repeat(" ", len(s))
175+
})
176+
} else if option == processOptionSpaceToTab {
177+
line = spaceRegex.ReplaceAllStringFunc(line, func(s string) string {
178+
prefix := ""
179+
if s[0] == '+' || s[0] == '-' {
180+
prefix = string(s[0])
181+
s = s[1:]
182+
}
183+
return prefix + strings.Repeat("\t", len(s)/4)
184+
})
185+
} else {
186+
return fmt.Errorf("invalid process option: %s", option)
187+
}
188+
processedLines = append(processedLines, line)
189+
}
190+
191+
if err = scanner.Err(); err != nil {
192+
return fmt.Errorf("failed to read file: %w", err)
193+
}
194+
195+
err = os.WriteFile(filePath, []byte(strings.Join(processedLines, "\n")+"\n"), 0644)
196+
if err != nil {
197+
return fmt.Errorf("failed to write file: %w", err)
198+
}
199+
200+
return nil
201+
}

internal/pkg/patch/patch_test.go

+71-15
Original file line numberDiff line numberDiff line change
@@ -72,32 +72,88 @@ This is the original file.
7272
expectedPatchedContent := `Hello, world!
7373
This is the patched file.
7474
`
75-
Expect(string(patchedContent)).To(Equal(expectedPatchedContent))
75+
patchedContentStr := string(patchedContent)
76+
Expect(patchedContentStr).To(Equal(expectedPatchedContent))
7677
})
7778

78-
It("returns an error if the patch command is not found or not executable", func() {
79-
// Temporarily change PATH to exclude the real patch command
80-
originalPath := os.Getenv("PATH")
81-
err := os.Setenv("PATH", tempDir)
79+
It("returns an error if the patch file is invalid", func() {
80+
originalContent := `Hello, world!
81+
This is the original file.
82+
`
83+
84+
err := os.WriteFile(originalFile.Name(), []byte(originalContent), 0644)
85+
Expect(err).NotTo(HaveOccurred())
86+
87+
invalidPatchContent := fmt.Sprintf(`--- %s
88+
+++ new-file
89+
@@ -1,2 +1,2 @@
90+
`,
91+
filepath.Base(originalFile.Name()))
92+
93+
err = os.WriteFile(patchFile.Name(), []byte(invalidPatchContent), 0644)
8294
Expect(err).NotTo(HaveOccurred())
83-
defer func() {
84-
err := os.Setenv("PATH", originalPath)
85-
Expect(err).NotTo(HaveOccurred())
86-
}()
8795

8896
err = Patch(tempDir, patchFile.Name())
8997
Expect(err).To(HaveOccurred())
90-
Expect(strings.Contains(err.Error(), "patch command not found")).To(BeTrue())
98+
Expect(strings.Contains(err.Error(), "patch command failed")).To(BeTrue())
9199
})
100+
})
92101

93-
It("returns an error if the patch file is invalid", func() {
94-
invalidPatchContent := `This is not a valid patch file.`
95-
err := os.WriteFile(patchFile.Name(), []byte(invalidPatchContent), 0644)
102+
Context("when patching a file with inconsistent indentation", func() {
103+
It("successfully applies the patch with spaces to the original file with tabs", func() {
104+
originalContent := "Hello, world!\n\tThis is the original file with tabs.\n"
105+
106+
err := os.WriteFile(originalFile.Name(), []byte(originalContent), 0644)
107+
Expect(err).NotTo(HaveOccurred())
108+
109+
patchContent := fmt.Sprintf(`--- %s
110+
+++ new-file
111+
@@ -1,2 +1,2 @@
112+
Hello, world!
113+
- This is the original file with tabs.
114+
+ This is the patched file with tabs.
115+
`,
116+
filepath.Base(originalFile.Name()))
117+
118+
err = os.WriteFile(patchFile.Name(), []byte(patchContent), 0644)
96119
Expect(err).NotTo(HaveOccurred())
97120

98121
err = Patch(tempDir, patchFile.Name())
99-
Expect(err).To(HaveOccurred())
100-
Expect(strings.Contains(err.Error(), "patch command failed")).To(BeTrue())
122+
Expect(err).NotTo(HaveOccurred())
123+
124+
patchedContent, err := os.ReadFile(originalFile.Name())
125+
Expect(err).NotTo(HaveOccurred())
126+
127+
expectedPatchedContent := "Hello, world!\n\tThis is the patched file with tabs.\n"
128+
Expect(string(patchedContent)).To(Equal(expectedPatchedContent))
129+
})
130+
131+
It("successfully applies the patch with tabs to the original file with spaces", func() {
132+
originalContent := "Hello, world!\n This is the original file with spaces.\n"
133+
134+
err := os.WriteFile(originalFile.Name(), []byte(originalContent), 0644)
135+
Expect(err).NotTo(HaveOccurred())
136+
137+
patchContent := fmt.Sprintf(`--- %s
138+
+++ new-file
139+
@@ -1,2 +1,2 @@
140+
Hello, world!
141+
- This is the original file with spaces.
142+
+ This is the patched file with spaces.
143+
`,
144+
filepath.Base(originalFile.Name()))
145+
146+
err = os.WriteFile(patchFile.Name(), []byte(patchContent), 0644)
147+
Expect(err).NotTo(HaveOccurred())
148+
149+
err = Patch(tempDir, patchFile.Name())
150+
Expect(err).NotTo(HaveOccurred())
151+
152+
patchedContent, err := os.ReadFile(originalFile.Name())
153+
Expect(err).NotTo(HaveOccurred())
154+
155+
expectedPatchedContent := "Hello, world!\n This is the patched file with spaces.\n"
156+
Expect(string(patchedContent)).To(Equal(expectedPatchedContent))
101157
})
102158
})
103159
})

0 commit comments

Comments
 (0)