深入理解git系列

title

文章参考: http://git.oschina.net/progit/

1,git简介

版本控制

简而言之,就是可以报存你所有的修改,所有的历史版本;有了它,就可以将某个文件回溯到之前的状态,甚至可以将整个项目回退到过去某个时间点的状态;你可以比较文件的变化细节,查出最后是谁修改了哪个地方,从而找出导致怪异问题出现的原因,又是谁在何时报告了某个功能缺陷等等

类别

集中化的版本控制系统(eg: svn)

分布式版本控制系统(eg: git)

文件的三种状态

对于任何一个文件,在 Git 内都只有三种状态:

  • 已提交(committed),已提交表示该文件已经被安全地保存在本地数据库中了;
  • 已修改(modified,已修改表示修改了某个文件,但还没有提交保存;
  • 已暂存(staged)。已暂存表示把已修改的文件放在下次提交时要保存的清单中。
    git管理项目时,文件流转的三个工作区域:git的工作目录,暂存区域,本地仓库
  • 每个项目都有一个Git目录(如果 git clone 出来的话,就是其中 .git 的目录)
  • 从项目中取出某个版本的所有文件和目录,用以开始后续工作的叫做工作目录
  • 所谓的暂存区域只不过是个简单的文件,一般都放在 Git 目录中

2,git基础

取得项目的Git仓库

有两种方法:1,在现存的目录下,通过导入所有文件来创建新的Git仓库;2,从已有的Git仓库克隆出一个新的镜像仓库

  • 在工作目录中初始化新仓库

    1
    2
    3
    git init // 初始化后,在当前目录下会出现一个名为 .git 的目录,所有 Git 需要的数据和资源都存放在这个目录中
    git add README // 如果当前目录下有几个文件想要纳入版本控制,需要先用`git add`命令告诉git开始对这些文件进行跟踪,并进行提交
    git commit -m 'initial project version'
  • 从现有仓库克隆
    克隆仓库的命令格式为 git clone [url]。比如,要克隆 Ruby 语言的 Git 代码仓库 Grit,可以用下面的命令:

    1
    git clone git://github.com/schacon/grit.git

当前目录下创建一个名为grit的目录,其中包含一个.git的目录用于保存下载下来的所有版本记录,然后从中取出最新版本的文件拷贝
如果希望在git clone的时候,自己定义要新建的项目目录的名称,可以再上面的命令的末尾指定新的名字

1
git clone git://github.com/schacon/grit.git mygrit

现在新建的目录就成了mygrit

文件状态

git status 可以查看文件目前处于什么状态
git add xx 这是一个多功能命,根据目标文件的状态不同,此命令的效果也不同;可以用它开始跟踪新文件,或者把已跟踪的文件放到暂存区,还能用于合并时把有冲突的文件标记为已解决状态

忽略某些文件

我们可以创建一个.gitignore的文件,列出要忽略的文件模式
.gitignore的格式规范如下:

  • 所有空行或者以注释符号#开头的行都会被Git忽略
  • 可以使用标准的glob模式匹配(shell所使用的简化了的正则表达式)
  • 匹配模式最后跟反斜杠(/)说明要忽略的是目录
  • 要忽略指定模式以外的文件或目录,可以再模式前加上(!)取反
1
2
3
4
5
6
7
8
9
10
11
# 此为注释 – 将被 Git 忽略
# 忽略所有 .a 结尾的文件
*.a
# 但 lib.a 除外
!lib.a
# 仅仅忽略项目根目录下的 TODO 文件,不包括 subdir/TODO
/TODO
# 忽略 build/ 目录下的所有文件
build/
# 会忽略 doc/notes.txt 但不包括 doc/server/arch.txt
doc/*.txt

查看已暂存和未暂存的更新

1
2
git diff
git diff --cached

提交更新

1
2
git commit
git commit -m'docs: change readme' // -m参数后跟提交的方式

git commit 规范参考

跳过使用暂存区域 git commit -a -m'feat: add new tasks'

git 提供了一个跳过使用暂存区域的方式,只要在提交的时候给git commit加上-a选项,git就会自动把所有已经跟踪过的文件暂存起来一并提交,从而跳过 git add步骤

1
git commit -a -m'feat: add new tasks'

移除文件 git rm

从git中移除某个文件,必须要从已跟踪文件清单移除(从暂存区域移除),然后提交,git rm命令完成此项工作,并连带从工作目录中删除制定的文件,这样以后就不会出现在未跟踪文件清单了

1
git rm .test

如果是简单从工作目录中手工删除文件,仅删除了工作目录中的,仍在追踪名单中
然后再运行 git rm记录此次移除文件的操作
最后提交就不会被跟踪了。但是如果删除文件之前修改过且已经放到暂存区,则必须要加-f(即force的首字母)
在实际项目中,经常会遇到下面的情况需要删除文件
想把文件从git仓库中删除,但是仍希望保留在工作目录中,就是仅从跟踪清单中删除,比如一些大型日志或一堆.a的编译文件,不小心纳入仓库,但是要移除追踪但不删除文件,以便稍后在.gitignore文件中补上,加参数--cached

1
git rm --cached readme.txt

后面可以列出文件或者目录的名字,也可以使用 glob 模式。
注意到星号 * 之前的反斜杠 \,因为 Git 有它自己的文件模式扩展匹配方式,

1
2
git rm log/\*.log    // 此命令删除所有 log/ 目录下扩展名为 .log 的文件
git rm \*~ //会递归删除当前目录及其子目录中所有 ~ 结尾的文件。必须加反斜杠

移动文件git mv

1
git mv file_from file_to

相当于运行

1
2
3
mv README.txt README
git rm README.txt
git add README

查看提交历史 git log

1
2
3
git log -p -2 // -p选项展开每次提交的内容差异, -2则仅显示最近两次更新, 做代码审查,或者快速浏览其他写作者提交的更新都做了哪些
git log --stat // 仅显示简要的增改行数
git log --pretty // 指定使用完全不同于默认风格的方式展示提交历史,比如`git log --pretty=oneline`将每个提示放在一行显示,另外还有short, full和fuller

format,可以定制要显示的记录格式,这样的输出便于后期编程提取分析

1
2
3
4
git log --pretty=format:"%h - %an, %ar : %s"
ca82a6d - Scott Chacon, 11 months ago : changed the version number
085bb3b - Scott Chacon, 11 months ago : removed unnecessary test code
a11bef0 - Scott Chacon, 11 months ago : first commit

用 oneline 或 format 时结合 –graph 选项,可以看到开头多出一些 ASCII 字符串表示的简单图形,形象地展示了每个提交所在的分支及其分化衍合情况

1
2
3
4
5
6
7
8
9
10
11
$ git log --pretty=format:"%h %s" --graph
* 2d3acf9 ignore errors from SIGCHLD on trap
* 5e3ee11 Merge branch 'master' of git://github.com/dustin/grit
|\
| * 420eac9 Added a method for getting the current branch.
* | 30e367c timeout code and tests
* | 5a09431 add timeout protection to grit
* | e1193f8 support for heads with slashes in them
|/
* d6016bc require time for xmlschema
* 11d191e Merge branch 'defunkt' into local

列出了常用的格式占位符写法及其代表的意义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
选项 说明
%H 提交对象(commit)的完整哈希字串
%h 提交对象的简短哈希字串
%T 树对象(tree)的完整哈希字串
%t 树对象的简短哈希字串
%P 父对象(parent)的完整哈希字串
%p 父对象的简短哈希字串
%an 作者(author)的名字
%ae 作者的电子邮件地址
%ad 作者修订日期(可以用 -date= 选项定制格式)
%ar 作者修订日期,按多久以前的方式显示
%cn 提交者(committer)的名字
%ce 提交者的电子邮件地址
%cd 提交日期
%cr 提交日期,按多久以前的方式显示
%s 提交说明

一些其他常用的选项及其释义。

1
2
3
4
5
6
7
8
9
10
选项 说明
-p 按补丁格式显示每个更新之间的差异。
--stat 显示每次更新的文件修改统计信息。
--shortstat 只显示 --stat 中最后的行数修改添加移除统计。
--name-only 仅在提交信息后显示已修改的文件清单。
--name-status 显示新增、修改、删除的文件清单。
--abbrev-commit 仅显示 SHA-1 的前几个字符,而非所有的 40 个字符。
--relative-date 使用较短的相对时间显示(比如,“2 weeks ago”)。
--graph 显示 ASCII 图形表示的分支合并历史。
--pretty 使用其他格式显示历史提交信息。可用的选项包括 oneline,short,full,fuller 和 format(后跟指定格式)。

限制输入长度 –since 和 –unti

1
2
3
4
5
6
7
git log --since=2.weeks // 列出所有最近两周内的提交
选项 说明
-(n) 仅显示最近的 n 条提交
--since, --after 仅显示指定时间之后的提交。
--until, --before 仅显示指定时间之前的提交。
--author 仅显示指定作者相关的提交。
--committer 仅显示指定提交者相关的提交。

如果要查看 Git 仓库中,2017 年 10 月期间,Junio Hamano 提交的但未合并的测试脚本(位于项目的 t/ 目录下的文件),可以用下面的查询命令

1
git log --pretty="%h - %s" --author=gitster --since="2008-10-01" \

撤销操作

修改最后一次提交 git commit --amend

1
2
3
git commit -m 'initial commit'
git add forgotten_file
git commit --amend

如果刚才提交时忘了暂存某些修改,可以先补上暂存操作,然后再运行 –amend 提交:上面的三条命令最终只是产生一个提交,第二个提交命令修正了第一个的提交内容。

取消已经暂存的文件 git reset HEAD <file>...

有两个修改过的文件,我们想要分开提交,但不小心用 git add . 全加到了暂存区域。该如何撤消暂存其中的一个文件呢

1
git reset HEAD benchmarks.rb

取消对文件的修改 git checkout -- <file>..

如果觉得刚才对 benchmarks.rb 的修改完全没有必要,该如何取消修改,回到之前的状态(也就是修改之前的版本)呢

1
git checkout -- benchmarks.rb

任何已经提交到 Git 的都可以被恢复。即便在已经删除的分支中的提交,或者用 –amend 重新改写的提交,都可以被恢复(关于数据恢复的内容见第九章)。所以,你可能失去的数据,仅限于没有提交过的

远程仓库的使用

  • 查看当前的远程库 git remove -v,名为 origin 的远程库,Git 默认使用这个名字来标识你所克隆的原始仓库
  • 添加远程仓库git remote add [shortname] [url]
  • 从远程仓库抓取数据git fetch [remote-name]
  • 推送数据到远程仓库 git push [remote-name] [branch-name]
  • 查看某个远程仓库信息git remote show [remote-name]
  • 远程仓库的删除和重命名 git remote rename pb paul git remote rename rm [remote-name]
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    git remove -v
    git remote add pb git://github.com/paulboone/ticgit.git
    git fetch pb //
    git push origin master //把本地的 master 分支推送到 origin 服务器上
    git remote show origin // 要看所克隆的 origin 仓库

    $ git remote rename pb paul
    $ git remote
    origin
    paul

    git remote rm paul
    $ git remote
    origin

git fetch 命令只是将远端的数据拉到本地仓库,并不自动合并到当前工作分支,git pull 命令自动抓取数据下来,然后将远端分支自动合并到本地仓库中当前分支

打标签

  • 列显已有的标签 git tag
  • 新建标签,含附注的标签

3,分支

简单的分支与合并,实际工作中大体的工作流程
1,开发某个网站
2,为了实现某个需求,创建一个分支
3,在这个分支上开展工作;
假设此时,突然有个紧急的问题要修复,那么你可以按照下面的方式处理:
1,返回原先已经发布到生产服务器上的分支;
2,为这次紧急修补创建一个新分支,并在其中修复问题
3,通过测试后,回到生产服务器所在的分支,将修补分支合并进来,然后再推送到生产服务器上
4,切换到之前实现新需求的分支,继续工作

新建分支

  • 新建分支 git branch xx,这条命令是新建了一个分支,但是当前工作分支仍在mater分支上,要想把当前工作分支切换到新建的分支上,要执行git checkout xx

新建并切换到某分支git checkout -b xx相当与下面这两条命令

1
2
git branch xx
git checkout xx

切换分支的时候要确保你当前分支不存在没有提交的修改,否则git会阻止你切换分支

删除分支

  • 删除分支 git branch -d xx

    合并分支

  • 分支的合并,比如master分支要合并xx分支,首先切换到master分支,然后执行git merge xx,此时xx分支已经合并到master分支上,接下来删除xx分支

    1
    2
    3
    git checkout master
    git merge xx
    git branch -d xx
  • 遇到冲突时的合并,如果在不同的分支中都修改看同一个文件,git就无法将两者合并到一起

    1
    2
    3
    4
    5
    6
    7
    <<<<<<< HEAD:index.html
    <div id="footer">contact : email.support@github.com</div>
    =======
    <div id="footer">
    please contact us at support@github.com
    </div>
    >>>>>>> iss53:index.html

=======隔开的上半部分,即HEAD(即master分支,在运行merge命令时所切换到的分支),下半部分是xx分支的内容
你可以二者选其一或者整合两者到一起,然后删除<<<<<<<=======>>>>>>>这些行.解决完所有冲突后,运行git add将他们标记为已解决状态(实际就是将该文件存到暂存区,因为一旦暂存,就表示冲突已经解决)

分支的管理

1
2
3
4
git branch // 列出当前所有分支,注意看master分支前的 * 字符:它表示当前所在的分支
git branch -v // 查看各个分支最后一个提交对象的信息
git branch --merged // 筛选与当前分支合并的分支
git branch --no-merged // 筛选与当前分支尚未合并的分支,此时如果使用`git branch -d xx`删除该分支会提示错误,因为这样会丢失数据,如果你确实想删除该分支上的改动,运行`git branch -D xx`强制删除

利用分支开发的工作流程

长期分支

在master分支中保留完全稳定的代码,即已经发布或即将发布的代码,与此同时,还有一个名为develop或next的平行分支,专门用于后续的开发
或仅用于稳定性测试 — 当然并不是说一定要绝对稳定,不过一旦进入某种稳定状态,便可以把它合并到 master 里。这样,在确保这些已完成的特性分支(短期分支,比如之前的 iss53 分支)能够通过所有测试,并且不会引入更多错误之后,就可以并到主干分支中,等待下一次的发布。
你可以用这招维护不同层次的稳定性。某些大项目还会有个 proposed(建议)或 pu(proposed updates,建议更新)分支,它包含着那些可能还没有成熟到进入 next 或 master 的内容。这么做的目的是拥有不同层次的稳定性:当这些分支进入到更稳定的水平时,再把它们合并到更高层分支中去。

特性分支

一个特性分支是指一个短期的,用来实现单一特性或与其相关工作的分支。

远程分支

远程分支是对远程仓库中分支的索引。
(远程仓库)/(分支名)表示远程分支

推送远程分支

git push (远程仓库名) (分支名)

1
2
3
4
5
6
7
8
 git push origin serverfix
Git 自动把 serverfix 分支名扩展为 refs/heads/serverfix:refs/heads/serverfix,意为“取出我在本地的 serverfix 分支,推送到远程仓库的 serverfix 分支中去”。
仓库中去,仍旧称它为 server
git push origin serverfix:serverfix
上传我本地的 serverfix 分支到远程仓库中去,仍旧称它为 serverfix 分支

git push origin serverfix:awesomebranch
上传我本地的 serverfix 分支到远程仓库中去,称它为 awesomebranch 分支

如果要把该远程分支的内容合并到当前分支,可以运行 git merge origin/serverfix

想要基于远程分支上新建一个新的分支 git checkout -b serverfix origin/serverfix

跟踪远程分支

从远程分支checkout出来的本地分支,成为跟踪分支。跟踪分支是一种和某个远程分支有直接联系的公司;
克隆仓库时,git通常会自动创建一个名为master分支来跟踪origin/master;这正是 git push 和 git pull 一开始就能正常工作的原因

git checkout -b serverfix origin/serverfix 可以用 –track 简化:

git checkout –track origin/serverfix

本地分支设定不同于远程分支的名字,只需在第一个版本的命令里换个名字:
git checkout -b sf origin/serverfix

删除远程分支

git push [远程名] :[分支名]

git push [远程名] [本地分支]:[远程分支],如果省略 [本地分支],如果省略 [本地分支]

分支的衍合

把一个分支整合到另一个分支的办法有两种: merge rebase
使用衍合的目的是想要得到一个能在远程分支上干净应用的补丁,简单的说就是修改提交历史
一旦分支中的提交对象发布到公共仓库,就千万不要对该分支进行衍合操作。

1
2
3
4
5
git rebase --onto master server client
取出 client 分支,找出 client 分支和 server 分支的共同祖先之后的变化,然后把它们在 master 上重演一遍”

git rebase [主分支] [特性分支] 命令会先取出特性分支 server,然后在主分支 master 上重演:
git rebase master server

git