Skip to content
Open
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
14 changes: 14 additions & 0 deletions cmd/patch.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package cmd

import (
"github.com/spf13/cobra"
)

var patchCmd = &cobra.Command{
Use: "patch",
Short: "Patch(es) management root command.",
}

func init() {
rootCmd.AddCommand(patchCmd)
}
28 changes: 28 additions & 0 deletions cmd/patch_record.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package cmd

import (
"fmt"

"github.com/RohitRavindra-dev/devlocal/internal/filesystem"
"github.com/RohitRavindra-dev/devlocal/internal/service/patch"
"github.com/spf13/cobra"
)

var patchRecordCmd = &cobra.Command{
Use: "record <file/directory>",
Short: "Record local modifications as a DevLocal patch",
Args: cobra.ExactArgs(1),
PreRunE: func(cmd *cobra.Command, args []string) error {
fmt.Println("[Started] to record a new patch")
// check for setup
return filesystem.ValidateDevLocalFilesystem()
},
RunE: func(cmd *cobra.Command, args []string) error {
path := args[0]
return patch.Run(path)
},
}

func init() {
patchCmd.AddCommand(patchRecordCmd)
}
13 changes: 11 additions & 2 deletions internal/filesystem/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import (
"gopkg.in/yaml.v3"
)

func FileExists(path string) (bool, error) {
// bool: isDir err: if path doesnt exists
func PathExists(path string) (bool, error) {
info, err := os.Stat(path)

if os.IsNotExist(err) {
Expand All @@ -20,7 +21,15 @@ func FileExists(path string) (bool, error) {
return false, err
}

if info.IsDir() {
return info.IsDir(), nil
}

func FileExists(path string) (bool, error) {
isDir, err := PathExists(path)

if err != nil {
return false, err
} else if isDir {
return false, fmt.Errorf("%s is a directory, expected a file", path)
}

Expand Down
48 changes: 48 additions & 0 deletions internal/git/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package git
import (
"fmt"
"os/exec"
"strings"
)

func executeGitCommand(args []string) error {
Expand All @@ -17,6 +18,18 @@ func executeGitCommand(args []string) error {
return nil
}

// TODO make this the only git command util
func executeGitCommandWithOutput(args []string) (string, error) {
cmd := exec.Command("git", args...)

out, err := cmd.CombinedOutput()
if err != nil {
return string(out), fmt.Errorf("failed to run git %v: %w\n%s", args, err, out)
}

return string(out), nil
}
Comment on lines +21 to +31

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Use exec.CommandContext for timeout/cancellation support.

exec.Command without a context means a hung git process will block the CLI indefinitely. The noctx linter also flags this. Consider accepting a context.Context (or at minimum a timeout) so callers can cancel long-running git operations.

🔒️ Proposed fix
-// TODO make this the only git command util
-func executeGitCommandWithOutput(args []string) (string, error) {
-	cmd := exec.Command("git", args...)
+// TODO make this the only git command util
+func executeGitCommandWithOutput(ctx context.Context, args []string) (string, error) {
+	ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
+	defer cancel()
+	cmd := exec.CommandContext("git", args...)
🧰 Tools
🪛 golangci-lint (2.12.2)

[error] 23-23: os/exec.Command must not be called. use os/exec.CommandContext

(noctx)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/git/utils.go` around lines 21 - 31, Update
executeGitCommandWithOutput to accept a context.Context parameter and construct
the command with exec.CommandContext instead of exec.Command, propagating the
context so callers can cancel or time-limit hung git operations. Update all
callers to pass their appropriate context while preserving the existing output
and error formatting.

Source: Linters/SAST tools


func isFileTracked(file string) bool {
trackCheckArgs := []string{
"ls-files",
Expand All @@ -28,3 +41,38 @@ func isFileTracked(file string) bool {

return err == nil
}

func FileHasChanges(filePath string) (bool, error) {
out, err := executeGitCommandWithOutput([]string{
"diff",
"--name-only",
"--",
filePath,
})
if err != nil {
return false, err
}

return strings.TrimSpace(out) != "", nil
}

func ListFilesWithChangesInDir(dirpath string) ([]string, error) {
filesWithChanges, err := executeGitCommandWithOutput([]string{
"diff",
"--name-only",
"--",
dirpath,
})

if err != nil {
return nil, err
}

filesWithChanges = strings.TrimSpace(filesWithChanges)

if filesWithChanges == "" {
return []string{}, nil
}

return strings.Split(filesWithChanges, "\n"), nil
}
Comment on lines +59 to +78

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Handle carriage returns in filenames on Windows.

strings.Split(filesWithChanges, "\n") will leave trailing \r characters in filenames when git runs on Windows (CRLF line endings). This could cause downstream path mismatches. Consider using strings.Fields or trimming \r from each entry.

💚 Proposed fix
 	return strings.Split(filesWithChanges, "\n"), nil
+	// Alternative: handles \r\n on Windows
+	// return strings.Fields(filesWithChanges), nil
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/git/utils.go` around lines 59 - 78, Update ListFilesWithChangesInDir
to normalize Git’s CRLF output before returning filenames, ensuring entries
split from filesWithChanges do not retain trailing carriage returns. Preserve
the existing empty-result behavior and error propagation.

56 changes: 56 additions & 0 deletions internal/service/patch/patcher.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package patch

import (
"fmt"
"strings"

"github.com/RohitRavindra-dev/devlocal/internal/filesystem"
"github.com/RohitRavindra-dev/devlocal/internal/git"
)

func listFilesToPatch(path string, isDir bool) ([]string, error) {

filesToPatch := []string{}

if isDir {
filesWithChanges, err := git.ListFilesWithChangesInDir(path)
if err != nil {
return nil, err
}
filesToPatch = append(filesToPatch, filesWithChanges...)
} else {
hasChanges, err := git.FileHasChanges(path)
if err != nil {
return nil, err
}
if hasChanges {
filesToPatch = append(filesToPatch, path)
}
}

return filesToPatch, nil

}

func Run(path string) error {

if path == "" {
return fmt.Errorf("[Error] path is empty!")
}

isDir, err := filesystem.PathExists(path)
if err != nil {
return err
}

filesToPatch, err := listFilesToPatch(path, isDir)
if err != nil {
return err
}
if len(filesToPatch) == 0 {
return fmt.Errorf("[Error] no files with changes at indicated path: %s", path)
}
fmt.Printf("[Started] applying patch to files: \n\t\t- %s\n", strings.Join(filesToPatch, "\n\t\t- "))
fmt.Println("[Completed] recording new patch into devlocal")
return nil
}
Comment on lines +35 to +56

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Fix error string punctuation and misleading status message.

Two issues:

  1. Line 38: "[Error] path is empty!" ends with ! — staticcheck ST1005 requires error strings not end with punctuation or newlines.
  2. Line 53: The message says "applying patch to files" but this is the record command, not apply. The message is misleading to users.
💚 Proposed fixes
-		return fmt.Errorf("[Error] path is empty!")
+		return fmt.Errorf("[Error] path is empty")
-	fmt.Printf("[Started] applying patch to files: \n\t\t- %s\n", strings.Join(filesToPatch, "\n\t\t- "))
-	fmt.Println("[Completed] recording new patch into devlocal")
+	fmt.Printf("[Started] recording patch for files: \n\t\t- %s\n", strings.Join(filesToPatch, "\n\t\t- "))
+	fmt.Println("[Completed] recording new patch into devlocal")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func Run(path string) error {
if path == "" {
return fmt.Errorf("[Error] path is empty!")
}
isDir, err := filesystem.PathExists(path)
if err != nil {
return err
}
filesToPatch, err := listFilesToPatch(path, isDir)
if err != nil {
return err
}
if len(filesToPatch) == 0 {
return fmt.Errorf("[Error] no files with changes at indicated path: %s", path)
}
fmt.Printf("[Started] applying patch to files: \n\t\t- %s\n", strings.Join(filesToPatch, "\n\t\t- "))
fmt.Println("[Completed] recording new patch into devlocal")
return nil
}
func Run(path string) error {
if path == "" {
return fmt.Errorf("[Error] path is empty")
}
isDir, err := filesystem.PathExists(path)
if err != nil {
return err
}
filesToPatch, err := listFilesToPatch(path, isDir)
if err != nil {
return err
}
if len(filesToPatch) == 0 {
return fmt.Errorf("[Error] no files with changes at indicated path: %s", path)
}
fmt.Printf("[Started] recording patch for files: \n\t\t- %s\n", strings.Join(filesToPatch, "\n\t\t- "))
fmt.Println("[Completed] recording new patch into devlocal")
return nil
}
🧰 Tools
🪛 golangci-lint (2.12.2)

[error] 38-38: ST1005: error strings should not end with punctuation or newlines

(staticcheck)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/service/patch/patcher.go` around lines 35 - 56, Update Run to remove
the trailing exclamation mark from the empty-path error so it satisfies ST1005,
and change the status message before completion to describe recording the patch
rather than applying it. Keep the existing control flow and file listing
unchanged.

Source: Linters/SAST tools

10 changes: 9 additions & 1 deletion todos.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,12 @@
- status
- conflicts
- overlook
- config
- config


2. record
1. we validate the path is not empty string
2. we validate the file is valid
3. we validate that the file has changes else an empty patch is rejected
4. we check if a patch exists for that file already, if yes defer/todo
5. else we create a patch
Loading