Git
Git push与pull的默认行为
摘自博客:http://blog.angular.in/git-pushmo-ren-fen-zhi/
感觉写的非常好,没忍住摘了下来,怕哪天网站塌了就没有这篇技术文章了
一直以来对git push
与git pull
命令的默认行为感觉混乱,今天抽空总结下。
git push
通常对于一个本地的新建分支,例如git checkout -b develop
, 在develop分支commit了代码之后,如果直接执行git push
命令,develop分支将不会被push到远程仓库(但此时git push
操作有可能会推送一些代码到远程仓库,这取决于我们本地git config配置中的push.default
默认行为,下文将会逐一详解)。
因此我们至少需要显式指定将要推送的分支名,例如git push origin develop
,才能将本地新分支推送到远程仓库。
当我们通过显式指定分支名进行初次push操作后,本地有了新的commit,此时执行git push
命令会有什么效果呢?
如果你未曾改动过git config中的push.default
属性,根据我们使用的git不同版本(Git 2.0之前或之后),git push
通常会有两种截然不同的行为:
develop分支中本地新增的commit被push到远程仓库
push失败,并收到git如下的警告
为什么git版本不同会有两种不同的push行为?
因为在git的全局配置中,有一个push.default属性,其决定了git push
操作的默认行为。在Git 2.0之前,这个属性的默认被设为'matching',2.0之后则被更改为了'simple'。
我们可以通过git version
确定当前的git版本(如果小于2.0,更新是个更好的选择),通过git config --global push.default 'option'
改变push.default的默认行为(或者也可直接编辑~/.gitconfig文件)。
push.default 有以下几个可选值: nothing, current, upstream, simple, matching
其用途分别为:
nothing - push操作无效,除非显式指定远程分支,例如
git push origin develop
(我觉得。。。可以给那些不愿学git的同事配上此项)。current - push当前分支到远程同名分支,如果远程同名分支不存在则自动创建同名分支。
upstream - push当前分支到它的upstream分支上(这一项其实用于经常从本地分支push/pull到同一远程仓库的情景,这种模式叫做central workflow)。
simple - simple和upstream是相似的,只有一点不同,simple必须保证本地分支和它的远程 upstream分支同名,否则会拒绝push操作。
matching - push所有本地和远程两端都存在的同名分支。
因此如果我们使用了git2.0之前的版本,push.default = matching,git push后则会推送当前分支代码到远程分支,而2.0之后,push.default = simple,如果没有指定当前分支的upstream分支,就会收到上文的fatal提示。
upstream & downstream
说到这里,需要解释一下git中的upstream到底是什么:
git中存在upstream和downstream,简言之,当我们把仓库A中某分支x的代码push到仓库B分支y,此时仓库B的这个分支y就叫做A中x分支的upstream,而x则被称作y的downstream,这是一个相对关系,每一个本地分支都相对地可以有一个远程的upstream分支(注意这个upstream分支可以不同名,但通常我们都会使用同名分支作为upstream)。
初次提交本地分支,例如git push origin develop
操作,并不会定义当前本地分支的upstream分支,我们可以通过git push --set-upstream origin develop
,关联本地develop分支的upstream分支,另一个更为简洁的方式是初次push时,加入-u参数(就是--set-upstream的缩写),例如git push -u origin develop
,这个操作在push的同时会指定当前分支的upstream。
如果要取消上游分支:git branch --unset-upstream
注意push.default = current可以在远程同名分支不存在的情况下自动创建同名分支,有些时候这也是个极其方便的模式,比如初次push你可以直接输入 git push 而不必显示指定远程分支。
git pull
弄清楚git push
的默认行为后,再来看看git pull
。
当我们未指定当前分支的upstream时,通常git pull
操作会得到如下的提示:
git pull
的默认行为和git push
完全不同。当我们执行git pull
的时候,实际上是做了git fetch + git merge
操作,fetch操作将会更新本地仓库的remote tracking,也就是refs/remotes中的代码,并不会对refs/heads中本地当前的代码造成影响。
当我们进行pull的第二个行为merge时,对git来说,如果我们没有设定当前分支的upstream,它并不知道我们要合并哪个分支到当前分支,所以我们需要通过下面的代码指定当前分支的upstream:
实际上,如果我们没有指定upstream,git在merge时会访问git config中当前分支(develop)merge的默认配置,我们可以通过配置下面的内容指定某个分支的默认merge操作
或者通过command-line直接设置:
这样当我们在develop分支git pull时,如果没有指定upstream分支,git将根据我们的config文件去merge origin/develop
;如果指定了upstream分支,则会忽略config中的merge默认配置。
以上就是git push和git pull操作的全部默认行为,如有错误,欢迎斧正🙈
[1] 为什么merge = refs/heads/develop 而不是refs/remotes/develop? 因为这里merge指代的是我们想要merge的远程分支,是remote上的refs/heads/develop,文中即是origin上的refs/heads/develop,这和我们在本地直接执行git merge
是不同的(本地执行git merge origin/develop
则是直接merge refs/remotes/develop)。
Git配置
三种变量配置存储行为:
系统配置配置:
/etc/gitconfig
,查看参数--system
当前用户配置:
~/.gitconfig
,查看参数--global
当前仓库配置:
.git/config
,查看参数--local
默认下一级的配置变量会覆盖上一级的,可以使用git config
+对应存储行为参数+-l
来查看,如果没有指定存储行为那么直接获取当前能获取到的变量信息,可以加上--show-origin
参数查看当前变量配置来源文件
就行存储行为变量配置时候也非常简单,带上--global
参数之后,即可直接将变量信息写入到对应的配置文件当中去,如果没有指定只会写入当前临时变量区,因此以后就不用再去添加了 ,变量设置格式为:
查找对应的参考手册很容易:
git <command> --help
,本地HTML文件,是英文的并且比较全,如果只是想获取使用参数:git <command> -h
Git基础
我们使用git clone
命令克隆仓库的时候,执行逻辑为:从远程仓库拉取.git
文件夹,然后从中读取最新版本的文件的拷贝,拷贝到当前工作区当中来
如果需要单独指定文件名字:git clone <url> <dir-name>
文件的两种状态:
已追踪:在上一次的快照当中有它们的记录,可细分为
未修改
已修改
已放入暂存区
未追踪:在上一次的快照当中没有它们的记录,往往是新创建的
直接使用git status
来查看文件状态,会输出的比较繁琐,可以使用git status -s
命令来简短输出内容,输出的几种格式:
M
:修改过的文件,绿色的:已经加入缓冲区,红色的:未加入缓冲区??
:未追踪的文件A
:新添加到的暂存区的文件,一般是开始追踪的文件
.gitignore
忽略文件,使用标准的glob模式匹配(Shell使用的,简化了的正则表达式)
很好的一个Demo:
文件 .gitignore 的格式规范如下: • 所有空行或者以 # 开头的行都会被 Git 忽略。 • 可以使用标准的 glob 模式匹配,它会递归地应用在整个工作区中。 • 匹配模式可以以(/)开头防止递归。 • 匹配模式可以以(/)结尾指定目录。 • 要忽略指定模式以外的文件或目录,可以在模式前加上叹号(!)取反。 所谓的 glob 模式是指 shell 所使用的简化了的正则表达式。 星号(*)匹配零个或多个任意字符;[abc] 匹配 任何一个列在方括号中的字符 (这个例子要么匹配一个 a,要么匹配一个 b,要么匹配一个 c); 问号(?)只 匹配一个任意字符;如果在方括号中使用短划线分隔两个字符, 表示所有在这两个字符范围内的都可以匹配 (比如 [0-9] 表示匹配所有 0 到 9 的数字)。 使用两个星号()表示匹配任意中间目录,比如 a//z 可以 匹配 a/z 、 a/b/z 或 a/b/c/z 等。
通常使用git diff
命令查看两点:
1、哪些更新尚未暂存——git diff
2、哪些更新已经暂存并等待下次提交——git diff --staged
或者git diff -cached
默认的
git diff
的可读性往往不强:可以使用
--word-diff
的参数来格式化修改非常好用
可以直接使用git commit命令,命令输出过程当中会调用git status帮助你查看哪些文件进行了修改
git提供了git commit -a
的方式跳过暂存区,直接将所有以追踪的文件进行提交,相当于帮你自动执行add已经track的文件了
删除文件:
git rm:从工作区和暂存区当中删除,如果暂存区当中存在修改未提交,此时需要-f参数才能rm成功
rm:操作系统指令,删除工作区内容,进行add后将删除提交到缓冲区就等价于git rm了
git rm --cached:仅在暂存区当中添加文件删除信息,磁盘上仍保留
如果将删除信息写入到暂存区了并且提交了(上述三条指令都有这个作用),那么Git仓库的最新版本当中文件消失
git历史
git log查看,可以使用git log -p
查看每次提交的修改,以及使用git log -p -2
查看最近两次提交的修改,或者使用git log --stat
列出审计信息,如增加修改文件数量等等。
一个非常有用的命令:git commit --amend
amend:修正
该命令会将当前暂存区内容直接合并到Git仓库的最新版本当中,并且可以重新提交commit信息,因此如果commit info写错了可以使用该命令来完成,版本的SHA-1的值也会变
实际操作就是用一个新的提交覆盖了旧的提交,因为上面的SHA-1的值完全改变了
取消暂存的文件,在原来的git版本当中我们使用git status
命令的时候,git建议我们使用git reset HEAD <file>
去将文件从暂存区当中删除,在新版本的git当中建议使用git restore --staged <file>
,可能因为git reset是一个非常危险的命令,可能更改工作区当中的内容
想要撤销对工作区当中文件的修改,命令行当中也提示了:git restore <file>
,会回到最近的一个版本当中去
在git当中任何已经提交的东西几乎总是可以恢复的,包括被删除的分支中的提交和使用amend覆盖的提交,如果没有提交很有可能找不回来了
git fetch <remote> [<branch>]
命令相当于拉取远程仓库到本地仓库,但他不会合并或者修改你的工作区,因此git clone <remote> [<dir>]
相当于是先fetch下来到本地然后再合并到当前工作区的组合命令了,git pull <remote>
命令也是使用fetch远程仓库然后尝试合并到当前分支,如果有冲突就得我们手动去fetch然后处理了。
可以使用git remote show <origin>
查看远程分支状态
git推荐给一些重要的版本打上tag,可以使用git tag
查看打上的所有标签
默认就是查看所有的,如果使用参数
git tag -l "user*"
这时候-l就不是list,而是强调使用后面的
标签:轻量标签和附注标签
轻量标签:某个特定提交的引用
附注标签:存储在Git仓库当中的一个完整对象,有自己的SHA-1,可以被校验的
通常建议创建附注标签:git tag -a <tagname>
,通常是v1.2的形式,然后输入附加信息
使用git show <tagname>
查看具体信息
轻量标签:不带参数a即可,没有附加信息
附注标签较之于轻量标签会多这个信息:
如果想给历史版本打标签也非常简单:git tag -a <tagname> SHA-1
SHA-1的值建议使用git log --pretty=oneline
来查看
默认操作下标签是不共享的,如果想要将本地标签push到服务器上,得显式的加上:
git push <remote> <branch> <tagname>
如果需要上传所有标签:
git push <remote> <branch> --tags
这样可以把标签上传到服务器,别人从服务器上拉取的时候也可以看到标签信息
如果删除本地标签,-d参数就好了,但是远程标签不会删除,即使我们也按照上面的push标签信息过去了也不会,要使用:git push origin --delete <tagname>
来完成
git别名,可以使用一个别名代替组合命令:
因此git last
等价于git log -1
Git分支
使用分支最主要的一点就是把工作从主线当中分离开来,以免影响主线的开发
Git在add到暂存区的时候就会计算文件的校验和信息,在commit的时候会产生一个完整的提交对象,提交对象可能有一个或者多个父对象指向上一次提交。
假设我们提交了三个文件到仓库,这次提交会产生五个对象,三个blob文件(文件快照),一个树信息(记录文件快照的层次结构,通过BLOB的SHA-1的值来构造的)和一个提交对象
提交对象包含上述的所有信息,此外还包括作者信息以及提交信息等等
Git的分支本质上仅仅是指向提交对象的可变指针,只是在每次提交操作的时候会向后移动罢了
最直观的感受:创建一个testing分支:git branch testing
Git区分当前是在哪一分支的,也非常简单:
HEAD指向当前分支即可,相当于是当前分支的别名,因此在我们commit的时候,自然而然会让我们当前分支向后移动了
切换分支也非常简单:git checkout <branch>
,然后修改一下文件并进行提交,此时Git仓库状态为:
如果我们此时再切换回master分支,做了两件事儿:
使HEAD重新指向master
将工作目录恢复成master所指向的快照内容
有一个非常奇怪的事儿,如果我们修改了内容,没有提交到git仓库,就checkout分支了,无论当前这个文件在工作目录还是在暂存区,都会直接带到切换后的工作目录和暂存区当中去
⚠️:不用太过在意修改内容,如果修改了一个文件,在别的分支是没有的,是不允许切换过去的
官方给出的解答:在你这么做之前,要留意你的工作目录和暂存区里那些还没有被提交的修改, 它可能会和你即将检出的分支产生冲突从而阻止 Git 切换到该分支。 最好的方法是,在你切换分支之前,保持好一个干净的状态。
可以使用如下命令查看多分支:
切换到原分支之后Git会重置工作区内容,让工作区内容和最后一次提交时候一模一样
分支合并:当前分支git merge <branch>
合并其他分支
一般切Master然后merge其他,简单的三方合并:C2,C4,C5
大部分情况不会出现冲突,出现冲突的情况:对同一文件的同一部分(同一行)进行了不同的修改
文件会被标注成unmerged状态,并且git会手动修改文件内容成:
=======
用于文件内容分割,上半部分为HEAD的,下半部分为iss53的
修改完成之后add然后commit即可
git branch参数:
--merged 与 --no-merged 这两个有用的选项可以过滤这个列表中已经合并或尚未合并到当前分支的分支
远程追踪分支:
可以看到,这里的<remote>/<branch>
即为远程追踪分支,是最后一次网络通信之后git remote show <remote>
,git远程仓库的本地状态记录,这里的master如果没有remote信息就是本地分支了,不是远程追踪分支
获取远程分支的状态,可以选择使用:git remote show <remote>
来查看远程仓库是否有更新,或者使用git fetch <remote>
来拉取远程仓库到本地,这时候我们使用git branch -a
指令查看:
会发现有一个remotes分支,但是是红色的,本地从远程fetch之后,不会自动生成一个分支去对应远程,只有一个不可修改的remote/<remote>/<branch>
指针,如果想要基于这个分支进行工作,需要基于这个分支创建一个新的分支
$ git branch remotes remotes/gitee/master Branch 'remotes' set up to track remote branch 'master' from 'gitee'.
或者使用如下命令顺带分支切换:
git checkout -b remotes remotes/gitee/master
使用上述命令,该本地分支称为追踪分支,被追踪的远程分支称为上游分支,追踪分支每次在进行切换到的时候,会自动对标本地仓库fetch下来的远程追踪分支,看下是否为最新,或者我们可以直接在该追踪分支当中执行git pull
命令,会自动的去哪个远程节点上合并哪个远程分支
如果需要修改当前分支所追踪的上游分支,使用git branch -u <fetch-branch-info>
即可
git branch -vv
查看所有本地分支所追踪的远程分支状态已经领先【需要提交到远程】落后【需要拉取到本地】状态
建议所有的上述操作都需要先
git fetch
一下,因为git只会与本地缓存数据来判断,或者git fetch --all
直接fetch所有的远程分支
git pull等于fetch加上一个merge
git推送:git push <remote> <localbranch>:<remotebranch>
删除远程分支:git push <remote> --delete <branch>
在git当中整合来自不同分支的修改主要有两种方式:merge(合并)和rebase(变基)
除了merge来完成分支合并之外,还可以使用另一种技术,变基来达到相同的效果,核心思路:提取C4当中的补丁和修改,然后再C3的基础上重新应用一次,以达到以下的效果,需要切换到experiment分支然后git rebase master
,或者直接git rebase master experiment
:
然后这时候再merge一下:
使用变基之后,整个项目的提交历史会变得非常的整洁
如果想要向其他人维护的项目贡献代码时,在这种情况下,首先在自己的分支里进行开发(或者零时分支),然后再变基到稳定的分支或者主分支上提交到主项目,这样项目维护者只需要快进合并即可
变基是将一系列提交按照原有次序依次应用到另一分支上,而合并是把最终结果合在一起
变基存在很大的风险,需要遵从一条准则:如果提交存在于你的仓库之外,而别人可能基于这些提交进行开发,那么不要执行变基
merge和rebase唯一的区别就在于:在查看历史的时候rebase会更加清楚,因为变基之后整个修改过程在master分支上都变成线性的了,merge则会每个分支的合并都会单独展示出来。这也是使用merge和rebase的两种不同的观点,这两种工具都能达到合并分支的目的。
总的使用原则:只对尚未推送或者分享给别人的本地修改执行变基操作清理历史,从不对已推送至别处的提交执行变基操作。
在我看来,变基操作只使用到本地主题分支把,多了就别用了
变基操作和合并操作的主体不同,变基操作是将当前分支的修改rebase到其他分支上去,一般都是experiment分支执行
git rebase master
,这样执行之后会将experiment的修改加到master分支之后,然后需要将master merge上experiment⚠️:
建议在master分支下使用
git rebase experiment,这样会有这种效果
master分支自动将修改提交到experiment后面去了,这样experiment就不会被强制修改了,这样用就可以和merge操作同一,并且省去一次切换过程⚠️⚠️:完完全全不建议这样搞,假设这样一种情况,很多补丁都通过变基方式提交到master分支上,如果采用这样一种方式,,master最后一次提交将被无限后置,这与实际是非常不符合的。
而合并操作是将其他分支合并过来,通常都是master分支执行
git merge experiment
分布式Git
分布式Git下的几种开发流程:
1、集中式工作流:也就是最常见的,每个developer都有仓库的写权限,写完了直接push,类似于集中式版本管理
2、集成管理者工作流:每个开发者拥有自己仓库的写权限和其他所有人仓库的读权限,通常会有一个代表官方的仓库,你可以请求官方仓库的维护者拉取更新(Pull Request)并合并到主项目。维护者需要将你的仓库作为远程仓库添加进来然后合并进主项目当中并推送回官方仓库,通常工作流程:
1、项目维护者推送主仓库
2、贡献者克隆此仓库,做出修改
3、贡献者将自己的修改推送到自己的仓库当中去
4、贡献者给维护者发邮件,请求拉取自己的更新
5、维护者在自己本地合并贡献者仓库的代码
6、维护者将合并后的修改推回到主仓库当中去
越来越感觉变基和合并的差距不是很大了,就是对历史的保留程度不一样而已了,但为了历史程度的简单,不然每个修改就多了一个分支信息,往往都是直接变基的
3、主管与副主管工作流:是对集成管理者工作流的扩展,往往只有超大型项目才会使用,Linux内核就是这样开发出来的,副主管完成对项目当中某个特定部分的集成管理,主管则完成对总的集成管理:
1、普通开发者在自己的主题分支上工作,并根据 master 分支进行变基。 这里是主管推送的参考仓库的 master 分支。
2、副主管将普通开发者的主题分支合并到自己的 master 分支中。
3、主管将所有副主管的 master 分支并入自己的 master 分支中。
4、最后,主管将集成后的 master 分支推送到参考仓库中,以便所有其他开发者以此为基础进行变基。
可以看到,由于普通开发者自己的主题分支不会共享,所以可以变基,否则就要求去merge了,变基还有一个优势就是可以改写历史
你通常会在一个主题分支上工作一会儿,当它准备好整合时就合并到你的 master 分支。 当想要共享工作时,如果有改动的话就抓取它然后合并到你自己的 master 分支, 之后推送到服务器上的 master 分支。
之所以使用变基是因为可以很容易的让你丢弃掉主题分支,手动选择需要将哪一特性直接加到master分支当中去,如果master分支更新了,我们需要合并到远程的master分支上面去,最常见的一种做法是:
基于最新的origin/master来进行变基,这里之所以要-f推送到远程,是因为改变了myproject上原来的topicA的轨迹
git merge --squash <branch>
使用--squash
参数可以不出现分支的合并操作,只是将变更重放到该分支当中了,但是分支并没有显式的合并,呈现出来的状态如图:
仍然是分叉的状态,并且将所有的修改都直接压缩成一次等待去提交了,对主题分支使用这种方法变相等于变基然后合并成一次提交了,但Git内部是知道的,因为你可以直接将topicA删除掉
在github中可以提交fork之后创建主题分支,手动合并或者变基然后提交master,但是也可以直接pull Request那个主题分支,Git会自动帮你在评审通过后进行merge操作
但这个merge操作不一定会成功,你需要将源版本库添加为一个新的远端,抓取内容,合并该远端的内容到你的主分支当中去,修复问题并最终重新推送回你提交Pull Request使用的分支。
Pull Request会自动跟踪你的分支的Commit信息的,因此可以在后期完成提交工作
最后更新于