Skip to content

Add include directive support for dotfile management#253

Merged
AnnatarHe merged 2 commits intomainfrom
claude/refactor-dotfiles-management-FsCxC
Mar 11, 2026
Merged

Add include directive support for dotfile management#253
AnnatarHe merged 2 commits intomainfrom
claude/refactor-dotfiles-management-FsCxC

Conversation

@AnnatarHe
Copy link
Copy Markdown
Contributor

@AnnatarHe AnnatarHe commented Mar 11, 2026

Summary

This PR introduces include directive support to enable separation of synced and local configuration in dotfiles. When pushing/pulling dotfiles, the original config file gets an include line added that sources a .shelltime version, allowing users to maintain local customizations alongside synced settings.

Key Changes

  • New IncludeDirective type (dotfile_include.go): Defines how config files include their shelltime-managed versions with fields for original path, shelltime path, include line syntax, and check string for detection.

  • Core include management functions:

    • ensureIncludeSetup(): Handles initial setup by copying content to .shelltime file and adding include line to original
    • ensureIncludeLineInFile(): Ensures include line exists in original config during pull operations
    • removeIncludeLines(): Extracts synced content by removing include directives from original files
    • CollectWithIncludeSupport(): Collects dotfiles from .shelltime files instead of originals when directives exist
    • SaveWithIncludeSupport(): Saves to .shelltime files and ensures include lines in originals
  • App-specific include directives: Added GetIncludeDirectives() method to all supported apps:

    • Bash: .bashrc, .bash_profile, .bash_aliases, .bash_logout
    • Zsh: .zshrc, .zshenv, .zprofile
    • Git: ~/.gitconfig and ~/.config/git/config with multi-line [include] syntax
    • Fish: ~/.config/fish/config.fish
    • SSH: ~/.ssh/config with Include directive
    • Nvim: ~/.vimrc
  • Updated collection and save flows: Modified CollectDotfiles() and Save() methods in bash, zsh, git, fish, ssh, and nvim apps to use include-aware variants.

  • Interface update: Added GetIncludeDirectives() to DotfileApp interface; apps without include support return nil.

  • Comprehensive test coverage (dotfile_include_test.go): 747 lines of tests covering:

    • Include line removal and detection
    • First-time setup scenarios
    • Partial setup recovery (missing include line or .shelltime file)
    • Multi-line include formats (git config)
    • Collection and save operations with mixed includable/non-includable paths
    • End-to-end push/pull workflows
    • Dry-run mode behavior

Implementation Details

  • Include lines are always placed at the top of the original file to ensure they execute before local customizations
  • The implementation gracefully handles missing files and partial setups
  • Dry-run mode prevents modifications to original files while still allowing .shelltime file writes
  • Multi-line include directives (like git's [include] section) are properly handled with line-by-line matching
  • Fallback mechanism removes include lines by check string if exact line matching fails

https://claude.ai/code/session_01WKUeEgn1nMULetY8i2WwDR


Open with Devin

Instead of directly syncing config files, configs that support include
directives now use a two-file approach:
- The original file (e.g., ~/.gitconfig) gets an include line added at top
- The actual synced content lives in a .shelltime suffixed file
  (e.g., ~/.gitconfig.shelltime)

On push: ensures include line exists in original, copies content to
.shelltime file, and uploads the .shelltime version to server.

On pull: ensures include line exists in original, downloads content
from server to the .shelltime file.

Supported include syntaxes by app:
- git: [include] path = ~/.gitconfig.shelltime
- zsh/bash: [[ -f ~/.zshrc.shelltime ]] && source ~/.zshrc.shelltime
- fish: test -f <path>.shelltime; and source <path>.shelltime
- ssh: Include ~/.ssh/config.shelltime
- nvim: if filereadable(expand('~/.vimrc.shelltime')) | source ... | endif

Apps without include support (ghostty, claude, starship, npm, kitty,
kubernetes) keep the existing direct push/pull behavior.
Directory-based configs also keep existing behavior.

https://claude.ai/code/session_01WKUeEgn1nMULetY8i2WwDR
@chatgpt-codex-connector
Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances dotfile management by introducing include directive support. This feature allows users to maintain local customizations in their dotfiles while keeping the core, synced configurations separate in .shelltime files. The change streamlines the process of pushing and pulling dotfiles by intelligently managing include lines and ensuring proper content separation, ultimately improving flexibility and reducing conflicts for users with mixed local and synced settings.

Highlights

  • Include Directive Support: Introduced a new IncludeDirective type and core functions to manage separation of synced and local configuration in dotfiles, allowing original config files to source .shelltime versions.
  • Core Include Management Functions: Added ensureIncludeSetup(), ensureIncludeLineInFile(), removeIncludeLines(), CollectWithIncludeSupport(), and SaveWithIncludeSupport() to handle the lifecycle of include directives and .shelltime files.
  • App-Specific Include Directives: Implemented GetIncludeDirectives() for Bash, Zsh, Git, Fish, SSH, and Nvim applications, providing specific include line syntaxes for their respective configuration files.
  • Updated Collection and Save Flows: Modified CollectDotfiles() and Save() methods across various applications to leverage the new include-aware collection and saving mechanisms.
  • Interface Update: Extended the DotfileApp interface with GetIncludeDirectives() to support the new functionality, with a default nil return for apps without include support.
  • Comprehensive Test Coverage: Added extensive tests for include line management, setup scenarios, multi-line formats, collection/save operations, and end-to-end workflows, including dry-run mode.
Changelog
  • model/dotfile_apps.go
    • Added GetIncludeDirectives() method to the DotfileApp interface.
    • Implemented a default GetIncludeDirectives() method in BaseApp that returns nil.
  • model/dotfile_bash.go
    • Implemented GetIncludeDirectives() to define include directives for Bash configuration files (.bashrc, .bash_profile, .bash_aliases, .bash_logout).
    • Updated CollectDotfiles() to use CollectWithIncludeSupport() for include-aware collection.
    • Added Save() method to use SaveWithIncludeSupport() for include-aware saving.
  • model/dotfile_fish.go
    • Implemented GetIncludeDirectives() to define an include directive for config.fish.
    • Updated CollectDotfiles() to use CollectWithIncludeSupport() for include-aware collection.
    • Added Save() method to use SaveWithIncludeSupport() for include-aware saving.
  • model/dotfile_git.go
    • Implemented GetIncludeDirectives() to define include directives for Git configuration files (.gitconfig, ~/.config/git/config).
    • Updated CollectDotfiles() to use CollectWithIncludeSupport() for include-aware collection.
    • Added Save() method to use SaveWithIncludeSupport() for include-aware saving.
  • model/dotfile_include.go
    • Added new file dotfile_include.go.
    • Defined the IncludeDirective struct to specify how config files include their shelltime-managed versions.
    • Implemented ensureIncludeSetup() to handle initial setup, content copying, and include line addition.
    • Implemented ensureIncludeLineInFile() to ensure include lines are present in original config files during pull operations.
    • Implemented CollectWithIncludeSupport() to collect dotfiles from .shelltime files when directives are present.
    • Implemented SaveWithIncludeSupport() to save to .shelltime files and ensure include lines in original files.
    • Added utility functions writeFileWithDir() and removeIncludeLines() for file manipulation.
  • model/dotfile_include_test.go
    • Added new file dotfile_include_test.go.
    • Included comprehensive unit tests for removeIncludeLines(), ensureIncludeSetup(), ensureIncludeLineInFile(), CollectWithIncludeSupport(), and SaveWithIncludeSupport().
    • Added tests for GetIncludeDirectives() across various applications.
    • Provided an end-to-end test for push and pull workflows with include directives.
  • model/dotfile_nvim.go
    • Implemented GetIncludeDirectives() to define an include directive for ~/.vimrc.
    • Updated CollectDotfiles() to use CollectWithIncludeSupport() for include-aware collection.
    • Added Save() method to use SaveWithIncludeSupport() for include-aware saving.
  • model/dotfile_ssh.go
    • Implemented GetIncludeDirectives() to define an include directive for ~/.ssh/config.
    • Updated CollectDotfiles() to use CollectWithIncludeSupport() for include-aware collection.
    • Added Save() method to use SaveWithIncludeSupport() for include-aware saving.
  • model/dotfile_zsh.go
    • Implemented GetIncludeDirectives() to define include directives for Zsh configuration files (.zshrc, .zshenv, .zprofile).
    • Updated CollectDotfiles() to use CollectWithIncludeSupport() for include-aware collection.
    • Added Save() method to use SaveWithIncludeSupport() for include-aware saving.
Activity
  • The pull request introduces significant new functionality for dotfile management.
  • Comprehensive tests have been added to ensure the reliability of the new include directive feature.
  • The changes span multiple files, modifying core interfaces and application-specific logic.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

The pull request introduces robust include directive support for dotfile management, allowing for separation of synced and local configurations. The implementation covers various applications and includes comprehensive test coverage for different scenarios. The overall approach is well-structured and addresses a common need in dotfile management. My feedback focuses on a minor consistency improvement in the BaseApp interface implementation.

Note: Security Review did not run due to the size of the PR.

Comment thread model/dotfile_apps.go
Comment on lines +84 to +86
func (b *BaseApp) GetIncludeDirectives() []IncludeDirective {
return nil
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The comment for GetIncludeDirectives states it returns an "empty slice by default", but the implementation returns nil. While nil slices often behave like empty slices, it's generally a good practice in Go to return an actual empty slice ([]IncludeDirective{}) to ensure consistency and avoid potential nil checks by consumers who might expect a non-nil slice.

Suggested change
func (b *BaseApp) GetIncludeDirectives() []IncludeDirective {
return nil
}
func (b *BaseApp) GetIncludeDirectives() []IncludeDirective {
return []IncludeDirective{}
}

@AnnatarHe
Copy link
Copy Markdown
Contributor Author

@claude review it

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Mar 11, 2026

Claude finished @AnnatarHe's task in 1m 29s —— View job


Code Review: Add include directive support for dotfile management

  • Read all changed files
  • Analyze code quality, bugs, and security
  • Run tests and static analysis
  • Post review feedback

Overall this is a well-structured feature with good separation of concerns. The IncludeDirective abstraction is clean and the per-app implementations are consistent. However, there are 2 failing tests and several issues worth addressing.


Bugs (failing tests)

1. End-to-end test fails — SaveWithIncludeSupport merges instead of replacing (model/dotfile_include_test.go:740)

The E2E test push_then_pull_workflow fails because SaveWithIncludeSupport delegates to BaseApp.Save() which uses diff-merge logic (DiffMergeService). When the server returns completely new content for the .shelltime file, the diff-merge appends the new lines rather than replacing the file. The test expects the server content to fully replace the .shelltime file, but gets merged content with both old and new [user] sections:

expected: "[user]\n    name = Updated User\n..."
actual:   "[user]\n    name = Test User\n    email = test@example.com\n    name = Updated User\n..."

This is a real bug — when pulling dotfiles from the server, the .shelltime file should be overwritten with server content, not diff-merged. The diff-merge behavior makes sense for the original config files where users have local customizations, but for .shelltime files (which are fully server-managed), it's incorrect.

Fix this →

2. saves_non-shelltime_file_normally test fails (model/dotfile_include_test.go:545-571)

This test also fails due to the diff-merge producing unexpected output for a simple file write. The diff shows +*.swp and -*.swp appearing as changes instead of a clean write.


Potential Issues

3. File permission not preserved (model/dotfile_include.go:63,69,211)

writeFileWithDir and os.WriteFile always use 0644. For SSH config (~/.ssh/config), the correct permission is typically 0600. SSH may refuse to use a config file with overly permissive permissions. Consider preserving the original file's permissions or using stricter defaults for SSH.

Fix this →

4. Comment vs implementation mismatch (model/dotfile_apps.go:82-86)

The comment says "returns an empty slice" but the implementation returns nil. As Gemini noted, this is a minor inconsistency. Since callers use nil checks in the tests (assert.Nil), I'd suggest either changing the comment to say "returns nil" or returning []IncludeDirective{} — just be consistent.

5. CheckString could false-positive on unrelated content (model/dotfile_include.go:49)

strings.Contains(content, directive.CheckString) with check strings like .bashrc.shelltime could match if a user has a comment mentioning .bashrc.shelltime in their file. This is unlikely but worth noting. A more robust check would look for the actual include line rather than a substring.

6. SSH Include directive placement (model/dotfile_ssh.go:29)

SSH's Include directive is placement-sensitive — it must appear before any Host/Match blocks it should affect. Placing it at the very top is correct for global includes, but the current implementation doesn't verify this. If a user manually moves it, re-running could add a duplicate at the top. The CheckString detection (config.shelltime) prevents this, but just something to keep in mind for edge cases.

7. Race condition on first-time setup (model/dotfile_include.go:57-63)

In ensureIncludeSetup, the .shelltime file is written first, then the original is modified. If the process crashes between these two writes, you'd end up with a .shelltime file but no include line — which is the "hasInclude && !shelltimeExists == false" state. The code handles this gracefully on re-run (the !hasInclude branch at line 66 would fire), so this is fine in practice.


Minor Suggestions

8. The Zsh app defines include directives for .zshrc, .zshenv, .zprofile but GetConfigPaths() also includes ~/.config/zsh (a directory). This directory won't get include treatment, which is correct, but it's worth a comment explaining the intentional asymmetry.

9. Consider adding a removeIncludeLines edge case test for content that is only the include line with nothing else — verifying it returns an empty string.

10. The fish include line uses the test -f ... ; and source ... syntax which is correct for fish shell. Nice attention to shell-specific syntax across all apps.


Summary

The architecture is solid. The main blocker is the diff-merge bug causing 2 test failures — .shelltime files should be overwritten rather than diff-merged during pull. The SSH permissions issue is worth fixing before merge to avoid surprising users. Everything else is minor.


|

@AnnatarHe
Copy link
Copy Markdown
Contributor Author

@claude fix testcases

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Mar 11, 2026

Claude finished @AnnatarHe's task in 2m 18s —— View job


Fixing failing test cases

  • Read and understand the failing tests and related code
  • Identify root cause
  • Fix SaveWithIncludeSupport to write .shelltime files directly
  • Fix BaseApp.Save to write new files directly (avoids packfile delta binary artifacts)
  • Run tests to verify fixes — all model tests pass
  • Commit and push changes

Summary

Fixed 2 failing tests with two changes:

  1. SaveWithIncludeSupport (model/dotfile_include.go): .shelltime files are now written directly via writeFileWithDir instead of being passed through b.Save()'s diff-merge. These files are fully server-managed and should be overwritten, not merged.

  2. BaseApp.Save (model/dotfile_apps.go): New files (that don't exist on disk yet) are now written directly instead of going through the packfile delta diff-merge pipeline, which was producing binary prefix bytes (\x10\x10) when the base content was empty.
    | Branch

…erge

SaveWithIncludeSupport now writes .shelltime files directly (overwrite)
since they are fully server-managed. BaseApp.Save now writes new files
directly to avoid packfile delta binary artifacts with empty base content.

Co-authored-by: Le He <AnnatarHe@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@codecov
Copy link
Copy Markdown

codecov Bot commented Mar 11, 2026

Codecov Report

❌ Patch coverage is 75.51867% with 59 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
model/dotfile_include.go 76.11% 18 Missing and 14 partials ⚠️
model/dotfile_apps.go 47.05% 5 Missing and 4 partials ⚠️
model/dotfile_bash.go 88.00% 3 Missing ⚠️
model/dotfile_fish.go 70.00% 3 Missing ⚠️
model/dotfile_git.go 80.00% 3 Missing ⚠️
model/dotfile_nvim.go 70.00% 3 Missing ⚠️
model/dotfile_ssh.go 70.00% 3 Missing ⚠️
model/dotfile_zsh.go 85.00% 3 Missing ⚠️
Flag Coverage Δ
unittests 39.45% <75.51%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
model/dotfile_bash.go 64.86% <88.00%> (+64.86%) ⬆️
model/dotfile_fish.go 42.85% <70.00%> (+42.85%) ⬆️
model/dotfile_git.go 51.85% <80.00%> (+51.85%) ⬆️
model/dotfile_nvim.go 45.00% <70.00%> (+45.00%) ⬆️
model/dotfile_ssh.go 47.36% <70.00%> (+47.36%) ⬆️
model/dotfile_zsh.go 59.37% <85.00%> (+59.37%) ⬆️
model/dotfile_apps.go 57.48% <47.05%> (-0.94%) ⬇️
model/dotfile_include.go 76.11% <76.11%> (ø)

... and 6 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Copy Markdown

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 2 potential issues.

View 5 additional findings in Devin Review.

Open in Devin Review

Comment thread model/dotfile_include.go
Comment on lines +228 to +234
func writeFileWithDir(path, content string) error {
dir := filepath.Dir(path)
if err := os.MkdirAll(dir, 0755); err != nil {
return err
}
return os.WriteFile(path, []byte(content), 0644)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 SSH ~/.ssh directory created with 0755 instead of required 0700 permissions

The writeFileWithDir function creates parent directories with 0755 permissions (model/dotfile_include.go:230). When the SSH app's include support triggers creation of ~/.ssh/config.shelltime, if ~/.ssh doesn't already exist, os.MkdirAll(dir, 0755) creates it with world-readable permissions. OpenSSH requires ~/.ssh to be 0700 and will refuse to use keys/config from an insecure directory.

This is reachable during pull: if a user pulls SSH config to a machine without ~/.ssh/, ensureIncludeLineInFile at model/dotfile_include.go:112 calls writeFileWithDir to create ~/.ssh/config, which creates ~/.ssh with 0755. Similarly, SaveWithIncludeSupport at model/dotfile_include.go:210 calls writeFileWithDir to create ~/.ssh/config.shelltime. The resulting directory permissions silently break SSH.

Prompt for agents
The writeFileWithDir function in model/dotfile_include.go:228-234 hardcodes directory permissions to 0755. This breaks SSH, which requires ~/.ssh to be 0700. Similarly, the ensureIncludeSetup function at lines 59, 63, 69 and ensureIncludeLineInFile at line 112 write files with 0644 permissions, but SSH config files are conventionally 0600.

To fix this properly, you should either:
1. Make writeFileWithDir accept a directory permission parameter and a file permission parameter, allowing SSH-specific code to pass 0700 and 0600 respectively.
2. Or add SSH-specific overrides in the SshApp that handle the special permission requirements.

All call sites that may write to ~/.ssh/ paths need to use 0700 for directory creation and 0600 for file creation. This includes:
- model/dotfile_include.go:59 (writeFileWithDir for .shelltime file)
- model/dotfile_include.go:63 (os.WriteFile for original file)
- model/dotfile_include.go:69 (os.WriteFile for original file)
- model/dotfile_include.go:112 (writeFileWithDir for original file)
- model/dotfile_include.go:210 (writeFileWithDir for .shelltime file)
- model/dotfile_include.go:228-234 (the generic writeFileWithDir helper)
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread model/dotfile_include.go
Comment on lines +262 to +268
// Fallback: remove any line containing the check string
var filtered []string
for _, line := range lines {
if !strings.Contains(line, directive.CheckString) {
filtered = append(filtered, line)
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 removeIncludeLines fallback leaves orphaned [include] section header for git's multi-line directive

For git's multi-line include directive (IncludeLine: "[include]\n path = ~/.gitconfig.shelltime", CheckString: ".gitconfig.shelltime"), the fallback path in removeIncludeLines (lines 262-268) only removes lines containing the CheckString. This removes the path = ~/.gitconfig.shelltime line but leaves the [include] section header orphaned in the file, since [include] doesn't contain .gitconfig.shelltime.

This is triggered when the include directive is not at the top of the file (e.g., user moved it). The result is a gitconfig with an empty [include] section, which is harmless for git but leaves config file debris.

Prompt for agents
In model/dotfile_include.go, the removeIncludeLines function's fallback logic at lines 262-268 only removes lines containing the CheckString. For multi-line include directives like git's [include] section, this leaves the section header orphaned.

To fix this, the fallback should also try to find and remove the full multi-line include block anywhere in the file (not just at the top). One approach: scan through the file looking for a sequence of lines that match the includeLines (similar to the top-of-file check at lines 244-259 but searching at any position), and remove those lines plus any trailing blank line.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@AnnatarHe AnnatarHe merged commit 57e2f03 into main Mar 11, 2026
5 of 6 checks passed
@AnnatarHe AnnatarHe deleted the claude/refactor-dotfiles-management-FsCxC branch March 11, 2026 16:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants