Skip to content

Instantly share code, notes, and snippets.

@njpearman
Last active February 19, 2020 13:34
Show Gist options
  • Save njpearman/8890fb2d4fc0767712449e61b6bfb399 to your computer and use it in GitHub Desktop.
Save njpearman/8890fb2d4fc0767712449e61b6bfb399 to your computer and use it in GitHub Desktop.
Reattaching tags to a rebased branch in a git repo
#!/bin/bash
# This script applies tags that have become detached from a rebased branch, assuming
# that the original message for each tag has been unchanged in the rebased branch.
# This is necessary because tags get bound to a commit hash and a rebase will alter
# the commit hashes on a branch, "detaching" any tags that have been attached. It is
# of course debatable whether tags _should_ be moved around at all, but I needed to
# do this for a particular repo I have been working on. It has also been a good
# opportunity for me to explore git and some command line tools. It is not meant to
# be a reference script and there will almost certainly be much more effective ways
# of achieving what this script achieves. However, I hope that it is an interesting
# read.
#
# The script will most likely break if the original message no longer exists in the
# rebased branch. Things will also go wrong if a more recent commit has a matching
# commit message.
#
# The script makes assumptions, such as that the message to attach to the tag is
# 'End of unit'.
#
# I had particular difficulty with piping the `git log ..` commands generated by awk.
# I don't understand the process handling for the `git log` command but the output
# from those awk'd commands are put onto one line rather than continuing with the pipe.
# Other `git` commands behaved in the expected way, being piped into the next command
# but not `git log`. This is why I needed to use `sed` to insert linefeeds, `\n`, to
# then move to the next step.
#
# The unparameterised `xargs` is just to remove the whitespace at the start of
# the line created by the formatting of `awk`. This is in fact a lucky hack as
# the `awk` call prepends whitespace as a delimiter; the output from the `git log ...`
# is not "joined" and therefore there is either a leading or trailing empty item
# depending on how `awk` formats the command. I luckily did this with leading
# whitespace first time, which is easy to remove.
#
# It is highly likely that this script can be compacted as I suspect that formatting
# arguments could be used, removing the need for some of the filtering and formatting
# done with `grep` and `sed`.
#
# Stepping through each piped command, this script specifically:
# * gets a sorted list of git commits filtered to be only those that are tagged, and formats
# the log to only output the associated tag, ordered oldest first.
# * removes everything from these lines except the tag name
# * diffs this list with a sorted list of all tags in the repo
# * filters the diff for those lines that are tags that are in the repo but not
# on the current branch
# * removes the diff indicator at the start of each filtered line.
# * uses `git show` to get the commit message associated with each missing tag.
# * filters only the lines that include the summary of 'tag: <tag>:<summary>'
# * formats and executes a `git log ...` command to find commits on the current branch
# that match the original commit message associated with the tag.
# * removes the leading whitespace using `xargs`
# * replaces the whitespace with a linefeed.
# * uses `awk` to delete the unattached tag and add it again to the commit hash with
# a message that matches the originally associated commit, and a date matching the
# commit date. In order to get the correct date, a checkout is done.
git log --pretty=format:'%D' | sort \
| sed -e 's/.*: //' \
| diff - <(git for-each-ref --format '%(tag)' | sort) \
| grep '^>' \
| sed 's/> //' \
| xargs git show --pretty=format:%D:%s \
| grep ^tag: \
| awk -F ':' ' { system("git log -1 --pretty=format:\"" $2 ":%h\" --grep=\"" $3 "\"") } ' \
| xargs \
| sed 's/ /\n/g' \
| awk -F ':' '{system("git checkout " $1); system("git tag -d " $1); system("GIT_COMMITTER_DATE=\"$(git show --format=%aD | head -1)\" git tag -m \"End of unit\" " $1 " " $2)}'
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment