思考的轨迹

人若无名 专心练剑

Git常用命令的使用情景

| Comments

上一篇已经大概讲了一下Git中的常用命令,本文希望能够在此基础上再做些总结,以结束Git第一阶段的学习。

本文尽量指出在不同情景下如何选择合适的Git命令来达到自己的目的,当然,由于Git太过强大、灵活,有时同一问题可以有多种不同的解决方案,这里不太可能全部列出,只会选择一些自己测试可用的方法。

文中会稍微谈一些Git内部实现的细节,但不会太具体,同时这些内容大部分是基于自己在学习过程中的一些理解,所以也不能保证相关的解释一定是正确的。

如发现有误,请告之,在此谢过!

1. 工作的开始 –创建Git仓库

Git仓库的创建通常有如下几种情况:

  • 在项目的开始阶段创建Git仓库,这时,项目刚开始,使用项目中的文件都没有被纳入Git仓库中

进入项目的根目录,直接执行git init,这样就在根目录下创建了Git仓库,表现就是在根目录下多了一个隐藏的.git目录,它对应的就是git仓库。

  • 如果项目已经进行了一段时间或者是之前的项目想使用git进行版本管理,这时要创建Git仓库,也是直接到项目的根目录下,执行git init即可

  • 如果项目是多人协作,通常需要一个中心服务器来协作多人之间的工作成果

在中心服务器上通常是创建一个裸仓库(没有工作目录),方法是执行在项目根目录下执行git init --bare

  • 如果项目已经使用了git,且已经放到了中心服务器上,这时,有新人要参加该项目,他要开始工作,就需要使用git clone从中心服务器上获得该项目的git仓库

2. 为当前工作区做文件快照

我们在工作目录中进行工作,然后需要将工作区中文件的变化情况告诉git。git与其他的版本控制系统的区别在于如果工作区的文件改动过,则git会在提交前进行一次文件快照,记录当前工作目录下所有文件和文件夹的拓扑结构及内容。为了效率和节省空间,内容相同的文件在git仓库中只会有一个blob对象来保存其内容,对于变化的文件,git会在内部创建新的blob对象,而没有变化的文件则在快照中引用之前仓库中的blob对象,这样,从每一次的提交中我们就能够知道当时工作区中文件和目录的情况。而其他版本控制系统则是保存文件的差异。

在工作区,文件的状态一般有如下几种:

  • 未跟踪的文件:新增加的文件或之前没有提交到git仓库的文件且没有加到.gitignore文件中
  • 已修改未暂存:已经纳入git仓库,在工作区被修改的文件,但还没有进行文件快照
  • 已暂存等待提交: 已经纳入git仓库,在工作区被修改的文件,且已经进行文件快照
  • 已删除:已经纳入git仓库,在工作区中被删除

给工作区进行文件快照,使用git add命令,常见用法有:

  • git add . 工作区的文件变动全部被添加到git的index file中
  • git add -u 将工作区中已经纳入git仓库的文件变动添加到index file中,但新增加的文件不会被添加
  • git add file 手动一个一个地将指定的文件添加到index file中

要查看当前文件快照的内容,可以使用git ls-files --stage命令来实现,它会列出index file中文件的blob对象id和文件名。

3. 将文件快照提交到git仓库中

对工作目录进行文件快照后,其修改的文件并未真正纳入git仓库,需要使用git commit命令将其真正提交保存到git仓库中。

每一次提交,git都会要求输入相应的commit信息,这样也便于今后能够快速找到指定的版本。

如果已经对工作区进行了文件快照,则直接执行git commit -m "commit information", 这里用单引号或双引号都可以,如果这时没有加上-m选项,那么git会自动打开编辑器,让你输入提交信息,复杂的提交信息可以这样使用。

如果并没有对工作区进行文件快照,且只希望将已经纳入git管理的文件的变更提交到仓库中,这时,可以直接执行git commit -a -m 'commit information',实际上,这只是将git add 和git commit两条命令合并起来执行。

