Excluding Files From Local Tracking
Have you ever needed to add a file to a repository without committing it, but modifying the .gitignore file wasn’t an option, and at the same time, changing the global ignore file didn’t make sense? I recently faced this exact dilemma, which led me to create my own solution.
Not long ago I forked a single-file CLI tool with the intention of submitting a couple of PRs but I ran into two issues:
The project itself used black and isort although they weren't listed as dependencies in the pyproject.toml, (a nix flake was used instead).
The .gitignore file was lacking an Environments section and normally I use direnv to load environment variables from a .envrc.
Problem One: Formatting
I didn't have nix installed at the time and my editor is configured for Ruff, so I settled on using pre-commit with the following config:
repos:
- repo: https://github.com/psf/black-pre-commit-mirror
rev: 25.1.0
hooks:
- id: black
language_version: python3.12
- repo: https://github.com/pycqa/isort
rev: 6.0.1
hooks:
- id: isort
name: isort (python)
That's great, however, I had no intention to submit this file in a PR but globally ignoring it would be inappropriate because I almost always do want to commit pre-commit configs.
Problem Two: Loading the Environment
The PR I submitted involved adding environment variable configuration for the full surface of the CLI, along with it, I added an Environments section to the .gitignore file. However, this second addition was quickly rejected for being unrelated to the PR 😕. Ultimately, this encouraged me to implement a local solution.
Solution
It turns out git supports an exclude file located at .git/info/exclude. In it you can add files/patterns that you wish to ignore for that specific repository.
So I wrote a CLI tool exclude that makes it effortless to add, del, list and reset this file. While I won’t delve into the internals of the CLI, I want to highlight one important aspect related to testing: the use of interfaces.
Take the runAddCommand function, it accepts both an io.Writer and an io.ReadWriter:
func runAddCommand(out io.Writer, f io.ReadWriter, args []string) error
This means I can pass it an os.Stdout (printing to console) and an os.File (the actual exclude file) for production use, meanwhile, for testing I can pass it two bytes.Buffer.
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var out bytes.Buffer
f := bytes.NewBufferString(tt.existing)
err := runAddCommand(&out, f, tt.args)
if err != nil {
t.Fatalf("runAddCommand returned an error: %v", err)
}
Go's powerful interfaces are just one of the reasons I love the language. By using them to enforce contracts we can keep our functions flexible and testable while ensuring type safety.
Conclusion
The exclude CLI may be a niche tool but it works and solved my problem.
There are some other articles that touch on the issue of file exclusions from tracking, I won't link to them but they're readily findable with a quick Google search.
Subscribe to this blog's RSS feed