公司已经全面切换Git,我们的新项目刚好作为组内的第一个尝鲜项目。其实也用过Github一段时间了,对Git也不能说是完全小白。只是以前基本都是一个人作战,现在是多个人合作,刚好趁这个机会总结一下,让团队其他同事可以通过这篇文章快速的切换到Git来。

  1. Git基础

1.1 Git配置

Git有三个级别的配置文件,分别是:

  • 版本库级别的配置文件: 工程目录下, 使用git config -e打开编辑
  • 全局配置文件: 用户主目录下, 使用git config -e --global打开编辑
  • 系统级配置文件: /etc目录下,使用git config -e --system打开编辑

命令git config可以用于读取和更改INI配置文件的内容,使用命令

git config <section>.<key>

可以直接读取INI配置文件中某个配置的键值:

$ git config user.name
arganzheng
$ git config user.email
arganzheng@gmail.com

如果想更改或设置INI文件中某个属性的值也非常简单,带上value就是了:

git config <section>.<key> <value>

例如:

$ git config user.name arganzheng
$ git config user.email arganzheng@gmail.com

当然,一般来说,我们会讲这个信息设置在全局配置文件中,只需要带上--global参数就可以了:

$ git config --global user.name arganzheng
$ git config --global user.email arganzheng@gmail.com

还可以设置一些Git别名,方便使用更为简洁的命令:

$ git config --global alias.br branch
$ git config --global alias.ci "commit -s"
$ git config --global alias.co checkout
$ git config --global alias.st "-p status"

NOTES

  • 如果你有sudo权限,那么可以使用sudo git config --system alias.ci "commit -s"将其设置为系统级别的配置
  • 命令git commit -s中的参数-s含义为在提交说明的最后添加“Signed-off-by:”签名。
  • 命令git -p status中的参数-p含义是为git status命令的输出添加分页器。

1.2 Git目录

每一个项目有且只有一个’Git目录’(这和SVN,CVS的每个子目录中都有此类目录相反),这个叫’.git’的目录在你项目的根目录下(即工作目录下,这是默认设置,但并不是必须的)。 ‘Git目录’是为你的项目存储所有历史和元信息的目录 - 包括所有的对象(commits,trees,blobs,tags), 这些对象指向不同的分支:

$>tree -L 1
.
|-- HEAD         # 这个git项目当前处在哪个分支里
|-- config       # 项目的配置信息,git config命令会改动它
|-- description  # 项目的描述信息
|-- hooks/       # 系统默认钩子脚本目录
|-- index        # 索引文件
|-- logs/        # 各个refs的历史信息
|-- objects/     # Git本地仓库的所有对象 (commits, trees, blobs, tags)
`-- refs/        # 标识你项目里的每个分支指向了哪个提交(commit)。

1.3 Git的四个区域

理解和掌握Git每个命令关键在于理解Git的四个区域(相对SVN只有工作目录和远程仓库)。如下图所示:

git-notes.png

  • working directory/workspace: 工作目录
  • index/stage: 索引区/暂存区
  • local repository: 本地仓库
  • remote repository: 远程仓库

1、working directory:工作目录存储着你现在签出(checkout)来用来编辑的文件. 当你在项目的不同分支间切换时, 工作区里的文件经常会被替换和删除。工作目录只用来临时保存签出(checkout) 文件的地方, 你可以编辑工作目录的文件直到下次提交(commit)为止。

2、index/stage:索引区/暂存区是一个在你的工作目录和项目仓库之间的暂存区(staging area)。有了它, 你可以把许多内容的修改一起提交(commit)。当执行提交(commit)时,实际上是将暂存区的内容提交到版本库中, 而不是工作目录中的内容。Git很多命令都会涉及到暂存区的概念,例如:git diff命令。

git-and-repo-cheatsheet.png

3、local repository: 不同于SVN这种只有一个中央仓库的集中式版本管理系统,Git、Mercurial这些都属于分布式版本管理系统 (distributed version control)。每个git项目就是一个git仓库,每个人都是本地版本库的主人,可以在本地的版本库中随心所欲地提交修改、创建分支和里程碑,只是所有的操作都是在本地,而不是在程服务器。

4、remote repository: 这个类似于SVN的中央仓库,远程仓库是指托管在因特网或其他网络中的版本库。你可以有好几个远程仓库,通常有些仓库对你只读,有些则可以读写。本地仓库与远程仓库之间通过推送或者拉取进行同步。

下面这个图展示了工作区、版本库中的暂存区和版本库之间的关系:

工作区、版本库、暂存区原理图

  • 图中左侧为工作区,右侧为版本库。在版本库中标记为index的区域是暂存区,标记为master的是master分支所代表的目录树。
  • 图中可以看出此时HEAD实际是指向master分支的一个“游标”。所以图示的命令中出现HEAD的地方可以用master来替换。
  • 图中的objects标识的区域为Git的对象库,实际位于.git/objects目录下。
  • 当对工作区修改(或新增)的文件执行git add命令时,暂存区的目录树被更新,同时工作区修改(或新增)的文件内容被写入到对象库中的一个新的对象中,而该对象的ID被记录在暂存区的文件索引中。 当执行提交操作(git commit)时,暂存区的目录树写到版本库(对象库)中,master分支会做相应的更新。即master最新指向的目录树就是提交时原暂存区的目录树。
  • 当执行git reset HEAD命令时,暂存区的目录树会被重写,被master分支指向的目录树所替换,但是工作区不受影响。
  • 当执行git rm –cached 命令时,会直接从暂存区删除文件,工作区则不做出改变。
  • 当执行git checkout .或者git checkout – 命令时,会用暂存区全部或指定的文件替换工作区的文件。这个操作很危险,会清除工作区中未添加到暂存区的改动。
  • 当执行git checkout HEAD .或者git checkout HEAD 命令时,会用HEAD指向的master分支中的全部或者部分文件替换暂存区和以及工作区中的文件。这个命令也是极具危险性的,因为不但会清除工作区中未提交的改动,也会清除暂存区中未提交的改动。

1.4 Git的文件状态

Git工作目录下的每一个文件都将处于下面的某一个状态中:

  • Untracked
  • Tracked
    • Unmodified
    • Modified
    • Staged

所以使用 Git 时文件的生命周期如下图所示:

git-file-lifecycle.png

我们可以用git status查看文件处于什么状态。

TIPS 状态简览

git status 命令的输出十分详细,但其用语有些繁琐。使用git status -s命令,可以得到一种更为紧凑的格式输出,类似于svn status:

$ git status -s
 M README
MM Rakefile
A  lib/git.rb
M  lib/simplegit.rb
?? LICENSE.txt

但是不同与svn status,状态栏的第二列表示的不是属性修改。在Git中,出现在第二列右边的 M 表示该文件被修改了但是还没放入暂存区,出现在靠左边的 M 表示该文件被修改了并放入了暂存区。

1.5 Git基本操作

初始化一个新的仓库:

$ cd /Users/argan/study/git/workspace
$ git init demo 
Initialized empty Git repository in /Users/argan/study/git/workspace/demo/.git/ 

TIPS && NOTES

上面我们是从零开始新建了一个空的项目,如果你有一个项目已经存在,想把它纳入Git版本控制,那么可以直接进入到这个项目的根目录,执行git init就可以了。

更常见的获取本地Git仓库的方式是:从已有的Git仓库中clone(克隆,复制)到本地来,如:

$ git clone https://github.com/arganzheng/arganzheng.github.com.git

创建新文件,将其添加到索引区/暂存区中:

$ cd demo
$ echo "hello git" > README.md

$ git status
On branch master

Initial commit

Untracked files:
  (use "git add <file>..." to include in what will be committed)

	README.md

nothing added to commit but untracked files present (use "git add" to track)
$ git add README.md 

$ git status
On branch master

Initial commit

Changes to be committed:
  (use "git rm --cached <file>..." to unstage)

	new file:   README.md

提交到本地仓库:

$ git commit -s -m "initialized"
[master (root-commit) a5edef9] initialized
 1 file changed, 1 insertion(+)
 create mode 100644 README.md

看看状态和日志:

$ git status
On branch master
nothing to commit, working directory clean

$ git log --stat
commit a5edef90edd82f0b0385105286c730db372bce8f
Author: arganzheng <arganzheng@gmail.com>
Date:   Sat Nov 19 18:19:44 2016 +0800

    initialized

    Signed-off-by: arganzheng <arganzheng@gmail.com>

 README.md | 1 +
 1 file changed, 1 insertion(+)

再次修改,注意工作区的修改要提交之前都得先add到索引区/暂存区:

$ echo "modify" >> README.md
$ git status
On branch master
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

	modified:   README.md

no changes added to commit (use "git add" and/or "git commit -a")
$ git add README.md
$ git status
On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

	modified:   README.md

TIPS && NOTES Git跟踪的是内容不是文件

很多版本控制系统都提供了一个 “add” 命令:告诉系统开始去跟踪某一个文件的改动。但是Git里的 ”add” 命令从某种程度上讲更为简单和强大. git add 不但是用来添加不在版本控制中的新文件,也用于添加已在版本控制中但是刚修改过的文件(这跟SVN不同); 在这两种情况下, Git都会获得当前文件的快照并且把内容暂存(stage)到索引中,为下一次commit做好准备。

  1. Git分支

2.1 Git分支地址

Git的分支地址由 主机名/分支名 唯一确定。比如 origin master。其中origin就是远程主机名称,master 可以用git remote命令查看:

$ git remote
origin
$ git remote -v
origin	https://github.com/arganzheng/arganzheng.github.com.git (fetch)
origin	https://github.com/arganzheng/arganzheng.github.com.git (push)

$ git branch
* master
$ git branch -va
* master                538da30 ranking
  remotes/origin/HEAD   -> origin/master
  remotes/origin/master 538da30 ranking

这个名称对应的URL其实是配置在git的config文件中:

$ cat .git/config
[core]
	repositoryformatversion = 0
	filemode = true
	bare = false
	logallrefupdates = true
	ignorecase = true
	precomposeunicode = true
[remote "origin"]
	url = https://github.com/arganzheng/arganzheng.github.com.git
	fetch = +refs/heads/*:refs/ remotes/origin/*
[branch "master"]
	remote = origin
	merge = refs/heads/master

2.2 切换分支

Git的分支是其非常重要的特性之一,因为Git有本地仓库的概念,所以你可以随意的创建、合并、删除分支。

创建一个叫做hotfix的分支,并切换过去:

$ git branch
* master
$ git status 
On branch master
nothing to commit, working directory clean

$ git branch hotfix # 创建一个叫做hotfix的分支
$ git branch
  hotfix
* master

切换到hotfix分支:

$ git checkout hotfix	# 切换到hotfix分支
Switched to branch 'hotfix'
$ git status
On branch hotfix
nothing to commit, working directory clean

从一个清洁的工作目录(clean working directory)切换到新的分支是非常简单的事情。如果工作目录有修改呢?

答案是看情况。

如果修改的内容跟切换后的分支没有交集(比如增加新文件),那么工作目录尚未提交的情況下checkout到其他的分支時,修改內容會从原來的分支移动到切换后的分支,这个跟svn switch是一样的:

$ echo "new file created on master" >> createdByMaster.txt
$ git status
On branch master
Untracked files:
  (use "git add <file>..." to include in what will be committed)

	createdByMaster.txt

nothing added to commit but untracked files present (use "git add" to track)
$ git checkout hotfix
Switched to branch 'hotfix'
$ git status
On branch hotfix
Untracked files:
  (use "git add <file>..." to include in what will be committed)

	createdByMaster.txt

nothing added to commit but untracked files present (use "git add" to track)

暂存区有修改未提交也是一样带过去:

$ git checkout master
Switched to branch 'master'
$ git status
On branch master
Untracked files:
  (use "git add <file>..." to include in what will be committed)

	createdByMaster.txt

nothing added to commit but untracked files present (use "git add" to track)
$ git add createdByMaster.txt
$ git status
On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

	new file:   createdByMaster.txt

$ git checkout hotfix
A	createdByMaster.txt
Switched to branch 'hotfix'
$ git status
On branch hotfix
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

	new file:   createdByMaster.txt

但如果在切换后的分支中有相同的文件,而且有任何修改的話,checkout就会失败。这时一定要先提交修改內容,或者将修改的內容放到stash中储藏后再checkout:

$ echo "test switch" >> README.md
$ git status
On branch master
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

	modified:   README.md

no changes added to commit (use "git add" and/or "git commit -a")
$ git checkout hotfix
error: Your local changes to the following files would be overwritten by checkout:
	README.md
Please, commit your changes or stash them before you can switch branches.
Aborting

保存到暂存区也是不行的:

$ git add README.md
$ git status
On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

	modified:   README.md

$ git checkout hotfix
error: Your local changes to the following files would be overwritten by checkout:
	README.md
Please, commit your changes or stash them before you can switch branches.
Aborting

因为hotfix分支同样存在README.md文件,然后一个本地的git repo只有一个工作目录和暂存区,checkout操作只是将HEAD指针从一个分支切换到另一个分支而已,如果不提交或者储藏你的工作区和暂存区的修改,就会丢失这部分修改。

这时候你有三种选择:

  1. 把本地变更提交(commit),再切换分支。
  2. 把本地变更储藏(stash)起来,再切换分支,切换前的状态可以在checkout回来之后执行stash apply恢复
  3. 丢弃本地变更,使用-f参数强制checkout(危险,不建议)。

第一种方式很好理解,第二种方式是Git独有的,我们体验一下:

$ echo "added on hotfix" >> README.md
$ git status
On branch hotfix
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

	new file:   createdByMaster.txt

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

	modified:   README.md
$ git checkout master
error: Your local changes to the following files would be overwritten by checkout:
	README.md
Please, commit your changes or stash them before you can switch branches.
Aborting

储藏本地变更:

$ git stash
Saved working directory and index state WIP on hotfix: 96cd658 for switch to master
HEAD is now at 96cd658 for switch to master
$ git status
On branch hotfix
nothing to commit, working directory clean

可以看到工作区和暂存区的变更都被储藏起来了。现在工作区是干净的,可以简单切换过去:

$ git checkout master
Switched to branch 'master'
$ git status
On branch master
nothing to commit, working directory clean

储藏栈是所有分支共用的,可以使用git stash list查看现有的储藏:

$ git stash list
stash@{0}: WIP on hotfix: 96cd658 for switch to master

还可以查看储藏的变更情况:

$ git stash show
 README.md           | 1 +
 createdByMaster.txt | 1 +
 2 files changed, 2 insertions(+)

你可以重新应用你刚刚实施的储藏,所采用的命令就是git stash apply。如果你想应用更早的储藏,你可以通过名字指定它,像这样:git stash apply stash@{2}。如果你不指明,Git 默认使用最近的储藏并尝试应用它:

$ git stash apply
Auto-merging README.md
CONFLICT (content): Merge conflict in README.md

因为两边都有修改,所以合并有冲突:

$ git status
On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

	new file:   createdByMaster.txt

Unmerged paths:
  (use "git reset HEAD <file>..." to unstage)
  (use "git add <file>..." to mark resolution)

	both modified:   README.md

$ cat README.md
hello git
<<<<<<< Updated upstream
test switch for a dirty working directory
=======
added on hotfix
>>>>>>> Stashed changes

解决冲突,使用git add标记为冲突解决:

$ git add README.md
$ git status
On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

	modified:   README.md
	new file:   createdByMaster.txt

$ git diff --cached
diff --git a/README.md b/README.md
index 8abad86..85398b1 100644
--- a/README.md
+++ b/README.md
@@ -0,0 +3 @@
 hello git
 test switch for a dirty working directory
+added on hotfix
diff --git a/createdByMaster.txt b/createdByMaster.txt
new file mode 100644
index 0000000..baad091
--- /dev/null
+++ b/createdByMaster.txt
@@ -0,0 +1 @@
+new file created on master

关于git stash的更多信息,可以参考这篇文章,写的很详细:6.3 Git 工具 - 储藏(Stashing)

2.3 合并分支

分支的合并有两种方式:使用 merge 或者 rebase。根据使用的方法合并的分支历史有很大的差别。

  • merge:把两个分支的最新快照和以及二者最近的共同祖先进行三方合并,合并的结果是生成一个新的快照(并提交,如果没有没有冲突的话)
    • fast-forward merge:快转合并
    • three-way merge:三方合并
  • rebase:提取目标分支中引入的补丁和修改,然后在当前分支的基础上依次再应用一次。

这两种整合方法的最终结果没有任何区别,但是rebase使得提交历史更加整洁。你在查看一个经过变基的分支的历史记录时会发现,尽管实际的开发工作是并行的,但它们看上去就像是先后串行的一样,提交历史是一条直线没有分叉。

看起来rebase似乎比较推荐,但是实际上要用它得遵守一条准则:不要对在你的仓库外有副本的分支执行变基。 具体参考 变基的风险

2.4 远程分支

在 [2.1Git分支地址] 中我们知道一个git分支的地址是由于 主机名/分支名 确定的,所以如果你要把你的本地变更推送到远程分支上,那么可以使用git push命令:

git push <remote> <branch>

如果你要本地所有的分支(不包括tags分支)变更都推送到远程仓库,那么可以使用--all参数:

git push <remote> --all

如果要推送tags分支,那么可以使用:

git push <remote> --tags

TIPS 关于icode提交时奇怪的分支名称

百度使用icode的小伙伴们在执行:

$ git push

会提示错误说不允许直接执行git push,而是要执行:

$ git push origin HEAD:refs/for/master  # 分支名称为master
$ git push origin HEAD:refs/for/svn/gi-impl_1-0-1_BRANCH # 分支名称为svn/gi-impl_1-0-1_BRANCH

这个诡异的名字refs/for/<branch>其实是CodeReview工具Gerrit的要求。这样,代码会暂时提交到一个临时分支,用于code review,review通过执行合并才会真正提交到远程分支。如下图所示:

git-notes.png

具体参见: Why is git push gerrit HEAD:refs/for/master used instead of git push origin master

与SVN命令的简单对比

1、checkout远程分支到本地

SVN:

$ svn co URL

Git:

$ git clone URL
# 如果是branch的话还需要checkout过去
$ git checkout branch_name 

2、更新本地工作目录

SVN:

$ svn up 

Git: 记住,在Git中有两种方式整合变化(integrate changes): merge 或者 rebase。

2.1 Merge - the changes are merged directly creating a new commit with a new history.

$ git pull

或者分两步走:

$ git fetch <remote>
$ git merge origin/<current-branch>

2.2 Rebase - A rebase rolls back your history to the point where you forked from the remote, applies the remote changes, and then reapplies your local changes commit by commit. This rewrites history and should therefore only be used on unpublished branches.

$ git fetch
$ git rebase

或者

$ git pull --rebase # (preferred method)

3、拉分支

SVN:

$ svn cp <branch_url>

Git:

$ git branch <branch_name>
$ git tag -a <tag_name>

4、切换分支

SVN:

$ svn switch URL

Git:

$ git checkout branch_name

5、基本文件跟踪

SVN:

$ svn add file 	# add new file
$ svn rm file 	# delete file
$ svn mv file 	# rename file

Git:

$ git add file 	# add new file
$ git rm file 	# delete file
$ git mv file 	# rename file	

6、查看本地状态

SVN:

$ svn status

Git:

$ git status -s

7、查看差异

SVN:

$ svn diff | less
$ svn diff -r<revisionNumber> <path>

Git:

$ git diff # 默认就是less分页查看
$ git diff <revisionNumber> <path>

8、查看日志

SVN:

$ svn log
$ svn blame file

Git:

$ git log
$ git blame file

9、将本地修改提交到中央仓库

SVN:

$ svn ci

Git:

# 1. Commit to local repository
$ git commit 
# 2. Push the changes to remote repository
$ git push

TIPS

如果只想推送当前分支的提交,那么可以使用这个命令:

$ git push --set-upstream origin current_branch_name

10、回滚

1. 回滚本地变更

SVN:

$ svn revert file 	# 回滚某个文件
$ svn revert -R . 	# 回滚整个目录

Git:

$ git checkout -- file 	# 回滚某个文件
$ git reset --hard HEAD # 回滚整个工作目录

2. 回滚到某个历史版本

SVN:

$ svn merge -r HEAD:<revisionNumber> URL  

或者

$ svn up -r <revisionNumber>

Git:

$ git revert -n HEAD~3

或者

$ git checkout revisionNumber 

11、合并

SVN:

svn merge URL 

Git: 记住,在Git中有两种方式整合变化(integrate changes): merge 或者 rebase。

$ git merge <branch>

或者

$ git rebase

TIPS cherry-pick

Aside from merging, sometimes you want to just pick one commit from a different branch. To apply the changes in revision rev and commit them to the current branch use:

$ git cherry-pick rev

相当于SVN命令:

$ svn merge -c rev url

Git高级功能

  • Advanced log
  • rebase
  • cherry-pick
  • reflog
  • Shallow clone
  • Hooks
  • LFS

参考文档

  1. 常用 Git 命令清单
  2. 我的git笔记
  3. Git远程操作详解 阮老师的文章总是那么通俗易懂
  4. Git分支管理策略
  5. Learn Git Branching 一个在线学习Git分支的网站
  6. Git 分支
  7. Pro Git Book中文版
  8. Atlassian公司的这篇tutorial
  9. Git笔记(三)——[cherry-pick, merge, rebase] 比较高级的话题