git会为每一次的提交产生一个commit对象,该commit对象指向了当前的文件快照(由tree对象和blob对象组成),并且还会指向其前一次的commit对象,以形成commit提交历史。

另外,git内部有一个HEAD始终会指向当前分支最新的一次commit对象,每一次提交,HEAD也会跟着移动。

4. 查看工作区的文件状态

要了解当前工作区中的文件有没有被修改或有没有增加或者删除文件,都可以通过git status来查看,同时还能够了解到修改的文件没有被放入到index file中或者没有被提交到git仓库中。如果当前工作区中文件的变化都已经被提交到仓库中,则此时的工作区就是处于一个clean的状态。

5. 将对外Release版本和内部开发版本分开

对于软件开发,常常会遇到要修正bug、增加新功能等需求,为了不影响正常Release出去的版本,我们往往会希望自己修改的部分最好与之前的版本分开,这时候,使用分支是个不错的选择。在git中,分支是非常强大的,并且实现的成本的也很低,这是其他版本控制系统无法比拟的。灵活使用git的分支,可以帮助我们实现比以往更好、更简单的开发管理。

git branch可以列出当前git仓库中已经存在的分支名;知道分支名,我们可以使用git checkout 分支名将工作区切换到指定的分支,这样我们就可以在这个分支上进行开发或者修正bug等工作,同时,在该分支所作的改动不会影响到其他分支。

要创建新分支,可以实现git branch 新分支名来创建,但此时并不会自动切换到新创建的分支,需要继续使用git checkout 分支名来切换。

如果想在创建分支后自动切换到该分支的话,则可以使用git checkout -b 新分支名

上述的分支创建都是默认基于当前所在分支来创建新的分支,因此,创建并切换到新分支后工作区的文件和之前是一样的。

如果想基于某个指定的版本来创建分支,则需要在创建时明确地指出希望是基于哪个起点来建立新分支,这个起点可以是某个分支的最新commit,某次commit或者指定的tag等,即git checkout -b 新分支名 [分支的起点]

在切换分支时,如果当前所在分支的工作区有文件被改动,则必须将这些改动提交到仓库或者使用git stash将当前分支状态暂时保存起来,否则分支切换就会失败。

当开发分支的功能已经完成,则可以将最新成果合并到主分支或者release分支。

要合并开发分支,需要先将分支切换到主分支或release分支,然后执行git merge 开发分支名来完成。

如果开发分支的修改是在主分支增加内容,不修改之前的内容(可以增加新文件但没有删除文件、同一文件没有修改之前的内容)时,这时合并应该是成功的(新增的部分自动合并在一起)且自动提交到git仓库中。如果同一文件在同一行的同位置有变动,则这时合并就发生的冲突(git会指示出哪个文件有冲突),自动合并过程被中止,这时就需要我们手动打开有冲突的文件(有冲突的部分可以git会在文件中加上特殊的符号标示出来,具体可以使用git help merge查看相关文档),并自己决定如何两个分支的内容。冲突解决后,需要自己提交到仓库中。

如果冲突的内容比较多时,我们可以借助git mergetool打开配置的工具来协作解决冲突。

分支合并完成后,我们可以使用git branch -d 分支名删除不需要的分支,要使该命令执行成功,应保证要被删除的分支已经被其他分支合并,否则会失败。如果要强制删除,不管该分支有没有被合并,则应该使用git branch -D 分支名,这种情况通常是用于删除那些试验失败的分支。

要查看分支,除了在Bash中使用git branch来列出已存在的分支,还可以使用gitk命令打开图形界面比较直观的查看各分支的提交历史和分支间的相互关系。

6. 误操作的撤销

在git中,各种命令的执行会影响到工作区、暂存区、git仓库三者的状态。

如果发生了误操作,想撤销这个操作,并恢复到某个希望的状态时,这时就需要使用git reset命令了。

