Git best practices

Git 基本操作

创建Git仓库

1
git init

查看代码情况

1
2
git status
git diff {filepath}

提交代码

1
2
3
4
git clone {clone.git}
git add -A
git commit -m "message"
git push orgin master

保存/恢复临时修改文件

1
2
git stash
git checkout stash@{0} -- {filepath}

读取文件历史版本

1
git checkout {commithash} -- {filepath}

创建标签

1
2
3
git tag -a v1.0  # tag most recent commit
git tag -a v0.9 85fc7e7 # tag on a specific commit
git tag # view tags

Pull/Fork工作流

  1. fork from the github
  2. clone to local and commit changes
    1
    2
    3
    4
    5
    6
    git clone {clone.git}
    git checkout -b feature
    # make changes to this branch
    git add -p
    git commit -m "feature message"
    git push origin feature
  3. rebase commits after remote master
    1
    2
    3
    4
    5
    6
    7
    git remote add upstream {remote.git}
    git pull upstream master
    # no changes on master, then automatically git history is updated
    git checkout feature
    git pull --rebase origin master
    # apply changes from origin master to feature newly commits
    git push origin feature --force
  4. raise pull request for code review

提交历史管理

缩减冗余commit log

  • 通过rebase修改commit history
1
2
3
# rewrite last 10 commit logs
git rebase -i HEAD~10 feature
git push orign feature --force
  • 通过squash命令压缩commit history
1
2
3
4
5
6
7
8
git reset --hard HEAD~12 # 将git指针定位到历史版本

git merge --squash HEAD@{1} # 从该点开始merge到最新的版本

git commmit -m "xxx" # 提交commit重写

git push origin master --force # 强制改写历史

分支管理

创建新分支

在创建新的local分支时,也可以添加commit hash告诉git分支最新的HEAD指向

1
2
git checkout -b feature
# create a branch based on current branch

切换分支/从Detached HEAD切换

1
git checkout feature

删除本地分支

1
2
git branch -d feature
git branch -D feature # force delete

删除远程分支

1
2
git push orgin --delete feature
git push origin :feature

合并分支

1
2
git merge feature
# merge changes from feature branch to current master branch

CherryPick别的分支commit/branch

在需要apply的分支上保证没有unstaged change,运行如下命令

1
git cherry-pick <commitHash>/<feature branch name>

github migration from port 22 to 443

github服务器已经将22端口关闭,git工具需要访问ssh.github.com:443而不是默认的github.com:22 SSH端口。

需要更新~/.ssh/config文件如下:

1
2
3
4
5
6
Host github.com
HostName ssh.github.com
User git
Port 443
PreferredAuthentications publickey
IdentityFile ~/.ssh/id_rsa

测试方法

1
ssh -T git@github.com

历史管理

版本控制原理

代码库,从本质上来讲是一个文件树,和每一个文件的blob,代码库的版本控制主要也是根据这两个数据结构:

  • 一棵树:维护文件绝对路径全名->文件blob SHA1的映射关系
  • 一系列的blob:维护文件blob SHA1->文件内容的映射关系
