Gitのルートコミットの前にコミットを挿入しますか?


231

前に、gitリポジトリで最初の2つのコミット押しつぶす方法について尋ねました。

ソリューションはかなり興味深いものであり、gitの他のいくつかのものほど心をゆがめるものではありませんが、プロジェクトの開発に沿って何度も手順を繰り返す必要がある場合、これらのソリューションはまだよくある痛ましいことです。

したがって、私はむしろ苦痛を一度だけ経験し、それから標準のインタラクティブなリベースを永遠に使用できるようにしたいと思います。

私がやりたいことは、最初の目的のためだけに存在する空の初期コミットを作成することです。コードも、何もありません。スペースを確保するだけで、リベースのベースになります。

私の質問は、既存のリポジトリがある場合、最初のコミットの前に新しい空のコミットを挿入し、他の全員を前にシフトするにはどうすればよいですか?


3
;)とにかくそれは答えを正当化すると思います。私は、ひたすらヒストリーを編集することで、気が狂う多くの方法を探っています。心配しないでください。共有リポジトリではありません。
2009年

11
ある執念深い、狂気の履歴エディタから別のものへ、質問を投稿してくれてありがとう!; D
マルコ、

10
@kchの弁護において、完全に正当な理由の1つは、私が自分自身を見つけた1つの理由です。リポジトリでキャプチャされなかった履歴バージョンのスナップショットを追加します。
Old McStopher、2012

4
私には別の正当な理由があります!最初のコミットの前に空のコミットを追加して、最初のコミットにリベースし、リポジトリの最初のコミットで追加されたバイナリブロートを削除できるようにする(:
pospi

回答:


314

これを達成するには2つのステップがあります。

  1. 新しい空のコミットを作成する
  2. この空のコミットから開始するように履歴を書き換えます

newroot便宜上、新しい空のコミットを一時的なブランチに配置します。

1.新しい空のコミットを作成する

これを行うにはいくつかの方法があります。

配管だけを使う

最もクリーンなアプローチは、Gitのplumbを使用して、直接コミットを作成することです。これにより、作業コピーやインデックス、またはチェックアウトされているブランチなどに触れないようにします。

  1. 空のディレクトリのツリーオブジェクトを作成します。

    tree=`git hash-object -wt tree --stdin < /dev/null`
    
  2. コミットをラップしてください:

    commit=`git commit-tree -m 'root commit' $tree`
    
  3. それへの参照を作成します。

    git branch newroot $commit
    

もちろん、シェルを十分に理解している場合は、手順全体をワンライナーに再配置できます。

配管なし

通常の磁器コマンドでは、newroot正当な理由もなく、ブランチをチェックアウトしてインデックスと作業コピーを繰り返し更新しないと、空のコミットを作成できません。しかし、これを理解する方が簡単だと感じる人もいます。

git checkout --orphan newroot
git rm -rf .
git clean -fd
git commit --allow-empty -m 'root commit'

--orphanへの切り替えがないGitの非常に古いバージョンではcheckout、最初の行を次のように置き換える必要があることに注意してください。

git symbolic-ref HEAD refs/heads/newroot

2.この空のコミットから開始するように履歴を書き換えます

ここには2つのオプションがあります。リベース、またはクリーンな履歴のリライトです。

リベース

git rebase --onto newroot --root master

これには単純さのメリットがあります。ただし、ブランチの最後のコミットごとにコミッターの名前と日付も更新します。

また、エッジケースの履歴によっては、何も含まれていないコミットに基づいているにもかかわらず、マージの競合が原因で失敗する場合もあります。

歴史の書き換え

よりクリーンなアプローチは、ブランチを書き直すことです。とは異なりgit rebase、ブランチがどのコミットから始まるかを調べる必要があります:

git replace <currentroot> --graft newroot
git filter-branch master

書き換えは明らかに2番目のステップで行われます。説明が必要な最初のステップです。何git replace行うことは、それはそれはあなたが交換したいオブジェクトへの参照を見ている時はいつでも、Gitのではなく、そのオブジェクトの交換を見なければならないことにGitを伝えています。

では--graftスイッチ、あなたはそれを通常よりもわずかに異なる何かを語っています。まだ置換オブジェクトはありませんが、置換オブジェクトの親コミットがリストしたもの(つまりコミット)であることを除いて、<currentroot>コミットオブジェクトをそれ自体の正確なコピーで置き換えたいと考えています。)。次に、このコミットを作成してから、元のコミットの代わりとしてそのコミットを宣言します。newrootgit replace

ここで、を実行するとgit log、すでに期待どおりの外観になっていることがわかりますnewroot。ブランチはから始まります。

ただし、git replace 実際に履歴が変更されるわけではありません。また、リポジトリから伝播されることもありません。あるオブジェクトから別のオブジェクトへのローカルリダイレクトをリポジトリに追加するだけです。これが意味することは、この置換の効果は他の誰にも見えないということです-あなただけです。

そのため、filter-branchステップが必要です。ではgit replace、あなたがルートの調整の親コミットして正確なコピーを作成してコミット。git filter-branchその後、次のすべてのコミットについてもこのプロセスを繰り返します。ここで、履歴を実際に書き換えて、共有できるようにします。


1
その--onto newrootオプションは冗長です。渡すnewroot引数は上流の引数-と同じであるため、それなしで実行できますnewroot
ヴィルヘルムテル

7
配管コマンドの代わりに磁器を使用しないのはなぜですか。私はgit symbolic-ref HEAD refs / heads / newrootgit checkout --orphan newroot
albfan

4
@nenopera:この回答はgit-checkoutその切り替えが行われる前に書かれたため。ポインタをありがとう、そのアプローチを最初に言及するように更新しました。
アリストテレスPagaltzis 2013

1
newrootが空でない場合は、を使用git rebase --merge -s recursive -X theirs --onto newroot --root masterしてすべての競合を自動的に解決します(この回答を参照)。@AlexanderKuzin
ユーザーの

1
@Geremia最後のコミットのみを修正できるため、リポジトリにルートコミットのみが含まれている場合は機能する可能性があります。それ以外の場合は、リポジトリ内の他のすべてのコミットを、修正されたルートコミットの上にリベースする必要があります。しかし、それでも、このトピックは、ルートコミットを変更するのではなく、既存のルートの前に別のコミットを挿入することを意味します。
ユーザー

30

アリストテレス・パガルツィスとウヴェ・クライネ・ケーニッヒの回答とリチャード・ブロノスキーのコメントの融合。

git symbolic-ref HEAD refs/heads/newroot
git rm --cached -r .
git clean -f -d
# touch .gitignore && git add .gitignore # if necessary
git commit --allow-empty -m 'initial'
git rebase --onto newroot --root master
git branch -d newroot

(すべてを1つの場所に配置するため)


これは素晴らしいです。これがgit rebase -i --rootが内部で行ったことと同じであるとよいでしょう。
aredridel 2012

うん、そうではないことに驚いた。
アントニーハッチキンズ

git rebase newroot masterエラーのため、rebaseコマンドをに変更する必要がありました。
marbel82

@ antony-hatchkinsこれをありがとう。既存のgitリポジトリがあり、(ここでは説明しないさまざまな理由により)最初のコミットとしてNON-EMPTY gitコミットを追加しようとしています。ですから、git commit --allow-empty -m 'initial'をgit addに置き換えました。; git commit -m "最初のlaravelコミット"; git push; そして、このリベース手順:git rebase --onto newroot --root masterが失敗し、TONのマージ競合が発生します。何かアドバイス?:((
kp123

@ kp123空のコミットを試してください:)
Antony Hatchkins '31 / 12/31

12

私はアリストテレスの答えが好きです。しかし、大規模なリポジトリ(コミット数が5000を超える)の場合、フィルター分岐は、いくつかの理由でリベースよりもうまく機能します。3)タグを書き直すことができます-それらを保持します。各コミットの内容についての質問がないため、filter-branchが機能することに注意してください。これは、この「リベース」の前とまったく同じです。

私のステップは:

# first you need a new empty branch; let's call it `newroot`
git symbolic-ref HEAD refs/heads/newroot
git rm --cached -r .
git clean -f -d

# then you apply the same steps
git commit --allow-empty -m 'root commit'

# then use filter-branch to rebase everything on newroot
git filter-branch --parent-filter 'sed "s/^\$/-p <sha of newroot>/"' --tag-name-filter cat master

