Git 입문

목표: add·commit·push만 외워 쓰는 단계를 넘어, “rebase 해서 올려 주세요”, “force push 하지 마세요”, “reset 했더니 커밋이 날아갔어요” 같은 상황에서 저장소 안에서 무엇이 움직였는지 떠올리고 안전하게 대처하기.

clone·commit·push의 기본 흐름이 낯설다면 컴퓨터 기본 도구와 개발 환경 입문의 Git 절을 먼저 읽는 편이 좋다.


한 줄로 먼저

Git은 프로젝트 전체의 사진(스냅샷)을 커밋으로 찍어 사슬처럼 잇고, 브랜치라는 이름표를 그 사슬의 한 지점에 붙여 두는 도구다.

거의 모든 Git 명령은 다음 넷 중 하나를 움직인다.

무엇어디에 있나움직이는 대표 명령
작업 폴더 (working tree)에디터로 보고 고치는 파일들편집, git restore, git switch
스테이징 영역 (index).git/index. 다음 커밋의 초안git add, git restore --staged
커밋 그래프.git/objects. 지금까지 찍은 스냅샷git commit, git merge, git rebase
이름표 (브랜치, HEAD).git/refs, .git/HEADgit switch, git reset, git branch
작업 폴더 ──git add──▶ 스테이징 ──git commit──▶ 새 커밋 (현재 브랜치 이름표가 앞으로 이동)

0. 왜 “제대로” 알아야 하나

명령만 외우면 평소에는 문제가 없다. 문제는 평소와 다른 일이 생길 때다.

  • git pull을 했더니 에디터가 열리며 “Merge branch ‘main’…” 메시지를 쓰라고 한다.
  • git push가 rejected (non-fast-forward)로 거절된다.
  • git reset --hard를 쳤더니 하루치 작업이 사라졌다.
  • PR을 합쳤는데 로컬 브랜치가 “아직 안 합쳐졌다”고 나온다.

전부 위 표의 넷 중 무엇이 어디로 움직였는지만 알면 설명된다. 그리고 한 번 커밋한 것은 생각보다 잘 사라지지 않는다(7절 reflog).

처음 한 번은 이름과 이메일을 설정한다. 모든 커밋에 영구히 기록된다.

git config --global user.name "Hong Gildong"
git config --global user.email "you@example.com"
git config --global init.defaultBranch main

GitHub은 Git 작업에 계정 비밀번호를 받지 않는다. SSH 키(git@github.com:user/repo.git)나 토큰·gh auth login(HTTPS)을 쓴다. SSH 키를 등록했다면 ssh -T git@github.com으로 확인한다.


1. 세 구역: 작업 폴더, 스테이징, 커밋

스테이징 영역(index) 은 “다음 커밋의 초안”이다. 파일 열 개를 고쳤어도 관련 있는 세 개만 골라 한 커밋으로 묶을 수 있다. git add -p를 쓰면 한 파일 안에서도 바뀐 덩어리(hunk)마다 골라 넣는다.

git add는 그 순간의 파일 내용을 스테이징에 복사한다. add 뒤에 파일을 또 고치면, 그 수정은 다시 add해야 커밋에 들어간다.

상태뜻
untrackedGit이 한 번도 기록한 적 없는 새 파일
modified기록된 적 있는데 작업 폴더에서 바뀜 (아직 add 전)
staged바뀐 내용이 스테이징에 올라감
git diff            # 작업 폴더 vs 스테이징   (아직 add 안 한 변경)
git diff --staged   # 스테이징 vs 마지막 커밋 (커밋하면 들어갈 변경)
git diff HEAD       # 작업 폴더 vs 마지막 커밋 (둘을 합친 전체)

git diff가 비어 있는데 커밋할 게 있다면 이미 전부 add했다는 뜻이다. 커밋 직전에 git diff --staged를 훑어보는 습관이 좋다.


2. 커밋의 정체: 스냅샷과 해시

커밋은 “차이”가 아니라 “사진”이다

Git의 개념 모델에서 커밋 하나는 그 시점 프로젝트 전체의 스냅샷이다. 바뀌지 않은 파일은 예전 것을 그대로 가리키므로 공간이 낭비되지 않는다. (실제 저장할 때는 압축과 델타를 쓰지만, 이해할 모델은 스냅샷이다.)

