Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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))
})
})
})