版本控制系统及Git使用小记

版本控制系统(Version Control System,VCS)可分为集中式版本控制系统(CVCS)、分布式版本控制系统(DVCS)。

CVCS有:开源免费的 CSV(稳定性不好)、SVN(目前用得最多的CVCS),收费的 ClearBase(IBM的,安装包比Windows OS还大、运行比蜗牛还慢)等。

DVCS有:Git(最简单、最快、最流行)、BitKeeperMercurialBazaarVss(Microsoft Visual SourceSafe)TFS(Microsoft Team Founddation Server)等。

DVCS与CVCS的区别

DVCS:         CVCS: 

对于DVCS,本地磁盘上有项目的所有完整提交历史(本地仓库);而CVCS则没有,执行提交、查看等命令时都需要从服务端获取。这也是DVCS的一个重要优势,可不联网使用、本地相当于有服务端的完整备份故故障容错性更好(笔者忍受过一离开实验室就没法使用svn的痛苦..)。

 

Git速度快的原因:

Git中保存的是文件各个版本的快照(每个快照都有一个唯一标识,即SHA-1值。例如commit id就是这个值),而不是像很多其他版本管理系统那样保存文件差异;

Git中所有文件操作都是增加数据(创建、修改、删除文件底层都是产生新快照);(版本管理系统的一大作用是保证工作不丢失,故只增不减也可理解;也因此,任何提交过的内容都可找回(只要记得commit id),除非被Git当做垃圾清理掉了)

快照间通过指针相连形成快照引用链,每个快照可有多个前驱、多个后继快照。

基于上述原因:

Git中分支创建、切换等操作非常快(只要创建或移动指针即可,而有些VCS创建分支就是所有文件复制一份,可想这样有多低效);

分支、标签等本质上是某个快照的别名,通常通过这些别名在不同分支或标签间切换。然而,实际上你可以回退到任何地方(只要你知道它的commit id),即使这些地方没有分支或标签名或原先有但后来被删了。

缺点:空间占用比较大,因只增不删故只要提交过的文件都会在仓库中。很明显,这是空间换时间的做法(故不需要的文件尽量别加入到仓库,否则即使你之后删了它,虽然你在当前快照看不到但它实际上仍在Git仓库中,这会导致整个项目空间占用越来越大)。不过,Git也有垃圾回收机制gc,在涉及网络传输或者Git仓库真的体积很大的时候,不仅会清除无用的object,还会把已有的相似object打包压缩

另外,实际使用中发现,Git对于文件重命名或文件在不同目录中的移动都可以自动追踪为从源文件到目的文件的变化(而不会认为是删旧文件和创建全新新文件的过程),即在目的文件上仍会保留该文件的完整历史提交记录(实际上,可在目的文件上通过blame命令查看内容的原始来源)。这点在项目重构、项目结构优化的场景下很好用,因为这些场景下常需要进行文件移动或重命名。不知其他的版本管理工具是否也能做到这样。

 

以下记录一些Git实践的经验或总结。

