One tool enables another enables new features blog home

Posted Sunday, 24-Apr-2022 by Ingo Karkat

Just three weeks ago, I had created a git lgtagged extension as part of an investigation into a compatibility bug in AWK. Now, as I was working on update alerting of third-party Git repositories in the shared parts of my home directory, I realized that I could quickly build a notification for tagged checkouts with that.

a bit of background

I share and synchronize configuration and common tool parts of my home directory. For my own repositories, I already had a git wips (works-in-progress) command that would alert me about uncommitted changes, commits that had not yet been pushed to the GitHub clone, or active feature branches. My work involves a lot of switching between projects and side quests (as previous articles have tried to show), so in the past it happened a lot that I forgot about an active project because I got sidetracked. The reporting of unfinished work in various forms during the synchronization that I perform regularly (up to several times during the day) has helped tremendously with that.

But I did not have a good mechanism for open source projects that I have checked out from other authors on GitHub. Updates of those only happened rarely (e.g. when checking to see if there were any fixes after encountering a problem with it or sending an enhancement). For all the merits of never touch a running system, I felt that more frequent updates would be beneficial — I get notified about system updates (from the package manager and other ecosystems like Python and Node.js) on all of my systems (and those of my family members), and install them as soon as possible.

branches

If my working copy is simply following a branch in upstream (typically master or stable), a notification is easy to obtain:

if git existsremote upstream && git remotebr upstream >/dev/null 2>&1; then
    git uinlgn
fi

The condition ensures that there's a remote called upstream (I always check out Git repositories that don't belong to me as upstream, so that I can be sure that a remote named origin (the default name) belongs to me; this is facilitated by my git uclone alias.) Furthermore, there needs to be a corresponding upstream branch. Finally, git uinlgn does logging in the one-line format (lg) without an additional commit (n) of commits that already exist in upstream (u) but not yet in the local branch (in).

The actual code adds some usability embellishments (also through home-grown tools) by truncating an overly long list of commits and adding an introductory comment. One variable is concerned with color output, and as the work-in-progress checks are run sequentially and should stop if one kind of unfinished work has been found, it exits in case of found commits:

if git existsremote upstream && git remotebr upstream >/dev/null 2>&1 && \
    git uinlgn "${colorArg[@]}" | \
        headtail --separator $'\t\t[... use "git uinl[o]g" for the full list ...]' | \
        outputAndPrintf 'Behind upstream:\n'; then
            exit 0
fi

tags

That solution works only when directly following an upstream branch. But some projects do version tagging on the main development branch; and I then prefer to follow the latest stable tag instead of using some development snapshot. (If you closely follow a project, the snapshots give you all the latest features (and the opportunity to even influence their ongoing development), but when you rarely update, there's a real risk you're getting stuck with a buggy intermediate version.)

The above code already bails out on the git remotebr upstream when a tag is checked out (a so-called detached head checkout). So, what would be needed to do the equivalent notification with tags?

We need to locate any tagged commits that lie between the heads of upstream branches and the currently checked-out tag. We might get away with just looking at the default upstream branch, but with the recent renamings of master to main as part of the current wokeness wave, that already is a can of worms. And some projects may do the tagging on a separate release branch. Fortunately, I have a git brrefdo extension that (with a small bit of enhancements — first side quest!) can iterate all upstream branches. We just need to ensure to eventually de-duplicate the resulting tag list, as multiple branches may contain the tagged commit.

I had previously implemented the logging of just the tagged commits — actually, git-lgtagged was just a spin-off abstraction for the git-lgandtagged I had needed; spending the few minutes to extract a command-line flag and separate alias is already paying off handsomely here! So, I let git-brrefdo iterate over all upstream branches (this alternate source is configured via the GIT_BRREFDO_BRANCH_COMMAND environment variable; each branch name is injected into the following command via the {} placeholder). This results in the following code (with boilerplate stuff deemphasized):

git existsremote upstream && \
    tagName="$(git istagged --print)" && \
    if GIT_BRREFDO_BRANCH_COMMAND='git ubr' git-brrefdo --no-header --no-pager --quiet --yes -- \
        git lgtagged --color=always "${tagName}..{}" | \
            uniqueStable | \
            outputAndPrintf 'Behind these tags in upstream:\n'; then
         exit 0
fi

The de-duplication is done by my uniqueStable, a small AWK script that maintains the original ordering (which improves usability as the tag order is kept). Had we just used sort -u, we'd have to rely on the natural sorting of the tag name convention (and deal with the fact that it's not just tag name, but entire log lines that are output, with a log of output in front of the tag to ignore).

Unfortunately, that doesn't yet solve the problem completely, because the dotted range notation in $tagName..$upstreamBranch does not ensure that the tag actually occurs in the sequence of commits; it only excludes any commits from the tag onwards down the history. So we need an explicit condition that ensures the tag is contained in the branch's history. A StackOverflow answer provides me the git tag --merged command that can do that; which I quickly encapsulate in a new git containstag extension (side quest 2).

git existsremote upstream && \
    tagName="$(git istagged --print)" && printf -v quotedTagName %q "$tagName" && \
    if GIT_BRREFDO_BRANCH_COMMAND='git ubr' git-brrefdo --no-header --no-pager --quiet --yes --command \
    "git containstag $quotedTagName {} && git lgtagged --color=always ${quotedTagName}..{}" | \
            uniqueStable | \
            outputAndPrintf 'Behind these tags in upstream:\n'; then
    exit 0
fi

I'm not fully happy with the command list that is passed to git-brrefdo, as that necessitates the use of --command and quoting of tagName. The quick payoff from the last extraction encourages me to take the time for a dedicated new git loguntiltag extension that in turn enables the git lgtaggeduntiltag specialization I need here:

git existsremote upstream && \
    tagName="$(git istagged --print)" && \
    if GIT_BRREFDO_BRANCH_COMMAND='git ubr' git-brrefdo --no-header --no-pager --quiet --yes -- \
        git lgtaggeduntiltag "$tagName" --color=always {} | \
            uniqueStable | \
            outputAndPrintf 'Behind these tags in upstream:\n'; then
    exit 0
fi

And that's the whole new behindtags check that alerts me when new tagged commits have been fetched from upstream while my working copy is still on a previous tag.

example

Here's a check run on the NERDcommenter Vim plugin; I had checked out the outdated version 2.5.1 and missed 4 years of updates. My git wips command presents the tags for one patch and one minor release, and I can easily update by checking out the latest tag:

$ git lg1
* fd61bc7 (Caleb Maclennan, 4 years, 6 months ago)  (HEAD, tag: 2.5.1) Udate version and date strings
$ git wips
Behind these tags in upstream:
9a32fd2 (fcying, 3 years, 10 months ago)  (tag: 2.5.2) Add NERDToggleCheckAllLines option to NERDCommenterToggle (#334)
eddd535 (Caleb Maclennan, 5 months ago)  (tag: 2.6.0) Release 2.6.0 (for those using releases, tracking upstream master is recommended)
$ git co 2.6.0
Previous HEAD position was fd61bc7 Udate version and date strings
HEAD is now at eddd535 Release 2.6.0 (for those using releases, tracking upstream master is recommended)
$ git wips

As the check run will run automatically once per day during the synchronization of my shared data, I hopefully will never trail behind pending updates any more!

Ingo Karkat, 24-Apr-2022

blog comments powered by Disqus