Software Workshop

사전 준비 사항



Git 기초

Git 설치 및 초기 설정

git --version

git config --global user.name  "홍길동"
git config --global user.email "hong@example.com"
git config --global init.defaultBranch main
git config --global core.editor vim   
git config --global pull.rebase false           # 초반엔 merge pull 권장
git config --global -l

개념 이해(요약)

git_github.png

로컬 저장소 실습

mkdir acme-notes && cd acme-notes
git init

echo "# Acme Notes" > README.md
mkdir notes
git status

echo "- 첫 노트" > notes/001.txt
git status

git add .
git status

git commit -m "init: seed project with README and first note"
git status

git log --oneline --graph --all

.gitignore 추가:

mkdir -p tmp && echo "scratch" > tmp/a.txt
git status

echo ".DS_Store" >> .gitignore
echo "tmp/"      >> .gitignore
git add .gitignore
git commit -m "chore: add basic .gitignore"
git status
git log --oneline --graph --all

tmp/a.txt는 어떻게 된 것인지 확인해보기


GitHub 연동

GitHub에서 빈 원격 저장소 acme-notes 생성(README 없음)


git push & git clone


git remote add origin https://github.com/<USER>/acme-notes
git remote -v                # origin이 뭔지 확인해볼 것

git branch -l

git branch -M main           # 로컬 브랜치를 강제로 "main" 브랜치를 사용하겠다는 의미
git push origin main         # upstream으로 origin을 선택하고, "main" 브랜치로 push

# 다른 컴퓨터나 다른 디렉토리에서 다음 명령 실행해보기
cd ..
git clone https://github.com/<USER>/acme-notes acme-notes2
cd acme-notes2
git branch -l
git status
git log --oneline --graph --all

# 원래 작업하던 디렉토리에서
cd ../acme-notes
echo "- 두 번째 노트" > notes/002.txt
git add notes/002.txt
git status

git commit -m "feat: add second note"
git status

git push -u origin main     # 위 예제에서 -u 옵션을 안 줬더니 문제가 생김. 여기서 default upstream을 origin으로 선택

# 다른 clone에서 확인해보기
cd ../acme-notes2
git pull
git log --oneline --graph --all

브랜치

브랜치란 나무의 가지가 각각 다른 방향으로 자라듯이 저장소의 형상이 다르게 분화하는 것을 의미함

다시 합칠 수도 있고, 잘라서 붙일 수도 있고, 가지치기할 수도 있음


브랜치 이동


git switch -c master
git branch testing

two-branches.png

브랜치가 있다고 내가 어떤 브랜치에 작업 중인지는 알 수 없음.

내가 작업 중인 브랜치를 가리키는 것은 HEAD



head-to-master.png

git log --oneline --graph --all

git switch testing

head-to-testing.png


touch test.txt
git add test.txt
git commit -m "made a change"
git log --oneline --graph --all

advance-testing.png


git switch master

checkout-master.png


echo "change" > test.txt
git add test.txt
git commit -m "made another change"
git log --oneline --graph --all

advance-master.png

가장 일반적인 컨벤션

이와 달리 마음대로 브랜치를 만들 수 있음. 그러나 팀에서는 보통 유명한 브랜치 컨벤션/전략을 채택함


merge의 기초


초기 상태 - HEAD가 master를 가리키고 있음

git switch master

git switch -c iss53
echo "another file" > test2.txt
git add test2.txt
git commit -m "issue: new file"

basic-branching-3.png

갑자기 hotfix가 필요함

git switch master
git switch -c hotfix
echo "- 추가사항" >> test.txt
git add test.txt
git commit -m "hotfix: append new content"

basic-branching-4.png

git switch master
git merge hotfix

특별히 옵션을 지정하지 않아서 Fast-Forward 모드로 동작함

master 브랜치의 포인터가 hotfix로 이동함

basic-branching-5 (1).png

hotfix가 끝났으므로 삭제

git branch -d hotfix

git switch iss53
echo "made third change" >> test2.txt
git add test2.txt
git commit -m "change: another content"
basic-branching-6.png
git switch master
git merge iss53
git log --oneline --graph --all

