1、
如果通过git add命令增加了文件进入版本控制,git并不知道这个文件是之前不存在的,还是之前已经存在但是刚刚加入版本控制的。
git只能将该文件标记为add类型,这时如果进行git reset --hard操作来清空工作区的变更,造成的结果是这些新add进来的文件会从磁盘上被全部删除掉。
并且无法再通过任何git命令还原回来。
2、
查看某文件的提交变更历史(不包含具体的变更内容) git log file_name。
查看某一个文件的提交变更历史(用patch的方式显示详细内容变化) git log --follow -p file_name。 -p=--patch ,-s=--no-patch
--follow会跟踪被修改过路径的文件
查看某一次提交的全部变更使用git show commit_id。
查看某文件最后一次提交的变更内容使用git show file_name,相当于limit 1版本的git log --follow -p file_name,也可以指定commit_id来查看某次提交的变更。
3、
git pull 默认从当前track的remote/branch(通常是origin/master)分支进行fetch和merge操作,但这不是必须的。
可以指定git pull 远程仓库/远程分支来拉取任何分支的内容到本地工作区中,也就是说fetch操作只对比文件内容,与所在分支和目标分支的无关。
这里仅仅相当于是在当前分支上用某些内容变更了工作区(虽然这些内容是以与另一个分支做patch的方式引入的)。
更进一步讲,pull中的fetch操作在网络可以权限可用的情况下是一定可以成功的,但merge操作分两种情况,若不能自动完成,
则会在冲突文件中做HEAD=====> 冲突部分 <=====end的标记,并且把该文件在工作区中标记为unmerged状态。
使用者需要手动解决冲突,但git并不关心是否真正解决了冲突(清除掉了所有git自动添加的<====>标记),只要对unmerged状态的文件采用git add操作,
git就认为使用者已经解决了冲突,并把该文件状态修正为modified,并可以提交工作区内容(unmerged状态的文件是不可提交的)。
Xcode中的pull工具其实是合并了fetch、merge、fix conflict and commit这三步操作,而在第三步中,并不一定要修改冲突点,
只要修改冲突文件的任意一个位置并进行保存,Xcode就会认为使用者解决了冲突,并通过git add的方式将该文件从unmerged标记为modified,
右下角那个用来完成操作的“pull”按钮,其实是commit的意思。因为在这一步之前,已经完成了fetch和merge,也就是说完成了git意义上的pull,
这个pull可以理解为Xcode自己定义的广义pull(包含了fix conflict and commit)。
4、
git的自动合并(auto merge)很智能,但并不一定靠谱,曾经出现过可以顺利自动合并,也没有合并出语法错误,但事实上代码逻辑被修改了的情况。
所以自动合并的文件出于严谨的目的一定要在自动合并完成之后再检查一遍合并点的正确性。
5、
git clone的时候,会自动从远程仓库下载所有remote/branch的分支信息,但只会创建master分支的跟踪信息。
也就是说,git branch -a会看到类似
*master
origrin/master
origin/test
等类似的信息,此时若要创建新的本地分支来跟踪其他的远程分支,可以使用git branch --track/--set-upstream test[新的分支名] origin/test[跟踪的分支]。
实际上,本地分支与远程分支是等价的,都是在push、pull、merge等命令作用的时候,只考虑当前分支及其upstream的上游跟踪分支,而不在乎跟踪的分支是本地分支还是远程分支。
例如:git branch --track test origin/test git branch --track test2 master
则test跟踪远程的origin/test分支,而test2跟踪本地的master分支,切换到跟踪分支以后使用git status可以看到当前分支所跟踪的分支。
也可以使用git config --list看到所有远程和分支的相关信息,例如:
remote.origin.url=https:xxx.git
remote.origin.fetch=+refs/heads/*:refs/remotes/origin/*
branch.master.remote=origin
branch.master.merge=refs/heads/master
branch.test.remote=origin
branch.test.merge=refs/heads/test
branch.test2.remote=.(这里代表跟踪的是本地仓库的分支)
branch.test2.merge=refs/heads/master
需要注意的是:--track/--set-upstream是在创建新分支的同时建立跟踪关系的时候使用,如果要对一个已经创建的分支添加跟踪,
需要使用git branch --set-upsteam-to=origin/master master[最后这个master如果缺省的话,则缺省使用当前所在分支]
通常这个写法在移除了remote又添加的时候使用,因为git remote remove origin这个操作除了会删除远程分支的引用,还会删除其关联的所有对象。
那么再次通过git remote add origin https://xxx.git添加以后,并不会像git clone时一样fetch下来远程所有的分支并默认建立master到origin/master的跟踪关系。
所以需要先git fetch origin[origin如果缺省的话,则会抓取所有remote的相关信息]来获取所有远程分支信息(注意git fetch 远程仓库只抓取分支信息,git fetch 远程仓库/分支名 才会抓取该分支下的所有文件变更信息)。
然后在通过git branch --set-upsteam-to=origin/master master来设置本地master分支对远程origin/master分支的追踪,这就还原回了删除remote之前的状态。
6、
git merge在不冲突的情况下会产生一次自动提交,若要禁用该自动提交(比如合并完成后要做其他修改,或者修改commit注释内容),
可以通过git merge --no-commit来假装自动merge失败,从而有机会来手动控制继续微调内容或自定义commit注释,此时工作区merge的变更已经存在,
且不存在unmerged需要处理冲突的文件,随时可以执行提交。
7、
checkout有两个作用,切换分支和检出文件,个人认为这是git设计上的一个小缺陷,完全可以用两个不同的命令(比如切换用switch)。
而且checkout由于有两个不同的含义,所以对于后面参数代表的意义也存在歧义的可能,若真的出现歧义冲突,则必须了解规范中优先解读哪一种才能正确理解行为。
当然从实际的代码实现层面,切换分支的真正行为也就是将HEAD指向新的分支头commit,并且把该commit下的所有内容检出到工作区,
这样来看的话,其实切换分支确实就是一个检出操作。
git checkout file_name 的作用是将暂存区的文件检出到工作区,若该文件在暂存区中不存在,则从HEAD中将文件检出到工作区。
git checkout . 检出所有文件也是同理,若某个文件在暂存区中存在,则检出暂存区中的版本,若不存在则检出HEAD指向的版本。
其实以上两条若把暂存区的默认初始值假设为与HEAD一致,则可以都理解为从暂存区检出到工作区。(reset会造成暂存区与HEAD不一致的情况)
git checkout commit_id file_name,把某个commit指向的文件版本同时检出到暂存区和工作区。
对于单个文件而言,带commit_id和不带commit_id(暂存区中)的checkout分别代表一个固化的恢复版本和临时(可覆盖)的历史版本,
就好比一个单机闯关游戏,commit_id可以比作一个章节的开始,而暂存区可以比作该章节中任何时刻都可以保存,但只能保存一个临时存档后来的会覆盖前面的临时存档。
当角色挂掉了的时候,既可以选择从最后一次存档点继续游戏,也可以选择从章节开始重新进行。但如果从章节开头重新开始,那么上次章节内部的临时存档记录则被清除。
checkout也不是必须向前检出,由于checkout的检出目标是commit_id,它不但可以向后检出,甚至可以从不在当前分支的其他分支后面的commit检出。
例如,如下操作:
git checkout -b test HEAD^^ 让test分支指向前两次提交的commit,而这是在当前分支上回退,在HEAD^^这里我原来已经产生了分叉提交。
graph结构如下:在test分支下可以使用git log --all --graph --oneline查看所有分支下的commit
* e5e4346 (origin/master, master) Merge branch 'master' of remote origin
|\
| * cf1cc27 这是branch2
* | 3626d7e 这是当前branch
|/
* 302d136 (HEAD -> test)
* a31275c
这时我在test分支上,可以使用git checkout 3626d7 .命令来向后检出同分支上的内容(实际上不算同分支,因为test就是头了,再产生commit将是第三个分支,
但由于是从master的历史节点上派生出来的分支,姑且叫做同分支)。
也可以使用git checkout cf1cc27 .命令来向后检出不同分支上的内容,也就是说检出的本质是与要检出的commit的位置无关的(在当前HEAD的后面或者不在同一分支)。
当然,上面这个操作使用.来处理所有文件其实没有什么意义,那样的话直接git checkout -b new_branch 后面的commit_id就好了。
总而言之,checkout命令的本质是找到一个tree-ish节点,然后如果命令后面还指定了paths文件列表或者.所有文件,则一定是用某个commit下index关联的paths文件列表的文件内容来覆盖工作区和暂存区,如果指定了commit_id则用该commit,没有指定的话则用HEAD。这个行为的本质是不移动HEAD指针,检出文件列表中的内容。需要注意的是如果新增了track的文件,那么checkout是无法删掉这个文件的,因为在commit_id下并没有该文件对应的index,所以该文件在checkout以后依然保留原来的状态,想删除的话或者git reset HEAD file_name到untrack状态,要么git rm -f file_name强制删除。
如果checkout命令后面有branch,但没有跟paths文件列表,则一定是切换分支命令。这个行为的本质是一定把HEAD指针移动到commit_id的位置,并且检出该commit下的所有文件到暂存区和工作区中。这个操作需要注意的地方是,如果检出的commit下的文件与当前工作区或者暂存区的文件变更有冲突(commit的所有文件与暂存区和工作区的所有文件逐个比对,任何一个相同文件有变化都算,如果同一个文件同时出现在暂存区和工作区,则忽略工作区的副本,以暂存区中的为准对比),则会error终止,会提示使用者要么commit当前工作区中的内容,要么将这些文件先stash才能checkout,也就是说切换分支操作虽然也用分支HEAD指向的commit的文件覆盖工作区和暂存区,但遇到有冲突的情况下不会强制覆盖,而是要求用户必须解决冲突后才可以checkout,最终的结果就是工作区的内容一定会保留,非工作区修改的部分检出到commit_id指定的版本。
如果checkout命令后面有commit_id,但没有跟paths文件列表,则也一定是切换分支命令,稍有不同的是这个分支被叫做游离分支(detached branch),因为除了HEAD指向该提交链的头部,没有额外的branch指向提交链,所以会导致如果在这条分支上面后续再产生提交,则要再次checkout到其他的分支之前,要么废弃掉这些实验性质的提交(其实也不算废弃,因为提交还是实际存在的,可以用commit_id找回来),要么用一个命名的branch来指向当前的HEAD,再把HEAD移动到别的branch上。除此之外,与上一条的切换分支没有任何区别。需要注意的一点是,即使branch和commit_id指向的是同一个提交,也必须用branch才能让branch和HEAD重合,否则指定commit_id再继续提交的话,git依然会认为这是一条碰巧分叉起点与branch相同的新匿名分支。
注意,git checkout后面不带任何参数则没有任何效果,但会打印出文件变化,类似于--dry-run。
8、
git reset file_name命令会重置暂存区的指定文件,将该文件放回到工作区移动之前的分类下,更详细的说就是,stage区的文件一定是通过git add命令,从untrack区、unmerged区、或者uncommit区等区域移动到了stage区。
那么对于这几个来源的文件进行reset命令操作,他们会分别回到untrack区、unmerged区、或者uncommit区。不同的un_xxx区之间是不能通过任何操作来移动文件分类的。
如果在一个新track的文件被添加到stage区以后,使用git reset --hard命令,则如第1条所述这个文件将被从磁盘删除。
但如果先通过git reset将文件还原回untrack区,然后再使用git reset --hard命令则不会删除,因为不会去比对未跟踪的文件的一致性和存在性。
git reset 的另一个用法是接commit_id,这种用法后面也可以接文件列表,不提供文件列表的话就是reset全部内容。有三个参数--soft/--mixed(缺省)/--hard。
注意:
git reset [-q] [ < tree-ish > ] [—] < paths >…
git reset [—soft | —mixed [-N] | —hard | —merge | —keep] [-q] [< commit >]
以上的reset的两种使用方式,说明当接paths文件列表的时候,就不能使用—soft | —mixed这些定义模式的参数mode。
或者更准确的说,默认是使用--mixed(参见下面对mixed的描述)。当写上--mixed的时候,会有个警告warning: --mixed with paths is deprecated。
写--soft和--hard的话无法执行直接报错。fatal: Cannot do soft reset with paths.
git reset [mode] [commit] 不指定commit的话,则以上一次commit(即HEAD)为重置基准。
该命令主要的两个行为是:第一个行为是把当前分支的头指针和HEAD指针都指向commit的位置(注意与checkout的区别,checkout只移动HEAD指针并建立新分支,
而reset是不建立新分支,只是移动当前分支的头指针)。
第二个行为是根据mode的指定是--soft/--mixed/--hard,来决定暂存区和工作区的reset情况。
--soft 将reset指定的commit与当前commit做对比,所有变化内容放入暂存区,工作区内容不变。若执行reset前某一个文件已经放入暂存区,而reset造成的变化也有这个文件,则reset的变化内容覆盖暂存区中原来的内容,但毕竟对该文件的所有修改还保留工作区,所以这么做也没什么问题,只是无法把之前人工操作的stage内容reset还原回工作区了。
--mixed 将reset指定的commit与当前commit做对比,所有变化内容放入工作区,清空暂存区。如果变化内容与工作区已经变化的文件相同,则保留工作区中已经变化的情况。换更简单的说法,就是保留工作区的原貌,只不过将工作区文件的比对变化基准由原来的HEAD变成reset指定的commit,所以工作区中原来跟HEAD相比没有变化,但HEAD与指定的commit相比有变化的文件,会被标记为modified,变化内容为两次commit的差异部分。
也就是说--soft和--mixed的唯一区别就是,工作区和reset都修改的文件,--soft能在暂存区中保留reset的痕迹并可以用他来替换工作区,而--mixed不能。
--hard 将reset指定的commit与当前commit不做对比,并清空工作区和暂存区。换更简单的说法就是,完全回到reset那个commit刚提交完成时候的状态。
--keep 模式与soft相反,保留原来的暂存区内容,而不是把reset变化的内容放入暂存区,由于工作区内容都一样,所以暂存区保留哪个版本其实大部分情况下没什么太大区别。
-- merge 还没看,不清楚。
通常情况下,reset还要保留工作区变更的可能性不大,除非是打算合并reset这部分的几个commit,重新合成一个commit(但这好像还有其他的办法)。一般用reset的目的都是废弃几个实验性质的commit,把现场还原到之前的某个commit的时候。但通常实验场景也有branch来支持,所以其实我没想明白reset在什么时候用(后悔某些commit的时候?)。
以上的reset都是用同一个分支前面的commit来测试的,并没有试过不同分支下使用reset的时候,产生同一个文件冲突的时候会有什么样的现象。