티스토리 뷰

728x90
반응형

개념

자신의 레포지토리를 clone해서 서버로 push하는 경우 그 레포지토리는 origin이라고 할 수 있습니다. 하지만 오픈소스 협업 구조에서는 서버에 있는 레포지토리가 항상 2개 이상(fork를 해서)이라고 볼 수 있습니다. 메인 프로젝트의 레포지토리를 upstream이라고 하고, fork한 자신의 레포지토리를 origin이라고 합니다.

example

여기서 자신만 commit을 해서 기여하는 것이 아니라 다른사람도 똑같이 clone 및 fork를 해서, pull request를 요청 할 수 있습니다. 그렇기 때문에 본인이 fork해서 commit까지 한 뒤 merge하기 전, 그 시간동안 다른 개발자가 같은 곳에 commit을 하고, merge가 됐으면 충돌이 발생할 수 있습니다. 이때는 어떻게 대응해야 할까요? 먼저 fetch라는 명령어로 최신 내용을 가지고 오고, rebase라는 명령을 통해서 base(commit으로 차곡 차곡 스택처럼 쌓인 것)를 최신화해야 합니다.

example

꼭 fetch, rebase를 써야만 하는 것은 아닙니다. pull(fetch + merge)를 사용할수도 있습니다. rebase는 좀 더 깔끔하게 commit을 관리할 수 있습니다.

git fetch와 rebase를 명령어로 알아보겠습니다. master 브랜치로 진행한다고 생각하겠습니다.

$ git remote add upstream (팀 프로젝트, 오픈소스 프로젝트 url)
$ git remote -v 
# origin, upstream 로컬과 서버 총 4개 확인

$ git fetch upstream master
$ git rebase upstream/master

먼저 git remote add upstream으로 upstream 레포지토리를 등록합니다. git fetch upstream master는 upstream 레포지토리의 master 브랜치의 최신 내용을 가지고 오게 됩니다. 다음으로 git rebase upstream/master에서 upstream/master는 브랜치명입니다. git fetch upstream master를 했을 때 내부적으로 생성되는 브랜치 입니다. 이 브랜치에 최신 base들이 들어있습니다. rebase라는 작업이 일어났을 때 내부적으로는 3가지의 과정이 단계적으로 일어나게 됩니다.

  1. 자신이 한 commit을 rewind(되감기)합니다.
  2. git fetch로 가지고 온 최신 내용을 올려서 합칩니다.
  3. 자신의 commit을 그 위로 올립니다.

example example example

대부분의 경우 rebase가 잘 되겠지만 간혹 rebase가 되지 않는 경우도 있습니다. rebase의 2번째 단계에서 오픈소스의 최신 commit들을 올릴 때인데 이 때 commit 1, 2, 3 즉, 최신 이력 이전의 commit 부분이 수정되었을 때 rebase가 드물게 오류가 날수도 있습니다. 또한, 3단계에서 본인이 commit한 것을 올리는 과정에서 commit A, B와 commit C, D가 같은 부분을 수정했을 때 충돌이 날 수 있습니다. 3단계에서의 충돌은 수작업으로 수정을 해줘야 됩니다.

아래에서 rebase와 관련된 실습을 해보겠습니다.



과거의 commit으로 가서, commit 넣기

어떤 레포지토리에서 여러 commit 내역이 있다고 했을 때, 특정 commit으로 되돌아가는 실습을 해보겠습니다. 레포지토리는 git-traning이라는 레포지토리를 사용합니다. 레포지토리를 클론하고 안으로 들어가겠습니다.

$ git clone https://github.com/taeung/git-training
$ cd git-training

git log --oneline을 하게되면 지금까지의 commit들이 한줄로 보기좋게 뜨게 됩니다. 맨처음이 최신, 마지막이 가장 오래된 commit입니다. 그리고 git log --oneline | nl을 하게 되면, commit이 총 몇개인지 알 수 있습니다.

$ git log --oneline
2504a40 (HEAD -> master, origin/master, origin/HEAD) Add v3 PDF
32b69ec New version git-training
18de302 test git-pull-request
f3be23d packing knapsack: Implement code to solve this question
b2218e5 packing knapsack: Add test script generator
4c744f0 packing knapsack: Add test script
1863ec4 packing knapsack: Input & Output
3102e80 packing knapsack: Rename packing_knapsack to pack_knapsack
91f2d24 packing knapsack: Change basic code
f9b87bb packing knapsack: Basic code solving this question
864cf0f Add knapsack problem PDF
73e7acf Add README file

$ git log --oneline | nl
     1  2504a40 Add v3 PDF
     2  32b69ec New version git-training
     3  18de302 test git-pull-request
     4  f3be23d packing knapsack: Implement code to solve this question
     5  b2218e5 packing knapsack: Add test script generator
     6  4c744f0 packing knapsack: Add test script
     7  1863ec4 packing knapsack: Input & Output
     8  3102e80 packing knapsack: Rename packing_knapsack to pack_knapsack
     9  91f2d24 packing knapsack: Change basic code
    10  f9b87bb packing knapsack: Basic code solving this question
    11  864cf0f Add knapsack problem PDF
    12  73e7acf Add README file

다음 git rebase -i --root 명령어를 칩니다. -i는 interactive로 대화형으로 진행한다는 의미이고, --root 옵션을 주어야지 최초의 commit부터 rebase 할 수 있습니다. 명령어를 치게되면 수정할 수 있는 vi화면이 나오게 되는데 여기서는 위쪽 commit이 최초의 commit이고, 맨 아래의 commit이 가장 최근의 commit입니다. 여기서 2번째를 edit으로 바꾸고 저장하고 나와서 git log --oneline으로 확인하게 되면 2번째 commit을 했던 시점으로 돌아가는 것을 볼 수 있습니다.

$ git rebase -i --root


pick 73e7acf Add README file
edit 864cf0f Add knapsack problem PDF
pick f9b87bb packing knapsack: Basic code solving this question
pick 91f2d24 packing knapsack: Change basic code
pick 3102e80 packing knapsack: Rename packing_knapsack to pack_knapsack
pick 1863ec4 packing knapsack: Input & Output
pick 4c744f0 packing knapsack: Add test script
pick b2218e5 packing knapsack: Add test script generator
pick f3be23d packing knapsack: Implement code to solve this question
pick 18de302 test git-pull-request
pick 32b69ec New version git-training
pick 2504a40 Add v3 PDF

# Rebase 2504a40 onto 2504a40 (12 commands)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup <commit> = like "squash", but discard this commit's log message
# x, exec <command> = run command (the rest of the line) using shell

$ git log --oneline
864cf0f (HEAD) Add knapsack problem PDF
73e7acf Add README file

다시 원상복구하려면 git rebase --continue를 하면됩니다.

$ git rebase --continue
Successfully rebased and updated refs/heads/master.

$ git log --oneline
2504a40 (HEAD -> master, origin/master, origin/HEAD) Add v3 PDF
32b69ec New version git-training
18de302 test git-pull-request
f3be23d packing knapsack: Implement code to solve this question
b2218e5 packing knapsack: Add test script generator
4c744f0 packing knapsack: Add test script
1863ec4 packing knapsack: Input & Output
3102e80 packing knapsack: Rename packing_knapsack to pack_knapsack
91f2d24 packing knapsack: Change basic code
f9b87bb packing knapsack: Basic code solving this question
864cf0f Add knapsack problem PDF
73e7acf Add README file

이와 같은 방법으로 git rebase -i --root로 여러 개의 commit을 수정하여 되감은 뒤에, git rebase --continue를 여러번해서 하나 하나씩 되감은 것을 풀수도 있습니다.

다시 git rebase -i --root 명령어를 사용해서 최초 두번째 commit으로 간뒤에 거기서 새로운 commit을 작성해보겠습니다.

$ git rebase -i --root
# 두번째 edit으로 수정하고, 저장 후 빠져나옵니다.

$ touch A.c && git add A.c && git commit -m "Add A.c"
$ touch B.c && git add B.c && git commit -m "Add B.c"
$ touch C.c && git add C.c && git commit -m "Add C.c"

$ git log --oneline
e60dd57 (HEAD) Add C.c
def97d5 Add B.c
fd20564 Add A.c
864cf0f Add knapsack problem PDF
73e7acf Add README file

$ git rebase --continue
$ git log --oneline 
d333c77 (HEAD -> master) Add v3 PDF
65a47a1 New version git-training
c2934e5 test git-pull-request
8260769 packing knapsack: Implement code to solve this question
074657c packing knapsack: Add test script generator
6d7bf08 packing knapsack: Add test script
cccb1e9 packing knapsack: Input & Output
334178a packing knapsack: Rename packing_knapsack to pack_knapsack
999c9e8 packing knapsack: Change basic code
5de490b packing knapsack: Basic code solving this question
e60dd57 Add C.c
def97d5 Add B.c
fd20564 Add A.c
864cf0f Add knapsack problem PDF
73e7acf Add README file

이런식으로 rebase를 활용해서 중간에 새로운 commit을 넣을 수 있습니다.



이전 commit들 합치기

위에서 최초 2번째 commit으로 간 뒤에 commit 3개를 넣었습니다. 여기서 먼저 Add B.c, Add C.c commit을 합친 뒤에 합친 commit을 Add A.c와 합쳐보겠습니다. 먼저 git rebase -i -root로 이전 commit으로 돌아가야합니다.

$ git rebase -i --root 
pick 73e7acf Add README file
pick 864cf0f Add knapsack problem PDF
pick fd20564 Add A.c
pick def97d5 Add B.c
edit e60dd57 Add C.c
pick 5de490b packing knapsack: Basic code solving this question
pick 999c9e8 packing knapsack: Change basic code
pick 334178a packing knapsack: Rename packing_knapsack to pack_knapsack
pick cccb1e9 packing knapsack: Input & Output
pick 6d7bf08 packing knapsack: Add test script
pick 074657c packing knapsack: Add test script generator
pick 8260769 packing knapsack: Implement code to solve this question
pick c2934e5 test git-pull-request
pick 65a47a1 New version git-training
pick d333c77 Add v3 PDF

# Rebase d333c77 onto 8260769 (15 commands)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending

여기서 Add B.c에 edit을 써주는 것이 아니고, Add C.c에 edit을 해줘야 합니다. Add B.c, Add C.c commit을 수정하려고 하는데 Add B.c로 가버리면 한칸 더 오래전 commit으로 간 것이기 때문에 Add C.c commit을 edit으로 바꾸고 저장 후 나옵니다.

다음 git reset --soft HEAD~1 명령어를 치면 됩니다. reset 명령어는 commit의 이력을 예전상태로 되돌릴 수 있습니다. 거기에 --soft 옵션을 주게 되면 commit의 파일이나 수정된 사항들은 삭제되지 않고, 유지된 상태로 git add 상태에 있게 됩니다. 여기서 --hard 옵션을 주게되면 파일 및 수정된 사항들도 모두 삭제를 하게 됩니다.

HEAD는 현재 체크아웃된 commit(작업중인 commit)을 가리킵니다. HEAD는 항상 작업트리의 가장 최근 commit을 가리킨다고 볼 수 있습니다. 작업트리에 변화를 주는 git 명령어들은 대부분 HEAD를 변경하는 것으로 시작합니다.

~는 틸트 연산자로 commit트리에서 위로 여러 단계를 올라가고 싶을 때 사용합니다. 틸트 연산자 뒤에 올라가고 싶은 부모의 수를 써주면 그 수만큼 올라가게 됩니다.

$ git reset --soft HEAD~1

$ git status
interactive rebase in progress; onto 9492049
Last commands done (5 commands done):
   pick def97d5 Add B.c
   edit e60dd57 Add C.c
  (see more in file .git/rebase-merge/done)
Next commands to do (10 remaining commands):
   pick 5de490b packing knapsack: Basic code solving this question
   pick 999c9e8 packing knapsack: Change basic code
  (use "git rebase --edit-todo" to view and edit)
You are currently editing a commit while rebasing branch 'master' on '9492049'.
  (use "git commit --amend" to amend the current commit)
  (use "git rebase --continue" once you are satisfied with your changes)

Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        new file:   C.c

$ git log --oneline
def97d5 (HEAD) Add B.c
864cf0f Add knapsack problem PDF
73e7acf Add README file

new file: C.c가 있는 것을 알 수 있고(삭제되지 않았고, git add가 적용 되어있음), git log --oneline을 하게 되면 HEAD가 commit Add B.c에 있는 것을 알 수 있습니다.

다음 git commit --amend로 B.c C.c를 합쳤다는 commit 메시지를 수정할수도 있습니다. 수정한뒤에 git rebase --continue로 원래의 commit으로 빠져 나오면 됩니다.

$ git commit --amend

Add B.c + C.c

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# Date:      Mon Aug 2 13:53:19 2021 +0900
#
# interactive rebase in progress; onto 9492049
...
...

$ git rebase --continue
$ git log --oneline
d097d60 (HEAD -> master) Add v3 PDF
65adf14 New version git-training
5a1270d test git-pull-request
5691886 packing knapsack: Implement code to solve this question
b61c113 packing knapsack: Add test script generator
2a09db1 packing knapsack: Add test script
17200fb packing knapsack: Input & Output
73e6722 packing knapsack: Rename packing_knapsack to pack_knapsack
0c695d1 packing knapsack: Change basic code
d475b29 packing knapsack: Basic code solving this question
2b3efd0 Add B.c + C.c
fd20564 Add A.c
864cf0f Add knapsack problem PDF
73e7acf Add README file

이젠 Add B.c + C.c랑 Add A.c commit을 합쳐봅시다.

$ git rebase -i --root
# B.c + C.c에 edit

$ git reset --soft HEAD~1

$ git commit --amend 
# 적절히 commit 메시지 수정

$ git rebase --continue

$ git log --oneline
3391c11 (HEAD -> master) Add v3 PDF
86f27f2 New version git-training
1f5a22e test git-pull-request
afceb24 packing knapsack: Implement code to solve this question
81a9817 packing knapsack: Add test script generator
035a38b packing knapsack: Add test script
aee21de packing knapsack: Input & Output
257941e packing knapsack: Rename packing_knapsack to pack_knapsack
0ff1536 packing knapsack: Change basic code
5d23562 packing knapsack: Basic code solving this question
e5e2d2a Add A.c, B.c, C.c
864cf0f Add knapsack problem PDF
73e7acf Add README file

git reset --hard 옵션을 통해 아에 이전 commit을 삭제할수도 있습니다.

$ git rebase -i --root
# 없애고 싶은 commit edit
$ git reset --hard HEAD~1
$ git rebase --continue



git blame 명령어로 commit ID 추적하기

이번에는 node 프로젝트를 클론하고, blame 명령어를 실습 해보겠습니다.

$ git clone https://github.com/nodejs/node.git
$ cd node
$ git blame src/node.cc

git blame 명령어로 누가 언제 수정을 했고, 수정한 라인이 몇번째 라인인지 알 수 있습니다. git blame을 활용하여 작업을 하면 소스리딩에 유리해질 수 있습니다.

src폴더에 node_http_parser.cc라는 소스코드가 있는데 여기서 Parser 클래스의 최초 commit을 찾아보도록 하겠습니다.

git blame src/node_http_parser.cc
# /를 누른뒤에 class Parser를 검색(엔터)

193줄에 7c4b의 commit이 있는 것을 확인할 수 있습니다. 이 commit을 복사한 후에 git show 명령어를 통해서 class Parser를 검색하게 되면 최초의 commit이 아님을 알 수 있습니다.

$ git show 7c4b09b24bb

