Git 是 Linus Torvalds 为了帮助管理 Linux 内核开发而开发的一个开放源码的版本控制软件,它是一个开源的分布式版本控制系统,用于敏捷高效地处理任何或小或大的项目。
Git 基本流程
Git 会有三个区域:
- Working Tree:当前的工作区域
- Index/Stage:暂存区域,即使用
git add
后的结果 - Repository:提交的历史,即使用
git commit
后的结果
以下简单敘述一下把文件存入Repository流程:
刚开始 working tree 、 index 与 repository(HEAD) 里面的內容都是一致的
当 Git 管理的文件夹里面的内容出现改变后,此时 working tree 的内容就会跟 index 及 repository(HEAD) 的不一致,而 Git 知道是哪些文件(Tracked File)被改动过,直接将文件状态设置为 modified (Unstaged files)。
当我们执行
git add
后,会将这些改变的文件内容加入 index 中(Staged files),所以此时 working tree 跟 index 的内容是一致的,但他们与 repository(HEAD) 内容不一致。接着执行
git commit
后,将 Git 索引中所有改变的文件内容提交至 Repository 中,建立出新的 commit 节点(HEAD)后,working tree、index 与 repository(HEAD) 区域的内容又会保持一致。
Git 基础
获取 Git 仓库
在现有目录中初始化仓库
$ git init
该命令将创建一个名为 .git
的子目录,这个子目录含有你初始化的 Git 仓库中所有的必须文件,这些文件是 Git 仓库的骨干。但是,在这个时候,我们仅仅是做了一个初始化的操作,你的项目里的文件还没有被跟踪。
如果你是在一个已经存在文件的文件夹(而不是空文件夹)中初始化 Git 仓库来进行版本控制的话,你应该开始跟踪这些文件并提交。你可通过 git add 命令来实现对指定文件的跟踪,然后执行 git commit 提交:
$ git add *.c
$ git commit -m 'initial project version'
克隆现有的仓库
如果你想获取一份已经存在的 Git 仓库拷贝,可以使用 git clone [url]
来达成目的。
$ git clone https://github.com/apache/dubbo.git
如果想自定义本地仓库的名字,可以使用如下命令:
$ git clone https://github.com/apache/dubbo.git dubbo-x
记录每次更新到仓库
我们使用 Git 时文件的生命周期如下:
查看文件的状态
可以使用 git status
查看文件处于什么状态。
$ git status
On branch master
No commits yet
# 已经被 commit 的内容,这些内容是 Unmodified 状态
Changes to be committed:
(use "git rm --cached <file>..." to unstage)
new file: MODIFY
# 暂存态文件发生变更,这些内容是 Modified 状态
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: MODIFY
# 未跟踪文件,这些内容是 Untracked 状态
Untracked files:
(use "git add <file>..." to include in what will be committed)
README
当我们需要开始跟踪 Untracked 状态的文件,或者将已跟踪的 Modified 状态的文件放到暂存区,我们可以使用 git add
命令。它也可以被用做合并时把有冲突的文件标记为已解决。
我们也可以使用 git status -s / git status -short
得到一种更为紧凑的格式输出
$ git status -s
AM MODIFY
?? README
新添加的未跟踪文件前面有??标记,新添加到暂存区中的文件前面有A标记,修改过的文件前面有M标记。你可能注意到了M有两个可以出现的位置,出现在右边的M表示该文件被修改了但是还没放入暂存区,出现在靠左边的M表示该文件被修改了并放入了暂存区。
查看未暂存和已暂存的修改
git status
命令输出的是暂存文件级别的修改(也就是 commit 的修改)。我们通过 git diff
可以查看未暂存的修改,通过 git diff --staged
查看已暂存的修改。
如果不小心用
git add
添加了不需要的文件进入暂存区,我们可以使用git reset HEAD <file>
从暂存区中删除。
提交更新
我们可以使用 git commit
来提交更新,或者是使用 git commit -m "{}"
将提交信息和命令放在同一行。
但有一点烦琐的事,我们需要使用 git add {}
命令把想要提交的文件加入暂存区,然后通过 git commit
提交成快照。幸运的是,我们可以使用 git commit -a
来将所有跟踪文件暂存起来一并提交。
移除文件
通过 git rm
可以从暂存区中删除文件,并连带从工作目录删除指定文件。也可以加上 --cached
只删除暂存区文件,而保留工作目录的文件。
移动文件
Git 中 git mv file_from file_to
其实是合并操作,具体如下:
$ mv file_from file_to
$ git rm file_from
$ git add file_to
忽略文件
如果我们不想跟踪一些文件,我们可以创建一个名为 .gitignore
的文件,列出要忽略的文件模式。
文件 .gitignore
的格式规范如下:
- 所有空行或者以 # 开头的行都会被 Git 忽略。
- 可以使用标准的 glob 模式匹配。
- 匹配模式可以以
(/)
开头防止递归。 - 匹配模式可以以
(/)
结尾指定目录。 - 要忽略指定模式以外的文件或目录,可以在模式前加上惊叹号
(!)
取反。
Glob 模式
所谓的 Glob 模式是指 Shell 所使用的简化了的正则表达式。星号
(*)
匹配零个或多个任意字符;[ abc ]
匹配任何一个列在方括号中的字符(这个例子要么匹配一个 a,要么匹配一个 b,要么匹配一个 c);问号(?)
只匹配一个任意字符;如果在方括号中使用短划线分隔两个字符,表示所有在这两个字符范围内的都可以匹配(比如[0-9]
表示匹配所有 0 到 9 的数字)。使用两个星号(*)
表示匹配任意中间目录,比如a/**/z*
可以匹配 a/z,a/b/z 或 a/b/c/z
查看提交历史
默认不用任何参数的话,git log 会按提交时间列出所有的更新,最近的更新排在最上面。正如你所看到的,这个命令会列出每个提交的 SHA-1 校验和、作者的名字和电子邮件地址、提交时间以及提交说明。
常用参数
场景 | 参数 | 描述 |
---|---|---|
查看最近几次提交 | -{number} |
例:-2 查看最近两次提交 |
按补丁格式查看内容差异 | -p |
|
查看提交的简略统计信息 | -stat |
|
格式化输出信息 | --pretty |
有 --pretty=oneline 、--pretty=short 、--pretty=full 、--pretty=fuller 可以用 |
按ASCII图形表示分支合并历史 | --graph |
pretty=format 是定制输出,例: —pretty=format:”%h - %an, %ar : %s”
限制输出长度
场景 | 参数 | 描述 |
---|---|---|
使用较短的相对时间显示 | --relative-date |
如,2 week ago |
仅显示 SHA-1 的前几个字符 | --abbrev-commit |
|
限制查看最近一段时间的提交 | --since 和 --until |
时间可以是具体的某一天 “2008-01-15” 或是 “2 years 1 day 3 minutes ago“ |
显示指定作者的提交 | —author | |
按关键字检索说明中的关键字 | —grep | |
按关键字检索提交内容 | -S | -Sfunction_name 列出添加或者是移除了某些字符串的提交 |
重新提交
我们可以通过以下命令将暂存区的文件提交,如果自上次提交以来你还未做任何修改,那么快照保持不变,只是修改了提交信息。最终只会有一个提交出现在 git log 中。
$ git commit -m "test amend"
$ git add test.file
$ git commit --amend -m "test amend2"
存储本地快照
如果当前修改不想提交,但又需要切换到另一个分支做其他的工作,可以使用 git stash
命令。
命令 | 描述 |
---|---|
git stash |
保存所有未提交的修改(包括暂存的和非暂存的)内容 |
git stash save "test stash" |
额外加一个 message 用于记录版本 |
git stash pop |
pop 堆栈中的 stash,恢复工作目录的修改 |
git stash apply |
peek 堆栈中的 stash,恢复工作目录的修改 |
git stash list |
查看 stash 列表 {0} 代表栈顶 |
git stash drop stash{1} |
删除指定 stash |
git stash clear |
删除全部 stash |
git stash show [stash name] [-p / --patch] |
查看指定 stash 的 diff-p 选项可以查看特定 stash 详细 diff |
git stash branch [new branch] |
从 stash 创建分支 |
默认情况下,
git stash
会缓存下列文件:
- 添加到暂存区的修改(staged changes)
Git 跟踪的但并未添加到暂存区的修改(unstaged changes)
但不会缓存一下文件:
在工作目录中新的文件(untracked files)
被忽略的文件(ignored files)
git stash
命令提供了参数用于缓存上面两种类型的文件。使用-u
或者--include-untracked
可以stash untracked文件。使用-a
或者--all
命令可以stash当前目录下的所有修改。
使用远程仓库
命令 | 描述 |
---|---|
git remote (-v) |
查看远程仓库列表 |
git remote show [remote name] |
查看远程仓库的URL和跟踪分支 |
git remote add <short name> <url> |
添加远程仓库 |
git fetch [remote name] |
从远程仓库中抓取与拉取 |
git pull [remote name] [branch name] |
向远程仓库推送分支 |
git remote rename [old name] [new name] |
修改远程仓库名称 |
标签
命令 | 描述 |
---|---|
git tag |
列出标签 |
git tag -l '{pattern}' |
列出感兴趣的标签 |
git show {tag name} |
查看标签信息与对应的提交信息 |
git tag -a {tag name} -m '{message}' |
创建附注标签 |
git tag {tag name} |
创建轻量标签 |
git tag -a {tag name} {校验和} |
根据校验和打标签 |
git push {remote name} {tag name} |
向远程仓库推送标签 |
git push origin --tags |
同时向远程仓库推送很多标签 |
附注标签 annotated 和轻量标签 lightweight 的区别
轻量标签很像一个不会改变的分支,它只是一个特定提交的引用。
附注标签是存储在 Git 数据库中的一个完整对象。它们是可以被校验的;其中包含打标签者的名字、电子邮件地址、日期时间;还有一个标签信息;并且可以使用 GNU Privacy Guard (GPG) 签名与验证。通常建议创建附注标签,这样你可以拥有以上所有信息;但是如果你只是想用一个临时的标签,或者因为某些原因不想要保存那些信息,轻量标签也是可用的。
别名
通过以下配置,使得 只需要输入 git co
即可以使用 git checkout
。
$ git config --global alias.co checkout
$ git config --global alias.br branch
$ git config --global alias.ci commit
$ git config --global alias.st status
Git 分支
当使用 git commit 进行提交操作时,Git 会先计算每一个子目录的校验和,然后在
Git 仓库中这些校验和保存为树对象(tree)。随后,Git 便会创建一个提交对象(blob),它除了包含上面提到的那些信息外,还包含指向这个树对象(项目根目录)的指针。如此一来,Git 就可以在需要的时候重现此次保存的快照。
现在,Git 仓库中有五个对象:三个 blob 对象(保存着文件快照)、一个树对象(记录着目录结构和 blob 对象索引)以及一个提交对象(包含着指向前述树对象的指针和所有提交信息)。
做些修改再提交,这次的提交对象会包含一个指向上次提交对象的父指针。
Git的分支,其实本质上仅仅是指向提交对象的可变指针。Git 的默认分支名字是 master。在多次提交操作之后,你其实已经有一个指向最后那个提交对象的 master 分支。它会在每次的提交操作中自动向前移动。
分支创建
通过 git branch testing
为当前提交对象上创建一个指针。
注意 branch 只是创建分支,并不会自动切换。
而 Git 通过一个名为 HEAD 的特殊指针,指向当前所在的本地分支。
分支切换
我们可以通过两种方式切换分支(也就是让 HEAD 指针指向对应分支):switch
和 checkout
。switch
是切换到已有分支; checkout
是如果 branch 存在直接切换,否则从 remote 仓库拉出来。
当我们 commit 一些代码,testing 分支向前移动了一个版本,而 master 却没有。分支如下图。
此时我们再切回 master 分支,并 commit 一些代码,在 Git 的版本树中会产生分叉。分支如下图。
我们可以使用 git log --oneline --graph --all
查看分支情况。
$ git log --oneline --decorate --graph --all
* c2b9e (HEAD, master) made other changes
| * 87ab2 ( testing) made a change
|/
* f30ab add feature #32 - ability to add new formats to the
* 34ac2 fixed bug #1328 - stack overflow under certain conditions
* 98ca9 initial commit of my project
分支删除
我们可以通过以下命令删除分支。
# 删除分支(但必须提交所有修改)
$ git branch -d [branch]
# 强制删除分支
$ git branch -D [branch]
远程分支
通过 git clone
我们可以从远程仓库拉取版本库到本地,如果不指定 Git 会默认命名远程分支为 origin。我们也可以通过 git clone -o remote
命令指定远程分支名称。
如果你在本地的 master 分支做了一些工作,然而在同一时间,其他人推送提交到 git.ourcompany.com
并更新了它的 master 分支,那么你的提交历史将向不同的方向前进。也许,只要你不与 origin 服务器连接,你的 origin/master 指针就不会移动。
同步远程分支
我们可以通过 git fetch [origin]
命令从远端同步数据库,移动 origin/master
指针指向新的、更新后的位置。
推送到远程分支
我们可以通过 git push [origin] [branch]
来推送分支。
这里有些工作被简化了。Git 自动将 testing 分支名字展开为 refs/heads/testing:refs/heads/testing,那意味着,“推送本地的 testing 分支来更新远程仓库上的 testing 分支。你也可以运行 git push origin serverfix:serverfix
,它会做同样的事。也可以通过这种格式来推送本地分支到一个命名不相同的远程分支 git push origin testing:testing-remote
来将本地的 testing 分支推送到远程仓库上的 testing-remote 分支。
跟踪分支
跟踪分支是与远程分支有直接关系的本地分支。如果在一个跟踪分支上输入 git pull
,Git 能自动地识别去哪个服务器上抓取、合并到哪个分支。
# checkout 远程仓库名称 不修改名字
$ git checkout --track [remote name]/[branch]
# checkout 远程仓库名称 修改名字
$ git checkout -b [branch] [remote name]/[branch] --track [remote name]/[branch]
从一个远程跟踪分支检出一个本地分支会自动创建一个叫做 “跟踪分支”。
我们可以通过
git branch -vv
查看本地分支对应的跟踪分支。$ git branch -vv new_prod ac1c3bf5 [remote/prod: ahead 1, behind 1] fix bug
在远程分支的后面有 ahead 和 behind,代表本地分支比 remote/prod 分支领先一个提交、落后一个提交。
设置已有的本地分支跟踪一个刚刚拉取下来的远程分支,或者想要修改正在跟踪的上游分支,你可以在任意时间使用 -u
或 --set-upstream-to
选项运行 git branch
来显式地设置。
上游快捷方式
当设置好跟踪分支后,可以通过
@{upstream}
或@{u}
快捷方式来引用它。所以在 master 分支时并且它正在跟踪origin/master
时,如果愿意的话可以使用git merge @{u}
来取代git merge origin/master
。
分支 Merge
通过使用 git merge [branch]
将指定分支合并到当前的分支。
Fast-Forward 快进合并
如果通过以下命令合并分支,由于 master 是 hotfix 的直接上游,所以 Git 只需要将简单地将指针向前移动即可。
$ git checkout master
$ git merge hotfix
Updating f42c576. .3a0874c
Fast-forward
index. htmlI 2
++
1 file changed, 2 insertions(+)
如下图所示:
合并过程
由于当前产生了分叉分支,而现在需要将 iss53 分支合并到 master 分支。会执行以下命令:
$ git checkout master
$ git merge iss53
Merge made by the recursive strategy.
index.html | 1 +
1 file changed , 1 insertion(+)
因为,master 分支所在提交并不是 iss53 分支所在提交的直接祖先,Git 不得不做一些额外的工作。出现这种情况的时候,Git 会使用两个分支的末端所指的快照(C4 和 C5)以及这两个分支的工作祖先(C2),做一个简单的三方合并。
和之间将分支指针向前推进所不同的是,Git 将此次三方合并的结果做了一个新的快照并且自动创建一个新的提交指向它。这个被称作一次合并提交, 它的特别之处在于他有不止一个父提交。
需要指出的是,Git 会自行决定选取哪一个提交作为最优的共同祖先,并以此作为合并的基础。
分支 Rebase
整合分支最容易的方法是 merge 命令。它会把两个分支的最新快照(C4 和 C5)以及二者最近的共同祖先(C2)进行三方合并,合并的结果是生成一个新的快照(并提交)。
但我们还有另一种方式整合两个分支,即 rebase 命令。
普通 rebase
$ git checkout experiment
$ git rebase master
它的原理是首先找到这两个分支(即当前分支 experiment、变基操作的目标基底分支 master)的最近共同祖先 C2,然后对比当前分支相对于该祖先的历次提交,提取相应的修改并存为临时文件,然后将当前分支指向目标基底 C3,最后以此将之前另存为临时文件的修改依序应用。
现在回到 master 分支,进行一次快进合并。
$ git checkout master
$ git merge experiment
请注意,无论是通过变基,还是通过三方合并,整合的最终结果所指向的快照始终是一样的,只不过提交历史不同罢了。变基是将一系列提交按照原有次序依次应用到另一分支上,而合并是把最终结果合在一起。
如果不想切换分支,则可以使用 git rebase [target branch] [topic branch]
命令直接将特性分支变基到目标分支。
分离特性 rebase
在对两个分支进行变基时,所生成的“重演”并不一定要在目标分支上应用,你也可以指定另外的一个分支进行应用。就像从一个特性分支里再分出一个特性分支的提交历史中的例子这样。你创建了一个特性分支 server,为服务端添加了一些功能,提交了 C3 和 C4。然后从 C3 上创建了特性分支 client,为客户端添加了一些功能,提交了 C8 和 C9。最后,你回到 server 分支,又提交了 C10。
假设你希望将 client 中的修改合并到主分支并发布,但暂时并不想合并 server 中的修改,因为它们还需要经过更全面的测试。这时,你就可以使用 git rebase 命令的 —onto 选项,选中在 client 分支里但不在 server 分支里的修改(即 C8 和 C9),将它们在 master 分支上重演:
$ git rebase --onto master server client
以上命令的意思是:“取出 client 分支,找出处于 client 分支和 server 分支的共同祖先之后的修改,然后把它们在 master 分支上重演一遍”。这理解起来有一点复杂,不过效果非常酷。
现在可以快进合并 master 分支了。
$ git checkout master
$ git merge client
分支 Reset
命令 git reset
有三种模式:Soft、Mixed、Hard,具体使用方法见下图。
如果你使用了 git reset --hard <commit RSA version>
会将 stage 区和工作目录里的内容会被完全重置为和 <commit RSA version>
的新位置相同的内容。意味着所有没有提交的 commit 修改会丢失,提交过的 commit 可以通过 git pull
重新同步下来。
git reset --mixed
和 git reset --soft
同理。