flowchart TB
    subgraph commits["Commit 提交历史(每个提交 = 一个版本快照)"]
        direction TB
        C1["commit e8a1f2b3
快照整体 SHA1"] C0["commit 9f1e2d3c
上一个版本"] C1 -->|"父提交链接(版本链)"| C0 end subgraph trees["Tree 文件树(快照)"] direction TB T1["tree 快照对象"] M1["'src/main.py' → blob 5c6d7e8f"] M2["'src/util.py' → blob 9a0b1c2d"] M3["'README.md' → blob 3e4f5a6b"] T1 -->|"文件绝对路径 → blob SHA1"| M1 & M2 & M3 end subgraph blobs["Blob 对象库(内容寻址存储)"] direction TB B1["blob 5c6d7e8f → 文件内容 A"] B2["blob 9a0b1c2d → 文件内容 B"] B3["blob 3e4f5a6b → 文件内容 C"] end C1 -->|"① 通过快照整体 SHA1 定位"| T1 M1 -->|"③ 通过 blob SHA1 取内容"| B1 M2 --> B2 M3 --> B3

每一个commit,都维护了这棵树的快照,以及对应的快照的整体SHA1,SHA1的设计思想是基于hash函数的,保证了SHA1构建寻址键的特性。Git可以快速通过SHA1查询到对应的版本,每一个版本内部文件路径的SHA1也能快速查询到对应的文件内容。

flowchart LR
    Q1["① 用 commit 整体 SHA1 定位到版本快照"] --> Q2["② 在 Tree 中按文件路径查得 blob SHA1"] --> Q3["③ 用 blob SHA1 在对象库定位文件内容"]

查询任意两个commit SHA1之间的提交

非对称差异提交记录

非对称的提交记录是用src..dest表示,代表dest SHA1所在的提交记录,去除src SHA1之前的提交记录后得到的差异commit。注意src可以包含自己特有的commits,dest中即使没有也不会显示为差异commit。

1
2
3
4
5
6
7

#代表PR里面会展示的commit
git log staging..feature

#代表staging领先的commits
git log feature..staging

举个例子:

从可达性看:

  • feature 可达:{F3, F2, F1, B}
  • staging 可达:{S2, S1, B}
  • 交集(共有):{B}
flowchart TB
    B["B · 9f2a1c0
共同祖先(merge-base)"] subgraph staging["staging 分支"] direction TB S2["S2 · 7c1f9a2
staging HEAD"] --> S1["S1 · 3d8e4b1"] end subgraph feature["feature 分支"] direction TB F3["F3 · 2f9b6d4
feature HEAD"] --> F2["F2 · e4a7c15"] --> F1["F1 · b5e2d83"] end S1 --> B F1 --> B

git log staging..feature含义:feature 可达 − staging 可达,得到 {F3, F2, F1}。

flowchart TB
    B["B · 9f2a1c0
共同祖先 · 被排除"] subgraph staging["staging 分支"] direction TB S2["S2 · 7c1f9a2
被排除(staging 独有,但这里不算差异)"] --> S1["S1 · 3d8e4b1
被排除"] end subgraph feature["feature 分支"] direction TB F3["F3 · 2f9b6d4
输出"] --> F2["F2 · e4a7c15
输出"] --> F1["F1 · b5e2d83
输出"] end S1 --> B F1 --> B classDef hit fill:#1a7f37,stroke:#0d4720,color:#ffffff,stroke-width:2px classDef miss fill:#e8e8e8,stroke:#9a9a9a,color:#5a5a5a class F1,F2,F3 hit class S1,S2,B miss

git log feature..staging含义:staging 可达 − feature 可达,得到 {S2, S1}。方向调转,结果完全不同,这就是”不对称”的直观体现。

flowchart TB
    B["B · 9f2a1c0
共同祖先 · 被排除"] subgraph staging["staging 分支"] direction TB S2["S2 · 7c1f9a2
输出"] --> S1["S1 · 3d8e4b1
输出"] end subgraph feature["feature 分支"] direction TB F3["F3 · 2f9b6d4
被排除"] --> F2["F2 · e4a7c15
被排除"] --> F1["F1 · b5e2d83
被排除"] end S1 --> B F1 --> B classDef hit fill:#1a7f37,stroke:#0d4720,color:#ffffff,stroke-width:2px classDef miss fill:#e8e8e8,stroke:#9a9a9a,color:#5a5a5a class S1,S2 hit class F1,F2,F3,B miss

对称差异提交记录

对称的提交记录是用src…dest表示,代表src SHA1和dest SHA1之间的差异commit。会列出出去src和feature共有的commit之外的所有提交记录。

1
2
3
git log staging...feature
#OR
git log feature...staging

git log staging…feature含义:并集 − 交集,即 staging..feature 与 feature..staging 的合集,得到 {F3, F2, F1, S2, S1};只有双方共有的 B(及其祖先)被剔除。因为它对称,git log feature…staging 结果完全相同。

flowchart TB
    B["B · 9f2a1c0
共同祖先 · 被排除"] subgraph staging["staging 分支"] direction TB S2["S2 · 7c1f9a2
输出"] --> S1["S1 · 3d8e4b1
输出"] end subgraph feature["feature 分支"] direction TB F3["F3 · 2f9b6d4
输出"] --> F2["F2 · e4a7c15
输出"] --> F1["F1 · b5e2d83
输出"] end S1 --> B F1 --> B classDef hit fill:#1a7f37,stroke:#0d4720,color:#ffffff,stroke-width:2px classDef miss fill:#e8e8e8,stroke:#9a9a9a,color:#5a5a5a class S1,S2,F1,F2,F3 hit class B miss

format-patch生成commit patch

format-patch是将src..dest的提交记录,生成patch文件,方便reviewer进行review。

1
git format-patch -M -o patches/ staging^2..staging

查看所有分支的提交记录

1
git log --pretty=oneline --abbrev-commit --graph --all