C6 커밋을 만들고 3-way merge를 수행함

3-way merge: 공통 조상인 C3와 각 브랜치가 가리키는 C4, C5 커밋을 합쳐서 C6를 만들어냄

master는 C6로 이동함


basic-merging-2.png

불필요한 이슈는 삭제함

git branch -d iss53
git log --oneline --graph --all

브랜치 네트워크가 사라지는 것은 아니고 브랜치를 가리키는 포인터가 사라진 것을 알 수 있음



충돌 해소

git branch iss1
git branch iss2

git switch iss1
echo "new file" > test3.txt
git add test3.txt
git commit -m "feature: new file"


git switch iss2
echo "temporary file" > test3.txt
git add test3.txt
git commit -m "feature: temporary file"

git switch master
git merge iss1
git merge iss2

충돌 발생

자동 병합: test3.txt

충돌 (추가/추가): test3.txt에 병합 충돌

자동 병합이 실패했습니다. 충돌을 바로잡고 결과물을 커밋하십시오.

# 파일 열어 <<<<<<<, =======, >>>>>>> 구간 수동 수정
vi test3.txt

git add test3.txt
git commit -m "feature: final file"

원격 브랜치

origin으로 지정된 원격 저장소의 브랜치를 clone으로 복제하면 다음과 같은 형상이 됨

origin은 "최초의" 원격 저장소를 지칭하는 이름일 뿐이고, 다른 추가 저장소를 추가해서 사용할 수 있음

remote-branches-1.png

여기서 다른 누군가가 변경 작업을 하고 commit/push하게 되면 master는 더 전진하게 됨
나도 변경 작업을 하고 commit을 하면 원격 브랜치 origin/master보다 나의 master가 더 전진하게 됨

remote-branches-2.png

git fetch를 하게 되면 pull과 달리 브랜치 정보만 업데이트함

remote-branches-3.png


다른 서버를 원격으로 추가해서 사용하기

git remote add teamone https://git.team1.ourcompany.com/repo   # 이 저장소 URL은 유효하지 않음
git remote -v

git fetch teamone

remote-branches-5.png


Rebase


나눠진 브랜치를 합치는 방법에는 merge와 rebase 두 가지가 있음


merge

basic-rebase-1.png
git switch master
git merge experiment

basic-rebase-2.png
공통 조상과 각 브랜치의 커밋을 이용하여 3-way merge


rebase

git switch experiment
git rebase master          # 일반적인 merge와 방향이 다름에 주의

이 명령을 실행하면 내부적으로 다음과 같은 변경이 수행됨

우선 experiment 브랜치가 가리키던 커밋들을 근본(base)이 되는 master 브랜치 뒤에 재 적용하고 experiment 포인터가 이동


basic-rebase-3.png

추가적으로 master에 experiment를 merge하면 다음과 같이 rebase에 의한 merge 작업이 완료됨


basic-rebase-4.png

rebase 사용 시 주의사항

이미 공개 저장소에 push한 커밋을 rebase하면 안 됨. 공유 브랜치를 대상으로 rebase하면 안됨


실전 개발


feature 브랜치 생성하고 작업 후 commit/push

git switch -c feature/tags                         # 신규 브랜치 생성
git branch                                         # 현재 브랜치 확인
git branch -vv                                     # 로컬 브랜치와 원격지 브랜치를 같이 확인

echo "- 태그 기능 설계 문서" > docs_tags.md
git add docs_tags.md
git commit -m "docs: draft tag feature design"

git push origin feature/tags                       # (동일한 이름의) 원격지 브랜치에 push
git branch -r                                      # 원격지 브랜치 목록 확인
git branch -vv

main 브랜치에 merge하기

git switch main
git branch -vv

git pull                                           # --ff-only 옵션을 추가하는 게 안전함

git merge --no-ff feature/tags 
git log --oneline --graph --all

git push
git log --oneline --graph --all
git branch -d feature/tags
git push origin --delete feature/tags
git log --oneline --graph --all

merge할 때 --no-ff 옵션을 지정하면 feature 브랜치가 남지만, 옵션을 지정하지 않으면 main 브랜치의 연장선에 놓임

협업을 위해서는 --no-ff 옵션을 이용할 것


브랜치 관리 전략


Git Flow

git-flow_overall_graph.png

(Microsoft) Release Flow

release-flow.png

github flow도 있으나 너무 단순하여 실전에서는 사용하지 않음



Github을 이용한 협업

팀 내 협업 워크플로우

가정

절차

  1. 로컬 작업 브랜치 생성
    clone: origin -> local

    git clone https://github.com/team/repo
    git switch -c feature/tags
    

  2. 기능 구현 후 커밋

    vi docs_tags.md
    git add docs_tags.md
    git commit -m "docs: draft tag feature design"
    

  3. 원격에 브랜치 push
    push: local feature/tags -> origin feature/tags

    git push origin feature/tags
    

  4. Github에서 PR 생성

    • PR이란 Pull Request의 약어로서 origin 저장소로부터 업데이트해달라는(그리고 리뷰해달라는) 요청
    1. 내용 작성
      1. base: main
      2. compare: feature/tags
      • merge 요청: origin feature/tags -> origin main
    2. 리뷰어 지정
    3. 리뷰 진행
  5. 리뷰 의견 반영하여 수정

    git commit -am "fix: apply review"
    git push
    

  6. 리뷰를 마치고 CI를 통과하면 main에 merge

    • 개발자나 권한을 가진 리뷰어가 merge 버튼을 클릭함
    • 팀 규칙에 따라 필요하면 squash merge로 merge할 것 (추천)
    • 로컬과 리모트의 feature/tags 브랜치 삭제

오픈소스 프로젝트에서의 협업

절차

  1. 오픈소스 프로젝트의 저장소를 fork

    • fork된 저장소가 origin이 됨
    • fork: upstream -> origin
  2. 이 저장소를 clone해서 로컬에서 개발
    clone: origin -> local

    git clone https://github.com/me/forked_repo
    

  3. 원본 저장소를 upstream이라는 이름의 remote로 관리

    git remote add upstream https://github.com/owner/original_repo
    

  4. 작업 브랜치 생성
    push: local fix/typo -> origin fix/typo

    git switch -c fix/typo
    vi ...
    git push -u origin fix/typo
    

  5. GitHub에서 Pull Request 생성

    • base: owner/main
    • compare: me/fix/typo
    • merge 요청: origin fix/typo -> upstream main
  6. 코드리뷰 → 요구사항 반영 → CI 통과 → 리뷰어/owner가 Merge

  7. PR이 끝나고 저장소 동기화
    fetch - merge - push: upstream main -> local main -> origin main

    fetch-merge 방식 대신에 pull --rebase 방식도 있음

    # 원본 저장소에서 받아서
    git fetch upstream
    
    # 내 로컬 main 브랜치에 원본 저장소의 main 브랜치를 merge
    git switch main
    git merge upstream/main
    
    # 내 origin의 main 브랜치에 갱신
    git push origin main
    

실무 편의 기능

git diff                               # 변경 사항을 비교
git diff --staged                      # 스테이지에 올라간 변경 사항을 비교

git blame README.md                    # 소스코드의 행별 변경자를 확인

git restore --staged README.md         # 실수로 add한 파일 되돌리기
git restore README.md                  # 워킹 디렉토리 변경 취소

git stash push -m "WIP: batch edit"    # 수정 중인 내용을 잠깐 숨겨두기
git stash list                         # 숨겨둔 내용 목록 확인하기
git stash pop                          # 가장 최근에 숨겨둔 내용을 꺼내오기

git revert <commit_sha>                # 안전하게 되돌리기 (예전 커밋의 내용을 가져다가 새로운 커밋으로 만들기)

커밋 관리 고급 기법


vi test.txt
git commit
git commit --amend                    # 직전 커밋에 대해 추가 메시지 작성

# 스쿼시 준비(fixup)
git commit -m "fix: typo in 002"      # 잘게 나눠 커밋
git commit -m "style: rewrap 002"

# 인터랙티브 리베이스(로컬 전용, 원격 공유 전에)
git rebase -i HEAD~3
# pick → squash/fixup 로 정리, 메시지 정돈

