Managing local changes which should not be committed using only git stashes

Tim HiltonTim Hilton
5 min read

Problem

The application I’m currently working on requires some local config changes in order to run. The values in source control aren’t suitable for local running, but there is no infrastructure configured for injecting local secrets from outside source control (e.g. a secrets.json file). It’s out of scope for me to introduce this infrastructure, so I have needed to find a way of working with local changes that I want to keep out of source control.

In the past I’ve used JetBrains Rider which has built in functionality for this. Changes can be split across multiple changelists, and commits can easily be composed from a single changelist. Changelists can also be collapsed in the git window, so they’re easy to ignore. However, Visual Studio has no equivalent functionality. Using only built-in git functionality, how can I manage local changes that are necessary every time I want to run the app but avoid committing them by accident?

Git stashes

I’ve worked out a process for this using git stashes. Stashing changes in git removes them from your working directory, but preserves them within git so they are easy to retrieve in the future. At the point of retrieval, you can either apply or pop the stash. Both load the changes into the working directory, but apply keeps the stash and pop removes it (assuming there are no errors or merge conflicts).

I have utilised this functionality to store my local changes in a stash so that I can easily commit everything else, but also keep track of my local changes and build them up over time.

Git aliases

I’ve created some git aliases which help with this, leaning heavily on GitHub Copilot to create them, then tweaking them as necessary. This is a snippet from the [alias] section of my .gitconfig file. To use these snippets, just copy and paste this into your .gitconfig file (and remove the [alias] if you already have an [alias] section).

[alias]
    # Stash current changes, suffixing the provided message with the word 'config' 
    # and the current date & time. (This uses powershell and may fail on a non-windows OS.)
    # Usage 👉 git stash-config "MyApp working"
    stash-config = "!f() { git stash -u -m \"$1 config $(date +%Y-%m-%d' '%H:%M)\"; }; f"

    # Apply the latest stash where the name matches a search term 
    # (does not need to be the full name, is case insensitive).
    # Usage 👉 git apply-stash "partial-name"
    apply-stash = "!f() { \
        stash=$(git stash list | grep -i \"$1\" | head -n 1); \
        if [ -n \"$stash\" ]; then \
            stash_id=$(echo \"$stash\" | awk -F: '{print $1}'); \
            echo \"Applying $stash\"; \
            git stash apply \"$stash_id\"; \
        else \
            echo \"No stash found matching: $1\"; \
        fi; \
    }; f"

    # Stages all changes (including new, modified, deleted, and renamed files) and applies the latest stash.
    # Intended usage is when stopping a project where configuration changes are stored in a stash.
    # Before starting the application, we want to apply the latest stash containing the word 'config'
    # but keep them separate from the changes under test (which is why they are staged).
    start = "!git add -A && git apply-stash config"

    # Reverts all unstaged changes to tracked files, and delete all untracked files and directories.
    # Intended usage is when stopping a project where configuration changes are stored in a stash.
    # When we stop the application, we want to revert the unstaged changes 
    # (which we are expecting to just be the configuration changes) so we have a clean working directory.
    stop = "!git checkout -- . && git clean -fd"

Workflow

Starting with a blank slate, I make my local config changes which I want to keep out of source control. I then use git stash-config to create a stash with those changes. At this point I have a blank slate again, and am ready to do development work.

When I’m ready to test my changes, I run git start. This stages the changes I want to keep, and applies the most recent config stash, allowing my application to run. When I’ve finished testing, I run git stop to get rid of the config changes again. This leaves me with my real changes staged, no unstaged changes, and my config changes still in a stash for future use. I can make further changes (then use git start and git stop again for testing) until I’m ready to commit, at which point I already have all my changes staged and ready to commit.

I can now easily make changes to the config and keep those changes out of source control. I always start with a blank slate when I need to make these config changes. I run git start to get all my existing config into my working directory, then make whatever additional changes I want. I then run git stash-config again to create a new stash. (The old stash is still preserved, so if I need to go back to it at some point I can easily do that manually.)

This workflow does require some discipline. If I forget to run git stop before making code changes, my working directory will then be a mix of ‘real’ changes and config changes from the stash. Similarly, if I make changes to the code while it’s running, it leads to the same problem of a working directory which is mixed. In both cases the solution is the same - to manually review the changes in the working directory, stage those which should be kept, then run git stop (or git stash-config if there are new config changes which should be kept).

Conclusion

This workflow is more complex than I’d like. I much prefer using changesets in Rider, but in their absence this has been working well for me. One thing I particularly like is that I can still create other stashes, and as long as I don’t use the word ‘config’ in their descriptions they will be ignored by this process.

0
Subscribe to my newsletter

Read articles from Tim Hilton directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Tim Hilton
Tim Hilton