简体   繁体   中英

Updating a git repo blindly, possibly to a different branch

As part of deployment automation, I have a local git repository that was created with a command of the form

git clone --single-branch -b $BRANCH $REMOTE $PATH

From time to time it is necessary for the automation to pull further changes from the remote, and this may involve a change of branch. The automation has no state; it knows what the branch is supposed to be, but not what it was. Also, there should never be any local changes in this repository, but mistakes happen.

I am looking for a command, or sequence of commands, that will have the same overall effect as wiping out the repository and re-cloning it, but which will minimize the amount of data re-downloaded in the common case (ie $BRANCH has not changed and git pull would have done a fast-forward merge).

Getting most of the way there is easy: just confirm a few things and then run git fetch etc. As a sort of proto-type shell script:

cd ... # the place you want to test.
       # note that `git-sh-setup` (below) demands
       # that this be the top of the repository.

git rev-parse --verify HEAD >/dev/null || exit 1

(this verifies that it is in fact a git repository)

branch=$(git symbolic-ref --short HEAD) || exit 1

(this tests which branch is currently checked-out, if any; it fails with fatal: ref HEAD is not a symbolic ref if HEAD is detached; refine with -q and a different error if needed)

if ! git rev-parse --verify refs/remotes/$remote/$branch >/dev/null 2>&1; then
    echo "$remote/$branch does not exist" 1>&2
    exit 1
fi

(this makes sure there is a remotes/origin/master or whatever, spelling out refs/remotes/ just to be generally paranoid—feel free to remove refs/remotes/ from these)

ahead=$(git rev-list --count refs/remotes/$remote/$branch..$branch)
if [ $ahead -gt 0 ]; then
    echo "$branch is ahead of $remote/$branch by $ahead commit(s)" 1>&2
    exit 1
fi

(this makes sure there are no commits on $branch locally that are not present in the upstream)

. $(git --exec-path)/git-sh-setup # for require_clean_work_tree
require_clean_work_tree "update from remote"

(this is somewhat optional, it depends on how strict you want to be about requiring that the tree be clean, but the next bits assume "clean")

git fetch $remote +refs/heads/$branch:refs/remotes/$remote/$branch

(this updates $remote/$branch to match what's actually on the remote now, regardless of any fetch = lines stored in the repository)

git reset --hard refs/remotes/$remote/$branch

(this last step moves $branch , which we've confirmed we're on, to point to the same commit as the newly updated $remote/$branch , and also replaces the work-directory contents with those from the newly updated post-fetch branch).

If some of the above steps fail (the "exit 1" bits, which might need modification for your purposes) then you fall back to re-cloning.


If you're willing to let the local repository grow (with more branches), you can ditch much of the above and simply run git fetch with the appropriate refspec, and/or add that refspec to the fetch = lines associated with the remote. (Add a git clean -fdx to remove any files that should not be present, and note that the git reset --hard step will remove 1 any commits that should not be present.)

In particular, what git clone --single-branch does is modify the fetch line for the given remote. For instance:

git clone --single-branch -b foo git://...

clones the repo and checks out branch foo as usual, but instead of leaving the .git/config entries for origin reading:

[remote "origin"]
    url = git://...
    fetch = +refs/heads/*:refs/remotes/origin/*

it replaces the fetch line so that you have instead:

    fetch = +refs/heads/foo:refs/remotes/origin/foo

This means future git fetch operations—remember that git pull is just git fetch followed by either merge or rebase—limit themselves to bringing over objects needed to update your local refs/remotes/origin/foo reflecting refs/heads/foo (branch foo) on the remote, rather than those for refs/remotes/origin/* (reflecting all branches on the remote through the first * ).

If you wish to now update with +refs/heads/bar:refs/remotes/origin/bar (as if you'd cloned that branch), you can simply add or replace that as a (or the) fetch = line under [remote "origin"] .

If branches share a lot of history, this will save a lot of data-transfer over re-cloning, though of course you'll wind up with both foo and bar objects in the local repository. If branches share very little history, there's little cost to starting over (unless you ever switch back of course).


1 Except for traces left in the branch's reflogs.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM