Git原理与高级使用(3)

2,060 阅读14分钟

远程版本库

相信大家对远程版本库都有所了解,并且也都有在使用类似github,gitlab或者bitbucket之类的服务,那我们这里就主要来说一下本地版本库与远程版本库交互时的一些注意点,通常在我们创建好了远程仓库并且打算将本地同步上去时会先执行git remote add origin https://github.com/test/test.git,这里其实就可以当做我们给远程版本库取了一个别名origin,其实origin这个名字是可以随便定的,当然服从规范的话我们通常不会去用其他的名字。接着我们就会执行git push -u origin master来将我们本地版本库推送到远程,这里注意我们加-u其实是建立了一个origin和master之间的关联,之后我们就只要简单执行git push,git就会自动去选择将master分支推送给远程,同样git pull也是。这时候我们可以通过执行git remote show来查看现有的远程地址,

所以其实本地的一个版本库是可以同时有多个远程地址的,例如origin2,origin3。如果想要只查看某个远程地址的信息和通本地的关联可以使用git remote show origin

这里还有一点要注意的是,有时候在提交时会出现这样一段警告

这里就涉及了git push的一个默认行为模式,在git 2.0之前默认是使用matching的,也就是它会将现在本地关联的这条分支推送到远程存在并且同名的分支,例如现在我们将master与origin关联,那么当我们使用matching模式去提交时,就会去找远程是否有一个也叫做master的分支,如果有然后就将我们本地分支推送到远程这个master分支,而在git 2.0之后就采取了一个相对保守的策略,也就是simple模式,它在push时用的远程分支会是我们执行git pull时的那个远程分支,例如说我git pull是默认是一个叫做dev的分支,那么git push时就会推送去这个dev分支,为什么说simple是更保守呢,就是因为matching其实是一种git大胆的猜测我们默认推送和拉取用的是同名的远程分支和本地分支。我们可以像图中所说使用git config去设置或改变默认行为模式。

在我们将本地版本库与远程版本库做了关联之后,每次我们调用git status就会看到这样一条信息

这里背后其实在本地我们会多了一个叫做origin/master的分支,它被用来追踪远程的master分支,每次当我们执行git pull时,其实背后是跑了两条指令git fetch与git merge,git fetch会先同步远程的master分支与本地的origin/master分支,然后再将origin/master分支merge到本地的master分支,而在我们执行git push时,也是先将本地分支与origin/master分支同步,再将本地master分支推送到远程分支

要查看远程分支时执行git branch -a即可

分支实践

分支的实践个人觉得没有所谓的最佳,其实都是要根据不同项目的情况来决定,这里就介绍一个自己平常比较多使用的方式,分为3个主要分支加一种类型的分支:

  1. develop分支 (频繁变化的一个分支,主要用于开发人员每日开发所用)
  2. test分支 (供测试和产品等人员使用的一个分支,变化不是特别频繁)
  3. master分支 (生产发布分支,变化非常不频繁的一个分支,通常会加予权限来管理)
  4. bugfix(hotfix)分支 (这个代表的是一个类型的分支,也就是当生产系统中出现了紧急的bug,用于紧 急修复的分支)

裸库

git裸库实际上就是一个没有工作区的git仓库,那它的作用是什么呢,其实就是用来做一个存放于中转的仓库,通常我们会把它放到自己的服务器上,因为我们不需要在服务器上去做文件的操作,所以其实我们只需要有.git那个文件夹的内容即可,根据前面几篇的内容我们知道其实所有版本信息都在这个.git文件夹下。创建裸库的话只要我们执行git init --bare即可

Submodule

在开发中,有时候我们可能有多个项目会依赖于一个库,那如果我们把项目放在不同的版本控制系统,那么每当我们的库有更新时我们就需要重新把最新的库拷贝过来重新部署升级到各个项目中,试想如果这个库一天有多次的改动,那项目也要跟着去修改多次就显得很没效率,于是就有了git的submodule来专门解决这种问题,git submodule可以让我们在项目中引用另外一个版本库的项目,使它在我们当前的项目中可见,并且只要另外那个版本库一旦有更新,我们要做的只是使用一条git指令然后就会自动更新最新的代码了。我们通过在项目中执行git submodule add git@github.com:desmond/git_child.git mymodule就可以将git_child这个项目以submodule的形式加入到我们当前项目的mymodule文件夹下,注意的是这个目录事先不可以存在,如果已存在的话git会报错

这时候可以看到child就成功加入到了当前项目中,并且git还会创建一个.gitmodules的文件,里面记录了submodule的信息

当child发生了更新时,我们只要在mysubmodule中执行git pull就可以获取到最新的提交信息了,那假设我们项目中有多个submodule的话那更新不就很麻烦了吗,git其实也提供了一个git submodule foreach git pull,当我们在项目根目录下执行后,git就会循环的给每个submodule都执行一次pull操作

当然在pull完之后,我们还要记得对submodule执行一次git push才会真正把本地项目中submodule的改动推送到远程版本库中

另外要注意的是,如果我们是第一次从远程版本库clone下来时,git是不会帮我们把submodule中的内容也一起获取的,这时候我们就要先执行git submodule init,再执行git submodule update --recursive,此时本地就会获取到submodule中的内容。不过git clone也提供了一个选项帮我们简化上面的操作,我们只需执行git clone git地址 --recursive就能做到上面的效果了

如果我们clone完之后进入submodule的文件夹,就会看到现在的分支会变成一个游离的状态指向submodule的最新commit,不过本质上它就是我们submodule上master的最新commit,所以我们可以直接执行git checkout master切换回去

Subtree

subtree和submodule其实解决的都是同一类问题,但当我们在使用submodule时通常是通过更新被依赖的module然后再在使用这个module的项目中去更新,但是如果反过来想做到修改项目中的module也可以更新module本身的话submodule就会出现些bug,所以我们就引入了subtree。也就是说subtree可以用来完成双向修改。所以在我们掌握了subtree之后,其实就完全可以代替原本的submodule了,git官方也是这么推荐的。

首先我们执行git remote add subtree-origin git地址在本地项目中添加module的远程地址,接着执行git subtree add --predix=subtree subtree-origin master --squash就可以在本支的subtree文件夹下加入module的master分支内容,这里squash的意思是在加入module内容时,会将module的所有commit合并成一个commit合并进本地的内容,这样在本地的提交记录中我们就只会看到一条关于module库的commit。而如果不加squash的话,就可以看到module的每一条commit都会被合并到本地

下图可以看到如果使用squash的情况下产生的commit,其实就两条,一条就是把所有的commit合并成一个,另一条就是将这条commit合并成一个subtree

当我们创建submodule时,其实那个文件夹代表的是指向module地址的一个引用,而subtree是真正的在项目中放入了module的内容,这是它与submodule不同的地方

那么此时如果module本身更新了,我们可以在项目中执行git subtree pull --prefix=subtree subtree-origin master --squash来更新项目

那如果我们要通过更新项目自身来后将修改apply到module时要怎么做呢,我们只需要执行git subtree push --prefix=subtree subtree-origin master即可,但是这里如果我们使用squash选项的话其实是会失败的,这里就牵扯到了squash这个选项的问题,当我们使用squash时其实我们等于是产生了一个新的commit,即便我们知道这个commit其实就是把三个commit合并在一起,但是在commit对象链中通过该commit是无法与module里面的commit联结上的,这就会导致我们在git pull和git push时由于没有共同的父commit对象而出现conflict的情况,需要我们手动去处理,那如果不加squash呢,那这个问题就可以解决了,因为两边其实都是拥有一样的commit对象链,但是不用squash的话,我们就会在项目本身中看到module的commit,这部分commit其实我们大多数时候是不关心的,同时如果项目中有除了subtree修改提交外还是其他文件的修改,那当我们同步推送回module本身时,也会把这些不属于module的commit给同步过去,这样就会造成两边都被污染。所以大家可以根据实际情况来选择使用哪一种,但是一个宗旨就是如果一开始就使用squash选项,那么务必要确保之后所有subtree操作都要使用squash,反之亦然。

指令

  1. 查看所有分支(包括在本地的远程关联分支)

    git branch -a,如果想显示每条分支的最后一条commit,可以执行git branch -av

  2. clone远程项目并自定义文件夹名

    git clone 远程地址 文件夹名

  3. 为分支创建一个与远程的关联

    git push --set-upstream origin 分支名,这时候关联就会建立并且在本地多了一个'origin/分支名'的branch,其实这条命令与git push -u origin master是达到一样的效果,不过git新版本推荐是使用--set-upstream的做法

  4. git拉取之后怎么创建远程的分支

    通常在我们拉取了远程仓库之后会在本地创建一个关联分支

    那在我们获取到关联分支后如何创建一个本地的与之对应的分支呢,我们可以执行git checkout -b test origin/test来创建,这样就会多出一个本地的test分支并且commit对象链与origin/test一样,另外,我们也可以使用git checkout --track origin/test来创建一个本地分支,它与前者的区别就在于它会自动用origin/后面的这个远程名字来作为本地创建的分支名

  5. 如何删除远程的一个分支

    第一种方法我们可以通过执行git push origin :test,这里其实代表的意思是我们将一个空分支推送到远程的test分支,就变相做了一个删除的动作,第二种方法比较直接,就是执行git push origin --delete test

  6. 创建一个与本地分支名不同的远程分支

    git push --set-upstream origin develop:develop2这条指令就代表在远程创建一个develop2分支并且与本地develop分支关联,但是如果这个时候我们在本地develop分支调用git push会受到这样一个错误警告

    也就是说在远程与本地分支不同名时,我们只能通过git push origin HEAD:develop2或者git push origin develop:develop2的方式来推送,当然其实这两条命令背后是一样的,因为当前HEAD指向的就是develop分支

  7. 修改远程仓库的名字

    git remote rename origin origin2,这样就把远程仓库名从origin重命名为origin2了

  8. 删除远程仓库名

    git remote rm origin

场景

  1. 如何删除submodule呢

    git并没有提供直接的指令帮助我们删除submodule,所以我们就需要先执行git rm --cached mymodule将submodule从暂存区中移除,再之心rm -rf mymodule将文件夹彻底删除,接着提交我们的修改,同时.gitmodule这个文件也没有用了,所以也可以通过上述方法将其删除,这样我们就成功的删除了submodule

  2. 怎样跟上当初fork的项目记录

    我们可以通过在本地fork的版本库中加入一个原作的远程分支,然后执行git fetch将原作的最新代码拉取下来,通常我们会把这个分支名叫做upstream,接着我们可以执行merge操作,将原作的upstream分支merge到我们当前分支,然后提交推送,就跟上最新记录了。

Q&A

  1. 使用subtree时,有什么方法可以既不会污染项目本身又可以不会经常出现需要解决冲突的情况

    这里要先说的一个概念是squash其实并不是subtree这个指令专有的,git merge同样也可以指定这个选项并且作用是相同的

    我们可以通过另外建立一个类似叫no-squash的分支然后不加squash选项更新subtree,这样就保留了module的历史记录,没有烦人的反复冲突问题,然后再将no-squash分支合并到主分支,但是这里合并时用的是squash的方式git merge --squash, 这样项目的主分支上只会体现一个commit,比直接git subtree add/pull --squash还要简洁(原本是两个commit)。

    当然这种做法也有缺点,首先新开的分支历史记录就会稍显混乱,另外就是每次新分支做subtree的操作就要记得merge一次回master,但我自己觉得还是在能够接受的范围内得。

  2. 在使用git pull时经常会出现多了一个merge commit的情况,这是为什么呢

    其实经常会出现别人推送到远程后我们又做了一些修改,这时候当我们执行git pull时,会先执行git fetch,接着执行git merge,这时候其实很多时候会因为合并产生额外的commit,这时候我们就可以通过执行git pull --rebase来解决这个问题

  3. 假设没有github,要怎么在两个host之间传修改呢

    这个算是比较冷门一点的问题,不过git也是有方法帮我们来做的,我们可以通过执行git format-patch -n -o path这里n只的是最近的n条commit,然后我们使用-o后面接一个路径,接着git就会在指定路径中创建每个commit对应的一个patch文件,接着我们可以把这些patch文件通过email等方式传给另外一个host,然后在那个host的项目中执行git am path,这里的path就是放传过来的这些文件,这样git就会帮我们更新了