「--tag-name-filter cat」オプションは、新しく作成されたコミットを指すようにタグが書き換えられることを意味することに注意してください。


これは、興味深いユースケースでもある空でないコミットの作成には役立ちません。
ceztko 2015

他のソリューションと比較すると、あなたの取るに足らない副作用が1つだけあります。それはハッシュを変更しますが、履歴全体はそのままです。ありがとうございました!
Vladyslav Savchenko

5

私はアリストテレスとケントの答えをうまく使いました:

# first you need a new empty branch; let's call it `newroot`
git checkout --orphan newroot
git rm -rf .
git commit --allow-empty -m 'root commit'
git filter-branch --parent-filter \
'sed "s/^\$/-p <sha of newroot>/"' --tag-name-filter cat -- --all
# clean up
git checkout master
git branch -D newroot
# make sure your branches are OK first before this...
git for-each-ref --format="%(refname)" refs/original/ | \
xargs -n 1 git update-ref -d

これによりmaster、タグだけでなく、すべてのブランチが(だけでなく)書き換えられます。


この最後の行は何をしますか?
Diederick C. Niehorster

refs/original/各参照を検索して削除します。削除する参照は、他のブランチによってすでに参照されているはずなので、実際には消えず、単にrefs/original/削除されます。
ldav1s 2016年

これでうまくいきました。さらに、私が使用timedatectl set-time '2017-01-01 00:00:00'与えるためにnewroot古いタイムスタンプを。
午後


4

私が使用していることを考えるgit replaceとすると、git filter-branch使用してより良いソリューションですgit rebase

  • よりよい性能
  • より簡単でリスクが少ない(各ステップで結果を確認し、実行した操作を取り消すことができます...)
  • 複数のブランチでうまく機能し、結果が保証されます

その背後にあるアイデアは、次のとおりです。

  • はるか昔の新しい空のコミットを作成する
  • 古いルートコミットを、新しいルートコミットが親として追加されることを除いて、まったく同じようなコミットで置き換えます。
  • すべてが期待どおりであることを確認して実行します git filter-branch
  • もう一度、すべてが問題ないことを確認し、不要になったgitファイルをクリーンアップします。

最初の2つのステップのスクリプトは次のとおりです。

#!/bin/bash
root_commit_sha=$(git rev-list --max-parents=0 HEAD)
git checkout --force --orphan new-root
find . -path ./.git -prune -o -exec rm -rf {} \; 2> /dev/null
git add -A
GIT_COMMITTER_DATE="2000-01-01T12:00:00" git commit --date==2000-01-01T12:00:00 --allow-empty -m "empty root commit"
new_root_commit_sha=$(git rev-parse HEAD)

echo "The commit '$new_root_commit_sha' will be added before existing root commit '$root_commit_sha'..."

parent="parent $new_root_commit_sha"
replacement_commit=$(
 git cat-file commit $root_commit_sha | sed "s/author/$parent\nauthor/" |
 git hash-object -t commit -w --stdin
) || return 3
git replace "$root_commit_sha" "$replacement_commit"

このスクリプトはリスクなしで実行できます(これまでに実行したことがないアクションを実行する前にバックアップを作成するのは良い考えですが;))。結果が予期したものと異なる場合は、フォルダーに作成されたファイルを削除して、.git/refs/replace再試行してください。 )

リポジトリの状態が期待どおりであることを確認したら、次のコマンドを実行してすべてのブランチの履歴を更新します

git filter-branch -- --all

これで、古いものと新しいものの2つの履歴が表示されるはずです(詳細については、ヘルプを参照filter-branchしてください)。2を比較して、すべて問題ないかどうかを再度確認できます。問題がなければ、不要なファイルを削除します。

rm -rf ./.git/refs/original
rm -rf ./.git/refs/replace

あなたのmasterブランチに戻って一時的なブランチを削除することができます:

git checkout master
git branch -D new-root

今、すべてを行う必要があります;)


3

私は興奮してこの素敵なスクリプトの「べき等」バージョンを書きました...それは常に同じ空のコミットを挿入し、それを2回実行しても、コミットハッシュは毎回変更されません。だから、これがgit-insert-empty-rootに対する私の見解です:

#!/bin/sh -ev
# idempotence achieved!
tmp_branch=__tmp_empty_root
git symbolic-ref HEAD refs/heads/$tmp_branch
git rm --cached -r . || true
git clean -f -d
touch -d '1970-01-01 UTC' .
GIT_COMMITTER_DATE='1970-01-01T00:00:00 +0000' git commit \
  --date='1970-01-01T00:00:00 +0000' --allow-empty -m 'initial'
git rebase --committer-date-is-author-date --onto $tmp_branch --root master
git branch -d $tmp_branch

余分な複雑さの価値はありますか?多分そうではありませんが、これを使用します。

このSHOULDを使用すると、レポのいくつかの複製されたコピーに対してこの操作を実行でき、同じ結果が得られるため、互換性があります...テスト...はい、機能しますが、再びリモート、例えば:

git remote rm origin
git remote add --track master user@host:path/to/repo

3

"git init"の直後に空のコミットを作成するのを忘れた場合、リポジトリの最初に空のコミットを追加するには:

git rebase --root --onto $(git commit-tree -m 'Initial commit (empty)' 4b825dc642cb6eb9a060e54bf8d69288fbee4904)

1
:4b825dc ...空の木のハッシュですstackoverflow.com/questions/9765453/...
mrksは

2

さて、これが私が思いついたものです:

# Just setting variables on top for clarity.
# Set this to the path to your original repository.
ORIGINAL_REPO=/path/to/original/repository

# Create a new repository…
mkdir fun
cd fun
git init
# …and add an initial empty commit to it
git commit --allow-empty -m "The first evil."

# Add the original repository as a remote
git remote add previous $ORIGINAL_REPO
git fetch previous

# Get the hash for the first commit in the original repository
FIRST=`git log previous/master --pretty=format:%H  --reverse | head -1`
# Cherry-pick it
git cherry-pick $FIRST
# Then rebase the remainder of the original branch on top of the newly 
# cherry-picked, previously first commit, which is happily the second 
# on this branch, right after the empty one.
git rebase --onto master master previous/master

# rebase --onto leaves your head detached, I don't really know why)
# So now you overwrite your master branch with the newly rebased tree.
# You're now kinda done.
git branch -f master
git checkout master
# But do clean up: remove the remote, you don't need it anymore
git remote rm previous

2

改善さbashれたケントの答えに基づいた私のスクリプトは次のとおりです。

  • master完了すると、だけでなく、元のブランチをチェックアウトします。
  • 一時的なブランチを避けようとしましたがgit checkout --orphan、デタッチされた状態ではなくブランチでのみ機能するため、新しいルートをコミットして削除するのに十分な時間チェックアウトされています。
  • filter-branch(Kentは手動置換のためにプレースホルダーをそこに残しました)中に新しいルートコミットのハッシュを使用します。
  • このfilter-branch操作はローカルブランチのみを書き換え、リモートも書き換えません
  • 作成者とコミッターのメタデータは標準化されているので、ルートコミットはリポジトリ間で同一です。

#!/bin/bash

# Save the current branch so we can check it out again later
INITIAL_BRANCH=`git symbolic-ref --short HEAD`
TEMP_BRANCH='newroot'

# Create a new temporary branch at a new root, and remove everything from the tree
git checkout --orphan "$TEMP_BRANCH"
git rm -rf .

# Commit this empty state with generic metadata that will not change - this should result in the same commit hash every time
export GIT_AUTHOR_NAME='nobody'
export GIT_AUTHOR_EMAIL='nobody@example.org'
export GIT_AUTHOR_DATE='2000-01-01T00:00:00+0000'
export GIT_COMMITTER_NAME="$GIT_AUTHOR_NAME"
export GIT_COMMITTER_EMAIL="$GIT_AUTHOR_EMAIL"
export GIT_COMMITTER_DATE="$GIT_AUTHOR_DATE"
git commit --allow-empty -m 'empty root'
NEWROOT=`git rev-parse HEAD`

# Check out the commit we just made and delete the temporary branch
git checkout --detach "$NEWROOT"
git branch -D "$TEMP_BRANCH"

