Git에서 루트 커밋 앞에 커밋을 삽입 하시겠습니까?


231

git 저장소에서 처음 두 커밋스쿼시하는 방법에 대해 이전에 요청했습니다 .

해결책은 다소 흥미롭고 실제로 git의 다른 것들만큼 마음이 뒤 틀리지는 않지만 프로젝트 개발 과정에서 여러 번 절차를 반복 해야하는 경우 여전히 끔찍한 상처입니다.

따라서 한 번만 고통을 겪고 나서 표준 대화 형 rebase를 영원히 사용할 수 있습니다.

그러므로 내가하고 싶은 것은 첫 번째 목적을 위해서만 존재하는 빈 초기 커밋을 갖는 것입니다. 코드도없고 아무것도 없습니다. 공간을 확보하여 재베이스의 기반이 될 수 있습니다.

내 질문은 기존 저장소를 가지고 첫 번째 빈 커밋 앞에 새 빈 커밋을 삽입하고 다른 모든 사람을 앞으로 옮기는 방법은 무엇입니까?


3
;) 어쨌든 대답이 필요하다고 생각합니다. 나는 역사를 강박 적으로 편집함으로써 미치게 될 수있는 많은 방법들을 탐구하고있다. 공유 저장소가 아니라 걱정하지 마십시오.
kch

11
강박적이고 미친 역사 편집기에서 다른 편집기로 질문을 게시 해 주셔서 감사합니다! ; D
마르코

10
@kch의 방어에서 완벽하게 정당한 이유 중 하나는 내가 찾은 이유입니다. 레포에서 캡처되지 않은 이전 버전의 스냅 샷 추가.
Old McStopher

4
또 다른 합당한 이유가 있습니다! 첫 번째 커밋으로 리베이스하고 리포지토리의 초기 커밋에 추가 된 이진 블로 트를 제거하기 위해 첫 번째 커밋을 추가하기 (:
pospi

답변:


314

이를 달성하기위한 2 단계가 있습니다.

  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전환 할 수없는 매우 오래된 Git 버전 checkout에서는 첫 번째 줄을 다음과 같이 바꿔야합니다.

git symbolic-ref HEAD refs/heads/newroot

2.이 빈 커밋에서 시작하도록 히스토리를 다시 작성하십시오.

여기에는 rebasing 또는 clean history rewrite의 두 가지 옵션이 있습니다.

리베이스

git rebase --onto newroot --root master

이것은 단순성의 장점이 있습니다. 그러나 분기에서 마지막 커밋마다 커미터 이름과 날짜도 업데이트합니다.

또한 일부 사례 기록을 사용하면 아무것도 포함하지 않은 커밋에 기반하고 있음에도 불구하고 병합 충돌로 인해 실패 할 수도 있습니다.

역사 재 작성

보다 깔끔한 접근 방식은 분기를 다시 작성하는 것입니다. with와 달리 git rebase, 분기가 시작하는 커밋을 찾아야합니다.

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

재 작성은 분명히 두 번째 단계에서 발생합니다. 설명이 필요한 첫 번째 단계입니다. 뭐git replace 않는 것은 당신이 교체하려는 개체에 대한 참조를 볼 때마다, 힘내 대신 그 객체의 교체 보는 것이 힘내을 알 수있다.

--graft스위치를 사용하면 평소와 약간 다른 것을 알려줍니다. 대체 객체가 아직 없다고 말하고 있지만 교체 의 상위 커밋이 나열된 것 (즉, 커밋 )이 <currentroot>아닌 것을 제외하고 는 커밋 객체를 정확한 사본으로 교체 newroot하려고합니다. ). 그런 다음 git replace이 커밋을 생성 한 다음 해당 커밋을 원래 커밋의 대체로 선언합니다.

이제를 수행 git log하면 원하는대로 항목이 이미 표시됩니다 newroot. 분기는에서 시작합니다 .

그러나 git replace 실제로 히스토리를 수정하지는 않으며 저장소에서 전파되지도 않습니다. 단지 한 객체에서 다른 객체로 리포지토리에 로컬 리디렉션을 추가하기 만합니다. 이것이 의미하는 바는 아무도이 교체의 효과를 볼 수 없다는 것입니다.

그것이 filter-branch단계가 필요한 이유 입니다. 로 git replace루트에 대한 조정 부모 커밋과 정확한 복사본을 만들 커밋 git filter-branch그런 다음 모든 커밋에 대해서도이 프로세스를 반복합니다. 그것은 당신이 그것을 공유 할 수 있도록 실제로 역사가 다시 쓰여지는 곳입니다.


1
--onto newroot옵션은 중복입니다. 전달한 newroot인수가 업스트림 인수-와 동일 하므로이를 사용하지 않고 수행 할 수 있습니다 newroot.
wilhelmtell

7
배관 명령 대신 도자기를 사용하지 않는 이유는 무엇입니까? 나는 대체 할 것 자식 상징적-심판 HEAD의 심판을 / 헤드 / newroot자식 체크 아웃 --orphan newroot
albfan

4
@ nenopera :이 답변은 git-checkout그 스위치를 사용 하기 전에 작성되었으므로 . 포인터 덕분에 그 접근법을 먼저 언급하도록 업데이트했습니다.
아리스토텔레스 Pagaltzis

1
newroot가 비어 있지 않으면 git rebase --merge -s recursive -X theirs --onto newroot --root master모든 충돌을 자동으로 해결 하는 데 사용 하십시오 ( 답변 참조 ). @AlexanderKuzin
사용자

1
@Geremia 마지막 커밋 만 수정할 수 있으므로 리포지토리에 루트 커밋 만 포함되어 있으면 작동 할 수 있습니다. 그렇지 않으면 수정 된 루트 커밋 위에 리포지토리의 다른 모든 커밋을 리베이스해야합니다. 그러나 그럼에도 불구하고 주제는 루트 커밋을 변경하지 않고 기존 루트 앞에 다른 루트 커밋을 삽입하려고 함을 나타냅니다.
사용자

30

Aristotle 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

(한곳에 모든 것을 넣는 것만으로)


이것은 우수하다. 이것이 git rebase -i --root가 내부적으로 수행 한 것일 수 있다면 좋을 것입니다.
aredridel

네, 그렇지 않다는 사실에 놀랐습니다.
Antony Hatchkins

git rebase newroot master오류 때문에 rebase 명령을 (으)로 변경해야했습니다 .
marbel82

@ antony-hatchkins 감사합니다. 나는 기존의 git repo를 가지고 있으며 (여기서 가지 않는 여러 가지 이유로) NON-EMPTY git commit을 첫 번째 커밋으로 추가하려고합니다. 그래서 git commit --allow-empty -m 'initial'을 git add로 대체했습니다.; git commit -m "초기 라 라벨 커밋"; 자식 푸시; 그리고이 rebase 단계 : git rebase --onto newroot --root master는 TON의 병합 충돌로 실패합니다. 어떤 충고? : ((
kp123

@ kp123 빈 커밋 시도 :)
Antony Hatchkins

12

나는 아리스토텔레스의 대답을 좋아한다. 그러나 대규모 저장소 (> 5000 커밋)의 경우 필터 브랜치는 여러 가지 이유로 rebase보다 더 잘 작동합니다 .1) 더 빠릅니다 .2) 병합 충돌이있을 때 사람의 개입이 필요하지 않습니다. 3) 태그를 다시 작성하여 보존 할 수 있습니다. filter-branch는 각 커밋의 내용에 대해 의문의 여지가 없으므로 작동합니다.이 'rebase'이전과 정확히 동일합니다.

내 단계는 다음과 같습니다

# 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

다른 솔루션과 비교할 때 해시를 변경하지만 전체 기록은 그대로 유지됩니다. 감사합니다!
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

이것은 나를 위해 일했습니다. 또한 오래된 타임 스탬프 timedatectl set-time '2017-01-01 00:00:00'를 제공 newroot했습니다.
chrm

4

git rebase --root --onto $emptyrootcommit

트릭을 쉽게해야합니다


3
$emptyrootcommit아무것도 확장하지 않는 쉘 변수입니까?
Flimm

@Flimm : $ emptyrootcommit은 원래 포스터가 이미 가지고있는 빈 커밋의 sha1입니다.
Uwe Kleine-König

4

내가 사용하는 것을 생각 git replace하고하는 것은 git filter-branch를 사용하는 것보다 더 나은 솔루션입니다 git rebase:

  • 더 나은 성능
  • 쉽고 덜 위험합니다 (각 단계에서 결과를 확인하고 수행 한 작업을 취소 할 수 있습니다 ...)
  • 보장 된 결과로 여러 가지와 잘 작동

그 뒤에 아이디어는 :

  • 과거에 새로운 빈 커밋 만들기
  • 새 루트 커밋이 부모로 추가된다는 점을 제외하고는 이전 루트 커밋을 정확히 비슷한 커밋으로 교체하십시오.
  • 모든 것이 예상대로 실행되고 실행되는지 확인 git filter-branch
  • 다시 한 번 모든 것이 정상인지 확인하고 더 이상 필요없는 자식 파일을 정리하십시오.

다음은 두 가지 첫 단계에 대한 스크립트입니다.

#!/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

이제 이전 내역과 새 내역이 표시 filter-branch됩니다 (자세한 내용 은 도움말 참조 ). 2를 비교하고 모두 정상인지 다시 확인할 수 있습니다. 만족하면 더 이상 필요없는 파일을 삭제하십시오.

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

master지점으로 돌아가 임시 지점을 삭제할 수 있습니다 .

git checkout master
git branch -D new-root

이제 모든 것이 완료되어야합니다.)


3

나는 흥분 하고이 멋진 스크립트의 'idempotent'버전을 썼습니다 ... 항상 동일한 빈 커밋을 삽입하고 두 번 실행하면 커밋 해시가 변경되지 않습니다. 그래서 여기에 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

"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수동 커밋의 해시를 사용합니다 .
  • 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

루트 커밋을 전환하려면

먼저 원하는 커밋을 만듭니다.

둘째, 다음을 사용하여 커밋 순서를 전환하십시오.

자식 rebase -i-루트

다음과 같이 루트 커밋까지 커밋과 함께 편집기가 나타납니다.

1234 오래된 루트 메시지를 선택

중간에 커밋 0294 선택

루트에 넣고 싶은 5678 커밋을 선택하십시오.

그런 다음 커밋을 첫 번째 줄에 배치하여 원하는 커밋을 먼저 넣을 수 있습니다. 예제에서 :

루트에 넣고 싶은 5678 커밋을 선택하십시오.

1234 오래된 루트 메시지를 선택

중간에 커밋 0294 선택

커밋 순서가 변경 될 편집기를 종료하십시오.

추신 : git이 사용하는 편집기를 변경하려면 다음을 실행하십시오.

git config --global core.editor name_of_the_editor_program_you_want_to_use


1
이제 rebase가 --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).

리포지토리에는 커밋을 기다리는 로컬 수정 사항이 포함되어서는 안됩니다.
참고 git checkout --orphan로 새로운 버전의 git에서 작동합니다.
대부분의 경우 git status유용한 힌트를 제공합니다.


-6

새 저장소를 시작하십시오.

날짜를 원하는 시작 날짜로 다시 설정하십시오.

원하는 방식으로 시스템 시간을 조정하여 원하는 방식으로 모든 작업을 수행하십시오. 불필요한 입력을 피하기 위해 필요에 따라 기존 리포지토리에서 파일을 가져옵니다.

오늘 도착하면 리포지토리를 교체하면 완료됩니다.

당신이 미쳤지 만 (합리적이지만) 합리적으로 지능적이라면 (이와 같이 미친 아이디어를 생각하기 위해서는 일정량의 똑똑이 있어야하기 때문에) 프로세스를 스크립팅합니다.

또한 과거부터 일주일에 다른 방식으로 과거를 원한다고 결정할 때 더 좋을 것입니다.


나는 당신이 시스템 날짜를 엉망으로 만드는 해결책에 대해 나쁜 감정을 가지고 있지만, 당신은 나에게 아이디어를주었습니다. 감사합니다.
kch

-7

이 게시물이 오래되었다는 것을 알고 있지만이 페이지는 Googling "commit git 삽입"의 첫 번째 페이지입니다.

간단한 일을 복잡하게 만드는 이유는 무엇입니까?

ABC가 있고 ABZC를 원합니다.

  1. git rebase -i trunk (또는 B 이전의 무엇이든)
  2. B 라인에서 편집 할 픽 변경
  3. 변경하십시오. git add ..
  4. git commit( git commit --amendB를 편집하고 Z를 만들지 않음)

[ git commit더 많은 커밋을 삽입하기 위해 원하는만큼 만들 수 있습니다 . 물론 5 단계에서 문제가있을 수 있지만 git과의 병합 충돌을 해결하는 것이 좋습니다. 그렇지 않다면 연습하십시오!]

  1. git rebase --continue

간단하지 않습니까?

이해한다면 git rebase '루트'커밋을 추가해도 문제가되지 않습니다.

자식과 함께 즐기세요!


5
이 질문은 ABC에서 ZABC를 원하는 첫 번째 커밋 을 삽입하도록 요청합니다 . git rebase이 작업은 간단 하지 않습니다.
Petr Viktorin 2016 년
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.