Skip to content

Commit 8e16e3e

Browse files
authored
Merge pull request #326 from neongreen/copilot/finish-folder-sync-implementation
Complete folder sync implementation: exclude patterns and duplicate command fix
2 parents 20e31f8 + 8427a9a commit 8e16e3e

9 files changed

Lines changed: 767 additions & 24 deletions

File tree

conf/cmd/apply.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ func applyFolder(conf *config.Config, configDir, folderName string, dryRun bool)
161161

162162
// Detect drift
163163
confPath := config.FolderCopyPath(configDir, folderName)
164-
drifts, err := folders.DetectDrift(folder.SourcePath, confPath)
164+
drifts, err := folders.DetectDriftWithExcludes(folder.SourcePath, confPath, folder.Exclude)
165165
if err != nil {
166166
return fmt.Errorf("failed to detect drift: %w", err)
167167
}

conf/cmd/import_folders.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ func importFolder(conf *config.Config, configDir, folderName string, dryRun bool
2525

2626
// Detect drift
2727
confPath := config.FolderCopyPath(configDir, folderName)
28-
drifts, err := folders.DetectDrift(folder.SourcePath, confPath)
28+
drifts, err := folders.DetectDriftWithExcludes(folder.SourcePath, confPath, folder.Exclude)
2929
if err != nil {
3030
return fmt.Errorf("failed to detect drift: %w", err)
3131
}

conf/cmd/root.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ func init() {
3535
RootCmd.AddCommand(starshipCmd)
3636
RootCmd.AddCommand(shimsCmd)
3737
RootCmd.AddCommand(applyCmd)
38-
RootCmd.AddCommand(statusCmd)
38+
// statusCmd is registered in status.go init()
3939
RootCmd.AddCommand(syncCmd)
4040
RootCmd.AddCommand(importCmd)
4141
RootCmd.AddCommand(completionCmd)

conf/cmd/status.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ func showFolderStatus(conf *config.Config, configDir, folderName string) error {
124124

125125
// Detect drift
126126
confPath := config.FolderCopyPath(configDir, folderName)
127-
drifts, err := folders.DetectDrift(folder.SourcePath, confPath)
127+
drifts, err := folders.DetectDriftWithExcludes(folder.SourcePath, confPath, folder.Exclude)
128128
if err != nil {
129129
return fmt.Errorf("failed to detect drift: %w", err)
130130
}

conf/cmd/sync_folders.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ func syncFolder(conf *config.Config, configDir, folderName string, dryRun bool)
3939
}
4040
} else {
4141
// Detect drift between source and conf copy
42-
drifts, err := folders.DetectDrift(sourcePath, confCopyPath)
42+
drifts, err := folders.DetectDriftWithExcludes(sourcePath, confCopyPath, folder.Exclude)
4343
if err != nil {
4444
return fmt.Errorf("failed to detect drift: %w", err)
4545
}
@@ -75,7 +75,8 @@ func syncFolder(conf *config.Config, configDir, folderName string, dryRun bool)
7575
}
7676

7777
// Detect drift between conf copy and iCloud
78-
icloudDrifts, err := folders.DetectDrift(confCopyPath, icloudFolderPath)
78+
// Note: We use the same exclude patterns for iCloud sync to maintain consistency
79+
icloudDrifts, err := folders.DetectDriftWithExcludes(confCopyPath, icloudFolderPath, folder.Exclude)
7980
if err != nil {
8081
return fmt.Errorf("failed to detect iCloud drift: %w", err)
8182
}

conf/cmd/track.go

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,8 @@ Examples:
7474
fmt.Printf(" Name: %s\n", folderName)
7575
fmt.Printf(" Copy to: %s\n", folderCopyPath)
7676

77-
// Copy folder to conf directory
78-
if err := copyDir(expandedSource, folderCopyPath); err != nil {
77+
// Copy folder to conf directory, excluding specified patterns
78+
if err := copyDir(expandedSource, folderCopyPath, excludePatterns); err != nil {
7979
return fmt.Errorf("failed to copy folder: %w", err)
8080
}
8181
fmt.Printf(" ✓ Copied folder\n")
@@ -111,8 +111,8 @@ Examples:
111111
},
112112
}
113113

