Stay ahead of git with this sharp script

I work on quite a few git repositories at once, and I don’t always commit changes in one before making changes to another. Or if I do, I don’t always push the changes up straight away. That might not be best practice in software development, but hey, it’s what I do. The issue for me is remembering what state each repo is in. Here’s the script I use to tell me.

The script iterates through the contents of my git directory — stored in the shell variable $GIT — and, for directories containing a .git subdirectory, it uses the output of the git status command to check for uncommitted or unmerged changes.

The latter are revealed by the repo being ahead of the branch on origin. The script uses grep to look for the string is ahead in the output.

Uncommitted changes are revealed with the --porcelain flag, which has a non-empty output in this case, but otherwise returns nothing.

Since you can’t have both states at once, the script sets the repo state accordingly, and its value is then stored in an array which matches index for index an array of the names of repos with changes.

if cd "$GIT"; then
    # Process the files
    for repo in *; do
        if [[ -d "$repo" && -d "$repo/.git" ]]; then
            if cd "$repo"; then
                local state=""
                local unmerged=$(git status)
                unmerged=$(grep 'is ahead' < <((echo -e "$unmerged")))
                if [[ -n "$unmerged" ]]; then

                local uncommitted=$(git status --porcelain --ignore-submodules)
                if [[ -n "$uncommitted" ]]; then

                if [[ -n "$state" ]]; then
                    if [[ ${#repo} -gt $max ]] max=${#repo}

                cd ..

Why store all this in an array? That’s for the last part, which outputs the list of changed repos in tidy formatting laid out according to the repo with the longest name.

Every time the script finds a repo with changes, it checks the length of the repo name against the numeric value stored in the variable max:

if [[ ${#repo} -gt $max ]] max=${#repo}

If the repo’s name is longer than max, max is updated with that length.

In the output section, the script uses printf’s %*s formatter to right-align the text at the column indicated by the value of max:

printf '%*s has %s changes\n' $max ${repos[i]} ${states[i]}

The other two arguments are, respectively, the name of a repo and its state. A for loop is used to iterate through the changed repos list:

if [[ ${#repos} -eq 0 ]]; then
    echo "All repos up to date"
    echo "Repos with changes:"
    for (( i = 1 ; i <= ${#repos[@]} ; i++ )); do
        printf '%*s has %s changes\n' $max ${repos[i]} ${states[i]}

Before this, the script checks the length the repos array — ${#repos} — to make sure there are changed repos to output.

You can get the full script from my Scripts repo. Which is up to date.

2 thoughts on “Stay ahead of git with this sharp script

  1. jeremy pereira

    Hi. This is not a comment on this post but on which seems to have comments disabled. The section on signal handling is, unfortunately, bad advice.

    You can’t write a safe signal handler in pure Swift because a signal handler should not contain any code that is not re-entrant. That includes any standard library code that is not documented as “async signal safe”. Buffered IO and memory allocation are the biggest problems. Just constructing a Swift string in a signal handler could corrupt the heap if the signal was delivered at the wrong moment.

    There are two ways that I know of to handle signals in Swift code. The official way is to use DispatchSource.makeSignalSource() but you need a run loop for that to work. The other way is to create a minimal signal handler in C that just sets a flag and poll the flag regularly. I wrote a proof of concept here:

    1. smittytone Post author

      Thanks very much for this, Jeremy. That’s a really interesting point and I enjoyed your blog post, which I’ve linked to in a new post of my own. I need to reference that from the post you were referring to here (for which I’ve closed off comments because I just get too much spam otherwise).



Comments are closed.