简体   繁体   中英

How to Git checkout on repo and submodules simultaneously?

I'm currently working on a repo with its own files, as well as two submodules which have files in them as well. I want to be able to checkout branches on all three of them at once.

During my workflow, I create and move between branches in all three repos simultaneously (so for example if I start working on a new feature, I'll make a new feature branch on the main repo, as well as a new branch with the same name on both submodule's repos). It's important to note this is less of a personal preference and more a convention where I am working.

Whenever I checkout a branch on the main repo (Even if it's using --recurse-submodules), the subrepos are detached heads and not the latest commit on their branch of the same name.

Is this even possible to do? If so, how can one do this?

TL;DR

It takes at least two shell commands, though you can put them all on one line, or make a shell or Git alias for them:

git checkout <branch>
git submodule foreach --recursive 'git checkout <branch>'

You only need the --recursive if some submodule(s) have their own submodule(s) and you want to apply the foreach recursively.

Long

Submodules are supposed to be in detached-HEAD mode. That's how they work: the superproject repository contains, as part of each commit, the hash ID of the correct commit for the submodule.

That said, sometimes, to make a new superproject commit, you'd like to stop using the commit specified by some previous superproject commit, and start using some other commit, in one or more of your submodules. To do that, you must tell a submodule-controlling-Git-command to git checkout some other commit in that submodule.

For instance, suppose we are in the superproject, which I'll call R :

$ cd R
$ git status
... superproject status shows up here ...

Within R , we have dir/subm , which is a submodule. If we run:

$ git rev-parse :dir/subm

(note the leading colon, which is gitrevisions syntax for accessing the index) we get the raw hash ID stored in Git's index (that is, the index for R , as currently checked out in our current working directory, which is our working tree here). That's the commit that the superproject says should be checked out, as a detached HEAD. So if we then run:

$ cd dir/subm
$ git status

we can expect to see HEAD detached at hash , where the hash is the one we just saw above. That's because the superproject Git ran:

(cd dir/subm && git checkout --detach $hash)

with $hash set to that hash ID. But now that we're in dir/subm , we have a regular ordinary Git repository. We can run:

git checkout dev

or whatever we like: we're in the submodule repository S now, not the superproject repository R . Technically we're in the working tree of S . There will be a directory named .git here, or, in modern Git, a file named .git containing the path to the S repository, contained within ../../.git/modules/ somewhere, most likely. But the key is that we're in a Git repository so we can run git checkout , do work, run git add , and then run git commit . That way, we can make an entirely new commit, if we need to.

If we just need to pick some existing commit, well, we can run git log and find it and git checkout the appropriate commit. One way or another, now that we're sitting in S , we pick out or create the commit we want.

Now we just have to pop back up to the working tree for R . Since we got here by doing down twice—into dir/subm/ —we get there by going up twice:

cd ../..

Now that we're back in R , we run:

git add dir/subm

(If you have an ancient Git, be very careful to leave off the trailing slash here!) This updates Git's index for R so that instead of the previous detached HEAD commit hash ID, it now contains the actual, current HEAD hash ID for S . The R Git got this by running:

(cd dir/subm; git rev-parse HEAD)

which printed out the hash ID of the commit we just forced into place in S by working there.

Since Git makes a new commit from whatever is in Git's index, we can now, or some time soon in the future, run git commit here, in R , to make a new commit. This new commit will tell anyone using this repository later to have their R Git command their S Git to git checkout --detach $hash where $hash is the hash we just had our Git stuff into Git's index.

In other words, a future checkout will still use a detached HEAD in S , but now it will use this one.

Now, as to this part:

During my workflow, I create and move between branches in all three repos simultaneously (so for example if I start working on a new feature, I'll make a new feature branch on the main repo, as well as a new branch with the same name on both submodule's repos). It's important to note this is less of a personal preference and more a convention where I am working.

That's all fine. You can (cd dir/subm; git checkout -b feature/xyzzy $hash) to create a new branch named feature/xyzzy and put yourself on that branch, in the submodules. The superproject Git doesn't care about that, it only cares—at some point, when you run git add dir/subm —what HEAD resolves to, hash-ID-wise, in dir/subm at that time . It doesn't matter whether the submodule HEAD is attached or not. The name —if any.—in the submodule is irrelevant to the superproject.

Note that the name in the submodule S is relevant, though, when you go to git push commits from S to some other Git. So this part almost requires an attached HEAD, with a branch name. (It is possible to develop on a detached HEAD, then use git push url-or-remote HEAD:refs/heads/ branch , but that's not a fun way of working. For one thing, it's too easy to accidentally lose track of the right commit hash ID that way. It's much nicer to put S on a branch name to do work in it.) So that's why you might use git checkout -b feature/xyzzy $hash . But: where should $hash come from, here? Well, that part is up to you. Instead of a raw hash ID, you can use HEAD or some existing branch name or some existing remote-tracking name or whatever you like. Git just needs some commit ID to put into the new branch name, if you're creating a new branch name.

If there's an origin/feature/xyzzy name that contains the correct hash ID, you don't even have to fish it out. Remember, S is an ordinary Git repository. So git checkout feature/xyzzy , while in S , means "use the existing name, but if it doesn't exist, use the DWIM mode to look for origin/whatever". The only catch here is that you must run these commands in the S submodule.

Let's me recommend a post-checkout hook for the superproject like this:

#!/bin/sh

# post-checkout hook that switches submodule branches to the same branch as the superproject

prev_HEAD="$1"
new_HEAD="$2"
new_branch="$3"

if [ "$new_branch" = 1 ]; then
    branch_name=`git symbolic-ref -q HEAD`
    if test -n "$branch_name"; then
        git submodule foreach git checkout "$branch_name"
    else
        echo "WARNING: detached HEAD"
        git submodule update
    fi
fi

exit 0

For recursive checkout: git submodule foreach --recursive and git submodule update --recursive .

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