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 😕. 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