깃에 대해 내가 알고 있는 것과 모르는 것

깃은 알다가도 모르겠고 또 안다고 생각했는데 잊어먹어서 헷갈리는 경우가 많다. 남에게 가르쳐줄 수 있을 정도로 확실히 안다고 자신하는 게 아닌 상태라면 애매한 부분들에 대해 틈틈이 이런저런 테스트를 직접 해보며 몸으로 기억하는 게 맞는 것 같다. 아래는 내가 확실히 알거나, 직접 테스트해서 알아낸 것들이다.

알고 있는 것

  • git clone url: 원격 리포지토리를 폴더 하나로 통으로 받아온다.
  • git clone url .: 리포지토리 내부에 README.md파일 등 root 경로에 있는 폴더 및 파일들, .git 폴더들을 받아온다. 폴더를 따로 안 만들고 바탕화면 같은 곳에서 시도하면 무수히 많은 파일들이 다운받아질 수 있으나, 원격의 프로젝트명과 로컬의 프로젝트명을 다르게 할 수 있다는 장점(?)이 있다.
  • 새로 생성한 파일이 있으면 git commit -am 옵션이 안 된다.
  • pull = fetch + merge branch
  • 병합이란 최신의 incomming 커밋과 현재의 commit을 병합하는 것이다.
  • fast forward vs 3 way merge
    • fast-forward(분기한 이후 메인 브랜치가 커밋이 없을 때): 병합 커밋 미생성.
    • 3 way merge(분기한 이후 메인 브랜치도 커밋이 있을 때): 병합 커밋 생성(충돌 없어도)
  • rebase: 분기의 뿌리(trunk)였던 브랜치에서 새 커밋이 있었을 때, 뿌리 브랜치의 최신 커밋 위에 커밋한 것처럼 바꾸는 기능이다. 3 way merge와의 차이점은, 같은 파일을 건드려 충돌이 날 때 조차도 병합 커밋을 만들지 않는다. 분기된 브랜치에서 rebase를 시도했을 때 Current Change는 main 브랜치의 내용을 보여준다(현재 브랜치의 작업물이 Incoming Change로 보여진다). 분기된 브랜치의 커밋 개수만큼 진행한다(일단 분기된 브랜치를 따로 어디다 떼어놓고, 최신의 母브랜치 위에 한 개씩 붙이는 느낌으로 진행된다).
  • 깃을 버전 관리 시스템(VCS)이라고들 한다. 어렵게 설명할 것 없이 여기서 버전이란 ‘커밋’이다. 깃은 커밋 중심으로 돌아가며 나머지(브랜치, 태그, HEAD)는 포인터에 불과(?)하다(프로젝트 폴더에서 git init을 하면 생성되는, 버전 추적을 담당하고 있는 .git 폴더 내부를 보면 브랜치, 태그와 똑같은 이름의 파일이 몇 kb밖에 용량 차지 안 하는, 거의 바로가기 파일과 같은 느낌으로 생성되어져있을 뿐이다).
  • 기본적으로 head는 프로젝트에서 단 하나이며 통상 브랜치 포인터를 참조하고, 브랜치 포인터는 커밋을 참조한다. checkout은 head의 참조를 바꾼다. 직접적으로 커밋을 참조할 수도 있다. reset은 브랜치의 참조를 바꾼다.
  • 깃은 커밋 중심으로 돌아가며, 커밋은 삭제되지 않는다. 따라서 사실 과거는 git reset –hard로도 지워지지 않는다. git reset –hard를 했더라도, 최신의 커밋 해시를 입력해서 git reset하면 다시 최신의 커밋으로 돌아와 있다.
  • vs코드 기준, 변경 사항을 stage에 올리지 않은 상황이라면 원격에서 변경이 있을 때 sync할 게 있다는 버튼(hover 시 뜨는 메시지 내용: Pull [n] commits from from origin/main)이 뜨지만, 스테이징한 게 있는 상태라면 내려받을 커밋 개수에 대한 정보가 담긴 버튼이 안 보이고 커밋 버튼만 보인다. 이 때 충돌 사항이 있다면? 싱크 버튼 눌러도 please clean your repository working tree before checkout이라며 실패한다. cli로 fetch+merge, pull 시도해도 당연히 다 실패. cli에서는 다음과 같이 더 구체적으로 말해준다. Your local changes to the following files would be overwritten by merge: “충돌난 파일명”. Please commit your changes or stash them before you merge. 만약 같은 파일에 같은 줄이 달라지는 게 아닐지라도 아무튼 같은 파일을 건드리면 위의 메시지를 발생시키며 실패한다(바뀐 파일을 스냅샷 찍기 때문). 같은 파일을 건드린 게 없으면 당연히 pull 성공
  • git pull로 땡겨올 때 내가 위치한 브랜치만 싱크가 맞춰지며, 다른 브랜치의 변경 내역은 해당 브랜치로 가야 한다. 생각해보면 당연하다. 같은 브랜치 하나를 원격에서 pull로 땡겨올 때 충돌이 있으면 커밋을 하거나 stash해두라고 하면서 취소되며, 커밋 후 pull 시도 시 여러 파일이 충돌난 경우 충돌 해결하는 것도 고생인데, 여러 브랜치를 pull한다는 건 권고되지 않는 것일 테다.
  • 충돌이 일어나는 것은 당연한 일이지만 피곤한 일이다. 애초에 충돌이 나지 않게 서로 다른 브랜치에서 작업하는 것은 물론이고 서로 다른 파일에서 작업하는 게 속 편하다.
  • 좋은 버전 관리를 위해선 깃 커밋 컨벤션, 깃 브랜치 관리 전략이 있어야 한다. main/develop/feature/hot-fix/release 로 브랜치를 나누는 gitflow 전략이 있으나, 이렇게 많은 브랜치를 만들면 관리 포인트가 많아져 각각의 브랜치가 어떤 개발 단계에 있으며 내부적으로 어떤 이슈가 있는지 알고는 있어도 고치지 않고, 명세화도 안 하고 있다가 잊어먹게 되고, 나중에 브랜치를 합치고 나서야 이슈를 발견하게 되는 경우가 있으므로, 여러 브랜치를 만들지 말고 main과 develop브랜치만 만들고 CI/CD 시스템을 구축해서 테스트를 많이 하는 체계가 더 낫다는 의견이 있는데, 이 의견에 동의한다.
  • stash의 용처: 브랜치 이동하기 전 변경 내역이 있어서 넘어갈 수 없다고 뜰 때, 원격에서 변경 내역이 있어서 pull 받으려는데 충돌 파일이 있어 실패할 때 사용된다(이럴 때 커밋을 하거나 stash 하라고 뜬다). 깃에 의해 이력 관리가 되고 있는 파일들만 이동되기 때문에, stash로 커밋 전의 변경 사항에 대해 일종의 임시 저장소에 보내기 전에 git add .을 하거나 git stash -u 옵션을 붙여야 좋다. 그리고 stash를 통해서 일종의 임시 저장소에 옮긴 파일들은 다른 브랜치에서도 꺼내서 적용할 수 있기 때문에, 잘못된 브랜치에서 작업하고 있었을 경우 stash로 옮겨놓고 옳은 브랜치로 가서 거기서 stash함에 있던 버전을 적용하는 용도로도 사용할 수 있었다.
  • 깃허브는 깃랩과 달리 fork한 리포지토리에 대해 관계를 끊을 수가 없다. 웬만한 IDE툴에서 일정 시간마다 fetch가 수행되어 원격으로부터 pull 받을 새 커밋 사항이 있다는 정보를 확인할 수 있는데, 오픈소스 프로젝트를 떠온 경우 더 이상 새 버전에 대한 정보를 받고 싶지 않고 merge할 생각도 없을 때가 있다. 이런 경우 fork 방식이 아니라 zip으로 받거나 버전 관리 없이 프로젝트를 다운받아서 시작하는 게 속 편하다.
  • 커밋 이력을 변경시키기 때문에 강제푸시를 하게 되는 기능(amend, rebase)은 반드시 나 혼자 쓰는 브랜치 즉 다른 사람이 내 브랜치를 pull 받아 쓸 일이 없는 브랜치에서 해야 한다.
  • 깃허브 리포지토리에서 .을 누르거나 url의 .com 부분을 dev로 변경 시 웹에서도 코딩이 가능하다.
  • 인터랙티브 리베이스(git rebase -i)는 신세계다. 커밋의 순서를 재조정할 수 있으며, 훨씬 이전의 커밋명도 바꿀 수 있으며, 여러 개 만들었던 커밋을 하나의 커밋으로 합칠 수도 있다.
  • cherry pick: 원하는 커밋(들)만 따온다.
  • outer 프로젝트 안에 git init 후 inner 프로젝트 생성 및 거기서도 git init해서, git init할 때 어떤 파일들이 생성되는지, 브랜치 만들 때 어떤 일이 생기는지 등 관찰하는 테스트를 해봤는데, 블랙홀마냥 밖에서는 안에서의 변경 사항을 관측할 수 없었다. 추적되지 않았다.
  • 20년 넘게 서비스되고 있는 레거시 프로젝트에 배정받아 일단 메인 브랜치를 클론받아 온다고 가정했을 때, 모든 커밋 이력을 받아오는 건 부담되는 일일 것이다. 특정 커밋 이후만 클론받아 올 수 있는데, git clone {url} --depth=1 이런 식으로 적으면 최신의 한 개의 커밋 이력만 가져온다(shallow commit이라 한다). 100개의 메인 브랜치 커밋 이력이 있을 때, –depth=10으로 하면 최근 10개의 메인 브랜치의 커밋 히스토리와 해당 커밋 이후에 메인 브랜치에서 뻗어져 나온 브랜치 정보만 받아온다(git branch -vv로 확인).
  • git remote add origin {url}은 원격에 있는 저장소와 로컬의 저장소를 엮는다(연결할 때 통상 origin이라는 별칭을 붙인다). 잘 연결됐는지는 git remote -v로 확인한다.
  • git push -u origin main 은 푸시하면서 원격의 main 브랜치에 로컬의 브랜치에서 만든 커밋을 연결하면서 올린다. 로컬의 브랜치에서 원격의 브랜치에 처음 올릴 때는 이렇게 브랜치마다 서로 관계를 엮어줘야 한다. 이후에는 -u 옵션을 안 써줘도 된다. 로컬과 원격의 브랜치 관계가 서로 잘 엮였는지는 git branch -vv로 확인한다.
  • checkout, reset이 포인터를 변경한다면, reset은 좀 더 근본적으로 커밋이 참조하고 있는 이전 커밋들의 관계를 재조정하는 작업인 것 같다. 따라서 git rebase -i(인터렉티브 리베이스)를 통해 여러 일을 할 수 있다(ex 커밋 순서 재정렬, 커밋 삭제 등). 그 중 직접 해본 건 너무 자잘하게 했던 커밋들을 하나의 기능 단위로 합치기, 바로 직전 커밋 말고 훨씬 이전의 커밋들의 커밋명 변경하기를 해봤다.

잘 모르는 것

  • 비밀키 파일이 public repository에 공개된 커밋이 있은 후 다시 해당 파일을 깃 버전 관리에서 제외해서 운영 중이나, 여전히 몇 커밋 전의 비밀키 파일은 노출되어 있을 때 어떻게 하는 게 좋을지 모르겠다.(-> 팀 개발을 위한 git/github 시작하기 특별편에 방법이 소개되어 있다는 정도만 안다).
  • git pull, git merge, git cherry-pick 셋 모두에 –allow-unrelated-histories 옵션이 있는데, 원격의 것으로 덮어쓰려 할 때는 세 명령어의 차이가 별로 없지 않을까?