From 2d7b9af6c9d94d3cdd16fd3fe6237632a2129c20 Mon Sep 17 00:00:00 2001 From: AnnatarHe Date: Wed, 17 Sep 2025 14:46:39 +0800 Subject: [PATCH] feat(dotfile): add support for ignoring sections in collected files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add optional parameter to CollectFromPaths to skip content between SHELLTIME IGNORE BEGIN and SHELLTIME IGNORE END markers. This allows users to exclude sensitive information from dotfile collection. Fixes #102 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- model/dotfile_apps.go | 43 ++++++++++++++++++++++++++++- model/dotfile_apps_test.go | 54 +++++++++++++++++++++++++++++++++---- model/dotfile_bash.go | 3 ++- model/dotfile_claude.go | 3 ++- model/dotfile_fish.go | 3 ++- model/dotfile_ghostty.go | 3 ++- model/dotfile_git.go | 3 ++- model/dotfile_kitty.go | 3 ++- model/dotfile_kubernetes.go | 3 ++- model/dotfile_npm.go | 3 ++- model/dotfile_nvim.go | 3 ++- model/dotfile_ssh.go | 3 ++- model/dotfile_starship.go | 3 ++- model/dotfile_zsh.go | 3 ++- 14 files changed, 115 insertions(+), 18 deletions(-) diff --git a/model/dotfile_apps.go b/model/dotfile_apps.go index 82fb603..7434cb1 100644 --- a/model/dotfile_apps.go +++ b/model/dotfile_apps.go @@ -110,7 +110,12 @@ func (b *BaseApp) readFileContent(path string) (string, *time.Time, error) { return string(content), &modTime, nil } -func (b *BaseApp) CollectFromPaths(_ context.Context, appName string, paths []string) ([]DotfileItem, error) { +func (b *BaseApp) CollectFromPaths(_ context.Context, appName string, paths []string, skipIgnoredSections *bool) ([]DotfileItem, error) { + // Default to true if not specified + shouldSkipIgnored := true + if skipIgnoredSections != nil { + shouldSkipIgnored = *skipIgnoredSections + } hostname, _ := os.Hostname() var dotfiles []DotfileItem @@ -143,6 +148,11 @@ func (b *BaseApp) CollectFromPaths(_ context.Context, appName string, paths []st continue } + // Filter ignored sections if requested + if shouldSkipIgnored { + content = b.filterIgnoredSections(content) + } + dotfiles = append(dotfiles, DotfileItem{ App: appName, Path: file, @@ -160,6 +170,11 @@ func (b *BaseApp) CollectFromPaths(_ context.Context, appName string, paths []st continue } + // Filter ignored sections if requested + if shouldSkipIgnored { + content = b.filterIgnoredSections(content) + } + dotfiles = append(dotfiles, DotfileItem{ App: appName, Path: expandedPath, @@ -188,6 +203,32 @@ func (b *BaseApp) collectFromDirectory(dir string) ([]string, error) { return files, err } +// filterIgnoredSections removes content between SHELLTIME IGNORE BEGIN and SHELLTIME IGNORE END markers +func (b *BaseApp) filterIgnoredSections(content string) string { + lines := strings.Split(content, "\n") + var filteredLines []string + var inIgnoreBlock bool + + for _, line := range lines { + // Check for ignore markers (can be in comments) + if strings.Contains(line, "SHELLTIME IGNORE BEGIN") { + inIgnoreBlock = true + continue + } + if strings.Contains(line, "SHELLTIME IGNORE END") { + inIgnoreBlock = false + continue + } + + // Only include lines that are not in an ignore block + if !inIgnoreBlock { + filteredLines = append(filteredLines, line) + } + } + + return strings.Join(filteredLines, "\n") +} + // IsEqual checks if the provided files match the local files by comparing SHA256 hashes func (b *BaseApp) IsEqual(_ context.Context, files map[string]string) (map[string]bool, error) { result := make(map[string]bool) diff --git a/model/dotfile_apps_test.go b/model/dotfile_apps_test.go index 211930d..f1b0889 100644 --- a/model/dotfile_apps_test.go +++ b/model/dotfile_apps_test.go @@ -134,7 +134,8 @@ func TestBaseApp_CollectFromPaths(t *testing.T) { require.NoError(t, err) t.Run("collect from single file", func(t *testing.T) { - dotfiles, err := app.CollectFromPaths(ctx, "testapp", []string{configFile}) + skipIgnored := true + dotfiles, err := app.CollectFromPaths(ctx, "testapp", []string{configFile}, &skipIgnored) require.NoError(t, err) assert.Len(t, dotfiles, 1) @@ -148,7 +149,8 @@ func TestBaseApp_CollectFromPaths(t *testing.T) { }) t.Run("collect from directory", func(t *testing.T) { - dotfiles, err := app.CollectFromPaths(ctx, "testapp", []string{subDir}) + skipIgnored := true + dotfiles, err := app.CollectFromPaths(ctx, "testapp", []string{subDir}, &skipIgnored) require.NoError(t, err) // Should find 2 files (hidden files are ignored) @@ -169,17 +171,58 @@ func TestBaseApp_CollectFromPaths(t *testing.T) { }) t.Run("collect from mixed paths", func(t *testing.T) { - dotfiles, err := app.CollectFromPaths(ctx, "testapp", []string{configFile, subDir}) + skipIgnored := true + dotfiles, err := app.CollectFromPaths(ctx, "testapp", []string{configFile, subDir}, &skipIgnored) require.NoError(t, err) assert.Len(t, dotfiles, 3) // 1 file + 2 files from directory }) t.Run("collect from non-existent path", func(t *testing.T) { nonExistentPath := filepath.Join(tmpDir, "does-not-exist") - dotfiles, err := app.CollectFromPaths(ctx, "testapp", []string{nonExistentPath}) + skipIgnored := true + dotfiles, err := app.CollectFromPaths(ctx, "testapp", []string{nonExistentPath}, &skipIgnored) require.NoError(t, err) assert.Empty(t, dotfiles) // Should skip non-existent paths }) + + t.Run("collect with ignored sections", func(t *testing.T) { + // Create a file with ignored sections + configWithIgnore := filepath.Join(tmpDir, "config_with_ignore.conf") + configContentWithIgnore := `line1 +# SHELLTIME IGNORE BEGIN +secret_key=123456 +password=hidden +# SHELLTIME IGNORE END +line2 +visible_key=value` + err = os.WriteFile(configWithIgnore, []byte(configContentWithIgnore), 0644) + require.NoError(t, err) + + // Test with skipIgnored = true (default behavior) + skipIgnored := true + dotfiles, err := app.CollectFromPaths(ctx, "testapp", []string{configWithIgnore}, &skipIgnored) + require.NoError(t, err) + require.Len(t, dotfiles, 1) + + // Should not contain ignored sections + assert.NotContains(t, dotfiles[0].Content, "secret_key") + assert.NotContains(t, dotfiles[0].Content, "password=hidden") + assert.NotContains(t, dotfiles[0].Content, "SHELLTIME IGNORE") + assert.Contains(t, dotfiles[0].Content, "line1") + assert.Contains(t, dotfiles[0].Content, "line2") + assert.Contains(t, dotfiles[0].Content, "visible_key=value") + + // Test with skipIgnored = false + skipIgnored = false + dotfiles, err = app.CollectFromPaths(ctx, "testapp", []string{configWithIgnore}, &skipIgnored) + require.NoError(t, err) + require.Len(t, dotfiles, 1) + + // Should contain all content including ignored sections + assert.Contains(t, dotfiles[0].Content, "secret_key") + assert.Contains(t, dotfiles[0].Content, "password=hidden") + assert.Contains(t, dotfiles[0].Content, "SHELLTIME IGNORE") + }) } func TestBaseApp_collectFromDirectory(t *testing.T) { @@ -524,7 +567,8 @@ func TestBaseApp_Integration(t *testing.T) { t.Run("full workflow", func(t *testing.T) { // 1. Collect dotfiles - dotfiles, err := app.CollectFromPaths(ctx, "testapp", []string{configFile, subDir}) + skipIgnored := true + dotfiles, err := app.CollectFromPaths(ctx, "testapp", []string{configFile, subDir}, &skipIgnored) require.NoError(t, err) assert.Len(t, dotfiles, 2) diff --git a/model/dotfile_bash.go b/model/dotfile_bash.go index a460f17..15d6317 100644 --- a/model/dotfile_bash.go +++ b/model/dotfile_bash.go @@ -25,5 +25,6 @@ func (b *BashApp) GetConfigPaths() []string { } func (b *BashApp) CollectDotfiles(ctx context.Context) ([]DotfileItem, error) { - return b.CollectFromPaths(ctx, b.Name(), b.GetConfigPaths()) + skipIgnored := true + return b.CollectFromPaths(ctx, b.Name(), b.GetConfigPaths(), &skipIgnored) } \ No newline at end of file diff --git a/model/dotfile_claude.go b/model/dotfile_claude.go index 4aa0850..07fe6f2 100644 --- a/model/dotfile_claude.go +++ b/model/dotfile_claude.go @@ -23,5 +23,6 @@ func (c *ClaudeApp) GetConfigPaths() []string { } func (c *ClaudeApp) CollectDotfiles(ctx context.Context) ([]DotfileItem, error) { - return c.CollectFromPaths(ctx, c.Name(), c.GetConfigPaths()) + skipIgnored := true + return c.CollectFromPaths(ctx, c.Name(), c.GetConfigPaths(), &skipIgnored) } diff --git a/model/dotfile_fish.go b/model/dotfile_fish.go index 5fc7573..ae277aa 100644 --- a/model/dotfile_fish.go +++ b/model/dotfile_fish.go @@ -24,5 +24,6 @@ func (f *FishApp) GetConfigPaths() []string { } func (f *FishApp) CollectDotfiles(ctx context.Context) ([]DotfileItem, error) { - return f.CollectFromPaths(ctx, f.Name(), f.GetConfigPaths()) + skipIgnored := true + return f.CollectFromPaths(ctx, f.Name(), f.GetConfigPaths(), &skipIgnored) } \ No newline at end of file diff --git a/model/dotfile_ghostty.go b/model/dotfile_ghostty.go index 1d9c26d..497ddbc 100644 --- a/model/dotfile_ghostty.go +++ b/model/dotfile_ghostty.go @@ -23,5 +23,6 @@ func (g *GhosttyApp) GetConfigPaths() []string { } func (g *GhosttyApp) CollectDotfiles(ctx context.Context) ([]DotfileItem, error) { - return g.CollectFromPaths(ctx, g.Name(), g.GetConfigPaths()) + skipIgnored := true + return g.CollectFromPaths(ctx, g.Name(), g.GetConfigPaths(), &skipIgnored) } \ No newline at end of file diff --git a/model/dotfile_git.go b/model/dotfile_git.go index dcd7a3a..e976412 100644 --- a/model/dotfile_git.go +++ b/model/dotfile_git.go @@ -25,5 +25,6 @@ func (g *GitApp) GetConfigPaths() []string { } func (g *GitApp) CollectDotfiles(ctx context.Context) ([]DotfileItem, error) { - return g.CollectFromPaths(ctx, g.Name(), g.GetConfigPaths()) + skipIgnored := true + return g.CollectFromPaths(ctx, g.Name(), g.GetConfigPaths(), &skipIgnored) } \ No newline at end of file diff --git a/model/dotfile_kitty.go b/model/dotfile_kitty.go index 796454c..7d6a77f 100644 --- a/model/dotfile_kitty.go +++ b/model/dotfile_kitty.go @@ -22,5 +22,6 @@ func (k *KittyApp) GetConfigPaths() []string { } func (k *KittyApp) CollectDotfiles(ctx context.Context) ([]DotfileItem, error) { - return k.CollectFromPaths(ctx, k.Name(), k.GetConfigPaths()) + skipIgnored := true + return k.CollectFromPaths(ctx, k.Name(), k.GetConfigPaths(), &skipIgnored) } diff --git a/model/dotfile_kubernetes.go b/model/dotfile_kubernetes.go index 9d997f7..b2f0da8 100644 --- a/model/dotfile_kubernetes.go +++ b/model/dotfile_kubernetes.go @@ -22,5 +22,6 @@ func (k *KubernetesApp) GetConfigPaths() []string { } func (k *KubernetesApp) CollectDotfiles(ctx context.Context) ([]DotfileItem, error) { - return k.CollectFromPaths(ctx, k.Name(), k.GetConfigPaths()) + skipIgnored := true + return k.CollectFromPaths(ctx, k.Name(), k.GetConfigPaths(), &skipIgnored) } diff --git a/model/dotfile_npm.go b/model/dotfile_npm.go index 3917517..5154e6b 100644 --- a/model/dotfile_npm.go +++ b/model/dotfile_npm.go @@ -22,5 +22,6 @@ func (n *NpmApp) GetConfigPaths() []string { } func (n *NpmApp) CollectDotfiles(ctx context.Context) ([]DotfileItem, error) { - return n.CollectFromPaths(ctx, n.Name(), n.GetConfigPaths()) + skipIgnored := true + return n.CollectFromPaths(ctx, n.Name(), n.GetConfigPaths(), &skipIgnored) } diff --git a/model/dotfile_nvim.go b/model/dotfile_nvim.go index 5522988..37feb68 100644 --- a/model/dotfile_nvim.go +++ b/model/dotfile_nvim.go @@ -23,5 +23,6 @@ func (n *NvimApp) GetConfigPaths() []string { } func (n *NvimApp) CollectDotfiles(ctx context.Context) ([]DotfileItem, error) { - return n.CollectFromPaths(ctx, n.Name(), n.GetConfigPaths()) + skipIgnored := true + return n.CollectFromPaths(ctx, n.Name(), n.GetConfigPaths(), &skipIgnored) } \ No newline at end of file diff --git a/model/dotfile_ssh.go b/model/dotfile_ssh.go index cff6da9..d5b4758 100644 --- a/model/dotfile_ssh.go +++ b/model/dotfile_ssh.go @@ -22,5 +22,6 @@ func (s *SshApp) GetConfigPaths() []string { } func (s *SshApp) CollectDotfiles(ctx context.Context) ([]DotfileItem, error) { - return s.CollectFromPaths(ctx, s.Name(), s.GetConfigPaths()) + skipIgnored := true + return s.CollectFromPaths(ctx, s.Name(), s.GetConfigPaths(), &skipIgnored) } diff --git a/model/dotfile_starship.go b/model/dotfile_starship.go index 0e96e1b..1438782 100644 --- a/model/dotfile_starship.go +++ b/model/dotfile_starship.go @@ -22,5 +22,6 @@ func (s *StarshipApp) GetConfigPaths() []string { } func (s *StarshipApp) CollectDotfiles(ctx context.Context) ([]DotfileItem, error) { - return s.CollectFromPaths(ctx, s.Name(), s.GetConfigPaths()) + skipIgnored := true + return s.CollectFromPaths(ctx, s.Name(), s.GetConfigPaths(), &skipIgnored) } diff --git a/model/dotfile_zsh.go b/model/dotfile_zsh.go index 75e4789..9c72114 100644 --- a/model/dotfile_zsh.go +++ b/model/dotfile_zsh.go @@ -25,5 +25,6 @@ func (z *ZshApp) GetConfigPaths() []string { } func (z *ZshApp) CollectDotfiles(ctx context.Context) ([]DotfileItem, error) { - return z.CollectFromPaths(ctx, z.Name(), z.GetConfigPaths()) + skipIgnored := true + return z.CollectFromPaths(ctx, z.Name(), z.GetConfigPaths(), &skipIgnored) } \ No newline at end of file