Skip to content

Commit

Permalink
Merge pull request #1496 from merico-dev/new-dtm
Browse files Browse the repository at this point in the history
Update CI Workflow and Fix Mixed Tab and Space Indentation in Patch File
  • Loading branch information
daniel-hutao committed Apr 27, 2023
2 parents 782ee3c + aac178c commit 7c411c4
Show file tree
Hide file tree
Showing 3 changed files with 249 additions and 16 deletions.
28 changes: 28 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: CI

on:
push:
branches:
- main
pull_request:
branches:
- main

jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: 1.20
cache: true

- name: Check out code
uses: actions/checkout@v3

- name: Run tests
run: go test -v ./...

- name: Build project
run: make build
151 changes: 150 additions & 1 deletion internal/pkg/patch/patch.go
Original file line number Diff line number Diff line change
@@ -1,19 +1,36 @@
package patch

import (
"bufio"
"fmt"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"

"github.com/devstream-io/devstream/internal/log"
)

const (
processOptionTabToSpace ProcessOption = "tabToSpace"
processOptionSpaceToTab ProcessOption = "spaceToTab"
)

type ProcessOption string

// Patch calls the patch command to apply a diff file to an original
func Patch(workDir, patchFile string) error {
log.Infof("Patching file: %s", patchFile)

// Fix patch file if it mixed tab and space indentation
err := fixPatchFile(workDir, patchFile)
if err != nil {
return fmt.Errorf("patch file fix failed: %w", err)
}

// Check if the patch command exists and is executable
err := checkPatchCommand()
err = checkPatchCommand()
if err != nil {
return fmt.Errorf("patch command check failed: %w", err)
}
Expand Down Expand Up @@ -50,3 +67,135 @@ func checkPatchCommand() error {

return nil
}

// fixPatchFile fixes the patch file if it mixed tab and space indentation.
// The patch file is generated by GPT4, and it may have different indentation with the original file.
// The original file path is contained in the patch file, so we can use the fix the patch file by using the original file.
// If the original file uses tab indentation, we replace all spaces with tabs in the patch file.
// If the original file uses space indentation, we replace all tabs with spaces in the patch file.
func fixPatchFile(workDir, patchFile string) error {
// Read the original file path from the patch file
originalFilePath, err := extractOriginalFilePathFromPatchFile(patchFile)
originalFilePath = filepath.Join(workDir, originalFilePath)

if err != nil {
return fmt.Errorf("failed to extract original file path from patch string: %w", err)
}

// Check if the original file contain tabs in the indentation
original, err := os.Open(originalFilePath)
if err != nil {
return fmt.Errorf("failed to open original file: %w", err)
}
defer original.Close()

hasTab := false
scanner := bufio.NewScanner(original)
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(line, "\t") {
hasTab = true
break
}
}

if err = scanner.Err(); err != nil {
return fmt.Errorf("failed to read original file: %w", err)
}

// The original file uses tab indentation
if hasTab {
// Replace all space indentation with tabs in the patch file
if err = processTabSpaceSwitch(patchFile, processOptionSpaceToTab); err != nil {
return fmt.Errorf("failed to process tab to space: %w", err)
}
// The original file uses space indentation
} else {
// Replace all tab indentation with spaces in the patch file
if err = processTabSpaceSwitch(patchFile, processOptionTabToSpace); err != nil {
return fmt.Errorf("failed to process space to tab: %w", err)
}
}

return nil

}

// ExtractOriginalFilePathFromPatchString extracts the original file path from a patch string
// e.g. --- pkg/patch/patch.go 2021-08-15 16:00:00.000000000 +0900 -> pkg/patch/patch.go
func extractOriginalFilePathFromPatchFile(patchFile string) (string, error) {
// Read content from the patch file
fileContent, err := os.ReadFile(patchFile)
if err != nil {
return "", fmt.Errorf("failed to read patch file: %w", err)
}

lines := strings.Split(string(fileContent), "\n")

for _, line := range lines {
if strings.HasPrefix(line, "--- ") {
fields := strings.Fields(line)
if len(fields) > 1 {
return fields[1], nil
}
}
}

return "", fmt.Errorf("original file path not found in patch string")
}

