How to "reorder tags" in git?

Suppose I have the following simple git repository: one branch, some of them commit one after another, and some of them were tagged (with annotated tags) after they committed each of them, and then one fine day I decided that I want to change the first commit (which, by the way, is not marked if it changes something). So I run git rebase --interactive --root and just mark "edit" for the initial commit, change something and git rebase --continue . Now all the commits in my repository have been recreated, so their sha1 have changed. However, the tags that I created did not completely change, still pointing to sha1 from previous commits.

Is there an automatic way to update tags for corresponding fixes created during reboot?

Some people suggest using git filter-branch --tag-name-filter cat -- --tags , but this first warns me that each of my tags has not changed, and then says that each of my tags is changed to itself (one same tag name and same hash code). And yet git show --tags says the tags still point to old commits.

+8
git git-rebase rebase git-tag
source share
3 answers

In a way, it's too late (but hold on, there is good news). The filter-branch code is capable of customizing tags because it retains the old-sha1-new-sha1 mapping during filtering.

In fact, both filter-branch and rebase use the same basic idea, which is that each commit is copied, expanding the original content, making any desired changes and then making a new commit from the result. This means that at each step of copying, it is trivial to write a <old-sha1, new-sha1> pair to a file, and then, as soon as you are done, you fix the links by looking at new-sha1 from your old-sha1. When all the links are complete, you will be tied to the new numbering and you will remove the display.

Now the map has disappeared, so "in a way, it's too late."

Fortunately, this is not too late. :-) Your relocation is repeated, or at least its key parts are probably there. Moreover, if your rebase was simple enough, you may not need to repeat it at all.

Let's look at the "repetition" of thought. We have the original graph G of some arbitrary shape:

  o--o / \ o--o--o---o--o <-- branch-tip \ / o--o--o--o 

(hey flying saucer!). We did git rebase --root on (some part) of it, copying (some or all) does (keeps merging or not) to get some new graph G ':

  o--o--o--o <-- branch-tip / / o--o / / \ o--o--o---o--o \ / o--o--o--o 

