簡體   English   中英

在 Git 的根提交之前插入一個提交?

[英]Insert a commit before the root commit in Git?

我之前問過如何壓縮 git 存儲庫中的前兩個提交

雖然這些解決方案相當有趣,並且不像 git 中的其他一些東西那樣令人費解,但如果您需要在項目開發過程中多次重復該過程,它們仍然有點傷腦筋。

所以,我寧願只經歷一次痛苦,然后就可以永遠使用標准的交互式變基。

那么,我想要做的是有一個空的初始提交,該提交只是為了成為第一個而存在。 沒有代碼,什么都沒有。 只是占用空間,因此它可以成為 rebase 的基礎。

那么我的問題是,擁有一個現有的存儲庫,我該如何在第一個提交之前插入一個新的空提交,並將其他人向前推進?

有兩個步驟來實現這一點:

  1. 創建一個新的空提交
  2. 重寫歷史以從這個空提交開始

為方便起見,我們會將新的空提交放在臨時分支newroot上。

1.創建一個新的空提交

有多種方法可以做到這一點。

僅使用管道

最干凈的方法是使用 Git 的管道直接創建提交,避免接觸工作副本或索引或簽出哪個分支等。

  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開關到checkout的非常舊版本的 Git 上,您必須將第一行替換為:

git symbolic-ref HEAD refs/heads/newroot

2.重寫歷史以從這個空提交開始

你有兩個選擇:變基,或者干凈的歷史重寫。

變基

git rebase --onto newroot --root master

這具有簡單的優點。 但是,它還會在分支上的每次最后提交時更新提交者名稱和日期。

此外,對於一些極端情況的歷史,它甚至可能由於合並沖突而失敗——盡管事實上你正在基於一個不包含任何內容的提交。

歷史改寫

更簡潔的方法是重寫分支。 git rebase不同,您需要查找您的分支從哪個提交開始:

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

顯然,重寫發生在第二步; 這是需要解釋的第一步。 git replace的作用是告訴 Git,每當它看到對要替換的對象的引用時,Git 應該改為查看該對象的替換。

使用--graft開關,您告訴它的內容與正常情況略有不同。 您是說還沒有替換對象,但是您想用其自身的精確副本替換<currentroot>提交對象,除了替換的父提交應該是您列出的那個(即newroot提交)。 然后git replace繼續為您創建此提交,然后聲明該提交作為您原始提交的替換。

現在,如果您執行git log ,您會看到事情已經如您所願:分支從newroot開始。

但是,請注意git replace實際上並不會修改歷史記錄——它也不會傳播到您的存儲庫之外。 它只是將本地重定向添加到您的存儲庫,從一個對象到另一個對象。 這意味着沒有其他人看到這種替換的效果——只有你。

這就是為什么filter-branch步驟是必要的。 使用git replace您可以創建一個精確的副本,其中包含為根提交調整的父提交; git filter-branch然后對所有以下提交重復此過程。 那是歷史實際上被重寫的地方,以便您可以分享它。

合並亞里士多德 Pagaltzis 和 Uwe Kleine-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

(只是把所有東西放在一個地方)

我喜歡亞里士多德的回答。 但是發現對於大型存儲庫(> 5000 次提交),filter-branch 比 rebase 工作得更好,原因有幾個: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”選項意味着將重寫標簽以指向新創建的提交。

我認為使用git replacegit filter-branch是比使用git rebase更好的解決方案:

  • 更好的性能
  • 更容易且風險更小(您可以在每個步驟中驗證您的結果並撤消您所做的......)
  • 與多個分支一起工作,保證結果

其背后的想法是:

  1. 在過去創建一個新的空提交
  2. 用一個完全相似的提交替換舊的根提交,除了新的根提交被添加為父提交
  3. 驗證一切是否符合預期並運行git filter-branch
  4. 再次驗證一切正常並清理不再需要的 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/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

現在,一切都應該完成;)

要在存儲庫的開頭添加一個空提交,如果您忘記在“git init”之后立即創建一個空提交:

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

我成功地使用了亞里士多德和肯特的答案:

# 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
# pre- git 2.28...
git checkout master
# or git 2.28 and later...
git checkout $(git config --get init.defaultBranch)
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

除了標簽之外,這還將重寫所有分支(不僅僅是masterinit.defaultBranch )。

我很興奮並為這個漂亮的腳本寫了一個“冪等”版本……它總是會插入相同的空提交,如果你運行它兩次,它不會每次都改變你的提交哈希。 所以,這是我對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

是否值得額外的復雜性? 也許不是,但我會用這個。

這也應該允許對 repo 的多個克隆副本執行此操作,並最終得到相同的結果,因此它們仍然兼容......測試......是的,它確實有效,但還需要刪除和添加你的再次遠程,例如:

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

git rebase --root --onto $emptyrootcommit

應該很容易做到這一點

要切換根提交:

首先,首先創建您想要的提交。

其次,使用以下命令切換提交的順序:

git rebase -i --root

一個編輯器將與提交一起出現,直到根提交,例如:

選擇 1234 舊根消息

pick 0294 中間的一個提交

選擇要放在根目錄的 5678 提交

然后,您可以將所需的提交放在第一行,方法是將其放在第一行。 在示例中:

選擇要放在根目錄的 5678 提交

選擇 1234 舊根消息

pick 0294 中間的一個提交

退出編輯器,提交順序將發生變化。

PS:要更改 git 使用的編輯器,請運行:

git config --global core.editor name_of_the_editor_program_you_want_to_use

這是我的bash腳本,基於Kent的改進答案:

  • 完成后,它會檢查原始分支,而不僅僅是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"

好吧,這就是我想出的:

# 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

結合最新和最偉大的。 沒有副作用,沒有沖突,保留標簽。

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 可能會搞砸,除非其他分支也得到修復。

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

請注意,您的 repo 不應包含等待提交的本地修改。
注意git checkout --orphan可以在新版本的 git 上工作,我猜。
請注意,大多數時候git status會提供有用的提示。

啟動一個新的存儲庫。

將您的日期設置回您想要的開始日期。

以您希望的方式做所有事情,調整系統時間以反映您希望以這種方式完成的時間。 根據需要從現有存儲庫中提取文件,以避免大量不必要的輸入。

到了今天,交換存儲庫就完成了。

如果你只是瘋狂(成熟)但相當聰明(可能,因為你必須有一定的聰明才智才能想出這樣的瘋狂想法),你會編寫這個過程。

當您決定希望過去一周后以其他方式發生時,這也會變得更好。

我知道這篇文章很舊,但這個頁面是谷歌搜索“插入提交 git”時的第一個頁面。

為什么要把簡單的事情復雜化?

你有ABC,你想要ABZC。

  1. git rebase -i trunk (或B之前的任何東西)
  2. 在 B 行更改選擇以編輯
  3. 進行更改: git add ..
  4. git commit ( git commit --amend將編輯 B 而不是創建 Z)

[您可以在此處進行任意數量的git commit以插入更多提交。 當然,第 5 步你可能會遇到麻煩,但是用 git 解決合並沖突是你應該具備的技能。 如果沒有,請練習!]

  1. git rebase --continue

很簡單,不是嗎?

如果您了解git rebase ,添加“根”提交應該不是問題。

玩得開心!

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM