119

I'm adding Releases to my projects on GitHub by adding tags to various commits in the Main branch.

In one of my projects I did not add the tags to the commits in chronological order. (I found obvious commits and tagged them, and then I found less obvious, older commits and tagged them.)

Now GitHub is showing v1.0.1 as current, with v0.7.0 preceding it, and v1.1.2 preceding that.

It appears to use the date on a tag's creation as the Release date instead of the commit that is being tagged. How can I edit my tags so that their dates are the same as the commit they are tagging?

mapping of releases and dates between gitk and GitHub

Phrogz
  • 296,393
  • 112
  • 651
  • 745

5 Answers5

152

WARNING: This will not preserve tag messages for annotated tags.

Summary

For each tag that needs to be changed:

  1. Go back in time to the commit representing the tag
  2. Delete the tag (locally and remotely)
    • This will turn your "Release" on GitHub into a Draft that you can later delete.
  3. Re-add the same-named tag using a magic invocation that sets its date to the date of the commit.
  4. Push the new tags with fixed dates back up to GitHub.
  5. Go to GitHub, delete any now-draft releases, and re-create new releases from the new tags

In code:

# Fixing tag named '1.0.1'
git checkout 1.0.1               # Go to the associated commit
git tag -d 1.0.1                 # Locally delete the tag
git push origin :refs/tags/1.0.1 # Push this deletion up to GitHub

# Create the tag, with a date derived from the current head
GIT_COMMITTER_DATE="$(git show --format=%aD | head -1)" git tag -a 1.0.1 -m"v1.0.1"

git push --tags                  # Send the fixed tags to GitHub

Details

According to How to Tag in Git:

If you forget to tag a release or version bump, you can always tag it retroactively like so:

git checkout SHA1_OF_PAST_COMMIT
git tag -m"Retroactively tagging version 1.5" v1.5

And while that's perfectly usable, it has the effect of putting your tags out of chronological order which can screw with build systems that look for the "latest" tag. But have no fear. Linus thought of everything:

# This moves you to the point in history where the commit exists
git checkout SHA1_OF_PAST_COMMIT

# This command gives you the datetime of the commit you're standing on
git show --format=%aD  | head -1

# And this temporarily sets git tag's clock back to the date you copy/pasted in from above
GIT_COMMITTER_DATE="Thu Nov 11 12:21:57 2010 -0800" git tag -a 0.9.33 -m"Retroactively tagging version 0.9.33"

# Combining the two...
GIT_COMMITTER_DATE="$(git show --format=%aD  | head -1)" git tag -a 0.9.33 -m"Retroactively tagging version 0.9.33"

However, if you have already added the tag, you cannot use the above with git tag -f existingtag or else git will complain when you try to merge:

Rammy:docubot phrogz$ git push --tags
To git@github.com:Phrogz/docubot.git
 ! [rejected]        1.0.1 -> 1.0.1 (already exists)
error: failed to push some refs to 'git@github.com:Phrogz/docubot.git'
hint: Updates were rejected because the tag already exists in the remote.

Instead, you must remove the tag locally:

git tag -d 1.0.1

Push that deletion remotely:

git push origin :refs/tags/1.0.1

On GitHub, reload Releases—the release has now been marked as a "Draft"—and remove the draft.

Now, add the backdated tag based on the instructions above, and finally push the resulting tag to GitHub:

git push --tags

and then go and re-add the GitHub Release information again.

Stevoisiak
  • 23,794
  • 27
  • 122
  • 225