I drew this separation only with the original root node (and now it's a sailboat with a crane on it, not a flying saucer). There may be more in common or less. Some of the old nodes may have become completely unacceptable and therefore collected in garbage (probably not: all the source nodes should be stored in the logs for at least 30 days). But in any case, we still have tags pointing to some kind of 'old G-part' G ', and these links guarantee that these nodes and all their parents are still in the new G'.

Thus, if we know how the initial rebase was made, we can repeat it on subgraph G ', which is an important part of G. How difficult or simple it is, and which commands to use to do this, depends on whether all the original G to G ', what is the rebase command, how much G' does the original G overlap, etc. (since git rev-list , which is our key for getting the list of nodes, probably does not have the ability to distinguish between "original", "in-in-G" and "new to G" nodes). But this can probably be done: this is just a small programming problem, for now.

If you repeat this, this time you will want to save the mapping, especially if the resulting graph G '' does not completely overlap G ', because what you need now is not the map itself, but the projection of this mapping, from G to G' .

We simply give each node in the original G a unique relative address (for example, "from the tip", find the parent commit # 2, from which commit, find the parent commit # 1, from this commit ... ") and then find the corresponding relative address in G. '' This allows us to rebuild critical parts of the map.

Depending on the simplicity of the initial rebase, we can go directly to this phase. For example, if we know for sure that the entire graph was copied without alignment (so that we have two independent flying saucers), then the relative address for the tag T in G is the relative address that we want in G ', and now it is trivial to use this relative address to make a new tag indicating the copied commit.

Big update based on new information

Using the additional information that the original graph was completely linear, and that we copied all the commits, we can use a very simple strategy. We still need to restore the map, but now it’s easy, since each old fixation has exactly one new latch, which has some linear distance (which is easy to imagine as a single number) from either end of the original chart (I will use the distance from the tip) .

That is, the old graph looks like this: just one branch:

 A <- B <- C ... <- Z <-- master 

Tags simply point to one of the commits (via the annotated tag object), for example, perhaps the foo tag points to an annotated tag object that points to a W commit. Then we note that W are four errors from Z

The new chart looks exactly the same, except that each commit has been replaced with a copy of it. Call these A' , B' , etc. Through Z' . The (single) branch indicates the fixation of the very end, i.e. Z' . We want to adjust the original foo tag so that we have a new object with an annotated tag pointing to W' .

We will need the SHA-1 identifier of the original tip-most commit command. This should be easy to find in the reflog for the (single) branch, and probably just master@{1} (although it depends on how many times you changed the branch since then, and if there are new commits that you added after the reboot, we must consider them as well). It could also be in a special ORIG_HEAD ref, which git rebase leaves behind if you decide that you don't like the result of the redirect.

Assume that master@{1} is the correct ID and that there are no such new commits. Then:

 orig_master=$(git rev-parse master@{1}) 

will save this identifier in $orig_master .

If we wanted to build a complete map, it would do this:

 $ git rev-list $orig_master > /tmp/orig_list $ git rev-list master > /tmp/new_list $ wc -l /tmp/orig_list /tmp/new_list 

(the output for both files should be the same, and if not, some kind of assumption went wrong here, meanwhile I will also leave the $ shell prefix lower, since the rest of this should really go into the script, even for a one-time use, in case of typos and the need for settings)

 exec 3 < /tmp/orig_list 4 < /tmp/new_list while read orig_id; do read new_id <& 4; echo $orig_id $new_id; done <& 3 > /tmp/mapping 

(this one, completely untested, is intended for inserting two files together - a view of the Python zip shell in two lists - to get a display). But we really don’t need a comparison, all we need is a "distance from the tip" counts, so I'm going to pretend that we were not worried here.

Now we need to iterate over all the tags:

 # We don't want a pipe here because it's # not clear what happens if we update an existing # tag while `git for-each-ref` is still running. git for-each-ref refs/tags > /tmp/all-tags # it also probably a good idea to copy these # into a refs/original/refs/tags name space, a la # git filter-branch. while read sha1 objtype tagname; do git update-ref -m backup refs/original/$tagname $sha1 done < /tmp/all-tags # now replace the old tags with new ones. # it easy to handle lightweight tags too. while read sha1 objtype tagname; do case $objtype in tag) adj_anno_tag $sha1 $tagname;; commit) adj_lightweight_tag $sha1 $tagname;; *) echo "error: shouldn't have objtype=$objtype";; esac done < /tmp/all-tags 

We still need to write two shell functions adj_anno_tag and adj_lightweight_tag . First, let him write a shell function that creates a new identifier, given the old identifier, i.e. View mapping. If we used a real mapping file, we would use grep or awk for the first record and then print the second. However, using the untidy method with one old file, we need the line number of the matching identifier, which we can get with grep -n :

 map_sha1() { local grep_result line grep_result=$(grep -n $1 /tmp/orig_list) || { echo "WARNING: ID $1 is not mapped" 1>&2 echo $1 return 1 } # annoyingly, grep produces "4:matched-text" # on a match. strip off the part we don't want. line=${grep_result%%:*} # now just get git to spit out the ID of the (line - 1)'th # commit before the tip of the current master. the "minus # one" part is because line 1 represents master~0, line 2 # is master~1, and so on. git rev-parse master~$((line - 1)) } 

The WARNING case should never happen, and rev-parse will never fail, but we should probably check the return status of this shell function.

The lightweight tag updater is now pretty trivial:

 adj_lightweight_tag() { local old_sha1=$1 new_sha1 tag=$2 new_sha1=$(map_sha1 $old_sha1) || return git update-ref -m remap $tag $new_sha1 $old_sha1 } 

Updating an annotated tag is more difficult, but we can steal the code from git filter-branch . I will not bring everything here here; instead, I just give you this bit:

 $ vim $(git --exec-path)/git-filter-branch 

and these instructions: find the second occurrence of git for-each-ref and notice the git cat-file passed over the sed channel, with the result passed to git mktag , which sets the shell variable new_sha1 .

This is what we need to copy the tag object. The new copy should point to the object found with $ (map_sha1) for the commit that the old tag points to. We can find what to do the same filter-branch using git rev-parse $old_sha1^{commit} .

(By the way, recording this answer and looking at the script filter branch, it occurs to me that there is an error in the filter branch that we import into our post-rebase patch code: if the existing annotated tag points to another tag, we do not fix it We only capture light tags and tags that point directly to the commit.)

Please note that none of the above code examples has been tested and turns it into a more universal script (which can be run after any permutation, for example, or, even better, included in the interactive rebase) requires a considerable amount of additional work.

+10
source share

Thanks torek detailed walkthrough, I put together an implementation.

 #!/usr/bin/env bash set -eo pipefail orig_master="$(git rev-parse ORIG_HEAD)" sane_grep () { GREP_OPTIONS= LC_ALL=C grep "$@" } map_sha1() { local result line # git rev-list $orig_master > /tmp/orig_list result="$(git rev-list "${orig_master}" | sane_grep -n "$1" || { echo "WARNING: ID $1 is not mapped" 1>&2 return 1 })" if [[ -n "${result}" ]] then # annoyingly, grep produces "4:matched-text" # on a match. strip off the part we don't want. result=${result%%:*} # now just get git to spit out the ID of the (line - 1)'th # commit before the tip of the current master. the "minus # one" part is because line 1 represents master~0, line 2 # is master~1, and so on. git rev-parse master~$((result - 1)) fi } adjust_lightweight_tag () { local old_sha1=$1 new_sha1 tag=$2 new_sha1=$(map_sha1 "${old_sha1}") if [[ -n "${new_sha1}" ]] then git update-ref "${tag}" "${new_sha1}" fi } die () { echo "$1" exit 1 } adjust_annotated_tag () { local sha1t=$1 local ref=$2 local tag="${ref#refs/tags/}" local sha1="$(git rev-parse -q "${sha1t}^{commit}")" local new_sha1="$(map_sha1 "${sha1}")" if [[ -n "${new_sha1}" ]] then local new_sha1=$( ( printf 'object %s\ntype commit\ntag %s\n' \ "$new_sha1" "$tag" git cat-file tag "$ref" | sed -n \ -e '1,/^$/{ /^object /d /^type /d /^tag /d }' \ -e '/^-----BEGIN PGP SIGNATURE-----/q' \ -e 'p' ) | git mktag ) || die "Could not create new tag object for $ref" if git cat-file tag "$ref" | \ sane_grep '^-----BEGIN PGP SIGNATURE-----' >/dev/null 2>&1 then echo "gpg signature stripped from tag object $sha1t" fi echo "$tag ($sha1 -> $new_sha1)" git update-ref "$ref" "$new_sha1" fi } git for-each-ref --format='%(objectname) %(objecttype) %(refname)' refs/tags | while read sha1 type ref do case $type in tag) adjust_annotated_tag "${sha1}" "${ref}" || true ;; commit) adjust_lightweight_tag "${sha1}" "${ref}" || true echo ;; *) echo "ERROR: unknown object type ${type}" ;; esac done 
+3
source share

You can use git rebasetags

Used the same way you would use git rebase

git rebasetags <rebase args>

In case rebase is interactive, you will be presented with a bash shell where you can make changes. After exiting this shell, the tags will be restored.

enter image description here

From this post

+1
source share

All Articles