写在前面
Git是一个非常好的版本管理工具,最早是根据廖雪峰等人的博客进行的学习,一直只用到了一些常用的命令做简单的备份回退等。最近结合《ProGit》和《Git权威指南》进行了查漏补缺和更系统深入的学习,下面是结合ProGit一书做的读书简记。
起步
1 初次运行Git前的配置
Git 自带一个 git config 的工具来帮助设置控制 Git 外观和行为的配置变量。这些变量存储在三个不同的位置:
①. /etc/gitconfig 文件: 包含系统上每一个用户及他们仓库的通用配置。 如果使用带有 --system 选项的git config 时,它会从此文件读写配置变量。
②. ~/.gitconfig 或 ~/.config/git/config 文件:只针对当前用户。 可以传递 --global 选项让 Git读写此文件。
③. 当前使用仓库的 Git 目录中的 config 文件(就是 .git/config):针对该仓库。
每一个级别覆盖上一级别的配置,所以 .git/config 的配置变量会覆盖 /etc/gitconfig 中的配置变量。在 Windows 系统中,Git 会查找 $HOME 目录下(一般情况下是 C:\Users\$USER)的 .gitconfig 文件。Git 同样也会寻找 /etc/gitconfig 文件,但只限于 MSys 的根目录下,即安装 Git 时所选的目标位置。
2 git config --list
命令可以列出所有Git当时能找到的配置。
基础
1 Git不同于其他版本控制系统,Git是直接记录快照,而不是差异比较。
2 Git有工作目录、暂存(索引)区域、Git仓库三种工作状态(有时还有远程仓库),关系见下图。
3 用git status
查看当前文件状态,git status --short
或者git status -s
可输出更简洁的状态。
新添加的未跟踪文件前面有 ?? 标记,新添加到暂存区中的文件前面有 A 标记,修改过的文件前面有 M 标记。你可能注意到了 M 有两个可以出现的位置,出现在右边的 M 表示该文件被修改了但是还没放入暂存区,出现在靠左边的 M 表示该文件被修改了并放入了暂存区。
4 .gitignore文件中可以指定忽略模式,支持正则表达式。要忽略指定模式以外的文件或目录,可以在模式前加上惊叹号(!)取反。
5 查看已暂存和未暂存的修改。git diff
比较的是工作目录中当前文件和暂存区域快照之间的差异,也就是修改之后还没有暂存起来的变化内容。若要查看已暂存的将要添加到下次提交里的内容(和工作目录中当前文件的差异),可以用git diff --cached
命令。(Git 1.6.1 及更高版本还允许使用 git diff --staged
,效果是相同的,但更好记些。)
6 'git commit -a'可以跳过暂存区直接提交(跳过git add
),但尽量避免使用。
7 移除文件。使用git rm
移除。如果要移除的文件已经修改过且暂存了,必须使用-f选项强制删除。如果想要把文件从Git仓库中删除,但保留在工作目录中,使用git rm --cached file
。
8 重命名使用git mv file_from file_to
。
9 查看提交历史使用git log
命令。下图是其常用选项:
一些例子如下:
git log -p -2 #显示最近两次提交每次提交的差异
git --stat #在每次提交的下面列出额所有被修改过的文件、有多少文件被修改了以及被修改
git log --pretty=format:"%h %s" --graph #提交对象的简短哈希字串 提交说明 图显
限制输出长度的选项有:
例如:
git log --since=2.weeks #列出所有最近两周内的提交
git log -Sfunction_name #找出添加或移除了某一个特定函数的引用的提交
10 远程仓库的使用。可以使用git remote --help
查看详细使用说明。
git remote -v #显示远程仓库信息;
git remote add [shortname] [url] #添加一个新的远程Git仓库
git fetch [remote-name] #从远程仓库中抓取,不同于git pull,并不会合并,
git push [remote-name] [branch-name] #推送到远程仓库
git remote show [remote-name] #查看远程仓库
git remote rename [ori-name] [dst-name] #重命名
git remote rm [shortname] #移除
11 打标签。
git tag #列出标签
git tag -a v1.4 -m 'my version 1.4' #使用-a创建附注标签,-m指定存储在标签中的信息
git show v1.4 #查看标签信息与对应的提交信息
git tag v1.4-lw #创建轻量标签
git tag -a v1.2 9fceb02 #后期打标签,9fceb02是之前的一个提交
git push origin v1.5 #共享标签
git push origin --tags #一次推送多个标签
git checkout -b [branchname] [tagname] #在特定的标签上创建一个新分支
12 Git别名。
git config --global alias.co checkout #为checkout起别名co
git config --global alias.last 'log -1 HEAD' #git last 显示最后一次提交
git config --global alias.visual '!gitk' #想要执行外部命令,而不是一个 Git 子命令。在命令前面加入 ! 符号
Git分支
1 Git保存数据保存的是文件的快照,进行提交操作是会提交一个对象,该对象包含一个指向暂存内容快照的指针,还包含了作者的姓名和邮箱、提交时输入的信息以及指向它的父对象的指针。内容快照使用的blob对象来保存。下图是个直观展示,该Git 仓库中有五个对象:三个blob对象(保存着文件快照)、一个树对象(记录着目录结构和blob对象索引)以及一个提交对象(包含着指向前述树对象的指针和所有提交信息)。
2 常用命令:
git branch [branch_name] #创建分支
git checkout [branch_name] #切换分支
git checkout -b [branch_name] #新建并切换分支
git branch -d hotfix #删除hotfix分支
git merge hotfix #合并hotfix分支,先切换到master分支。当合并有冲突后,git status查看,修改后add,重新合并。
git branch --merged|--no-merged #过滤这个列表中已经合并或尚未合并到当前分支的分支
3 远程分支。origin是运行git clone
时默认的远程仓库名字。如果运行git clone -o lanbing510
,那么默认的远程分支名字会是lanbing510/master。
git checkout --track origin/serverfix #本地创建serverfix分支来跟踪远程仓库上的serverfix分支
git checkout -b sf origin/serverfix #创建一个sf的本地分支来跟踪远程仓库serverfix分支
git checkout -u origin/serverfix #设置已有分支来跟踪远程分支
git branch -vv #查看设置是所有跟踪分支
git push origin --delete serverfix #删除远程分支
4 变基。整个不同分支的修改有合并(merge)和变基(rebase)两种方法。merge可以保留记录,rebase使分支更清晰。使用变基友风险,谨记:不要对在你的仓库外有副本的分支执行变基。
git rebase --onto master server client #取出 client 分支,找出处于 client 分支和 server 分支的共同祖先之后的修改,然后把它们在 master 分支上重演一遍
git rebase master serve #取出serve分支,在master分支上重演
分布式Git
1 常见的分布式工作流程有:
① 集中式工作流
② 集成管理者工作流
③ 司令官与副官工作流。典型工作流程如下:
2 向一个项目贡献
① 首先,你不会想要把空白错误(根据 git help diff 的描述,结合下面给出的图片,空白错误是指行尾的空格、Tab 制表符,和行首空格后跟 Tab 制表符的行为)提交上去。Git 提供了一个简单的方式来检查这点:在提交前,运行 git diff --check,它将会找到可能的空白错误并将它们为你列出来;
② 接下来,尝试让每一个提交成为一个逻辑上的独立变更集;
③ 最后一件要牢记的事是提交信息。有一个创建优质提交信息的习惯会使 Git 的使用与协作容易的多。一般情况下,信息应当以少于50个字符(25个汉字)的单行开始且简要地描述变更,接着是一个空白行,再接着是一个更详细的解释。
④ 一些常用命令
git log --no-merges issue54..origin/master #要求 Git 只显示所有在后面分支(在本例中是origin/master)但不在前面分支(在本例中是 issue54)的提交的列表
git log origin/master --not issue54 #作用同上一条命令
git log refA refB --not refC #refA 或 refB 包含的但是不被 refC 包含的提交
git log master...experiment #三点语法,查看master 或者 experiment 中包含的但不是两者共有的提交
git log --left-right master...experiment #还会显示出每次提交位于哪一侧
git request-pull origin/master myfork #在派生项目中,生成拉取请求的内容
git merge --no-commit --squash featureB #--squash 选项接受被合并的分支上的所有工作,并将其压缩至一个变更集,使仓库变成一个真正的合并发生的状态,而不会真的生成一个合并提交。这意味着你的未来的提交将会只有一个父提交,并允许你引入另一个分支的所有改动,然后在记录一个新提交前做更多的改动。同样 --no-commit 选项在默认合并过程中可以用来延迟生成合并提交
git format-patch -M origin/master #通过邮件的公开项目,使用该命令,format-patch 命令打印出它创建的补丁文件名字。-M 开关告诉 Git 查找重命名
cat *.patch |git imap-send #将patch通过邮箱发送出去,前提是.gitconfig中配置好了imap
3 维护项目
一些常用命令:
git apply xx.patch #应用使用 git diff 或 Unix diff 命令(不推荐)创建的补丁
git apply --check xx.patch #应用补丁之前检查是否可以顺利应用
git am xx.patch #应用使用format-patch生成的补丁
git diff master...contrib #三点语法,显示自当前特性分支与 master 分支的共同祖先起,该分支中的工作。
git archive master --prefix='project/' | gzip > `git describe master`.tar.gz #归档
git archive master --prefix='project/' --format=zip > `git describe master`.zip #归档
git shortlog --no-merges master --not v1.0.1 #制作提交简报
Git进阶
1 储藏与清理
下面是储藏和清理的一些常用命令:
git stash #储藏工作目录,准备干净的合并
git stash list #查看储藏的东西
git stash apply #应用储藏的东西
git stash apply stash@{2} #应用更旧的储藏
git stash drop stash@{0} #移动指定的储藏
git stash pop #应用并丢弃储藏
git stash --include-untracked 或 -u #同时储藏未跟踪文件
git stash --keep-index #不储藏任何你通过git add命令已暂存的东西
git stash branch [branch_name] #从储藏创建一个分支
git clean #清理工作目录
git stash --all #移除每一样东西并存放在栈中
git clean -x #做一次完全干净的构建而移除所有由构建生成的.o 文件
git clean -f -d #强制移除工作目录中所有未追踪的文件以及空的子目录,可以使用git clean -d -n来做一次演习,看看要做什么
2 搜索
git grep -n gmtime_r #寻找gmtime_r并输出所找到的匹配行行号
git grep -count gmtime_r #使 Git 输出概述的信息,仅仅包括哪些文件包含匹配以及每个文件包含了多少个匹配
git grep -p gmtime_r *.c #-p选项看匹配的行是属于哪一个方法或者函数,该命令是查看哪个函数调用了gmtime_r
git grep --break --heading -n -e '#define' --and \( -e LINK -e BUF_MAX \) v1.8.0 #查看在旧版本 1.8.0 的 Git 代码库中定义了常量名包含 “LINK” 或者 “BUF_MAX” 这两个字符串所在的行,--break 和 --heading 选项来使输出更加容易阅读
git log -L :git_deflate_bound:zlib.c #查看 zlib.c 文件中`git_deflate_bound` 函数的每一次变更
3 重写历史。
① git commit --amend
修正最后一次提交。
② 修改多个提交信息。通过交互式变基工具,可以在任何想要修改的提交后停止,然后修改信息、添加文件或做任何想做的事情。可以通过给 git rebase
增加 -i选项来交互式地运行变基。例如:
git rebase -i HEAD~3 #修改最近三次提交信息,运行命令后会进入交互式界面,按照提示进行即可。
③ git rebase -i
同样可以进行排序提交,压缩提交,拆分提交等操作,用到时可以详细参考ProGit的相应部分。
④ 如果想要通过脚本的方式改写大量提交的话可以使用 filter-branch 例如,全局修改你的邮箱地址或从每一个提交中移除一个文件。例:
git filter-branch --tree-filter 'rm -f passwords.txt' HEAD #从整个提交历史中移除一个叫做 passwords.txt 的文件
4 重置操作(reset)。
① git reset --soft
只移动分支,不更新暂存(索引)和工作目录;
② git reset --mixed
不指定--soft和--hard时的默认选项,更新暂存,不更新工作目录;
③ git reset --hard
更新暂存和工作目录;
④ 通过路径来重置。
git reset file.txt #等价于git reset --mixed HEAD file.txt ,用HEAD分支的file.txt更新暂存(索引)区域
git reset eb43bf file.txt #用eb43bf提交的file.txt更新索引
5 检出(checkout)。
① 不带路径的情况。运行 git checkout [branch]
与运行 git reset --hard [branch]
非常相似,它会更新所有三棵树使其看起来像 [branch],不过有两点重要的区别:
首先不同于 reset --hard,checkout 对工作目录是安全的,它会通过检查来确保不会将已更改的文件吹走;
第二个重要的不同点在于如何更新 HEAD。reset 会移动 HEAD 分支的指向,而 checkout 只会移动 HEAD 自身来指向另一个分支。结合下图会更清晰的理解:
② 带路径。运行 checkout 的另一种方式就是指定一个文件路径,这会像 reset 一样不会移动 HEAD。它就像 git reset [branch] file
那样用该次提交中的那个文件来更新索引,但是它也会覆盖工作目录中对应的文件。它就像是git reset --hard [branch] file
(如果 reset 允许你这样运行的话)- 这样对工作目录并不安全,它也不会移动 HEAD。
6 重置和检出的总结速查。下面的速查表列出了命令对树的影响。"HEAD" 一列中的 "REF" 表示该命令移动了 HEAD 指向的分支引用,而"HEAD" 则表示只移动了 HEAD 自身。特别注意 WD Safe? 一列 - 如果它标记为 NO,那么运行该命令之前请考虑一下。
7 高级合并
① 合并出现冲突后,Git索引会储藏了所有版本(共同的版本stage1,我们的版本stage2,他们的版本stage3)。
② 合并出现冲突后,可以打开冲突的文件,根据指示修改。
③ 一些常用命令:
git merge --abort #尝试恢复到你运行合并前的状态。但当运行命令前,在工作目录中有未储藏、未提交的修改时它不能完美处理,除此之外它都工作地很好
git merge -Xignore-all-space 或 -Xignore-space-change [branch_name] #忽略任意数量的已有空白的修改 或 忽略所有空白修改
git show :1:hello.rb > hello.common.rb #导出共同版本
git show :2:hello.rb > hello.ours.rb #导出我们的版本
git merge-file -p hello.ours.rb hello.common.rb hello.theirs.rb > hello.rb #手动合并冲突修改后的文件,合并完毕后git clean来清理手动合并创建但不再使用的文件
git diff --ours #合并前比较结果与在你的分支上的内容,换一句话说,看看合并引入了什么
git diff --base #查看文件在两边是如何改动的
git checkout --conflict=diff3 hello.rb #重新检出文件并替换合并冲突标记
git config --global merge.conflictstyle diff3 #通过设置 merge.conflictstyle 选项为 diff3 来做为以后合并冲突的默认选项
git log --oneline --left-right HEAD...MERGE_HEAD #得到此次合并中包含的每一个分支的所有独立提交的列表
git log --oneline --left-right --merge #只显示任何一边接触了合并冲突文件的提交
git revert -m 1 HEAD #-m 1 标记指出 “mainline” 需要被保留下来的父结点
git merge -Xours [branch_name] #选择特定的一边Ours并忽略另外一边Theirs而不是让你手动合并冲突
git merge-file --ours #合并单个文件
8 rerere
git rerere
功能是一个隐藏的功能。正如它的名字 "reuse recorded resolution" 所指,它允许你让 Git 记住解决一个块冲突的方法,这样在下一次看到相同冲突时,Git 可以为你自动地解决它。为了启用 rerere 功能,仅仅需要运行这个配置选项:git config --global rerere.enabled true
也通过在特定的仓库中创建 .git/rr-cache 目录来开启它,但是设置选项更干净并且可以应用到全局。
9 打包
git bundle create repo.bundle HEAD master #打包,如果你在打包时没有包含 HEAD 引用,你还需要在命令后指定一个 -b master 或者其他被引入的分支,否则Git 不知道应该检出哪一个分支
git clone repo.bundle repo #解包
git bundle create commits.bundle master #获取在我们的 master 分支而不在原始仓库中的提交
10 文件标注
git blame -L 12,22 simplegit.rb #文件标注,展示文件中每一行最后一次修改的提交 -L 选项来限制输出范围在第12至22行
git blame -C -L 141,153 GITPackUpload.m #在 git blame 后面加上一个-C,Git 会分析你正在标注的文件,并且尝试找出文件中从别的地方复制过来的代码片段的原始出处
小注
本文对个人用户基本用不到的钩子、合并树、凭证管理、搭建Git仓库、底层命令等内容没做总结,用到时可以直接阅读原文。二八原则,掌握80%常用的内容就可以熟练运用Git,有需要的时候再继续深入。最后,最好的学习方法就是实践实践再实践,一定要多思考多实践。
参考文献
[1] ProGit Version 2. Scott Chacon and Ben Straub.
[2] Git权威指南. 蒋鑫.
[3] 阮一峰的网络日志。