1 介绍
Git是一个开源的版本管理系统,和Linux系统是同一作者——Linus Torvalds,用于管理Linux内核开发而开发的,作者给它起名Git(饭桶),介绍是The stupid content tracker, 傻瓜内容跟踪器……
为什么要认真了解它?因为它是个主流团队协作工具,协作工具用不好会影响到别人——《因代码不规范,国外程序员枪击4个同事》,据说是因为同事每次都git push -f
,也不知道真实情况如何……
最近使用比较多,Git命令的各种参数每次都是查完就忘,除了一些常用的外,遇到复杂点的场景都得重新查,其实也有一些图形界面工具(类似Sourcetree1)可以简化Git的使用,不过相对于命令行来说,图形界面的一个动作可能会执行一套组合命令,有时感觉不太可控。Git的官方文档2已经很详细,但太全面了查起某个具体功能来也有点大海捞针的感觉,为了摆脱查找的耗费,把常用命令和常见场景总结一下,毕竟查自己的要比查搜索引擎的要更方便。
有关Git相关的基础知识可以参考Git教程-廖雪峰的官方网站3,讲得挺清晰。
1.1 示意图
1.1.1 主要区域
Git工作涉及的区域主要有四个——工作树、、暂存区、本地分支、远端分支。
图中的命令标示的只是常规使用方式,Git的命令通过附带参数的不同可以达到不同的效果,同一个效果也可以使用不同的命令来完成,并不是绝对的。
工作树(工作区)
工作树就是当前作业正在进行的地方,所有对文件的改动都是在工作树上进行的。
官方文档里叫“Working Tree”,也有的资料里叫“Working Directory”。
暂存区(Index)
修改完成的文件可以放入暂存区,此时的文件状态变为Staged,暂存区是工作区和分支之间的过渡区域。
官方文档里叫“Index”,也有的资料里叫“Stage”。
分支(Branch)
分支是由多个commit构成的历史记录的链路,可以存在多条不同的分支。
每个分支的瞬时状态其实是指向某个特定commit的指针;
本地目录中的“.git/refs/heads/”目录以普通的文本形式保存了本地仓库下的所有分支。
远端分支(RemoteBranch)
保存在服务器端的Git仓库内的分支。
本地目录中的“.git/refs/remotes/”目录以普通的文本形式保存了远端仓库下的所有分支,也包含远端Head指针当前的位置。
1.1.2 文件状态
Git管理的文件有四种状态——Untracked、Unmodify、Modified、Staged。
图中的命令同样只是常规使用方式,命令通过附带参数可以达到不同效果,同一效果也可以使用不同命令来完成,并不绝对。
Untracked
文件未跟踪。此文件在文件夹中, 但没有加入版本跟踪, 不参与版本控制。
可通过
git add
命令变为Staged状态。
Unmodify
文件已跟踪, 但未修改。文件与版本库中完全一致。
可直接修改文件, 变为Modified状态。
使用git rm
命令可将文件移出版本跟踪, 则成为Untracked状态。
Modified
文件已修改。文件与版本库中不一致
可通过
git add
命令变为Staged状态。
使用git restore
命令可丢弃修改,返回Unmodify状态。
Staged
文件已暂存。文件放入暂存区。
执行
git commit
可将修改同步到本地库中, 执行后版本库中的文件和本地文件又变为一致, 文件为Unmodify状态。
1.2 概念
提交(commit)
提交是从暂存区向分支进行上传的操作,每一次提交就会生成一个对应的commit。
每次进行提交时,Git通过SHA-1算法生成一个40位的十六进制Hash值,该SHA1哈希值就是commit-id,这个commit-id使每次commit提交都是独一无二的;
每个commit提交还会包含它的上一次commit提交的哈希值,Git以此来构建历史记录的提交链路;
每个commit-id是由其内容决定的,数值不能被改变,要修改commit-id只能重新创建一个全新的提交。
commit内部包含tree、blob,需要进一步了解的可以参考Git中三大对象commit、tree和blob之间的关系4
Head指针(HEAD)
一个指向你正在工作的当前分支的指针。
Head指针一般都是指的本地Head指针,远端其实也有这个Head指针,但是正常情况下不会手动去更改远端Head指针的位置。
本地目录中的“.git/HEAD”文件以普通的文本形式保存了本地仓库的Head指针。
本地仓库(Repository)
通过git init
对文件夹进行初始化,Git会在对应的文件夹构建管理空间,包括配置信息,commit提交信息,以及分支状态等。
本地仓库包含了暂存区和本地分支;
本地目录中的“.git/”隐藏目录保存了Git在当前文件夹的管理信息。
远端仓库(Remote)
服务器端的Git仓库,可以向远端服务器推送代码用于开源或者团队协作。
远端仓库名默认为“origin”;
本地目录中的“.git/ORIG_HEAD”文件以普通的文本形式保存了远端仓库的Head指针对应的commit-id。
上游分支/下游分支(upstream/downstream)
某个本地分支与某个远端分支建立了追踪关系,则该远端分支成为该本地分支的上游分支;该本地分支则是该远端分支的下游分支。
upstream常作为命令中的参数被使用,例如:–set-upstream-to、–set-upstream等
标签(tag)
在某个具有特定意义的commit上做的标记,一般用来标识具有里程碑意义的各个commit,比如项目发布的各个历史版本都可以打上对应的tag来进行标记。
引用(refs)
refs
是采用更加亲切的名称来指定某个commit,而不必使用Hash值来指定对应的commit,分支名就是一种引用。
其实引用直接或者间接指向了某个commit提交;
分支名是一种引用,比如分支“master”,但它的引用全称是“refs/heads/master”;
引用的具体内容以普通的文本形式被放在“.git/refs/”路径下;
“refs/heads/”下描述了本地仓库的分支;
“refs/remotes/”下是远端仓库以及对应的分支;
“refs/tags/”描述了在commit上打上的本地仓库的标签。
相对引用(符号‘~’与符号‘^’)
可以使用相对引用来指定某个commit,相对引用的使用涉及到两个符号~
与^
,两个符号的意义如下:
~
在第一父级上追踪,可用多个~
表示向前追踪的级数,也可在~后接数字表示向前追踪的级数;
^
在父级上追踪,通过在^
后缀数字1或2可以指定追踪的父级是第一父级还是第二父级,可省略后缀数字,表示默认追踪第一父级,可用多个^
表示向前追踪的级数。
第一父级是执行合并命令时所在的分支;
第二父级是执行合并命令时被合并的分支;
~
和^
可以在同一个表达式里配合使用。
示例:
以下分支图展示了在master分支上通过git marge hotfix
命令将hotfix分支合并到master分支之上的情况,通过~
和^
可以引用到不同分支的commit。
- 对于
commit节点8
来说,它是当前的HEAD节点,而且它只有一个父级,即节点7
,同时也是它的第一父级,所以节点7
可以表示成HEAD~1
或者HEAD^1
(也可省略数字1表示为HEAD~
或HEAD^
);- 对于
commit节点7
来说,它是一个合并节点,它的第一父级是节点6
(因为是从hotfix向master合并),第二父级是节点4,所以在节点7
引用的基础上,以下形式都可以是节点6
的表示方式:HEAD~1~1
、HEAD~1^1
、HEAD^1~1
、HEAD^1^1
、HEAD~~
、HEAD^^
、HEAD~^
、HEAD^~
、HEAD~2
;- 其他commit节点以此类推……
具体引用(refspec)
Git通过“具体引用”来表示本地仓库分支到远程仓库分支的映射,refspec
本质上是一种格式。
refspec
被表示为[+]<src>:<dst>
的形式,<src>
表示本地仓库的源分支,dst
表示远程仓库的目标分支,可选的+
表示是否执行non-fast-forward(一种快速合并方式);
refspec
可用在对远端的推送、删除等操作上。
1.3 Git目录下的文件
".git/"
——根目录
".git/hooks/"
——存放一些shell脚本
".git/info/exclude"
——仓库的一些注释信息
".git/logs/"
——存放所有更新的引用记录
".git/objects/"
——存放所有的Git对象,对象的SHA1哈希值的前两位是文件夹名称,后38位作为对象文件名。
".git/refs/"
——存放了所有引用内容
".git/refs/heads/"
——本地引用
".git/refs/remotes/"
——远端引用
".git/refs/tags/"
——标签引用
".git/CHERRY_PICK_HEAD"
——使用cherry-pick命令会更新此commit
".git/COMMIT_EDITMSG"
——最新一次commit所附带的描述
".git/config"
——仓库的配置信息
".git/description"
——仓库的描述信息
".git/FETCH_HEAD"
——使用fetch命令后会更新此commit,用于组合操作中的引用暂存,例如pull会先fetch再merge
".git/HEAD"
——当前检出的commit
".git/index"
——暂存区(二进制文件)
".git/MERGE_HEAD"
——使用merge命令会更新此commit,对应合并进当前分支的commit
".git/ORIG_HEAD"
——指向操作前的Head,用于某些命令的回退
".git/packed-refs"
——存放git运行垃圾回收机制后的一些引用,用于提高性能,垃圾回收不影响正常的Git功能,refs/文件夹下的一些内容有可能会被压缩到该文件内
".git/REBASE_HEAD"
——使用rebase命令会更新此commit
要更改主分支的位置就是修改refs/heads/master指向的内容。同样地,创建一个新的分支就是把commit哈希写入新文件这样简单。这也是为何Gi相比SVN是更加轻量的重要原因。
2 操作
2.1 基本快照操作
状态查看(status)
常用形式:
// 用得最多的一个命令了
git status
暂存改动内容(add)
常用形式:
// 将“工作树”所有改动添加至“暂存区”
git add .
附加参数:
// 干跑,演示执行效果,并不实际运行(这个参数在很多命令里都可以附带,很有用)
--dry-run | -n
// 只跟踪tracked状态的文件,无视新增的文件
--update | -u
扩展:
// 针对所有改动
git add .
git add *
// 针对某个文件
git add myFile.cs
// 针对多个文件(用空格间隔)
git add myFile.cs myFile2.cs
// 针对某个文件夹(递归)
git add myFolder/
// 针对某类型文件
git add *.cs
提交改动内容(commit)
常用形式:
// 将“暂存区”所有改动提交至“当前分支”
git commit -m <msg>
配置了git的默认编辑器之后,不带-m可自动打开编辑器处理描述文字
附加参数:
// 先自动执行add操作,再commit
--all | -a
// 修改上一次的commit,会重新生成一个commitId(可以只修改message,也可以附带add到暂存区的改动)
--amend
// 演示执行效果
--dry-run
// 添加描述文字
--message=<msg> | -m <msg>
// 修改指定的commit内容,会生成一个以“fixup!”开头的描述文字的commit提交
// fixup功能需要配合附带--autosquash参数的rebase命令执行
--fixup=<commit-id>
// 修改指定的commit内容和描述文字,会生成一个以“fixup!”开头的描述文字的commit提交
--fixup=amend:<commit-id>
// 修改指定的commit描述文字,会生成一个以“fixup!”开头的描述文字的commit提交
--fixup=reword:<commit-id>
查看提交历史(log)
常用形式:
// 显示所有提交的历史记录
git log
附加参数:
// 将信息压缩在一行显示
--oneline
// 以分支图的形式显示历史信息
--graph
// 限制显示的历史记录数目
--max-count=<number> | -n <number> | -<number>
// 使用作者来过滤显示的历史记录
--author=<pattern> | --committer=<pattern>
删除文件(rm)
常用形式:
// 删除文件,并记录这次的删除操作(删除操作会记入暂存区,用于commit的时候对分支中的文件进行处理)
git rm myFile.cs
附加参数:
// 强制删除,如果文件被改动过(不管是否add到暂存区),使用普通删除都会报错,需要使用强制删除
--force | -f
// 删除暂存区文件,但保留工作区文件
--cached
比较文件(diff)
常用形式:(这个功能其实用得不多,因为在VisualStudio或VSCode等IDE环境中在合并的时候有更完善的diff功能)
// 对比文件。当工作树有改动,暂存区为空,对比工作树与最后一次commit的共同文件差异;当工作树与暂存区都有改动,对比的是工作树与暂存区的共同文件差异
git diff
附加参数:
// 对比暂存区与最后一次commit间的差异文件
--cached | --staged
还原文件(restore)
可以从“暂存区”向“工作树”还原;
也可以从“当前分支”向“暂存区”还原;
也可以从“当前分支”向“工作树”还原
常用形式:(restore
命令只修改工作树和暂存区,不会进行提交)
// 还原工作树文件,即撤销更改
git restore myFile.cs
附加参数:
// 还原工作树文件(这也是默认参数),优先以暂存区为源,如果暂存区没有,则以Head为源
--worktree | -W
// 还原暂存区文件
--staged | -S
// 指定被还原文件的来源,默认使用Head
--source=<tree> | -s <tree>
扩展:
// 针对当前目录下的所有内容
git restore .
// 针对顶层目录下的所有内容
git restore :/
// 针对某个文件
git restore myFile.cs
// 针对某类文件
git restore '*.cs'
版本回退(reset)
常用形式:(该命令通过移动本地Head指针至指定的提交上,以此进行回退)
// 默认还原工作树文件(使用相对引用,也可直接使用commit-id)
git reset --hard HEAD^
附加参数:
// 重置工作树和暂存区的改动
--hard
// 保留工作树的改动,重置暂存区的改动(默认)
--mixed
// 保留工作树和暂存区的改动
--soft
版本回滚(revert)
常用形式:(该命令不会更改之前的提交,而是进行新的提交,以此还原其它提交所做的变动,常用于已经push到远端的提交进行功能回撤)
// 回滚某个指定的commit,会自动进行新的提交,生成新的commit-id
git revert <commit-id>
附加参数:
// 回滚的改动不自动进行提交,而是放入暂存区
--no-commit | -n
// 回滚范围内所有的commit,并按照顺序依次生成多个新的提交
// 范围:从<old commit-id>(不包含)到<new commit-id>(包含)
// 新提交的顺序:先生成<new commit-id>,最后生成<old commit-id>(并不包含)
// 想要只生成单个commit,可以使用-n参数,再手动提交一次即可
git revert <old commit-id>..<new commit-id>
// 针对merge合并的分支进行回滚,相当于抛弃某个分支内的所有修改,并生成新的提交
// <parent-number>是1或者2,表示需要保留的分支
// 可以用show命令查看合并点的信息,在显示的“Merge行”可以看到commit顺序,从左到右分别为1、2
--mainline | -m <parent-number> <合并点的commit-id>
// 继续回滚操作(回滚过程中,如果有冲突,会产生暂停,需要解决完冲突)
--continue
// 跳过当前commit,继续余下的回滚操作(回滚过程中)
--skip
// 放弃回滚操作,回到操作前的状态(回滚过程中)
--abort
// 退出回滚操作,保持当前状态,不回到操作前的状态(回滚过程中)
--quit
这里要提一下在合并的分支上进行回滚操作的一些注意事项:
假设当前的情况是在feature分支(临时功能分支)上开发了一个新功能,并合并到了dev分支(主要开发分支)上,之后又在dev分支上提交了几个commit,在某一天突然发现该新功能存在Bug,需要revert掉,于是执行git revert -m 1 xxxx
生成了回滚commit提交W,如下:
回到feature分支后,进行了两次提交C和D之后,修复了新功能中的Bug,之后又向dev分支合并,如下:
此时会出现一个问题,就是A提交和B提交中的改动不会被合并到dev分支中,因为它们已经在W提交中被回滚了。
此时想要把A提交和B提交的改动一起合并到dev分支,则需要在合并前在dev分支上再做一些操作,把回滚M提交
而生成的W提交
再做一次回滚,如下:(M’是回滚W而生成的提交)
这样之前的A提交和B提交的改动就能正确地合并进dev分支了。参考revert-a-faulty-merge5
查看操作记录(reflog)
常用形式:
// 查看之前的改动记录,可以得到检索列表,在其它命令中可使用检索到的commit-id进行操作,也可以使用改动记录标识`HEAD@{n}`作为commit-id进行操作
git reflog
在Git的相关图形界面上对分支进行的操作也同样能在reflog命令里被查找到。
拣选(cherry-pick)
常用形式:(该命令用于将一个分支上的某些commit改动应用到目标分支上,而不必将整个分支的改动合并到目标分支)
// 拣选特定的commit到当前分支,会产生新的commit提交
git cherry-pick <commit-id>
附加参数:
// 将改动放入工作区,不产生新的提交
--no-commit | -n
// 针对merge合并的节点也可以拣选,但需要指定拣选的改动来自哪个父级的代码变动
// <parent-number>是1或者2,表示需要保留的分支
// 可以用show命令查看合并点的信息,在显示的“Merge行”可以看到commit顺序,从左到右分别为1、2
--mainline | -m <parent-number> <合并点的commit-id>
// 继续拣选操作(拣选过程中,如果有冲突,会产生暂停,需要解决完冲突)
--continue
// 跳过当前commit,继续余下的拣选操作(拣选过程中)
--skip
// 放弃拣选操作,回到操作前的状态(拣选过程中)
--abort
// 退出拣选操作,保持当前状态,不回到操作前的状态(拣选过程中)
--quit
扩展:
// 打开外部编辑器,编辑描述
--edit | -e
// 拣选多个commit到当前分支
git cherry-pick <commit-id1> <commit-id2>
// 拣选多个连续的commit到当前分支,不包含`commit-id1`,如果需要包含,可用`<commit-id1>^`
git cherry-pick <commit-id1>..<commit-idn>
衍合(rebase)
常用形式:(以某个commit为基点,重构之后的commit(一般针对分叉的情况),在分支图上形成一条直线,强迫症患者之友)
// 以指定的上游分支与当前分支的分叉点为基,进行重构,当前分支的所有改动会链接在上游分支的最后一个commit之后
git rebase <Branch>
附加参数:
// 在衍合操作时使用交互模式,可以在编辑器内手动对每个commit指定相应的操作(pick、drop、squash等)来完成整个衍合
--interactive | -i
// 自动按照需要的方式完成衍合操作,一般需要之前的commit中附带了--fixup、--squash、--amend等待定操作来指示自动衍合的行为,即commit中会出现以“fixup!”或“amend!”或"squash!"为开头的描述文字
--autosquash
// 将当前分支在新的基点处进行衍合
--onto <NewBase>
// 继续衍合操作(衍合过程中,如果有冲突,会产生暂停,需要解决完冲突)
--continue
// 跳过当前commit,继续余下的衍合操作(衍合过程中)
--skip
// 放弃衍合操作,回到操作前的状态(衍合过程中)
--abort
// 退出衍合操作,保持当前状态,不回到操作前的状态(衍合过程中)
--quit
关于rebase的理解:
其实rebase的本质就是把当前产生变化的commit在需要的地方重新执行一遍,以相同的信息重构基点之后的commit,重构会导致大部分commit-id的变化,以master为基进行衍合不会修改master,这意味着你的子分支的commit将在master的最后一个commit之后执行,即变成一条直线。
假设当前的情况是在feature分支(功能分支)上开发了新功能,并合并到了dev分支(主要开发分支)上,如果我们在dev分支上使用git merge feature
进行合并(merge
是合并指定分支到当前分支的操作),会产生如下分支图:
merge
可以正常地工作,但这只是一个分支的合并,试想在一个分支众多、存在交叉提交的环境下,分支图会像没有整理好的电缆线箱一样,错综复杂,很快失去可读性。但是如果使用rebase
的方式来处理,在feature分支中使用git rebase dev
,会产生如下分支图:
分支图里没有分叉,featrue分支的所有commit以dev分支头(commit 4)为基点,进行了重构,这期间feature分支上的所有commit对应的commit-id都会重新生成,目前分支的合并还未完成,还需要切换到dev分支,在dev分支上使用git merge feature
,此时的合并会自动使用fast-forward
快进合并(直接移动dev的分支头指针到feature的分支头指针位置)来完成合并,分支图最终形成一条直线,如下:
2.2 分支与合并操作
2.2.1 远端操作(remote)
添加远端仓库关联
常用形式:
// 添加远端仓库,Remote一般约定为origin,也可以自己起名,RemoteURL有以下两种形式
// ssh协议形式:git@github.com:myName/myProject.git
// http协议形式:https://github.com/myName/myProject.git
git remote add <Remote> <RemoteURL>
显示远端仓库列表信息
常用形式:
// 显示所有远端仓库的信息
git remote -v
修改远端仓库名
常用形式:
// 修改远端仓库名在本地的简称
git remote rename <OldRemote> <NewRemote>
移除远端仓库
常用形式:
// 移除简称对应的远端仓库
git remote rm <Remote>
2.2.2 分支操作(branch/switch/checkout)
因为git版本的不断发展,在新关键字的加入的同时为了兼容老版本,同一个操作会有不同的命令能够完成。
查看分支
常用形式:
// 查看所有分支
git branch -a
附加参数:
// 查看远端分支
--remotes | -r
// 查看本地分支对应的远程分支
-vv
创建分支
常用形式:
// 从当前Head创建本地分支
git branch <New-Branch>
// 从当前Head创建本地分支,并切换到新分支
git checkout -b <New-Branch>
git switch -c <New-Branch>
// 从远程分支创建本地分支
git branch --track <New-Branch> <Remote>/<RemoteBranch>
git checkout -b <New-Branch> <Remote>/<RemoteBranch>
git switch -c <New-Branch> <Remote>/<RemoteBranch>
附加参数:
// 创建分支
(git checkout) -b
(git switch) --create | -c
// 强制创建分支,分支名被占用的情况下会覆盖旧分支
(git branch) --force | -f
(git checkout) -B
(git switch)--force-create | -C
// 从远程分支创建本地分支的同时附带追踪关系
(git branch)(git checkout)(git switch) --track | -t
// 从远程分支创建本地分支的同时不附带追踪关系
(git branch)(git checkout)(git switch) --no-track
扩展:
// 从某个commit或者tag创建分支
git branch <New-Branch> <commit-id>
切换分支
常用形式:
git checkout <Branch>
git switch <Branch>
删除分支
常用形式:
// 删除本地分支,删除分支时不能停留在当前分支上,否则会报错
git branch -d <Branch>
// 删除远端分支
git push -delete origin <RemoteBranch>
附加参数:
// 创建分支
(git checkout) -b
(git switch) --create | -c
// 强制创建分支,分支名被占用的情况下会进行覆盖
(git branch) --force | -f
(git checkout) -B
(git switch)--force-create | -C
移动(重命名)分支
常用形式:
// 移动分支会附带分支上的配置和操作记录(reflog)
git branch -m <Old-Branch> <New-Branch>
附加参数:
// 强制移动分支,分支名被占用的情况下会进行覆盖
-M
复制分支
// 复制分支会附带旧分支上的配置和操作记录(reflog)到新分支
git branch -c <Old-Branch> <New-Branch>
附加参数:
// 强制复制分支,分支名被占用的情况下会进行覆盖
-C
合并分支(merge)
// 合并指定分支到当前分支
git merge <Branch>
附加参数:
// 附带描述文字
-m <msg>
// 是否执行fast-forward快进合并
--ff
--no-ff
--ff-only
// 合并时执行squash挤压操作,并将结果暂停commit提交,执行完成需要手动提交。squash会在合并时将指定分支的所有commit放入一个新生成的commit之中,新commit的作者为当前的操作者,指定分支上的历史提交记录将不会转移到合并分支,意味着历史提交记录的丢失
--squash
// 继续合并操作(合并过程中,如果有冲突,会产生暂停,需要解决完冲突)
--continue
// 放弃合并操作,回到操作前的状态(合并过程中)
--abort
// 退出合并操作,保持当前状态,不回到操作前的状态(合并过程中)
--quit
建立本地和远端的追踪关系
常用形式:
// 建立分支与远端分支的追踪关系
git branch -u <Remote>/<RemoteBranch> <Branch>
附加参数:
// 创建追踪关系,未指定<Branch>则默认当前分支
--set-upstream-to | -u
// 解除追踪关系,未指定<Branch>则默认当前分支
--unset-upstream
拉取远端分支的改动到本地分支(pull)
常用形式:
// 从建立追踪关系的远端分支拉取改动到本地分支,如果当前分支只有一个追踪分支,则本地分支名<dst>可省;如果当前分支只与一个主机存在追踪关系,则远端主机名<repository>可省
git pull <repository> <src>:<dst>
<src>:<dst>
是具体引用<refspec>
,完整形式是[+]<src>:<dst>
,可以写成<RemoteBranch>:<Branch>
,远端->本地,与push的方向相反。
关于无参数的git pull
行为,可以参考Git push与pull的默认行为6
附加参数:
// 是否执行fast-forward快进合并
--ff
--no-ff
--ff-only
// 以rebase的方式执行pull合并
--rebase | -r
推送本地分支的改动到远端分支(push)
常用形式:
// 推送当前分支到建立追踪关系的远端分支,如果当前分支只有一个追踪分支,则远端分支名<dst>可省;如果当前分支只与一个主机存在追踪关系,则远端主机名<repository>可省
git push <repository> <src>:<dst>
<src>:<dst>
是具体引用<refspec>
,完整形式是[+]<src>:<dst>
,可以写成<Branch>:<RemoteBranch>
,本地->远端。
关于无参数的git push
行为,可以参考Git push与pull的默认行为6
附加参数:
// 强制推送,使用本地分支覆盖远端分支
--force | -f
// 推送所有标签到远端
--tags
扩展:
// 直接推送(假设远端主机名origin,远端同名分支dev)
git push origin dev:refs/heads/dev
// 如果远端开始了code review,使用上面的push会产生“remote rejected”错误,需要使用`refs/for/`的形式
git push origin dev:refs/for/dev
// 推送单个标签,第二行是完整形式
git push origin <tagname>
git push origin refs/tags/tagname:refs/tags/tagname
// 删除远程标签,推送空标签到远端相当于删除标签
git push origin :refs/tags/tagname
git push origin --delete <tagname>
储藏当前工作状态(stash)
常用形式:
// 储藏当前工作树和暂存区的改动到储藏栈
git stash -m <msg>
附加参数:
// 查看储藏栈内的内容
git stash list
// 显示某个储藏的详细内容
git stash show [<stash>]
// 带push允许指定具体的文件
git stash push <myFile1> <myFile2>
// 从储藏栈内弹出储藏的场景,恢复至工作树,不带储藏记录标识[<stash>]的话,默认指定为储藏栈顶元素
git stash pop [<stash>]
// 应用储藏的场景,恢复至工作树,但不从储藏栈内弹出
git stash apply [<stash>]
// pop和apply命令中使用,用于将储藏的内容按照原有的形式分别放回工作树和暂存区,默认情况下储藏将全部放回工作树
--index
// 从储藏栈内丢弃某个储藏,不带储藏记录标识[<stash>]的话,默认指定为储藏栈顶元素
git stash drop [<stash>]
// 清空储藏栈
git stash clear
stash支持把当前工作区和暂存区保存起来然后在切换到其它分支上再pop或者apply出来
打标签(tag)
常用形式:(标签相当于一个指向对应commit的不可移动的指针,对某个commit进行标记)
// 对指定的commit进行标签标记
git tag <tagname> <commit-id>
附加参数:
// 生成附注标签,附注标签会包含日期、作者、描述文字等相关信息,并且有自己的commit-id,附注标签可以看作commit的别名,一般用于推送到远端,而普通标签一般只在本地使用
--annotate | -a
// 删除指定标签
--delete | -d
// 查看标签列表
--list | -l
// 为附注标签添加描述文字,普通标签不能带该参数
--message=<msg> | -m <msg>
扩展:
// 查看远程标签
git ls-remote --tags origin
3 使用场景
3.1 版本回退之后,又想要找回
查看之前的改动记录,得到检索列表,可使用检索到的commit-id进行操作,也可以使用改动记录标识HEAD@{n}
作为commit-id进行操作
git reflog
操作对应的commit-id或者改动标识
git reset --hard HEAD@{5}
3.1 远程分支已删除,但在本地分支查看远端依然存在
查看所有分支
git branch -a
查看远端分支情况
git remote show origin
同步已删除的远程分支在本地的索引,支持–dry-run
git remote prune origin
3.2 需要修改远程分支名称,但不想丢失历史操作记录
移动(相当于重命名)本地分支,带配置信息和操作记录(reflog)
git branch -m <Old-Branch> <New-Branch>
删除远端旧分支
git push --delete origin <Old-RemoteBranch>
推送修改后的本地分支
git push origin <New-Branch>:<New-RemoteBranch>
建立本地分支和新远端分支的追踪关系
git branch --set-upsteam-to <Remote>/<New-RemoteBranch> <New-Branch>
3.3 在功能分支上开发完成后,想要通过rebase衍合进主分支
场景一:在某个分支上(例如dev分支)直接开发,拉取远端分支(远端dev分支)代码的时候,在pull命令中附带–rebase参数进行衍合。
git pull --rebase origin dev:refs/heads/dev
场景二:在某个功能分支上(例如feature)开发,开发完成后需要向主分支(dev分支)合并,此时需要先从远端向本地更新dev分支,再对本地的feature分支进行以dev分支为基的衍合。
切换到dev分支上:
git switch dev
更新远端dev分支到本地dev分支(也使用衍合方式):
git pull --rebase origin dev:refs/heads/dev
切换回feature分支:
git switch feature
对feature分支以dev为基进行衍合:
> `git rebase dev`
再次切换到dev分支上:
git switch dev
将feature分支合并入dev分支(自动fast-forward):
git merge feature
3.4 修改某个历史commit提交(协作仓库慎用)
突然觉得之前的某次提交不够完美,需要重新修正一下,但是在那次提交之后又进行了很多次提交,需要这样操作:
查找需要修正的提交的commit-id:
git log --oneline --graph
在工作树上进行修正并将改动放入暂存区:
git add .
进行fixup方式的提交,指定需要被修正的commit的commit-id,执行完成后本地会生成一个commit,它的描述文字会以“fixup!”开头:
git commit --fixup=<commit-id>
进行对需要修正的commit的rebase
衍合操作,此处指定的commit-id是作为基点的commit,所以不能是被修正的commit本身,而应该比它更早某个commit即可,这里我们用相对引用“^”符号来指定被修正的commit的父级:
git rebase -i --autosquash <commit-id>^
4 Git工作流规范(时间不够,以后再完善这一部分……)
GitFlow工作流
待补充
分支命名规范
待补充
commit描述文字格式规范
待补充
关于pull request 和 merge request
它们其实是一个东西,都是合并请求,只是因为不同的git站点的方式不同
细节待补充
5 总结一些注意事项
Git的本质是针对commit的管理,Git的上手难度不难,只使用基本功能也能正常地进行工作,但是一些高级的功能能够更好地帮助维护者进行版本管理,需要在实践中不断熟练,以后有更深入的理解再来添加内容,下面列出一些使用时候的注意事项:
Git对大小写敏感
虽然Git对大小写敏感,但会在仓库克隆或初始化时,根据当前系统来设置是否忽略大小写,在Windows下会设置为忽略大小写。
少量多次进行commit提交
将任务划分成比较细的粒度,多次commit提交,在解决合并冲突的时候会更加轻松,cherry-pick等操作也会更加清晰。
尽量少用checkout命令,防止误操作
对分支的操作不推荐使用checkout命令,使用switch、branch可以替代分支操作方面的功能,因为checkout除了可以操作分支,它还可以操作文件,可能会因为误操作而修改工作区。
切换分支时,保证当前分支的工作区和缓存区是干净的
如果当前分支工作树和缓存区是干净的(即你在上次commit之后再没做任何改动,可用git status查看),切换到别的分支跳不会有影响。但是如果当前分支下有未完成的工作,切换到别的分支会把改动带过去,可以stash后再切换。
避免revert -m
对合并点的回滚
针对合并的commit节点进行revert回滚操作,不会在未来的分支有提示,如果未来需要把回滚的分支重新合并到主分支上,需要revert之前执行revert的那个commit节点(非合并节点),第一容易遗忘,第二还需要艰难地沿着分支树寻找到执行revert的节点。(文中有详细提到)
慎用git push -f
操作
这个操作实际上只是把问题丢给了别人(即同在一个远端仓库提交代码的小伙伴),而别人并不清楚你把毛线打了什么结,要解开这个结,花费的成本往往会成倍增长,还记得开头提到的新闻吗……
使用rebase时要注意对base的选择
如果试图通过以子分支(例如feature功能分支)为基来衍合主分支(master分支),就是在自找苦吃,除非你的仓库只有你自己在使用,否则你会花费更多的时间去修复你的错误,或者
git push -f
把问题丢给别人……
对squash功能要特别注意
带有squash参数的命令,可能会涉及到使用rebase来完成最后的操作,会导致重构,使得某些commit-id发生变化,在协作的仓库中使用需要特别注意
如果不完全明白某个操作的含义,不要随便使用
查看相关文档,正确地使用命令和参数,可以先使用–dry-run(很多命令附带这个参数)来演示执行一下,否则当你试图操作,但是并不知道自己在做什么的时候,就是恶梦的开始,不正确的使用有可能会丢失大量工作,
Sourcetree:https://www.sourcetreeapp.com ↩︎
git官方文档:https://git-scm.com/docs ↩︎
Git教程-廖雪峰的官方网站:https://www.liaoxuefeng.com/wiki/896043488029600 ↩︎
Git中三大对象commit、tree和blob之间的关系:https://blog.csdn.net/yao_94/article/details/88648468 ↩︎
revert-a-faulty-merge:https://github.com/git/git/blob/master/Documentation/howto/revert-a-faulty-merge.txt ↩︎
Git push与pull的默认行为:https://segmentfault.com/a/1190000002783245 ↩︎ ↩︎