본문 바로가기
Book Study/Pro Git

[Pro Git] ch02 - Git의 기초 (git add, .gitignore, git status, git diff)

by 랩린안 2022. 10. 1.

출처 : Pro Git

- 저자 : Scott Chacon, Ben Straub

 

2장 을 다 읽으면 저장소를 만들고 설정하는 방법, 파일을 추적하거나(Track) 추적 을 그만두는 방법, 변경 내용을 Stage 하고 커밋하는 방법을 알게 된다. 파일이 나 파일 패턴을 무시하도록 Git을 설정하는 방법, 실수를 쉽고 빠르게 만회하 는 방법, 프로젝트 히스토리를 조회하고 커밋을 비교하는 방법, 리모트 저장 소에 Push 하고 Pull 하는 방법을 살펴본다.

 

Git 저장소 만들기

Git 저장소를 만드는 방법은 두 가지다. 기존 프로젝트를 Git 저장소로 만드는 방법이 있고, 다른 서버에 있는 저장소를 Clone 하는 방법이 있다.

 

기존 디렉토리를 Git 저장소로 만들기

기존 프로젝트를 Git으로 관리하고 싶을 때, 프로젝트의 디렉토리로 이동해서 아래과 같은 명령을 실행한다

 

$ git init

이 명령은 .git 이라는 하위 디렉토리를 만든다. .git 디렉토리에는 저장 소에 필요한 뼈대 파일(Skeleton)이 들어 있다. 이 명령만으로는 아직 프로젝 트의 어떤 파일도 관리하지 않는다.

 

Git이 파일을 관리하게 하려면 저장소에 파일을 추가하고 커밋해야 한다.

git add 명령으로 파일을 추가하고 git commit 명령으로 커밋한다

 

$ git add *.c
$ git add LICENSE
$ git commit -m 'initial project version'

명령어를 몇개로 순식간에 Git 저장소를 만들고 파일이 관리되게 했다.

 

기존 저장소를 Clone 하기

다른 프로젝트에 참여하려거나(Contribute) Git 저장소를 복사하고 싶을 때 git clone 명령을 사용한다.

 

git clone을 실행하면 프로젝트 히스토리를 전부 받아온다. 실 제로 서버의 디스크가 망가져도 클라이언트 저장소 중에서 아무거나 하나 가 져다가 복구하면 된다(서버에만 적용했던 설정은 복구하지 못하지만 모든 데 이터는 복구된다)

git clone [url] 명령으로 저장소를 Clone 한다.

 

$ git clone https://github.com/libgit2/libgit2

이 명령은 “libgit2”이라는 디렉토리를 만들고 그 안에 .git 디렉토리를 만 든다. 그리고 저장소의 데이터를 모두 가져와서 자동으로 가장 최신 버전을 Checkout 해 놓는다. libgit2 디렉토리로 이동하면 Checkout으로 생성한 파 일을 볼 수 있고 당장 하고자 하는 일을 시작할 수 있다. 아래과 같은 명령을 사 용하여 저장소를 Clone 하면 “libgit2”이 아니라 다른 디렉토리 이름으로 Clone 할 수 있다.

 

 

$ git clone https://github.com/libgit2/libgit2 mylibgit

디렉토리 이름이 mylibgit 이라는 것만 빼면 이 명령의 결과와 앞선 명령 의 결과는 같다. Git은 다양한 프로토콜을 지원한다. 이제까지는 https:// 프로토콜을 사 용했지만 git://를 사용할 수도 있고 user@server:path/to/repo.git 처 럼 SSH 프로토콜을 사용할 수도 있다.

 

 

수정하고 저장소에 저장하기

만질 수 있는 Git 저장소를 하나 만들었고 워킹 디렉토리에 Checkout도 했다. 이제는 파일을 수정하고 파일의 스냅샷을 커밋해 보자. 파일을 수정하다가 저장하고 싶으면 스냅샷을 커밋한다.

 

워킹 디렉토리의 모든 파일은 크게 Tracked(관리대상임)와 Untracked(관리 대상이 아님)로 나눈다. Tracked 파일은 이미 스냅샷에 포함돼 있던 파일이다. Tracked 파일은 또 Unmodified(수정하지 않음)와 Modified(수정함) 그리고 Staged(커밋으로 저장소에 기록할) 상태 중 하나이다. 그리고 나머지 파일은 모두 Untracked 파일이다. Untracked 파일은 워킹 디렉토리에 있는 파일 중 스 냅샷에도 Staging Area에도 포함되지 않은 파일이다. 처음 저장소를 Clone 하 면 모든 파일은 Tracked이면서 Unmodified 상태이다. 파일을 Checkout 하고 나서 아무것도 수정하지 않았기 때문에 그렇다. 마지막 커밋 이후 아직 아무것도 수정하지 않은 상태에서 어떤 파일을 수 정하면 Git은 그 파일을 Modified 상태로 인식한다. 실제로 커밋을 하기 위해 서는 이 수정한 파일을 Staged 상태로 만들고, Staged 상태의 파일을 커밋한 다. 이런 라이프사이클을 계속 반복한다.

 

파일의 라이프사이클

 

 

파일의 상태 확인하기

파일의 상태를 확인하려면 보통 git status 명령을 사용한다. Clone 한 후에 바로 이 명령을 실행하면 아래과 같은 메시지를 볼 수 있다.

 

$ git status
On branch master
nothing to commit, working directory clean

위의 내용은 파일을 하나도 수정하지 않았다는 것을 말해준다. Tracked나 Modified 상태인 파일이 없다는 의미다. Untracked 파일은 아직 없어서 목록에 나타나지 않는다. 그리고 현재 작업 중인 브랜치를 알려주며 서버의 같은 브 랜치로부터 진행된 작업이 없는 것을 나타낸다. 기본 브랜치가 master이기 때 문에 현재 브랜치 이름이 “master”로 나온다. 

 

내가 해보니 이렇게 나온다.

On branch main
Your branch is up to date with 'origin/main'.

nothing to commit, working tree clean

마스터에서 main으로 바뀌었다고 한다.

말 대로 파일을 하나도 수정하지 않았다!

$ git status
On branch master
Changes to be committed:
 (use "git reset HEAD <file>..." to unstage)

 new file: README

 

“Changes to be committed” 에 들어 있는 파일은 Staged 상태라는 것을 의 미한다. 커밋하면 git add를 실행한 시점의 파일이 커밋되어 저장소 히스토리에 남는다.

 

앞에서 git init 명령을 실행했을 때, 그 다음 git add (files) 명령을 실행했던 걸 기억할 것이다. 이 명령을 통해 디렉토리에 있는 파일을 추적하고 관리하도록 한다. git add 명령은 파일 또는 디렉토리의 경 로명을 아규먼트로 받는다. 만일 디렉토리를 아규먼트로 줄 경우, 그 디렉토 리 아래에 있는 모든 파일을 재귀적으로 추가한다.

 

 

Modified 상태의 파일을 Stage 하기

이미 Tracked 상태인 파일을 수정하는 법을 알아보자. “benchmarks.rb” 라는 파일을 수정하고 나서 git status 명령을 다시 실행하면 결과는 아래와 같 다

 

$ git status
On branch master
Changes to be committed:
 (use "git reset HEAD <file>..." to unstage)
 new file: README
Changes not staged for commit:
 (use "git add <file>..." to update what will be committed)
 (use "git checkout -- <file>..." to discard changes in working directory)
 modified: benchmarks.rb

이 “benchmarks.rb” 파일은 “Changes not staged for commit”에 있다. 이것 은 수정한 파일이 Tracked 상태이지만 아직 Staged 상태는 아니라는 것이다. Staged 상태로 만들려면 git add 명령을 실행해야 한다. git add 명령은 파 일을 새로 추적할 때도 사용하고 수정한 파일을 Staged 상태로 만들 때도 사용 한다. Merge 할 때 충돌난 상태의 파일을 Resolve 상태로 만들때도 사용한다. add의 의미는 프로젝트에 파일을 추가한다기 보다는 다음 커밋에 추가한다 고 받아들이는게 좋다. git add 명령을 실행하여 “benchmarks.rb” 파일을 Staged 상태로 만들고 git status 명령으로 결과를 확인해보자.

 

 

$ git add benchmarks.rb
$ git status

On branch master
Changes to be committed:
 (use "git reset HEAD <file>..." to unstage)
 new file: README
 modified: benchmarks.rb

 

두 파일 모두 Staged 상태이므로 다음 커밋에 포함된다. 하지만 아직 더 수 정해야 한다는 것을 알게 되어 바로 커밋하지 못하는 상황이 되었다고 생각해 보자. 이 상황에서 benchmark.rb 파일을 열고 수정한다. 이제 커밋할 준비가 다 됐다고 생각할 테지만, Git은 그렇지 않다. git status 명령으로 파일의 상태를 다시 확인해보자.

$ vim benchmarks.rb
$ git status
On branch master
Changes to be committed:
 (use "git reset HEAD <file>..." to unstage)
 new file: README
 modified: benchmarks.rb
Changes not staged for commit:
 (use "git add <file>..." to update what will be committed)
 (use "git checkout -- <file>..." to discard changes in working directory)
 modified: benchmarks.rb

 

benchmarks.rb가 Staged 상태이면서 동시에 Unstaged 상태로 나온다. 어떻게 이런 일이 가능할까? git add 명령을 실행하면 Git은 파일을 바로 Staged 상태로 만든다. 지금 이 시점에서 커밋을 하면 git commit 명령을 실 행하는 시점의 버전이 커밋되는 것이 아니라 마지막으로 git add 명령을 실 행했을 때의 버전이 커밋된다. 그러니까 git add 명령을 실행한 후에 또 파일 을 수정하면 git add 명령을 다시 실행해서 최신 버전을 Staged 상태로 만들 어야 한다

 

$ git add benchmarks.rb
$ git status
On branch master
Changes to be committed:
 (use "git reset HEAD <file>..." to unstage)
 new file: README
 modified: benchmarks.rb

 

파일 상태를 짤막하게 확인하기

git status 명령으로 확인할 수 있는 내용이 좀 많아 보일 수 있다. 사실 그렇 다. 좀 더 간단하게 변경 내용을 보여주는 옵션이 있다. git status -s 또는 git status --short 처럼 옵션을 주면 현재 변경한 상태를 짤막하게 보여 준다.

 

$ git status -s
 M README
MM Rakefile
A lib/git.rb
M lib/simplegit.rb
?? LICENSE.txt

아직 추적하지 않는 새 파일 앞에는 ??표시가 붙는다. Staged 상태로 추가 한 파일 중 새로 생성한 파일 앞에는 A 표시가, 수정한 파일 앞에는 M 표시가 붙 는다. 위 명령의 결과는 한 라인에 두 가지 정보를 보여준다. 왼쪽에는 파일 변 경 내용이 어떤 것인지를, 오른쪽에는 해당하는 파일의 이름을 표시한다. README 파일 같은 경우 내용을 변경했지만 아직 Stage 상태로 추가하지는 않았 다. lib/simplegit.rb 파일은 내용을 변경하고 Stage 상태로 추가까지 한 상 태이다. 위 결과에서 차이점을 비교해보자. Rakefile은 변경하고 Stage 상태 로 추가한 후 또 내용을 변경해서 Staged 이면서 Unstaged 상태인 파일이다.

 

파일 무시하기

어떤 파일은 Git이 관리할 필요가 없다. 보통 로그 파일이나 빌드 시스템이 자 동으로 생성한 파일이 그렇다. 그런 파일을 무시하려면 .gitignore 파일을 만들고 그 안에 무시할 파일 패턴을 적는다. 아래는 .gitignore 파일의 예다.

 

*.[oa]
*~

 

첫번째 라인은 확장자가 “.o” 나 “.a” 인 파일을 Git이 무시하라는 것이고 둘 째 라인은 ~로 끝나는 모든 파일을 무시하라는 것이다. “.o” 와 “.a” 는 각각 빌 드 시스템이 만들어내는 오브젝트와 아카이브 파일이고 ~로 끝나는 파일은 Emacs나 VI 같은 텍스트 편집기가 임시로 만들어내는 파일이다. 또 log, tmp, pid 같은 디렉토리나, 자동으로 생성하는 문서 같은 것도 추가할 수 있다.

 

.gitignore 파일은 보통 처음에 만들어 두는 것이 편리하다. 그래서 Git 저장소에 커밋하고 싶지 않은 파일을 실수로 커밋하는 일을 방지할 수 있다.

 

.gitignore 파일에 입력하는 패턴은 아래 규칙을 따른다.

 

• 아무것도 없는 라인이나, #로 시작하는 라인은 무시한다.

• 표준 Glob 패턴을 사용한다.

• 슬래시(/)로 시작하면 하위 디렉토리에 적용되지(Recursivity) 않는다.

• 디렉토리는 슬래시(/)를 끝에 사용하는 것으로 표현한다.

• 느낌표(!)로 시작하는 패턴의 파일은 무시하지 않는다.

 

Glob 패턴은 정규표현식을 단순하게 만든 것으로 생각하면 되고 보통 쉘에 서 많이 사용한다. 애스터리스크(*)는 문자가 하나도 없거나 하나 이상을 의 미하고, [abc]는 중괄호 안에 있는 문자 중 하나를 의미한다(그러니까 이 경 우에는 a, b, c). 물음표(?)는 문자 하나를 말하고, [0-9]처럼 중괄호 안의 캐릭 터 사이에 하이픈(-)을 사용하면 그 캐릭터 사이에 있는 문자 하나를 말한다. 애스터리스크 2개를 사용하여 디렉토리 안의 디렉토리 까지 지정할 수 있다. a/**/z 패턴은 a/z, a/b/z, a/b/c/z 디렉토리에 사용할 수 있다.

 

 

아래는 .gitignore 파일의 예이다.

 

# 확장자가 .a인 파일 무시
*.a
# 윗 라인에서 확장자가 .a인 파일은 무시하게 했지만 lib.a는 무시하지 않음
!lib.a
# 현재 디렉토리에 있는 TODO파일은 무시하고 subdir/TODO처럼 하위디렉토리에 있는 파일은/TODO
# build/ 디렉토리에 있는 모든 파일은 무시
build/
# doc/notes.txt 파일은 무시하고 doc/server/arch.txt 파일은 무시하지 않음
doc/*.txt
# doc 디렉토리 아래의 모든 .txt 파일을 무시
doc/**/*.txt

더많은 예제를 확인 가능하다.

https://github.com/github/gitignore 

 

GitHub - github/gitignore: A collection of useful .gitignore templates

A collection of useful .gitignore templates. Contribute to github/gitignore development by creating an account on GitHub.

github.com

 

Staged와 Unstaged 상태의 변경 내용을 보기

단순히 파일이 변경됐다는 사실이 아니라 어떤 내용이 변경됐는지 살펴보기 엔 git status 명령이 아니라 git diff 명령을 사용해야 한다. 보통 우리는 수정했지만, 아직 Staged 파일이 아닌것?과 어떤 파일이 Staged 상태인지?가 궁금하기 때문에 git status 명령으로도 충분하다.

 

더 자세하게는 git diff 명령을 사용하는데 Patch처럼 어떤 라인을 추가했고 삭제했는지가 궁금 할 때에 사용한다.

git diff는 나중에 더 자세히 다룬다. README 파일을 수정해서 Staged 상태로 만들고 benchmarks.rb 파일은 그 냥 수정만 해둔다. 이 상태에서 git status 명령을 실행하면 아래와 같은 메 시지를 볼 수 있다.

 

 

$ git status
On branch master
Changes to be committed:
 (use "git reset HEAD <file>..." to unstage)
 new file: README
Changes not staged for commit:
 (use "git add <file>..." to update what will be committed)
 (use "git checkout -- <file>..." to discard changes in working directory)
 modified: benchmarks.rb
$ git diff
diff --git a/benchmarks.rb b/benchmarks.rb
index 3cb747f..e445e28 100644
--- a/benchmarks.rb
+++ b/benchmarks.rb
@@ -36,6 +36,10 @@ def main
 @commit.parents[0].parents[0].parents[0]
 end
+ run_code(x, 'commits 1') do
+ git.commits.size
+ end
+
 run_code(x, 'commits 2') do
 log = git.commits('master', 15)
 log.size

 

이 명령은 워킹 디렉토리에 있는 것과 Staging Area에 있는 것을 비교한다. 그래서 수정하고 아직 Stage 하지 않은 것을 보여준다. 만약 커밋하려고 Staging Area에 넣은 파일의 변경 부분을 보고 싶으면 git diff --cached 옵션을 사용한다. 이 명령은 저장소에 커밋한 것과 Staging Area에 있는 것을 비교한다

 

git diff --staged
diff --git a/README b/README
new file mode 100644
index 0000000..03902a1
--- /dev/null
+++ b/README
@@ -0,0 +1,4 @@
+My Project
+
+ This is my project and it is amazing.

꼭 잊지 말아야 할 것이 있는데 git diff 명령은 마지막으로 커밋한 후에 수정한 것들 전부를 보여주지 않는다. git diff는 Unstaged 상태인 것들만 보여준다. 이 부분이 조금 헷갈릴 수 있다. 수정한 파일을 모두 Staging Area에 넣었다면 git diff 명령은 아무것도 출력하지 않는다.

 

benchmarks.rb 파일을 Stage 한 후에 다시 수정해도 git diff 명령을 사 용할 수 있다. 이때는 Staged 상태인 것과 Unstaged 상태인 것을 비교한다

 

$ git add benchmarks.rb
$ echo '# test line' >> benchmarks.rb
$ git status
On branch master
Changes to be committed:
 (use "git reset HEAD <file>..." to unstage)
 modified: benchmarks.rb
Changes not staged for commit:
 (use "git add <file>..." to update what will be committed)
 (use "git checkout -- <file>..." to discard changes in working directory)
 modified: benchmarks.rb

git diff 명령으로 Unstaged 상태인 변경 부분을 확인할 수 있다.

 

$ git diff
diff --git a/benchmarks.rb b/benchmarks.rb
index e445e28..86b2f7c 100644
--- a/benchmarks.rb
+++ b/benchmarks.rb
@@ -127,3 +127,4 @@ end
 main()
 ##pp Grit::Git

Staged 상태인 파일은 git diff --cached 옵션으로 확인한다

$ git diff --cached
diff --git a/benchmarks.rb b/benchmarks.rb
index 3cb747f..e445e28 100644
--- a/benchmarks.rb
+++ b/benchmarks.rb
@@ -36,6 +36,10 @@ def main
 @commit.parents[0].parents[0].parents[0]
 end
+ run_code(x, 'commits 1') do
+ git.commits.size
+ end
+
 run_code(x, 'commits 2') do
 log = git.commits('master', 15)
 log.size