Skip to content

Commit 9739e70

Browse files
authored
Merge pull request #98 from vinyas-bharadwaj/main
2 parents 9ff358b + 654dad4 commit 9739e70

3 files changed

Lines changed: 230 additions & 49 deletions

File tree

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ toolchain go1.24.7
66

77
require (
88
github.com/atotto/clipboard v0.1.4
9-
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
109
github.com/google/generative-ai-go v0.19.0
10+
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
1111
github.com/manifoldco/promptui v0.9.0
1212
github.com/openai/openai-go/v3 v3.0.1
1313
github.com/pterm/pterm v0.12.80

internal/git/operations.go

Lines changed: 176 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,107 @@ func IsRepository(path string) bool {
2222
return strings.TrimSpace(string(output)) == "true"
2323
}
2424

25+
// parseGitStatusLine represents a parsed git status line
26+
type parseGitStatusLine struct {
27+
status string
28+
filenames []string
29+
}
30+
31+
// parseGitNameStatus parses a single line from git diff --name-status output
32+
// Handles various git status codes including rename (R) and copy (C) operations
33+
func parseGitNameStatus(line string) parseGitStatusLine {
34+
if line == "" {
35+
return parseGitStatusLine{}
36+
}
37+
38+
// Git uses tabs to separate fields in --name-status output
39+
parts := strings.Split(line, "\t")
40+
if len(parts) < 2 {
41+
return parseGitStatusLine{}
42+
}
43+
44+
status := parts[0]
45+
46+
// Handle rename/copy status codes (e.g., "R100", "C75")
47+
if len(status) > 1 && (status[0] == 'R' || status[0] == 'C') {
48+
// For rename/copy, we expect: "R100\toldname\tnewname" or "C75\toldname\tnewname"
49+
if len(parts) >= 3 {
50+
// For renames/copies, both old and new filenames need to be checked
51+
oldFile := parts[1]
52+
newFile := parts[2]
53+
return parseGitStatusLine{
54+
status: status,
55+
filenames: []string{oldFile, newFile},
56+
}
57+
}
58+
}
59+
60+
// Handle regular status codes (M, A, D, etc.)
61+
filename := parts[1]
62+
return parseGitStatusLine{
63+
status: status,
64+
filenames: []string{filename},
65+
}
66+
}
67+
68+
// processGitStatusOutput processes git diff --name-status output and returns filtered results
69+
func processGitStatusOutput(nameStatusOutput string, returnFilenames bool) ([]string, []string) {
70+
if nameStatusOutput == "" {
71+
return nil, nil
72+
}
73+
74+
lines := strings.Split(strings.TrimSpace(nameStatusOutput), "\n")
75+
var filteredLines []string
76+
var nonBinaryFiles []string
77+
78+
for _, line := range lines {
79+
if line == "" {
80+
continue
81+
}
82+
83+
parsed := parseGitNameStatus(line)
84+
if len(parsed.filenames) == 0 {
85+
continue
86+
}
87+
88+
// Check if any of the filenames are binary
89+
hasBinaryFile := false
90+
for _, filename := range parsed.filenames {
91+
if utils.IsBinaryFile(filename) {
92+
hasBinaryFile = true
93+
break
94+
}
95+
}
96+
97+
// If no binary files found, include this line/files
98+
if !hasBinaryFile {
99+
filteredLines = append(filteredLines, line)
100+
if returnFilenames {
101+
nonBinaryFiles = append(nonBinaryFiles, parsed.filenames...)
102+
}
103+
}
104+
}
105+
106+
return filteredLines, nonBinaryFiles
107+
}
108+
109+
// filterBinaryFiles filters out binary files from git diff --name-status output
110+
func filterBinaryFiles(nameStatusOutput string) string {
111+
filteredLines, _ := processGitStatusOutput(nameStatusOutput, false)
112+
113+
if len(filteredLines) == 0 {
114+
return ""
115+
}
116+
117+
return strings.Join(filteredLines, "\n")
118+
}
119+
120+
// extractNonBinaryFiles extracts non-binary filenames from git diff --name-status output
121+
func extractNonBinaryFiles(nameStatusOutput string) []string {
122+
_, nonBinaryFiles := processGitStatusOutput(nameStatusOutput, true)
123+
return nonBinaryFiles
124+
}
125+
25126
// GetChanges retrieves all Git changes including staged, unstaged, and untracked files
26127
func GetChanges(config *types.RepoConfig) (string, error) {
27128
var changes strings.Builder
@@ -34,20 +135,29 @@ func GetChanges(config *types.RepoConfig) (string, error) {
34135
}
35136

36137
if len(output) > 0 {
37-
changes.WriteString("Unstaged changes:\n")
38-
changes.WriteString(string(output))
39-
changes.WriteString("\n\n")
40-
41-
// Get the content of these changes
42-
diffCmd := exec.Command("git", "-C", config.Path, "diff")
43-
diffOutput, err := diffCmd.Output()
44-
if err != nil {
45-
return "", fmt.Errorf("git diff content failed: %v", err)
46-
}
138+
// Filter out binary files from the name-status output
139+
filteredOutput := filterBinaryFiles(string(output))
140+
141+
if filteredOutput != "" {
142+
changes.WriteString("Unstaged changes:\n")
143+
changes.WriteString(filteredOutput)
144+
changes.WriteString("\n\n")
145+
146+
// Get the content of these changes (only for non-binary files)
147+
nonBinaryFiles := extractNonBinaryFiles(string(output))
148+
if len(nonBinaryFiles) > 0 {
149+
diffCmd := exec.Command("git", "-C", config.Path, "diff", "--")
150+
diffCmd.Args = append(diffCmd.Args, nonBinaryFiles...)
151+
diffOutput, err := diffCmd.Output()
152+
if err != nil {
153+
return "", fmt.Errorf("git diff content failed: %v", err)
154+
}
47155

48-
changes.WriteString("Unstaged diff content:\n")
49-
changes.WriteString(string(diffOutput))
50-
changes.WriteString("\n\n")
156+
changes.WriteString("Unstaged diff content:\n")
157+
changes.WriteString(string(diffOutput))
158+
changes.WriteString("\n\n")
159+
}
160+
}
51161
}
52162

53163
// 2. Check for staged changes
@@ -58,20 +168,29 @@ func GetChanges(config *types.RepoConfig) (string, error) {
58168
}
59169

60170
if len(stagedOutput) > 0 {
61-
changes.WriteString("Staged changes:\n")
62-
changes.WriteString(string(stagedOutput))
63-
changes.WriteString("\n\n")
64-
65-
// Get the content of these changes
66-
stagedDiffCmd := exec.Command("git", "-C", config.Path, "diff", "--cached")
67-
stagedDiffOutput, err := stagedDiffCmd.Output()
68-
if err != nil {
69-
return "", fmt.Errorf("git diff --cached content failed: %v", err)
70-
}
171+
// Filter out binary files from the staged changes
172+
filteredStagedOutput := filterBinaryFiles(string(stagedOutput))
173+
174+
if filteredStagedOutput != "" {
175+
changes.WriteString("Staged changes:\n")
176+
changes.WriteString(filteredStagedOutput)
177+
changes.WriteString("\n\n")
71178

72-
changes.WriteString("Staged diff content:\n")
73-
changes.WriteString(string(stagedDiffOutput))
74-
changes.WriteString("\n\n")
179+
// Get the content of these changes (only for non-binary files)
180+
nonBinaryStagedFiles := extractNonBinaryFiles(string(stagedOutput))
181+
if len(nonBinaryStagedFiles) > 0 {
182+
stagedDiffCmd := exec.Command("git", "-C", config.Path, "diff", "--cached", "--")
183+
stagedDiffCmd.Args = append(stagedDiffCmd.Args, nonBinaryStagedFiles...)
184+
stagedDiffOutput, err := stagedDiffCmd.Output()
185+
if err != nil {
186+
return "", fmt.Errorf("git diff --cached content failed: %v", err)
187+
}
188+
189+
changes.WriteString("Staged diff content:\n")
190+
changes.WriteString(string(stagedDiffOutput))
191+
changes.WriteString("\n\n")
192+
}
193+
}
75194
}
76195

77196
// 3. Check for untracked files
@@ -82,34 +201,44 @@ func GetChanges(config *types.RepoConfig) (string, error) {
82201
}
83202

84203
if len(untrackedOutput) > 0 {
85-
changes.WriteString("Untracked files:\n")
86-
changes.WriteString(string(untrackedOutput))
87-
changes.WriteString("\n\n")
88-
89-
// Try to get content of untracked files (limited to text files and smaller size)
204+
// Filter out binary files from untracked files
90205
untrackedFiles := strings.Split(strings.TrimSpace(string(untrackedOutput)), "\n")
206+
var nonBinaryUntrackedFiles []string
207+
91208
for _, file := range untrackedFiles {
92209
if file == "" {
93210
continue
94211
}
212+
if !utils.IsBinaryFile(file) {
213+
nonBinaryUntrackedFiles = append(nonBinaryUntrackedFiles, file)
214+
}
215+
}
216+
217+
if len(nonBinaryUntrackedFiles) > 0 {
218+
changes.WriteString("Untracked files:\n")
219+
changes.WriteString(strings.Join(nonBinaryUntrackedFiles, "\n"))
220+
changes.WriteString("\n\n")
95221

96-
fullPath := filepath.Join(config.Path, file)
97-
if utils.IsTextFile(fullPath) && utils.IsSmallFile(fullPath) {
98-
fileContent, err := os.ReadFile(fullPath)
99-
if err != nil {
100-
// Log but don't fail - untracked file may have been deleted or is inaccessible
101-
continue
102-
}
103-
changes.WriteString(fmt.Sprintf("Content of new file %s:\n", file))
104-
105-
// Use special scrubbing for .env files
106-
if strings.HasSuffix(strings.ToLower(file), ".env") ||
107-
strings.Contains(strings.ToLower(file), ".env.") {
108-
changes.WriteString(scrubber.ScrubEnvFile(string(fileContent)))
109-
} else {
110-
changes.WriteString(string(fileContent))
222+
// Try to get content of untracked files (limited to text files and smaller size)
223+
for _, file := range nonBinaryUntrackedFiles {
224+
fullPath := filepath.Join(config.Path, file)
225+
if utils.IsTextFile(fullPath) && utils.IsSmallFile(fullPath) {
226+
fileContent, err := os.ReadFile(fullPath)
227+
if err != nil {
228+
// Log but don't fail - untracked file may have been deleted or is inaccessible
229+
continue
230+
}
231+
changes.WriteString(fmt.Sprintf("Content of new file %s:\n", file))
232+
233+
// Use special scrubbing for .env files
234+
if strings.HasSuffix(strings.ToLower(file), ".env") ||
235+
strings.Contains(strings.ToLower(file), ".env.") {
236+
changes.WriteString(scrubber.ScrubEnvFile(string(fileContent)))
237+
} else {
238+
changes.WriteString(string(fileContent))
239+
}
240+
changes.WriteString("\n\n")
111241
}
112-
changes.WriteString("\n\n")
113242
}
114243
}
115244
}

internal/utils/utils.go

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,10 @@ func IsTextFile(filename string) bool {
2121
textExtensions := []string{
2222
".txt", ".md", ".go", ".js", ".py", ".java", ".c", ".cpp", ".h",
2323
".html", ".css", ".json", ".xml", ".yaml", ".yml", ".sh", ".bash",
24-
".ts", ".tsx", ".jsx", ".php", ".rb", ".rs", ".dart",
24+
".ts", ".tsx", ".jsx", ".php", ".rb", ".rs", ".dart", ".sql", ".r",
25+
".scala", ".kt", ".swift", ".m", ".pl", ".lua", ".vim", ".csv",
26+
".log", ".cfg", ".conf", ".ini", ".toml", ".lock", ".gitignore",
27+
".dockerfile", ".makefile", ".cmake", ".pro", ".pri", ".svg",
2528
}
2629

2730
ext := strings.ToLower(filepath.Ext(filename))
@@ -31,6 +34,55 @@ func IsTextFile(filename string) bool {
3134
}
3235
}
3336

37+
// Common extensionless files that are typically text
38+
if ext == "" {
39+
baseName := strings.ToLower(filepath.Base(filename))
40+
commonTextFiles := []string{
41+
"readme", "dockerfile", "makefile", "rakefile", "gemfile",
42+
"procfile", "jenkinsfile", "vagrantfile", "changelog", "authors",
43+
"contributors", "copying", "install", "news", "todo",
44+
}
45+
46+
for _, textFile := range commonTextFiles {
47+
if baseName == textFile {
48+
return true
49+
}
50+
}
51+
}
52+
53+
return false
54+
}
55+
56+
// IsBinaryFile checks if a file is likely to be a binary file that should be excluded from diffs
57+
func IsBinaryFile(filename string) bool {
58+
// List of common binary file extensions
59+
binaryExtensions := []string{
60+
// Images (excluding SVG which is XML text)
61+
".jpg", ".jpeg", ".png", ".gif", ".bmp", ".tiff", ".tif", ".ico", ".webp",
62+
// Audio/Video
63+
".mp3", ".mp4", ".avi", ".mkv", ".mov", ".wmv", ".flv", ".wav", ".ogg", ".m4a",
64+
// Archives/Compressed
65+
".zip", ".tar", ".gz", ".7z", ".rar", ".bz2", ".xz", ".lz", ".lzma",
66+
// Executables/Libraries
67+
".exe", ".dll", ".so", ".dylib", ".a", ".lib", ".bin", ".deb", ".rpm", ".dmg", ".msi",
68+
// Documents
69+
".pdf", ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx", ".odt", ".ods", ".odp",
70+
// Fonts
71+
".ttf", ".otf", ".woff", ".woff2", ".eot",
72+
// Other binary formats
73+
".db", ".sqlite", ".sqlite3", ".mdb", ".accdb", ".pickle", ".pkl", ".pyc", ".pyo",
74+
".class", ".jar", ".war", ".ear", ".apk", ".ipa",
75+
}
76+
77+
ext := strings.ToLower(filepath.Ext(filename))
78+
for _, binExt := range binaryExtensions {
79+
if ext == binExt {
80+
return true
81+
}
82+
}
83+
84+
// Note: Files with unknown extensions are not considered binary by default
85+
// This allows them to be processed as text files for diff analysis
3486
return false
3587
}
3688

0 commit comments

Comments
 (0)