web-dev-qa-db-ja.com

gitで「タグをリベース」する方法は?

私が次の単純なgitリポジトリを持っていると仮定します:単一のブランチ、いくつかは次々にコミットし、それらのいくつかはそれらのそれぞれをコミットした後にタグ付けされました(注釈付きタグで)そしてある日私は決定します最初のコミットを変更したい(ちなみに、何かが変更された場合、タグ付けされません)。だから私はgit rebase --interactive --rootを実行し、最初のコミットに「編集」のマークを付け、その中の何かを変更してgit rebase --continueします。これで、リポジトリ内のすべてのコミットが再作成されたため、sha1が変更されました。ただし、作成したタグは完全に変更されておらず、以前のコミットのsha1を指しています。

リベース時に作成された対応するコミットにタグを自動的に更新する方法はありますか?

git filter-branch --tag-name-filter cat -- --tagsの使用を提案する人もいますが、最初に各タグが変更されていないことを警告し、次に各タグが自分自身に変更されている(同じタグ名と同じコミットハッシュ)と言います。それでも、git show --tagsは、タグがまだ古いコミットを指していることを示しています。

11
user4256966

ある意味では手遅れです(しかし、ちょっと待ってください、良いニュースがあります)。 _filter-branch_コードは、フィルタリング中にold-sha1からnew-sha1へのマッピングを保持するため、タグを調整できます。

実際、_filter-branch_とrebaseはどちらも同じ基本的な考え方を使用しています。つまり、元のコンテンツを展開し、必要な変更を加えてから、各コミットをコピーにします。結果から新しいコミットを作成します。つまり、各コピーステップで、<old-sha1、new-sha1>ペアをファイルに書き込むのは簡単です。完了したら、old-sha1からnew-sha1を検索して参照を修正します。 。すべての参照が完了すると、新しい番号付けにコミットし、マッピングを削除します。

地図はもうなくなっているので、「ある意味では手遅れです」。

幸いなことに、手遅れではありません。 :-)あなたのリベースは繰り返し可能です、あるいは少なくとも、それの重要な部分はおそらくそうです。さらに、リベースが十分に単純な場合は、それをまったく繰り返す必要がない場合があります。

「繰り返し」の考えを見てみましょう。任意の形状の元のグラフGがあります。

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

(おっ、空飛ぶ円盤!)。それ(の一部)に対して_git rebase --root_を実行し、(一部またはすべての)コミットをコピーして(マージを保持するかどうかにかかわらず)、新しいグラフG 'を取得しました。

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

元のルートノードのみを共有してこれを描画しました(現在は、空飛ぶ円盤ではなく、クレーンが付いた帆船です)。より多くの共有、またはより少ない共有があるかもしれません。古いノードの一部は完全に参照されなくなったため、ガベージコレクションされた可能性があります(おそらくそうではありません。reflogはすべての元のノードを少なくとも30日間存続させる必要があります)。しかし、いずれにせよ、G 'の「古いG部分」を指すタグがまだあり、それら参照はそれらノードとそのすべての親がまだ存在することを保証します新しいG 'で。

したがって、元のリベースがどのように行われたかがわかっている場合は、Gの重要な部分であるG 'のサブグラフでそれを繰り返すことができます。これがどれほど難しいか簡単か、そしてそれを行うためにどのコマンドを使用するか、元のGがすべてG 'にあるかどうか、リベースコマンドが何であったか、元のGをオーバーレイするG'の量などによって異なります(ノードのリストを取得するためのキーである_git rev-list_以降、おそらく、「元の、was-in-G」ノードと「new toG '」ノードを区別する方法はありません。しかし、おそらくそれは可能です。現時点では、これは単なるプログラミングの問題です。

これを繰り返す場合、特に結果のグラフG ''がG 'と完全に重なっていない場合は、マッピングを保持する必要があります。必要なのはマップ自体ではなく、投影このマップのGからG 'へ。