// processTabSpaceSwitch processes the tab/space indentation switch in a file
// If the option is processOptionTabToSpace, it replaces all tabs with spaces
// If the option is processOptionSpaceToTab, it replaces all spaces with tabs
func processTabSpaceSwitch(filePath string, option ProcessOption) error {
file, err := os.Open(filePath)
if err != nil {
return fmt.Errorf("failed to open file: %w", err)
}
defer file.Close()

scanner := bufio.NewScanner(file)
var processedLines []string

// Matches the start of the string (^) followed by an optional + or - sign, followed by one or more groups of 4 spaces ( {4})+
spaceRegex := regexp.MustCompile(`^(\+|\-)?( {4})+`)
// Matches the start of the string (^) followed by an optional + or - sign, followed by one or more tabs (\t)+
tabRegex := regexp.MustCompile(`^(\+|\-)?\t+`)

for scanner.Scan() {
line := scanner.Text()
if option == processOptionTabToSpace {
line = tabRegex.ReplaceAllStringFunc(line, func(s string) string {
prefix := ""
if s[0] == '+' || s[0] == '-' {
prefix = string(s[0])
s = s[1:]
}
return prefix + strings.Repeat(" ", len(s))
})
} else if option == processOptionSpaceToTab {
line = spaceRegex.ReplaceAllStringFunc(line, func(s string) string {
prefix := ""
if s[0] == '+' || s[0] == '-' {
prefix = string(s[0])
s = s[1:]
}
return prefix + strings.Repeat("\t", len(s)/4)
})
} else {
return fmt.Errorf("invalid process option: %s", option)
}
processedLines = append(processedLines, line)
}

if err = scanner.Err(); err != nil {
return fmt.Errorf("failed to read file: %w", err)
}

err = os.WriteFile(filePath, []byte(strings.Join(processedLines, "\n")+"\n"), 0644)
if err != nil {
return fmt.Errorf("failed to write file: %w", err)
}

return nil
}
86 changes: 71 additions & 15 deletions internal/pkg/patch/patch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,32 +72,88 @@ This is the original file.
expectedPatchedContent := `Hello, world!
This is the patched file.
`
Expect(string(patchedContent)).To(Equal(expectedPatchedContent))
patchedContentStr := string(patchedContent)
Expect(patchedContentStr).To(Equal(expectedPatchedContent))
})

It("returns an error if the patch command is not found or not executable", func() {
// Temporarily change PATH to exclude the real patch command
originalPath := os.Getenv("PATH")
err := os.Setenv("PATH", tempDir)
It("returns an error if the patch file is invalid", func() {
originalContent := `Hello, world!
This is the original file.
`

err := os.WriteFile(originalFile.Name(), []byte(originalContent), 0644)
Expect(err).NotTo(HaveOccurred())

invalidPatchContent := fmt.Sprintf(`--- %s
+++ new-file
@@ -1,2 +1,2 @@
`,
filepath.Base(originalFile.Name()))

err = os.WriteFile(patchFile.Name(), []byte(invalidPatchContent), 0644)
Expect(err).NotTo(HaveOccurred())
defer func() {
err := os.Setenv("PATH", originalPath)
Expect(err).NotTo(HaveOccurred())
}()

err = Patch(tempDir, patchFile.Name())
Expect(err).To(HaveOccurred())
Expect(strings.Contains(err.Error(), "patch command not found")).To(BeTrue())
Expect(strings.Contains(err.Error(), "patch command failed")).To(BeTrue())
})
})

It("returns an error if the patch file is invalid", func() {
invalidPatchContent := `This is not a valid patch file.`
err := os.WriteFile(patchFile.Name(), []byte(invalidPatchContent), 0644)
Context("when patching a file with inconsistent indentation", func() {
It("successfully applies the patch with spaces to the original file with tabs", func() {
originalContent := "Hello, world!\n\tThis is the original file with tabs.\n"

err := os.WriteFile(originalFile.Name(), []byte(originalContent), 0644)
Expect(err).NotTo(HaveOccurred())

patchContent := fmt.Sprintf(`--- %s
+++ new-file
@@ -1,2 +1,2 @@
Hello, world!
- This is the original file with tabs.
+ This is the patched file with tabs.
`,
filepath.Base(originalFile.Name()))

err = os.WriteFile(patchFile.Name(), []byte(patchContent), 0644)
Expect(err).NotTo(HaveOccurred())

err = Patch(tempDir, patchFile.Name())
Expect(err).To(HaveOccurred())
Expect(strings.Contains(err.Error(), "patch command failed")).To(BeTrue())
Expect(err).NotTo(HaveOccurred())

patchedContent, err := os.ReadFile(originalFile.Name())
Expect(err).NotTo(HaveOccurred())

expectedPatchedContent := "Hello, world!\n\tThis is the patched file with tabs.\n"
Expect(string(patchedContent)).To(Equal(expectedPatchedContent))
})

It("successfully applies the patch with tabs to the original file with spaces", func() {
originalContent := "Hello, world!\n This is the original file with spaces.\n"

err := os.WriteFile(originalFile.Name(), []byte(originalContent), 0644)
Expect(err).NotTo(HaveOccurred())

patchContent := fmt.Sprintf(`--- %s
+++ new-file
@@ -1,2 +1,2 @@
Hello, world!
- This is the original file with spaces.
+ This is the patched file with spaces.
`,
filepath.Base(originalFile.Name()))

err = os.WriteFile(patchFile.Name(), []byte(patchContent), 0644)
Expect(err).NotTo(HaveOccurred())

err = Patch(tempDir, patchFile.Name())
Expect(err).NotTo(HaveOccurred())

patchedContent, err := os.ReadFile(originalFile.Name())
Expect(err).NotTo(HaveOccurred())

expectedPatchedContent := "Hello, world!\n This is the patched file with spaces.\n"
Expect(string(patchedContent)).To(Equal(expectedPatchedContent))
})
})
})

0 comments on commit 7c411c4

Please sign in to comment.