git使用心得

作为当前最流行的版本控制利器,git已经越来越成为各类项目首选的版本管理工具,掌握git对于developer来说已经是一项必备技能。

分支

master

master是一个项目中有且仅有一个的主分支,也是给项目加上git特性(git init)或拷贝git项目(git clone)后的默认分支,它应当一直处于可发布的稳定版本状态。每当master更新到一个稳定版本后,可以通过给其打标签的方式标记稳定的版本号。

develop

develop是用于开发的分支,每当进入版本迭代后及版本发布前,开发团队应在develop分支进行开发,每次版本迭代正式完成时,develop分支应当并入master分支进行版本正式发布。

feature

feature是当某个迭代较大时,为了开发某个功能,由develop分支分出来的功能分支,通常需要在功能开发完成后再并入develop分支。

release

release分支叫作预发布分支,顾名思义,就是在版本正式发布即develop分支并入master分支之前用于预先发布(通常用于模拟生产环境的测试)的分支。在预发布到正式发布之间,对代码的修改应当在release分支进行,且这些修改应该仅仅是修复bug,而不进行功能性的改动;修改完毕后,应当并回develop分支。

hotfix

hotfix是热修复/紧急修复分支。在版本正式发布后,仍然难免会出现bug(e.g. 开发/测试/Stagging环境与生产环境的不同导致bug未在预发布阶段被发现)。此时从master分支分出hotfix分支,紧急修复bug,修复完后并入master及develop分支,通常还需要为master分支打上patch tag(e.g. v1.2->v1.2.1)。
对于某些迭代周期较长的软件来说,开发者会时不时发布hotfix这种小patch给用户来堵漏;当hotfix积累到一定程度后,会将累积的hotfix打包成SP(Service Pack补丁包),以省去未打补丁的用户逐个堵漏的麻烦。

一张图可以说明上述版本间的关系和区别:
img

区域

要理解版本控制的原理及代码的流转规则,需要先了解区域的概念,再去熟悉命令,这样才能有清晰的全局概念。由近及远,git管辖的区域包括Workspace, Stage(Index), Repository, Remote四个区域。

Workspace

Workspace是Developer直接开发的区域,例如项目在本地的文件夹。

Stage / Index

Stage或Index是位于Workspace和Repository之间的缓存区,如果把Workspace比作货架,Repository比作收银台,那么Stage就是购物车。在把代码从Workspace提交到Repository之前,需要先添加进Stage,或者从Stage中撤出已添加的修改。

Repository

Repository是本地仓库,保存着本地的所有分支及下载的远程分支,用于跟远程仓库同步。

  • 如无特别说明,下文中的分支指的都是Repository分支,即本地分支。
    commit
    Developer在Workspace进行的每次修改(变更),都需要先提交到Repository,再提交到Remote,才能与其他Developer同步代码。这样的提交叫作commit,每个commit都被分配了一个SHA-1的hash值作为其ID,记录了变更的详细情况(文件的新增、删除、修改、重命名等)。
    每次commit都需要添加commit message,对该commit进行说明。
  • 如无特别说明,下文中提到的对某个分支的操作均针对某个Repository分支的最近一次commit。HEAD是一个指针,指向本地的活动分支。每次切换分支后,HEAD指向新切换的分支;每次commit后,HEAD随着分支的前进而前进,指向分支的最新commit。
    index
    index是一个索引文件,记录每次代码的变化,可以理解为Stage区的具体表现。

    Remote

    作为版本控制/代码同步的核心,Remote远程仓库用于版本的统一。Remote在Developer的本地有一个副本,默认名为origin。
    track
    各个Repository与Remote进行联系的时候,使用的是以分支为单位的track机制,即每个Repository分支都对某个Remote分支进行track,以此来进行追踪和同步,Repository分支默认track同名的Remote分支。
    • 如无特别说明,下文中提到的对Remote仓库及Remote某个仓库的操作均为对Remote远程仓库在本地的副本进行的操作,若需更新副本,使用git fetch命令;若需将副本的变更提交到远程,使用git push命令。

下面这张图可以理清4个区域的关系:
img

常用指令清单

基本命令

掌握下面10个命令,日常的开发工作就没多大问题了。

  • init
    在当前文件夹建立git特征,从此就可以使用git来管理文件夹里的文件了,同时默认切换到master分支。
  • clone
    将远程仓库(Remote)里的指定项目整个下载到本地仓库(Repository),并在当前路径建立Workspace。
  • fetch
    将远程仓库里指定项目的所有变更下载到本地仓库。
  • status
    显示Workspace里发生变更的文件。
  • add
    添加Workspace里的文件到Stage。
  • commit
    提交Stage里的文件到Repository。
  • push
    将Repository里当前分支的变更提交到Remote。
  • pull
    相当于fetch + merge。下载Remote里当前分支的变更,并与Repository里对应的分支合并。
  • branch
    对Repository的分支进行操作。
  • checkout
    切换分支/覆盖本地修改。

初始化

新建
  • 在当前目录为项目添加git特征。

    1
    git init
  • 新建一个空目录,并为其添加git特征。

    1
    git init [目录]
拷贝
  • 从远程仓库下载一个完整的git项目到当前目录(e.g. git clone http://10.10.10.10/test.git)。
    1
    git clone [url]
配置

git的配置文件格式如下,全局配置文件在用户目录下。

1
2
3
4
5
6
7
8
9
10
[user]
name = Luck
password = pAsSw0rD
[push]
default = simple
[credential]
helper = store
[core]
autocrlf = false
quotepath = off

因此修改配置时直接按[大项].[小项]名即可设置某个配置项的值。

  • 修改配置项(e.g. 用户名)。

    1
    git config --global user.name "newLuck"
  • 直接编辑配置文件。

    1
    git config -e --global
  • 显示当前的配置信息。

    1
    git config --list
  • 配置命令别名。

    1
    2
    3
    git config --global alias.<别名> <命令>
    git config —-global alias.st status
    git config --global alias.cmt commit

远程仓库管理

  • 显示所有远程仓库。

    1
    git remote [-v]
  • 下载当前本地仓库对应的远程仓库或某个指定远程仓库的所有分支及所有变更。

    1
    git fetch [远程仓库名]
  • 显示某个远程仓库的详细信息。

    1
    git remote show [远程仓库名]
  • 添加远程仓库。

    1
    git remote add [远程仓库名] [url]
  • 重命名远程仓库。

    1
    git remote rename [远程仓库名] [新远程仓库名]
  • 修改远程仓库的url。

    1
    git remote set-url [远程仓库名] [新url]
  • 删除远程仓库。

    1
    git remote remove [远程仓库名]

分支管理

  • 显示所有Repository的分支。

    1
    git branch
  • 显示所有Remote的分支。

    1
    git branch -r
  • 显示所有Repository + Remote的分支。

    1
    git branch -a
  • 新建一个分支,但不切换过去;或切换到已存在的某个分支,同时更新Workspace。

    1
    git branch [分支名]
  • 从当前分支分出一个新分支并切换过去。

    1
    git branch -b [新分支名]
  • 从当前分支的某个commit新建一个分支。

    1
    git branch [新分支名] [commit ID]
  • 从指定某个分支分出一个新分支。

    1
    git checkout -b [新分支名] [旧分支名]
  • 切换到指定分支。

    1
    git checkout [分支名]
  • 切换到上一个分支。

    1
    git checkout -
  • 新建一个分支,并对指定的Remote分支进行track。

    1
    git branch --track [新Repository分支名] [Remote分支名]
  • 让已存在的分支对指定的Remote分支进行track。

    1
    git branch --set-upstream [Repository分支名] [Remote分支名]
  • 将指定的分支合并进当前分支。

    1
    git merge [待合并进来的分支名]
  • 将指定分支合并进指定的另外一个分支。

    1
    git merge [待合并进来的分支] [主动分支]
  • 将某个commit合并进当前分支。

    1
    git cherry-pick [commit ID]
  • 删除指定的Repository分支。

    1
    git branch -d [Repository分支名]
  • 删除指定的Remote分支。

    1
    git branch -dr [Remote分支名]
  • 如果待合并的分支与主动分支与某个分岔点各有若干次commit(diverged),将待合并的分支带着所有自分岔点后的commit,合并进主动分支的最后一次commit后。下文将详细展开。

    1
    git rebase [主动分支] [待合并的分支]

Workspace与Stage

提交变更到Stage
  • 添加文件/文件夹/当前路径下所有文件到Stage。

    1
    2
    3
    4
    5
    git add [file A] [file B] ...

    git add [directory A] [directory B] ...

    git add .
  • 以文件中的块(hunk)为最小单位进行staging,交互式地进行选择。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    git add -p / git add --patch
    [y,n,q,a,d,/,?]?
    ? - 使用帮助
    y - stage该hunk
    n - 不stage该hunk
    q - 停止staging选择
    a - stage该hunk及后面的所有hunk
    d - 不stage该hunk及后面的所有hunk
    g - 跳到某个hunk去做选择
    / - 根据后接的正则匹配hunk
    s - 将当前hunk切割成更小的hunk
  • 删除Workspace的文件/文件夹,并将删除的变更保存到Stage。

    1
    git rm [file A] [file B] ...
  • 重命名Workspace的文件/文件夹,并将改名的变更保存到Stage。

    1
    git mv [file A] [file NewA]
  • 停止track指定的文件(将指定文件的变更从Stage中删除,但Workspace仍然保留该次变更)。

    1
    git rm --cached [file X] [file Y] ...
撤销变更
  • 从Stage恢复变更后的指定文件/全部文件到Workspace。

    1
    2
    3
    git checkout [file A] [file B] ...

    git checkout .
  • 从Repository的某个commit恢复文件,同时恢复到Stage和Workspace。

    1
    git checkout [commit ID] [file A] [file B] ...
  • 从Stage中取消指定文件的变更,即将某些文件的变更从Stage中生拽出来,但Workspace仍然有这些变更。

    1
    git reset HEAD [file A] [file B] ...
  • 从Repository恢复指定文件,将Stage恢复到上一个commit,但Workspace不变。

    1
    git reset [file A] [file B] ...
  • 从Repository恢复全部文件,将Stage恢复到指定的commit,当前分支的指针指向该commit,但保留Workspace的变更。

    1
    git reset [commit ID]
  • 从Repository恢复全部文件,恢复到上一个commit,重置Stage和Workspace。

    1
    git reset --hard
  • 从Repository恢复全部文件,恢复到指定的commit,重置Stage和Workspace。

    1
    git reset --hard [commit ID]
  • 将Repository恢复到某次commit,但保持Stage和Workspace的变更。

    1
    git reset --keep [commit ID]
  • 新建一个commit,专门用于撤销指定的某次commit,该commit的所有变更都会还原,同时应用到当前分支。

    1
    git revert [commit ID]
Stash

有时候我们想要暂时隐藏Workspace和Stage的变更,相当于回滚到上次commit之后Workspace和Stage均clean的状态,待到需要时再将变更还原出来,此时我们就需要用到Stash命令。
e.g. pull新代码,但不想立即跟当前的代码合并并产生新的commit,于是先隐藏,pull之后再还原继续写。当然,也可以放弃之前stash的工作。

1
2
3
4
5
6
//再继续本地修改前,急于先应用Remote的变更
git stash
git pull
git stash pop
...//继续修改
git commit -m "new commit after pull"

1
2
3
4
5
//发现有bug,先把其他修改stash,专门解决bug,再还原做到一半的修改
git stash
git commit --amend [bugged file] -m "bug fixed"
git stash pop
git commit -a -m "new commit after bug fix"
1
2
3
4
//下载Remote的变更看看,已经有同样的修改,还比我改得好,放弃自己的
git stash
git pull
git stash drop

每次git stash后会将变更缓存到git栈中,需要恢复变更时可以使用git stash popgit stash apply,两者都会还原stash的变更,但是pop会弹栈,而apply仅仅是读取栈顶的变更。

同步

提交到Repository
  • 将Stage中指定文件的变更提交到Repository。

    1
    git commit [file A] [file B] ... -m [commit message]
  • 将Stage的所有变更提交到Repository。

    1
    git commit -m [commit message]
  • 将Workspace中的变更直接提交到Repository。

    1
    git commit -a
  • 提交时显示变更信息。

    1
    git commit -v
  • 重做上一次commit,如果没有改动,则只修改commit message。

    1
    git commit --amend -m [commit message]
  • 重做上一次commit,并新增指定文件的变更。

    1
    git commit --amend [file A] [file B] ...
    • 使用–amend参数修改的commit,只能修改已经commit到Repository但仍未push到Remote的commit,对于已经push的修改,需要用其他方法。
与Remote同步
  • 下载指定Remote分支的变更,并与Repository分支合并,同时更新Workspace;remote参数默认为当前分支track的Remote仓库,branch参数默认为当前分支。

    1
    git pull [remote] [branch]
  • 上传指定Repository分支的变更到Remote分支,如果有冲突将失败;remote参数默认为当前分支track的Remote仓库,branch参数默认为当前分支。

    1
    git push [remote] [branch]
  • 上传当前Repository分支的变更到指定Remote的分支,即使有冲突仍然进行。

    1
    git push [remote] --force
  • 上传所有Repository分支的变更到对应的Remote分支。

    1
    git push [remote] --all

标签

  • 显示所有tag。

    1
    git tag
  • 为最近一次commit新建一个tag。

    1
    git tag [tag]
  • 为指定的commit新建一个tag。

    1
    git tag [tag] [commit ID]
  • 查看tag信息。

    1
    git show [tag]
  • 提交指定的tag到Remote。

    1
    git push [Remote] [tag]
  • 提交所有的tag到Remote。

    1
    git push [Remote] --tags
  • 删除Repository的tag。

    1
    git tag -d [tag]
  • 删除Remote的tag。

    1
    git push [Remote仓库] :refs/tag/[tag]

信息查看

status
  • 显示当前分支的变更及track的情况,如超过/落后于Remote分支多少个commit,Workspace中哪些文件有变更等。

    1
    git status
  • 显示简要的变更/track情况。

    1
    git status -s
  • 显示指定分支的变更情况。

    1
    git status -b [branch]
log
  • 显示当前分支的版本史。

    1
    git log
  • 显示当前分支的详细版本史,包括每次commit的文件、commit message以及统计数据。

    1
    git log --stat
  • 在版本史中根据关键字搜索条目。

    1
    git log -S [keyword]
  • 显示当前分支的版本史,每个commit占一行。

    1
    git log --pretty=format:%s
  • 筛选commit message符合条件的版本史条目。

    1
    git log --grep "Merge"
  • 显示指定文件的版本史。

    1
    git log --follow [file]
  • 显示指定文件的每次diff。

    1
    git log -p [file]
  • 显示最近五次commit。

    1
    git log -5 --pretty --oneline
  • 显示提交过的用户,按提交次数倒叙排列。

    1
    git shortlog -sn
  • 显示指定文件被哪些用户修改过,以及对应的commit。

    1
    git blame [file]
diff
  • 显示Stage和Workspace的差异。

    1
    git diff
  • 显示Stage和当前分支最近一次commit的差异。

    1
    2
    git diff --cached [file]
    git diff --staged [file]
  • 显示Workspace与当前分支最近一次commit的差异。

    1
    git diff HEAD
  • 显示两次commit的差异。

    1
    git diff [commit A] [commit B]
  • 显示以空格为分隔符的变更。

    1
    git diff --word-diff
show
  • 显示某次commit的具体信息。

    1
    git show [commit ID]
  • 显示某次commit中哪些文件发生了变更。

    1
    git show --name-only [commit ID]
  • 显示某次commit中某个文件的变更信息。

    1
    git show [commit ID]:[文件名]
reflog
  • 显示当前分支的最近几次commit。
    1
    git reflog

Tricks

Q: 跟团队里其他人的代码冲突了怎么办?
A: 冲突不可怕,兵来将挡,冲突来了resolve掉。冲突的时候,会有下面这样的显示,这个时候手动处理也好,Merge Tool也罢,干掉了那些奇怪的标记(<<<< HEAD,====, >>>> 1a2b, etc.) 之后才算resolve掉了冲突,才可以commit。

1
2
3
4
5
<<<<<< HEAD
This is what you've written.
======
This is what your teammate's done.
>>>>>>> 1a2b3c

当然也有这么几个原则:

  1. 定好接口,划分模块,理清分支,明确分工,同时每个teammate应该细心打磨自己负责的与整个项目对接的代码。
  2. 尽量避免冲突,保证步骤规范:先add + commit或stash,再pull合并。
  3. 万一冲突了,而Merge Tool效果不佳时,与冲突的另一方协商,通过文件对比手动resolve冲突,之后新起merge commit。

Q: 关于提交的文件有什么要注意的?
A: 不要提交二进制文件,如:

  • Android项目不要提交gen及bin目录。
  • Maven项目不要提交target目录。
    而这些需要由.gitignore文件来控制,下面是一个典型Android项目的.gitignore文件。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    # Built application files
    *.apk
    *.ap_

    # Files for the ART/Dalvik VM
    *.dex

    # Java class files
    *.class

    # Generated files
    bin/
    gen/
    out/

    # Gradle files
    .gradle/
    build/

    # Local configuration file (sdk path, etc)
    local.properties

    # Proguard folder generated by Eclipse
    proguard/

    # Log Files
    *.log

    # Android Studio Navigation editor temp files
    .navigation/

    # Android Studio captures folder
    captures/

    # Intellij
    *.iml
    .idea/workspace.xml
    .idea/libraries

    # Keystore files
    *.jks

    # External native build folder generated in Android Studio 2.2 and later
    .externalNativeBuild

Q: 使用pull更新本地代码时有没有标准动作?
A: pull相当于fetch from upstream + merge into workspace,因此其实pull可以由fetch+merge替代。有以下几个原则需要遵守:

  1. pull前,如果Workspace有修改,必须选择将变更commit或stash,才可以进行pull。
  2. 如果pull前有commit变更,git会自动合并,自动合并失败的代码需要手动resolve冲突。
  3. 如果pull前有stash变更,可以在pull后选择放弃变更或应用变更。

Q: 使用push提交本地修改时有没有标准动作?
A: 首先要pull或fetch + merge代码,使用Merge Tool或手动解决掉所有冲突,确保本地代码不报错,然后确保本地变更已经被add + commit,最后push。

Q: 粗大事了,我的代码有毛病,怎么放弃修改,让远程仓库覆盖本地?
A: 需要分为几种情况讨论:在Workspace的变更、已add进Stage的变更,已commit到Repository的变更。

  1. Workspace的变更
    如果本地什么变更都不想要了,这种情况比较容易处理,只需要git checkout .
    如果只是急着获取Remote的代码,并不想丢失本地的变更,如上文stash的描述:

    1
    2
    3
    git stash
    git pull
    git stash apply
  2. Stage的变更
    已经Stage,尚未commit的变更,需要从Repository的最近一次commit恢复。

    1
    2
    git reset HEAD
    //强制更新指定的部分文件: git reset HEAD [file1] [file2] ...
  3. Repository的变更
    若变更已经commit到Repository,这个时候需要回退到Repository中的某次commit,也可以从upstream即Remote来reset:

    1
    2
    3
    4
    5
    6
    //从Repository直接reset到某次commit
    git reset --hard [commit ID]

    //下载Remote的变更后,强制reset到某次commit
    git fetch
    git reset --hard [commit ID]

Q: 当.gitignore加入到已存在的项目中,偶尔会track不想同步的文件,如何解决?
A: 以下两个例子可以说明。

  1. e.g. 单次移除不想同步的文件。

    1
    2
    3
    git rm –cache [不想同步的文件]
    git commit -a -m "Mr. gitignore, please do your job!"
    git push
  2. e.g. .classpath文件/.project文件在.gitignore生效前添加进了仓库。

    1
    2
    3
    4
    - 将.classpath挪出项目
    - 修改.gitignore, 确保添加了.classpath条目
    - git add & commit & push
    - 将.classpath挪回项目

合并之Merging v.s. Rebasing

git mergegit rebase的效果是类似的,都是将一个分支的变更合并到另外一个分支中。来看这样一个场景,当develop分支分出特性分支(featureA),之后develop分支前进了两个commit,特性分支也前进了两个commit。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
C0-C1-C2-C3       	(develop)
\
C11-C12 (featureA)

# By featureA's developer
git checkout featureA
git fetch
..//C11的修改
git commit -m "C11"
..//C12的修改
git commit -m "C12"
git push

# By develop branch maintainer
git checkout develop
git fetch
..//C2的修改
git commit -m "C2"
..//C3的修改
git commit -m "C3"
git push

此时如果使用merge,将特性分支的变更合入develop分支,就会在develop分支产生一个merge commit(C4),特性分支保持两个commit不变。

1
2
3
4
5
6
7
8
9
10
11
C0-C1-C2-C3--C4       	(develop)
\ /
C11-C12 (featureA)

# By develop branch maintainer
git checkout develop
git fetch
git merge featureA
..//解决冲突
git commit -m "C4"
git push

而此时如果rebase,会将特性分支两次commit的变更灵活地扦插到develop分支上,以patch commit即补丁的形式加在develop的最新commit后面。rebase使得版本史中的commit都在一条线上,不会有merge commit的产生,看起来更清晰。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
C0-C1-C2-C3      		  (featureA)

C0-C1-C2-C3-C11'-C12' (develop)

C11(discarded)-C12(discarded)

# By featureA's developer
git checkout featureA
git fetch
git rebase develop
..//冲突产生了,有三个方案

//1. 解决冲突,继续rebase
..//解决冲突
git add [fileA] [fileB] ...
git rebase --continue
//不需要再次commit, git会继续打补丁

//2. 放弃引起冲突的featureA分支的commit
git rebase --skip

//3. 放弃此次rebase
git rebase --abort

git push

同时也可以使用git rebase -i进行交互式地rebase。

Github

Github是全球最大的同性交友平台git协作平台,成千上万的开源项目都在该平台上热火朝天地进行着,无数人在贡献着设计、代码、文档,而贡献的途径主要是PR Pull Request。

PR

首先需要了解的是,提交PR前,需要将原项目Fork到自己名下,即复制一份到自己的github账号里。
在contribution的过程中,有三个角色需要注意。

  • upstream
    这是项目的公共仓库,所有开发者的代码在此同步。
  • origin
    这是自己的仓库,作为本地和upstream的桥梁。
  • local
    本地仓库。

远程仓库需要这样配置:

1
2
3
4
git remote remove origin
git remote add origin Luck/[project-name]
git remote add upstream [organization-name]/[project-name]
git remote set-url --push upstream no-pushing

通常的步骤如下:

  1. upstream(master) sync到local master。
  2. 在local:
    1. master切出fix-issue-xxx分支。
    2. coding,fix-issue-xxx分支经过若干次commit后变为fix-issue-xxx’分支。
    3. local push fix-issue-xxx’分支到origin。
  3. 在github:
    1. 提交PR,来源为origin的fix-issue-xxx’分支,填写PR description。
    2. maintainer review,经过一系列探讨及黑话交流 e.g. SGTM(Sounds Good To Me), LGTM(Looks Good To Me), PTAL(Please Take A Look)后,决定通过与否。
    3. PR status=1-desgin review, 2-code review, 3-doc review, 4-merge。
    4. 正式merge!upstream master->master’。
  4. upstream(master’) sync到local,local master->master’。

关于PR,有几点需要注意:

  1. Sign your commit,授信确保不会被假冒。
  2. In one commit,必要时squash。
  3. Commit message应当简明扼要又包含足够多的信息。
  4. PR description应当按规范填写。
  5. Fixed issue referenced,确保解决的issue都提到了。

项目人员组织架构

  • Chief Arch
    首席架构师, BDFL Benevolent Dictator For Life, 但是一般不行使独裁权,负责所有子系统技术架构的全局完整性。
  • 运营团队
    管理项目发行,促进社区沟通;管理下游分发与上游依赖关系。
  • Maintainer
    • 首席Maintainer:项目各方面的质量把控。
    • Core Maintainer:整体模块的技术、风格、哲学
    • 子系统Maintainer:对应子系统的路线图规划,PR和Bug管理。
  • Curator 监管者
    问题和PR分类,引导贡献者到相关资料和讨论。
  • Release Captain
    负责每一次release,由core maintainer轮值。

参考资料

Git Documentation - git-scm
Intro-git - HackBerkeley
极简教程
分支游戏