Git tips === ###### tags: `git` [TOC] # How to Write a Git Commit Message 首先是推薦文章內的重點:好的 commit message 的七個規則 The seven rules of a great Git commit message 1. Separate subject from body with a blank line 標題與內文以一個空白行分隔 > 這樣可以在使用 $ git log 閱讀時叫容易分辨出哪行是標題 2. Limit the subject line to 50 characters 標題字數最多 50 > 因為標題字數超過 50 在 github 上會被隱藏到 ... 裡面 > 需要額外點開才能完整知道這個 commit 在做什麼 > 超過 50 字可能就不是一個精簡的標題了 3. Capitalize the subject line 標題首字大寫 4. Do not end the subject line with a period 標題結尾不要句點 5. Use the imperative mood in the subject line 標題用命令口吻 6. Wrap the body at 72 characters 內文一行字數不超過 72 > 這樣在 $ git log 裡面看會比較清楚,排版比較好看 7. Use the body to explain what and why vs. how 內文說明是什麼、為什麼而不是如何做 > 因為看 commit 的人是想知道你為什麼要做這件事,而不是要知道你如何做這件事的 ref from: [how to write git message](https://chris.beams.io/posts/git-commit/) --- # index and working tree ![](https://i.imgur.com/s6RFucd.png) --- # [Git 工具 - 使用 Git 做 Debug](https://git-scm.com/book/zh-tw/v1/Git-%E5%B7%A5%E5%85%B7-%E4%BD%BF%E7%94%A8-Git-%E5%81%9A-Debug) git blame [file]查看該file適合人在何時修改的 -L [start,end]:限制範圍在該file的第幾行到第幾行 -C :找出原始程式碼的出處 ![](https://i.imgur.com/cF7FT4n.jpg) # Git flow ## git flow 初始化 git flow init ## 開始新功能設計 **新功能的開發是由 'develop' 分支的最後一個版本發展出來的** git flow feature start [branch name] **完成新功能** **當你在新功能分支完成新功能後,使用下列指令來結束新功能開發:** git flow feature finish [branch name] **這個指令會替你完成下列動作:** - **合並(Merge) MYFEATURE 分支回到 'develop'** - **刪除這個新功能分支** - **切換回 'develop' 分支** **發佈新功能分支** 讓遠端其他人也可使用這個分支 git flow feature publish [branch name] **取得新功能分支** 取得遠端上的新分支 git flow feature pull [branch name] **追蹤遠端上的分支** git flow feature track [branch name] ## 推出新版本 **使用下列指令準備推出(release)新版本分支:** git flow release start RELEASE [BASE] **完成推出新版本** **完成 release 版本是一個很大的 git 分支操作。但你只許要執行下列指令:** git flow release finish RELEASE **git flow 會幫你執行下面幾個步驟:** - **合並 release 分支到 'master'** - **在 master 加上版本號標籤(Tag)** - **合並 release 分支到 'develop'** - **移除 release 分支** ## 快速修復hotfix **開始快速修復** git flow hotfix start VERSION [BASENAME] **你必須填寫一個 VERSION 參數,這代表當你完成 hotfix 時發佈的版本號碼。** **此外你可以自由挑選修復開始的分支名稱**`**[BASENAME]**`**,分支名稱如果不填寫的狀況下,預設是 'master'。** **完成快速修復** git flow hotfix finish VERSION - 使用 `git reflog` 來查看 `HEAD` 的修改紀錄 - 使用 `git log -g` 查看 reflog 的詳細內容 # git reset **比較 **`**git reset HEAD^**`** 的參數** **指令** **效果** ``` git reset HEAD^ ``` 回到前一個 patch,且恢復檔案的狀態 ``` git reset --soft HEAD^ ``` 回到前一個 patch,但保持檔案狀態為 **Changes to be committed** ``` git reset --mixed ``` 回到前一個patch,狀態為unstage ``` git reset --hard HEAD^ ``` 回到前一個 patch,且強制清除檔案的修改內容 * reset --merge ``` always discards the index (staged changes); aborts if unstaged and staged changes present on any file ``` * reset --keep ``` keeps, but unstages, the index; aborts if the reset target touches the same file ``` 簡單來說`--keep`只要有修改到相同檔案部份,reset就會abort,但是`--merge`只有在修改要reset的檔案或是新增要reset時沒有的檔案,殘會abort,`--merege`域社會清除所有在index中的檔案,需要特別注意,而`--keep`會將期轉為unstage。 https://stackoverflow.com/questions/24728543/git-reset-merge-vs-git-reset-keep # git commit --amend 修改提交訊息 因為凡事只要做了 commit 的動作,git 就會重新產生一組新的 commit id 而 git commit --amend 後面也可以加 -m 的參數帶入新的 commit message $ git commit --amend -m <title> # 只要提交 title $ git commit --amend -m <title> -m <message> # 提交 title 以及 message $ git commit --amend -m <title> -m <msg1> -m <msg2> .... # 提交多段 messages 若不小心和上一個patch合併3則可利用下指令將兩者分離 git reset --soft HEAD@{1} # 使用 `git cherry-pick <commit id>` 挑入 patch https://zlargon.gitbooks.io/git-tutorial/content/patch/remove.html # git revert 還原指定的patch - 使用 `git revert <commit id>` 還原指定的 patch - 使用 `git revert --continue` 告知 git 已經解完衝突 - 使用 `git revert --abort` 來要放棄這次 revert # git 只提交檔案部份內容 - 使用 `git add -p` 提交檔案部分的內容 - 使用 `git checkout -p` 回復檔案部分的內容 # git patch ## 生成patch: ``` git format-patch -n ``` or ``` git format-patch <commit> 從某一個 commit 開始往後生成patch git format-patch -n <commit> 從某一個 commit 開始從前先成 n 個patch ``` 打包最新的三個 commit ``` git format-patch -3 ``` ## 套用patch: ``` git apply --check patch #套用patch時確認能否直接套用 git am xxx.patch ``` ## Make some changes, create a patch git diff > patch-issue-1.patch ## Add a file and create a patch git add newfile git diff --staged > patch-issue-2.patch ## Add a file, make some changes, and create a patch git add newfile git diff HEAD > patch-issue-2.patch ## Make a patch for a commit git format-patch COMMIT_ID ## Make patches for the last two commits git format-patch HEAD~2 ## Make patches for all non-pushed commits git format-patch origin/master ## Create patches that contain binary content git format-patch --binary --full-index origin/master # track remote ``` git checkout --track(-t) origin/serverfix ``` # remove remote branch ``` git push [遠程名] :[分支名] git push origin :serverfix ``` # delete origin branch ``` git push origin --delete [branch] ``` # pull all repository in directory ``` ls | xargs -P10 -I{} git -C {} pull ``` # git rev-list Lists commit objects in reverse chronological order ``` $git checkout `git rev-list -1 --before="$DATE" master` Note: checking out '79ad22cfb7d1ea950f4ffa2860f63bd4d0f31692'. You are in 'detached HEAD' state. You can look around, make experimental changes and commit them, and you can discard any commits you make in this state without impacting any branches by performing another checkout. If you want to create a new branch to retain commits you create, you may do so (now or later) by using -b with the checkout command again. Example: git checkout -b <new-branch-name> HEAD is now at 79ad22c... Need to update validate_sync with new column ``` 效果等同於 ``` $ DATE='Sep 3 2014' $ git rev-list -1 --before="$DATE" master | xargs -Iz git checkout z # 找出所有git object中,data<Sep 3 2014的onject,只取最接近現在的一個,然後checkout至該object。 ``` 之後就可以對該節點進行操做 ``` $ git checkout `git rev-list -1 --before="$DATE" master` ## 用一個tag標注,方便以後checkout $ git tag bug_23142 ## go back master $ git checkout master ## go to the mark tag $ git checkout bug_23142 ## 也可以直接建立一個新的branch $ git co -b "bug23142" ``` 利用file size尋找 ``` $ SIZE=167092 $ git rev-list --all \ | while read commit do if git ls-tree -l -r $commit \ | grep -q -w $SIZE then echo $commit break fi done d91807d59a6326e48077311e96e4d5730f24304c ``` Finding a git commit by error message ``` git log --format=%h \ | xargs -n 1 -I sh -c \ "echo -n {}; git show {}:bucardo | head -8627 | tail -1" \ | less ## About 35 lines down: 379c9006 $dbh = DBI->connect($BDSN, 'bucardo'... ``` # View remote urls ``` git remote -v ``` # Change origin url ``` git remote set-url origin http//github.com/repo.git ``` # See the files changed in a specific commit ``` git diff-tree --no-commit-id --name-only -r COMMIT_ID or git show --pretty="format:" --name-only COMMIT_ID source: http://stackoverflow.com/a/424142/1391963 ``` # See diff before push ``` git diff --cached origin/master ``` # See commit history for just the current branch ``` git cherry -v master (master is the branch you want to compare) ``` # Delete all untracked files ``` git clean -f Including directories: git clean -f -d Preventing sudden cardiac arrest: git clean -n -f -d Source: http://stackoverflow.com/q/61212/1391963 ``` # Rebase rebase is like merge,but it will merge your branch after the specific branch. 將整個feature branch移動到master branch之後 ``` git co feature git rebase master ``` ## 交互式rebase 可以指定要被merge的commit 讓user可以edit list before rebasing. ``` git checkout feature git rebase -i master ``` ``` 1 pick c01b36d hello commit 2 pick 30fcc2a commit test 3 pick d15923d test2 4 pick 8cd6a3f test3 5 6 # Rebase 9d960fb..8cd6a3f onto 9d960fb (4 command(s)) 7 # 8 # Commands: 9 # p, pick = use commit 10 # r, reword = use commit, but edit the commit message 11 # e, edit = use commit, but stop for amending 12 # s, squash = use commit, but meld into previous commit 13 # f, fixup = like "squash", but discard this commit's log message 14 # x, exec = run command (the rest of the line) using shell 15 # d, drop = remove commit 16 # 17 # These lines can be re-ordered; they are executed from top to bottom. 18 # 19 # If you remove a line here THAT COMMIT WILL BE LOST. 20 # 21 # However, if you remove everything, the rebase will be aborted. 22 # 23 # Note that empty commits are commented out ``` :::danger 絕對不要在公用的branch 上使用rebase ::: :::info 如果沒有把握,可以先創立一個暫時的rebase分支,確認沒問題後才merge回主要分支 git checkout feature git checkout -b temporary-branch git rebase -i master \# [清理目录] git checkout master git merge temporary-branch ::: 可以參考 https://blog.yorkxin.org/2011/07/29/git-rebase # git bisect ``` git bisect start git bisect bad git bisect good <some tag or rev that knew was working> git bisect run [unittest runner of choice] git bisect reset # to exit and put code back to state before git bisect start ``` # 分離某個子目錄到新的repository ``` git clone xxx cd mcplugins git checkout origni/master -b mylib git filter-branch --prune-empty --subdirectory-filter plugins/mylib mylib git push xxx/mylib.git mylib:master ``` # 衝突發生,但是知道哪個檔案正確 ``` git merge y # CONFLICT ... git checkout --theirs conflict.txt # conflict.txt is the conflict file git add conflict.txt git commit ``` > N.b. the function is there as hack to get $@ doing > what you would expect it to as a shell user. > Add the below to your .gitconfig for easy ours/theirs aliases. > ours = "!f() { git checkout --ours $@ && git add $@; }; f" > theirs = "!f() { git checkout --theirs $@ && git add $@; }; f" # git merge-base git merge-base找出兩分之開始分叉的base # git remove sensitive data 1. remove ``` git filter-branch --force --index-filter \ 'git rm --cached --ignore-unmatch [PATH-TO-YOUR-FILE-WITH-SENSITIVE-DATA]' \ --prune-empty --tag-name-filter cat -- --all ``` - Force Git to process, but not check out, the entire history of every branch and tag - Remove the specified file, as well as any empty commits generated as a result - Overwrite your existing tags 2. Add your file with sensitive data to .gitignore to ensure that you don't accidentally commit it again. ``` echo "YOUR-FILE-WITH-SENSITIVE-DATA" >> .gitignore $ git add .gitignore $ git commit -m "Add YOUR-FILE-WITH-SENSITIVE-DATA to .gitignore" ``` 3. push to original repository ``` git push origin --force --all ``` 4. Tell your collaborators to rebase, not `merge`, any branches they created off of your old (tainted) repository history. One merge commit could reintroduce some or all of the tainted history that you just went to the trouble of purging. 5. If you are confidient that git filter branch have no side effect that you can clean up your repository ``` git for-each-ref --format='delete %(refname)' refs/original | git update-ref --stdin git reflog expire --expire=now --all git gc --prune=now ``` # git remove commit 假設我們的版控歷史紀錄如下: R–A–B–C–D–E–HEAD 接下來要移除 B 跟 C 的 commit tree,變成 R–A–D’–E–HEAD 依序執行下列指令可以移除 B & C ``` # detach head and move to D commit git checkout <SHA-for-D> # move HEAD to A, but leave the index and working tree as for D git reset --soft <SHA-for-A> # Redo the D commit re-using the commit message, but now on top of A git commit -C <SHA-for-D> # Re-apply everything from the old D onwards onto this new place git rebase --onto HEAD <SHA-for-D> master # push it git push --force ``` or use cherry-pick ``` git rebase --hard a git cherry-pick d git cherry-pick e ``` # config ## 指定編輯器 ``` git config --global core.editor vim ``` ## 檢查設定 ``` git config --list git config <key> ``` # tig ``` git source tree ``` ![](https://i.imgur.com/LxY80fc.png) # 同步不同remote server的repository 1. 查看remote 來源 * git remote -v 2. set upstream * git remote add upstream <upstram-url> 3. git remote -v * 檢查是否多一組remote 2. 和第二組server同步 * git pull upstream master 3. 合併 * git pull --rebase upstream master 4. push 回自己的repository 5. git push origin master # git submodule * `git submodule add <repo>` * `git submodule init` * `git submodule update` * `git submodule foreach git pull origin master` * Clone all submodules * `git clone --recursive git://xxxx` * if already cloned: * `git submodule update --init --recursive` * Remove submodule Creating a submodule is pretty straight-forward, but deleting them less so. The commands you need are: ``` $ git submodule deinit submodulename $ git rm submodulename $ git rm --cached submodulename $ rm -rf .git/modules/submodulename ``` * reset hard for all submodules * `git submodule foreach git reset --hard` * `git submodule foreach --recursive git reset --hard` # creating a bare repo * git init --bare # git hooks git hook help <https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks> # Rewrite branches git filter-branch # merge remote pull request to local ``` git fetch origin pull/ID/head:BRANCHNAME ``` and ``` git checkout BRANCHNAME ``` If the PR code changes and you want to update: ``` # Do this while in the pr37 branch $ git pull upstream pull/37/head ``` # Merging the PR You'll only do this the first time -- it creates the local upstream_master branch, tracks it to upstream_master, and switches to the branch: ``` $ git checkout -t -b upstream_master upstream/master ``` After the first time you'll just do: ``` $ git checkout upstream_master ``` Now merge the PR: ``` $ git merge pr37 ``` NOTE: You should edit the merge commit message to reference the PR (using, say #37 in it). Now push: ``` $ git push upstream HEAD:master ``` # 在repository中更改使用者 eg:A->B->C->D 更改B以後的commit的使用者 1. git rebase -i A 2. 將pick改為edit 3. git commit --amend --author="Author Name <email@address.com>" 4. git rebade --continue 5. 重複1直到rebase結束 ## 徹底清除 ``` git filter-branch --env-filter ' WRONG_EMAIL="wrong@example.com" NEW_NAME="New Name Value" NEW_EMAIL="correct@example.com" if [ "$GIT_COMMITTER_EMAIL" = "$WRONG_EMAIL" ] then export GIT_COMMITTER_NAME="$NEW_NAME" export GIT_COMMITTER_EMAIL="$NEW_EMAIL" fi if [ "$GIT_AUTHOR_EMAIL" = "$WRONG_EMAIL" ] then export GIT_AUTHOR_NAME="$NEW_NAME" export GIT_AUTHOR_EMAIL="$NEW_EMAIL" fi ' --tag-name-filter cat -- --branches --tags ``` # 局部config ``` git config user.name "YOUR NAME" git config user.email "YOUR EMAIL" ``` # Finding * git log -S "string to find" * Commons parameters: * `--source`:顯示在命令行上給出的每次引用的引用名稱 * `--all`:start from every branch * `reverse`:prints in reverse order ## find author committer ``` git log --author="name or email" git log --committer="name or email" ``` ## list commits containing specific files ``` git log -- <path to file> git log -- **/*.js git log --name-status -- **/*.js ``` ## view commit history for a specific function ``` git log -L :FunctionName:FilePath ``` ## Find a tag where a commit is referenced ``` git tag --contains <commitid> ``` # Copy a folder or file from one branch to another ``` $ git checkout <branch-you-want-the-directory-from> -- <folder-name or file-name> ``` # Restore a deleted file First find the commit when the file last existed: ``` $ git rev-list -n 1 HEAD -- filename ``` Then checkout that file: ``` git checkout deletingcommitid^ -- filename ``` # Delete tag ``` $ git tag -d <tag_name> $ git push <remote> :refs/tags/<tag_name> ``` # Exporting a repository as a Zip file ``` $ git archive --format zip --output /full/path/to/zipfile.zip master ``` # Tracking files ## I want to change a file name's capitalization, without changing the contents of the file ``` (master)$ git mv --force myfile MyFile ``` ## I want to overwrite local files when doing a git pull ``` (master)$ git fetch --all (master)$ git reset --hard origin/master ``` ## I want to remove a file from Git but keep the file ``` (master)$ git rm --cached log.txt ``` ## I want to revert a file to a specific revision Assuming the hash of the commit you want is c5f567: ``` (master)$ git checkout c5f567 -- file1/to/restore file2/to/restore ``` If you want to revert to changes made just 1 commit before c5f567, pass the commit hash as c5f567~1: ``` (master)$ git checkout c5f567~1 -- file1/to/restore file2/to/restore ``` ## I want to list changes of a specific file between commits or branches Assuming you want to compare last commit with file from commit c5f567: ``` $ git diff HEAD:path_to_file/file c5f567:path_to_file/file ``` Same goes for branches: ``` $ git diff master:path_to_file/file staging:path_to_file/file ``` ## I want Git to ignore changes to a specific file This works great for config templates or other files that require locally adding credentials that shouldn't be committed. ``` $ git update-index --assume-unchanged file-to-ignore ``` Note that this does not remove the file from source control - it is only ignored locally. To undo this and tell Git to notice changes again, this clears the ignore flag: ``` $ git update-index --no-assume-unchanged file-to-stop-ignoring ``` # I want to cache a username and password for a repository You might have a repository that requires authentication. In which case you can cache a username and password so you don't have to enter it on every push and pull. Credential helper can do this for you. ``` $ git config --global credential.helper cache ``` # Set git to use the credential memory cache ``` $ git config --global credential.helper 'cache --timeout=3600' ``` # Set the cache to timeout after 1 hour (setting is in seconds) To find a credential helper: ``` $ git help -a | grep credential # Shows you possible credential helpers ``` # I want to make Git ignore permissions and filemode changes ``` $ git config core.fileMode false ``` If you want to make this the default behaviour for logged-in users, then use: ``` $ git config --global core.fileMode false ``` # revert file after commit and push * git reset HEAD^ path/to/file/to/revert Git 2.23: * git restore --source=HEAD^ --staged -- path/to/file shorter: * git restore -s@^ -S -- path/to/file view commit files: * git diff --name-only HEAD^ ## undo changes one file: ``` git checkout master # first get back to master git checkout experiment -- app.js # then copy the version of app.js # from branch "experiment" ``` Git 2.23: git switch/restore ``` git switch master git restore -s experiment -- app.js ``` ## [Push 到其他人的遠端分支](https://stackoverflow.com/questions/15530510/how-do-i-push-to-a-pull-request-on-github) ``` $ git push git@github.com:owner/repo.git HEAD:target-branch ``` ## pull specific branch from github PR ``` git fetch origin pull/ID/head:BRANCHNAME ``` ## [10 new Git commands you should start using today](https://appwrite.io/blog/post/10-git-commands-you-should-start-using) ## A simple git settings ``` [user] email = xxx@gmail.com name = splasky [alias] co = checkout cm = commit st = status br = branch -v shbra = show-branch ; git grep will only search in current directory grep = grep -n --color -E addall = !sh -c 'git add . && git add -u' ; reset rt-hard = reset --hard rt = reset --mixed rt-merge = reset --merge rt-keep = reset --keep rt-modify = checkout -- rt-modify-all = checkout -f ;stage to unstage unstage =reset HEAD ;reset to the last commit uncommit = reset --soft HEAD^ ; rebase rb = rebase -i rc = rebase --continue rs = rebase --skip ra = rebase --abort ; revert rv = revert ; log lg = log --color --graph --all --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit -- log-diff = log -p log-info = log --abbrev-commit --decorate --pretty=medium --name-status who = shortlog -n -s --no-merges ; remove remote branch del-remote-branch=push origin --delete ; push push = push --tags ; remote info = remote -v remote-update = remote update re-set-url = remote set-url ; show remote git repository info ls-re = ls-remote ; clean untrack files rm-untrack = clean -df ; git worktree wk = worktree wka = worktree add wklist = worktree list wklock = worktree lock st-show = stash show -p [color] ui=true [core] editor = vim [merge] tool = vimdiff ```