(关于相关命令的使用,首推荐参阅官方文档:https://git-scm.com/docs/ 。也可通过 在命令后加--helpgit help 命令 查看用法。推荐在有点使用经验后有问题直接查阅官方文档,比看网上以讹传讹的一堆烂文章靠谱..) 

 

1 数据模型

数据模型

Git数据模型如下图所示:

在Git中有四个部分(各种Git操作实际上就是让文件或文件快照在四个部分间流动):

工作区(本地项目的根目录):称为workspaceworking directory,对项目的某个版本独立提取出来的内容。 这些从 Git 仓库的压缩数据库中提取出来的文件,放在磁盘上供你使用或修改。

暂存区(项目根目录的.git文件夹下):称为stage areacache areaindex area,是一个文件,保存了下次将提交的文件列表信息。 有时候也被称作`‘索引’',不过一般说法还是叫暂存区域。

本地仓库(项目根目录的.git文件夹下):称为local repository,是 Git 用来保存项目元数据和对象数据库的地方。这是 Git 中最重要的部分,克隆仓库时就是复制这里的数据。

远程仓库(远程服务器上):称为remote repository,与上类似,只不过数据是存在远程服务器。

 实际使用时大部分操作在工作区、暂存区、本地仓库三者间折腾。

 

文件状态及生命周期

Git中保存的是各个版本的文件快照而不是文件差异

文件状态周期及操作:

从效果上看,在最细粒度上对文件的操作有 追踪、修改、暂存、提交(其中修改不是git操作、其他三种是),及其相应的四种逆操作。

因此,相应地,文件有四种状态:

未追踪(位于工作区):新建的文件为此状态,表示Git尚未追踪该文件、在Git快照中没有该文件。下面都状态都是已追踪的。

已修改(位于工作区):工作区中的文件修改后就是此状态。

未修改/已提交(位于工作区):暂存区的文件提交到本地仓库后变为此状态,表示已追踪但无任何编辑的文件的状态。

已暂存(位于暂存区):表示对一个新建文件或已修改文件的当前版本做了标记,使之包含在下次提交的快照中。

  

文件状态变化的Git操作:

文件各状态间转换的git命令总结:

正操作:修改操作——编辑文件,追踪、暂存操作——git add,提交操作——git commit

反操作:反修改——git checkout,反追踪——git rm --cached,反暂存(且反提交且反追踪)——git reset --mixed,反提交——git reset --soft。四个全反——git reset --hard

注:

图中箭头大多是Git操作,如git rm、git add、git commit等。图中的git reset HEAD xx 实际上就是 git reset --mixed HEAD xx

git reset --mixed 也可将新加的已暂存的文件置为未追踪,图中未画出。

Git的大多数操作就是针对已追踪的文件的(特别是已修改、已暂存状态的文件);对于未追踪文件,除了add操作将之变为已修改外,其他大多数命令都无法奈之何。

这里的“修改”不仅是文件内容的变动,文件权限等的变动也会被git识别为修改操作。

本质:各种复杂的操作实际上都可归为上述的文件状态的转换,故可通过该图来掌握各种操作

 

基本使用流程

基本的 Git 工作流程如下:

在工作目录中修改或添加文件。

暂存文件——将文件的快照放入暂存区域。

提交更新——找到暂存区域的文件,将快照永久性存储到 Git 仓库目录。

 

 

2 基本操作

高级命令和底层命令

由于 Git 最初是一套面向版本控制系统的工具集,而不是一个完整的、用户友好的版本控制系统,所以它还包含了一部分用于完成底层工作的命令。 这些命令被设计成能以 UNIX 命令行的风格连接在一起,抑或借由脚本调用来完成工作。 这部分命令一般被称作“底层(plumbing)”命令,而那些更友好的命令则被称作“高层(porcelain)”命令

底层命令得以让你窥探 Git 内部的工作机制,也有助于说明 Git 是如何完成工作的,以及它为何如此运作。 多数底层命令并不面向最终用户:它们更适合作为新命令和自定义脚本的组成部分。对于使用者来说,通常是用高层命令。

命令列表(from https://git-scm.com/docs):

普通用户日常用到的是 checkout、branch、remote 等大约 30 个诸如此类动词形式的“高层命令”。此文介绍的绝大多数就是高层命令的用法,在后文内部原理一节才会介绍些底层命令。

 

使用总结:

 

 

 

 

命令自动补全

若是使用Base Git,则默认是没有命令补全功能的。为了获得命令自动补全的功能:

1 Git源代码中的 contrib/completion/git-completion.bash 复制到本地

2 并将其路径加入到 你的 ~/.bash_profile 文件中: . /usr/local/etc/git-completion.bash ,并为 git-completion.bash 添加可执行权限

3 保存文件后重新打开Bash窗口,或者执行  source ~/.bash_profile 命令

 

自定义提示符

默认情况下,Git在命令行的提示符格式如下:  G104E19067:CourseDesignServer zhangsan$ git status ,分别为 Host:ProjectName User。可以自定义提示符格式():

1 将Git源码中的 contrib/completion/git-prompt.bash 复制到本地;

2 将本地的该文件路径加入到 .bashrc 文件中: . /usr/local/etc/git-prompt.bash ,并为 git-prompt.bash 添加可执行权限

3 配置自定义格式: export GIT_PS1_SHOWDIRTYSTATE=1 export PS1='\w$(__git_ps1 " (%s)")\$ ' ,可将该内容也加入到 .bashrc 文件。

3 保存文件后重新打开Bash窗口,或者 source .bashrc 

结果示例: ~/software/eclipse-workspace/SenseStudyCourseCenter (dev)$ ,提示符格式变为 当前目录+当前分支名 的格式,且有文件变更时会在分支名后以 加号 等提示。更多的格式或配置可参阅该sh文件里的说明。

 

快捷命令自定义

可通过 Linux alias 将常用的git命令定义成快捷命令。在 ~/.bash_profile 文件(不存在则创建)里添加如下内容:

$ alias -p

alias ga='git add .'
alias gb='git branch'
alias gc='git chekout'
alias gd='git diff'
alias gf='git fetch'
alias gl='git log'
alias gm='git commit'
alias gpl='git pull'
alias gps='git push'alias gs='git status'
alias gsa='git stash apply'
alias gsl='git stash list'
alias gss='git stash save'

实际上,git自身已经提供了大部分alias,可通过 git alias 查看这些alias,如下:

a     => !git add . && git status
aa     => !git add . && git add -u . && git status
ac     => !git add . && git commit
acm     => !git add . && git commit -m
alias     => !git config --list | grep 'alias\.' | sed 's/alias\.\([^=]*\)=\(.*\)/\1\     => \2/' | sort
au     => !git add -u . && git status
c     => commit
ca     => commit --amend
cm     => commit -m
d     => diff
l     => log --graph --all --pretty=format:'%C(yellow)%h%C(cyan)%d%Creset %s %C(white)- %an, %ar%Creset'
lg     => log --color --graph --pretty=format:'%C(bold white)%h%Creset -%C(bold green)%d%Creset %s %C(bold green)(%cr)%Creset %C(bold blue)<%an>%Creset' --abbrev-commit --date=relative
ll     => log --stat --abbrev-commit
llg     => log --color --graph --pretty=format:'%C(bold white)%H %d%Creset%n%s%n%+b%C(bold blue)%an <%ae>%Creset %C(bold green)%cr (%ci)' --abbrev-commit
master     => checkout master
s     => status
spull     => svn rebase
spush     => svn dcommit
View Code

 

 

配置(config)

更多配置相关内容可参阅:https://www.progit.cn/#_git_config,或通过命令 git config -h 查阅:

$ git config -h
usage: git config [<options>]

Config file location
    --global              use global config file
    --system              use system config file
    --local               use repository config file
    --worktree            use per-worktree config file
    -f, --file <file>     use given config file
    --blob <blob-id>      read config from given blob object

Action
    --get                 get value: name [value-regex]
    --get-all             get all values: key [value-regex]
    --get-regexp          get values for regexp: name-regex [value-regex]
    --get-urlmatch        get value specific for the URL: section[.var] URL
    --replace-all         replace all matching variables: name value [value_regex]
    --add                 add a new variable: name value
    --unset               remove a variable: name [value-regex]
    --unset-all           remove all matches: name [value-regex]
    --rename-section      rename section: old-name new-name
    --remove-section      remove a section: name
    -l, --list            list all
    -e, --edit            open an editor
    --get-color           find the color configured: slot [default]
    --get-colorbool       find the color setting: slot [stdout-is-tty]

Type
    -t, --type <>         value is given this type
    --bool                value is "true" or "false"
    --int                 value is decimal number
    --bool-or-int         value is --bool or --int
    --path                value is a path (file or directory name)
    --expiry-date         value is an expiry date

Other
    -z, --null            terminate values with NUL byte
    --name-only           show variable names only
    --includes            respect include directives on lookup
    --show-origin         show origin of config (file, standard input, blob, command line)
    --default <value>     with --get, use default value when missing entry
git config -h

客户端配置

通常进行如下两项配置即可:

 git config --global user.name xx  :配置用户名

 git config --global user.email xxx :配置邮箱

其他配置:

 git config --list :获取生效的所有配置项,结果示例:

credential.helper=osxkeychain
user.name=zhangsan
user.email=zhangsan@test.com
color.ui=auto
credential.helper=cache
core.editor=/usr/bin/vim
core.repositoryformatversion=0
core.filemode=true
core.bare=false
core.logallrefupdates=true
core.ignorecase=true
core.precomposeunicode=true

remote.origin.url=git@gitlab.test.com:Sense/SenseServerCommons.git
remote.origin.fetch=+refs/heads/*:refs/remotes/origin/*
branch.dev.remote=origin
branch.dev.merge=refs/heads/dev
View Code

 git config --global core.editor emacs :配置Git所用的文本编辑器。默认情况下,Git会调用环境变量 $VISUAL 或 $EDITOR 设置的文本编辑器,若未设置则用vi。

 git config --global commit.template file_path :指定文件路径,当commit时,Git会使用该文件内容作为默认的commit msg。

 git config --global core.pager '' :设置Git运行 log、diff 等命令时所用的分页器,默认用 less。也可设置成空串表示关闭分页器,此时命令的所有输出会显示在一页。

 git config --global core.excludesfile file_paths :全局设置不让Git管理的文件。此配置与.gitignore文件的功能类似,只不过前者是全局的而后者是与具体项目相关的。

 以上都是进行全局配置,也可以公共 --system、--local、--worktree 等进行不同有效域的配置。

服务端配置:

 git config --system receive.fsckObjects true :让Git服务器在收到每次推送时都检查每个对象的有效性及SHA-1校验和是否一致,这三个是耗时操作,通常是禁用的。

 git config --system receive.denyNonFastForwards true :禁用 非fast forward的推送,包括禁用 force push。此禁用可通过 删除远程分支并重新上传 来绕过,可结合下面的禁用删除的配置来防止。

 git config --system receive.denyDeletes true :禁用推送 删除分支合标签 的操作。

 

 

初始化仓库(init)

 git init 

 

删除文件(rm)

用git命令删除文件只能删除已被追踪的文件。

 git rm "file.txt" :将文件从工作区及暂存区删除。执行该命令后该操作所造成的修改就自动被 add到暂存区了,故该命令效果类似  rm file.txt && git add file.txt (但并不完全相同,见下面)

特殊情况:

-f 强制删除:若文件被修改过但未提交(即处于已修改或已暂存状态),则默认删不掉,以防误删未提交的数据。确实要删的话可加 -f  参数来强制删除。

--cached:暂存区中删除该文件、工作区中该文件变为未追踪状态。示例如下:

 

 

 

移动文件(mv)

 git mv a b ,该命令与 git rm 命令类似,执行该命令后该操作所做的修改就自动被add到暂存区了,故该命令等价于: mv a b  &&  git add a b 

 

 

暂存/提交修改(add/commit)

 git add "file1.txt" :表示将已修改的文件产生快照到暂存区,以备下一次提交。该命令用于:跟踪未跟踪状态的新文件、把已跟踪的修改了的文件放入暂存区、把合并时有冲突的文件标记为已解决冲突状态等。该命令的内部原理是创建文件快照放在暂存区。

有时候并不想将所有文件add,此时:法1可通过指定文件列表或文件名模式,法2可用交互式提交提交命令 git add -i 

 git commit -m "添加文件1" :表示将暂存区的快照内容提交到本地仓库。提交后会针对该次提交产生一个commit id,用来表示该提交。commit id是个SHA哈希值,如7a7d2d6490ba219db0a959ee19e1a70985a8a87d,很多其他命令可以通过该值来引用该提交(也可只写前若干位,只要该部分值不会对应多个提交即可)。

add 和 commit的两步操作可以一步到位: git commit -a -m "xxx" ,通过commit的 -a 参数,不用执行add操作就可以将所有工作区和暂存区的修改提交到本地仓库。

 

commit时的msg应该是有意义的,有个规范,见:https://www.conventionalcommits.org/en/v1.0.0/

主要语义:

feat: 功能,新增功能或特性。

fix: 修改,修复问题或 bug。

refactor: 重构,对代码进行重构或优化,不涉及功能修改。

style: 样式,对代码风格或格式进行修改,不涉及功能修改。

docs: 文档,修改文档、注释等内容,不涉及代码修改。

test: 测试,增加或修改测试代码。

chore: 杂项,更新构建工具、依赖库版本等非代码相关的内容。

perf: 性能,对代码进行性能优化,例如提高运行速度、减少资源占用等。

revert: 回滚,撤销之前的提交,通常伴随着被撤销提交的 commit id

 

 

 

查看状态(status)

 git status :会列出当前未提交到仓库的各种修改,包括新增的(即未追踪)、编辑过的(即已修改)、已标记暂存的(即已暂存)

 git status -s :状态简览,推荐用此。与不带参数的相比,此命令列出的信息更简洁,示例如下:

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

前面的字符表示文件状态:??标记新加的未被跟踪的文件、M标记修改过但未暂存的文件、A标记暂存的文件、MM标记暂存后又再修改的文件(左右M分别表示暂存、修改)。

查看本地已提交但未推送到远程的commit

 git cherry -v  

 

比较修改(diff)

分别列出每个文件的当前内容与其最近一次提交的差异

 git diff file1 file2 file3 ,工作区和暂存区的比较

 git diff --cached file1 file2 file3 ,暂存区和本地仓库比较。Git 1.6.1及之后的版本还允许用 --staged 参数,与--cached等效。

 git diff commitId1 commitId2 :列出两个提交间的差异

 git diff test..dev :两点语法,列出由test分支最新提交变为dev分支最新提交所需做的改变(注意不是集合上 dev-test 的结果)

 git diff test...dev :三点语法,列出由test、dev分支最近公共提交变为dev分支最新提交所需做的改变。能干净地列出某个分支与其创建时的起点间的区别。

注意这里两点、三点语法的效果与后面git log操作的两点、三点语法的效果的区别。

diff 的内部原理(参阅这个PPT): 

 

 

 

 

储存当前的修改(stash)

git stash list            //查看储存的修改
git stash push/pop   //将修改压入栈顶 或 将储存的修改从栈顶移除并应用到工作区
git stash push -m '注释' 文件名  //将指定文件的修改压入栈顶
git stash save  '注释'        //储存修改,放到储存栈中。不要用-a,其会将.gitignore中列举忽略的文件也储存
git stash apply stash@{id}  //将储存的修改应用到工作区,但不从栈中移除
git stash drop stash@{id}   //将储存的指定修改从栈中移除,不会应用到工作区
git stash clean                   //删除栈中所有储存的修改
git stash branch <branchname> [<stash>] //将指定的储存内容应用到指定的新分支,会基于该stash储存时所基于的分支创建新分支、会移除该stash。慎用此命令!

注:

储存时默认会将 暂存区中未提交的修改(即已暂存)工作区中已追踪的文件修改(即已修改) 储存起来。储存时的选项:

-u 或 --include-untracked:未追踪的文件也储存,默认不会储存未追踪的文件。

-a:不仅未追踪文件,连.gitignore中所列忽略的文件修改也会被储存。很少用此选项。

--keep-index:暂存区中未提交的修改不要储存。

储存的内容可以应用到任意分支,而不要求储存内容的来源分支与应用的目标分支是同一分支。

将储存的内容应用到工作区时,并不要求工作区是干净的。

 

打包修改(patch/bundle) 

使用 git patch 或 git bundle 命令可以将更改打包成文件,可以传输该文件,并将文件应用到其他分支甚至其他项目。

两者的主要区别:

  1. git patch 用于将更改打包成 diff 文件,以便将其应用到其他存储库中。而 git bundle 则打包多个 Git 对象(例如分支、标签和提交等)成二进制文件,并在不同的存储库之间传输和应用这些对象;
  2. git patch 只能应用于某个特定的提交或一组提交,而 git bundle 可以包含一个完整的 Git 存储库,并且可以跨网络或离线环境传输和应用;
  3. git patch 通常用于小规模的更改或修复,而 git bundle 更适用于需要传输大量或整个 Git 存储库的情况。

 

 

查看修改或历史(log/shortlog/reflog/bisect/show/blame)

1. 查看提交历史(log)

查看commit历史: git log ,会列出提交历史。有很多参数用于 筛选提交、展示形式 等的设置。如:

 git log -p -2 :参数-p指示列出展开每次提交所做的修改(相当于git show commitId),-2指定显示最近的两次更新。

 git log --stat :列出每次提交所修改的文件,此命令比较常用,前面 git status -s 的作用与此类似只不过是用于工作区的。例如 git log --stat -1 可列出最后一次提交所修改的文件。

 git log --pretty=oneline :列出提交历史的简要信息,每条仅包含commitId和注释,示例: 8ebc1882fa63e8048a8ad983e9de7fa413f54580 add file test.txt 。也可用 git log --oneline ,只不过结果更简洁,示例: 8ebc1882 add file test.txt 

--pretty指定提交历史的展示格式,值有 oneline、short、full、fuller、format等。

 git log --oneline --branches pom.xml :查看所有分支中哪些提交对pom.xml文件进行了修改。

 git log --pretty=format:"%ad" -2  :列出最细两次提交时间。--format 用于指定自定义格式,其选项可参阅常用选项。Git内置了很多palceholder用来提取commit msg中的信息,主要有:

%H: commit hash
%h: 缩短的 commit hash
%T: tree hash
%t: 缩短的 tree hash
%P: parent hashes
%p: 缩短的 parent hashes
%an: 作者名字
%aN: mailmap 的作者名字 (.mailmap 对应,详情参照git-shortlog(1)或者git-blame(1))
%ae: 作者邮箱
%aE: 作者邮箱 (.mailmap 对应,详情参照git-shortlog(1)或者git-blame(1))
%ad: 日期 (--date= 制定的格式)
%aD: 日期, RFC2822 格式
%ar: 日期, 相对格式 (1 day ago)
%at: 日期, UNIX timestamp
%ai: 日期, ISO 8601 格式
%cn: 提交者名字
%cN: 提交者名字 (.mailmap 对应,详情参照git-shortlog(1)或者git-blame(1))
%ce: 提交者 email
%cE: 提交者 email (.mailmap 对应,详情参照git-shortlog(1)或者git-blame(1))
%cd: 提交日期 (--date= 制定的格式)
%cD: 提交日期, RFC2822 格式
%cr: 提交日期, 相对格式 (1 day ago)
%ct: 提交日期, UNIX timestamp
%ci: 提交日期, ISO 8601 格式
%d: ref 名称
%e: encoding
%s: commit 信息标题
%f: sanitized subject line, suitable for a filename
%b: commit 信息内容
%N: commit notes
%gD: reflog selector, e.g., refs/stash@{1}
%gd: shortened reflog selector, e.g., stash@{1}
%gs: reflog subject
%Cred: 切换到红色
%Cgreen: 切换到绿色
%Cblue: 切换到蓝色
%Creset: 重设颜色
%C(...): 制定颜色, as described in color.branch.* config option
%m: left, right or boundary mark
%n: 换行
%%: a raw %
%x00: print a byte from a hex code
%w([[,[,]]]): switch line wrapping, like the -w option of git-shortlog(1)
View Code

 git log --graph --all --decorate --oneline :查看提交历史graph,会显示 ASCII 图形表示的分支合并历史。结果示例:

筛选历史提交记录

  git log --since = 2.weeks # or 2018-01-03  :根据提交信息搜索提交,此为最近一周内的提交。根据提交信息搜索的还有 --until、--author、--grep等。--all-match用于指定多个搜索条件为与的关系,否则默认为或的关系。

 git log -Sauth :根据修改的文件内容搜索提交,可列出添加或移除了某些字符串的提交,如找出添加或移除了对某一特定函数的引用的提交。

 git log expserver2c/ :搜索指定文件的历史提交

排除筛选

--not语法: git log dev testing staging --not master ,查看在dev或testing或staging但不在master上的提交。--not用于排除,

^ 语法:作用同--not,不过--not要放在最后而这里不必,故更灵活。如上述命令等价于 git log dev ^master testing staging 

区间筛选

二点语法: git log dev..test ,列出在test分支但不在dev分支的commit,即后者commit集减前者commit集。等价于 git log test --not dev 。

这个命令很有用,可以查看即将合并的内容,如查看即将推送到远端的内容: git log origin/master..HEAD 。

两点语法中可以少分支参数,此时参数默认为'HEAD'。下面的三点语法同。

三点语法: git log dev...test ,结果为 (dev ∪ test) - (dev ∩ dev),即两分支各自独有的commit的集合。可加 --left-right 参数,结果会标识提交属于哪个分支。如: git log --oneline --left-right HEAD...MERGE_HEAD 可以查看与合并冲突有关的提交

 综合示例: git log --pretty="%h - %s" --author=gitster --since="2008-10-01" --before="2008-11-01" --no-merges -- t 

 

2. 查看提交简报(shorlog)

 git shortlog [<options>]  :查看提交简报(Summarize 'git log' output),会列出提交者及每个人的提commig msg,可通过参数 --no-merges、--not 等筛选要列出的内容。

 

3. 查看引用日志(reflog)

reflog——引用日志,顾名思义,就是将可变指针的引用历史记录下来。引用日志记录了你的 HEAD 和各分支引用的指向的变更历史。这个记录非常有用。

查看某个引用的所有变更记录(包含commit、reset等所有记录): git reflog $branch_name 。当不指定分支名时默认采用"HEAD",即查看的是HEAD的指向的变更记录。

值得注意的是,引用日志只存在于本地仓库,一个记录了你在你自己的仓库里做过什么的日志。 其他人拷贝的仓库里的引用日志不会和你的相同;而你新克隆一个仓库的时候,引用日志是空的,因为你在仓库里还没有操作。

可查看所有分支的所有操作记录,包括提交、合并、重置、还原操作及已经被删除的commit记录等(git log则不能查看已经删除了的commit记录)。示例:

a53d87ab HEAD@{1}: commit (amend): [add] 课程列表增加是否上架到商店过的字段
2940c47f HEAD@{2}: commit (amend): [add] 课程列表增加是否上架到商店过的字段
edf7bb6f HEAD@{3}: commit: [add] 课程列表增加是否上架到商店过的字段
bc7bde23 HEAD@{4}: commit (amend): [update]审核通过时不默认将课程推到商店
25b56994 (test) HEAD@{5}: checkout: moving from test to dev
25b56994 (test) HEAD@{6}: reset: moving to HEAD
25b56994 (test) HEAD@{7}: reset: moving to HEAD
25b56994 (test) HEAD@{8}: reset: moving to HEAD
25b56994 (test) HEAD@{9}: reset: moving to HEAD
25b56994 (test) HEAD@{10}: checkout: moving from dev to test
View Code

会列出每次操作后HEAD所处的commit的简要信息。若要在列出引用日志的同时列出该commit的详细信息,可用 git log -g 。

结合reflog和reset命令,可以达到 撤销操作、回滚到某个历史状态等 目的,见后文。

 

4. 二分查找commit(bisect)

场景:当前的代码运行有问题,自从上次可正常运行到现在已经引入了数十或上百提交,此时为了定位最早引入哪个commit导致出错的,就可用bisect命令来二分查找。

为用户提供交互式的二分查找命令来查找某个commit,命令列表:

git bisect [help|start|bad|good|new|old|terms|skip|next|reset|visualize|view|replay|log|run]

(base) G104E1901067:CourseDesignServer zhangshaoming1$ git bisect help
usage: git bisect [help|start|bad|good|new|old|terms|skip|next|reset|visualize|view|replay|log|run]

git bisect help
    print this long help message.
git bisect start [--term-{old,good}=<term> --term-{new,bad}=<term>]
         [--no-checkout] [<bad> [<good>...]] [--] [<pathspec>...]
    reset bisect state and start bisection.
git bisect (bad|new) [<rev>]
    mark <rev> a known-bad revision/
        a revision after change in a given property.
git bisect (good|old) [<rev>...]
    mark <rev>... known-good revisions/
        revisions before change in a given property.
git bisect terms [--term-good | --term-bad]
    show the terms used for old and new commits (default: bad, good)
git bisect skip [(<rev>|<range>)...]
    mark <rev>... untestable revisions.
git bisect next
    find next bisection to test and check it out.
git bisect reset [<commit>]
    finish bisection search and go back to commit.
git bisect (visualize|view)
    show bisect status in gitk.
git bisect replay <logfile>
    replay bisection log.
git bisect log
    show bisect log.
git bisect run <cmd>...
    use <cmd>... to automatically bisect.
View Code

基于上述场景,命令使用:

1 初始化搜索范围:

git bisect start           #启用二分查找,会创建一个临时分支并切换到该分支
git bisect bad $comitId1   #告诉Git指定的commit有问题,不指定id则默认为HEAD
git bisect good $comitId2  #告诉Git指定的commit是正常的,要求该good commit是上述bad commit 的祖先节点否则会报错

经上述命令后会 创建临时分支并切过去、列出这些范围内的提交个数、切到指定的提交范围的中点的提交:

Bisecting: 6 revisions left to test after this
[ecb6e1bc347ccecc5f9350d878ce677feb13d3b2] error handling on repo

2 交互式减少搜索范围:

用户在该中点提交上测试代码是否仍有问题,根据测试结果执行 git bisect good 或 git bisect bad 以让Git二分减小搜索范围(这里的"good"、"bad"可定义其他的,只要在start时指定即可,可看上面命令列表中start用法):

$ git bisect good
Bisecting: 3 revisions left to test after this
[b047b02ea83310a70fd603dc8cd7a6cd13d15c04] secure this thing

范围足够少后,Git会告知我们第一个导致代码有问题的提交:

$ git bisect good
b047b02ea83310a70fd603dc8cd7a6cd13d15c04 is first bad commit
commit b047b02ea83310a70fd603dc8cd7a6cd13d15c04
Author: PJ Hyett <pjhyett@example.com>
Date:   Tue Jan 27 14:48:32 2009 -0800

    secure this thing

:040000 040000 40ee3e7821b895e52c1695092db9bdc4c61d1730
f24d3c6ebcfc639b1a3814550e62d60b8e68a8e4 M  config

搜索过程中可通过 git bisect log 查看二分点列表(即被test的提交列表)及每个二分点的"good"或"bad"标记。

3 结束搜索: git bisect reset #结束二分查找,会删除临时分支并切回启用前的分支 

4 自动运行:也可不用让用户参与到每次二分点的"good"、"bad"决策,而是给出脚本,让Git自动在每个被checkout的提交里执行脚本直到找到首个致错提交:

$ git bisect start HEAD v1.0
$ git bisect run test-error.sh

也可以执行 make 或者 make tests 或者其他命令或工具来进行自动化测试。

 

 

5. 查看某次提交的修改(show)

查看某次commit做的修改:  git show ${commit_id}  ,commit id可以只列出前几个字母,只要没有歧义即可。

查看最近2次的修改内容:用前面的git log命令,示例: git lot -p -2  

 

6. 查看文件每行的提交信息/来源(blame)

 git blame simplegit.rb :查看文件每行最近一次的修改者及修改时间等信息。结果示例:

^4832fe2 (Scott Chacon  2008-03-15 10:31:28 -0700 12)  def show(tree = 'master')
^4832fe2 (Scott Chacon  2008-03-15 10:31:28 -0700 13)   command("git show #{tree}")
^4832fe2 (Scott Chacon  2008-03-15 10:31:28 -0700 14)  end
^4832fe2 (Scott Chacon  2008-03-15 10:31:28 -0700 15)
9f6560e4 (Scott Chacon  2008-03-17 21:52:20 -0700 16)  def log(tree = 'master')
79eaf55d (Scott Chacon  2008-04-06 10:15:08 -0700 17)   command("git log #{tree}")
9f6560e4 (Scott Chacon  2008-03-17 21:52:20 -0700 18)  end
9f6560e4 (Scott Chacon  2008-03-17 21:52:20 -0700 19)
42cf2861 (Magnus Chacon 2008-04-13 10:45:01 -0700 20)  def blame(path)
42cf2861 (Magnus Chacon 2008-04-13 10:45:01 -0700 21)   command("git blame #{path}")
42cf2861 (Magnus Chacon 2008-04-13 10:45:01 -0700 22)  end
View Code

每行含义:最近一次修改的 所属commitId、修改者、修改时间、行号、修改后的内容。commitId左边带 ^ 符号的(如^4832fe2)表示该文件 第一次被提交时就有的且之后未被修改 的行

常用选项:

-L:指定要查看的行范围,如 -L 12, 22    -L 12, +22 两者表示的范围分别为 [12, 22]、[12, 12+22]

-C:列出每行的文件来源。如果当前文件中某些内容是从其他文件复制过来的(重构项目或复制代码时常有此情况),Git会列出这些行的原始文件出处(每行信息中多了个字段表示出处)。该功能非常有用。示例:

$ git blame -C -L 141,153 GITPackUpload.m
f344f58d GITServerHandler.m (Scott 2009-01-04 141)
f344f58d GITServerHandler.m (Scott 2009-01-04 142) - (void) gatherObjectShasFromC
f344f58d GITServerHandler.m (Scott 2009-01-04 143) {
70befddd GITServerHandler.m (Scott 2009-03-22 144)         //NSLog(@"GATHER COMMI
ad11ac80 GITPackUpload.m    (Scott 2009-03-24 145)
ad11ac80 GITPackUpload.m    (Scott 2009-03-24 146)         NSString *parentSha;
ad11ac80 GITPackUpload.m    (Scott 2009-03-24 147)         GITCommit *commit = [g
View Code

Eclipse等IDE中有插件可以看到每行的修改者等信息,背后实际上应该就是借助于git的此命令来实现的。示例:

实际使用中发现,Git对于文件重命名或文件在不同目录中的移动都可以自动追踪到从源文件到目的文件的变化(而不会认为是删旧文件创建全新新文件),即在目的文件上仍保留完整的历史提交记录。这点在项目重构、项目结构优化的场景下很好用,因为这些场景下场需要进行文件移动或重命名。

实际上,Git 不会显式地记录文件的重命名操作。 它会记录快照,然后在事后尝试计算出重命名的动作。示例:你在工作区将一个文件重命名后执行git status时会提示你删掉了一个文件并新增了一个untracked文件,当你通过git add把变更提交到暂存区后再执行git status会发现Git提示你将文件重命名了而非删旧增新。

不论是用户的复制操作、还是Git推断出的重命名操作,都可通过 git blame 命令找出文件中从别的地方复制过来的代码片段的原始出处,即使该代码是在其他目录其他名字的文件里的。通常来说,你会认为复制代码到的目标提交是最原始的提交,因为那是你第一次在这个文件中修改了这几行。 但 Git 会告诉你,你第一次写这几行代码的那个提交才是原始提交,即使这是在另外一个文件里写的。

 

标签(tag)

有两种标签:附注标签(annotated tag)、轻量标签(lightweight tag)

 

创建标签:git tag [-a] <tagname>  [<commit> | <object>] ,通过指定commitId可以对指定的提交创建标签,未指定commit时默认为HEAD。

附注标签: git tag -a tag_name -m 'your tag msg' ,如 git tag -a v1.4 -m 'my version 1.4' ,-a表明这是个annotated tag。

轻量标签: git tag tag_name 

创建标签实际上内部就是创建了个commit。

查看标签列表: git tag [-l 'search pattern'] ,如 git tag 或 git tag -l "611*" 

查看标签信息: git tag show tag_name 

删除标签: git tag -d tag_name 

标签同步到远程仓库(提交代码修改并不会将创建的标签也提交): git push origin tag_name 、 git push origin --tags 分别为同步一个、同步所有。

切换到某标签的版本上: git checkout -b new_branch_name tag_name ,会基于指定的标签创建一个新分支。

 

搜索文件内容(grep)

与shell的grep命令类似,支持强大的搜索功能。不同的是可以指定分支、速度更快等。

 

 git grep -n authUtil* dev :在dev分支所有文件中搜索包含能部分匹配"authUtil*"正则的行。也可以指定从某个tag的代码中搜索(分支名换成标签名),甚至可以是指定的文件如 *.java。

会输出 分支、文件名、行号、内容,结果示例:

(base) G104E190:ss1Server test1$ git grep -n authUtil dev
dev:expserver-common/src/main/java/com/ss/ss1/expserver_common/web/impl/DefaulCommonAccountStorageImpl.java:75:   private ExpserverAuthUtil<?> authUtil;
dev:expserver-common/src/main/java/com/ss/ss1/expserver_common/web/impl/DefaulCommonAccountStorageImpl.java:89:           String userId = authUtil.getUserIdFromRequest(request);
dev:expserver-common/src/main/java/com/ss/ss1/expserver_common/web/impl/DefaulCommonAccountStorageImpl.java:437:          String userId = authUtil.getUserIdFromRequest(request);
dev:expserver-common/src/main/java/com/ss/ss1/expserver_common/web/impl/DefaulCommonAccountStorageImpl.java:579:          String userId = authUtil.getUserIdFromRequest(request);
dev:expserver-common/src/main/java/com/ss/ss1/expserver_common/web/impl/DefaulCommonAccountStorageImpl.java:590:          String userId = authUtil.getUserIdFromRequest(request);
dev:expserver-common/src/main/java/com/ss/ss1/expserver_common/web/impl/DefaultCommonCourseImpl.java:23:  ExpserverAuthUtil<?> authUtil;
dev:expserver-common/src/main/java/com/ss/ss1/expserver_common/web/impl/DefaultCommonCourseImpl.java:34://                        String userId = authUtil.getUserIdFromRequest(request);
View Code

主要选项:

-n 输出行号

--count 不输出匹配的内容而是统计每个文件中匹配到多少行。

更多选项可参阅命令文档: git help grep 

 

生成构建号(describe)

 git describe dev :生成一个字符串,它由 最近的附注标签名、自该标签之后的提交数目、所指定的commit的 SHA-1 值构成,如 1.6.2-rc1-20-g8c5b85c 。

只有存在附注标签时才能生成该字符串

 

生成压缩包(archive)

 git archive dev --prefix='marchon/' --format=zip > `git describe dev`.zip :会将指定分支的文件生成压缩包。

--prefix:指定前缀,分支根目录下(仅限第一层)的所有文件或文件夹名都会加该前缀,若前缀带斜杠,则相当于指定了个父文件夹

--format:指定压缩格式,不指定则默认为.tar.gz格式。

 

 

3 分支管理

分支实际上只是个对快照的引用。

 

主机(Host)

主机是指远程仓库的地址,本地仓库可以对应一个或多个远程仓库,因此可以有多个主机。具体可参阅后面 git remote 一节。

默认的远程主机名为origin、默认的分支名为master。这里的'默认'只是默认名,与自己创建的相比并没有特殊的地方。

 

本地仓库中的分支类型

本地分支:码农维护的分支。如master、dev等。

远程跟踪分支:git维护的分支。是远程分支状态的引用,名字格式为  远程主机名/分支名 ,如 origin/master、origin/dev 等。也可加 remotes 或 refs/remotes 前缀,如 origin/dev、remotes/origin/dev、refs/remotes/origin/dev,是等价的。

远程跟踪分支不是远程分支, 它们是你不能移动的本地引用,当你做任何涉及到网络通信操作的git命令(如 git fetch)时,它们会被git自动更新。 远程跟踪分支像是你上次连接到远程仓库时,那些分支所处状态的书签。

 

 

分支操作

分支操作的内部原理:Git中存的是文件快照,各个提交形成快照引用链,故创建分支、切换分支等只需创建一个指针或修改指针指向即可,速度非常快。

基本分支操作

创建分支: git branch 新分支名 [源分支名] [源分支commit终点]  

分支重命名: git branch -m oldBranchName newBranchName 

删除本地分支: git branch -d 分支名 

指定本地分支与远程分支的关联:  git branch --set-upstream master origin/next  指定master分支追踪origin next分支

删除远程分支: git branch --delete origin branch_name 或 git push origin :branch_name 或 git push --delete origin branch_name ,会将远程分支删除,origin为远程主机名。虽然删除了,但Git 服务器通常会保留数据一段时间直到垃圾回收运行,故在此期间还是可恢复的。

切换分支: git checkout 分支名 ,或   git checkout -b 新分支名 [源分支名] [原分支commit终点] ,后者为从指定源分支(默认为master)创建并切换分支。

查看分支(假设远程和本地库中都只有master分支,远程主机在本地被取名为origin):

    • 查看本地: git branch ,得到master
    • 查看远程分支: git branch -r ,得到origin/master
    • 查看本地和远程的所有分支: git branch -a ,得到master和remotes/origin/master两条记录。远程具有哪些分支可能在动态变化,在本地有涉及到网络的操作时会自动更新远程分支信息到本地。
    • 查看各分支最后一次提交的信息: git branch -v 
      • 且列出所跟踪的远程分支: git branch -vv 
    • 查看哪些分支 已经/尚未 合并到当前分支: git branch --merged / git branch --no-merged 

取回远程分支

 git fetch 远程主机名 [分支名] ,如git fetch origin master,取回远程分支信息并更新本地的远程跟踪分支。

若没有指定分支名则取回所有分支的更新;取回分支只是将远程主机版本库的更新取回,对本地分支没影响;所取回的更新,在本地主机上要用"远程主机名/分支名"的形式读取

取回远程分支前后的状态示例:

取回前:,   取回后:

  

合并分支

示意图:_  _

为便于表述,这里定义几个概念:

bc:branch current,当前所处的分支

bm:branch merged,要被合入到当前分支的分支

lcbc:last commit of branch current,当前分支的最后一次提交,如图中的C3(br为master时)

lcbm:last commit of branch merged,被合并分支的最后一次提交,如图中的C4(bm为experiment时)

lcc:last common commit of two branchs,两分支的最近的一次公共父提交,即分支的最近公共父节点,如图中的C2

三种合并方式

1.  git merge [--no-ff] 被合并分支名 :将bm合并到bc。(关于创建与合并分支的原理,可参阅 创建与合并分支-廖雪峰Git分支管理策略-阮一峰分支合并-腾讯技术工程

(1)recursive策略——若两分支的commit是分叉的,则会进行三方合并。即基于lcbc、lcbm、lcc三个提交进行合并、并以两分支最后一个commit作为祖先来创建新的commit节点,新commit的message通常自动为"Merge branch 'xx1' into xx2"。若有冲突则此时还需要解决冲突并提交。为何要三方合并?因为对一个文件的同时修改仅凭lcbc、lcbm无法确定以谁的为准,有了lcc后就能确定。

示例:如将B分支(a1、b1、b2)合入A分支(a1、a2)后A用 git log 查看将变成(a1、[a2、b1、b2]、c1),这里中间的三个commit并不一定是这个顺序而是会按它们的先后时间排序

节首图中在master分支执行 git merge experiment 的结果示例:

有时两分支的lcc是不唯一的,这时会先以这两个lcc作为待合并节点去寻找这两lcc的lcc执行合并,依次递归,这也是"recursive"合并策略命名的来源。示例如下,要对节点5、6合并,由于它们lcc为节点2、3不唯一,故先合并 2、3及其lcc 1 得到临时节点作为节点5、6的lcc、再将临时节点及5、6合并得到内容为C的节点7,如图:

(2)fast foward策略——若两分支的commit不存在分叉(即bc的提交链是bm的提交链的“前缀”),则合并时会直接把指针前移(称为Fast forward,即快进式合并)。显然这是上述情况的特例,但可通过 --no-ff  参数指定不要进行Fast forward,即合并时强制生成新commit节点。

示例:如将B分支(a1、b1、b2)合入A分支(a1)时A将也变成(a1、b1、b2)

(3)squash 策略。create a single commit instead of doing a merge: git merge --squash feature-1 ,带--squash参数的用于合并多个commit为一个新commit。

会将 从最近公共祖先之后 被合并分支上有而当前分支没有 的所有修改应用到当前分支,并暂存到暂存区(但不会提交),之后需要手动提交,提交时默认的commit msg为"Squashed commit of the following:"+各被合并的commig的msg。可见,只是将修改应用到当前分支成为一个新的commit。

2. git rebase 被合并的分支名  :也用于合并分支,其将bc从lcc之后新增的提交所对应的修改在bm上replay一遍(产生新的提交并丢弃bc原有的提交),因此不同于有分叉的merge操作,rebase操作后提交历史不会有分叉而是一条直线,效果是就像一直在bc上操作一样。注意这里是”重放“,重放后的修改虽然一样,但他们是不同的commit,可参阅下面示例,C4和C4'虽然效果一样但是是不同提交。'rebase'或'变基'的名字就是来源于此。rebase操作的实质是丢弃一些现有的提交,然后相应地新建一些内容一样但实际上不同的提交。 

节首图中在experiment执行 git rebase master 的结果示意图:

更多用法: git rebase -i [start-commit] [end-commit] # 区间左开右闭、默认end-commit为HEAD ,可详细筛选、修改要合并的commit。

 git rebase与git merge的区别在于

  区别:

是否会产生新commit:后者合并时会产生一个merge commit(commit message形如“Merge branch 'dev' of xxx into local_dev”)而前者不会。

commit是否有序及是否是一条线:合并后commit的组织顺序不同——假设基于分支A的分支A1、A2上分别有3、4个新commit,现在A1要将A2的修改合并进来,则通过后者进行合并后7个新commit会被按照时间排序,而通过前者执行合并后7个commit不会整体按时间排序,而是要么A1的3个commit在前要么A2的4个commit在前,取决于其最早的一个commit谁早,也因此rebase后当前分支的commit历史会被打乱

rebase的适用场景:通常用于为保持干净的、“线性”(没分叉)提交记录的合并,这要求合并的源、目的两者共同修改少,否则合并时会有很多冲突,且由于历史提交会被打乱,导致冲突很难解决。这个前提在项目中较少能满足,故实际使用少。

具体而言,用得多的场景是对 有尚未更新到远程仓库的提交的本地分支 执行基于远程分支的rebase操作。如:在同一分支上,本地分支将远程分支同步到本地,可以使用rebase;基于dev分支拉dev-feature分支并在新分支上开发新feature,这期间定期在dev-feature执行git rebase dev。

rebase的使用原则:一旦当前分支中的commit发布到公共仓库,就千万不要对该分支进行rebase操作,因为别人可能拉取了你rebase前的分支,其某些commit和你rebase后的分支对应commit的效果虽然一样但commitId不同了。只对尚未推送或分享给别人的本地修改执行变基操作,从不对已推送至别处的提交执行变基操作。

总而言之,当commit差异较少时用rebase、反之则用merge。如果担心出错,则总是用merge就好。

更多可参阅:https://www.progit.cn/#_rebasing

3.  git cherry-pick commitId1 commitId2 :会将指定commit(不管是哪个分支的)的修改应用到当前分支。注意并不会把commit合并进来,而是只将修改应用到当前分支(即replay),并产生新的commit。可以指定多个commit,甚至可以指定commit区间。

 

拓展:merge、cherry-pick、rebase 的内部原理(见这个PPT

2-way diff、3-way diff 原理:见diff一节。

3-way merge 原理:先 3-way diff 然后 merge。

merge、cherry-pick、rebase 原理:(由下可见最终都是转为 3-way merge)

 ,

 

分支合并冲突处理,几个有用的选项或命令:(可参阅 高级合并

冲突内容查看:

1  git diff --ours file_path (--ours、--theirs、--base):在出现合并冲突时查看 我方的修改(ours)、他方的修改(theirs)、公共父节点文件内容(base)。列出发生冲突的文件的两个版本内容:git diff

2  git checkout --conflict=diff3 file_path :在出现合并冲突时指定冲突时的标记,可通过  git config --global merge.conflictstyle diff3  使配置全局生效。默认是两个分支的名字或最新commit id,指定为此值后会变为"ours"、"theirs"、且文件里包含base内容。示例:

$ git checkout --conflict=diff3 version.info

$ cat version.info 
<<<<<<< ours
a1
||||||| base
=======
a2
>>>>>>> theirs
View Code

3  git log --oneline --left-right HEAD...MERGE_HEAD  或  git log --oneline --left-right --merge :列出与冲突有关的commit。会分别列出并标记两个分支各自的提交,区别在于后者仅列出与冲突文件有关的提交。示例:

$ git log --oneline --left-right --merge
< 694971d update phrase to hola world
> c3ffff1 changed text to hello mundo
View Code

通过 -p 参数还可进一步列出冲突文件的内容区别。

冲突文件内容解决:

1  git checkout --ours version.info (--ours、--theirs):在出现合并冲突时冲突的部分采用 我方 或 他方 的修改。

2 正常处理:当然,正常情况下不能忽视别人的修改、一个文件里可能有多处冲突且并不总是采用我方的,所以得手动去修改文件后 add、commit。

冲突合并策略:

1  git merge --ablort :取消合并。若因某些原因发现自己处在一个混乱状态然后只是想重新合并,则可运行 git reset --hart HEAD 来清除工作区内容并回到合并前的状态。

2  git merge -Xignore-space-change xxx (-Xignore-all-space 或 -Xignore-space-change ):合并时忽略空白修改。如两个对同一行的修改一个是增加一空格一个是增加一行代码则合并时会忽略前者的修改。如:因为IDE代码格式化导致空白修改,可用此参数来忽略。

3  git merge -Xours xxx (-Xours、-Xtheirs):对于合并冲突的文件采取指定的一边的分支的代码,而不是在文件中做冲突标记。注意对于出现合并冲突的文件才会起作用,假设分支A、B分别对文件f.txt同一行做不同修改且各自提交了:接下来以-Xours将A合入B、接着以同样方式将B合入A,结果:由于前者会有冲突故合并后该行是B的内容、后者由于没有冲突故-Xours不起作用故合并后该行也是B的内容。

记住并自动复用分支冲突处理方案 Rerere(reuse recorded resolution):会记住文件冲突时用户的解决方案,当再次出现同样冲突时自动解决。

(详情参阅:https://www.progit.cn/#_rerere

1   git config --global rerere.enabled true :启用Rerere,这是全局配置,有该配置后,冲突的解决会被记录下来以便以后再遇到时自动复用。实际情况中,若总是启用Rerere,则可能会导致一些冲突没被开发者发现,故最好按需启用该功能,且记得不用时关掉此配置。

2   git rerere diff :查看自动解决冲突的结果。与 git dff 命令结果类似,会列出文件冲突内容,不同的是还会列出自动解决后的内容,示例:

 1 $ git rerere diff
 2 --- a/hello.rb
 3 +++ b/hello.rb
 4 @@ -1,11 +1,7 @@
 5  #! /usr/bin/env ruby
 6 
 7  def hello
 8 -<<<<<<<
 9 -  puts 'hello mundo'
10 -=======
11 -  puts 'hola world'
12 ->>>>>>>
13 +  puts 'hola mundo'
14  end
View Code

该示例中显示,删除了两个文件冲突的那行、并增加了一行,增加的一行是自动根据用户之前的解决过程来应用的。

3  git checkout --conflict=merge file_path :文件恢复到未自动解决冲突时的状态。

4  git rerere :对于尚未解决的文件冲突,用rerere自动解决之。通常会自动解决,但也有例外(如上个命令)。

 注意:Rerere是在文件出现合并冲突时才会起作用

分支合并策略

recursive,默认的合并策略,前面介绍的 merge(recursive、fast-forward、squash)、rebase、cherry-pick 操作都是此策略下的。

ours recursive, git merge -s ours dev ,通过-s ours指定策略,在合并时只保留当前分支的代码而忽略被合并分支的任何代码。注意它与上面“-Xours” 合并选项不是一回事,区别在于这里对于无冲突的文件修改也会做处理。

octopus,一次合并多个分支。上述策略默认只能将一个分支合并到当前分支,即一次只能对两个分支进行合并,若有很多分支的修改需要合并在一起则依次合并会导致大量合并节点产生,此时可用octopus,如 git merte b1 b2 b3 。

resolve

subtree

其他分支操作

合并其他仓库分支到当前仓库分支并保存两分支各自的提交记录

场景:初期产品p1、p2后端由于很多东西共用,故将他们放在一个java maven项目A下,分别属于不同的module。后来随着产品变大变多,需要将他们分别拆成单独项目。这里假设p1仍在A上开发(例如删除与p2有关的东西并自己飞奔前进了),p2将脱离A单独在新项目B中开发。

解决:

可能的方法(错误):一种方法是初始化B项目后将p2在A中的代码都复制到B并合并提交,此法的问题是这样导致p2在A中的所有历史提交记录都丢失了,故不可取

正确方法:在B中将A的remote host加入到B这样B就可拉取A的某个分支,然后执行带参数的合并操作。在B执行如下命令:

1 git remote add originA xxx.git # originA为自己取的A的origin名字、末尾为A的仓库地址 
2 git fetch originA devA:devA #在B上将A的某个分支拉取到B
3 git checkout devB #切换到B自身的某个分支
4 git merge devA --allow-unrelated-histories #将A的分支合并到B的该分支

 执行完这些命令后,B中在merge A之前的commit(如 init commit等)仍会在提交历史中。若不希望保留这些commit(如B是刚init的项目,只有init commit等与p2无关的commit),则可这样做:在merge后将B hard reset到p2的最后一次提交。

 

合并其他仓库分支到当前分支的子目录

参阅:https://www.progit.cn/#_git_reset

类似于上面,将其他仓库的分支B拉取到本仓库后,通过命令 git read-tree --prefix=thirdParty/ -u rack_branch 将该分支内容加入到当前分支a的某个子目录。之后子目录就可以追踪并合并B的更改(在B执行git pull后,在A执行git merge B)。

查看子目录与被引入目标分支的差异,不是diff命令,而是diff-tree: git diff-tree -p rack_branch 

此合并方式相当于把目标分支作为当前分支的一个子模块。使用场景,如:将所有项目的代码迁移提交到一个地方。

这个操作的原理实际上是在操作Git内部三种数据类型中的tree object,具体可参阅后文“内部原理”一节。

 

 

经验总结

多同步远程仓库上的提交到本地,并合并到当前分支,以防止两者差异太大、从而减少合并冲突。

尽量保持线性(没有分叉)的提交历史:在本地修改未推送到远程仓库的前提下,使用rebase来合并远程别人的提交,如git pull --rebase 、git rebase等

  

4 撤销/回退/替换/找回

从应用的角度介绍几个“反悔”操作。更多内容可参阅:http://www.ruanyifeng.com/blog/2019/12/git-undo.html

4.1 丢弃commit(reset)

reset命令原理:

(可参阅:https://www.progit.cn/#_git_reset )

Git中分支名(如dev、master)实际是个指针,指向某个提交。Git中还有个名为HEAD的特殊指针,指向某个分支。如HEAD -> dev -> 38eb946,为便表述,明确下概念:

“HEADE的指向”意为HEAD指向的分支(这里HEAD称为二级指针或符号引用),此即dev。git checkout branchName 命令移动的即此。可通过 git symboolic-ref HEAD 内部命令查看HEAD指向哪个分支。

“HEAD分支的指向”意为HEAD指向的分支所指向的提交,此即38eb946。git reset 命令移动的即此。

HEAD的作用是记录"下一次commit的父节点":Git总是以HEAD分支的指向作为当前分支的最新提交,即表示本地仓库中该分支的最新提交;当有新提交时总会以HEAD分支的指向作为前驱节点来添加新节点、然后移动HEAD分支指向新提交(可见HEAD效果相当于链表中的current指针者)。因此可以通过更改HEAD 分支的指向 来达到重置提交的目的,这就是reset命令

从执行过程看,运行reset命令时,会尝试执行如下三个步骤(有三种选项:--soft、--mixed、--hard):

1 移动 HEAD 分支的指向(若指定了 --soft,则到此停止)

2 使暂存区内容看起来像 HEAD 分支指向的内容(若指定 --mixed 或三种都未指定,则到此停止)

3 使工作目录内容看起来像暂存区内容(若指定 --hard,则到此停止)

从执行效果上看,移动HEAD分支的指向会导致增加(往新移)或减少(往旧移)一些commit,根据移动后 这些commit的修改 是否 相应地应用到暂存区和工作区,有三种重置效果:(soft、mixed、hard的作用是分别累积撤销了本地仓库、暂存区、工作区的修改,根据三个区是否撤销得到三种情形)

1 Y、N、N,暂存区不改、工作区不改: git reset --soft $cmtId  软重置。此时执行git status会提示"Changes to be committed"。

2 Y、Y、N,暂存区改、工作区不改: git reset --mixed [$cmtId] [file]  ,混合重置,reset不带参数时默认为此。此时执行git status会提示"Changes not staged for commit"。取消 将某文件暂存 的操作的命令  git reset HEAD xx 实际上就是这里的混合重置。

3 Y、Y、Y,暂存区改、工作区改: git reset --hard $cmtId  ,硬重置。此时执行git status会发现是干净的,没有要暂存和提交的东西。

这三种重置操作都可以用来撤销提交,区别在于将commit撤销后暂存区或工作区是否也同步撤销而已。另外,与add命令一样,可以加--patch选项来选择重置哪些内容。 

下面给个示例,假设目前工作区、暂存区、HEAD的状态如图1所示,针对目前状态分别执行三种reset后的结果如图所示:

图1:, 图2:

 图3:,  图4:

可见,“撤销提交到暂存区的修改”可通过第二种重置来完成,命令为: git reset HEAD xxx  ,执行git status时提示用的(use "git reset HEAD <file>..." to unstage)正是此命令。

 

reset命令说明:

 git reset --hard commit_id :硬重置,回退到指定的commit,该commit之后的所有修改和commit均会丢失。

  • 这里commit_id可以不全写,但要确保在仓库中唯一,Git会根据已写的自动查找;
  • 除了用commit_id外,也可用特殊标记:Git用 HEAD 表示当前分支的最新版本、 HEAD^ 表示上版本、 HEAD^^ 表示上上版本、 HEAD~100 表示往上100个版本以此类推
  • 回退后,HEAD的指向也成为当前的最新版,可能造成往历史版本回滚后滚不回真正的最新版,如对于版本号为1到10的十个版本,回滚到版本5后HEAD就是版本5了此时滚不回10。解决:可以通过git reflog查看版本10提交时的commit_id从而滚到最新版。
  • 通过reflot和reset操作可达到撤销合并操作的目的

 git reset --mixed commit_id [file]  :混合重置,回退到指定commit,暂存区也回退,但工作区不回退。--mixed可省略,如 git reset HEAD 、 git reset HEAD pom.xml 

  git reset --soft commit_id  :软重置,回退到指定的commit,该commit之后的所有提交丢失、但修改会保留在工作区和暂存区。此功能很有用,意味着回退后可以修改提交过的修改并重新提交。 

 

4.2 撤销commit(revert)

最好别用此命令!

1 revert只有单亲的commit: git revert [倒数第一个提交] [倒数第二个提交] ,撤销最新、次新、... 的提交。

这里指定的提交列表必须是由最新到旧的且中间不能跳过某些提交。执行后会产生对应个数的新提交,每个新提交都是将对应的原提交所做编辑撤销。

如假设当前提交历史为Ca <- Cb <- Cc <- Cd,则执行 git revert Cd Cc 得到Ca <- Cb <- Cc <- Cd <- Cd' <- Cc' 

2 revert具有双亲的commit:对于分支no fast forward合并产生的节点,由于有两个父节点,上述命令不work,因为不知要撤销哪个分支的提交。须通过-m 参数指定rever后要保留哪个父节点:值1为合并分支者、2为被合并分支者。示例:  git revert -m 1 HEAD 。示例(假设是master merge topic):

原理:revert操作实际上是通过将 要撤销的提交  被提交前时的HEAD文件快照(对于这提到的三次revert其分别为Cc、Cb、C6)替换被提交后的文件(Cd、Cd'、M)来产生新提交的(Cd'、Cc'、^M),也即对M的文件进行了修改。revert后^M与C6(或Cd'与Cc、Cc'与Cb)文件内容完全一样,但被撤销的提交历史仍然被保留。从效果及本质上看,相当于在M(或Cd或Cd')上做删除操作使得得到快照内容与C6(或Cc或Cb)的一样的提交^M(或Cd'或Cc')。

^M处与M处的区别:提交历史上前者包含后者的所有提交历史;但前者没有C3、C4所对应的文件修改,而后者有。因此,在有双亲节点的rever场景下,会有问题:revert后,topic分支再做修改并合入master时C3、C4的修改并不会被合入(因为前面revert M时相当于基于它们做修改产生^M,即基于M的内容删除了C3、C4的修改),这很容易让人迷惑且容易造成后续的工作出错,因此,应该慎用甚至别用revert。当然,也有解决方法,即对 ^M 执行revert得到^^M,^^M与^M相抵消,因此此时^^M与M的相比,前者不仅包含后者的提交历史,而且两者文件内容是完全一样的,基于^^M合并topic时会把C3、C4的修改合入。示例:

 

 reset与revert命令的区别:

从原理上看:reset本质是通过分支指向或文件状态标记的改变来实现回滚,不会产生新快照;而revert操作本质是通过修改文件内容来实现回滚,会产生新的快照。

从效果上看:都能达到撤销操作的目的。但revert会有上述 "revert后做同样的内容修改无法被合入的问题",而reset则不会,正是上述原理的区别才会导致这种问题。

 

 

4.3 重写/替换历史commit

这里的替换commit是指 取消commit、重新编辑后重新提交。替换前后commit id会变化,因为commit id是基于文件内容计算的hash,文件内容改变了,显然就不可能让commit id仍不变。

替换最近一次commit(amend)

 git commit --amend 

用来修改最近一次的提交,实际上不仅可以修改提交信息,还可以整个把上一次提交替换掉。

对于最近一次已经commit但未push到远程仓库的提交,可以直接修改其代码并在执行git add命令后通过命令  git commit --amend   来覆盖该commit;显然,如果代码没变化则此时相当于修改最近一次的message。详情参阅:https://stackoverflow.com/questions/179123/how-to-modify-existing-unpushed-commits 

修改指定某次的历史提交:https://xiewenbo.iteye.com/blog/1285693

 

替换前后的commit的id是不一样的。

 

用一个commit替换最近连续的若干个commit(soft reset)

通过上面说的软重置来实现。如下所示,先回退到某个commit,然后修改代码,最后提交。

git reset --soft $commitId  # 回退到指定commit,该commit之后的commit将移除且相应的修改放入工作区

edit file ...
git add . git com
-m "xxx"

 

历史commit删除/修改/替换/合并/拆分/改序(rebase -i)

rebase -i 命令很强大,可以对历史commit进行删除、修改、替换、多个合为一个、一个拆分为多个、更改先后顺序 等操作。前文的两个替换操作实际上都可由 rebase -i 命令完成。

关于该命令的使用,推荐参阅:https://www.progit.cn/#_rewriting_history

 

 git rebase -i HEAD~3 :回退到指定commit并弹出交互式环境让用户选择对其之后的每个commit作何操作,可以 保持不变、修改commit msg、修改内容、合并commit、丢弃commit等,很灵活。

内部原理:执行上述命令后将首先弹出交互式环境让用户选择对每个commit做什么操作,由之产生to do清单;保存该清单后会将分支回滚到指定commit(上面是HEAD~3),然后将操作清单中的依个应用到当前分支。

操作清单示例:

pick f7f3f6d changed my name a bit
pick 310154e updated README formatting and added blame
pick a5f4a0d added cat-file

如果删掉最后一个步骤,则相当于最后一个commit不会redo,从而相当于将该commit从提交历史中移除;如果调换步骤的顺序则相当于修改commit顺序。

 

修改大量/全部历史提交(filter-branch)

场景:从某个提交开始不小心把真实密码写在配置文件里,则要修改该次提交之后的所有提交来删掉密码;全局修改你的邮箱地址;从每一个提交中移除一个文件 等。filter-branch可以用来修改整个提交历史

note:由于修改commit会使得原commit的id发生改变,因此如果修改已经发布到远程仓库则应慎用此命令,否则会影响已经拉取了这些commit的其他人。

命令示例: git filter-branch --tree-filter 'rm -f passwords.txt' HEAD ,遍历当前分支的每个提交,将指定文件从该提交中删除后重新提交替换原提交。

--all 选项:使得在所有分支上都执行该命令

更多可参阅:https://www.progit.cn/#_rewriting_history

 

其他分支的commit的修改应用到当前分支(cherry-pick/rebase/merge)

法1:基于cherry-pick

在当前分支上执行: git cherry-pick $commitid1 $commitid2  ,即可将指定的commit的修改应用到到当前分支,并在当前分支上产生一个新commit,若出现冲突则解决之,然后 git cherry-pick --coutinue ,若没有冲突则会自动合并进来并自动commit。

注意,这里合入到当前分支后产生的commit id与原分支的commit id不同,因此题中说“修改应用到当前分支”而非“commit合并到当前分支”。

选项:

 -n :不自动commit

 -e :不采用被pick的commit的message,而是由操作者输入

更多可参阅:https://git-scm.com/docs/git-cherry-pick

 

法2:基于rebase

 git rebase dev :效果相当于基于dev分支创建一个临时分支,然后将当前分支与临时分支最近共同祖先提交以来的当前分支的改动应用到临时分支,然后临时分支的commit完全替换到当前分支的(把当前分支的指针指向该临时分支最新的commit即可)。与cherry-pick一样,是“应用”,故rebase后“应用”的commit的id变了,相当于是“虽效果一样但是是不同的提交”。

 

由于会修改提交的commit,故对于已经推送到远程仓库的commit,最好不要使用这两个命令去修改该commit,否则修改并提交历史中就会存在一个变更的两次commit从而令人困惑。当然,如果你真的修改了该commit而又不想在历史中出现一个修改产生两个commit的现象,在别人未同步过该远程分支的前提下可通过git push -f来覆盖远程服务器上的版本(不推荐此不负责任的做法)。

 

法3:基于merge

 git merge dev --squash feature-1 ,具体用法见前面分支合并merge 一节。

 

4.4 撤销添加到暂存区的修改(reset HEAD)

 git reset HEAD [filename] 

工作区文件的变化在执行git add命令后就被提交到了暂存区,通过该命令可以将指定文件的修改从暂存区撤销,回到工作区(可见相当于add命令的相反命令)。

由于暂存区的文件可能是 创建文件 或 修改文件 后执行git add产生的,故执行上述命令后文件可能为 未追踪或已修改状态。

实际上,在执行git add命令后,执行git status的结果中就有提示可通过此命令撤回add操作。示例: Changes to be committed: (use "git reset HEAD <file>..." to unstage) 

 

此命令实际上就是 git reset --mixed HEAD [filename] ,因为reset命令不写选项参数时默认即为 --mixed 。

此命令与git rm --cached的区别:

前者表示撤销暂存区中待提交的修改,若暂存区中无待提交修改,则该命令result in nothing;

而后者是无论暂存区中是否有待提交修改,都会从暂存区中删除文件、将工作区中文件改为未追踪状态。

 

 

4.5 撤销工作区的文件修改(checkout)

 git checkout -- [filename] 

撤销工作区中还未被add到暂存区(从而当然也就尚未被commit)的指定文件的修改,通过 git checkout .  可以撤销所有文件的修改。

这是个危险的命令,因为工作区修改了但未暂定的文件一旦被checkout,就无法找回 

 

4.6 撤销添加的未追踪的文件(clean)

 git clean :移除工作区中所有未追踪的文件。当然,也可以通过OS的删除命令来删除文件。

 

4.7 撤销历史动作(如合并操作)

假设当前在test分支且执行完了merge dev操作。此时想撤销合并操作。两步:

通过 git reflog 命令 找到合并之前的状态。

18db1460 (HEAD -> test, dev) HEAD@{0}: merge dev: Fast-forward
629b67c7 (origin/dev-2b-vo-dto-refine, origin/dev, dev-2b-vo-dto-refine) HEAD@{1}: checkout: moving from dev-2b-vo-dto-refine to test
629b67c7 (origin/dev-2b-vo-dto-refine, origin/dev, dev-2b-vo-dto-refine) HEAD@{2}: checkout: moving from test to dev-2b-vo-dto-refine
18db1460 (HEAD -> test, dev) HEAD@{3}: reset: moving to HEAD@{2}
d44ca9db HEAD@{4}: reset: moving to HEAD@{17}
7098c1d9 (origin/dev_before_VO_DAO_refine, dev_before_VO_DAO_refine) HEAD@{5}: reset: moving to HEAD@{1}
18db1460 (HEAD -> test, dev) HEAD@{6}: merge dev: Fast-forward
7098c1d9 (origin/dev_before_VO_DAO_refine, dev_before_VO_DAO_refine) HEAD@{7}: checkout: moving from dev_before_VO_DAO_refine to test
7098c1d9 (origin/dev_before_VO_DAO_refine, dev_before_VO_DAO_refine) HEAD@{8}: checkout: moving from test to dev_before_VO_DAO_refine
83ddd758 HEAD@{9}: reset: moving to HEAD
83ddd758 HEAD@{10}: commit: test
View Code

这里合并前为 HEAD@{1} 

通过 git reset HEAD@{1}  返回到合并之前的状态。

同理,此法也可以回滚其他操作,如提交操作。

 

此法的原理:其实就是在移动HEAD分支的指向来实现回滚操作。如对于上述示例,就是将HEAD分支的指向改为HEAD@{1}的值,即 629b67c7 。

 

4.8 找回历史提交(reflog、fsck)

为了回退到某个历史提交,需要先找到该提交所在的commit id。方法有:

1 借助引用日志(reflog):

通过 git reflog 命令可以查看历史上每次操作后所在的commit。得到该commit后就可以根据commit id来回滚(git reset等)到该commit。

 

2 查看Git 对象状态(fsck):

引用日志记录的条数是有限的,不会保留所有历史记录。故对于未被任何commit引用且在引用日志中没有相关记录(如从 .git/logs/ 中将相关的引用变更记录删除)的commit,reflog就无能为力了。这种commit就是一种“悬挂对象(dangling object)”,例如 执行rebase操作前本地的新commit在执行rebase后就成为了这种对象

此时一种方法是借助 git fsck --full 命令来列出未被其他任何对象引用的commit对象(即dangling object),从其中找到要恢复的commit对象并借助reset命令来恢复。

结果示例:

$ git fsck --full 
Checking object directories: 100% (256/256), done.
Checking objects: 100% (22/22), done.
dangling commit 9e88e3ad390082600b9add47e61928c440914e2e
dangling tree 3bcc389559d5d6b4a7539fe54b2be5ae3e257ea2
dangling commit 4bbc8e8ecad3ed35f95895d65616eefcc042f7c3
dangling blob d6aa6e7e23ac71dcec06168a894210cc144fc9b0
dangling commit d89990f53b4c63cde5796b06fa44cf34fcd692d7

原理(可先参阅后文内部原理一节):fsck 执行时会通过对象间的相互引用关系来遍历所有 Git 对象,并找出 reachable、unreachable(无法通过HEAD、分支、标签、stash出发而引用到的对象)、dangling(比unreachable更严格——在引用DAG中无入度的节点,但可以有出度) 等状态的对象。

关于三种状态对象的说明如下(可参阅:https://stackoverflow.com/questions/36621730/git-fsck-how-dangling-vs-unreachable-vs-lost-found-differ):

     C--D--E      <-- branch-a
    /
A--B--F---G--H    <-- branch-b
    \    /
     I--J--K--L   

//上图的commit节点中,K、L为 unreachable 者,L为 danling 者,除此外其他节点都是 reachable 者。

 

 

5 远程仓库交互

基本知识:

git中有四个部分:工作区(本地项目的根目录)、暂存区(项目根目录的.git文件夹下)、本地仓库(项目根目录的.git文件夹下)、远程仓库。

Git和其他版本控制系统如SVN的一个不同之处就是有暂存区(即stage或cache或index)的概念。

 

相关命令:

git clone

git fetch

git pull

git push

git remote

 

git clone 

 git clone <版本库的网址> [<本地目录名>] ,从远程主机克隆一个版本库。

  • 该命令会在本地主机生成一个目录,不指定名称的话与远程主机的版本库同名;
  • Git要求每个远程主机都必须指定一个主机名,默认为origin,可以通过-o参数指定,如 git clone -o jQuery https://github.com/jquery/jquery.git ,此外可以通过 -b 指定克隆版本库的指定分支。
  • git clone还支持HTTP(s)、FTP、SSH、file等协议,如本地不同位置的clone git clone file:///opt/git/project.git 、 git clone /Users/marchon/idea-workspace/SenseStudyServer ./ssserver 

git fetch

 git fetch 用法见上面的分支管理部分。

git pull

 git pull <远程主机名> <远程分支名>[:<本地分支名>] :从指定远程主机的某个分支拉取更新并merge到指定的本地分支,如 git pull origin next:master 将origin next分支更新拉取并合并到本地master分支。

  • 若未指定本地分支时默认为当前分支,如 git pull origin next 取回origin next分支并与当前分支合并,相当于 git fetch origin next、git merge origin/next两步操作。
  • 若当前分支与远程分支存在追踪关系,git pull就可以省略远程分支名,如git pull origin
    • 在git clone时所有本地分支默认与远程主机的同名分支建立追踪关系,也即本地的master分支自动"追踪"origin master分支。
    • Git也允许手动建立追踪关系,如 git branch --set-upstream master origin/next 指定master分支追踪origin next分支。
  • 若当前分支只有一个追踪分支,连远程主机名都可以省略,如 git pull

 git pull --rebase <远程主机名> <远程分支名>[:<本地分支名>] :用法与上面类似,只不过加--rebase参数,表示通过rebase命令而非通过merge命令合并。如 git pull --rebase origin next:master 等价于将origin next分支更新拉取并通过rebase合并到本地master分支。

 

merge:git pull (等价于 git fetch & git merge)

rebase:git pull --rebase(等价于 git fetch & git rebase)

 

git push

 git push <远程主机名> <本地分支名>:<远程分支名> :将本地分支的更新,推送到远程主机,如 git push origin master:next 将本地master分支推送到远程origin主机的next分支上。

  • 省略远程分支名表示将本地分支推送到与之存在"追踪关系"的远程分支(通常两者同名),如果该远程分支不存在,则会被新建。如 git push origin master 将本地的master分支推送到origin主机的master分支。
  • 省略本地分支名表示删除指定的远程分支,因为这等同于推送一个空的本地分支到远程分支。如 git push origin :master 等同于 git push origin --delete master ,表示删除origin主机的master分支。
  • 若当前分支与远程分支之间存在追踪关系,则本地分支和远程分支都可以省略。如 git push origin 将当前分支推送到origin主机的对应分支。
  • 若当前分支只有一个追踪分支,那么主机名都可以省略。如 git push
  • 若当前分支与多个主机存在追踪关系,则可使用-u选项指定一个默认主机,如 git push -u origin master 将本地的master分支推送到origin主机的master分支,同时指定origin为默认主机并将两分支关联起来,在以后的推送或者拉取时就可以不加任何参数使用git push了。 

 

git remote

默认的远程主机名为origin。

  git remote  :管理远程主机名

  • 不带参数时,列出所有远程主机
  • -v参数,列出所有远程主机及网址
  •  git remote add <主机名> <网址>  ,添加远程主机
  •  git remote rm <主机名>  ,删除远程主机
  •  git remote rename <主机名>  ,重命名远程主机
  •  git remote show <主机名> :查看远程仓库信息,会列出远程仓库的分支、与本地分支的对应关系等,示例:
    (base) G104E1:DesignServer zhangsan$ git remote show origin 
    * remote origin
      Fetch URL: git@gitlab.sz.xx.com:SenseStudyCreators/CourseDesignServer.git
      Push  URL: git@gitlab.sz.xx.com:SenseStudyCreators/CourseDesignServer.git
      HEAD branch: master
      Remote branches:
        dev                                                           tracked
        dev_2c                                                        tracked
        dev_2c_raw                                                    tracked
        dev_from_sensestudyserver-dev                                 tracked
        master                                                        tracked
        refs/remotes/origin/dev_2c_multi_login_with_same_account      stale (use 'git remote prune' to remove)
        refs/remotes/origin/dev_2c_raw__multi_login_with_same_account stale (use 'git remote prune' to remove)
        refs/remotes/origin/dev_2c_raw_multi_login_with_same_account  stale (use 'git remote prune' to remove)
        refs/remotes/origin/dev_multi_login_with_same_account         stale (use 'git remote prune' to remove)
        refs/remotes/origin/tmp                                       stale (use 'git remote prune' to remove)
      Local branches configured for 'git pull':
        dev                           merges with remote dev
        dev_2c                        merges with remote dev_2c
        dev_2c_raw                    merges with remote dev_2c_raw
        dev_from_sensestudyserver-dev merges with remote dev_from_sensestudyserver-dev
        master                        merges with remote master
      Local refs configured for 'git push':
        dev                           pushes to dev                           (up to date)
        dev_2c                        pushes to dev_2c                        (up to date)
        dev_2c_raw                    pushes to dev_2c_raw                    (up to date)
        dev_from_sensestudyserver-dev pushes to dev_from_sensestudyserver-dev (up to date)
        master                        pushes to master                        (up to date)
    View Code

     

当执行 git remote add 时,内部就是在 .git/confiig 添加一条记录,用以表明执行fetch时将哪些 远程仓库的分支 拉取到 本地的远程追踪分支。该记录默认值示例如下:

1 [remote "origin"]
2     url = git@gitlab.test.com:SSCreators/CourseDesignServer.git
3     fetch = +refs/heads/*:refs/remotes/origin/*
4 [remote "SenseStudyServer"]
5     url = git@gitlab.test.com:SS/SSServer.git
6     fetch = +refs/heads/*:refs/remotes/SenseStudyServer/*

+ 号告诉 Git 即使在不能fast-forward的情况下也要(强制)更新引用;

可通过fetch指定远程分支与本地追踪分支的对应关系以免执行fetch时拉取所有分支更新到本地;也可通过push指定本地分支与远程追踪分支的对应关系,从而实现执行 git push origin 时默认把本地某分支推送到远程指定分支的效果(这也是push、fetch、pull等命令有时可简化的内部原理)。两者的示例如下:

[remote "origin"]
    url = https://github.com/schacon/simplegit-progit
    fetch = +refs/heads/master:refs/remotes/origin/master
    fetch = +refs/heads/qa/*:refs/remotes/origin/qa/*
push = refs/heads/prod:refs/heads/qa/prod

从上面例子也可看出,分支名支持命名空间前缀,如上面的本地 prod 分支对应远程的 qa/prod 分支。

  

 

仓库关联

创建新仓库 / 将已有未被管理的项目加入到某个仓库 / 将已有已被管理的项目加入到另一个仓库:

 

 

 

6 工作流/开发流程

Git 作为一个源码管理系统,不可避免涉及到多人协作。

协作必须有一个规范的工作流程,让大家有效地合作,使得项目井井有条地发展下去。"工作流程"在英语里,叫做"workflow"或者"flow",原意是水流,比喻项目像水流那样,顺畅、自然地向前流动,不会发生冲击、对撞、甚至漩涡。

工作流中的分支类型

开发分支(developer)、稳定分支(master),开发分支前的功能分支(feature)、开发和稳定分支间的提测分支(testing)、稳定分支后的补丁分支(hotfix)。

三种工作流

广泛使用的工作流有三种:Git flowGithub flowGitlab flow(详情可参阅这篇文章),分别为面向“版本发布”(周期性迭代发新版)的工作流模式、面向“持续发布”(随时发新版)的工作流模式、两者的结合,它们分别有2、1、3个稳定分支。企业内部开发通常用第三种,开源项目协作则用第二种。

它们有一个共同点:都采用"功能驱动式开发"(Feature-driven development,简称FDD)。它指的是,需求是开发的起点,先有需求再有功能分支(feature branch)或者补丁分支(hotfix branch)。完成开发后,该分支就合并到主分支,然后被删除。

6.1 Git Flow

面向“版本发布”(周期性迭代发新版)的工作流模式。

其只有开发分支(developer branch)和稳定分支(master branch)两个永久分支,其他诸如功能分支(feature branch)、提测分支(release branch)、补丁分支(hotfix branch)都是临时分支,被合入永久分支后即被删除。

说明:

developer分支:开发分支,最新的日常开发代码,不稳定。

feature分支:需求开发分支,基于开发分支拉取得到。

小需求直接在devloper分支提交;

大需求则需新建feature分支开发(例如笔者的SenseStudyServer项目拆分改造),开发过程中不断将developer分支代码合并进来以免未来合回developer时两者差异过大,当feature分支阶段性开发完(例如完成大需求里的某个子需求,开发者觉得没问题了)后可合回developer,直到所有需求开发完也就都合回developer分支了,当然也可以都开发完后才一次性合回。合回时若改动较多则用merge request,全合回后删除此分支。

release分支:提测分支,需求开发完并提交或合并到开发分支后,基于开发分支拉取得到。测试人员基于该分支代码进行测试,发现bug时提给开发人员解决,开发人员直接在该分支解决,并在测试人员验证通过后合回developer分支,当然也可全部测试通过后再一次性合回。提测通过后发布到稳定分支,并删除此分支。

master分支:稳定分支,最新的稳定版代码。基于测试通过后的提测分支拉取得到。每个稳定版本都会打tag。

hotfix分支:补丁分支,某稳定版代码发现bug时,基于该稳定版代码拉取得到。解决bug后合回两个永久分支。(提测分支已没了,怎么在合并前验证bug是否解决呢???)

流程图:

说明

      • 大需求作为feature拉新feature分支进行。新feature分支在执行commit后,定期将dev merge进来,以减少未来并入dev时的冲突。新feature分支并不是完全开发完才能合入dev:开发过程中完成部分功能时也可合入dev,然后继续在原feature分支继续开发。
      • 最好是先本地合入dev再从dev合回feature分支,以产生类似"merge featurexx into dev"而非"merge dev into featurexx"这种令人迷惑的信息。
      • 需求做完拉release分支提测,提测发现的bug在release改并提交,确认bug修改好后将release并回dev。

更多可参阅:https://www.progit.cn/#_%E5%88%86%E6%94%AF%E5%BC%80%E5%8F%91%E5%B7%A5%E4%BD%9C%E6%B5%81 

 

6.2 Github Flow

面向“持续发布”(随时发新版)的工作流模式。github.com 使用此工作流。

其只有一个永久分支(master branch),作为开发分支也作为稳定分支。

开发分支体现在:有新需求时直接基于该分支拉临时分支(不区分是功能分支还是补丁分支,没有提测分支)并在临时分支修改代码,开发者自测通过(自测、单元测试通过等)后向永久分支发合入请求(PR,Pull Request),然后由收到通知的其他人评审代码确定是否接受合入,合入后临时分支删除。当然,对于每次大的修改完成后的版本也可以打tag。

稳定分支体现在:与Git Flow相比可看出没有提测分支,故没有专门的测试人员对代码进行测试,而是需要开发者自测保证代码没问题并由评审者监督确认。也因此,这模式下源码里的单元测试就很必须很重要了。

此模式的特点就在于:只有一个永久分支、需要开发者自测代码协作者评审代码而没有专门的测试分支测试人员测试环境。

此模式很简洁,适合于不属于同一个组织的团体或个人间的协作,典型的是开源项目的协作。作为对比,Git Flow模式则适合于管理【隶属于同一组织下的团队或个人对产品的持续迭代】。企业内部通常要求代码经其他人评审后方可合入,可见这实际上相当于上述两者的某种程度上的奇怪结合,有点畸类——团队内用基于版本发布的工作流模式但在开发过程中又要求遵循持续发布的工作流模式。

 

6.3 Gitlab Flow

Gitlab flow 是 Git flow 与 Github flow 的综合。它吸取了两者的优点,既有适应不同开发环境的弹性,又有单一主分支的简单和便利。它是 Gitlab.com 推荐的做法。企业内部通常用的是这种工作流

Gitlab flow 的最大原则叫做"上游优先"(upsteam first),即只存在一个主分支,它是所有其他分支的"上游"。只有上游分支采纳的代码变化,才能应用到其他分支。

这种模式与前述的 Git Flow 很像,区别是提测分支变成固定的永久分支了,故共有三个永久分支:开发分支(developer branch)、提测分支(testing branch)、稳定分支(master branch),通常基于这三个分支建立开发环境、测试环境、稳定环境三套环境。

当用于“持续发布”场景时,这里的开发分支相当于Github Flow中的稳定分支,因此,一切修改(新需求、修bug等)都在基于该开发分支建立的新临时分支进行、并通过PR合并回该开发分支。(相当于后两分支没用了???)

当用于“版本发布”场景时:对于每个发布的稳定版本,会建立专门的该版本分支(如2.2.0-RELEASE)且稳定环境基于版本分支构建,当然也可不建立多个版本分支而是直接在稳定分支上打tag来标记多个稳定版本。

 

 

实践经验

笔者工作中用到的是面向“版本发布”的Gitlab Flow,该模式适合定期迭代发版的场景。在该场景下还有一些额外需求:要求有个环境较实时地更新最新的feature、且要求该环境的代码几乎不能出现bug。对于这种场景,可以在上述模式的基础上,增加个staging分支作为提测分支,使用:

1 staging分支是提测分支,其基于dev分支,且定期将已经开发好完整新feature的dev分支合入staging分支;

2 基于staging分支部署两套环境:一套是会根据staging分支自动更新的供测试人员测试用的testing环境、另一套是不会根据staging分支自动更新的用于demo的staging的环境。

开发人员在dev上开发了个完整feature(小或大均可)且自测通过后将dev合入staging分支,此时testing环境自动更新,测试人员可在该环境上测试新功能,反馈bug给开发人员修改;

测试人员持续在testing环境测试(自动化测试等),测试通过后可按需(如demo展示)手动更新到staging环境;

当要发新版本时,直接基于staging分支的代码拉稳定分支release,测试人员在该分支上测试。

该流程的优点有:

发版时测试省时、bug少:测试人员平时即可在test环境持续地进行测试,故:当要发版时大多数feature已经事先测过了,故回归下即可;事先测过故release分支bug少。

满足新feature的demo需求:基于staging分支的staging环境按需更新——testing环境测试过的feature才手动更新到staging环境,故该环境能够较稳定地展示最新feature。

上面所述的release分支相当于是master分支,由于release有多个,所以相当于项目有多个master分支,这样就能够满足不同版本的持续迭代优化。 如release2.7相当于2.7版本的master分支。

 

7 其他

.gitignore

有些文件我们不希望被git管理,如项目的日志文件、编译时的临时文件、项目导入到IDE时IDE参数的配置相关的文件等。可以在项目根目录创建名为.igtignore文件,在其中列出要忽略的内容即可。

示例:

# 此为注释 – 将被 Git 忽略
# 忽略所有 .a 结尾的文件
*.a
# 但 lib.a 除外
!lib.a
# 仅仅忽略项目根目录下的 TODO 文件,不包括 subdir/TODO
/TODO
# 忽略 build/ 目录下的所有文件
build/
# 会忽略 doc/notes.txt 但不包括 doc/server/arch.txt
doc/*.txt
# 忽略 doc/ 目录下所有扩展名为 txt 的文件
doc/**/*.txt
.gitignore语法示例
/**/target/
/target/
!.mvn/wrapper/maven-wrapper.jar

### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache

### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/build/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/

### DS_Store ###
.DS_Store


.vscode
/bin/
.gitignore示例

GitHub上有各种语言项目的.gitignore模板,见:https://github.com/github/gitignore

注:

.gitignore文件配置的是针对当前项目忽略的内容,如果要针对所有项目都生效,可借助全局配置项 core.excludesfile  来实现,详情可参阅前面的“配置”一节的内容。

 

.gitattributes

(参考自:https://www.progit.cn/#_git_%E5%B1%9E%E6%80%A7https://git-scm.com/docs/gitattributes

Git允许我们针对特定的路径配置某些设置项,这样 Git 就只对特定的子目录或子文件集运用它们, 这些基于路径的设置项被称为 Git 属性(attributes)。通过使用属性,你可以对项目中的文件或目录单独定义不同的合并策略,让 Git 知道怎样比较非文本文件,或者让 Git 在提交或检出前过滤内容。

What:Git的 gitattributes 文件是一个文本文件,文件中的一行定义一个路径或文件的若干个属性。语法格式: 要匹配的文件模式 属性1 属性2 ... 

Where:局部设置——可以在你的目录下的 .gitattributes 文件内进行设置(通常是你的项目的根目录,也可以是其他目录)。如果不想让这些属性文件被提交,你也可以在 .git/info/attributes 文件中进行设置。

How

以下举几个有用的例子。

识别二进制文件

在 .gitattributes 文件里加上 *.o binary ,这样对于所有 .o 文件Git不会尝试转换或修正回车换行问题、当用git show或git diff命令时Git也不会比较或打印该文件的变化。

diff 二进制文件

原理:通过 .gitattributes 告诉Git怎么把二进制文件转为文本格式从而而能够用普通的diff方式进行对比。示例:

支持word文件比较的配置:

1  git config diff.word.textconv docx2txt 

2  echo '*.docx diff=word' >> .gitattributes 

3 下载 docx2txt(http://docx2txt.sourceforge.net/) 、安装、在可执行路径下创建名为docx2txt的文件脚本以将输出结果包装成Git支持的格式,文件内容: #!/bin/bash docx2txt.pl $1 - 

也可以借助Pandoc软件完成(经实践检验可行):安装好Pandoc(https://github.com/jgm/pandoc/blob/master/INSTALL.md)后进行如下配置即可:

1 在Git安装目录下(也可以在某个Git项目目录下,此时不是全局有效),找到.gitconfig文件并在该文件末尾添加以下内容:

[diff "pandoc"]
textconv=pandoc --to=markdown
prompt = false
[alias]
wdiff = diff --word-diff=color --unified=1
View Code

2 在Git项目根目录下,新建一个.gitattributes文件,并写入: *.docx diff=pandoc 

支持图片文件比较的配置:

1  git config diff.exif.textconv exiftool 

2  echo '*.png diff=exif' >> .gitattributes 

3 下载并安装 exiftool 程序,该程序会提取图片文件的EXIF信息——大部分图片格式中农都有记录的一种元数据,基于这些信息进行图片比较(文件大小、图片尺寸、类型等)。

内容过滤(filter)

Git .attributes提供了filter功能,使我们可以对名字符合指定pattern的文件或路径进行处理(如提取或替换内容等)。一个filter包含"clean"、"smudge"两个功能,分别对应“文件暂存前”、“文件检出前”的处理。如下两图展示了针对 *.txt 文件的filter处理过程:

clean:  , smudge:

需要注意的是,因为 .gitattributes 文件会随着项目一起提交,而过滤器不会(除非该文件也放到项目里被Git track),所以过滤器有可能会失效。 当你在设计这些过滤器时,要注重容错性——它们在出错时应该能优雅地退出,从而不至于影响项目的正常运行。

这里贴个示例,其功能是 在每次checkout时将项目的最后一次的提交信息写入到项目的某个文件中。假设当前在项目根目录:

1 创建 version_info/version.info 文件,内容为空;

2 设置fiter:创建 version_info/.gitattributes 文件并添加如下内容:  version.info filter=myVersion ,表示对名为"version.info"的文件执行名为"myVersion"的filter;

3 设置filter的clean、smudge功能:创建 versin_info/init_config.sh 脚本,脚本功能为在执行Git检出操作时执行指定的version.sh。内容:

#! /usr/bin/env bash

set -euo pipefail

cur_script_base_dir=$(cd "$(dirname "$0")" && pwd)
callback_script_path="${cur_script_base_dir}/version.sh"

# git config filter.myVersion.clean $callback_script_path
git cnofig filter.myVersiion.smuge $callback_script_path
View Code

4 创建名为 versiono_info/version.sh 的脚本,脚本功能为获取最后一次提交的并写入到"version.info"文件中。内容:

#! /usr/bin/env bash
set -euo pipefail

cur_scrip_base_dir=$(cd "$(dirname "$0")" && pwd)
out_file_name="${cur_scrip_base_dir}/version.info"
info="`git log '--pretty=format:%h(%ai) %cn' -1`"

echo $info > $out_file_name
View Code

经过上述配置后,每次执行Git检出操作时,Git会自动执行指定的fiter,其效果是往 versiono_info/version.info写入最近一次提交的信息,如: a9c89113(2020-11-10' 22:29:40 '+0800)' zhangsan 。由于把 .gitattributes 文件放在了项目中会被Git track,故别人拉取了此更新后也会具有上述功能(当然第一次时要执行 init_cnfig.sh 以让过滤器生效)。

 

归档时的文件忽略与信息提取

通过 git archive 命令可以进行项目归档。通过在 .gitattributes 文件增加配置可以实现增强的归档功能:

 test/ export-ignore ,该配置表示通过 git archive 创建项目压缩包时指定的目录不包括在压缩包中;

 version.info export-subst :该配置表示在执行 git archive 命令时根据 version.info 中配置的格式提取信息并替换该文件中原有内容。Git 中提供了很多内建的变量,可以在文件中直接使用这些变量。

示例:假设该文件原有内容为 Last Commit: format:%h(%ai) %cn ,在执行archive命令后内容变为: Last Commit: 1946768d(2020-11-11 00:40:51 +0800) zhangsan ,可见与前面的filter很类似。

 

其他使用场景:指定特定文件使用特殊合并策略等。

 

大小写

git默认对文件夹或文件名的大小写不区分,这是个小坑,需要注意。

 

 

连接GitHub/Gitlab

GtiHub不适合作为个人不愿公开的项目的托管,可使用Gitlab。被微软收购后可以了。

为Gitlab账号添加SSH key并使用Git连接Gitlab(为GitLab帐号添加SSH keys并连接GitLab):有两种方式从Gitlab上clone项目,http和ssh。

  • 前者每次clone、push等操作都需要用户输入账号的用户名和密码,比较麻烦;
  • 可以使用后者并配置SSH Key来避免这种麻烦。其实本质上使用SSH也需要输入账号和相应密码,但我们通过生成并添加SSH Key使得在clone等操作时计算机帮我们做了身份验证的事。(且由于生成了公钥和私钥并把公钥放到了Gitlab上,它们相当于一对锁和钥匙,在连接时进行公钥和私钥的匹配,所以用SSH方式不需要知道账号的密码了,在生成SSH Key时提示设置的密码也不是账号的密码,而是push操作时的密码,可以不设置,这样以后clone等操作都不需要输入密码了)

 

图形界面

Git 的原生环境是命令行终端。 在那里,你可以体验到最新的功能,也只有在那里,你才能尽情发挥 Git 的全部能力。 但是对于某些任务而言,纯文本并不是最佳的选择;有时候你确实需要一个可视化的展示方式,而且有些用户更习惯那种能点击的界面。

要注意的是,不同的界面是为不同的工作流程设计的。 一些客户端的作者为了支持某种他认为高效的工作流程,经过精心挑选,只显示了 Git 功能的一个子集。 每种工具都有其特定的目的和意义,从这个角度来看,不能说某种工具比其它的“更好”。 还有请注意,没有什么事情是图形界面客户端可以做而命令行客户端不能做的;命令行始终是你可以完全操控仓库并发挥出全部力量的地方。

具体而言,图形界面有 gitk、gui-gui 等,各OS上也有各种各样的Git 图形界面客户端,Git 官方网站整理了时下最流行的一些客户端,参阅:https://git-scm.com/downloads/guis

 

集成Git到应用

命令行式集成、Libgit2、JGit等。具体可参阅:https://www.progit.cn/#_%E5%B0%86_git_%E5%B5%8C%E5%85%A5%E4%BD%A0%E7%9A%84%E5%BA%94%E7%94%A8

 

Java Maven项目构建自动集成Git提交信息

借助 maven git-commit-id plugin 来完成。详情可参阅官方文档

通过在项目pom.xml中配置该插件,在项目构建后会自动生成 git.properties 文件到class path文件夹下。该文件包含了最近一次git提交的各种信息。通常可通过这些信息来确定线上项目的版本,以加快问题定位。

maven配置:

    <properties>
        <git-commit-id-plugin.output.file.name>git.properties</git-commit-id-plugin.output.file.name>
    </properties>
    <build>
        <plugins>

            <plugin>
                <groupId>pl.project13.maven</groupId>
                <artifactId>git-commit-id-plugin</artifactId>
                <version>3.0.0</version>
                <executions>
                    <execution>
                        <id>get-the-git-infos</id>
                        <phase>validate</phase>
                        <goals>
                            <goal>revision</goal>
                        </goals>
                    </execution>
                    <execution>
                        <id>validate-the-git-infos</id>
                        <phase>package</phase>
                        <goals>
                            <goal>validateRevision</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <dotGitDirectory>${project.basedir}/.git</dotGitDirectory>
                    <prefix>git</prefix>
                    <verbose>false</verbose>
                    <generateGitPropertiesFile>true</generateGitPropertiesFile>
                    <generateGitPropertiesFilename>${project.build.outputDirectory}/${git-commit-id-plugin.output.file.name}</generateGitPropertiesFilename>
                    <format>json</format>
                    <gitDescribe>
                        <skip>false</skip>
                        <always>false</always>
                        <dirty>-dirty</dirty>
                    </gitDescribe>
                    <includeOnlyProperties>
                        <!-- <includeOnlyProperty>^git.commit.id.full$</includeOnlyProperty> -->
                    </includeOnlyProperties>
                    <excludeProperties>
                        <excludeProperty>git.remote.origin.*</excludeProperty>
                    </excludeProperties>

                    <validationShouldFailIfNoMatch>true</validationShouldFailIfNoMatch>
                    <validationProperties>
                        <!-- 
                        <validationProperty>
                            <name>validating git dirty</name>
                            <value>${git.dirty}</value>
                            <shouldMatchTo>false</shouldMatchTo>
                        </validationProperty>
                        <validationProperty>
                            <name>validating project version</name>
                            <value>${project.version}</value>
                            <shouldMatchTo>
                                <![CDATA[^.*(?<!-SNAPSHOT)$]]>
                            </shouldMatchTo>
                        </validationProperty>
                        <validationProperty>
                            <name>validating current commit has a tag</name>
                            <value>${git.closest.tag.commit.count}</value>
                            <shouldMatchTo>0</shouldMatchTo>
                        </validationProperty>
                         -->
                    </validationProperties>
                </configuration>
            </plugin>
        </plugins>
View Code

该配置生成的git.properties文件内容:

  "git.branch" : "testing-eduadmin",
  "git.build.host" : "MacBook-Pro.local",
  "git.build.time" : "2022-04-26T18:06:05+0800",
  "git.build.user.email" : "zhangsan@xx.com",
  "git.build.user.name" : "marchon",
  "git.build.version" : "0.0.1-SNAPSHOT",
  "git.closest.tag.commit.count" : "1359",
  "git.closest.tag.name" : "dev_before_oauth_merge_in",
  "git.commit.id" : "9f48e761ed17442501b92e1e108ff25555203fe6",
  "git.commit.id.abbrev" : "9f48e76",
  "git.commit.id.describe" : "dev_before_oauth_merge_in-1359-g9f48e76-dirty",
  "git.commit.id.describe-short" : "dev_before_oauth_merge_in-1359-dirty",
  "git.commit.message.full" : "[add] git commit id",
  "git.commit.message.short" : "[add] git commit id",
  "git.commit.time" : "2022-04-26T17:52:30+0800",
  "git.commit.user.email" : "zhangsan@xx.com",
  "git.commit.user.name" : "marchon",
  "git.dirty" : "true",
  "git.local.branch.ahead" : "1",
  "git.local.branch.behind" : "0",
  "git.tags" : "",
  "git.total.commit.count" : "3641"
View Code

可见,包含 branch、最后一次提交的id、message、username、time 等信息,各字段的含义可参阅上述官方文档。

可在项目中专门写个接口来获取该文件的信息,这样就很容易定位线上项目的版本,利于排查问题。以SpringBoot项目为例(详情可参阅官方文档2):

在application.yml中设置个变量指向maven中配置的将要生成的目标文件的路径:

git:
  info:
    file:
      path: @git-commit-id-plugin.output.file.path@ # value from maven property
View Code

接着在项目中就可使用该路径来读取文件内容了: @Value("${git.info.file.path}") private String gitInfoFilPath; 。

    @ApiOperation(value = "获取项目构建的版本信息", notes = "主要用于定位线上运行的代码的版本")
    @GetMapping(value = PATH_BUILD_INFO)
    private Properties getBuildInfoProperties() throws IOException {
        String filePath = ConfigParam.gitInfoFilPath;
        String res = null;
        Resource resource = new ClassPathResource(filePath);
        try (InputStream in = resource.getInputStream()) {
            res = StreamUtils.copyToString(in, StandardCharsets.UTF_8);
        }
        log.info("get build info from {}: {}", filePath, res);
        return objectMapper.readValue(res, Properties.class);
    }
View Code

 

 

8 进阶之内部原理

Git 是一个内容寻址(content-addressable)文件系统,其核心部分是一个简单的键值对数据库(key-value data store)。 你可以向该数据库插入任意类型的内容,它会返回一个key,通过该key可以在任意时刻再次检索(retrieve)该内容。对于普通用户来说,内部原理和内部命令并不是必须了解的,但却可以通过学习 Git 内部原理相关知识来实现出自己的应用,并且以更高级、更得心应手的方式来驾驭 Git。

 

数据的标识与完整性保证

Git 中所有数据在存储前都计算校验和,然后以校验和来引用。 这意味着不可能在 Git 不知情时更改任何文件内容或目录内容。 这个功能建构在 Git 底层,是构成 Git 哲学不可或缺的部分。 若你在传送过程中丢失信息或损坏文件,Git 就能发现。实际上,Git 数据库中保存的信息(如commit id)都是以文件内容的哈希值来索引,而不是文件名。

校验和基于 Git 中文件的内容或目录结构计算出来,采用的是SHA-1散列算法。算出的值为由 40 个十六进制字符(0-9 和 a-f)组成字符串。如commit id: 094717bd5aaaa8797dedb721209871a93bf58618  

 

底层命令

底层命令(plumbing)得以让你窥探 Git 内部的工作机制,也有助于说明 Git 是如何完成工作的,以及它为何如此运作。 多数底层命令并不面向最终用户:它们更适合作为新命令和自定义脚本的组成部分。

这里介绍几个主要的底层命令,后面的节中会结合具体场景使用这些命令。包括:

 git count-objects -v :列出当前项目的版本库的数据对象数、总大小等。

 git cat-file -t $SHA_VAL :列出object类型,可能是 blob、tree、commit。

 git cat-file -p $SHA_VAL :列出对象的内容:若对象是blob类型则列出文件内容(数据是被压缩过的,直接cat是乱码,用此命令可正确查看到原先内容);若是tree类型则查看该tree的entry内容;若是commit类型则查看commit对象的内容,包括该commit的msg、包含的tree对象等。

 git cat-file -p dev^{tree}  :查看最新的提交上的tree对象的内容,也可将tree换成commit以查看最新commit对象的内容。

 git cat-file -s $SHA_VAL :查看指定对象的大小,单位字节

blob对象操作:

 git hash-object -w test.txt :将指定的文件写入 Git 数据库,返回SHA值。若文件内容一样(名字可不同),则返回值一样。

tree对象操作:

 git write-tree :基于暂存区的内容创建tree对象,返回SHA值。若暂存区内容(目录下的entry列表及各entry)一样,则返回值一样。

 git read-tree [--prefix=bak] $TREE_SHA_VAL :将指定的tree对象的内容恢复到暂存区。可以指定子目录以将内容恢复到当前目录的该子目录下。

commit对象操作:

 git commit-tree $TRE_SHA_VAL [ -p $PARENT_SHA_VAL ] $CMMIIT_MSG :基于指定的tree对象及父commit对象创建新commit对象。

引用的添加或修改:

 git update-ref refs/heads/test cac0ca :将本地test分支的最新提交指定为cac0ca。若test分支不存在则会创建。

也可以用该命令创建或修改标签(附注标签则不行),如: git update-ref refs/tags/v1.0 cac0c ,需要注意的是,该命令创建出来的是轻量标签,对于附注标签得借助高层命令创建,如: git tag -a v1.1 1a41 -m 'test tag' 

 git symbolic-ref HEAD [refs/heads/test] :将HEAD指向指定的分支。若未指定分支参数,则是查询HEAD的内容,也即指向哪个分支。

打包内容的查看:

 git verify-pack -v .git/objects/pack/pack-7c517ed807794f0544c0e565c9514904289884fb.pack :从内容以增量编码方式编码的指定包文件中解析并列出包括的 Git 对象信息。获取最大的文件: git verify-pack -v .git/objects/pack/pack-29…69.idx | sort -k 3 -n | tail -1 ,结合下个命令可以获得该文件名。

  git rev-list --objects --all  :列出所有的 [ blob object 及该blob相关对应的文件路径、commiit object ]。可借此命令查询某个SHA-1值blob对象的路径或文件名。

 

当我们运行 git add/commit 等高级命令时,Git 内部就是将高级命令转为上述的底层命令来完成实质工作——将被改写的文件保存为blob数据对象,更新暂存区,记录tree对象,最后创建一个指明了顶层tree对象和父commit的commit对象。 

 

后面结合这些底层命令来介绍 Git 的三种对象。

 

数据存储位置

当在一个新目录或已有目录执行 git init 时,Git 会创建一个 .git 目录。 这个目录包含了几乎所有 Git 存储和操作的对象。 如若想备份或复制一个版本库,只需把这个目录拷贝至另一处即可。

对于一个全新的 git init 版本库,默认的目录结构如下:

$ ls -F1 .git/
HEAD
config         项目特有的配置
description    仅供 GitWeb 程序使用
hooks/         存放客户端或服务端的hook scripts
info/
objects/       存放 Git 的 blob、tree、commit、annotated tag 对象,均以文件的形式压缩存储在此
refs/          存放 Git 的引用名称。包括 heads/、remotes/、tags/ 等,分别表示 本地分支、远程分支、标签 的列表
logs/ 存放引用日志等数据。包括 HEAD、heads/、remotes/、tags/ 等,如执行 git reflog 实际上就是从此目录下的HEAD文件解析得到数据,对于分支引用的变更记录同理(如git reflog dev)。

 

Git内部的数据存储

前面介绍的绝大多数命令都是应用层命令,它们最终都是转化为底层命令去操作 git 存储的数据来起作用的。这些命令较多较复杂,但如果透过现象看本质——看Git内部是如何存储和操作数据的,则能很容易理解这些命令的作用并更好地使用它们。

(参阅:Git对象内部原理-Git SCM

 

Git 对象的类型

git 中的数据包括三种类型:文件内容(blob)、目录结构(tree)、提交信息(commit),它们都在 .git/objects 文件夹下

各种类型数据都是存储在key-value存储系统中,key为SHA值,value为数据。

blob、tree的关系 与 Linux文件系统中的文件与目录的关系很类似。

Git中的数据是(三种都是)以SHA值作为唯一标识的,这与Linux文件系统中以inode作为文件或目录的唯一标识的思想类似。

三种object:

  • Blob 文件,用于描述文件信息
  • Tree 文件,用于描述 blob、tree 对象信息
  • Commit 文件,用于描述 tree 对象信息

1 数据对象(blob object):文件内容快照。存储压缩后的原始文件内容,不存文件名等其他信息。

每个被git track过(被执行了git add命令)的文件都会被git 压缩保存在 .git/objects 文件夹下——取文件内容与头信息的 SHA-1 校验和,创建以该校验和前两个字符为名称的子目录,并以 (校验和) 剩下 38 个字符为文件名创建文件 (保存至子目录下)。

如: .git/objects/1c/08073775a18a171deceb8cc12cb3e3ff6a80e4 。该文件内容是压缩过的,直接看是乱码,可用前述命令查看:

$ git hash-object -w a.txt
1c08073775a18a171deceb8cc12cb3e3ff6a80e4

$ git cat-file -t 1c08073775a18a171deceb8cc12cb3e3ff6a80e4
blob
$ git cat-file -p 1c08073775a18a171deceb8cc12cb3e3ff6a80e4
hello world in a.txt

也正因为“数据对象不考虑文件名等信息来计算 SHA 校验和,且以该校验和作为key”,故:

能识别重命名、移动操作:对于文件重命名、文件移动等操作,Git 能够准确地识别出来(计算出操作后数据对象的SHA校验和与操作前一致),而不会认为是删旧文件和创建新文件。这点也与Linux文件系统中inode的作用很像(可参阅:理解inode-阮一峰

相同内容的文件不会重复存储:对于两个文件,只要文件的内容一样,计算出的SHA值是一样的,从而在Git的key-value数据库中只会存一份。由于Git中存储的数据只增不删,故不用担心用户删除一个文件影响另一个文件在Git数据库中的内容。

2 树对象(tree object):目录结构快照。储存一个目录结构(类似于文件夹)下每一个entry(文件或文件夹)的权限(类似于Linux的权限设计但更简化)、类型(是blob还是tree)、对应的唯一标识(SHA1值)、文件名。示例:

$ git write-tree
ad4b1c6356a7e7cf46251f43bf6ec4fbd9b9188e

$ git cat-file -t ad4b1c6356a7e7cf46251f43bf6ec4fbd9b9188e
tree
$ git cat-file -p ad4b1c6356a7e7cf46251f43bf6ec4fbd9b9188e
100644 blob 1c08073775a18a171deceb8cc12cb3e3ff6a80e4    a.txt
100644 blob 4ea2fd26af8a696917021f69e26d1450e01b8a4a    b.txt
040000 tree 54d82d50790b32af25a01e2f09ad81f445aafee0    c

tree object与Linux文件系统设计上的区别:后者的一个文件包含内容和inode两部分,目录下的每个目录项包含文件名和文件名对应的inode号。即前者所存entry中除文件名外的内容相当于后者的inode。

Git 以一种类似于 UNIX 文件系统的方式存储blob、tree对象,但做了些许简化。 所有内容均以树对象和数据对象的形式存储,其中树对象对应了 UNIX 中的目录项,数据对象则大致上对应了 inode 或文件内容。

Git中文件权限有:1000644(普通文件)、100755(可执行文件)、120000(链接文件)、040000(tree对象)等。

3 提交对象(commit object):储存的是一个commit的内容,包括暂存区中项目目录结构的tree对象的哈希、上一个提交的哈希值(从HEAD指针获得。若为第一个提交则无父节点。若是merge产生的提交则可能出现多个父节点)、提交的作者、提交的具体时间、该提交的msg和commitId等。示例:

 1 # 创建三个commit
 2 $ echo "first commit" | git commit-tree ad4b1c6356a7e7cf46251f43bf6ec4fbd9b9188e
 3 97249fe833a0b0adff29a8ed3a88ab588a2f74df
 4 
 5 $ echo "second commit"| git commit-tree ad4b1c6356a7e7cf46251f43bf6ec4fbd9b9188e -p 97249fe833a0b0adff29a8ed3a88ab588a2f74df
 6 e377e785b8c9566a449d96bb52e04d4f8fe0b4be
 7 
 8 $ echo "third commit" | git commit-tree ad4b1c6356a7e7cf46251f43bf6ec4fbd9b9188e -p e377e785b8c9566a449d96bb52e04d4f8fe0b4be
 9 4bbc8e8ecad3ed35f95895d65616eefcc042f7c3
10 
11 
12 # 查看最后一个commit的信息
13 $ git cat-file -p 4bbc8e8ecad3ed35f95895d65616eefcc042f7c3
14 tree ad4b1c6356a7e7cf46251f43bf6ec4fbd9b9188e
15 parent e377e785b8c9566a449d96bb52e04d4f8fe0b4be
16 author zhangsan <zhangsan@test.com> 1606728767 +0800
17 committer zhangsan <zhangsan@test.com> 1606728767 +0800
18 
19 third commit
20 
21 
22 # 查看commit历史
23 $ git log --stat 4bbc8e8ecad3ed35f95895d65616eefcc042f7c3
24 commit 4bbc8e8ecad3ed35f95895d65616eefcc042f7c3
25 Author: zhangsan <zhangsan@test.com>
26 Date:   Mon Nov 30 17:32:47 2020 +0800
27 
28     third commit
29 
30 commit e377e785b8c9566a449d96bb52e04d4f8fe0b4be
31 Author: zhangsan <zhangsan@test.com>
32 Date:   Mon Nov 30 17:32:23 2020 +0800
33 
34     second commit
35 
36 commit 97249fe833a0b0adff29a8ed3a88ab588a2f74df
37 Author: zhangsan <zhangsan@test.com>
38 Date:   Mon Nov 30 17:31:47 2020 +0800
39 
40     first commit
41 
42  a.txt   | 1 +
43  b.txt   | 1 +
44  c/c.txt | 1 +
45  3 files changed, 3 insertions(+)
View Code

4 附注标签对象(annotated tag object):类似于commit object————它包含一个标签创建者信息、一个日期、一段注释信息,以及一个指针。主要区别是附注标签对象通常可指向任意(blob、tree、commit、tag)对象而不是像commit对象那样仅可指向tree对象,且在使用上通常不改变指向。示例:

 1 $ git tag -a v1.1 254f  -m "test tag"
 2 
 3 $ cat .git/refs/tags/v1.1 
 4 4203990a4d2528162f30a6484173466da2cf02b7
 5 
 6 $ git cat-file -t 4203990a4d2528162f30a6484173466da2cf02b7
 7 tag
 8 
 9 $ git cat-file -p 4203990a4d2528162f30a6484173466da2cf02b7
10 object 254fa86518ef965d6d3f14ca258cb757fb1c1128
11 type commit # tag所指向的对象的类型
12 tag v1.1
13 tagger zhangsan <zhangsan@test.com> 1606738469 +0800
14 
15 test tag
16 annotated tag object demo
View Code

 

三种object之间关系的示意图如下:

  ,更复杂的例子:

注:存储下级的SHA值相当于保存了指向下级的指针,因为可以该SHA值作为key从key-value存储系统中找到对应的value。

 

由于三种object数据间以 Merkle Tree(默克尔树,也称Hash Tree)的形式组织,因此只要其中一个被串改,就很容易被发现。

题外话,Merkle Tree是一种存储hash值的树,父节点的值是子节点值的和的hash。通过它可以快速判断数据是否损坏或修改,P2P网络等很多地方用到,示例如下:

 

Git 对象的存储

以 blob 对象为例,会:

1 组装 文件头信息 + 文件内容。文件头信息示例: blob 16\0 ,其格式为: 类型 + 空格 + 文件内容长度 + 空字节;

2 基于组装结计算 SHA-1 摘要值(或称哈希值或称校验和...);

3 使用zlib压缩上述组装的结果;

4 将压缩结果存到磁盘 .git/objects 目录下 ——以 SHA-1 前两个字符作为子目录、剩余字符作为文件名,压缩结果存到该文件中。

上述过程的代码演示:

 1 $ irb
 2 >> content = "what is up, doc?"
 3 => "what is up, doc?"
 4 
 5 >> header = "blob #{content.length}\0"
 6 => "blob 16\u0000"
 7 
 8 >> store = header + content
 9 => "blob 16\u0000what is up, doc?"
10 >> require 'digest/sha1'
11 => true
12 >> sha1 = Digest::SHA1.hexdigest(store)
13 => "bd9dbf5aae1a3862dd1526723246b20206e5fc37"
14 
15 >> require 'zlib'
16 => true
17 >> zlib_content = Zlib::Deflate.deflate(store)
18 => "x\x9CK\xCA\xC9OR04c(\xCFH,Q\xC8,V(-\xD0QH\xC9O\xB6\a\x00_\x1C\a\x9D"
Demo in Ruby

tree、commit 对象的存储方式与上面介绍的 blob 对象的类似,只不过:tree、commit对象的头部信息的类型分别是 tree 、commit;且tree、commit 对象的内容有各自固定的格式。

注:git 会在某些时机对这些对象进行打包(pack)处理以减少松散存储,详见下节。

 

Git 对象的 pack 打包及garbage清理(git gc)  

(详情可参阅:https://www.progit.cn/#_%E5%8C%85%E6%96%87%E4%BB%B6

如前面所述,Git 将其所有对象(blob、tree、commit、tag)都压缩存储在 .git/objects/ 下,且每个数据都是一个独立的文件,这种存储格式称为“松散(loose)”对象格式另一方面,Git 对于添加到版本库的文件是只增不减的,且对文件的任何改动都会生成一个全新的文件存储起来而不是存储文件差异。显然,随着项目的发展版本库会越来越大,特别是项目中有大文件时,对其一点改动就会产生新的大文件存储,这显然会加速臃肿。

为解决上诉问题,Git 会在某些时机下以增量编码(delta encoding)方案(该方案可减少重复内容的重复存储从而节省存储空间)将这些文件整理成一个称为“包文件(packfile)”的二进制大文件,以 提高效率(?何以见得)和节省空间 ,这就是所谓的打包(pack)功能。值得注意的是,GIt 项目数据在本地和远程间的传输(clone、push、fetch等)主要就是 pack数据 + 引用 的传输。 

 

打包时机:git gc 命令执行时。当版本库中有太多松散对象或你向远程服务器执行推送时 Git 会自动执行 git gc以进行打包;用户也可手动执行 git gc 命令触发打包。

打包示例(真实例子):

打包前的对象列表: 

 1 $ find .git/objects/ -type f
 2 .git/objects//92/7c2f5d56db108d6c2f728171a689a250078372
 3 .git/objects//3b/cc389559d5d6b4a7539fe54b2be5ae3e257ea2
 4 .git/objects//9e/88e3ad390082600b9add47e61928c440914e2e
 5 .git/objects//58/e6e28a1caabe68225a60e95086f13af611a565
 6 .git/objects//93/328424b327c5f7d944842b17f6e934238702cc
 7 .git/objects//ad/4b1c6356a7e7cf46251f43bf6ec4fbd9b9188e
 8 .git/objects//df/8ea086fb2c617ed19d6f081a348d6bae8890e2
 9 .git/objects//a2/854296f2a86af8ac0b3cf432d2458067836f3e
10 .git/objects//d6/aa6e7e23ac71dcec06168a894210cc144fc9b0
11 .git/objects//d8/9990f53b4c63cde5796b06fa44cf34fcd692d7
12 .git/objects//e2/ae29c73ce3a88b6b10724dac7cf90f4cb1592b
13 .git/objects//fc/70fe33dc8736a64e21f68908e8ca4da2e0e54a
14 .git/objects//e3/77e785b8c9566a449d96bb52e04d4f8fe0b4be
15 .git/objects//cf/60b8e9936b87840aaaebe3dbfcaf7bda35aa4a
16 .git/objects//4e/a2fd26af8a696917021f69e26d1450e01b8a4a
17 .git/objects//4b/bc8e8ecad3ed35f95895d65616eefcc042f7c3
18 .git/objects//4b/825dc642cb6eb9a060e54bf8d69288fbee4904
19 .git/objects//29/d1494479f0abf4ec26bcfc0a1c342f661bac71
20 .git/objects//42/03990a4d2528162f30a6484173466da2cf02b7
21 .git/objects//4c/dc4dc998ea18017c350d9f07ad67be114e6d0c
22 .git/objects//86/028654b45d8d08e6755fcc8ea4d58801b71cba
23 .git/objects//2a/eb1496a70ab5f932ef1660422228977c7d78f2
24 .git/objects//54/d82d50790b32af25a01e2f09ad81f445aafee0
25 .git/objects//06/a11fb709effd78512a48f8e4fc9b1650870495
26 .git/objects//6c/79cd14f03b6bb19fa341d78213fcdec636d019
27 .git/objects//97/249fe833a0b0adff29a8ed3a88ab588a2f74df
28 .git/objects//90/94645111cf1150caca07de1c908cf875f85321
29 .git/objects//e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391
30 .git/objects//f9/0ac1710ce572e530836fd7833ea469a513970f
31 .git/objects//e8/9b0af0a48d5350000bc267a9f637845c932c1c
32 .git/objects//f1/72ef45996de6ab7b1c82ad198313f33b70910a
33 .git/objects//e0/f1ee1826f922f041e557a16173f2a93835825e
34 .git/objects//15/2d03222c48ebc6d03fa8c89c8b5a23082a312e
35 .git/objects//71/d102391aa78d46efc3be52e5663823b9326cab
36 .git/objects//71/2cf10980cc2615ca8aedaa1774b3ee883a3e8d
37 .git/objects//1c/08073775a18a171deceb8cc12cb3e3ff6a80e4
38 .git/objects//25/4fa86518ef965d6d3f14ca258cb757fb1c1128
View Code 

通过 gc 命令打包:

1 $ git gc
2 Enumerating objects: 14, done.
3 Counting objects: 100% (14/14), done.
4 Delta compression using up to 4 threads
5 Compressing objects: 100% (7/7), done.
6 Writing objects: 100% (14/14), done.
7 Total 14 (delta 1), reused 0 (delta 0)
View Code

打包后的对象列表:

 1 $ find .git/objects/ -type f
 2 .git/objects//92/7c2f5d56db108d6c2f728171a689a250078372
 3 .git/objects//3b/cc389559d5d6b4a7539fe54b2be5ae3e257ea2
 4 .git/objects//9e/88e3ad390082600b9add47e61928c440914e2e
 5 .git/objects//58/e6e28a1caabe68225a60e95086f13af611a565
 6 .git/objects//93/328424b327c5f7d944842b17f6e934238702cc
 7 .git/objects//a2/854296f2a86af8ac0b3cf432d2458067836f3e
 8 .git/objects//d6/aa6e7e23ac71dcec06168a894210cc144fc9b0
 9 .git/objects//d8/9990f53b4c63cde5796b06fa44cf34fcd692d7
10 .git/objects//e2/ae29c73ce3a88b6b10724dac7cf90f4cb1592b
11 .git/objects//e3/77e785b8c9566a449d96bb52e04d4f8fe0b4be
12 .git/objects//4b/bc8e8ecad3ed35f95895d65616eefcc042f7c3
13 .git/objects//pack/pack-7c517ed807794f0544c0e565c9514904289884fb.idx
14 .git/objects//pack/pack-7c517ed807794f0544c0e565c9514904289884fb.pack
15 .git/objects//29/d1494479f0abf4ec26bcfc0a1c342f661bac71
16 .git/objects//4c/dc4dc998ea18017c350d9f07ad67be114e6d0c
17 .git/objects//86/028654b45d8d08e6755fcc8ea4d58801b71cba
18 .git/objects//2a/eb1496a70ab5f932ef1660422228977c7d78f2
19 .git/objects//info/packs
20 .git/objects//6c/79cd14f03b6bb19fa341d78213fcdec636d019
21 .git/objects//97/249fe833a0b0adff29a8ed3a88ab588a2f74df
22 .git/objects//90/94645111cf1150caca07de1c908cf875f85321
23 .git/objects//e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391
24 .git/objects//e8/9b0af0a48d5350000bc267a9f637845c932c1c
25 .git/objects//f1/72ef45996de6ab7b1c82ad198313f33b70910a
26 .git/objects//71/d102391aa78d46efc3be52e5663823b9326cab
27 .git/objects//71/2cf10980cc2615ca8aedaa1774b3ee883a3e8d
View Code

可见:打包后原先的大部分对象不见了(这些对象就是dangling对象,可删除之),有些对象仍存在;与此同时出现了一对新文件(.idx 和 .pack 文件)。

 

打包原理

1 打包目的:集中存储而非松散存储、利于多文件整体传输而非松散逐个传输。

2 打包前后都存在的对象是未被任何提交记录所引用的对象,称为悬空(dangling)对象。这些对象不会被打包到包文件中,相当于garbage 故可被安全地从磁盘删除(虽然实际上仍保留着并没立即被删除)。

3 而有被引用的对象会被 Git 合并到一个大的 .pack 包文件中并删掉原对象;此外还会创建 .idx 索引文件,用于记录每个对象的SHA值及在包文件中的偏移信息,从而可从包文件快速定位任一对象。Git 打包对象时,会查找命名及大小相近的文件,并只保存这些文件不同版本之间的差异内容(delta encoding方案)到打包文件中。

.pack 文件和 .idx 文件的名字一样只是后缀不同;每次打包都会生成新的打包文件名,打包文件名存在固定文件 .git/objects/info/packs 中;随着项目越来越大,允许有多个打包文件

可借助verify-packa命令查看包文件内容,命令及结果示例:

 1 $ git verify-pack -v .git/objects/pack/pack-7c517ed807794f0544c0e565c9514904289884fb.pack
 2 fc70fe33dc8736a64e21f68908e8ca4da2e0e54a commit 247 156 12
 3 254fa86518ef965d6d3f14ca258cb757fb1c1128 commit 254 162 168
 4 4203990a4d2528162f30a6484173466da2cf02b7 tag    149 129 330
 5 df8ea086fb2c617ed19d6f081a348d6bae8890e2 commit 26 38 459 1 254fa86518ef965d6d3f14ca258cb757fb1c1128
 6 cf60b8e9936b87840aaaebe3dbfcaf7bda35aa4a commit 205 130 497
 7 ad4b1c6356a7e7cf46251f43bf6ec4fbd9b9188e tree   94 98 627
 8 54d82d50790b32af25a01e2f09ad81f445aafee0 tree   33 43 725
 9 4b825dc642cb6eb9a060e54bf8d69288fbee4904 tree   0 9 768
10 f90ac1710ce572e530836fd7833ea469a513970f tree   74 54 777
11 06a11fb709effd78512a48f8e4fc9b1650870495 tree   36 47 831
12 1c08073775a18a171deceb8cc12cb3e3ff6a80e4 blob   21 31 878
13 4ea2fd26af8a696917021f69e26d1450e01b8a4a blob   21 31 909
14 152d03222c48ebc6d03fa8c89c8b5a23082a312e blob   21 31 940
15 e0f1ee1826f922f041e557a16173f2a93835825e blob   13 22 971
git verify-pack

结果中 commit对象254f 是完整保存的,大小为B;而 df8e... commit 26 38 459 1 254f... 一行表示commit对象df8e 引用了commit对象254f 的部分内容,其自身独有的内容只有26B,这样就不用重复保存公共部分的文件内容,从而达到节省存储空间的目的。

 

实际上,git gc 的效果不仅包括上述的”打包对象数据“,还包括:

打包引用数据:将 .git/refs/ 下的所有引用文件(每个文件的文件名为引用名、文件内容为commit id)整理到 .git/packed-refs 文件中(每条记录包括 commit id、引用名)以提高效率。

执行 gc 后若还添加或修改了引用,则仍会在 .git/refs/ 下产生对应文件。实际上 git 优先使用此目录下的引用记录,不存在时才去 .git/packed-refs 文件中找。

执行 gc 前后两者的内容示例:

 1 //执行git gc前
 2 
 3 $ find .git/refs/ -type f
 4 .git/refs//heads/testing
 5 .git/refs//heads/dev
 6 
 7 $ cat .git/packed-refs #无内容
 8 
 9 
10 
11 //执行git gc后
12 $ find .git/refs/ -type f #无内容
13 
14 $ cat .git/packed-refs
15 # pack-refs with: peeled fully-peeled sorted 
16 e5374c4e6b92fb9e993234440394669d01c25df6 refs/heads/dev
17 3e2e64d0862c7cdc440ab50c8e19a0df6b69d82c refs/heads/testing
View Code

查找及删除garbage数据:garbage数据是指 unreachable、dangling 的数据(前者包含后者),可通过命令 git fsck --full 找出garbage数据。示例:

1 $ git fsck --full
2 Checking object directories: 100% (256/256), done.
3 Checking objects: 100% (76440/76440), done.
4 dangling blob 3e2a25297aa9b9df6bb345a0267b5abb29e9108d
5 dangling blob ce013625030ba8dba906f756967f9e9ca394464a
6 dangling commit f7791e459e9c06d8de074cac3a415c28b84f9dda
7 dangling blob a06263a4b2d708f0ac6011ff7b6144969d6d996d
8 Verifying commits in commit graph: 100% (3917/3917), done.
View Code

建议删除:通过  git gc --auto  建议 Git 将未被任何commit引用的对象(blob、tree、commit)从文件系统删除。不过,通常在有7000个以上的这种对象或超过50个pack文件时才会有这个效果,可通过参数 gc.auto 、 gc.autopacklimit 来配置阈值。

立即删除:若要立刻删除这些unreachable数据,可用命令 git prune --expire now 。

 

综上,git gc 的作用有:打包对象数据、打包引用数据、引用链分析找出garbage数据 等,可通过打开调试开关的 $ GIT_TRACE_PERFORMANCE=true git gc 命令来查看gc的内部操作过程细节。

 

Git中的引用(HEAD、分支、标签、stash)

引用的内部表示

Git中的引用(references,或缩写为 refs)包括 HEAD、本地分支/远程追踪分支、标签、stash,它们的作用是帮助记住最新提交所在的位置,其本质上是指向commit的指针(一级或二级指针)。

HEAD 保存在 .git/HEAD 文件里, $ cat .git/HEAD 结果示例:  ref: refs/heads/dev 。其作用是保存工作区当前所在的分支,相当于一个二级指针(或称符号引用)。

本地分支保存在 .git/refs/heads 文件夹下:

$ ll .git/refs/heads/
total 16
-rw-r--r--  1 zhangsan  453037844  41  5 25 22:09 dev
-rw-r--r--  1 zhangsan  453037844  41  5 18 18:55 dev_oauth

$ cat .git/refs/heads/dev
ef4065fbadadf59595971ff5f95c6a22a995be3b

远程追踪分支保存在 .git/refs/remotes 文件夹下。

$ ll .git/refs/remotes/
total 0
drwxr-xr-x  8 zhangsan  453037844  256  5 25 18:21 origin

$ ll .git/refs/remotes/origin/
total 48
-rw-r--r--  1 zhangsan  453037844  41  5 25 18:21 dev
-rw-r--r--  1 zhangsan  453037844  41  4 30 10:39 staging
-rw-r--r--  1 zhangsan  453037844  41  3 19 17:40 master

$ cat .git/refs/remotes/origin/dev 
0d41e07621a7f1ddac9e84355a9f5921c8926e84
View Code

远程引用和本地分支之间最主要的区别在于,远程引用是只读的。 虽然可以 git checkout 到某个远程引用,但是 Git 并不会将 HEAD 引用指向该远程引用。因此,你永远不能通过 commit 命令来更新远程引用。 Git 将这些远程引用作为记录远程服务器上各分支最后已知位置状态的书签来管理。

标签 保存在 .git/refs/tags 文件夹下。

标签是一种特殊的引用,其可以指向任意类型的 Git 数据对象(blob、tree、commit)而不仅限于commit对象,且指向某个对象后通常不做改变。可见当指向commit时可以看做是一种特殊的分支引用、或者看做是某个commit的别名。示例:

ll .git/refs/tags/
total 24
-rw-r--r--  1 zhangsan  453037844  41  3 19 17:40 i18n-1.0.1
-rw-r--r--  1 zhangsan  453037844  41  3 17 15:23 auth-1.0.0
-rw-r--r--  1 zhangsan  453037844  41  3 17 15:07 i18n-1.0.0

stash:stash也是一种引用,当你将某种改动添加到stash存储区时,内部实际上就是创建一个commit对象,然后在stash列表中去引用该commit。当将stash中的某项应用到当前项目时实际上就是通过该引用将该commit对象应用到当前项目。

 

引用的内部修改

知道了分支、引用的内容原理后,就可以明白:

创建分支或标签等实际上就是在上述对应的文件夹下创建相应的文件;相应的,分支查询(git branch)等就是展示相应文件夹下的文件名;git push的过程就是 将本地分支相较于远程追踪分支的 新commit对象及其所包含的数据对象 都上传的过程。

因此,直接往该文件夹下写个文件就可以创建新分支或标签,示例: echo 3a4858d91d11dd67944e2490836df46db2497607 > .git/refs/heads/d 就创建了指向指定commit的名为d的分支,当然,SHA值无效则很容易出错故不推荐此做法。

当然,Git 不提倡直接编辑文件修改引用数据,而是提供了更加安全的命令:

 $ git update-ref refs/heads/master 1a410 ,用来添加或修改一级指针(如分支引用、标签引用)的数据。若指定的分支名不存在则会先创建。git branch $branch_name 命令实际上内部就是转为此命令生效的。

 git symbolic-ref HEAD 、 git symbolic-ref HEAD refs/heads/test1 :对于像HEAD这样的二级指针,可通过此命令查询或修改指针内容。

 

Git 数据的上传下载(协议) 

执行 git clone、fetch、push等命令时,版本库的数据是怎么从服务端传到本地或从本地传到服务端的?怎么知道远程有哪些分支/标签等引用的?怎么按需获取 blob/tree/commit 数据的?

具体可参阅:proogit.cn/#_传输协议

 

Git 调试

Git 提供了调试开关,可以打开这些开关来跟踪了解 Git 在运行一个命令时内部在做些什么。详情可参阅:Git 调试

 

追踪 git 命令执行时的内部执行过程

只需要在执行的命令前加上开关变量即可。例如: $ GIT_TRACE_PERFORMANCE=true git gc 会列出执行 git gc 时内部都做了什么,结果示例如下:

 1 $ GIT_TRACE_PERFORMANCE=true git gc
 2 14:43:03.601349 trace.c:477             performance: 0.002398000 s: git command: /Library/Developer/CommandLineTools/usr/libexec/git-core/git pack-refs --all --prune
 3 14:43:03.624867 trace.c:477             performance: 0.018377000 s: git command: /Library/Developer/CommandLineTools/usr/libexec/git-core/git reflog expire --all
 4 14:43:03.679541 read-cache.c:2267       performance: 0.000107000 s:  read cache .git/index
 5 Enumerating objects: 23, done.
 6 Counting objects: 100% (23/23), done.
 7 Delta compression using up to 4 threads
 8 Compressing objects: 100% (10/10), done.
 9 Writing objects: 100% (23/23), done.
10 Total 23 (delta 6), reused 23 (delta 6)
11 14:43:03.686129 trace.c:477             performance: 0.051698000 s: git command: /Library/Developer/CommandLineTools/usr/libexec/git-core/git pack-objects --local --delta-base-offset .git/objects/pack/.tmp-48608-pack --keep-true-parents --honor-pack-keep --non-empty --all --reflog --indexed-objects --unpack-unreachable=2.weeks.ago
12 14:43:03.772027 trace.c:477             performance: 0.142671000 s: git command: /Library/Developer/CommandLineTools/usr/libexec/git-core/git repack -d -l -A --unpack-unreachable=2.weeks.ago
13 14:43:03.779138 read-cache.c:2267       performance: 0.000306000 s:  read cache .git/index
14 14:43:03.783662 trace.c:477             performance: 0.006094000 s: git command: /Library/Developer/CommandLineTools/usr/libexec/git-core/git prune --expire 2.weeks.ago
15 14:43:03.789316 trace.c:477             performance: 0.001754000 s: git command: /Library/Developer/CommandLineTools/usr/libexec/git-core/git worktree prune --expire 3.months.ago
16 14:43:03.793715 trace.c:477             performance: 0.000790000 s: git command: /Library/Developer/CommandLineTools/usr/libexec/git-core/git rerere gc
17 14:43:03.795488 trace.c:477             performance: 0.203506000 s: git command: /Library/Developer/CommandLineTools/usr/bin/git gc
View Code

可见,执行 git gc 时会整理引用数据且会清理不再用到的引用、对象等数据。其中还执行了 git prune --expire 2.weeks.ago ——即早于两周前的对象才删除,这与前面说的执行gc时不会立马清除所有dangling对象相印证。


直接记录快照,而非差异比较

Git每次提交时有变化的文件产生一个新的文件快照,而SVN则是保留文件的变化。两者的示例图分别如下:

              

存储快照而非差异的优点之一是内容比较、创建分支、切换分支、回滚等非常简单迅速,缺点是占用空间较大,但总的来说利远大于弊。

 

近乎所有操作都是本地执行

这是DVCS的优势,由于每个本地仓库都是整个远程仓库的副本,因此绝大多数操作都可以在本地完成而不用与远程服务器交互。

 

 

9 参考资料

https://git-scm.com/docs:官方文档(推荐)、https://www.progit.cn:Pro Git书籍(实际上与上面的一样,不知是谁抄谁)

https://www.yiibai.com/git:国人整理的 git 教程

https://docs.google.com/presentation/d/18b-ehlVjU82_PzU64lkbVwkzPsK3T2yENmjwwL7FVfM/edit#slide=id.g24a41a05c5_0_27:Git 原理精要介绍的PPT(推荐)

https://learngitbranching.js.org/?locale=zh_CN:可视化的在线学习git的站点

 

posted @ 2015-01-05 16:00  March On  阅读(1233)  评论(0编辑  收藏  举报
top last
Welcome user from
(since 2020.6.1)