如果工作区中的一些文件被修改,并且已经暂存到index中(还未提交),这时发现有些文件的修改还未真正完成,想撤销之前的git add并在工作区中保留这些文件之前的修改内容时,我们git reset --mixed HEAD,这时index中内容和HEAD对应的index内容一致,但工作区还是我们修改后的状态,用git status会显示这些文件的状态时已修改未暂存,注意此时提交并没有被撤销。(注意,–mixed是缺省的选项,因此git reset HEADgit reset --mixed HEAD是等同的)

对于上述情况,如果仅仅是要index中撤销某个文件,可以使用git checkout -- file来完成。

如果上述已经提交,想撤销最近一次的提交,则可以使用git reset --mixed HEAD^,这时commit和index都被恢复到HEAD之前的一次提交状态,但工作区没有改变。

如果仅是想撤销最近一次的提交,但想保留当前index和工作区的状态,则使用git reset --soft HEAD^,而git reset –soft HEAD执行后,commit、index、work tree的状态都没有改变,因此,没有实际意思。

如果想将commit、index、work tree的状态都恢复到前一个版本的状态,则可以使git reset --hard HEAD^,这时要注意的是之前的所有修改被被丢弃,因此,这条命令使用时要确保当前的修改已经被提交到仓库中或者确定要放弃这些修改。另外,从这里也可以看出,使用git reset --hard 指定的版本ID可以将指定的版本的code从仓库中取出来查看。

如果发现提交时,填写的提交信息有误,则可以使用git commit --amend来修改;另外,如果发现提交时,漏掉某些文件,并希望将这些文件也追加到上次的提交中,可以先使用git add .将这些文件先暂存起来,然后使用git commit -amend来完成提交。

7. 查看提交历史

在git中,每一个提交在git仓库中都会有相应的历史记录,这样就便于我们今后在需要的时候来查看,例如,我们想知道某个版本对应的commit对象ID,这样我们就可以恢复到这个版本来查看这个版本的状态。

通过git log可以查看所有分支所有的提交历史信息。这些信息中,主要包括commit对象id、提交人的信息和提交时间、提交时附加上的提交信息等。

在经过一段时候后,可能分支上已经有很多次提交,这样git仓库中保留的历史也很多,因此,在查看时,我们需要经过一些过滤来选出我们需要的内容。

git log -p会显示出每次提交做了哪些改动,git log -3只列出最近三次的提交历史,git log --pretty=format:"%H : %s"只列出每次提交的ID和提交信息, git log --graph会以图形化的方式显示各分支的提交历史,git log commit1..commit4列出commit1和commit4之间的提交历史(不包括commit1,但包括commit4),git log commit1...commit4列出commit1和commit4之间的提交历史(不包括commit1和commit4)。

上述的选项可以组合起来使用,如git log -5 --graph就会以图形的方式显示最近5次的提交历史。

更多的git log的选项的使用方法请使用git help log来查看。

另外,用git show可以查看指定的某次提交历史,通过git reflog可以查看HEAD曾经指向的commit对象的ID。它们的详细用法请查看其帮助文档。

通过git shortlog -s -n会显示出总的提交次数。

8. 通过commit对象来查看该版本的文件快照内容

如果我们想看看指定版本的文件快照内容,则我们需要先知道该版本的commit对象的ID。

通过git loggit reflog来找出指定版本的commit对象的ID。

通过git cat-file -t ID来确认该对象的类型:commit(提交)、tree(目录)、blob(文件)、tag(标签)。

通过git cat-file commit commit-ID来查看commit对象的内容,主要包括:tree对象、前一个commit对象、提交人信息、提交附加信息等,其中,tree对象就是文件快照的根目录。

通过commit对象知道了文件快照根目录对应的tree对象,使用git ls-tree tree-id来查看tree对象的内容,主要包括:其他tree对象、blob对象。

知道了blob对象,我们就可以通过git cat-file blob blob-id来查看文件的内容。

