Git操作手册

  • github
  • git
  • git命令

本文肯定不是Git的最佳的教程,它只是本人的Git操作手册,我将从一些实际问题出发,让熟悉SVN用户顺利过度到Git来(当然包括我自己了),其中会加入一些个人感受或看法,相信会对大家有些启发。另外,全部把这些操作写在一个网页里的好处是哪天忘记了怎么做,只需要到这里来<Ctrl/Command>+<F>即可,无需再点来点去找了。这里讲的功能是我自己最常用的功能,其实估计只占Git全部功能的1/10不到,太复杂的东西也记不住,不明白就google或者stackoverflow去吧。内容如有必要,后面再补充。

  1. 名称由来

  2. 对Windows用户

  3. 工作区、暂存区和容器

  4. 使用摘要而不是提交流水号

  5. 设置用户名/Email

  6. 忽略文件列表

  7. 远程的库

  8. 提交修改

  9. 删除文件

  10. 修改文件名

  11. 查看状态

  12. 查看log

  13. 比较

  14. 让某一个文件还原为过去的某一个版本

  15. 创建、使用和合并分支

  16. 变基(rebase)

  17. “匿名分支”

  18. Tag

  19. 用blame追责

  20. 还原

  21. 改变历史

  22. 合并不同容器为一个容器并保留历史

名称由来

估计很少人知道Git这个名称是怎么来的,其实这个名字是Git的作者最初定的,Git在英文中是“饭桶”的意思,有趣的是,跟中文很相似,它同时有“没用的家伙”,“讨厌的人”等意思,Git的作者说他是个自大的王八蛋,所以他的项目也要起个很王八蛋的名字。这可不是我瞎编的,不信可以自己去维基百科找找资料看。高人的品味,岂是我等凡人所能理解。

对Windows用户

果断选择安装TortoiseGit,因为我发现用cygwin去使用Git在遇到中文时总是会有些问题,调来调去很心烦,MinGW也好不去哪里,感觉Git本身就不是为Windows设计的。(不奇怪啊,其原作者就是大名鼎鼎的Linux之父Torvalds Linus)虽然TortoiseGit是图形化界面,但最好还是自己用命令行来演练一下,以便加深理解。TortoiseGit的用法不在本文讲述范围中,因为TortoiseGit太好用了,为啥不做个Mac版的呢?

上面划掉的这段并不正确,TortoiseSVN所对应的TortoiseGit并不是一个能够独立运行的客户端,它其实是依赖于Git For Windows的,Git For Windows的官方网站是:https://git-scm.com/,我写这段文字的时候它是2.14.1版本,去下载一个吧,我之前刚开始用它的时候发现它对中文的支持有些问题,但现在显然已经没什么问题了,安装Git For Windows的时候记得将Git Bash加到资源管理器右键菜单中,方便你在各个工作目录中打开Git Bash,Git Bash其实是基于一个叫“mintty”的终端仿真程序,能够让Windows运行*NIX类似的终端,大多数Linux命令都是支持的。

工作区、暂存区和容器

SVN没有暂存区,暂存区是让Git变得复杂的重要因素,但也让Git变得更加灵活。

  • 工作区(working area)就是用户的工作目录,跟别的目录没什么区别,有什么文件就是什么内容。工作区根目录下有个叫“.git”的隐藏目录(在*nix中,“.”开头的文件或目录是隐藏的),是暂存区和容器的所在地。

  • 暂存区(index)存在于.git目录下,名副其实,就是暂存,用户commit之前要先把改动放到这里来。很奇怪,Git居然起了“Index”这么个名字,从其作用上看,“暂存区”是比较恰当的,叫“索引”嘛,真怪异。

  • 容器(Repository)相当于SVN的容器了,也存在于.git目录下,不过跟SVN不同的是,容器的内容并非只有commit命令能改变,Git确实有一套能直接修改容器的方法,从而起到“改变历史”的作用,显然,这是一把双刃剑。

使用摘要而不是提交流水号

SVN有个提交流水号,从1开始,一直递增下去,Git则没有,原因是Git是分布式的,没有一个集中管理的容器,简单流水号累加必然出现冲突,所以Git使用SHA-1摘要的方式来记录“流水号”,SHA-1摘要是160位的,表示成HEX码一共有40个字符,如:6c1a24e6593678f074a2f6ee37ec42606a2545ed。这实在太长了,不太方便我们指定这么一个提交号,通常我们只需要指定前面七个字符即可,如“6c1a24e”,在同一个项目中,前七个字符是不太可能出现重复的。

设置用户名/Email

  git config --global user.name "guogangj"
  git config --global user.email "guogangj@163.com"

想看看有没有设置成功?这样:

  git config --list | grep "user"

事实上,这个配置是写在~/.gitconfig文件中的:

  [user]
  name = guogangj
  email = guogangj@163.com

忽略文件列表

有些文件不想被加入到版本控制中去,和SVN一样,Git也支持这么一个“忽略文件列表”的设置,这样执行git add filename的时候就会提示错误,git add *的时候也默认不会把这些文件放进去。

  git config --global core.excludesfile ~/.gitignore

其中~/.gitignore的内容大致如此:

  *.swp
  *.tmp
  .DS_Store

据说还支持正则表达式,我没去研究。如果实在需要把个别被忽略文件加入到版本控制中去的话可以git add -f filename这样,强制加入。

如果仅仅是想让某个工程忽略掉某些文件,而不是让全部工程去忽略的话,可以在.git同级的目录下创建.gitignore文件。

远程的库

Git使用远程托管服务是需要身份验证的,跟SVN使用简单的用户名/密码验证机制不太一样,Git通常需要你造一对公私钥,让后把公钥交给服务器,这样服务器才能识别你的身份(不需要再通过额外的用户名密码),具体怎么弄,得看你使用的是那个远程库,如果是github.com,那就到github.com去看看上面的说明,别的也大同小异。比如我使用的是开源中国的免费托管服务:

  git clone git@git.oschina.net:guogangj/GitTest.git

clone其实和SVN的checkout最像,而git checkout和svn的checkout一点都不像。

git pull能把远程的库“拉”回来,其实它是git fetch和git merge两个命令的结合;git push能把本地的库“推”到远程的库去。

提交修改

跟SVN不太像,在commit之前,得指定要commit哪些东西,用git add命令来指定,add命令其实作用就是把修改内容从工作区搬到暂存区。放入暂存区之后,再从暂存区提交到本地容器。

  git add filename1 filename2
  git commit -m "The comment…”

如果add了之后觉得后悔了,想撤销add,怎么弄?

git reset filename1

直接一个git reset则是撤销所有add,即撤销掉缓存区所有改动。

git reset

删除文件

这个也和SVN不太一样:

  rm filename1
  #默认情况下,add命令会带上“--ignore-removal”参数来忽略被删文件,所以现在需要额外带上-A参数
  git add -A filename1
  git commit -m "filename1 is deleted."

你也许有点奇怪,为什么明明是删除,却用add命令?其实Git管理的是“修改”,删除本身也是一种修改,所以需要把这个修改add到暂存区去。

更方便的一个删除文件的命令是rm

  git rm filename1

这样也不需要在git add -A一下了。

修改文件名

  git mv filename_old filename_new

在SVN中,修改文件名会导致此文件丢失修改历史,而Git则可以通过这种方式追溯历史:

  git tree --follow filename_new

查看状态

到底哪些文件没变,哪些变了,变了的文件有没放入暂存区,有没有文件被删除或新增?怎么看?用git status。它会明明白白,清清楚楚地告诉你究竟现在是怎么一种情况。但直接用git status命令的话有些凌乱,我们需要更加简洁的信息:git status -sb

由于这条命令很常用,所以我们可以在~/.gitconfig中配置一下:

  [alias]
  check = status -sb

以后直接打“git check”即可,或者你写得更简单点“git ck”,这个命令使用相当频繁。

查看log

当然是“git log”,但显示结果有些乱,不太友好,我们在~/.gitconfig中配置一下:

  [alias]
  tree = log --graph --pretty=format:'%h %cd %C(yellow)%cn %C(green)%s %C(cyan)%d' --date=short