# /class Parser
-class Parser : public AsyncWrap {
+class Parser : public AsyncWrap, public StreamListener {
  public:
   Parser(Environment* env, Local<Object> wrap, enum http_parser_type type)
       : AsyncWrap(env, wrap, AsyncWrap::PROVIDER_HTTPPARSER),
@@ -494,14 +494,7 @@ class Parser : public AsyncWrap {
     Local<External> stream_obj = args[0].As<External>();
     StreamBase* stream = static_cast<StreamBase*>(stream_obj->Value());
     CHECK_NE(stream, nullptr);
...
...

git blame src/node_http_parser.cc로 가서 class Parser 클래스의 끝맺음 중괄호들을 보게 되면(끝맺음 괄호 하나만 있는 라인들), 히스토리가 오래 된 것을 알 수 있습니다. 끝중괄호들은 수정에 포함되지 않을 확률이 매우 높아서 이런 중괄호들의 commit 번호를 사용한다면 최초의 commit이 될 가능성이 매우 높습니다. 따라서 200번째 줄의 끝맺음 중괄호 42ee16978e8 commit이 최초의 commit인지 보겠습니다.

$ git blame src/node_http_parser.cc
# / 누른후 class Parser 검색(엔터)
# 200번째 줄의 끝중괄호 } commit번호 확인, 복사

$ git show 42ee16978e8
# / 누른후 class Parser 검색(엔터). 

이 commit이 최초의 commit임을 알 수 있습니다. 조금 더 논리적인 방식으로 확인할수도 있습니다.

$ git log --oneline --reverse -- src/node_http_parser.cc
$ git log -p --reverse --src/node_http_parser.cc
$ git show 42ee16978e8

--reverse는 commit을 거꾸로 출력하여 가장 최초 commit부터 출력합니다. 그리고 -- 뒤에 소스파일을 지정해서 그 소스코드의 commit만 볼 수 있습니다. 그 밑에 -p 옵션은 commit들과 소스코드까지 같이 볼 수 있는 명령어입니다.

이런 경우도 있습니다. 위에서 적용해본 명령어대로 src/node.cc에 적용시켜 보면, git log --oneline --reverse -- src/node.cc | haed -1 명령어로 가장 최초의 commit을 확인할 수 있는데, src폴더 안에서 node.cc의 최초 commit은 맞지만, 소스 자체의 최초 commit은 아닙니다.

$ git log --oneline --reverse -- src/node.cc | head -1
1a126ed11c use the WAF build system

왜냐하면, 예전에는 src폴더가 없고 node.cc가 다른 디렉토리나 루트에 있을수도 있기 때문입니다. 그래서 node.cc 자체의 최초의 commit은 아니라고 할 수 있습니다. 그 commit의 한칸 부모commit으로 가보면 node.cc 자체가 예전에는 다른 위치에 있었고, node.cc 자체의 최초 commit을 찾을 수 있습니다.

$ git reset --hard 1a126ed11c~1
# 레포 전체를 해당 commit의 한칸 위로 더 과거로 돌아감

$ ls
# node.cc가 루트폴더에 있고, src폴더가 없음

$ git show -q
# 2009년쯤임을 알 수 있음



오픈 소스에 기여할 때 rebase가 필요한 상황 실습

어떤 organization 레포가 하나 있고 여기에 README.md가 있는데 팀원 여려명이 동시에 fork를 떠서 readme 첫줄에 자신의 이름을 적은뒤에 pull request를 날렸고, pull request를 날린 팀원들 중 한명만 merge가 되었다고 가정해봅시다. 그럼 나머지 팀원들의 pull request는 충돌이 날 것입니다. 이 때 rebase 명령어 등을 사용해서 충돌을 해결할 수 있습니다.

우선 upstream 원래 레포의 정보를 등록해야 합니다.

$ git remote add upstream (팀 프로젝트, 오픈소스 프로젝트 url)
$ git remote -v 
# origin, upstream 로컬과 서버 총 4개 확인

git fetch 명령어로 팀 프로젝트의 최신 commit들을 다 가지고 옵니다. 먼저 merge가 된 다른 동료들의 commit 정보를 가지고 온다고 할 수 있습니다.

$ git fetch upstream master
# upstream(팀 레포, 오픈소스 원래 레포)의 master브랜치 최신 내용들을 다 가지고 온다. 

이렇게 되면 자동으로 upstream/master라는 브랜치가 생기게 되고, 다음 git rebase upstream/master 명령어를 통해 rebase를 하면 되는데, 이 떄 같은 README.md를 수정했기 때문에 충돌나게 됩니다. 글의 맨 위쪽 설명의 rebase 3단계에서 문제가 발생했다고 볼 수 있습니다. 이 때는 직접 readme를 열고 수정을 한 뒤에 rebase, push를 하면 merge를 할 수 있게 됩니다.

$ git rebase upstream/master

$ git diff
# 충돌 상황 보기

$ vi README.md
# 관련 없는 >>> <<< 이런 것들 다 지우고 수정

$ git add README.md
$ git rebase --continue

$ git push origin -f master



참고

git bash 에디터를 nano로 변경

$ git config --global core.editor nano

rebase 작업 중단

$ git rebase --abort

로컬에서 진행했던 것을 모두 초기화하고, 원격이랑 동기화

$ git reset --hard origin/master
728x90
반응형
댓글
반응형
250x250
글 보관함
최근에 달린 댓글
«   2024/03   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31
Total
Today
Yesterday
링크