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:


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, despite the fact that the PR was directly focused on environment variables 😕. To be honest, considering the project was a single-file CLI, adding an Environments section seemed like an obvious and unobtrusive improvement. Nevertheless, this motivated me to solve the problem locally.


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 go into the internal details of the CLI, I want to highlight one important aspect related to testing: the use of interfaces.

For instance, the runAddCommand function 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 os.File (the actual exclude file) for production use, meanwhile, for testing I can pass in 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)
		}

This demonstrates the strength of Go's interfaces, allowing developers to write code that is both flexible and testable.


Conclusion

The exclude CLI is a niche but effective tool that solved my problem. Written in Go, it runs as a single binary across all platforms.

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