设计哲学

If you’re not distributed, you are not worth using, it’s that simple.
If you perform badly, you are not worth using, it is that simple.
And if you cannot guarantee that the stuff I put into an SCM comes out exactly the same, you are not worth using.
Quite frankly, that pretty much took care of everything out there.

Linus · Torvalds @ Google Talk 2007

  • 分布式(Distribution)

    Git的每一个仓库都是平等的,每一个用户在本地都有一个完整的仓库,可以在离线的情况下完成几乎所有的操作。

  • 性能(Performance)

    Git直接记录快照,而不是差异比较。其它大部分CVS系统以文件变更列表的方式存储信息,它们保存的信息看作是一组基本文件和每个文件随时间逐步累积的差异。Git将数据看作是对小型文件系统的一组快照。在Git下提交更新时它会对当前的全部文件制作一个快照并保存快照索引,如果文件没有修改,Git不会重新存储,只是保留一个链接指向之前存储的文件。

    如果是巨型项目还可以将大项目拆分成小项目,然后大项目仅保存小项目指针,小项目在各自的仓库中进行版本控制。

  • 可靠性(Reliable)

    由于在使用bitkeeper管理kernel的时候一个bitkeeper节点遭遇过黑客入侵,文件内容可能被篡改,所以Linus在设计Git的时候特别在意文件内容的可靠性。Git中所有数据在存储前都使用哈希算法(SHA-1)计算校验和,然后以校验和来引用。这意味着不可能在Git不知情时更改任何文件内容或目录内容。 这个功能建构在Git底层,是构成Git哲学不可或缺的部分。

概念介绍

Git一般有三种状态:

  • 已修改(Modified)
  • 已暂存(Staged)
  • 已提交(Committed)

除以上三种状态外,还有一种已推送状态(Pushed),即将本地的修改推送到远程仓库。

Git项目一般有三个工作区域:

  • 工作目录:项目某个版本对应的内容,通过commit,tag,branch等确定,从Git仓库的压缩数据库中提取出来的文件,展示在磁盘上用于修改。
  • 暂存区域:保存即将提交的文件列表信息的文件,一般在Git仓库目录,也称索引。
  • 本地Git仓库:核心部分,保存项目的元数据和对象数据库,是一个名为.git的文件夹。

除以上的三个工作区域外,为了方便团队开发会维护一个远程仓库(remote repository)。

基本流程

  • 工作目录中修改文件
  • 暂存文件:git add <file>...
  • 提交更新:git commit -m <message>
  • 推送至远程仓库:git push <remote> <branch>

git-basic-0.png

基础命令

获取Git仓库

  • 初始化:git init
  • 克隆:git clone <url>

提交更改

  • 查看文件状态:git status
  • 添加内容到下一次提交:git add <file>...
  • 查看具体文件差异:
    • 未暂存文件:git diff
    • 已暂存文件:git diff --cached / git diff --staged
  • 提交更新:git commit -m <message>
    • git commit -a:跳过git add步骤,自动把所有已经跟踪过的文件暂存起来一并提交。

文件操作

  • 移除文件
    • 从工作目录中手工删除文件:git rm <file>...
    • 从Git仓库中移除,保留在当前目录但不再追踪:git rm --cached <file>...
    • git rm支持glob模式。
  • 移动文件:git mv <old_file> <new_file>

查看历史

  • git log
    • -p:显示每次提交的内容差异
    • --stat:显示每次提交的简略的统计信息
    • --pretty=format:"%h - %an, %ar: %s":自定义显示格式
  • git blame:查看文件每一行的最后变更时间和最后提交作者(甩锅必备
    • -L 12,22:行数限定,第12到22行
    • -C:找出文件中从别的地方复制过来的代码片段的原始出处

撤销操作

  • 附加遗漏的更改到上一次commit中:git commit --amend
  • 取消暂存的文件:git reset HEAD <file>...
  • 撤销对文件的修改:git checkout -- <file>...

远程仓库

  • 查看远程仓库克隆时的别名和URL:git remote -v
  • 添加远程仓库:git remote add <shortname> <url>
  • 拉取远程仓库中的更改:git fetch <shortname>
  • 推送内容到远程仓库:git push <shortname> <branch>
  • 查看远程仓库详细信息:git remote show <shortname>
  • 远程仓库重命名:git remote rename <old_shortname> <new_shrotname>
  • 远程仓库移除:git remote rm <shortname>

标签管理

  • 列出所有标签:git tag / git tag -l ‘v1.8.5*’
  • 创建标签
    • 轻量标签(lightwight):像一个不会改变的分支,只是一个特定的提交引用,一般用作临时标签。
      • 创建:git tag <tag>
    • 附注标签(annotated):存储在Git数据库中的一个完整对象,可以被校验,通常建议用这个。
      • 创建:git tag -a <tag> -m <message>。如:git tag -a v1.4 -m ‘my version 1.4’
  • 查看标签信息与对应的提交信息:git show <tag>。如:git show v1.4
  • 后期打标签:git tag -a <tag> <SHA-1>。如:git tag -a v1.4 -m ‘my version 1.4’
  • 共享标签
    • 默认情况下,git push并不会推送标签到远程仓库,创建完标签后必须显式地推送到远程仓库。 git push <remote> <tag>
    • 如果想要一次性推送很多标签,也可以使用:git push <remote> --tags
  • 删除标签
    • 删除本地:git tag -d <tag>
    • 删除远程仓库:git push <remote> :refs/tags/<tag>
  • 检出标签:git checkout <tag>

分支管理

  • 本地分支
    • 创建:git branch <branch>

    • 切换:git checkout <branch>

    • 创建并切换:git checkout -b <branch>

    • 合并

      • merge方法:git merge <branch>

      • rebase方法:git rebase <branch>

    • 删除:git barnch -d <branch>

    • 查看当前所有分支:git branch

      • 查看每个分支最后一次提交:git branch -v
      • 查看已经合并的分支:git branch --nerged
      • 查看未合并的分支:git branch --no-merged
      • 查看本地所有分支更多信息:git branch -vv
  • 远程分支:
    • 查看远程分支:
      • git ls-remote <remote>
      • git remote show <remote>
    • 推送本地分支到远程仓库:git push <remote> <branch>

    • 删除远程分支:git push origin --delete serverfox

    • 从服务器上拉取本地没有的数据:git fetch <remote> <branch>

      git fetch命令从服务器上抓取本地没有的数据时,它并不会修改工作目录中的内容。 它只会获取数据然后让你自己合并。 然而,有一个命令叫作 git pull 在大多数情况下它的含义是一个git fetch紧接着一个git merge 命令。由于git pull的魔法经常令人困惑所以通常单独显式地使用fetchmerge命令会更好一些。

常用操作

merge和rebase操作

merge合并分支

basic-branch-6‚.png

假如要合并iss53到master,只需要切换到master分支,然后进行merge操作。

$ git checkout master
Switched to branch 'master'
$ git merge iss53
Merge made by the 'recursive' strategy.
index.html |    1 +
1 file changed, 1 insertion(+)

basic-merging-1.png

此时Git会使用两个分支的末端所指的快照(C4 和 C5)以及这两个分支的工作祖先(C2),做一个简单的三方合并。做了一个新的快照并且自动创建一个新的提交指向它。 这个被称作一次合并提交,它的特别之处在于他有不止一个父提交。

basic-merging-2.png

合并完成之后就可以删除iss53分支了。

$ git branch -d iss53
rebase合并分支

basic-rebase-1.png

如果要合并C3C4,可以提取在C4中引入的补丁和修改,然后在C3的基础上应用一次,这种操作在Git中叫rebase。rebase命令将提交到某一分支上的所有修改都移至另一分支上,就好像“重新播放”一样。

$ git checkout experiment
$ git rebase master
First, rewinding head to replay your work on top of it...
Applying: added staged command

它的原理是首先找到这两个分支(即当前分支 experiment、rebase操作的目标基底分支 master)的最近共同祖先 C2,然后对比当前分支相对于该祖先的历次提交,提取相应的修改并存为临时文件,然后将当前分支指向目标基底 C3, 最后以此将之前另存为临时文件的修改依序应用。

basic-rebase-3.png

现在回到 master 分支,进行一次快进合并。

$ git checkout master
$ git merge experiment

basic-rebase-4.png

区别

这两种整合方法的最终结果没有任何区别,但是rebase使得提交历史更加整洁,因为rebase操作的实质是丢弃了一些现有的提交。一般我们这样做的目的是为了确保在向远程分支推送时能保持提交历史的整洁。 在这种情况下,首先在自己的分支里进行开发,当开发完成时先将代码rebase到 origin/master 上,然后再向主项目提交修改。 这样的话,该项目的维护者就不再需要进行整合工作,只需要快进合并便可。

请注意,无论是通过rebase,还是通过merge,整合的最终结果所指向的快照始终是一样的,只不过提交历史不同罢了。 rebase是将一系列提交按照原有次序依次应用到另一分支上,而merge是把最终结果合在一起。

原则

只对尚未推送或分享给别人的本地修改执行变基操作清理历史,从不对已推送至别处的提交执行变基操作。

合并分支时解决冲突

如果在不同分支中,对同一个文件的同一个部分做了不同的修改,Git合并的时候就会产生冲突,此时需要手动来解决冲突。比如合并iss53假如有冲突会有这样的提示:

$ git merge iss53
Auto-merging index.html
CONFLICT (content): Merge conflict in index.html
Automatic merge failed; fix conflicts and then commit the result.

此时Git虽然做了部分合并,但是没有自动创建一个新的合并提交,并且留下了有冲突的文件标记为unmerged等我们自己解决冲突。

$ git status
On branch master
You have unmerged paths.
  (fix conflicts and run "git commit")

Unmerged paths:
  (use "git add <file>..." to mark resolution)
  
    both modified:      index.html

no changes added to commit (use "git add" and/or "git commit -a")

出现冲突的文件会包含一些冲突标记:

<<<<<<< HEAD:index.html
<div id="footer">contact : [email protected]</div>
=======
<div id="footer">
 please contact us at [email protected]
</div>
>>>>>>> iss53:index.html

=======会区分不同修改,需要手动选择保留哪个分支的修改,然后删除这些标记。解决了所有的冲突之后,对每个文件使用git add命令将其暂存,一旦暂存这些原本有冲突的文件,Git就会将它们标记为冲突已解决。在此期间可以使用git status来查看文件状态。全部暂存完之后就可以用git commit来完成合并提交。

stash工具

Git提供了一个储存自己当前工作目录中未提交的更改的工具,叫做stash。比如当工作一段时间之后需要切换到另外一个分支做别的事情,但是又不想提交当前分支做了一半的更改,于是就可以将当前的更改使用git stash命令储存起来,然后去另外一个分支工作,最后切回这个分支,再取出之前储存的做了一半的更改。

  • 储藏修改:git stashgit stash save
  • 列出储藏:git stash list
  • 应用储藏:git stash apply <name>
  • 移除储藏:git stash drop <name>
  • 不要储藏任何通过git add暂存的东西:git stash save --keep-index
  • 储藏任何创建的未跟踪文件:git stash save --include-untrackedgit stash save -u
  • 从储藏创建一个分支:git stash branch <branch>
  • 清理工作目录:git clean

搜索

Git提供了两个有用的工具来快速地从它的数据库中浏览代码和提交。

Git Grep

git grep命令有一些的优点。 第一就是速度非常快,第二是你不仅仅可以可以搜索工作目录,还可以搜索任意的Git树。

  • -n:输出匹配行行号

    $ git grep -n gmtime_r
    
  • --count:包括哪些文件包含匹配以及每个文件包含了多少个匹配

    $ git grep --count gmtime_r
    
  • -p:输出匹配行属于哪个方法或者函数

    $ git grep -p gmtime_r *.c
    
  • --and:组合字符串

Git日志搜索

一般用来查看某一项是什么时候存在或者什么时候引入的。

  • -S:显示新增和删除string字符串的提交

    git log -SZLIB_BUF_MAX --oneline
    
  • -L:展示代码中一行或者一个函数的历史

    git log -L :git_deflate_bound:zlib.c
    

GitWeb服务

Git自带了一个简单的CGI脚本,可以启动一个Server用来查看本地的Git仓库。如果既不想用命令行查看又不想自己部署一套复杂的Web服务,可以使用这个,非常方便。

  • 启动:git instaweb --httpd=webrick --local --port=1234
  • 停止:git instaweb --httpd=webrick --stop

LFS插件

安装
  • MacOS:brew install git-lfs
  • Linux:apt-get install git-lfs
  • Win:下载安装包

然后执行git lfs install开启LFS功能。

命令:

  • git lfs track <file>...:增加追踪文件
  • git lfs track:查看现有追踪模式
  • git lfs ls-files:查看当前跟踪的文件列表
  • git clonegit lfs clone:clone仓库

参考资料