객체담는 것
blob파일 하나의 내용 (이름은 없음)
tree폴더 하나. 이름 → blob 또는 하위 tree
commit최상위 tree + 부모 커밋 + 작성자·시각 + 메시지

해시: 내용에서 나온 이름

모든 객체는 내용으로 계산한 해시(hash) 를 이름으로 쓴다. 해시는 데이터를 넣으면 고정 길이 지문이 나오는 함수다. 기본은 SHA-1이라 40자리 16진수이고, 보통 앞 7자리 정도로도 구분된다.

커밋 해시에는 부모 커밋의 해시가 들어간다. 그래서 과거 커밋 하나를 바꾸면 그 뒤 모든 해시가 바뀐다. 5절(rebase)과 7절(amend, reset)의 “공유한 커밋은 바꾸지 말라”는 규칙이 여기서 나온다.

커밋 그래프

이 노트의 그림은 왼쪽이 오래된 것, 오른쪽이 새것이다.

A───B───C───F      ← main
         \
          D───E    ← feature
  • 보통 커밋은 부모가 1개, 첫 커밋은 0개, 합치기 커밋(merge commit)은 2개다.
  • HEAD~1은 HEAD의 부모, HEAD~3은 세 단계 위 조상이다.
  • 그래프를 직접 보려면 git log --oneline --graph --all.

Git은 파일만 추적하므로 빈 폴더는 기록되지 않는다. 남기려면 안에 .gitkeep 같은 빈 파일을 둔다.


3. 브랜치와 HEAD: 커밋에 붙인 이름표

브랜치(branch)는 커밋 하나를 가리키는 이름표다. 복사본이 아니다. .git/refs/heads/main 파일을 열면 커밋 해시 한 줄이 들어 있다. 그래서 브랜치 만들기는 메모 한 줄 쓰기처럼 즉시 끝난다.

HEAD는 “지금 내가 서 있는 곳” 이다. 보통 HEAD는 커밋이 아니라 브랜치를 가리킨다(.git/HEAD 내용이 ref: refs/heads/main).

커밋 전:  A───B───C       ← main ← HEAD
커밋 후:  A───B───C───G   ← main ← HEAD

커밋하면 새 커밋 G가 생기고, HEAD가 가리키는 브랜치의 이름표만 G로 옮겨진다.

git switch -c exp/lr-sweep  # 새 브랜치를 만들고 이동 (예전 방식: git checkout -b)
git switch main             # 이동. 작업 폴더 파일도 그 커밋의 스냅샷으로 바뀜
git branch -d exp/lr-sweep  # 합쳐진 브랜치 삭제 (-D는 강제)

git switch와 git restore는 Git 2.23에서 들어왔다. 예전 git checkout이 “브랜치 이동”과 “파일 되돌리기”를 둘 다 하던 것을 나눈 것이다. 옛 문서의 checkout은 둘 중 무엇인지 구분해서 읽는다.

detached HEAD

git switch --detach a1b2c3d(또는 git checkout a1b2c3d)처럼 커밋으로 직접 이동하면 HEAD가 브랜치 대신 커밋을 바로 가리킨다. 이를 detached HEAD라고 한다. 예전 코드로 실험을 재현할 때 쓴다. 여기서 만든 커밋은 어느 브랜치도 가리키지 않으므로, 남기려면 떠나기 전에 git switch -c 새이름으로 이름표를 붙인다.


4. 합치기 1: merge

git merge feature는 “현재 브랜치에 feature를 합쳐라”는 뜻이다. 상황에 따라 두 방식 중 하나로 일어난다.

fast-forward: 이름표만 앞으로

main이 그동안 움직이지 않았다면 이름표를 밀기만 하면 된다. 새 커밋은 생기지 않는다.

전:  A───B───C          ← main
              \
               D───E    ← feature
후:  A───B───C───D───E  ← main, feature

3-way merge: 합치기 커밋을 새로 만든다

양쪽이 모두 나갔다면 이름표를 밀 수 없다. Git은 두 끝(F, E)과 공통 조상(merge base, C), 세 지점을 비교한다.

A───B───C───F───M   ← main
         \     /
          D───E     ← feature
  • 한쪽만 바꾼 부분은 그 변경을 가져온다.
  • 양쪽이 같은 곳을 다르게 바꿨으면 충돌(conflict) 이 난다(6절).
  • 결과는 부모가 둘인 merge commit M 이다. 두 갈래가 합쳐졌다는 사실이 이력에 남는다.

