Handling Go modules replacement directive version lag and fork maintenance

stevenacoffman profile image Steve Coffman ・3 min read

At Khan Academy, we have a public fork of a public repo, which we are carrying a patch for in a branch. The latest release tag in the upstream is 1.27.0

In our other repositories, we have a replace directive:

replace github.com/golangci/golangci-lint => github.com/Khan/golangci-lint v1.21.1-0.20200609223024-b6a17227ca75

Even though the date and git commit SHA1 portion were correct, we found the pseudo-version prefix of v1.21.1 continuing to lag the upstream v1.27.0 confusing, and we were wondering what actions we could do in our public fork to be able to get that to update without running afoul of the dreaded "version before v1.27.0 would have negative patch number" or "unknown revision" errors.

It turns out that it was because the branch with our patch was merging upstream changes, rather than rebasing. This meant the last common tag between upstream and our fork's branch was v1.21.1, not v1.27.0. Darn computers always just doing what you asked them, right?

So to fix this we had to rebase against the upstream's master's up to the last release commit (we don't want to use unreleased code). This is a little tricky to get correct, so here's the recipe in glorious bash:

export WORKING_BRANCH=custom-autofix
export REBASED_BRANCH=autofix-custom
git clone git@github.com:Khan/golangci-lint.git
cd golangci-lint
git remote add upstream git@github.com:golangci/golangci-lint.git
git pull upstream master
git fetch --tags upstream
# Push the tags from my local to our fork's master branch (not sure if required)
git push -f --tags origin master
git checkout $WORKING_BRANCH
# Rebase against upstream latest tag
git reset --soft $(git log $(git tag | sort -V | tail -1) -1 --pretty=%H)
git commit -s -S -a
git push origin $REBASED_BRANCH -u
export LATEST_GOLANGCI_COMMIT=$(git log -1 --pretty=%H)
# Ta-da! All done. Now go to our private repo that uses this:
cd ~/khan/privaterepo
go mod edit -replace=github.com/golangci/golangci-lint=github.com/khan/golangci-lint@${LATEST_GOLANGCI_COMMIT}
# This will create/update the replace directive to github.com/Khan/golangci-lint v1.27.2-0.20200610182323-d6b2676ecc9b
# I could also have used the branch name instead of commit SHA1.

WTF was all that about?

Now there's some stuff in that bash-fu to unpack:

git tag | sort -V | tail -1

This will give the latest tag, which also will be upstream's latest tag unless you've tagged a release in your fork with a semantic version that's later (tip: Don't do that).

git log $(git tag | sort -V | tail -1) -1 --pretty=%H

This will give the full commit SHA1 of the latest tag.

Wait, why does is it start with v1.27.2 instead of v1.27.0 or v1.27.1 ?

go mod will take the fork's latest release tag's semver and increment the patch number by one (because your replacement fork is a patch, see?).

The incrementing the semver prefix is the only thing wrong with calculating it by hand in your fork using:

echo "$(git tag | sort -V | tail -1)-$(git --no-pager show\
# output: v1.27.1-20200609183024-b6a17227ca75

To test things out, I made a newer release tag than upstream in our fork, v1.27.1, and go mod chose v1.27.2 which tells me it's not just looking at upstream's tags. It seems less confusing to me to just keep syncing upstream's tags to your fork.

Ok, I don't know if that is useful to anyone else!

Posted on Jun 11 by:


markdown guide