114-
// copyDir recursively copies a directory
115-
func copyDir(src, dst string) error {
114+
// copyDir recursively copies a directory, skipping files that match exclude patterns
115+
func copyDir(src, dst string, excludePatterns []string) error {
116116
// Get source directory info
117117
srcInfo, err := os.Stat(src)
118118
if err != nil {
@@ -131,12 +131,17 @@ func copyDir(src, dst string) error {
131131
}
132132

133133
for _, entry := range entries {
134+
// Check if entry should be excluded
135+
if shouldExclude(entry.Name(), excludePatterns) {
136+
continue
137+
}
138+
134139
srcPath := filepath.Join(src, entry.Name())
135140
dstPath := filepath.Join(dst, entry.Name())
136141

137142
if entry.IsDir() {
138143
// Recursively copy subdirectory
139-
if err := copyDir(srcPath, dstPath); err != nil {
144+
if err := copyDir(srcPath, dstPath, excludePatterns); err != nil {
140145
return err
141146
}
142147
} else {
@@ -150,6 +155,18 @@ func copyDir(src, dst string) error {
150155
return nil
151156
}
152157

158+
// shouldExclude checks if a filename matches any of the exclude patterns.
159+
// Invalid patterns are silently ignored (treated as non-matching).
160+
func shouldExclude(filename string, patterns []string) bool {
161+
for _, pattern := range patterns {
162+
// Error from filepath.Match indicates invalid pattern syntax - we skip such patterns
163+
if matched, err := filepath.Match(pattern, filename); err == nil && matched {
164+
return true
165+
}
166+
}
167+
return false
168+
}
169+
153170
// copyFile copies a single file, preserving permissions
154171
func copyFile(src, dst string) error {
155172
// Open source file

conf/pkg/folders/drift.go

Lines changed: 58 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,25 +13,33 @@ import (
1313
type FileStatus string
1414

1515
const (
16-
StatusInSync FileStatus = "IN_SYNC" // File content matches
17-
StatusModified FileStatus = "MODIFIED" // File exists in both, content differs
18-
StatusAdded FileStatus = "ADDED" // File in source, not in conf copy
19-
StatusDeleted FileStatus = "DELETED" // File in conf copy, not in source
16+
StatusInSync FileStatus = "IN_SYNC" // File content matches
17+
StatusModified FileStatus = "MODIFIED" // File exists in both, content differs
18+
StatusAdded FileStatus = "ADDED" // File in source, not in conf copy
19+
StatusDeleted FileStatus = "DELETED" // File in conf copy, not in source
2020
)
2121

2222
// FileDrift represents a difference between source and conf copy
2323
type FileDrift struct {
24-
RelPath string // Relative path from folder root
25-
Status FileStatus
26-
SourceHash string // SHA256 hash of source file (if exists)
27-
ConfHash string // SHA256 hash of conf file (if exists)
28-
SourceMtime int64 // Modification time of source file
29-
ConfMtime int64 // Modification time of conf file
30-
IsDir bool // Whether this is a directory
24+
RelPath string // Relative path from folder root
25+
Status FileStatus
26+
SourceHash string // SHA256 hash of source file (if exists)
27+
ConfHash string // SHA256 hash of conf file (if exists)
28+
SourceMtime int64 // Modification time of source file
29+
ConfMtime int64 // Modification time of conf file
30+
IsDir bool // Whether this is a directory
3131
}
3232

33-
// DetectDrift compares source folder with conf copy and returns all differences
33+
// DetectDrift compares source folder with conf copy and returns all differences.
34+
// This is a convenience wrapper that calls DetectDriftWithExcludes with no excludes.
3435
func DetectDrift(sourcePath, confPath string) ([]FileDrift, error) {
36+
return DetectDriftWithExcludes(sourcePath, confPath, nil)
37+
}
38+
39+
// DetectDriftWithExcludes compares source folder with conf copy and returns all differences,
40+
// excluding files that match any of the provided patterns.
41+
// Patterns support shell-style wildcards (*, ?) via filepath.Match.
42+
func DetectDriftWithExcludes(sourcePath, confPath string, excludePatterns []string) ([]FileDrift, error) {
3543
var drifts []FileDrift
3644

3745
// Build maps of all files in source and conf
@@ -51,6 +59,13 @@ func DetectDrift(sourcePath, confPath string) ([]FileDrift, error) {
5159
if relPath == "." {
5260
return nil
5361
}
62+
// Skip excluded files
63+
if shouldExclude(relPath, info.Name(), excludePatterns) {
64+
if info.IsDir() {
65+
return filepath.SkipDir
66+
}
67+
return nil
68+
}
5469
sourceFiles[relPath] = info
5570
return nil
5671
}); err != nil {
@@ -70,6 +85,13 @@ func DetectDrift(sourcePath, confPath string) ([]FileDrift, error) {
7085
if relPath == "." {
7186
return nil
7287
}
88+
// Skip excluded files
89+
if shouldExclude(relPath, info.Name(), excludePatterns) {
90+
if info.IsDir() {
91+
return filepath.SkipDir
92+
}
93+
return nil
94+
}
7395
confFiles[relPath] = info
7496
return nil
7597
}); err != nil {
@@ -196,5 +218,28 @@ func FormatDriftSummary(drifts []FileDrift) string {
196218
parts = append(parts, fmt.Sprintf("%d deleted", count))
197219
}
198220

199-
return fmt.Sprintf("%d files with drift (%s)", len(drifts), strings.Join(parts, ", "))
221+
fileWord := "files"
222+
if len(drifts) == 1 {
223+
fileWord = "file"
224+
}
225+
return fmt.Sprintf("%d %s with drift (%s)", len(drifts), fileWord, strings.Join(parts, ", "))
226+
}
227+
228+
// shouldExclude returns true if a file path matches any of the exclude patterns.
229+
// Patterns are matched against both the full relative path and the base filename.
230+
// Supports shell-style wildcards (* and ?) via filepath.Match.
231+
// Invalid patterns are silently ignored (treated as non-matching).
232+
func shouldExclude(relPath, baseName string, excludePatterns []string) bool {
233+
for _, pattern := range excludePatterns {
234+
// Match against base filename (e.g., "*.tmp" matches "foo.tmp")
235+
// Error from filepath.Match indicates invalid pattern syntax - we skip such patterns
236+
if matched, err := filepath.Match(pattern, baseName); err == nil && matched {
237+
return true
238+
}
239+
// Match against full relative path (e.g., "subdir/*.log")
240+
if matched, err := filepath.Match(pattern, relPath); err == nil && matched {
241+
return true
242+
}
243+
}
244+
return false
200245
}

0 commit comments

Comments
 (0)