Git notes

02 May 2020

Git notes

進階技巧

Reference: youtube: 13 Advanced Git Techniques

alias

git config –global alias.ac “commit -am”: 這就是把git commit -am縮寫成git ac, alias後面接的就是縮寫的結果

amend

git commit –amend -m “”: 把最後一個commit的message蓋掉成這個,

或者當commit完後才發現有檔案忘了加的時候, 可以先用git add再用git commit –amend –no-edit,把stage裡的檔案加進最新的那個commit裡面, 但這寫法要注意的是如果commit已經push到remote repo時,再用git add, git commit –amend會導致不同步, push origin會fail (此時就要用git push –force)

revert

回復上一動, undo a commit with a new commit, 當發現push上remote repo的commit有問題時,一個做法是reset以後再push到repo,但這樣會導致remote repo的commit history改變,其他co-work必須也要跟著update, 如果這時他們已經是基於最新的那個錯誤commit下工作時,那就會比較麻煩, 較好的做法就是revert把錯誤commit,發一個新的commit 把那些changes undo.

stash

東西做到一半, 但還沒完成不能push到remote repo, 此時可能要做某些pull remote repo的動作, 或者先去開發其他比較緊急的功能, 那勢必要先把現在的狀態存起來, 然後在基於上一個commit的code的狀態去處理比較緊急的事情 (總不能基於現在開發一半的code來做那個比較緊急的事情然後把緊急做的處理跟之前開發一半的功能一起push上去remote repo, 當然有替代方案, 可以利用git add -p來做一些ad-hoc小規模修改, 但大規模修改就不適用),

git stash會把目前本地端的改變存起來, 然後code回到上一個commit的狀態(未改變前), 這樣就可以先擱置目前在做的事情, 先去做其他緊急的事

git stash -> 存下目前code的狀態, stash本身是用stack的方式實作, 所以復數個stash會存下復數個code的狀態, pop時是Last in First out.

git stash list -> list目前的stash

git stash pop -> 把存在stash裡面的code狀態pop出來, 就是當緊急的事情處理完後,可以開始做那個做到一半的功能, 有specify idx就會使用那個index的code狀態

git stash drop -> 放棄目前的stash

log

git log本身資訊在commit很多, branch很多時很不方便看, 所以可以用下面的指令來模仿github上的inspect的commit flow

git log –oneline –graph – decorate

clean

git clean -df: 用於git reset –hard時,除了把有修改過的檔案回復上一動外, 有時有一些untracked file也會留下來, 導致現在的專案裡還是留存reset的commit不存在的檔案, 此時可以用這個指令把那些額外的檔案清掉

Pull/Fetch

Pull會自動把remote repo的commit pull下來跟當前的做整合, 如果有不一致(例如本地端有新的commit沒push到remote repo, remote repo有commit本地端沒有的)就會自動merge, 或者使用者解完Conflict後自動merge

Fetch的特色就是不會自動merge, 他會把遠端的repo的commit 抓下來放到origin/XXX的branch裡,由使用者確認內容後再決定要不要pull到本地端的Repo來整合remote的改變到local端.

tag

給當前的commit上一個標籤 分為一般輕量標籤跟標示標籤, 輕量標籤用在本地端做為暫時性或一次性的使用, 標示標籤就是真的放到remote repo做的標籤紀錄

輕量標籤: git tag
標示標籤: git tag -am "XXXX"

git tag -d : 刪除標籤

revert

安全的撤銷已經提交的commit, 他會在產生一個新的commit, 內容就是把要取消的那個commit他所有的變化都undo, 如下所示, 要取消B commit, 他就會push一個B’的commit,內容就是把B commit的變化undo

A-B-C-B’

cherry-pick

把某個branch的commit複製一個到現有的branch上, 用途是今天如果commit變化, 但commit在錯誤的branch上時, 就可以用這個指令把她複製到原本想要改的branch上

git structure

git有著以下的structure local端修改 -> index cache -> 進入commit log

進階用法

git reset

在很多時候我們都會有不小心commit錯的時候,這時要undo commit。就是用git reset

git reset 有分

  • soft
  • hard
  • mix