개인 브랜치에서는 rebase w/ fixup & squash

fixup 옵션으로 자잘한 수정사항임을 표지

rebase하면서 squash를 통해 단일 커밋으로 합침

rebase는 반드시 로컬/개인 브랜치를 대상으로만 실행할 것

git switch -c feature/branch

echo "v1" > a.txt
git add a.txt
git commit -m "feat: add a"

echo "v1 fixed" > a.txt
git add a.txt
git commit --fixup=HEAD              # 이 순간 HEAD는 직전 커밋(feat)을 가리킴

sed -i '' 's/fixed/fixed!!!/' a.txt
git add a.txt
git commit --fixup=HEAD~1            # 이 순간 HEAD~1은 전전 커밋(feat)을 가리킴

git rebase -i --autosquash HEAD~3    # 이 순간 HEAD~3는 전전전전 커밋(feat 이전 상태)을 가리키나 존재하지 않음
                                     # 결국 가장 최신 커밋 3개를 포함한 구간을 의미

git push -f origin feature/branch    # rebase 후 push는 강제로 이력을 변경해야 함

rebase & push는 main이나 develop, release 등의 공유하는 브랜치를 대상으로는 수행하면 안 됨

main 브랜치에서는 squash and merge

위 예제와 rebase까지능 동일하게 진행 가능함

-f 옵션 없이 push

git push -u origin feature/branch

Github에서 PR 과정에서 리뷰 후 squash and merge 실행

실수 복구와 디버깅

revert

revert는 커밋의 변경 효과를 반대로 적용한 새로운 커밋을 만듬

변경 내역이 커밋으로 남기 때문에 공유 브랜치에서 협업할 때는 revert를 사용하는 게 적합함


git revert <commit sha>

# revert하면 충돌이 생길 수도 있음. 이 경우 충돌 해소하고 revert --continue 필요
vi test4.txt
git add test4.txt

git revert --continue     # commit 대신

reset

파괴적 복구 방법 - 주의해서 사용할 것. 개인 브랜치에서만 사용할 것


git log --oneline --graph --all
git reset --soft HEAD~2             # 최근 두 개의 커밋을 되돌리고 다시 메시지 작성해서 커밋할 계획
git log --oneline --graph --all

soft 옵션으로 reset한 상태라면 작업 디렉토리에 HEAD와 HEAD~2 사이의 변경은 온전히 남아 있으므로 다시 되돌리기 쉬움

현재 작업 디렉토리를 git add/commit하면 됨


git log --oneline --graph --all
git reset --hard HEAD~2              # 최근 두 개의 커밋을 버림
git log --oneline --graph --all
git reset --hard origin/master       # 다 버리고 origin/master에 일치시킴
git log --oneline --graph --all

hard 옵션으로 reset한 상태라면 작업 디렉토리에 HEAD와 HEAD~2 사이의 변경이 사라지므로 git reflog를 보고 git reset이나 git checkout를 이용해서 되돌리는 수 밖에 없음


reset과 checkout 비교

reset은 HEAD와 develop을 과거 커밋으로 이동하고

checkout은 HEAD만 과거 커밋으로 이동함

reset-checkout.png


HEAD Index Workdir WD Safe?
Commit Level
reset --soft [commit] REF NO NO YES
reset [commit] REF YES NO YES
reset --hard [commit] REF YES YES NO
checkout <commit> HEAD YES YES YES
File Level
reset [commit] <paths> NO YES NO YES
checkout [commit] <paths> NO YES YES NO

bisect

이진 탐색으로 버그가 존재하는 커밋 찾아내기

git bisect start
git bisect bad HEAD
git bisect good v1.0.0
git bisect bad            # 버그와 관련된 테스트가 실패한다면 bad로 표지
git bisect good 
# 반복
# 0123abcd is the first bad commit 이런 메시지가 나오면 수동으로 종료
git bisect reset

자동화할 수도 있는데, test.sh이 0, 1 또는 125를 반환하여 실패, 성공, 빌드실패로 인한 건너뜀을 표지하도록 함

git bisect start
git bisect bad HEAD
git bisect good v1.0.0
git bisect run ./test.sh

참고자료