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 )

References