soft

$ git reset --soft <commit>

這個只會改local repo的commit,也就是說你打git log時,後面commit的變化全部不見,變成現在HEAD在上述指令的commit。但是local working directory和index cached(本機的資料夾裡的檔案還是維持原狀,也就是你在本地資料夾所改的,不會因為你reset就全部復原成該commit時的狀態)。 此時只要打git status就可以發現。你在那個commit後所動過的所有file都會列在那上面。但是index cached上面的檔案已經是現在最新的檔案,也就是和你的working tree git diff是不會噴出任何東西的。

hard則會有這個現象,local端做過的改變全部消失,回到commit時的狀況。

Mix (default)

$ git reset <commit>

這個是reset的default,他會保留local working directory(tree)的資料,所以你reset後,本地端改過的資料還是會維持原狀,不同的是,index cached裡面的資料會被清成那個commit時的資料,也就是說,你打git status時會顯示紅字,就是git追蹤的資料中你有改過,但是你還沒add把她放上index cached裡。

hard

$ git reset --hard <commit>

這個就是local的repo commit直接回到你設定的commit外,working directory的資料全部也被改掉,也就是說,你假設在上述指令的commit後,有改很多檔案很多東西,如果你打上述指令後,你再打開那些檔案,那些檔案全部變回那次commit時的樣子,後來改的東西在你本地端也全部消失。

非常的危險,之前就遇過改了一個下午的程式碼,結果不小心commit多add到一個log檔,所以reset,結果打成hard,下午改的程式碼歸零。 後來靠reflog大神救援QQ。

Reference

https://dotblogs.com.tw/wasichris/2016/04/29/225157

git squash requests

$ git log --oneline : check commit
$ git rebase -i <commit>

you will see

push <commit1>
push <commit2>
push <commit3>

change to 

squash <commit1>
squash <commit2>
push <commit3>

then push to remote

$ git push --force origin master

git back to commit

除了取消錯誤的commit,這也可以用於取消錯誤的merge 但對於錯誤的rebase要用下面的方法,此方法行不通

$ git reset --hard <commit>

git reflog

查看所有git歷史紀錄,包括何時checkout, 何時merge, 何時rebase 除了commit紀錄外,有更多仔細的紀錄,能看到何時下了什麼指令 對於recovery非常重要

git rebase

除了上述提到的squash commit的用法 最重要的用法就是在於把branch的head移到其他地方(通常是把branch移動到master所在處)

用途: 當在branch A上開發時,開發者B把branch B開發玩merge 回master,此時

  1. branch A需要master上的新feature
  2. branch A開發玩後merge回master會有conflict(而且是要在master branch上做),此時最好的方法是branch A移動到master最新的commit,出現的conflict在branch A上執行,盡量不要在master上做merge(master是要保證隨時都會work的,天知道merge完會不會出事,所以在branch merge先測,沒問題在merge回master)

方法 branch A rebase到master(branch A的頭移動到master最新commit)

$ git checkout <branch A>
$ git rebase master
$ 處理conflict

如果不想處理merge/rebase conflict, 直接打git merge/rebase –abort 這樣就可以回到merge/rebase之前的狀況

如果rebase錯的話

假設情境,有一條很早以前就開發完的branch C,原本是要把branch A rebase到master,結果一沒注意 就把branch C rebase 到master,導致原本的那個大D的branch名稱跑到最新的master commit。

如圖:

此狀況為,有綠色、紅色兩條branch,綠色branch的名字為綠色的字b2,紅色的branch名字為b1, 結果不小心在綠色branch打git rebase master結果綠色branch跑到master去….

解法: 用git reflog看一下commit,git reflog有所有使用者輸入的指令的歷史紀錄(每個行為都是一個commit),git reset –hard到錯誤的指令前的commit,這樣就可以把branch C恢復到原本屬於它的地方了

Reference

https://gitbook.tw/chapters/branch/merge-with-rebase.html

Pull Request

Pull request是網站服務(如github, gitlab)所提供的功能, 單純的git binary是沒有這項功能, 他的目的時能夠方便多人進行專案的開發, 主要有兩個優點:

當一個大型專案的branch在merge前需要有多個人來進行code review時, pull request就是一個很方便進行code review的工具, 因為他可以一個一個commit review並且comment, 然後可以設定要有approve才能merge回branch.

另一個功能是讓一般使用者能夠參與open-source的專案開發, 因為一般使用者不會是專案的main contributor,所以不會有直接修改那些專案的權限. 這時候如果要貢獻專案的話那就要在網頁端fork一份open-source專案, 並且自己開心的branch修改, 修改好後再發pull request給open-source的負責人審核, 審核流程就跟上述大型專案開發一樣, 通過後會merge會master, 這樣一般使用者就能透過這樣的功能來貢獻不屬於自己的專案

Git structure

git本身是一個linux內建的開發工具,為了讓他方便在網頁上視覺化或者操作, git的操作往往會配合其他的前端服務如github, gitlab, gitbucket等等. 也就是說git是一個核心工具, 但這些網站服務能夠幫助我們擴展git的功能以及更容易來使用git(包括提供pull request功能, 遠端儲存, 視覺化commit狀況等等), 因此這些網站服務是提供各個符合git規範的API, 讓git能夠發送request給這些網站來做repository的更新

Config

記錄了push時後面的alias分別是會把檔案更新到哪個地方的repository, 所以例如git push origin master就在config裡定義了要push到哪個網站服務(如github, gitlab…)的哪個repository. 又如git pull origin test定義了要push到哪個遠端repo的branch. git config檔通常在執行git init + git remote, 或者git clone時會順便更新, 又或者在執行git pull時把remote端的branch clone到本地端時,也會更新

範例如下:

[core]
        repositoryformatversion = 0
        filemode = false
        bare = false
        logallrefupdates = true
        ignorecase = true
[remote "origin"]
        url = https://github.com/XXX/YYY.git
        fetch = +refs/heads/*:refs/remotes/origin/*

.git/objects/*

這裡記錄了所有git的資訊,所以舉凡你在github上可以看到的commit history、修改的行數、紀錄,都是記錄在這個資料夾內。
git的每筆操作都有一個對應的hash值,例如git commit產生的log,是hash。

git hash主要可以包括這幾種東西:

  1. commit history(此時裡面存的會是該commit的comment,跟前後commit的hash)
  2. Tree objects(紀錄此次commit所改變的檔案有哪些)
  3. blob: 紀錄該檔案有哪些地方有改變

也因此有時候打下面指令git cat-file -p 時跑出來是亂碼,因為那個可能是blob,然後他那個修改的檔案是圖片

git修改檔案的的資料結構,tree,內部object也都是hash。因此objects內的檔案命名規則如下:
這個資料夾命名規則為資料夾取hash的前兩位,資料夾內部的檔案名稱則為hash的兩位以後的值
例如某個commit hash為:
06c3e4fa979d73b81b416f5d61e948c2b9506e1c
則該筆commit的紀錄、comment會記在objects/06/c3e4fa979d73b81b416f5d61e948c2b9506e1c 這個檔案裡。

檔案是採zlib的方式壓縮,所以可以用zlib的方式把它解壓縮或者直接打

$ git cat-file -p <commit> 即可

packfile

如同上述,objects資料夾內存所有關於git的東西,舉凡所有commit comment,改變、資料structure,這個久了會變成一個很肥的檔案。

因此git還有另一個東西叫packfile,他就是上述objects的解壓縮,當多到一定程度的commit時,會觸發packfile把部分objects裡面的東西壓縮塞進objects/pack和objects/info裡面。

objects/pack裡面就會是一個大檔案(通常好幾MB),內部就是含多個commit log這樣,把他們更壓縮再一起節省空間。

通常在objects/pack內,會有objects/pack-XXX.pack跟objects/pack-XXX.idx,這兩個檔案存一堆commit log(idx可以說是pack recover,解壓縮的一個重要角色,就是說明這個pack內壓縮了多少commit這樣)

詳情關於packfile架構可參考下列文件

packfile document
packfile structure-1
packfile structure-2

Reference

git objects document
packfile document
packfile structure-1
packfile structure-2
git指令集
git unpack-objects