这样只要git tree就行了,显示结果还是蛮好看的。(参考下面的一张图

查看某个文件的修改履历:

  git tree filename1

比较

比较工作区中的某个文件跟以前版本的差别:

  git diff b5624a7 filename1

比较某个文件历史提交之间的差别:

  git diff b5624a7 1433ae5 filename1

有时候可能需要用“--”隔开:

  git diff b5624a7 1433ae5 -- filename1 filename2 filename3

如果要比较两次提交之间的差别(非单个文件),那我个人觉得直接用diff在命令行界面上显示比较凌乱,最好得借助一些图形界面的工具了。最方便的图形化工具,无疑是gitk,这是Git包自带的,敲如gitk就启动了。它很方便地指定任意两次提交之间的比较,这点SourceTree(一个充满好评的Git图形化工具)貌似还没有这个功能,我还试用过一个收费的软件,叫Tower,号称最强大,但也不知道怎么弄,实在郁闷,说实在的,TortoiseGit能弄个Mac版本就好了。在gitk中要比较两次提交,可以先选中一次提交,再右击另一处提交,选择“Diff selected -> this”或者“Diff this -> selected”,选哪个,我的原则是旧的在前,新的在后,否则会出现一些理解上的障碍,如下图:

上图中,我选中的提交是比我右击的提交要旧的,所以selected放在this前。即使有了gitk,我还是觉得很不爽,因为它比命令行方式好不了多少,甚至说没什么差别,仅仅是好看了点,我最想要的一个分栏比较功能它都没有,所谓分栏比较就是一个文件放左边,一个文件放右边,而不是把两个文件的内容混在一起显示,很显然,分栏比较需要图形界面,TortoiseSVN就做得很好,Windows下还有如ExamDiff等工具,都是我很喜欢的工具,咋Mac下想找一个就那么难呢?

不过,我真找到了一个:DiffMerge,一看名称就知道它是干啥用的,我下载的是Mac版的DMG文件。

我们可以将它设置为gitk的External diff tool,在gitk的Preferences中,如下图:

然后就可以右击要比较的文件,选择External Diff,如图:

看起来不错,但还是有些不方便,因为这样的话我得先启动gitk,然后再选比较的文件,再右击……能不能在命令行方式下直接启动DiffMerge?答案是肯定的。一步步来:

1,首先确保DiffMerge的命令行脚本已经正确安装(DMG版本的得自己手动装装)

  ls /usr/bin/diffmerge

如果找不到这个脚本的话,那么将DMG镜像中的Extra中一些内容这样弄过去:

  #以root身份执行下面命令
  cp Extras/diffmerge.sh /usr/bin/diffmerge
  chmod 755 /usr/bin/diffmerge
  #增加diffmerge的帮助信息
  cp Extras/diffmerge.1 /usr/share/man/man1/diffmerge.1
  
chmod 644 /usr/share/man/man1/diffmerge.1

2,退出root,执行下面的命令,其实就是改变一下~/.gitconfig中的配置内容。

  git config --global diff.tool diffmerge
  git config --global difftool.diffmerge.cmd "/usr/bin/diffmerge \"\$LOCAL\" \"\$REMOTE\""
  git config --global merge.tool diffmerge
  git config --global mergetool.diffmerge.trustExitCode true
  git config --global mergetool.diffmerge.cmd "/usr/bin/diffmerge --merge --result=\"\$MERGED\" \"\$LOCAL\" \"\$BASE\" \"\$REMOTE\""

3,这样就搞定了,现在比较下当前的版本跟3fc7ea2这个版本:

  git difftool 3fc7ea2

你会发觉,git会一个个提示你是否执比较,如果选“Y”(默认就是“Y”,若命令带“-y”参数,那就不提示,直接全部“Y”),那么就会自动打开diffmerge执行比较,这是我们想看到的效果。另外difftool跟diff的语法是一样的,随你高兴,想咋用就咋用吧。

让某一个文件还原为过去的某个版本

要还原成先前的某个版本,当然要show一下它的内容,然后再还原。

  git tree filename1

  #显示filename1在1433ae5这次提交的内容
  git show 1433ae5 filename1
  git checkout 1433ae5 filename1

这样并不会改变当前的工作分支,仅仅是修改了filename1的文件内容而已,checkout这里的用法跟SVN的revert比较像。

创建、使用和合并分支

和SVN不同,大多数时候,Git是在分支上干活,而不是在主干上,分支上的活干好了之后,再将其合并入主干中。

  #从当前工作分支(master这个主干也可以看作一个分支哦)中分出一个叫newjob的新分支(-b参数表示新建分支,否则是切换分支)
  #这个命令相当于 git branch newjob 和 git checkout newjob,如果特别需要指定从master中做一个分支出来的话git branch newjob master
  git checkout -b newjob

  #接着干了一些修改

  #回到主干
  git checkout master

  #在主干上做一些修改

  #把newjob的修改合并到主干中
  git merge newjob

另外,查看所有分支的方法是:git branch -vva

想要切换到别的分支去干活也很简单,不要带-b参数即可:

  #切换到comment4分支
  git branch comment4

分支是临时的,这个与SVN所提倡的使用是一致的,当你这个分支开发完成了之后,要尽快将它合并回主干,然后你可以考虑删除掉分支了。

  git branch -d newjob

变基Rebase

用上面的方式生成分支,然后合并到master去的方式会在log上看到“分叉”,我们还有另一种方式可以减少这种分叉,当我们创建了分支,在分支上面commit了一些内容,然后master上也commit了一些内容,而这个时候工作在branch上的程序员想把自己的提交建立在master上的最新版本的基础上,于是就用到了rebase。

执行rebase之后:

  git checkout -b newjob

  #接着干了一些修改

  #master也发生了一些改动

  git rebase master

然后在master中合并branch,如果master后来没再有提交,就不会在log中产生分叉。

rebase有一条原则:只在自己的个人工作分支中执行rebase,千万不要在master这样的多人工作的分支上执行rebase。

“匿名分支”

这个“匿名分支”的名称是我自己起的,怎么来的?

  git checkout head^

这样就创建了一个分支(我是说相当于,“匿名分支”这个词比较好理解),但这个分支是没有名字的,你可以在这个匿名分支上执行一些改动,完之后切换回master去:

  git checkout master

这样你就会看到这么一个警告信息:“Warning: you are leaving 1 commit behind, not connected to any of your branches:……If you want to keep them by creating a new branch, this may be a good time to do so with: git branch new_branch_name 8dc240e”。git在告诉你,你很可能会丢失掉在这个匿名分支上的改动,如果你想保留这些改动,最好现在马上给这个匿名分支起个名字。

Tag

SVN中的Tag是个很有用的功能,我通常用它做个“里程碑”,Git中也有Tag:

  git tag tagname f11aa6e

这样就给f11aa6e这个提交取了个tagname,以后就可以用tagname来指代f11aa6e了,好记多了。删除tag:

  git tag -d tagname

用blame追责

SVN中有个很有用的命令,blame,可以用来追踪出了问题的某行代码是谁在什么时候改的,Git中也有同样的命令,也叫blame。

  git blame filename1

还原

最接近SVN的还原(reverse)应该是:

  git reset --hard

这样一来工作区和暂存区中的修改皆被抛弃掉。

如果仅仅要抛弃掉暂存区的修改,那么:

  git reset

改变历史

对SVN来说,如果你做了一个错误的提交,那只能用后面的修改来将它改正过来,而不能把历史的提交动作删除,正所谓历史不可改变,这个特性其实对于大多数项目来说是个不错的特性。而在Git中,历史是可以改变的!这是Git和SVN的一大不同。最最常见的需要改变历史的情况应该是这样:我刚刚commit了一下,但发现comment写错了,我想倒退回来,再重新commit一次:

  #做了一些改动

  git add .
  git commit -m "the wrong message"

  #发现注释写错了

  #工作区和暂存区都不变,将容器指针指向前一个版本
  git reset --soft HEAD^
  git commit -m "the correct message"

还可以放弃最近几轮的提交,比如:

  git reset --hard HEAD^^^

将还原至前三个版本,放弃最近的三次提交,如果你不记住最后的一次提交的摘要码的话就可能再找不回最近的这几次提交了,如果你还记得住的话,可以这样恢复回最近的提交去:

  git reset --hard 1749d74

你猜对了,其实reset最大的作用在于改变容器的HEAD指向,一般情况下不要乱用啊,否则真可能导致辛辛苦苦改动的东西彻底找不回了。

不常用功能(一):合并不同容器为一个容器并保留历史

我如今有两个容器,分别在:/Work/SegmentedTest 和 /Work/ManuallyAddParticipant 目录下,我想把它们合并为一个叫“iOSDevDemos”的容器。

  #先创建一个叫Prepare的目录,并把之前的两个容器克隆一下,做一些准备工作
  cd /Work
  mkdir Prepare
  cd Prepare
  git clone /Work/ManuallyAddParticipant/ ManuallyAddParticipant
  git clone /Work/SegmentedTest/ SegmentedTest

  #把它们的remote移除掉,以免不小心影响到原先的容器(其实这步也可省掉)
  cd ManuallyAddParticipant
  git remote rm origin
  cd ../SegmentedTest
  git remote rm origin
  cd ..

  #给它们各自创建一个子目录,并把它们的内容都移到各自的子目录去
  cd ManuallyAddParticipant
  mkdir ManuallyAddDemo
  #这步注意检查一下是不是所有的内容都成功移动至子目录ManuallyAddDemo中,需要适当灵活根据自己实际情况调整命令
  git mv *.* ManuallyAddDemo
  git commit -m "Prepare to combine"
  cd ../SegmentedTest
  mkdir SegmentedDemo
  git mv *.* SegmentedDemo
  git commit -m "Prepare to combine"
  #到此准备工作已经做好了

  #创建新的容器
  cd /work
  mkdir iOSDevDemos
  cd iOSDevDemos
  git init
  #将前面的两个准备好的容器添加到iOSDevDemos的remote列表中
  git remote add ManuallyAddDemo /Work/Prepare/ManuallyAddParticipant/
  git remote add SegmentedDemo /Work/Prepare/SegmentedTest/
  #将他们的内容pull到master分支来
  git pull ManuallyAddDemo master
  #这步会提示你写点merge的日志
  git pull SegmentedDemo master

  #收尾工作,删掉前面添加的这两个remote
  git remote rm ManuallyAddDemo
  git remote rm SegmentedDemo

总的合并示意图如此:

合并结果是这样: