ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • GitHub 브랜치 단위로 PR 하기 - 기초편-
    Contribution 2021. 11. 14. 01:26
    반응형

    1) 서론

    그동안 혼자서 깃허브 저장소를 만들고, 로컬에서 그대로 push 하는 방식을 사용했었습니다. 아마도 함께 공부하고 협업했던 경험이 없었던 것이 이유일 것 같은데요. 

     

    하지만 막상 개발자로서 취업 후 하나의 origin(remote) 저장소를 기준으로 fork, branch, PR 등 다양하게 사용하고 있었습니다. 이는 협업을 바탕으로 저장소 관리와 기록을 편리하게 위함인데요. 

     

    처음에는 push 잘 못했다가 문제가 생기지 않을까 두려움에 떨기도 했었습니다. 딱히 알려주는 사람도 없었구요. 그래서 다시는 잊지 않으려고, 일련의 흐름을 간단히 기록하려고 합니다.

     

    참고로 이 글에서는 Java 파일을 기준으로 합니다. 하지만 다른 언어 파일 혹은 단순 텍스트 파일도 관계없습니다.

     

     

     


    2) local 에서 remote 저장소로

    2. 1) Github 저장소 생성

    • "git-practice" 저장소 생성

    현재 자신의 컴퓨터가 아닌, 깃허브 혹은 외부에 생성한 저장소를 remote(원격) 저장소라고 부릅니다.

     

    2. 2) Local(로컬) 저장소 생성

    $ git init // 입력
    
    $ git:(master) // 입력 후
    • 깃으로 관리하고, 깃허브에 올리고자 하는 디렉터리에서 실행

     

    git init 명령어를 입력하면 해당 디렉토리는 git에 의해 기록되고 관리됩니다.

     

    이때 새로운 git 저장소가 된것을 local(지역) 저장소라고 부릅니다. 왜냐하면 자신의 컴퓨터에 있는 작업물은 로컬 환경이기 때문입니다. 앞서 깃허브의 저장소를 remote라고 부른 것과 대조됩니다.

     

     

    2. 3) add, commit 

    간단한 "hello world"를 출력하는 java 파일을 하나 만들겠습니다. 그냥 텍스트 파일도 관계없습니다.

    public class Hello {
        public static void main(String[] args) {
            System.out.println("Hello World");
        }
    }
    • Hello.java 파일 생성

     

    그렇다면 이 파일을 그대로 remote 저장소에 올리면 될까요? 아닙니다. git, github는 단순히 코드를 저장하기 위해서만 존재하지 않습니다. 버전 관리, 변화 추적, 기록 등이 핵심인데요. 

     

    local에서의 작업물을 staging area(index) 공간에 올리는 작업이 필요합니다. 그리고 이것을 local 저장소에 기록하는 과정이 필요합니다. remote(원격) 저장소에 업로드 전 local 저장소에 먼저 기록합니다.

     

    출처: https://velog.io/@aaronddy/Git-과-GitHub-sik46irf9x

     

    add 명령어를 사용하여 staging area(index)에 등록합니다. 이러한 일련의 작업을 staging(스테이징) 이라고도 부릅니다.

    // Hello.java를 staging area에 올린다
    $ git add Hello.java
    $ git status
    
    
    -- 출력
    On branch master
    
    No commits yet
    
    Changes to be committed: // staging area에 등록된 상태
      (use "git rm --cached <file>..." to unstage)
    
            new file:   Hello.java
    
    Untracked files: // staging area 포함 X
      (use "git add <file>..." to include in what will be committed)
    
            ../.idea/
            ../git-practice.iml
            ../out/

    이제 staging area(index)에 기록된 Hello.java를 commit 해보겠습니다.

     

    commit은 영어단어로의 의미는 '위탁하다', '기록으로 남기다'라는 뜻을 가지고 있습니다. 즉 commit은 본격적으로 버전 관리를 위해 기록을 하는 행위입니다. 추후에 이러한 기록들을 바탕으로 누가, 어디서, 어떻게 코드가 변경됐는지 역사를 알 수 있습니다.

     

    commit은 앞서 staging area(index)에 올라온 파일들만 가능한데요. 여기서 staging area 공간이 필요한 이유를 알 수 있습니다. staging area 공간을 활용해 로컬에서 작업한 파일들 중 일부만 add 후 공개할 수 있습니다. 그렇지 않다면, 모든 작업 파일들을 기록하고 공개해야 합니다. 보안에도 큰 문제가 생길 수 있고, 원치 않는 파일을 공개해야 할 수도 있습니다. 

     

    여기서의 기록은 local 저장소에 하고있습니다.

    $ git commit -m "first commit"
    
    [master (root-commit) dbc2ac8] first commit
     1 file changed, 5 insertions(+)
     create mode 100644 src/Hello.java
    • git commit -m [커밋 메시지]

     

    git은 커밋할 때 메시지를 반드시 입력해야 하는데요. 이것은 당연합니다.

     

    git은 변화를 기록하고, 버전을 관리하기 위해서 만들어졌습니다. 하지만 이러한 기록들이 어떤 기록인지 알 수 없다면, 진정한 기록의 의미가 줄어들 것입니다. 그래서 커밋을 할 때는 반드시 추후에 식별 가능한 의미 있는 메시지를 남겨야 합니다.

    // commit 후 
    $ git status
    
    -- 출력
    On branch master
    Untracked files:
      (use "git add <file>..." to include in what will be committed)
    
            .idea/
            git-practice.iml
            out/

    다시 git status를 입력하면, 이전의 Hello.java 파일은 보이지 않습니다. 왜냐하면 이미 local 저장소에 commit 되고 unmodified 상태가 됐습니다.

     

    갑자기 생소한 개념이 나왔습니다. 걱정 마세요.

     

    • modified: track, 변경된 파일
    • unmodified: track, commit 이후 변경 없는 파일
    • staged: commit 위해 staging area에서 준비된 상태

     

    unmodified 된 파일을 변경하면, 다시 modified 상태가 됩니다. 그리고 이를 add 후 staged 상태로 만들고 commit 합니다. 이러한 사이클을 계속해서 반복하게 됩니다.

     

    2. 4) push

    이제 commit 후 local 저장소에 기록된 내용(commit)을 push 하여 remote 저장소에 올려보겠습니다.

     

    아래는 local 저장소에 remote 저장소를 연결합니다. 단순히 origin이라는 이름을 가진 remote 저장소를 추가하는 것입니다.

    // local 저장소에 remote 저장소 연결
    $ git remote add origin https://github.com/YeonCheolGit/git-practice.git
    
    
    $  git remote -v
    
    
    -- 출력
    origin  https://github.com/YeonCheolGit/git-practice.git (fetch)
    origin  https://github.com/YeonCheolGit/git-practice.git (push)
    • git remote add [단축어] [원격 저장소 주소]

     

    현재 자신의 local 환경에서 작업 중인 local 브랜치는 master라는 이름을 가지고 있습니다. 그리고 이를 remote 저장소가 관리하는 브랜치로 등록해보겠습니다. 브랜치는 뒤에서 설명합니다.

    $ git branch // 브랜치 목록
    
    // origin(local 저장소)을 upstream으로 등록
    $ git push --set-upstream origin master
    • git push --set-upstream [remote] [local 브랜치]
      • git push -u [remote] [local 브랜치]도 가능
    • 최초 한 번만 필요, 이후에는 git push 

     

     

    드디어 remote 저장소에 push 했습니다!

     

    2. 4. 1) Index

    그동안 index는 staging area와 묶어서 공간이라고 표현했습니다. 사실 하나의 바이너리 파일이 더 정확한 표현입니다. 

     

    보통은 .git/index 공간에 존재하는데요. git이 관리하는 파일들과 디렉터리의 path name, permission 등이 기록되어있습니다.  

    // .git 디렉터리 보기
    $ ls -al
    
    // index의 파일을 보여준다
    $ git ls-files

     

    commit을 하게 되면 index를 바탕으로 새로운 tree object를 생성합니다. 그리고 이를 object database에 저장하고 새로운 commit이 발생하면 해당 tree object를 사용합니다. 

     

    사실 이러한 tree는 index에만 존재하는 것은 아닙니다. git clone 혹은 git init을 하게 되면 main working tree가 생성됩니다. 그리고 다른 브랜치가 생긴다면 linked working tree가 생겨서 연결되어 뻗어나갑니다. 즉 우리가 작업을 하는 공간을 working area라고 하는데요. 여기에서 이루어지는 작업을 기록하게 됩니다.

     

    index의 tree object는 working tree와 비교하여, 어떤 것이 변경됐는지 쉽게 파악할 수 있습니다. 그래서 변화된 파일을 modified 상태 등으로 파악할 수 있습니다.

     

    tree object는 working tree와 비교하지만, 다른 tree object와도 비교할 수 있습니다. 즉 다른 commit의 tree object와 비교하여, merge 과정에서의 conflict와 같은 상태를 비교할 수 있습니다.


    3) 브랜치 (branch) 단위 작업

    브랜치는 영어 단어 그대로 '지점', '지사'를 생각하면 됩니다. 메인으로 유지되는 기록에서 빠져나와 다른 흐름으로 기록합니다. 

     

    협업에서 이러한 브랜치는 매우 중요합니다. 하나의 기능, 변화 단위로 생성하여 기록하고 합칠 수 있습니다. 그리고 이러한 브랜치 단위를 바탕으로 어떠한 기능과 목적으로 코드가 작성됐는지 유추할 수 있습니다.

     

    제가 좋아하는 돼지국밥으로 예를 들겠습니다.

     

    본점이 장사가 너무 잘 돼 지점을 차립니다. 근데 지점에서 순대국밥을 만들어 팔 수도 있고, 이 순대국밥이 정말 맛있어서 본점의 정식 메뉴로 등록될 수도 있습니다. 그리고 지점에서 만든 순대국밥은 "OO지점 순대국밥"이라고 이름을 짓습니다. 그렇다면 해당 순대국밥이 어디서 만들어졌는지 파악할 수 있습니다.

     

    3. 1) 브랜치 생성 

    브랜치 생성 전 기존의 master 브랜치에서 아래의 명령어를 실행합니다. 

    $ git pull
    • remote 저장소로부터 최신의 버전을 받고, merge(병합) 합니다
    $ git branch hello-world-two // 브랜치 생성
    $ git checkout hello-world-two // 현재 브랜치 변경
    
    
    // 브랜치 생성, 변경 동시에
    $ git checkout -b hello-world-two

    현재 상태

     

    3. 2) hello-world-two 브랜치에서 push

    새롭게 생긴 브랜치에서 수정 후 다시 push 해보겠습니다.

    public class Hello {
        public static void main(String[] args) {
            System.out.println("Hello World");
            System.out.println("Hello World 2"); // 추가
        }
    }
    // 변경된(modified) 파일 add --> staged 상태
    $ git add Hello.java
    
     // commit --> local 저장소 기록
    $ git commit -m "second commit"
    
    // git push —set-upstream [remote] [local]
    $ git push -u origin hello-world-two

     

    위의 명령어를 실행하면 remote 저장소에 아래와 같은 문구가 나옵니다. 

     

    왜냐하면 현재 remote(origin) 브랜치는 기존 local의 master 브랜치의 내용을 가지고 있습니다. 하지만 hello-world-two 브랜치의 새로운 내용을 push 하고 있습니다. 그래서 이것을 해당 저장소에 알리고, merge하는 과정이 필요합니다.

     

    3. 3) PR(pull request)

    pull request는 다른 브랜치에서 작업한 것을 remote(origin) 브랜치에 merge(병합)할 때 필요합니다. 

     

    이때 PR은 작업이 완료가 됐으니 사람들에게 알리는 목적에 있습니다. 이러한 목적을 위해 PR 단계에서는 코드 리뷰하는 사람, 의견 등을 추가할 수 있습니다.

    • 특정 코드에 의견과 제안하는 코드도 남길 수 있습니다

     

    만약 리뷰가 완료됐다면 이제 merge 합니다.

     

    3. 4) merge

    이때  세 가지의 merge 방식이 있습니다. 

    3. 4. 1) merge

    일반적인 merge 방식입니다. 

     

    하나의 브랜치에서 여러 commit을 했을 때 이를 모두 기록하는데요. 그래서 추후에 자세히 확인을 할 때 장점이 있습니다.

     

    하지만 commit 할 때 꼭 의미 있는 단위로만 하는 것은 아닙니다. 현실적으로 의미 없는 오타 수정, 이름 변경 같은 것들이 발생할 수 있는데요. 이때는 사소한 commit들도 다 기록되면서, 그래프가 지저분해집니다. 그래서 나중에 문제를 파악할 때 어렵게 됩니다.

    출처:&amp;amp;amp;amp;nbsp;https://im-developer.tistory.com/182

    3. 4. 2) squash and merge

    sqaush는 "짓 누르다", "밀어 넣다"라는 영어 단어 의미로도 추측이 됩니다. 

     

    위와 같이 하나의 브랜치에서 여러 개의 commit이 발생했을 때 이를 하나로 합칩니다. 즉 일반적인 merge에 비해 깔끔한 그래프를 얻을 수 있습니다. 즉 commit 단위의 그래프가 아닌, 브랜치 단위의 그래프로 볼 수 있습니다.

     

    하지만 깔끔함을 위해서 자세한 정보를 포기해야 합니다. 일반적인 merge는 어떤 commit이 발생했는지 상세하게 알려주지만, sqaush and merge는 하나의 commit으로 합쳐져서 표기되기 때문에 알 수가 없습니다. 즉 merge 된 사실은 알 수 있으나, 어떤 목적으로 어떤 코드를 변경했는지 알 수 없습니다. 

     

    그리고 저는 이 글에서 squash and merge를 선택합니다.

    출처:&amp;amp;amp;amp;nbsp;https://im-developer.tistory.com/182

     

    3. 4. 3) rebase and merge

    rebase는 모든 commit을 하나의 줄기로 합칩니다. 

     

    모든 commit을 살리는 점에서 일반적인 merge와 비슷해 보입니다. 하지만 일반적인 merge는 병합하는 과정을 줄기로 표현합니다. 하지만 rebase는 merge commit이 남지 않습니다. 그래서 해당 commit이 어디서, 언제 merge 됐는지 알 수 없습니다. 

    출처:&amp;amp;amp;amp;nbsp;https://im-developer.tistory.com/182

     

     

    squash and merge 후 새로운 커밋이 생겼습니다. 그리고 hello-world-two 브랜치도 보입니다. 

     

    3. 5) pull

    다시 원래의 master(local) 브랜치로 이동합니다.

    $ git checkout master

    현재 master 브랜치는 origin(remote) 브랜치의 두 번째 커밋이 반영되지 않은 상태입니다. 

    public class Hello {
        public static void main(String[] args) {
            System.out.println("Hello World");
            // Hello World 2 없는 상태
        }
    }

    origin(remote) 브랜치에서 최신의 기록을 pull 합니다.

    $  git pull
    public class Hello {
        public static void main(String[] args) {
            System.out.println("Hello World");
            System.out.println("Hello World 2"); // 생김
        }
    }

    origin(remote) 브랜치에 반영된 "Hello World 2"가 생긴 것을 볼 수 있습니다. 

     

    이후에는 지금까지의 흐름과 같이 branch 생성하고 작업하면 됩니다. 

     

     

     

     

    반응형

    댓글

Designed by Tistory.