Phrogz
  • 296,393
  • 112
  • 651
  • 745
  • 3
    Here's a bash script that removes and re-adds every tag in a git repo: `git tag -l | while read -r tag; do \`git checkout $tag && git tag -d $tag && git push origin :refs/tags/$tag && GIT_COMMITTER_DATE="$(git show --format=%aD | head -1)" git tag -a $tag -m"$tag"\`; done; git push --tags` – Phrogz Feb 13 '14 at 03:14
  • 2
    using `git tag -af` makes `-d` unneeded and you stay local so you can check that all is fine - then you can `git push --tags -f` – Mr_and_Mrs_D Sep 02 '14 at 20:51
  • Thanks @vmrob, that worked (though I had to enter my GitHub password several times). – Colonel Panic Sep 18 '14 at 22:57
  • 3
    @Mr_and_Mrs_D Good suggestion and a good way to limit this operation to one push. With that in mind, I think the resulting (untested) one-liner would be `git tag -l | while read -r tag ; do COMMIT_HASH=$(git rev-list -1 $tag) && GIT_COMMITTER_DATE="$(git show $COMMIT_HASH --format=%aD | head -1)" git tag -a -f $tag -m"$tag" $COMMIT_HASH ; done && git push --tags --force` – vmrob Sep 19 '14 at 17:22
  • You don't necessarily have to delete the draft release on GitHub; you can reassign it to the new tag after you push it. – ptomato Oct 10 '15 at 22:50
  • This does not look or sound right... [git-tag(1)](http://www.kernel.org/pub/software/scm/git/docs/git-tag.html) is fairly adamant about ***not*** deleting publish tags. See the discussion ***"On Re-tagging"*** in the man pages. It seems like the discussion of ***"On Backdating Tags"*** in the man pages is more appropriate. – jww Jan 05 '16 at 15:17
  • @vmrob, could you please post your comment as an answer? Your updated one-liner worked like a charm for me. – Nikita Bosik Jan 06 '16 at 15:09
  • @NikitaBosik Funny... I actually had that as an answer at some point. I'll spruce it up and undelete it. – vmrob Jan 07 '16 at 20:54
  • 3
    This works in git shell for PowerShell, but you have to set the environment variable differently, and do it on two lines: `$env:GIT_COMMITTER_DATE="Thu Nov 11 12:21:57 2010 -0800"` and `git tag -a 0.9.33 -m"Retroactively tagging version 0.9.33" ` – roncli Aug 18 '16 at 20:47
  • This correctly retroactively changed the git tag for me, but GitHub release is still based on when the release was published, not the tag date. Is this suppose to be the case? – Richard Witherspoon Oct 31 '21 at 16:44
25

Here's a one-liner based on some of the comments in the other answer:

git tag -l | while read -r tag ; do COMMIT_HASH=$(git rev-list -1 $tag) && GIT_COMMITTER_DATE="$(git show $COMMIT_HASH --format=%aD | head -1)" git tag -a -f $tag -m"$tag" $COMMIT_HASH ; done && git push --tags --force

WARNING: this will nuke your upstream tags and will not preserve messages for annotated tags! Be sure that you know what you're doing and DEFINITELY don't do this for a public repository!!!

To break it down...

# Loop over tags
git tag -l | while read -r tag
do

    # get the commit hash of the current tag
    COMMIT_HASH=$(git rev-list -1 $tag)

    # get the commit date of the tag and create a new tag using
    # the tag's name and message. By specifying the environment
    # environment variable GIT_COMMITTER_DATE before this is
    # run, we override the default tag date. Note that if you
    # specify the variable on a different line, it will apply to
    # the current environment. This isn't desired as probably
    # don't want your future tags to also have that past date.
    # Of course, when you close your shell, the variable will no
    # longer persist.
    GIT_COMMITTER_DATE="$(git show $COMMIT_HASH --format=%aD | head -1)" git tag -a -f $tag -m"$tag" $COMMIT_HASH


done

# Force push tags and overwrite ones on the server with the same name
git push --tags --force

Thanks to @Mr_and_Mrs_D for the suggestion to use a single push.

Stevoisiak
  • 23,794
  • 27
  • 122
  • 225
vmrob
  • 2,966
  • 29
  • 40
  • Nice, thank you. I modified this to fix some repos with tags mixing 0.0.1 and v0.0.1 formats that were causing some problems for me. My original attempt was making new tags that were all from the current date, so this really helped. https://gist.github.com/petertwise/3802f392aa5f2d71143b5da8d02e47e0 – squarecandy Nov 23 '20 at 20:45
6

Building on the other answers, here's a way that will preserve the first line of the tag message

git tag -l | while read -r tag ; do COMMIT_HASH=$(git rev-list -1 $tag) COMMIT_MSG=$(git tag -l --format='%(contents)' $tag | head -n1) && GIT_COMMITTER_DATE="$(git show $COMMIT_HASH --format=%aD | head -1)" git tag -a -f $tag -m"$COMMIT_MSG" $COMMIT_HASH ; done
git tag -l -n1           #check by listing all tags with first line of message
git push --tags --force  #push edited tags up to remote

The bit responsible for preserving the messages is:

COMMIT_MSG=$(git tag -l --format='%(contents)' $tag | head -n1)

head -n1 will take the first line of the old commit message. You can modify it to -n2 or -n3 etc to get two or three lines instead.

If you want to change the date/time for just one tag, this is how you can break down the one-liner to do it in your bash shell:

tag=v0.1.0
COMMIT_HASH=$(git rev-list -1 $tag)
COMMIT_MSG=$(git tag -l --format='%(contents)' $tag | head -n1)
COMMIT_DATE=$(git show $COMMIT_HASH --format=%aD | head -1)
GIT_COMMITTER_DATE=$COMMIT_DATE git tag -s -a -f $tag -m"$COMMIT_MSG" $COMMIT_HASH

References:

rds
  • 26,253
  • 19
  • 107
  • 134
weiji14
  • 477
  • 7
  • 14
  • This is great, thanks. In the commands for changing a single tag, though, there's a `-s` flag that's not present in the one-liner, so I was getting `error: gpg failed to sign the data` because I don't have signing set up for git. That error threw me off for a bit. – wch Jun 08 '20 at 17:17
  • Thank you for your gold solution, no one solution worked like yours. Great work and big help – dalisoft Jul 12 '21 at 19:05
0

It seems that in new versions of git (tested on 2.33.0), when you git tag, the new tag's date will be set to the date of the commit.

So, you can remove the tag and recreate it without setting the environment variables and it will work too.

$ tag_commit=$(git show-ref v0.1.0 | cut -d' ' -f1)
$ git tag -d v1.0.0  # Remove tag locally
$ git push --delete origin v1.0.0  # Remove tag on remote
$ git tag v1.0.0 "$tag_commit"
$ git push --tags

This doesn't let you specify the message, though. As soon as you do, the current date will be used.

IS3NY
  • 43
  • 1
  • 5
  • With git version 2.33.1 git tag does not create the tag based on the commit version for me. – Richard Witherspoon Oct 31 '21 at 16:43
  • This is because you're using lightweight tags. Lightweight tags do not consist of a an additional "commit" object like annotated tags, which is why they cannot have their own messages—but it is _also_ why they cannot have their own timestamp. They are merely named pointers to a commit, and so they "inherit" all properties of that commit, namely including timestamp, author, and in some tools, commit message. Unfortunately, OP was definitely asking about existing __annotated__ tags (`git tag -a`). Besides, lightweight tags are only recommended for local tagging. – Pyr3z Aug 04 '23 at 18:55
0

A batch solution that DOES preserve messages

So, what I needed to do to 100+ tags essentially boiled down to applying a regex-replace on each of their names, make the annotated tag dates match the underlying commit dates (which satisfies OP's question), and leave everything else untouched.

The following script takes a sed -E expression as argument 1, and then it goes to town on the current repo. It does NOT do the pushing step, allowing you to recover from oopsies without clobbering the remote.

#!/bin/bash
# arg 1 = sed string to apply to tag names, e.g. 's|^foo|bar|'

if [[ -z "$1" ]]; then
  >&2 echo "ERR: 1 argument is required: valid sed string"
  exit 1
fi

# enable bash strict mode
set -euo pipefail
IFS=$'\n'

# assume currently in the desired git repo
list="$(paste <(git tag) <(git tag | sed -E "$1") | awk '{if($1!=$2){print $0}}')"

echo "$list" | while read line; do
  prev=${line%  *} # whitespace between '%' and '*' is a TAB
  next=${line#* }  # wspace between '*' and '}' = also a TAB

  date="$(git log -1 --format='%ct' "$prev^{}")"
   msg="$(git for-each-ref --format='%(contents)' refs/tags/$prev)"

  GIT_COMMITTER_DATE="$date" git tag -a $next -m "$msg" "$prev^{}"

  # use stdout to get the names of old tags (for deletion),
  echo "$prev"
  # and use stderr for verbose info
  >&2 echo "  is now: '$next'"
done

example usage

Hope this helps someone out there!

Pyr3z
  • 91
  • 1
  • 5