元のGの各ノードに一意の相対アドレスを指定し(たとえば、「ヒントから、親のコミット#2を見つける、そのコミットから、親のコミット#1を見つける、そのコミットから...」)、対応するものを見つけます。 G ''の相対アドレス。これにより、マップの重要な部分を再構築できます。

元のリベースの単純さによっては、このフェーズに直接ジャンプできる場合があります。たとえば、グラフ全体が平坦化せずにコピーされたことが確実にわかっている場合(つまり、2つの独立した空飛ぶ円盤があります)、GのタグTの相対アドレスは相対アドレスです。 G 'に必要なアドレス。これで、その相対アドレスを使用して、コピーされたコミットを指す新しいタグを作成するのは簡単です。

新しい情報に基づく大きな更新

元のグラフが完全に線形であり、すべてのコミットをコピーしたという追加情報を使用すると、非常に単純な戦略を使用できます。マップを再構築する必要がありますが、すべての古いコミットには、元のグラフの両端から直線距離(単一の数値として表すのが簡単)を持つ新しいコミットが1つだけあるため、簡単です(先端からの距離を使用してください)。

つまり、古いグラフは次のようになり、ブランチは1つだけです。

_A <- B <- C ... <- Z   <-- master
_

タグは、(注釈付きタグオブジェクトを介して)コミットの1つを指すだけです。たとえば、タグfooは、コミットWを指す注釈付きタグオブジェクトを指します。次に、WZからの4つのコミットであることに注意してください。

新しいグラフは、各コミットがそのコピーに置き換えられていることを除いて、まったく同じように見えます。これらを_A'_、_B'_などと_Z'_で呼びましょう。 (単一の)ブランチは、最も先端のコミット、つまり_Z'_を指します。元のタグfooを調整して、_W'_を指す新しい注釈付きタグオブジェクトを作成します。

元のtip-mostコミットのSHA-1IDが必要になります。これは(単一の)ブランチのreflogで簡単に見つけることができ、おそらく単に_master@{1}_です(ただし、それ以降にブランチを微調整した回数によって異なります。それ以降に追加した新しいコミットがある場合リベースでは、それらも考慮する必要があります)。リベースの結果が気に入らないと判断した場合に備えて、_ORIG_HEAD_が残す特別な参照_git rebase_にも含まれている可能性があります。

_master@{1}_が正しいIDであり、そのような新しいコミットがないと仮定しましょう。次に:

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

このIDを_$orig_master_に保存します。

完全なマップを作成したい場合は、次のようにします。

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

(両方のファイルの出力は同じである必要があります。そうでない場合は、ここでの仮定が間違っています。その間、シェル_$_プレフィックスも省略します。これは、残りの部分が実際にスクリプトに含まれるためです。タイプミスや微調整が必​​要な場合は、1回限りの使用でも)

_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
_

(これは、まったくテストされていませんが、マッピングを取得するために、2つのファイルを一緒に貼り付けることを目的としています。2つのリストにシェルバージョンのPython Zip)を貼り付けます)。実際にはマッピングは必要ありません。必要なのは「先端からの距離」のカウントだけなので、ここでは気にしないふりをします。

次に、すべてのタグを反復処理する必要があります。

_# 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's 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's 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
_

2つの_adj_anno_tag_および_adj_lightweight_tag_シェル関数を作成する必要があります。ただし、最初に、古いIDを指定して新しいIDを生成する、つまりマッピングを検索するシェル関数を作成しましょう。実際のマッピングファイルを使用した場合、最初のエントリにgrepまたはawkを実行してから、2番目のエントリを出力します。ただし、sleazy single-old-fileメソッドを使用すると、一致するIDの行番号が必要になります。これは_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))
}
_

警告のケースが発生することはなく、rev-parseが失敗することもありませんが、おそらくこのシェル関数の戻りステータスを確認する必要があります。

軽量のタグアップデーターは今ではかなり簡単です。

_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
}
_

注釈付きタグの更新はより困難ですが、_git filter-branch_からコードを盗むことができます。ここですべてを引用するつもりはありません。代わりに、私はあなたにこのビットを与えるだけです:

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

およびこれらの手順:_git for-each-ref_の2番目のオカレンスを検索し、sedにパイプされた_git cat-file_に注意してください。結果は_git mktag_に渡され、シェル変数_new_sha1_。

これは、タグオブジェクトをコピーするために必要なものです。新しいコピーは、古いタグが指しているコミットで$(map_sha1)を使用して見つかったオブジェクトを指している必要があります。 _filter-branch_を使用して、_git rev-parse $old_sha1^{commit}_と同じ方法でコミットを見つけることができます。

(ちなみに、この回答を書き、filter-branchスクリプトを見ると、filter-branchにバグがあり、リベース後のタグ修正コードにインポートします。既存の注釈付きタグがポイントしている場合別のタグに対しては修正しません。軽量タグとコミットを直接指すタグのみを修正します。)

上記のサンプルコードは実際にはテストされておらず、より汎用的なスクリプト(たとえば、リベースの後に実行できる、さらにはインタラクティブなリベース自体に組み込まれている)に変換するには、かなりの量が必要です。余分な仕事。

16
torek

torek の詳細なウォークスルーのおかげで、実装をまとめました。

#!/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
5
Laura A. Rivera

git rebasetagsを使用できます

git rebaseを使用するのと同じように使用します

git rebasetags <rebase args>

リベースがインタラクティブな場合は、変更を加えることができるbashシェルが表示されます。そのシェルを終了すると、タグが復元されます。

enter image description here

から この投稿

3
nachoparker