How to handle locally modified configuration files
One of the most common problems when working with git is how to handle configuration files that are modified locally. This post shows how to handle this situation.
Let’s suppose we have a repository with code (file code.txt
) and a JSON file containing configuration data (file config.json
).
The configuration file in the repository looks like this:
{
"param1" : "value1",
"param2" : "value2"
}
However, locally we need to change the value of param2
to alina.value2
:
alina
alina git:( main ) cat config.json
{
"param1" : "value1",
"param2" : "alina.value2"
}
alina git:( main ) █
This means that the file will always appear as locally modified, even if we don’t want to - and we should not - commit the change.
alina
alina git:( main ) git status
On branch main
Your branch is up to date with 'origin/main'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: config.json
no changes added to commit (use "git add" and/or "git commit -a")
alina git:( main ) █
This is a tricky situation because each time you change the code and want to commit it, you need to remember to remove your local changes to the config file.
alina
alina git:( main ) git add code.txt
alina git:( main ) git status
On branch main
Your branch is up to date with 'origin/main'.
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: code.txt
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: config.json
alina git:( main ) █
To discard your change, use git restore
:
alina
alina git:( main ) git restore config.json
alina git:( main ) git status
On branch main
Your branch is up to date with 'origin/main'.
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: code.txt
alina git:( main ) █
After that, Alina can push her changes:
alina
alina git:( main ) git commit -m 'code change'
[main 212d130] code change
1 file changed, 1 insertion(+)
create mode 100644 code.txt
alina git:( main ) git push
Enumerating objects: 4, done.
Counting objects: 100% (4/4), done.
Delta compression using up to 8 threads
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 303 bytes | 303.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
remote: . Processing 1 references
remote: Processed 1 references in total
To ssh://remote.mygit.com/gitpowerup/arenas.git
3bf0ed3..212d130 main -> main
alina git:( main ) █
At this stage, Alina must remember to change her configuration file again and restore it before committing her next change.
This is not convenient at all, and it’s easy to forget to restore the file before committing.
The solution
The git update-index --skip-worktree
command allows you to tell Git to ignore local changes to a file.
alina
alina git:( main ) git update-index --skip-worktree config.json
alina git:( main ) █
By doing that, Git “marks” that file in the index so that it knows that this file should not be considered for commits.
alina
alina git:( main ) git ls-files -v
H README.md
H code.txt
S config.json
alina git:( main ) █
From now on, the file will not appear as modified anymore and Alina can safely push her changes without any extra steps.
Even after a code change, the git status
won’t mention config.json
:
alina
alina git:( main ) git add code.txt
alina git:( main ) git status
On branch main
Your branch is up to date with 'origin/main'.
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: code.txt
alina git:( main ) git commit -m 'code change'
[main 7eb001c] code change
1 file changed, 1 insertion(+)
alina git:( main ) git push
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Delta compression using up to 8 threads
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 305 bytes | 305.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
remote: . Processing 1 references
remote: Processed 1 references in total
To ssh://remote.mygit.com/gitpowerup/arenas.git
212d130..7eb001c main -> main
alina git:( main ) █
However, her local config file is still “modified”:
alina
alina git:( main ) cat config.json
{
"param1" : "value1",
"param2" : "alina.value2"
}
alina git:( main ) █
What if the config file is changed in the remote repository?
Let’s suppose that the config file is changed in the remote repository and is now:
{
"param1" : "value1",
"param2" : "value2",
"param3" : "value3"
}
If Alina tries to push her changes, she will get an error:
alina
alina git:( main ) git add code.txt
alina git:( main ) git commit -m 'yet another code change'
[main a0edddd] yet another code change
1 file changed, 1 insertion(+)
alina git:( main ) git push
To ssh://remote.mygit.com/gitpowerup/arenas.git
! [rejected] main -> main (fetch first)
error: failed to push some refs to 'ssh://remote.mygit.com/gitpowerup/arenas.git'
hint: Updates were rejected because the remote contains work that you do
hint: not have locally. This is usually caused by another repository pushing
hint: to the same ref. You may want to first integrate the remote changes
hint: (e.g., 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
alina git:( main ) █
The error during the git push
tells her that someone changed files on the remote.
The first step is to get the changes from the remote repository:
alina
alina git:( main ) git fetch --all
remote: Enumerating objects: 5, done.
remote: Counting objects: 100% (5/5), done.
remote: Compressing objects: 100% (3/3), done.
remote: Total 3 (delta 1), reused 0 (delta 0), pack-reused 0
Unpacking objects: 100% (3/3), 265 bytes | 88.00 KiB/s, done.
From ssh://remote.mygit.com/gitpowerup/arenas
7eb001c..911dd27 main -> origin/main
alina git:( main ) █
And the git status
confirms that assumption:
alina
alina git:( main ) git status
On branch main
Your branch and 'origin/main' have diverged,
and have 1 and 1 different commits each, respectively.
(use "git pull" to merge the remote branch into yours)
nothing to commit, working tree clean
alina git:( main ) █
Alina decides to rebase her changes on top of the remote changes:
alina
alina git:( main ) git rebase --continueorigin/main
error: Your local changes to the following files would be overwritten by checkout:
config.json
Please commit your changes or stash them before you switch branches.
Aborting
error: could not detach HEAD
alina git:( main ) █
Git complains that the local changes to config.json
would be overwritten by the checkout.
That’s exactly what we want. Alina knows that there were remote changes to the config.json
file.
The solution is to remove the skip-worktree
flag from the file:
alina
alina git:( main ) git update-index --no-skip-worktree config.json
alina git:( main ) █
After that, config.json
will reappear as locally modified:
alina
alina git:( main ) git status
On branch main
Your branch and 'origin/main' have diverged,
and have 1 and 1 different commits each, respectively.
(use "git pull" to merge the remote branch into yours)
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: config.json
no changes added to commit (use "git add" and/or "git commit -a")
alina git:( main ) █
One option for Alina is to restore that file in the working directory:
alina
alina git:( main ) git restore config.json
alina git:( main ) █
And then perform the rebase:
alina
alina git:( main ) git rebase origin/main
Rebasing (1/1)
Successfully rebased and updated refs/heads/main.
alina git:( main ) █
The local config.json
file now has the new param3
:
alina
alina git:( main ) cat config.json
{
"param1" : "value1",
"param2" : "value2",
"param3" : "value3"
}
alina git:( main ) █
Alina can update the parameters she needs to change:
alina
alina git:( main ) cat config.json
{
"param1" : "value1",
"param2" : "alina.value2",
"param3" : "value3"
}
alina git:( main ) █
And then set the skip-worktree
flag again:
alina
alina git:( main ) git update-index --no-skip-worktree config.json
alina git:( main ) █
Alina can then safely push her code changes:
alina
alina git:( main ) git status
On branch main
Your branch is ahead of 'origin/main' by 1 commit.
(use "git push" to publish your local commits)
nothing to commit, working tree clean
alina git:( main ) █