Version Control (Git)

버전 컨트롤 시스템(VCSs)은 소스코드(또는 기타 파일이나 폴더들의 모음)의 변화를 추적하기 위해 사용되는 도구입니다. 이름에서 알수 있듯이 이 도구는 변경내역을 유지시켜주며, 나아가 협업을 용이하게 합니다. VCSs는 일련의 스냅샷에서 폴더와 폴더안의 내용물들의 변화를 추적합니다. 각각의 스냅샷은 최상위 폴더 내의 파일 / 폴더의 모든 상태를 캡슐화 합니다. VCSs는 누군가가 만들었던 각가의 스냅샷과 스냅샷에 연결된 메시지 등의 메타데이터를 유지합니다.

왜 버전 관리는 유용할까요? 혼자 작업을 하더라도 프로젝트의 지난 스냡샷을 보며 왜 변경사항 생겼는지를 기록하고, 병렬 개발 분기에서 작업을 하는 등 많은 일들을 할수 있게 해줍니다. 다른 사람들과 일을 할때 버전관리는 다른 사람들의 변화를 볼수있는 귀중한 도구입니다. 게다가 동시 개발에서의 갈등을 해결해 줍니다.

현대 VCSs 는 아래와 같은 질문에 대한 답변을 쉽게 할수 있게 해줍니다.

다른 VCSs가 존재하지만, Git은 사실상 버전관리 도구의 표준 입니다. 이 XKCD comic 은 깃의 명성을 보여줍니다.

xkcd 1597

깃 인터페이스의 허술한 추상화로 인하여 하향식으로 방식으로 Git을 배우는것은(Git의 인터페이스/ 명령어 인터페이스 부터 시작하는것) 많은 혼란을 줄수 있있습니다. 하향식 학습은 몇가지 명령어를 외우고, 그것들을 마법주문처럼 생각하며, 문제가가 생길때마다 위의 만화에서의 접근방법을 따르게 할수 있습니다.

깃은 분명 투박한 인터페이스를 가지고 있지만, 그 이면의 디자인과 아이디어들은 아름답습니다. 나쁜 인터페이스는 암기 되어져야 하지만, 아름다운 디자인은 이해되어 집니다. 이러한 이유로 우리는 깃에 대한 데이터 모델부터 이후 명령어 인터페이스를 포함하는 상향식 설명을 제공합니다. 한번 데이터 모델을 이해하면 아래에 있는 데이터 모델을 잘 다루는 방법의 측면에서 명령어들을 더 잘 이해할수 있습니다.

깃의 데이터 모델

버전 관리를 위한 많은 필수적 접근법이 있습니다. 갓은 기록 유지, 분기를 지원, 협업 기능 과 같이 버전과 관리에 대한 모든 우수한 기능을 사용할수 있는 매우 정교한 모델을 가지고있습니다.

스냅샷

깃은 최상위 디렉토리 내의 폴더와 파일 목록에대한 기록을 일련의 스냅샷으로 모델링 합니다. 깃에서는 파일은 “blob” 이라고 하며 그것은 단지 바이트 묶음임입니다 디렉토리는 “tree” 라고 하며 이름을 blob 또는 tree 에 매핑합니다(디렉토리는 다른 디렉토리들을 포함할수 있습니다.). 스냅샷은 추적중인 최상위 트리입니다. 예를 들면 아래와 같은 트리를 가질수 있습니다. :

<root> (tree)
|
+- foo (tree)
|  |
|  + bar.txt (blob, contents = "hello world")
|
+- baz.txt (blob, contents = "git is wonderful")

최상위 트리에는 “foo” 트리(“bar.txt” blob을 하나의 요로소로 포함한 자신)와 “baz.txt” blob 두 요소가 있습니다.

Modeling history: relating snapshots

어떻게 버전 제어 시스템을 스냅샵과 연관시켜야할까요? 간단한 모델 하나는 선형 히스토리를 갖는 것입니다. 히스토리는 시간순으로 정렬된 스냅 샷의 목록입니다. 여러 이유로 Git은 이와 같은 간단한 모델을 사용하지 않습니다.

Git에서 히스토리는 스냅 샷의 유향 비순환 그래프 (DAG)입니다. 멋진 수학 용어처럼 들릴지 모르지만 겁 먹지 마세요. 이 모든 것은 선행하는 “부모” 집합을 참조하는 Git의 각각의 스냅 샷을 뜻합니다. 두 개의 병렬 개발 분기를 결합 (merge)하는것처럼 하나의 스냅샹은 여러 부모로부터 내려올것이기 때문에 히스토리는 단일 부모 보다 부모 집합을 가집니다(선형 기록의 경우).

Git은 이러한 스냅 샷을 “커밋”이라고 부릅니다. 커밋 기록을 시각화 해본다면 다음과 같습니다. :

o <-- o <-- o <-- o
            ^  
             \
              --- o <-- o

위의 ASCII문자로 만든 그림에서 o는 개별 커밋 (스냅 샷)에 해당합니다. 화살표는 각 커밋의 부모를 가리 킵니다 (“이후”가 아니라 “이전” 관계). 세 번째 커밋 후 히스토리는 두 개의 개별 분기로 분기됩니다. 예를 들어, 서로 독립되어 병렬로 개발고있는 두 개의 개별 기능에 해당 될 수 있습니다. 앞으로 이러한 분기를 병합하여 두 기능을 모두 통합하는 새 스냅 샷을 생성 할 수 있으며, 만든어진 새로은 히스토리는 아래와 같습니다. 새로 생성 된 merge 커밋은 굵게 굵은 글자로 보여집니다.:

o <-- o <-- o <-- o <---- o
            ^            /
             \          v
              --- o <-- o

Git의 커밋은 변경할 수 없습니다. 그렇다고 실수를 고칠 수 없다는 의미는 아닙니다. 커밋 히스토리에 대한 “편집”은 실제로 완전히 새로운 커밋을 생성하고, 참조들(아래쪽을 보세요)이 새 커밋을 가리 키도록 업데이트됩니다.

Data model, as pseudocode

의사 코드로 작성된 Git의 데이터 모델을 보는 것은 유용할수 있습니다.

// a file is a bunch of bytes
type blob = array<byte>

// a directory contains named files and directories
type tree = map<string, tree | blob>

// a commit has parents, metadata, and the top-level tree
type commit = struct {
    parent: array<commit>
    author: string
    message: string
    snapshot: tree
}

깔끔하고 간결한 히스토리 모델입니다.

Objects and content-addressing

object는 blob, tree, commit 입니다.

type object = blob | tree | commit

Git 데이터 저장소에서 모든 객체는 SHA-1 해시로 콘텐츠 주소가 지정됩니다.

objects = map<string, object>

def store(object):
    id = sha1(object)
    objects[id] = object

def load(id):
    return objects[id]

Blob, 트리 및 커밋은 모두 객체이며 이와 같은 방식으로 통합됩니다. 다른 객체를 참조 할 때 디스크상의 표현에는 포함 되지 않지만 해시에 의해 참조됩니다.

예를 들어 위의 디렉토리 구조의 트리는 다음과 같습니다. (git cat-file -p 698281bc680d1995c5f4caaf3359721a5a58d48d를 사용하여 시각화)

100644 blob 4448adbf7ecd394f42ae135bbeed9676e894af85    baz.txt
040000 tree c68d233a33c5c06e0340e4c224f0afca87c8ce87    foo

트리 자체에는 baz.txt(blob) 과 foo (트리)에 대한 포인터가 포함되어 있습니다 . 해시에 의해 git cat-file -p 4448adbf7ecd394f42ae135bbeed9676e894af85 주소가 지정된 baz.txt 를 보면 다음과 같은 결과가 나타납니다.

git is wonderful

References

이제 모든 스냅 샷은 SHA-1 해시로 식별 할 수 있습니다. 인간에게는 40 개의 16 진수 문자열을 잘 기억하지 못하기떄문에 불편합니다.

Git은 이문제를 해결하기 휘하여 SHA-1 해시에 “references” 라는 인간이 읽을수있는 이름을 사용합니다. References는 커밋에 대한 포인터입니다. 변경이 불가능한 객체와는 다르게 references는 변경 가능합니다 (새 커밋을 가리 키도록 업데이트 할 수 있음). 예를 들어 ‘master’ references는 일반적으로 주요 개발 분기의 최신 커밋을 가리 킵니다.

references = map<string, string>

def update_reference(name, id):
    references[name] = id

def read_reference(name):
    return references[name]

def load_reference(name_or_id):
    if name_or_id in references:
        return load(references[name_or_id])
    else:
        return load(name_or_id)

이를 통해 Git은 긴 16 진수 문자열 대신 “master”와 같이 사람이 읽을 수있는 이름을 사용하여 히스토리의 특정 스냅 샷을 참조 할 수 있습니다.

추가로 종종 히스토리 에서 “현재 위치”라는 개념을 웝합니다. 그래서 무엇과 연관 되었는지(우리가 어떻게 parents 커밋 필드들 설정하였는지) 알기 위하여 새 스냅 샷을 남깁니다. 이떄 Git에서는 “현재 위치”는 “HEAD” 라는 특수 references를 사용합니다.

Repositories

마침내, 우리는 Git 저장소가 객체references 테이터 라는것을 (대략)정의 할 수 있습니다.

디스크에서 모든 Git 저장소는 객체와 references이며 이것은 Git의 데이터 모델의 전부입니다. 모든 git명령은 객체를 추가하고 참조를 추가 / 수정 하는 커밋 DAG의 조작에 의해 매핑됩니다.

명령을 입력 할 때마다 명령어가 만들어내는 기본그래프 자료구조의 조작에 대해 생각해보세요. 반대로 커밋 DAG에 특정 종류의 변경을 시도하는 경우, 예를 들어 “커밋되지 않은 변경 사항을 무시하고 ‘master’ reference가 커밋d83f9e을 가르키도록 만들어 보세요” 이를 수행하는 명령은 있을 수 있습니다 (예 : git checkout master; git reset –hard 5d83f9e).

Staging area

Staging area는 데이터 모델과 독립된 또 다른 개념이지만 커밋을 생성하는 인터페이스의 일부입니다.

위에서 설명한 것처럼 스냅샷을 구현하기위해 생각해볼수 있는 한가지 방법은 작업 디렉토리의 _현재 상태_를 기준으로 새로운 스냅샷을 생성하는 “create snapshot” 명렁어를 사용하는 것입니다.

일부 버전 제어 도구는 이와 같이 작동하지만 Git은 작동하지 않습니다.

우리는 깨끗한 스냅 샷을 원하며, 현재 상태에서 스냅 샷을 만드는 것이 항상 이상적인 것은 아닙니다.

예를 들어 이러한 두 개의 개별 기능을 구현한후 첫번째 기능은 첫 번째 기능으로 소개하고 두 번째 기능은 두 번째 기능으로 소개하는 두 개의 개별 커밋을 생성하려고 하는 시나리오를 생상해보세요. 또는 버그 수정과 동시에 코드 전체에 디버깅 명세 문이 추가 되는 시나리오를 상상해보세요. ;모든 인쇄 문을 삭제하는 동안 버그 수정을 커밋하고 싶습니다.

Git은 “스테이징 영역” 이라는 메커니즘을 통해 스냅 샷에 포함시켜야 하는 수정 사항을 지정할수 있도록 하여 이러한 시나리오를 수용합니다.

Git command-line interface

정보가 중복되는 것을 막기 위해 우리는 명령어에 대해 자세히 설명하지는 않을 것입니다. 자세한 내용은 강의 영상을 보거나, 더 많은 정보가 있는 Pro Git을 참고하기를 추천드립니다.

Basics

Branching and merging

Remotes

Undo

Advanced Git

Miscellaneous

Resources

Exercises

  1. Git에 대한 경험이 없는 경우 Pro Git 의 처음 몇 장을 읽어 보거나 Learn Git Branching 과 같은 튜토리얼을 훑어보세요. 이러한 작업을 통해 Git 명령어와 데이터 모델을 관련시켜 보세요.
  2. 수업을 위한 웹사이트를 클론해 보세요.
    1. 버전 history를 그래프로 시각화하여 살펴보세요.
    2. 마지막으로 README.md를 수정한 사람은 누구 입니까? (힌트 : git log 인수를 사용해보세요)
    3. _config.ymlcollections: 라인의 마지막 수정과 관련된 커밋 메시지는 무엇입니까? (힌트 : git blame 과 git show 를 사용 사용해 보세요)
  3. Git을 배울 때 흔히 저지르는 실수 중 하나는 대용량 파일을 커밋하거나 Git에서 관리해서는 안되는 민감한 정보를 추가하는 것입니다. 저장소에 파일을 추가하는 커밋을 몇번 해보고 history에서 해당 파일을 삭제 하세요. (당신은 아마 이걸을 보고 싶을 거에요).
  4. GitHub에서 일부 저장소를 clone하고 기존 파일 중 하나를 수정합니다. git stash할때 어떤 일이 생기나요? git log--all --oneline 을 실행할때 무엇을 볼수 있나요? git stash pop 실행 하여 git stash 한 작업을 취소하세요. 어떤 시나리오가 유용할가요?
  5. 많은 명령어 도구들과 마찬가지로 Git은 ~/.gitconfig 라고 불리는 환경설정 파일 (또는 dotfile)을 제공합니다. git graph를 실행할 때 git log --all --graph --decrypt --online 의 출력을 얻을 수 있도록 ~/.gitconfig 에 별칭을 생성해 보세요.
  6. git config --global core.excludesfile ~/.gitignore_global 을 실행 하면 ~/.gitignore_global 에서 전역 무시 패턴을 정의 할 수 있습니다. 이후 .DS_Store 와 같이 OS와 관련되거나 에디터와 관련된 임시 파일을 무시하도록 전역 gitignore 파일을 설정해 보세요.
  7. 수업을 위한 웹사이트의 저장소를 clone하고, 오타 또는 기타 개선 사항을 찾아 GitHub에서 pull request 해보세요.

이 페이지를 수정.

CC BY-NC-SA에 따라 라이센스를 부여합니다.