web-dev-qa-db-ja.com

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

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

解決策はかなり興味深いものであり、実際にはgitの他のいくつかのようにマインドワーピングではありませんが、プロジェクトの開発に沿って手順を何度も繰り返す必要がある場合、それはいまだに傷のofのバッグです。

したがって、私はむしろ一度だけ痛みを経験し、その後標準の対話型リベースを永遠に使用できるようにしたいと思います。

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

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

203
kch

2017年半ばの回答

副作用のない完全に空の新しいコミットを作成するには、おそらくGitの配管を直接使用するのが最適です。そのようにすることで、作業コピーやインデックスに触れたり、クリーンアップする一時的なブランチなどの副作用を回避できます。

  1. コミットを作成するには、そのためのディレクトリツリーが必要なので、最初に空のツリーを作成します。

    tree=`git hash-object -wt tree --stdin < /dev/null`
    
  2. これでコミットをラップできます:

    commit=`git commit-tree -m 'root commit' $tree`
    
  3. そして今、それに基づいてリベースすることができます:

    git rebase --onto $commit --root master
    

以上です。シェルを十分に理解していれば、そのすべてをワンライナーに再配置できます。

(N.B .:実際にはfilter-branchを使用します。後で編集します。)


過去の回答(他の回答が参照)

これは、同じソリューションのよりクリーンな実装です。追加のリポジトリを作成したり、リモートで操作したり、切り離されたヘッドを修正したりする必要はありません。

# first you need a new empty branch; let's call it `newroot`
git checkout --Orphan newroot
git rm -rf .

# then you apply the same steps
git commit --allow-empty -m 'root commit'
git rebase --onto newroot --root master
git branch -d newroot

出来上がり、masterになりました。その履歴は空のルートコミットを含むように書き換えられました。


注意:--Orphancheckoutに切り替えられていないGitの古いバージョンでは、空のブランチを作成するために配管が必要です。

git symbolic-ref HEAD refs/heads/newroot
git rm --cached -r .
git clean -f -d
283

Aristotle PagaltzisとUweKleine-Königの回答とRichard Bronoskyのコメントのマージ。

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か所に置くため)

29

アリストテレスの答えが好きです。しかし、大規模なリポジトリ(> 5000コミット)では、いくつかの理由でフィルターブランチがリベースよりも優れていることがわかりました1)より高速です2)マージの競合がある場合、人間の介入を必要としません。 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」オプションは、新しく作成されたコミットを指すようにタグが書き換えられることを意味することに注意してください。

11
Kent

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

# 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だけでなく)も書き換えます。

5
ldav1s

git replacegit 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
Philippe

私は興奮し、この素敵なスクリプトの「べき等」バージョンを書きました...それは常に同じ空のコミットを挿入し、あなたがそれを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

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

これにより、レポのクローンされた複数のコピーでこの操作を実行でき、同じ結果が得られるため、それらはまだ互換性があります...テスト...はい、機能しますが、削除して追加する必要もあります再びリモート、例えば:

git remote rm Origin
git remote add --track master user@Host:path/to/repo
3
Sam Watkins

git rebase --root --onto $emptyrootcommit

簡単にトリックを行う必要があります

3

「git init」の直後に空のコミットを作成するのを忘れた場合、リポジトリの開始時に空のコミットを追加するために使用できる簡単なワンライナーがあります。

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

さて、ここに私が思いついたものがあります:

# 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
kch

Kent の改善に基づく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='[email protected]'
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"
1
luxagen

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

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

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

git rebase -i --root

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

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

ピック0294途中でコミット

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

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

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

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

ピック0294途中でコミット

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

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

git config --global core.editorname_of_the_editor_program_you_want_to_use

1
tigre200

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).

リポジトリには、コミットされるのを待っているローカルの変更が含まれてはならないことに注意してください。
注意 git checkout --Orphanはgitの新しいバージョンで動作すると思います。
ほとんどの時間に注意してくださいgit statusは有用なヒントを提供します。

0
ony