note:git的这些内部对象都存储在.git/object目录下,其中,每个对象用其ID前2个字符作为存储该对象的文件夹名,并在该文件夹下使用其ID剩余的38个字符作为存储该对象的文件名,这个文件就是真正存储对象内容的地方,其中的内容时被压缩过的,需要使用上述的git命令来查看。如果执行过git gc,.git/object目录下的文件会被压缩存放到.git/object/pack和.git/object/info目录下。通过find可以列出.git目录下所有目录和文件,这样就可以清楚地知道当前仓库的目录结构。

要查看当前分支的index内容,使用git ls-files --stage,它列出了文件名及对应的blob对象的ID。

git ls-files根据不同的选项可以查看到文件,如将工作目录中的某些文件误删除了,这时可以使用git ls-files -d |xargs git checkout --即可恢复这些被删除的文件。

9. 给每个release版本打上标签(里程碑)

通常情况下,每个release版本都会有一个版本号与之对应,在git中,我们可以将该版本号作为tag来代表某个正式的release版本。这样的方便之处在于,以后我们可以通过tag快速定位到指定的版本,而不需要通过在冗长的提交历史中慢慢查找。

通过git tag可以列出仓库中所有的tag,要为当前分支最近一次的提交(HEAD)创建新的tag,使用git tag [-a] 标签名 -m "附加信息",这样以后就可以用该标签名来代替当前分支的这次提交commit对象的ID。

要删除某个tag,使用git tag -d 标签名即可。

note:在想远程仓库push时,tag并不会自动被push到远程仓库中,需要自己手动去push,如git push origin v1.0.0.1

10. 与中心服务器的交互

如果我们本地机器上没有某个项目的仓库,但中心服务器上已经有项目的仓库,我们可以用git clone 中心服务器上仓库地址 本地文件夹来从中心服务器上克隆一份项目的仓库,并在工作目录中进行开发工作。

如果我们本地机器上已经有了项目的仓库,则要从服务器上抓取最新的内容,可以使用git pull或者git fetch来实现,区别是git pull会将服务器上抓回来的内容与本地分支进行合并,而git fetch则不会进行合并。

在我们本地完成工作,需要将最新的成果放到服务器上时,我们可以使用git push来实现。

上述的操作都需要事先知道服务器上项目仓库的地址,并使用git remote add保存在本地并起了个别名,这样以后就可以直接使用别名来代替服务器上项目仓库的地址。

  • 查看本地已经添加的远程仓库: git remote仅显示已添加的远程仓库名,git remote -v可以一并查看远程仓库的地址
  • 在本添加远程仓库: git remote add 远程仓库名 远程仓库地址
  • 删除本地添加的远程仓库: git remote rm 远程仓库名
  • 重命名远程仓库名: git remote rename 原名 新名
  • 克隆远程仓库到本地: git clone 远程仓库地址 [克隆到指定文件夹]
  • 从远程仓库抓取最新数据到本地但不与本地分支进行合并: git fetch 远程仓库名
  • 从远程仓库抓取最新数据并自动与本地分支进行合并: git pull 远程仓库名 本地要合并的分支名
  • 将本地仓库推送到远程仓库中: git push 远程仓库名 本地分支名
  • 查看远程仓库信息: git remote show 远程仓库名
  • 将标签推送到远程仓库: git push 远程仓库名 标签名, 默认Git是不会将标签推送到远程仓库的

11. 比较差异

在实际工作中,对比文件在不同版本中有何差异是经常发生的事情。

在git中,我们可以通过git diff来比较工作区和index、index和git仓库、工作区和git仓库、不同版本同一文件等之间的差异。

  • git diff 比较了工作区与index的差异
  • git diff HEAD 比较了工作区与仓库中最近一次的提交间的差异
  • git diff --cached 比较了index与仓库中最近一次的提交间的差异
  • git diff HEAD^ HEAD 比较了当最近的这次提交与上一次提交之间的差异
  • git diff 指定的分支 比较了当前分支的HEAD与指定分支的HEAD之间的差异
  • git diff 分支1..分支2 比较了分支1到分支2之间的变动

