Git 버전관리 정리
Git 버전관리 정리
[추가 글] 좋은 git commit 메시지를 위한 영어 사전
Git (혼자서 작업하기)
1. Git 기초
커밋(commit)
- 게임의
세이브
에 해당하는 행동- 커밋을 해야 세이브가 된다.
- 다시말해 언제든지 커밋한 시점으로 돌아 갈 수 있다.
- 커밋을 하려면
저장을 원하는 파일들을 묶어서
커밋 명령을 수행하면 된다.
커밋 주의사항
- 반드시 한 번에 하나의 논리적 작업만을 커밋한다.
- 커밋 메시지는 잘 적어야 한다.
커밋 메시지 작성법
- 첫 줄에 간단하지만 명확한 내용을 쓴다.
- 한 줄 비우고
- 자세한 내용을 적는다.
스테이지에 올린다(add)
- commit에서
저장을 원하는 파일을 묶는
작업이 필요하다고 했다. - 이 작업을
스테이지에 파일을 올린다
라고 한다.
github에 업로드(push)
- 커밋을 하면 현재 작업 내용의 데이터가 내 컴퓨터(로컬)에 저장된다.
- 이 것을 Github에 업로드하는 걸 push라고 한다.
(가장 간단한) 일련의 절차
- Github에서 repository 생성
- 해당 repo URL 복사
- Git client(sourcetree, GitKraken, terminal …) setup
- 설치한 Git client App에서 복사한 URL Clone
- Clone : 원격 저장소의 내용을 내 컴퓨터(로컬)로 복사
- 작업 진행
- git add
- add : 로컬에서 작업한 파일들을 스테이지에 추가
- git commit
- commit : 스테이지에 올라온 파일들을 가지고 내 로컬에 저장(세이브)
- git push
- push : 커밋들을 원격 저장소에 업로드
2. 스테이지에 올리지 않은(add 명령 하기 전) 변경사항 취소하기
chekout
- checkout 명령을 통하여 마지막 커밋으로 되돌아 갈 수 있다.
$ git checkout .
: repository 내 모든 수정 되돌리기$ git checkout {dir}
: 특정 디렉토리 아래 모든 수정 되돌리기$ git checkout {filename}
: 특정 파일의 수정 되돌리기
- sourceTree의
코드뭉치 버리기
(마지막 커밋으로 되돌아 가고 싶을때 사용) 기능을 사용하면 변경사항을 되돌릴 수 있다.
3. 브랜치의 개념
- 이미 돌아가고 있는 프로그램에서 기능을 바꾸고 싶은 일이 생길 수 있다. 그럴 때 어떻게 해야 하나? 보통 초보 개발자들은 주석을 활용한다. 돌아가고 있는 부분을 삭제하면 아까우니까 주석 처리하고 개발한다.
- 이렇게 시간이 지나면서 코드가 엉망진창이 되버리는데 이런 상황을 막기 위해 브랜치를 사용한다.
- 브랜치란 : 가상의 작업 환경
- 브랜치를 생성하면 기존의 마스터 브랜치의 내용은 그대로 보존하면서 새로운 작업 환경을 생성한다.(기존 내용을 유지한 체 새로운 내용을 추가하고 싶을 때 사용 = 특정 브랜치(혹은 커밋) 으로 돌아가고 싶을 때)
- 소스트리로 예를 들면 해당 커밋에서 우클릭 -> 브랜치 -> 새 브랜치를 통해 브랜치를 생성한다. =
checkout
- 소스트리에서는 해당 브랜치 더블 클릭으로 쉽게 체크 아웃 가능.
- 명령어 :
$ git branch [옵션] [브랜치명 A] [브랜치명 B]
- 깃에서는 한 번에 하나의 브랜치에서만 작업 가능하다. 이를 헤드(HEAD) 브랜치(현재 작업 중인 브랜치) 라고한다.
4. 브랜치 병합하기
병합이란?
- 하나의 브랜치를 현재 브랜치와 합치는 것을 병합(merge)라고 한다.
- 현재 브랜치는 헤드(HEAD) 브랜치라고 한다고 했다.
- 예를 들어 헤드 브랜치아 master 이고 여기서 version2 브랜치를 병합하면 version2의 내용이 master에 반영되게 된다.
상황1. 헤드 브랜치에 변경 사항이 없을 경우 : fast forwad
- 합치려는 브랜치가 헤드 브랜치로부터 시작되었다.
- 그 사이 헤드 브랜치에는 전혀 갱신이 없었다.
- 주로 혼자 작업할 때 발생하는 상황
- 단순히 브랜치의 참조만 갱신되는 상황
- git branch 연습 사이트\
$ git commit
$ git barnch version2
$ git checkout version2
$ git commit
$ git commit
$ git checkout master
$ git merge version2
상황2. 가지가 생겨난 경우
- 과거의 커밋으로부터 브랜치를 생성해서 작업을 한 경우
- 새로운 브랜치 작업 이후에 헤드에 다른 새 커밋이 있는 경우
- 여러 브랜치를 동시에 작업하면서 병합을 시도할 경우
$ git commit
$ git brach version2
$ git brach version3
$ git checkout version2
$ git commit
$ git commit
$ git checkout version3
$ git commit
$ git commit
$ git checkout master
$ git merge version3
$ git merge version2
- 충돌 가능성이 있다.(conflict) : 수동 해결
- 충돌 무섭지 않다!
에러 메시지 꼭 확인
- 소스트리의 경우 헤드 브랜치에서 병합할 브랜치의 커밋 메시지 우클릭 -> 병합
5. pull 및 충돌 해결하기
git pull
- 서버의 내용이 최신을 경우 pull 적용
- pull = fetch(원격저장소의 변경사항 가져와서 원격브랜치를 갱신) + merge
안쓰는 브랜치 삭제하기
- 현재 브랜치(HEAD)가 아닌 경우 (이미 Merge가 된 경우) 간단하게 삭제 가능
충돌의 발생 원인
- 자동병합을 실패했을 경우
- 주로 두 커밋이 같은 파일을 편집했을 경우 발생
해결법
- 에디터를 이용한 해결
- 수동으로 수정
- 병합툴을 이용한 해결(diff tool)
- sourceTree를 이용한 해결
- 내 것 또는 저장소 것 선택하기
6. 이전 커밋으로 되돌리기(reset)
- 소스트리의
이 커밋까지 현재 브랜치를 초기화(Reset current branch to this commit)
버튼 git reset --hard [commit hash 값]
에 해당하는 명령으로 커밋을 되돌리기 (soft, mixed는 개별적으로 알아보기)- 원격 저장소에 올려져 있지 않은 이전 커밋은 사라짐
- reset 이후 push는 force 옵션을 선택해야 함
- 원격저장소에 저장된 이전 커밋 사라짐
- 따라서 유지하려면 reset을 한 후 원격저장소의 커밋과 merge를 한 후에 push를 해주면 –force를 사용하지 않아도 된다.
- 원격저장소에 저장된 이전 커밋 사라짐
- push –force 는 소스트리에서 지원하지 않기 때문에 CLI를 이용해야 함
- reset은 Commit이 없어질 가능성이 굉장히 높으므로 권장하지 않음
reset의 장.단점
- 장점: 쉽다.
- 단점: 커밋이 날아간다. push –force 가 필요하다.
7. 이전 커밋으로 되돌리기 (checkout, 브랜치를 만들어서 커밋 되돌리기)
- 혼자서 작업할 시 가장 추천하는 방법
- 설명
- 되돌릴 커밋 대상으로 브랜치 생성
- 체크아웃
- 작업 후 커밋
- master 브랜치에서(master로 checkout) 이전에 작업한 커밋 merge & push
- 작업 완료한 브랜치는 삭제
checkout의 장.단점
- 장점 : 쉽다. reset과는 달리 내용이 사라지지 않는다. (기록이 다 남아 있다.), 강제 push 필요하지 않음
- 단점 : 트리가 지저분해진다.
## 8. 이전 커밋으로 되돌리기 (Revert)
- 커밋을 보존하면서 작업 디렉토리의 내용만 되돌릴 수 있는 방법. (커밋은 남기고 되돌리기)
- 대상 커밋을 HEAD 커밋의 자식으로 새로 생성한다.
- revert 대상 커밋은 사라지지 않는다.
- revert 대상 커밋의 내용을 되돌린
새로운 커밋이 생겨난다.
- 소스트리의
커밋 되돌리기(reverse commit)
$ git commit (C2)
$ git commit (C3)
$ git revert C3 (C2와 같은 C3*가 새로 생김)
### 장.단점
- 장점 : 이전 커밋 기록이 다 남아 있다.
- 단점 : 충돌 날 가능성이 매우 높다. 다소 어렵다.
## 9. Revert를 이용해 여러 커밋 되돌리기
- 최신부터 순서대로 revert를 반복 적용하면 된다.
- 위 그림 설명
- commit 1 -> commit 2 -> commit 3 순으로 커밋이 진행됨.
- 가장 최큰 커밋(HEAD commit)인 commit 3에서 revert 진행(우클릭 -> 커밋 되돌리기) -> commit 2의 내용으로 돌아감. (여기 까지는 8번 이전 커밋으로 되돌리기에 해당)
- 그 다음 이전 커밋(commit 1)으로 되돌리고 싶으면 commit2에서 (우클릭 -> 커밋 되돌리기)를 진행하면 commit 1의 내용으로 돌아간다.
- 참고로 명령어를 사용하면 훨씬 쉽다.
$ git revert HEAD HEAD~1 // 명령어 사용
- $ git revert HEAD (가장 최근 커밋을 되돌려라.)
- HEAD~1 : HEAD의 부모
10. stash를 이용한 작업 내용 저장
- 브랜치 체크아웃시 주의사항
- 브랜치를 만들고 체크아웃을 하려면 현재 작업 디렉토리가 깨끗해야 한다.(
브랜치를 만들려면 마지막 커밋과 현재 작업 디렉토리의 내용이 같아야한다.
) - 그런데 갑작스럽게 체크아웃이 필요하다면?
- 브랜치를 만들고 체크아웃을 하려면 현재 작업 디렉토리가 깨끗해야 한다.(
첫번째 방법, 작업 중인 내용의 임시 저장
- 브랜치 1에서 일단 (임시) 커밋을 한다.
- 브랜치2로 체크아웃하고 볼 일을 본다…
- 다시 브랜치1로 되돌아 온다.
- 1의 작업을 이어서 마무리 짓는다.
커밋 덮어쓰기 (commit --amend)
(소스트리 : 마지막 커밋 정정)를 한다.- (옵션) 필요하다면(이미 push 한 경우) (push –force)를 한다.
(핵심) 두번째 방법, Stash를 이용해서 같은 작업 하기
- Stash를 만든다.
- 이 때 새로운 파일이 있었다면(신규 파일, untracked files) 일단 인덱스에 추가한다.
- 체크아웃한다.
- 되돌아 온다.
- Stash 를 Pop한다!
- 보통 커밋을 새로 생성한다.
- Stash : 다른 브랜치로 체크아웃하기 전에 현재 작업내용을 저장하는 임시 저장소
11. rebase 사용해서 트리 정리하기(히스토리 관리하기)
- Rebase 사용해 보기
- 병합(merge) 처럼 두 브랜치를 합칠 때 사용한다.
- 현재 브랜치가 대상 브랜치 위로 올라간다.
- 소스트리에서는 “재배치”라는 명령이다.
- 현재 브랜치에서 대상 브랜치에 우클릭하고 재배치(Rebase) 실행
- 충돌 시 수정 후 다시 한번 우클릭 재배치(Reabase) -> continue
### 장.단점
- 장점 : 커밋 히스토리가 깔끔하게 정리된다.
- 단점 : 잘못하면 위험하다.
이미 원격 저장소에 올라간 경우 + 협업을 하고 있는 경우 특히 위험하다.
- 깃에서도 원격 저장소에 올라간 경우 rebase를 사용하지 말라고 권고
Links
Git 사용 중 자주 만나는 이슈 정리
###코딩보다 어려운 버전 관리
2019.2.17
깃으로 버전 관리를 하다보면 각종 이슈가 자주 발목을 잡는다. 특히 복잡한 프로젝트의 경우 그 정도가 심한데… 입사 이후 '지금까지 내가 한 건 깃이 아니구나’를 깨닫고 더 공부해 보기로 했다. 그러다보니 매번 비슷한 문제 상황을 마주하게 되는 것 같아서 아래와 같은 케이스들을 정리해보았다.
- 파일 스테이징 취소하기
- 마지막 커밋 취소하기
- 마지막 커밋 메시지 수정하기
- 이미 푸시한 커밋 메시지 수정하기
- 커밋을 과거로 되돌리기
- 푸시한 커밋을 과거로 되돌리기
- 푸시한 파일 삭제하기
- 푸시한 파일 흔적없이 삭제하기
- 원격 저장소에서 업데이트 받아오기
- 병합 커밋없이 풀하기
- 포크한 로컬 저장소를 최신으로 유지하기
- 작업 내용을 백업하고 다른 브랜치 체크아웃하기
- 풀 리퀘스트에서 Squash and Merge된 커밋 제외하기
- 병합 충돌 해결하고 풀 리퀘스트 보내기
- 다른 브랜치에서 특정 커밋 복사해오기
- 브랜치 이름 수정하기
- Git 명령어 축약하기
먼저 아주 간단한 예시로 주요 용어를 살펴보면:
-add-> -commit-> -push->
+-------------+-------------+------------+-------------+
| Working dir | Index | Local repo | Remote repo |
+-------------+-------------+------------+-------------+
<-checkout- <-fetch-
- Alice는 깃허브에서 'project’라는 이름의 원격 저장소를 만들었다.
- 이 원격 저장소를
git clone https://github.com/alice/project.git
명령으로 자신의 컴퓨터에 복사해 로컬 저장소를 만들었다.- 로컬 저장소(Local repository): 컴퓨터의 로컬 환경에 위치한 저장소.
- 그리고 작업 디렉토리에서
index.html
파일을 작성했다.- 작업 디렉토리(Working directory): 실제 파일이 위치한 디렉토리.
- 이 파일을
git add index.html
명령으로 변경된(Modified) 파일들을 스테이징해 인덱스 영역에 등록했다.- 스테이징(Staging): 확정할 변경 사항을 준비시키는 것.
- 인덱스(Index): 확정할 준비가 된 변경 사항들이 모인 영역.
- 이어서
git commit -m "index.html 추가"
명령으로 스테이징된(Staged) 변경 사항을 커밋해 로컬 저장소에 등록했다.- 커밋(Commit): 인덱스의 변경 사항들을 확정하는 것. 여기까지는 로컬 저장소에서 일어나는 일이며, 아직 다른 사람에게는 변경 사항이 공개되지 않은 상태다.
- 헤드(HEAD): 작업 중인 브랜치의 선두를 가리키는 포인터. 헤드 이하의 커밋들을 확정된 것으로 취급하며, 필요에 따라 특정 커밋이나 브랜치를 가리키도록 헤드를 움직여 작업 디렉토리의 상태를 바꿀 수 있다.
- 마지막으로
git push origin master
명령으로 푸시해 커밋된(Committed) 변경 사항을 원격 저장소에 게시했다.- 푸시(Push): 확정된 변경 사항을 원격 저장소에 게시하는 것. 드디어 변경 사항이 공개된다.
origin
: 로컬 저장소의 원본 원격 저장소.clone
과정에서 자동으로 등록된다.clone
으로 로컬 저장소를 만든 것이 아니라면 따로 추가해야 한다.
포크(Fork)나 풀 리퀘스트(Pull request) 등 깃허브에 관한 내용은 오픈소스 입문을 위한 아주 구체적인 가이드에서 소개했다.
파일 스테이징 취소하기
- Alice는
git add main.js
명령으로 파일을 스테이징했다. main.js
를 언스테이징(Unstaging)하고자 한다.
reset
명령을 사용하면 파일을 언스테이징 할 수 있다.
$ git reset main.js
파일명을 명시하지 않으면 스테이지된 모든 파일을 언스테이징한다.
마지막 커밋 취소하기
- Alice는 방금
lib.js
파일을 빠뜨리고 커밋했다. - 커밋을 취소하고 파일을 추가해 새로 커밋하고자 한다.
헤드를 옮겨 마지막 커밋을 취소한다. --soft
옵션은 작업 디렉토리와 인덱스를 보존해 파일이 스테이지된 상태를 유지하도록 한다. HEAD^
는 헤드의 직전 위치를 의미한다. 즉, 현재 브랜치의 마지막 커밋을 뜻한다.
$ git reset --soft HEAD^
빠뜨린 파일을 추가하고 다시 커밋한다.
$ git add lib.js
$ git commit -m "자바스크립트 파일 추가"
마지막 커밋 메시지 수정하기
- Alice는 방금 커밋한 커밋 메시지 "테스트ㅌ 코드 추가"에 오타가 있는 것을 발견했다.
- 직전 커밋의 메시지를 "테스트 코드 추가"로 수정하고자 한다.
--amend
옵션으로 커밋해 직전 커밋 메시지를 수정한다.
$ git commit --amend -m "테스트 코드 추가"
이미 푸시한 커밋 메시지 수정하기
- Alice는 과거 커밋 메시지 "테스트ㅌ 코드 추가"에 오타가 있는 것을 발견했다.
- 해당 커밋 메시지를 "테스트 코드 추가"로 수정하고자 한다.
먼저 해당 커밋으로 리베이스해야 한다. --interactive
또는 -i
옵션을 주면 텍스트 에디터가 열리며 커밋 내역이 나타난다.
$ git rebase -i
pick 381cd2a 코드 품질 개선
pick f772ba1 테스트ㅌ 코드 추가
pick 2ad65fe 하단 버튼 추가
여기서 수정하고자 하는 커밋 해시(f772ba1
) 앞의 pick
을 edit
또는 e
로 바꾸고 저장한다.
pick 381cd2a 코드 품질 개선
edit f772ba1 테스트ㅌ 코드 추가
pick 2ad65fe 하단 버튼 추가
이제 커밋 메시지를 새로 작성해 커밋하고, 리베이스를 진행한다. 마음이 바뀌었다면 리베이스 명령의 --abort
옵션으로 리베이스를 중단할 수 있다.
$ git commit --amend -m "테스트 코드 추가"
$ git rebase --continue
좀 더 직관적이고 쉽게 커밋 정보를 변경하고 싶다면 git-amend와 같은 유틸을 쓸 수도 있다.
커밋을 과거로 되돌리기
- Alice는 버튼의 색깔을 바꾸는 작업들을 커밋했으나 모든 게 잘못됐다는 것을 깨달았다.
- 모든 버튼의 색깔을 똑같이 만들기 위해 과거 버전으로 돌아가 코드를 다시 작성하고자 한다.
우선 돌아가고 싶은 버전의 커밋 해시(21929f8
)를 확인한다.
$ git reflog
28ca4ca HEAD@{0}: commit: 오른쪽 버튼 색깔을 파란색으로 변경
8eefd4a HEAD@{1}: commit: 가운데 버튼 색깔을 초록색으로 변경
21929f8 HEAD@{2}: commit: 왼쪽 버튼 색깔을 빨간색으로 변경
그리고 reset
을 이용해 헤드를 특정 커밋으로 옮긴다. reset
의 기본 옵션은 --mixed
이며, 작업 디렉토리는 그대로 유지한 채 헤드와 인덱스를 변경한다. --hard
옵션의 경우 작업 디렉토리와 헤드, 인덱스를 모두 변경한다. 마지막으로 --soft
옵션은 헤드만 변경한다.
$ git reset --hard 21929f8
코드를 수정한 뒤, 파일을 스테이징하고 다시 커밋한다.
$ git add *
$ git commit -m "모든 버튼 색깔을 노란색으로 변경"
이 방법을 사용하면 되돌린 커밋 히스토리가 모두 사라진다. 커밋이후 --force
으로 푸시하면 이미 푸시한 커밋까지 되돌릴 수도 있지만, 권장하는 방법은 아니다.
푸시한 커밋을 과거로 되돌리기
- Alice는 버튼의 색깔을 바꾸는 작업들을 커밋, 푸시했으나 모든 게 잘못됐다는 것을 깨달았다.
- 모든 버튼의 색깔을 똑같이 만들기 위해 과거 버전으로 돌아가 코드를 다시 작성하고자 한다.
revert
명령을 사용하면 된다. revert
는 reset
과 달리 커밋 히스토리를 남긴다. 협업을 하는 상황에서 이미 푸시한 커밋을 되돌리고 싶다면 reset
보다는 revert
를 사용하는 것이 좋다. 먼저 돌아리고 싶은 버전의 커밋 해시들(21929f8
, 8eefd4a
, 28ca4ca
)을 확인한다.
$ git reflog
28ca4ca HEAD@{0}: commit: 오른쪽 버튼 색깔을 파란색으로 변경
8eefd4a HEAD@{1}: commit: 가운데 버튼 색깔을 초록색으로 변경
21929f8 HEAD@{2}: commit: 왼쪽 버튼 색깔을 빨간색으로 변경
그리고 되돌리고 싶은 커밋의 범위를 지정해 revert
를 실행하고, 에디터에서 커밋 메시지를 작성한다. 만약 특정 하나의 커밋만 되돌리고 싶다면 커밋 해시를 하나만 입력해도 된다.
$ git revert 21929f8...28ca4ca
Revert "왼쪽 버튼 색깔을 빨간색으로 변경"
Revert "가운데 버튼 색깔을 초록색으로 변경"
Revert "오른쪽 버튼 색깔을 파란색으로 변경"
푸시하면 버튼의 색깔을 바꾼 작업들이 취소된다.
$ git push origin master
히스토리에는 세 개의 커밋이 추가된 것을 볼 수 있다.
$ git reflog
132bf27 HEAD@{0}: commit: Revert "오른쪽 버튼 색깔을 빨간색으로 변경"
b2c409f HEAD@{1}: commit: Revert "가운데 버튼 색깔을 초록색으로 변경"
4ef1104 HEAD@{2}: commit: Revert "왼쪽 버튼 색깔을 파란색으로 변경"
28ca4ca HEAD@{3}: commit: 오른쪽 버튼 색깔을 파란색으로 변경
8eefd4a HEAD@{4}: commit: 가운데 버튼 색깔을 초록색으로 변경
21929f8 HEAD@{5}: commit: 왼쪽 버튼 색깔을 빨간색으로 변경
푸시한 파일 삭제하기
- Alice는 공유할 필요가 없는 파일을 원격 저장소에 푸시했다.
- 실수로 푸시한 파일
setting.txt
를 원격 저장소에서 삭제하고자 한다.
원격 저장소에 올라간 파일 setting.txt
를 삭제한다. --cached
옵션은 인덱스에서만 파일을 삭제한다는 의미로, --cached
옵션을 주면 로컬 저장소의 파일은 지우지 않는다.
$ git rm --cached setting.txt
이제 원격 저장소의 master 브랜치에 반영한다.
$ git commit -m "설정 파일 제거"
$ git push orgin master
히스토리가 그대로 남기 때문에 민감한 파일의 경우 이 방법으로 지워선 안 된다. 커밋에서 제외할 파일은 미리 .gitignore
파일에 명시하도록 하자.
푸시한 파일 흔적없이 삭제하기
- Alice는 절대 공개해서는 안되는 파일을 원격 저장소에 푸시했다.
- 실수로 푸시한 파일
password.txt
를 원격 저장소에서 삭제하고자 한다.
먼저 모든 히스토리에서 password.txt
를 제거해야 한다. filter-branch
를 사용하면 브랜치의 히스토리 전체에서 특정 커밋만 필터링해 수정할 수 있으며, 여기에 --index-filter
옵션을 주면 인덱스를 수정하게 된다. 아래 명령의 경우 모든 커밋에서 git rm --cached --ignore-unmatch password.txt
명령을 실행한다. 참고로 rm
명령의 --ignore-unmatch
옵션은 파일이 일치하지 않아도 0을 리턴하도록 해 명령이 반복되게 만든다.
$ git filter-branch -f --index-filter "git rm --cached --ignore-unmatch password.txt" HEAD
파일을 삭제하는 과정에서 아무런 내용이 없는 빈 커밋이 생길 수 있다. 이제 모든 히스토리에서 빈 커밋을 제거하고 푸시한다.
$ git filter-branch -f --prune-empty HEAD
$ git push -f origin master
원격 저장소에서 업데이트 받아오기
- Alice는 오랜만에 로컬 저장소의 master 브랜치를 업데이트하려 한다.
- 며칠전 Bob이 원본 원격 저장소 alice/project에 3개의 커밋을 추가했다.
- 원본 저장소 alice/project에 추가된 3개의 커밋을 로컬 저장소로 가져와 업데이트하고자 한다.
fetch
와 pull
두 가지 방법이 있다. fetch
를 실행하면 원격 저장소의 내용을 로컬 저장소로 가져오며, 임시로 FETCH_HEAD
라는 이름의 브랜치를 만든다. (git checkout FETCH_HEAD
명령으로 원격 저장소에서 가져온 업데이트를 확인할 수 있다.)
$ git fetch origin
그리고 merge
로 가져온 내용을 master 브랜치에 병합(Merge)한다. 만약 병합과정에서 충돌(Conflicts)이 발생하면 직접 파일을 수정해줘야 한다.
$ git checkout master
$ git merge origin/master
pull
의 경우 fetch
와 merge
를 연달아 진행한다. 현재 체크아웃하고 있는 브랜치의 원격 저장소에서 내용을 가져오는 경우 매개변수(아래 예시에서는 origin master
)를 생략해도 된다.
$ git pull origin master
병합 커밋없이 풀하기
- Bob은 alice/project 저장소의 master 브랜치에 main.js를 수정하고 커밋, 푸시했다.
- Alice는 master 브랜치에서 index.html 파일을 수정하고 커밋했다.
- Alice는
pull
명령으로 원격 저장소 alice/project에서 변경 사항을 가져오려 한다. - 이때 병합 커밋(Merge commit)을 만들지 않고 원격 저장소 내용을 가져오고자 한다.
원격 저장소와 로컬 저장소 모두 새로운 커밋을 가지고 있기 때문에 논 패스트 포워드(Non fast-forward)가 발생한다. 반대 경우인 패스트 포워드(Fast-forward)는 병합하려는 브랜치에 새로운 커밋이 없는 상황에서 발생하며, 이때는 HEAD를 최신 커밋으로 옮기는 것만으로 병합을 마칠 수 있다. 하지만 논 패스트 포워드는 별도의 병합 커밋이 필요하기 때문에 "Merge branch ‘master’ of https://github.com/alice/project"와 같은 메시지를 가진 커밋을 만든다.
혼자 작업하는 프로젝트라면 큰 상관이 없겠지만, 여러 사람이 프로젝트에 참여해 병합 커밋을 만들기 시작하면 히스토리가 지저분해진다.
───O2───O2───┐ (origin/master)
└────M1───M2 (master)
Merge branch 'master' of https://github.com/alice/project
이러한 병합 커밋을 만들지 않으려면 --rebase
옵션으로 리베이스 병합을 시키면 된다. (이때 스테이징되지 않은 변경 사항이 있으면 안 된다.)
$ git pull --rebase
로컬 저장소의 브랜치가 원격 저장소의 브랜치의 최신 커밋으로 리베이스되어 병합 커밋이 남지 않는다.
───O2───O2 (origin/master)
└────M1 (master)
포크한 로컬 저장소를 최신으로 유지하기
- Alice는 bob/project 저장소를 포크해서 alice/project 저장소를 만들었다.
- 그 사이 bob/project 저장소에는 3개의 커밋이 추가되었다.
- 원본 저장소인 bob/project에서 3개의 커밋을 가져와 alice/project를 업데이트하고자 한다.
먼저 Alice가 포크한 Bob의 원격 저장소(bob/project)를 업스트림(Upstream)이라는 이름으로 추가한다.
$ git remote add upstream https://github.com/bob/project.git
git remote
명령에 --verbose
또는 -v
옵션을 주면 원격 저장소 목록이 나오는데, 업스트림이 잘 추가됐다면 이 목록에서 확인할 수 있다.
$ git remote -v
origin https://github.com/alice/project.git
upstream https://github.com/bob/project.git
최신 내용을 가진 업스트림(bob/project)의 master 브랜치를 가져와 로컬 저장소(alice/project)의 master 브랜치에 병합한다. 이후에 bob/project에 새 커밋이 추가될 때도 같은 명령을 실행하면 된다.
$ git fetch upstream
$ git checkout master
$ git merge upstream/master
작업 내용을 백업하고 다른 브랜치 체크아웃하기
- Alice와 Bob은 각각 featA, featB 브랜치에서 index.js를 수정했다.
- Alice는 featA 브랜치에서 작업하던 중 Bob이 작업하고 있는 featB 브랜치를 확인할 일이 생겼다.
- featA의 작업 내용을 백업하고 featB 브랜치를 체크아웃하고자 한다.
featA 브랜치에 커밋되지 않은 변경 사항을 남겨두고 git checkout featB
를 실행해 featB 브랜치로 넘어가려하면 파일이 덮어쓰기 될 수 있다는 에러가 나온다.
$ git checkout featB
error: Your local changes to the following files would be overwritten by checkout:
index.js
Please commit your changes or stash them before you switch branches.
Aborting
체크아웃 뿐만 아니라 리베이스(Rebase)나 풀(Pull) 등 브랜치의 작업 디렉토리가 변경되는 상황에서 만날 수 있는 에러다. 작업 내용을 커밋하고 featB에 갔다가 다시 돌아와 마지막 커밋을 취소할 수도 있겠지만, 가장 깔끔한 방법은 stash
를 이용하는 것이다. 이렇게 하면 스테이지되지 않은 파일들을 임시로 백업해 작업 디렉토리를 깨끗하게 만들 수 있다.
아래와 같이 한 줄의 명령으로 간단하게 작업 내용을 백업할 수 있다.
$ git stash
Saved working directory and index state WIP on featA: e32584d Add featA index.js
작업 디렉토리가 헤드로 돌아갔으므로 안전하게 featB 브랜치를 체크아웃할 수 있게 됐다. 만약 백업한 내용을 되돌리고 싶다면 pop
하면 된다.
$ git stash pop
save <name>
으로 이름을 붙여 백업하고, list
로 목록을 확인할 수 있다. pop stash@<id>
로 특정 백업을 복원할 수도 있다.
$ git stash save ADD_INDEX_JS
Saved working directory and index state On featA: ADD_INDEX_JS
$ git stash list
stash@{0}: On featA: ADD_INDEX_JS
$ git stash pop stash@{0}
풀 리퀘스트에서 Squash and Merge된 커밋 제외하기
───M1───M2───S1───M3 (master)
└────A1───A2 (featA)
└────B1───B2 (featB)
- Alice는 master 브랜치에서 featA 브랜치를 만들어 A1, A2 커밋을 추가하고 master에 squash and merge했다.
- 그리고 featA 브랜치에서 featB 브랜치를 만들어 B1, B2 커밋을 추가했다.
- 그 사이 Bob이 master 브랜치에 M3 커밋을 추가했다.
- Alice는 featB 브랜치를 master 브랜치에 병합하기 위해 PR을 보냈다.
- A1, A2는 master에 S1 커밋으로 squash and merge 됐기 때문에 PR에는 A1, A2, B1, B2 커밋이 모두 포함된다.
- PR에서 A1, A2 커밋을 빼고 B1, B2 커밋만 포함시키고자 한다.
만약 Alice가 master 브랜치의 M3에서 featB 브랜치를 만들었다면 이런 일이 생기지 않았을 것이다. 설령 A1, A2, B1, B2 커밋이 그대로 병합돼도 내용에는 문제가 없을 것이다. 하지만 PR 커밋 내역에 이미 병합된 커밋들이 쌓여 있는 것은 보기 좋지 않다. 입사 후 이런 실수를 했는데, 너무 당황스러웠다.
B1, B2 커밋을 복사해 M3의 자식으로 만들기 위해 --onto
옵션으로 리베이스하고, --force
또는 -f
옵션으로 푸시한다. 보다시피 커밋을 제외하거나 뺀다는 개념이 아니다.
$ git checkout featB
$ git rebase --onto featA master
$ git push -f origin featB
이제 B1, B2 커밋이 복사되어 같은 내용의 B1’, B2’ 커밋이 M3를 부모로 갖게 된다. (기존 B1, B2 커밋은 버려지지만 사라지지는 않는다.) 최종적으로 PR에서 A1과 A2 커밋도 깔끔히 사라진다.
───M1───M2───S1─────────M3 (master)
└────A1───A2 (featA) └───B1'───B2' (featB)
└───B1───B2 [abandoned]
병합 충돌 해결하고 풀 리퀘스트 보내기
- Alice는 bob/project 저장소의 코드를 수정하고 bob/project의 master 브랜치에 풀 리퀘스트를 보냈다.
- 그 사이 Carol이 Alice와 같은 코드를 수정했고, Carol의 풀 리퀘스트가 먼저 master에 병합되었다.
- Alice의 풀 리퀘스트에 병합 충돌(Merge conflicts) 위험이 생겨 이를 해결하고자 한다.
Alice와 Carol이 같은 부분을 수정했기 때문에 병합 충돌이 발생했다. 이때는 master에 Alice의 변경 사항을 반영할지, Carol의 작업을 유지할지 수동으로 결정해줘야 한다.
이를 해결하는 데는 두 가지 방법이 있는데, 첫 번째는 bob/project의 master를 가져와 Alice의 브랜치 alice-branch에 병합하는 것이다.
$ git pull origin master
이렇게하면 bob/project의 master 브랜치의 내용을 로컬로 가져와 Alice의 브랜치에 병합된다. 이때 병합 충돌이 발생하는데, 에디터에서는 아래와 같이 보인다.
function add(a, b) {
<<<<<< HEAD
return a + b;
======
return b + a;
>>>>>> master
}
HEAD는 Alice의 변경 사항(Current chnage)이고, master는 Carol의 변경 사항(Incoming change)이다. 두 변경 사항 중 하나를 선택하면 된다. Alice는 ======
부터 >>>>>> master
까지의 내용을 지움으로써 자신이 작성한 변경 사항을 선택했다.
function add(a, b) {
return a + b;
}
파일을 저장한 뒤 병합을 계속 진행한다. 마지막으로 해당 내용을 푸시한다.
$ git merge --continue
$ git push origin alice-branch
이렇게 하면 Merge branch 'master' of https://github.com/bob/project
와 같은 메시지를 가진 병합 커밋이 생긴다. 히스토리를 더 깔끔하게 만들고 싶다면 두 번째 방법을 사용하면 된다. 두 번째 방법은 Alice의 브랜치를 origin/master 브랜치의 최신 커밋에 리베이스하는 것이다.
$ git checkout alice-branch
$ git pull --rebase origin master
bob/project의 master 브랜치를 가져와 최신 커밋에 Alice의 브랜치 alice-branch를 리베이스한다. 이제 Alice의 커밋은 master의 최신 커밋을 향해 한 발자국씩 나아간다.
┌────C1───┐ (carol-branch)
───M1───M2───M3───M4 (master)
└────A1 (alice-branch)
원래 Alice의 브랜치가 M1 커밋을 베이스로 했기 때문에 다음 커밋인 M2로 리베이스된다.
┌────C1───┐ (carol-branch)
───M1───M2───M3───M4 (master)
└────A1 (alice-branch)
이어서 M3 커밋에 리베이스된다.
┌────C1───┐ (carol-branch)
───M1───M2───M3───M4 (master)
└────F1 (alice-branch)
M3 커밋에 리베이스하자 병합 충돌이 발생해 리베이스가 중단됐다. 첫 번째 방법과 마찬가지로 두 변경 사항 중 하나를 선택하고 저장해 병합 충돌을 해결한다. 이어서 --continue
옵션으로 리베이스를 계속 진행한다.
$ git rebase --continue
마지막으로 Alice의 브랜치가 M4 커밋에 리베이스된다.
┌────C1───┐ (carol-branch)
───M1───M2───M3───M4 (master)
└────F1 (alice-branch)
M3 커밋에서 충돌이 있었으므로 그 다음 커밋인 M4에서도 같은 충돌이 발생한다. 앞선 방식과 똑같이 충돌을 해결하고, 리베이스를 마친 뒤 --force
또는 -f
옵션으로 푸시한다.
리베이스 방식을 사용할 때는 병합 충돌이 있는 모든 커밋들에 대해 충돌을 일일이 해결해줘야 한다. 즉, 충돌난 커밋이 너무 많을 때는 리베이스보다는 단순 병합하는 첫 번재 방법이 더 낫다. 할만할 것 같아서 시작했으나 도중에 무리라고 판단되면 git rebase --abort
로 리베이스를 중단할 수 있다.
$ git rebase --continue
$ git push -f origin alice-branch
--force
옵션으로 푸시하지 않으면 먼저 원격 저장소의 alice-branch를 풀해야 하는데, 그러면 로컬 저장소에서 리베이스한 커밋과 원격 저장소의 커밋이 중복되어 동일한 내용의 커밋이 생겨버린다.
다른 브랜치에서 특정 커밋 복사해오기
- Alice는 featA 브랜치에서
index.js
를 리팩토링해 커밋했다. - 이 커밋을 featB 브랜치로 복사하고자 한다.
먼저 featA 브랜치에서 복사할 커밋의 해시(4a391fc
)를 확인한다.
$ git checkout featA
$ git reflog
183d9ba HEAD@{0}: commit: 버튼 동작 구현
4a391fc HEAD@{1}: commit: index.js 리팩토링
b51bf86 HEAD@{2}: commit: index.js 추가
그리고 cherry-pick
명령으로 featB 브랜치에서 해당 커밋을 복사해온다.
$ git checkout featB
$ git cherry-pick 4a391fc
브랜치 이름 수정하기
- Alice는 feattA 브랜치에 오타가 있는 것을 발견했다.
- feattA 브랜치의 이름을 featA로 수정하고자 한다.
branch
명령에 --move
또는 -m
옵션을 사용하면 간단하게 변경할 수 있다.
$ git branch -m feattA featA
Git 명령어 축약하기
- Alice는 김지현님 덕분에
git log --reflog --graph --oneline --decorate
명령을 쓰면 로그를 더 편하게 볼 수 있다는 사실을 알게됐다. - 하지만 명령이 너무 길어 축약해 사용하고자 한다.
.gitconfig
파일에서 alias를 설정해주면 된다. [alias]
섹션에 축약형과 축약할 명령을 작성하고 저장한다.
$ vim ~/.gitconfig
[alias]
lg = log --reflog --graph --oneline --decorate
이제 git l
명령은 git log --reflog --graph --oneline --decorate
와 같다. commit
이나 checkout
등의 명령도 같은 방식으로 축약할 수 있다.
$ git lg
* 1d1eb90 (origin/master, origin/HEAD) 버튼 추가
|\
| * b0a574a (HEAD -> featB, origin/featB) 버튼 색상 변경
| * b2ff985 버튼 동작 구현
|/
* 02e5c44 (featA) 내비게이션바 추가
|\
기계인간님의 편리한 git alias 설정하기를 참고하면 극한의 편리를 추구할 수도 있다.
참고자료
- 생활코딩 “지옥에서 온 Git”
- 진유림님 “초심자를 위한 Github 협업 튜토리얼”
- Hemanth HM “git-tips”
- Scott Chacon, Ben Straub “Pro Git Book”
- Stackoverflow “Questions tagged ‘git’”
</article> <section id="article-social-container"> <div id="fb-like" class="fb-like" data-href="https://parksb.github.io/" data-layout="button_count" data-action="like" data-size="small" data-show-faces="true" data-share="true"></div> </section> <article id="article-comments"> </article> </article> </main> <div id="fb-root"></div> </body></html>