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


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.
Subscribe to my newsletter
Read articles from Tim Hilton directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