更复杂的比较可以使用git difftool打开配置的diff工具来进行对比,上述的选项或参数也同样适用于git difftool,如git difftool HEAD^ HEAD

12. 紧急任务支援时保存当前分支的工作状态

如果正在一个develop分支上正在开发新功能,但这时master分支(稳定版本)突然发现了bug,并需要及时修复,而develop分支此时的工作还没有完成,且不希望将之前的工作就这样提交到仓库中时,这时就可以用git stash来暂时保存这些状态到Git内部栈中,并用当前分支上一次的提交内容来恢复工作目录,然后切换到master分支进行bug修复工作,等修复完毕并提交到仓库上后,再使用git stash apply [stash@{0}]或者git stash pop将工作目录恢复到之前的状态,继续之前的工作。

同时也可以多次使用git stash将未提交的代码压入到Git栈中,但当多次使用’git stash’命令后,Git栈里将充满了未提交的代码,这时候到底要用哪个版本来恢复工作目录呢?git stash list命令可以将当前的Git栈信息打印出来,我们只需要将找到对应的版本号,例如使用git stash apply stash@{1}就可以用版本号为stash@{1}的内容来恢复工作目录。

当Git栈中所有的内容都被恢复后,可以使用git stash clear来将栈清空。

13. 不同分支都在提交时,开发分支合并主分支内容,且不影响主分支

假设master和develop是一个项目的两个分支,其中master是主分支,develop是从master而来的开发分支,如果在develop分支上提交过2次,之后又切换到master分支,做了一些修改并提交2次,这时,如果想将master分支的最新修改内容合并到develop分支,但同时也不能影响master分支时,就需要使用git rebase了,这时的上游分支为master。

1
2
3
4
5
6
7
8
9
执行git rebase master前:
              develop: 1 --> develop: 2
            /
master: 0 --> master: 1 --> master: 2

执行git rebase master后:
                                    develop: 1 --> develop: 2
                                   /
master: 0 --> master: 1 --> master: 2

14. 查看文件是何时被何人修改的

如果找到某个版本出现了问题,而之前的版本没问题,我们可以用git blame找出文件是何时被何人怎么修改的。 git blame [-L 行号1, 行号2] file

15. 备份工作区的所有文件

git archive --format=zip -o arch.zip HEADgit arch --format zip head>arch.zip

只备份了当前工作区的所有文件,不包括.git目录,会在工作目录中生成一个arch.zip文件。

16. 查找问题是在哪个版本被引入的

如果我们发现有个问题在某些版本没问题,而在有些版本有问题时,我们可以借助git bisect来帮助我们定位问题。 git bisect start git bisect good commit-id1 git bisect bad commit-id2 这时,git会按照二分法找出good版本和bad版本中间的那个提交版本,并自动将工作状态切换到那个版本,这时我们可以验证这个版本是不是有问题,如果有问题,通过git bisect bad告诉git,这时git会继续找出一个中间版本让我们来验证,直到我们找出,并通过git bisect good告诉git为止。

这样一步一步我们就可以找出引入问题的版本,最后,我们可以使用git bisect reset结束查找,git会删除查找过程中在仓库中生成的临时文件,并将状态恢复到。

17. 养成定期清理垃圾的习惯

执行git gc,git会帮助我们清除仓库中垃圾,释放一些空间,并以pack的压缩方式存储对象内容,其中,.git/refs目录中内容和.git/objects目录下的对象文件会被压缩存放到.git/objects/pack目录下,而在.git/objects/info目录下会有一个packs文件用于指向.git/objects/pack目录下的一个pack文件,而这个pack文件应该是存放压缩分支、tag等信息后的文件。

(全文完)

Comments