fast-forward가 가능해도 merge commit을 남기려면 --no-ff, fast-forward만 허용하려면 --ff-only를 붙인다.


5. 합치기 2: rebase

rebase는 커밋을 “다시 쌓는다”

git rebase main은 “내 브랜치의 커밋을 떼어 main 끝 위에 하나씩 다시 적용하라”는 뜻이다.

전:  A───B───C───F           ← main
              \
               D───E         ← feature (HEAD)
후:  A───B───C───F───D'───E' ← feature (HEAD)
                 ↑ main

D', E'는 내용은 같지만 부모가 바뀐 새 커밋이라 해시도 다르다. 이제 main에서 git merge feature를 하면 fast-forward로 한 줄이 된다.

git rebase -i HEAD~4(대화형 rebase)를 쓰면 최근 커밋 4개가 에디터에 열린다. 각 줄의 pick을 squash·fixup(합치기), reword(메시지 고치기), drop(지우기)으로 바꿔 PR 전에 자잘한 커밋을 정리한다.

구분mergerebase
하는 일두 갈래를 잇는 새 커밋을 만듦내 커밋을 상대 끝 위에 다시 만듦
기존 커밋그대로 둠새 해시의 복사본으로 바꿈
이력 모양갈래와 합류점이 보임한 줄로 곧게
충돌한 번에커밋마다 따로 날 수 있음
공유된 브랜치에써도 된다쓰지 않는다

황금률: 남과 공유한 커밋은 rebase하지 않는다

이미 push해서 남이 받아 간 커밋을 rebase하면 양쪽 이력이 갈라진다. 그 사람이 다음에 pull하면 같은 변경이 두 번 들어가거나 충돌이 쏟아진다. 그래서 혼자 쓰는 브랜치(내 PR 브랜치)는 rebase로 최신 main에 맞춰도 되고, 함께 쓰는 브랜치(main)에는 merge만 쓴다.


6. 충돌 해결 절차

충돌은 고장이 아니라 Git이 사람에게 판단을 맡긴 상태다.

  1. git status로 어떤 파일이 both modified인지 본다.

  2. 파일을 열어 표시를 찾는다. ======= 위가 지금 브랜치(ours), 아래가 합치려는 쪽(theirs) 이다.

    <<<<<<< HEAD
    lr = 3e-4
    =======
    lr = 1e-4
    >>>>>>> feature
  3. 원하는 최종 모습으로 고친다. 한쪽을 고르거나, 섞거나, 새로 쓴다. 표시 줄 세 개는 반드시 지운다.

  4. git add 파일 후, merge 중이면 git commit, rebase 중이면 git rebase --continue.

  5. 실행해서 확인한다. 표시가 사라졌다고 코드가 맞는 것은 아니다.

  • 처음부터 다시 하려면 git merge --abort 또는 git rebase --abort.
  • 한쪽으로 통째로 정하려면 git checkout --ours -- 파일(또는 --theirs) 후 git add.
  • rebase 중에는 ours와 theirs가 뒤집힌다. rebase는 main 위에 내 커밋을 올리는 과정이라 그 순간 HEAD가 main 쪽이기 때문이다.
  • git config --global merge.conflictStyle diff3을 켜면 공통 조상의 원래 내용도 함께 보여서 판단이 쉬워진다.
  • 충돌을 줄이는 가장 좋은 방법은 브랜치를 짧게 유지하고 자주 합치는 것이다.

7. 되돌리기

상황별 명령

하고 싶은 일명령주의
작업 폴더 수정 버리기git restore 파일커밋 안 한 수정은 복구 불가
스테이징만 취소git restore --staged 파일수정은 남음
파일 하나를 예전 버전으로git restore --source=HEAD~2 파일작업 폴더를 덮어씀
마지막 커밋 고치기git commit --amend해시가 바뀜. push 전에만
최근 커밋 취소 (로컬)git reset아래 세 모드
공유한 커밋 취소git revert 커밋안전
사라진 커밋 찾기git reflog안전
하던 일 잠깐 치우기git stash대체로 안전

reset의 세 모드

git reset <커밋>은 현재 브랜치 이름표를 그 커밋으로 옮긴다. 모드는 스테이징과 작업 폴더를 어디까지 같이 맞추는지를 정한다.

모드이름표스테이징작업 폴더쓰는 때
--soft옮김그대로그대로커밋 몇 개를 하나로 다시 묶을 때
--mixed (기본)옮김맞춤그대로커밋을 풀고 add부터 다시 할 때
--hard옮김맞춤맞춤그 시점으로 완전히 돌아갈 때
git reset --soft HEAD~3   # 최근 커밋 3개 취소, 변경은 staged로 남음 → 한 번에 다시 커밋
git reset HEAD~1          # 최근 커밋 취소, 변경은 작업 폴더에 남음
git reset --hard HEAD~1   # 최근 커밋과 그 변경을 작업 폴더에서까지 지움

--hard는 커밋하지 않은 수정까지 지운다. 실행 전에 git status로 남은 수정이 없는지 확인한다.

revert: 지우지 않고 반대 커밋을 쌓는다

git revert a1b2c3d는 그 커밋의 변경을 거꾸로 하는 새 커밋을 만든다. 이력은 앞으로만 쌓인다. 이미 push한 main의 잘못은 reset이 아니라 revert로 고친다.

reflog: Git의 블랙박스

reflog는 HEAD와 브랜치 이름표가 어디를 거쳐 왔는지 적은 로컬 기록이다. reset, rebase, amend로 “사라진” 커밋도 대부분 여기 남는다.

git reflog
# 9f8e7d6 HEAD@{0}: reset: moving to HEAD~1
# a1b2c3d HEAD@{1}: commit: 학습률 warmup 추가
git switch -c rescue a1b2c3d   # 그 커밋에 새 이름표를 붙여 살림

한계도 있다. 내 컴퓨터에만 있고, 오래된 항목은 정리되며(기본 설정에서 대략 수십 일~90일), 커밋된 적 있는 것만 살릴 수 있다.

stash: 하던 일을 잠깐 서랍에

git stash push -m "로더 리팩터링 중"   # 수정을 치우고 작업 폴더를 깨끗하게
git stash list                         # 목록
git stash pop                          # 꺼내 적용하고 목록에서 삭제 (apply는 남김)

기본적으로 untracked 파일은 치우지 않는다. 새 파일까지 넣으려면 -u를 붙인다. stash는 쌓아 두면 잊기 쉬우니, 하루 넘게 둘 거라면 브랜치를 만들어 커밋한다.


8. 원격 저장소

origin과 origin/main

원격(remote) 은 다른 곳에 있는 같은 저장소 주소에 붙인 별명이다. clone하면 원래 주소에 origin이 붙는다(git remote -v로 확인).

내 저장소에는 main 말고 origin/main 이라는 이름표도 있다. “마지막으로 확인했을 때 원격 main의 위치” 를 기억하는 읽기 전용 이름표다. 실시간이 아니라 fetch할 때만 갱신된다.

