J Fabian Meier
J Fabian Meier

Reputation: 35795

Exchange one file in a branch of a git repository

I have a file config.txt in a given branch of a remote git repository.

Now I have a new version of that file (outside of any git repository, just a generated file) and want to commit it. For that, I need to write a shell script. What would be a good approach for that?

Do I need a local copy of the whole git repository? Or can it be done more directly?

Upvotes: 0

Views: 63

Answers (1)

torek
torek

Reputation: 487993

First, remember that what Git really cares about are commits, and each commit is a full snapshot of all files. Moreover, each commit is made from whatever is in Git's index at the time you run git commit. A git checkout of a given commit fills the index from that commit, while also populating a work-tree with those files (in their ordinary form) so that you can see them.

Thus, if you want to make a new commit in which all files are the same as the previous commit except for config.txt which should be replaced wholesale, you must:

  • fill the index with those files
  • replace the config.txt file in the index with the new contents
  • commit the resulting index

The easy way to do that is to have the repository—or a clone of it—with a work-tree in location W (some directory/folder on your machine), in a "clean" state (so that git status in W will say nothing to commit, working tree clean). Then, in W, do:

git checkout branchname

which fills the index and work-tree of the repository in W. Then you can:

cp <path> config.txt

(where path is the new generated confing), then:

git add config.txt
git commit -m "replace config.txt with new generated config"

If this repository is copied from some other Git repository elsewhere, you can now git push the new commit to that other Git repository, and ask that Git repository to update its branch name to incorporate the new commit (which of course remembers its predecessor as usual).

Remember that this git push may be racing against other users who are also updating that other Git, so it might fail if they updated the other Git's branch. In this case, you should probably mostly start over: update the local clone to pick up their new commit(s), hard-reset the one branch, copy the generated config.txt, add, commit, and push. If that fails, restart yet again, until it succeeds.

(If there is a constraint by which the named branch should not have been updated by anyone else in this time period, you can skip the restart-and-retry loop, and simply report any failure for some human to diagnose.)

Do I need a local copy of the whole git repository?

You need enough of the Git repository to be able to check out the target branch (thus populating index and work-tree), replace the one file, git add, git commit, and git push. How much is "enough"? Well, all you really need is the one current branch tip of that one named branch, so a shallow clone of depth 1 suffices. Hence if you don't have a repository at all, you can start the retry loop with:

git clone --depth 1 --single-branch -b <branch-name> <url> [<local-dir>]

to obtain this shallow clone in whatever directory. Should the push fail and you wish to have a retry loop, you can just git fetch in this now-existing shallow clone to update origin/branch-name, then use git reset --hard origin/branch-name to reset for the next add/commit/push attempt. You should probably then remove this repository as shallow single-branch clones are something of a sand-trap / tar-pit / whatever your favorite term might be for other humans who stumble across them and aren't prepared for all the sharp pointy bits that shallow and single-branch repositories present.

Or can it be done more directly?

Rather than doing all of this on a client, in a clone, and git pushing to the server Git, you could send the newly generated config.txt to the server—to a location outside its clone, which is probably --bare anyway—by some non-Git method (such as scp or rsync) and run something directly on the server. This, of course, assumes you have that kind of access to the server.

If you have a bare repository—which is one with no work-tree—you can still make a new commit, because even a bare repository has an index. You simply need to read the existing branch-tip commit into the index:

git read-tree <branch-name>

then replace that one file in the index using git update-index (with --stdin or --cacheinfo), but to do so you'll need to use git hash-object -w to create the blob object from the desired content. Then, having updated the index, use git write-tree to write a new tree object, then use git commit-tree to build a commit from the resulting tree object, and last, use git update-ref to update the branch name to point to the new commit. If the branch might be updated via git push, you'll still need a retry loop, or some method of locking the branch against git push. Any locking method is up to you to invent, perhaps using hooks.

Upvotes: 1

Related Questions