# Rewrite all the local branches to insert the new root commit, delete the 
# original/* branches left behind, and check out the rewritten initial branch
git filter-branch --parent-filter "sed \"s/^\$/-p $NEWROOT/\"" --tag-name-filter cat -- --branches
git for-each-ref --format="%(refname)" refs/original/ | xargs -n 1 git update-ref -d
git checkout "$INITIAL_BRANCH"

2

ルートコミットを切り替えるには:

まず、最初に必要なコミットを作成します。

次に、次のコマンドを使用してコミットの順序を切り替えます。

git rebase -i --root

エディターは、次のように、ルートコミットまでのコミットで表示されます。

1234個の古いルートメッセージを選択

真ん中のコミット0294コミット

ルートに置きたい5678コミットを選択

その後、最初の行に配置することで、必要なコミットを最初に配置できます。例では:

ルートに置きたい5678コミットを選択

1234個の古いルートメッセージを選択

真ん中のコミット0294コミット

エディターを終了すると、コミット順序が変更されます。

PS:gitが使用するエディターを変更するには、次を実行:

git config --global core.editor name_of_the_editor_program_you_want_to_use


1
リベースに--rootが追加されたので、これは断然最も近いソリューションです。
ロスバートン

1

最新と最高の組み合わせ。副作用なし、競合なし、タグを保持。

git log --reverse

tree=`git hash-object -wt tree --stdin < /dev/null`
commit=`git commit-tree -m 'Initialize empty repository' $tree`
echo $commit # copy below, interpolation didn't work for me

git filter-branch --parent-filter 'sed "s/^\$/-p <commit>/"' --tag-name-filter cat master

git log --reverse

GitHubでは、CI実行データが失われ、他のブランチも修正されない限り、PRがめちゃくちゃになる可能性があることに注意してください。


0

Aristotle Pagaltzisなどの回答に続き、より単純なコマンドを使用する

zsh% git checkout --orphan empty     
Switched to a new branch 'empty'
zsh% git rm --cached -r .
zsh% git clean -fdx
zsh% git commit --allow-empty -m 'initial empty commit'
[empty (root-commit) 64ea894] initial empty commit
zsh% git checkout master
Switched to branch 'master'
zsh% git rebase empty
First, rewinding head to replay your work on top of it...
zsh% git branch -d empty 
Deleted branch empty (was 64ea894).

リポジトリには、コミットされるのを待っているローカルの変更が含まれていてはいけないことに注意してください。
Note git checkout --orphanはgitの新しいバージョンで動作するでしょう。
ほとんどの場合、git status役立つヒントが得られることに注意してください。


-6

新しいリポジトリを開始します。

日付を希望する開始日に戻します。

希望どおりにすべてを実行し、システム時間を調整して、希望どおりに実行したいときに反映するようにします。多くの不必要な入力を回避するために、必要に応じて既存のリポジトリからファイルをプルします。

今日に到達したら、リポジトリを入れ替えれば完了です。

クレイジー(確立)であるが、かなりインテリジェント(このようなクレイジーなアイデアを考えるにはある程度の賢さが必要なため)の場合は、プロセスをスクリプト化します。

また、1週間後に過去を別の方法で発生させたい場合にも便利です。


システムの日付をいじる必要のある解決策については気分が悪いですが、あなたは私に少し考えさせてくれましたが、悲しいかな、うまくいきました。ほんとありがと。
2009年

-7

私はこの投稿が古いことを知っていますが、このページはGooglingの "inserting commit git"の最初のページです。

なぜ単純なものを複雑にするのですか?

ABCがあり、ABZCが欲しい。

  1. git rebase -i trunk (またはBの前の何か)
  2. ピックを変更してBラインで編集する
  3. 変更を加えます。 git add ..
  4. git commitgit commit --amendこれはBを編集し、Zを作成しません)

[ git commitここには、コミットを挿入するために必要な数だけ作成できます。もちろん、ステップ5で問題が発生する可能性がありますが、gitとのマージの競合を解決することは、あなたが持っているべきスキルです。そうでない場合は、練習してください!]

  1. git rebase --continue

簡単ですね。

を理解していればgit rebase、「ルート」コミットの追加は問題になりません。

gitを楽しんでください!


5
この質問では、最初のコミットを挿入するよう求めています。ABCからZABCが必要です。簡単なgit rebaseことはこれを行うことができません。
Petr Viktorin、2011年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.