명령하는 일내 브랜치가 바뀌나
git fetch원격의 새 커밋을 받고 origin/*만 갱신안 바뀜
git pullfetch + 내 브랜치에 합치기(merge 또는 rebase)바뀜
git push내 커밋을 올리고 원격 브랜치를 앞으로 옮김원격이 바뀜

pull이 merge commit을 만들거나 충돌을 내는 이유는 그 안에 합치기가 숨어 있어서다. 먼저 보고 싶으면 나눠서 한다.

git fetch
git log --oneline main..origin/main   # 원격에만 있는 커밋
git merge origin/main                 # 또는 git rebase origin/main

pull의 합치기 방식은 git config --global pull.rebase true(rebase) 또는 pull.ff only(fast-forward만)로 정해 둘 수 있다. 최근 Git은 설정 없이 양쪽이 갈라져 있으면 방식을 정하라고 안내한다.

upstream과 push 거절

git push -u origin exp/lr-sweep

-u는 “이 로컬 브랜치의 짝(upstream)은 origin/exp/lr-sweep”이라고 기억시킨다. 이후엔 git push, git pull만 쳐도 된다. git branch -vv로 짝과 ahead 2, behind 1 같은 차이를 본다.

push가 rejected (non-fast-forward)로 거절되면 원격에 내가 받지 않은 커밋이 있다는 뜻이다. Git이 남의 커밋을 덮어쓰지 않으려는 것이다. 먼저 받아서 합친 뒤 다시 push한다.

force push는 덮어쓰기다

git push --force는 원격 이름표를 내 것으로 강제로 옮긴다. 그 사이 남이 올린 커밋은 원격 브랜치에서 사라진다.

  • main처럼 함께 쓰는 브랜치에는 하지 않는다. GitHub에서는 보통 branch protection 규칙으로 막아 둔다.
  • 내 PR 브랜치를 rebase한 뒤에는 필요하다. 이때는 git push --force-with-lease 를 쓴다. 내가 마지막으로 본 원격 상태와 지금이 다르면 거절해 주므로, 모르고 남의 커밋을 지울 위험이 줄어든다.

9. GitHub 협업 흐름

브랜치 방식과 fork 방식

  • 브랜치 방식: 저장소에 쓰기 권한이 있을 때(연구실 공용 저장소). 같은 저장소에 브랜치를 push하고 PR을 연다.
  • fork 방식: 권한이 없을 때(오픈소스 기여). 내 계정에 복사본(fork)을 만들어 거기에 push하고, 원래 저장소로 PR을 연다. 관례상 내 fork를 origin, 원래 저장소를 upstream이라는 원격 이름으로 부른다(8절의 upstream 브랜치와 이름만 같다).
git remote add upstream https://github.com/original/repo.git
git fetch upstream && git rebase upstream/main   # 원래 저장소 최신에 맞추기

PR 한 번의 일생

git switch main && git pull                 # 1. 최신 main에서 출발
git switch -c fix/tokenizer-padding         # 2. 작업 하나당 브랜치 하나
git push -u origin fix/tokenizer-padding    # 3. 커밋 후 올리기
  1. GitHub에서 Pull Request(PR) 를 연다. 무엇을, 왜 바꿨고, 어떻게 확인했는지 적는다. 본문에 Closes #12를 쓰면 기본 브랜치에 합쳐질 때 12번 이슈가 닫힌다. 미완성이면 Draft PR로 연다.
  2. 리뷰를 받는다. 리뷰어는 줄마다 댓글을 달고 Comment·Approve·Request changes 중 하나로 답한다.
  3. 고칠 점은 같은 브랜치에 커밋하고 push하면 PR에 자동 반영된다.
  4. 자동 테스트(CI)와 승인이 끝나면 합친다. CI는 CI-CD와 GitHub Actions 입문에서 다룬다.
  5. 합친 뒤 git switch main && git pull, 브랜치 삭제, git fetch --prune(원격에서 지워진 origin/* 정리).

세 가지 합치기 버튼

버튼main에 남는 것
Create a merge commitPR 커밋 전부 + merge commit
Squash and mergePR 전체를 압축한 새 커밋 하나
Rebase and mergePR 커밋들을 main 위에 다시 쌓은 복사본

squash merge 뒤에는 main에 내 원래 커밋이 없고 새 커밋 하나만 있다. 그래서 로컬에서 git branch -d가 “안 합쳐졌다”며 거부한다. 내용은 이미 들어갔으니 -D로 지운다. 0절의 마지막 의문이 이것이다.


10. 좋은 커밋 습관과 .gitignore

좋은 커밋

  • 한 커밋에는 한 가지 이유. 버그 수정과 README 오타는 따로. 그래야 하나만 revert할 수 있다.
  • 커밋마다 돌아가는 상태. 그래야 12절의 git bisect가 잘 동작한다.
  • 메시지는 “무엇을”보다 “왜”를. 무엇은 diff가 보여 준다.
배치 크기 1에서 attention mask 차원 오류 수정      ← 제목: 짧게, 마침표 없이
 
padding 없는 배치에서 mask가 (B, T) 대신 (T,)로     ← 한 줄 비우고 본문: 왜
만들어져 broadcast가 틀어졌다. 항상 2차원으로 만든다.

영어 제목은 50자 안팎의 명령형(Fix, Add)이 관례다. feat:, fix: 머리말을 붙이는 Conventional Commits를 쓰는 프로젝트도 있다. 저장소에 정해진 방식이 있으면 따른다.

.gitignore

Git이 무시할 파일 패턴을 저장소 맨 위에 적는다.

# 주석은 줄 맨 앞의 #만 인정된다
__pycache__/
.venv/
.env
# 폴더 전체
data/
checkpoints/
wandb/
# 모든 위치의 .pt 파일
*.pt
# 예외: 이 파일은 추적
!data/README.md
.DS_Store

이미 커밋된 파일은 .gitignore에 적어도 계속 추적된다. 추적을 끊으려면 git rm --cached -r checkpoints/(디스크의 파일은 남김) 후 커밋한다. .DS_Store처럼 모든 프로젝트에 공통인 것은 전역 무시 파일(~/.config/git/ignore)에 한 번 적는다.


11. 큰 파일과 비밀 정보

Git은 모든 버전을 저장소에 영구히 담는다. 1 GB 체크포인트를 열 번 고쳐 커밋하면 지금 파일이 하나여도 clone하는 모두가 그 이력을 내려받는다. 나중에 지워도 과거 커밋에 남는다. GitHub도 일반 push에서 일정 크기(현재 문서 기준 100 MB)를 넘는 파일은 거부한다.

도구방식어울리는 것
Git LFSGit에는 포인터 파일만, 실제 내용은 별도 저장소에저장소와 함께 다녀야 하는 중간 크기 파일
Hugging Face Hub모델·데이터셋 전용 저장소공유할 가중치, 데이터셋
DVC데이터 버전을 Git으로, 실제 파일은 S3·서버에데이터 파이프라인 버전 관리
서버 디스크Git에는 경로와 체크섬만 기록연구실 공용 스토리지의 큰 데이터
git lfs install                  # 사용자 계정당 한 번
git lfs track "*.safetensors"    # .gitattributes에 기록됨
git add .gitattributes model.safetensors && git commit -m "가중치를 LFS로 추가"

track은 그 뒤에 추가하는 파일에만 적용된다. 이미 일반 커밋으로 들어간 파일은 git lfs migrate로 이력을 다시 써야 옮겨진다. GitHub LFS에는 요금제별 용량·전송량 한도가 있으니, 큰 체크포인트는 처음부터 다른 곳에 두는 편이 대개 낫다.

비밀 정보를 push했다면 먼저 그 키를 폐기하고 새로 발급받는다. 이력에서 지우는 것(git filter-repo 등)은 그다음이고, 공유 이력을 다시 쓰는 일이라 팀 전원이 다시 받아야 한다. 처음부터 .env를 .gitignore에 넣는 것이 가장 싸다.


12. 연구 코드 관점

결과마다 커밋 해시를 남긴다

“3주 전 표의 그 숫자는 어떤 코드에서 나왔지?”에 답하려면 실험 기록에 커밋 해시가 있어야 한다. 해시 하나가 그 시점 코드 전체를 정확히 가리킨다.

import subprocess
 
def git_info():
    run = lambda *cmd: subprocess.check_output(["git", *cmd], text=True).strip()
    return {
        "commit": run("rev-parse", "HEAD"),
        "dirty": bool(run("status", "--porcelain")),  # 커밋 안 한 변경이 있나
    }
# 결과 폴더의 meta.json에 config와 함께 저장
  • dirty가 True면 해시만으로는 재현되지 않는다. 실행 전에 커밋하는 것이 가장 좋다. 급하면 git diff HEAD > outputs/run1/uncommitted.patch로 변경을 같이 남긴다(untracked 새 파일은 빠진다).
  • 셸에서는 git describe --always --dirty가 a1b2c3d-dirty 같은 표시를 준다.
  • W&B 같은 실험 관리 도구는 커밋 해시를 자동으로 기록하는 기능이 있다. 그래도 dirty 문제는 그대로다.
  • 해시와 함께 잠금 파일, 시드, 데이터 버전도 남긴다. Git이 코드를, Docker 입문의 이미지가 환경을 고정한다.

브랜치, 태그, 서버

  • 아이디어 하나당 브랜치 하나(exp/rope-scaling). 실패한 브랜치도 남겨 두면 “해 봤다”는 기록이 된다.
  • 제출본·결과표 시점에는 태그를 붙인다. 태그는 움직이지 않는 이름표다. git tag -a v1.0-submission -m "제출본" 후 git push origin v1.0-submission(태그는 따로 push해야 올라간다).
  • Mac에서 고치고 GPU 서버에서 돌린다면 scp보다 GitHub을 거쳐 같은 커밋을 받는 것이 안전하다. Mac에서 git push, 서버(ssh user@gpu-server)에서 git pull --ff-only와 git log -1 --oneline으로 확인한다. 서버 사용은 원격 GPU 서버 사용 입문에서 다룬다.

학습 중에는 브랜치를 바꾸지 않는다

git switch는 작업 폴더의 파일을 실제로 바꾼다. 실행 중인 프로그램이 나중에 읽는 설정 파일이나 아직 import하지 않은 모듈이 다른 브랜치 것으로 바뀔 수 있다. 여러 실험을 동시에 돌리려면 git worktree 로 폴더를 하나 더 만든다.

git worktree add -b exp/ablation ../myrepo-exp2   # 같은 저장소, 다른 폴더, 새 브랜치
git worktree remove ../myrepo-exp2

평가 점수가 어느 커밋부터 떨어졌는지 모를 때는 git bisect 가 이진 탐색으로 범위를 좁혀 준다. git bisect start, git bisect bad(지금), git bisect good v1.0(좋았던 때)를 주면 Git이 중간 커밋으로 옮겨 가며 good/bad 판정을 묻는다. 끝나면 git bisect reset.


13. 명령 치트시트

# 상태와 이력
git status
git diff / git diff --staged
git log --oneline --graph --all
git show <커밋>                     # 커밋 하나의 내용
git blame <파일>                    # 줄마다 마지막으로 바꾼 커밋
 
# 기록과 브랜치
git add -p                          # 부분 스테이징
git commit -m "메시지" / git commit --amend
git switch -c <이름> / git switch <이름>
git branch -vv                      # upstream + ahead/behind
 
# 합치기
git merge <브랜치> / git rebase <브랜치> / git rebase -i HEAD~N
git merge --abort / git rebase --abort
 
# 되돌리기
git restore <파일> / git restore --staged <파일>
git reset --soft|--mixed|--hard <커밋>
git revert <커밋>
git reflog
git stash push -u -m "메모" / git stash pop
 
# 원격
git fetch --prune
git pull --ff-only                  # 또는 git pull --rebase
git push -u origin <브랜치>
git push --force-with-lease         # rebase 후 내 브랜치에만
 
# 기록 남기기
git rev-parse HEAD
git describe --always --dirty

14. 아주 짧은 암기

  • 커밋 = 전체 스냅샷 + 부모 커밋. 해시는 내용에서 나온 이름이다.
  • 브랜치 = 커밋을 가리키는 이름표, HEAD = 지금 서 있는 곳.
  • add는 그 순간의 내용을 복사한다. 그 뒤의 수정은 다시 add.
  • fast-forward는 이름표만 이동, 3-way merge는 부모 둘인 새 커밋.
  • rebase는 새 해시로 다시 쌓기. 공유한 커밋에는 쓰지 않는다.
  • 충돌은 고치고 add → commit 또는 rebase --continue. rebase 중엔 ours/theirs가 뒤집힌다.
  • reset은 이름표 이동, revert는 반대 커밋 추가. push한 것은 revert로.
  • 커밋한 것은 reflog로 거의 살린다. 커밋 안 한 수정은 못 살린다.
  • fetch는 받기만, pull은 받고 합치기. origin/main은 마지막으로 본 원격 위치.
  • force push는 --force-with-lease로, 내 브랜치에만.
  • 결과에는 커밋 해시와 dirty 여부를 남긴다.

입문자가 조심할 점

  • git reset --hard와 git restore는 커밋 안 한 수정을 영구히 지운다. 불안하면 먼저 git stash나 임시 커밋으로 남긴다. git clean -fd(추적 안 하는 파일 삭제)도 마찬가지다.
  • push한 커밋은 공유된 것으로 본다. amend·rebase·reset 후 force push하면 남의 저장소와 어긋난다. 함께 쓰는 브랜치는 revert로 고친다.
  • .git 폴더가 곧 모든 이력이다. 지우거나 다른 저장소 안에 통째로 복사하지 않는다.
  • iCloud·Dropbox 같은 동기화 폴더에 코드 저장소를 두는 것은 피한다. 동기화 프로그램이 .git 안의 파일을 중간에 바꾸거나 충돌 사본을 만들면 저장소가 깨질 수 있다. 백업과 공유는 원격 저장소가 맡게 한다.
  • 비밀 정보는 커밋 전에 막는다. 올라간 키는 이력에서 지워도 노출된 것으로 보고 폐기한다.
  • 모르겠으면 git status부터. 지금 merge 중인지 rebase 중인지, 다음에 무엇을 치면 되는지를 대개 거기에 적어 준다.

출처