How to manage multiple Git repositories with a simple bash function

In the past I have already worked with a project that consisted of multiple Git repositories in a common project folder. For tracking each repository’s individual state together, Google’s repo tool was used. I ended up using mostly its powerful repo forall subcommand to execute various bash or git commands over the whole or subset of those projects.

Now, I was faced with the same situation, but without repo already in place. Could I get some of that forall feeling back, but without installing that (relatively) giant tool? It turns out, 9 lines of bash give me most of what I missed:

The following function, added to my ~/.bash_aliases is sufficient:

function git-forall()
{
    for d in */.git; do
        local f
        f="${d%/.git}"
        echo "${f}"
        git -C "${f}" "$@"
        echo ""
    done
}

It enables me to quickly check the status of all repos using git-forall status, refresh all branches to their current origin state using git-forall fetch, or even give me a short tree-view of the latest commits on any branch using git-forall lg --all -10. In the last command, lg is my custom alias for git log, but with custom columns and graph drawing enabled.

The only noteworthy bash feature might be the ${VAR%SUFFIX} syntax which removes the string SUFFIX from the contents of variable VAR if present.

Forall for all

With that function in place, I realised that a more general forall function was in order:

function forall()
{
    for d in */; do
        echo "${d}"
        cd "${d}"
        "$@"
        echo ""
        cd ..
    done
}

Now, I can quickly do something like ls -R but limited to a single level by typing forall ls, which otherwise would need the following, easy to remember find one-liner:

find -mindepth 1 -maxdepth 1 \
  -exec echo "{}" \; -exec ls "{}" \;

Yeah, quite easy to remember and fast to type!

Update 2024

As usual, snippets such as the above slowly evolve over time. Here is my currently quite stable working configuration, mostly adding robustness and highlighting for the titular output of the subdirectory (line echo -e "...${f}") and defaulting to git status if no additional arguments are given.

function git-forall()
{
    for d in */.git; do
        local f
        f="${d%/.git}"
        echo -e "\e[1m\e[94m${f}\e[0m/"
        if [[ $# -eq 0 ]]; then 
            git -C "${f}" status
        else
            git -C "${f}" "$@"
        fi  
        echo ""
    done
}

This snippet, together with some nifty git aliases in my .gitconfig allow me to quickly update a dozen Git repos belonging to a project by just calling git-forall rup (where rup expands to remote update --prune) . You can quickly set such shorthands by calling…

git config --global alias.rup "remote update --prune"

… or by manually editing your ~/.gitconfig.

Going further

Of course, this simple approach let’s lots of things to be desired. If you want more, but do not want to dive much deeper into bash scripting, see what others already have done:

  • gita by nosarthur provides git-forall on steroids with nice output formatting per command in about 2000 lines of Python code.
  • mr, short for “my repos” is an older (but well-maintained), 2700 Perl tool which supports not only git, but svn, bzr, cvs, hg, darcs, fossil and veracity. Impressive! Its behaviour is more driven by a central Makefile-like configuration. And even more helpful, tGhe homepage another dozen related and similar tools.
  • And of course, finally, repo itself should be considered at some point!

Posted

in

, ,

by

Tags: