This post is a bit of a continuation from the previous one on traversing a git tree. But in this case, I’ll talk about an alias I wrote to quickly open all the files that have been modified in a particular branch, which does wonders to help me when context-switching.

The motivation

When I start work on a new ticket1 I will often move to a branch so that all the work for that task is neatly packaged. After a day or two, and with a couple of commits on that branch, I’m ready to hand it off to the testers, and I pick up the next ticket from our queue.

This means, however, that I will often have 3 or 4 branches that are active, meaning that they are branches for tickets that are still not completely “done” (because they’re still being tested, or just because they haven’t been deployed yet). And these have the annoying tendency of coming back from the testers or the business with problems or changes that need to be made.

When that happens, it’s time to switch back to that branch2 to make a fix and push it up. Which brings us to the main problem.

Getting back into the context

When you switch back to an old branch, being able to quickly jump back into the context of what you were doing makes things a lot easier. But the older the branch is, the harder this becomes. And sometimes, tickets come back after considerable time: I recently had to pick up a branch I had last worked on a month or so ago.

One way to do it is to read through your commit messages for that branch, and depending on how good you are at writing those, that might be all you need to do. But my commit messages tend to describe what the commit is doing, and not how it is doing it. So they don’t necessarily give me what I need to implement a fix.

What I want is to see the files that have been changed, and hopefully open them all up in my editor so I can see what went wrong.

A brief spec

So, just to describe what I have in mind, what I want is a git alias that behaves like the following:

  1. If run in a commit that is not tracking another branch, it should show the names of the files that have changes in the working tree.

  2. If run in a commit that is tracking a branch, it should show the names of all the files that are different from those in the tracked branch, including any in the working tree.

  3. If in either of those cases it’s run with a name as an argument, it should behave as if the current branch was tracking the first common ancestor with the revision we’ve given it.

The implementation

This is how a bash function to do this might look:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
files () {
    # Accept a revision as argument
    local REV="$1";

    # If argument is empty, use tracked remote
    if [ -z "$REV" ];
        then REV=$(
            git rev-parse
                --abbrev-ref
                --symbolic-full-name @{u} 2>/dev/null
        );
    fi;

    if [ -z "$REV" ];
        # If still no revision use only HEAD
        then git diff --name-only | sort | uniq;
    else
        # Otherwise compare with the first common ancestor
        {
            git diff --name-only "$REV"...;
            git diff --name-only;
        } | sort | uniq;
    fi;
}
The code, as a plain bash function

There are two parts of this that I think deserve special attention.

The first is that, when using a revision as a point of comparison (the branch that starts in line 17), I end up calling git diff --name-only twice and passing the combined set of lines produced by those calls to the sort | uniq combo (in line 22).

This is because when called with a A...B range, it lists the changes starting with the first common ancestor of both A and B, and ending with B (which in our case defaults to being HEAD). But this does not include the files that are in your working tree and have not yet been committed. So to include them, I use the plain form of git diff.

The other part is actually getting the name of the tracked branch (lines 8-10).

This can be achieved with git rev-parse @{u}, which is short for git rev-parse <current-branch>@{upstream}. The 2>/dev/null is there since this command will fail if the current branch is not set to track anything, and the options are there only to make the output more readable3, so they’re not strictly speaking required.

And that’s it!

The only other addition I made to this is that, since you can already get a list of modified files, you can use xargs to pass them all to your text editor of choice (in my case, Kate) and open them all in one go.

With this extra alias

kate = "!f(){ git files $1 | xargs -r kate; }; f"

my workflow is now

git checkout $branch
git kate master

and I can easily see what has changed in this branch, no matter what. 🚀

Pasting this into your .gitconfig

In order to use it in your .gitconfig file, however, this needs to be escaped and quoted just-so. Here’s how that would look. Note that in this version, which is closer to what I actually use, the bit to get the tracked branch has been put into a separate alias, since I use it in a number of other places.

# Print name of tracked remote, or return 1 if not tracking
tracked = "!f() {                                                  \
    local BRANCH=$(                                                \
        git rev-parse                                              \
            --abbrev-ref                                           \
            --symbolic-full-name @{u} 2>/dev/null                  \
    );                                                             \
    if [ ! -z \"$BRANCH\" ]; then                                  \
        echo $BRANCH;                                              \
    else                                                           \
        return 1;                                                  \
    fi;                                                            \
    }; f"

# List all modified files between HEAD and a given revision,
# or the tracked remote if unspecified.
# If the revision is not an ancestor, compare instead against
# the first common ancestor.
files = "!f() {                                                    \
    local REV=\"$1\";                                              \
    if [ -z \"$REV\" ];                                            \
        then REV=$(git tracked);                                   \
    fi;                                                            \
    if [ -z \"$REV\" ];                                            \
        then git diff --name-only | sort | uniq;                   \
    else                                                           \
        {                                                          \
            git diff --name-only \"$REV\"...;                      \
            git diff --name-only;                                  \
        } | sort | uniq;                                           \
    fi;                                                            \
    }; f"

# Open modified files in kate
kate = "!f(){ git files $1 | xargs -r kate; }; f"

The contents of .gitconfig

  1. Every more-or-less self-contained unit of work is organised at work as a “ticket”. So basically these are stand-ins for a realtively well-defined task or project. They often will not be as well-defined as you’d wish, though. :) 

  2. This in itself had all sorts of tiny annoyances. I might follow this up with another write-up of what I did to sort those out. 

  3. Since I use this in a number of other aliases, I prefer to keep this as a separate alias (I call it git tracked), which explains why I like it to give me output that I can understand.