Git是一个高质量的版本控制工具,这几天花了些时间学习几个比较常用的Git命令,这里记录一下,供自己日后回顾使用。因为目前还没有用到类似于搭建服务器或者多人合作建立Git,分布式Git等高级的功能,所以这些都没有涉及,但对于单机的版本控制,下面的这些内容是够用的,而且这篇记录也会随着我后来的不断学习而添加新的内容。

学习Git,最有用的工具莫过于git help [cmd]了,查看某个命令的帮助后,后台自动会打开对应命令的完整文档网页,唯一的缺点就是例子不够丰富,这个时候可以求助 Google 或者自己在机器上多实验几次。

git add

git add用于将文件添加到缓存索引中,如果这个文件之前没有被添加到跟踪列表,那么这个文件将被添加到跟踪列表并被缓存索引,等待被提交(commit)。一般情况下可以用git status来查看当前被跟踪索引的情况。显然,一个文件一个文件地添加是很麻烦的,所以 git 允许我们用git add prefix*git add folder/*来使用星号表达式支持多文件添加。显然,像编译过程中会产生很多中间文件,如果用上面的这种星号表达式,显然会把这些中间文件添加到跟踪列表,所以需要用一个名为.gitignore的文件把所有不想被Git跟踪的文件或文件夹或类似的星号表达式以行为间距加入到.gitignore文件。

git rm

另外,如果我们不小心添加了一个我们不想跟踪或添加到缓存中的的文件,有什么办法可以把这个文件从列表中删除呢?git rm可以为我们提供这样的功能,其中如果用git rm file,不仅仅会把文件从跟踪列表或缓存列表中移除,同时还会把物理文件也给删除掉,而git rm --cached file则只执行前半部分的操作,而不会把真实的文件给删除,这通常对于我们来说是更有用的。

git status

这个命令是用来显示当前的Git状态的。它会告诉我们有哪些文件还没有被添加到跟踪列表(untracked files),有哪些被跟踪但被更新导致没有添加到缓存中的(changes not staged for commit),有哪些被跟踪而且已经添加到缓存中等待提交(changes to be committed)。下面这张图可以很清晰地说明上面的三种情况,而它提示的几个命令对于我们来说也是比较有帮助的。如git reset HEAD (file)... to unstage

git commit

用于提交之前添加到缓存中的文件,一旦执行这个命令,Git历史中就会包含一次提交记录。一般都是在git add之后,等待所有当前任务完成后再提交的。有时候,我们修复了一个bug后,就可以执行这个命令将最新的修改提交掉了;但是如果我们不小心漏了一个文件没有被添加到缓存从而导致该文件的修改没有添加到提交记录中,那我们需要怎样才能把这个文件添加到提交记录呢?重新建立一个新的提交显然是可行的,不过由于这个文件与修复bug是一起的,所以希望还是把这个文件提交到刚刚的提交记录中,这个时候可以用git commit --amend,它不会重新建立一个提交,而是将这个提交包含到之前的那个提交记录中,这样,提交次数就没有发生变化了,而这次的提交SHA还是原来的那个。

git log

当我们提交了很多次记录后,怎么查看这些记录呢?git log提供了这个功能,一般情况下它会包含一个SHA,提交人以及对这次提交的描述等,这会导致每个记录信息比较繁杂,一些扩展选项可以让我们以简短的形式查看提交记录。比如,我就会经常使用git log --pretty=oneline这个选项来查看记录,当然,git log包含的选项是非常丰富的,它甚至允许我们指定显示最近一周内的提交记录,以我们指定的格式显示结果,只显示某位提交者所提交的记录等等。

git diff

用于比较差异的一个工具。可以用来比较两次提交中某个文件的差异,也可以比较当前修改的文件与最近一次提交的仓库中对应的文件差异,还可以比较两次提交内容全部的差异。所以,这是一个非常有用的工具。

当修改了一部分代码以后,我们想了解某个文件与提交的文件差异,可以使用git diff file;如果想比较现在所有修改了的文件与提交文件的差异,直接用git diff就可以了。如果要比较以前某两次提交记录的差异,使用git diff SHA-1 [file-1] SHA-2 [file-2]就可以了,其中如果[]中的内容省略的话,比较的就是整体的差异,即每个文件的差异都会被显示出来。

也许你只是想比较已经添加到缓存区中的文件与最新的仓库文件的区别,那就可以在diff后加--cached参数。

前面说了那么多怎么提交,那么如果我今天提交了很多次记录,但我想回到今天提交的第一次记录所在的那个状态,要怎么解决呢?那就需要好好看看git revertgit reset这两个命令了。

git revert

git revert表示撤销某次操作,此次撤销操作之前的提交全都会保留起来,而这次撤销操作将会单独地当作一个新的提交操作记录在操作列表里。如果想要指定某个文件恢复到某个状态,那么用git checkout commit file来完成,注意,这个操作完成后,恢复的文件将会被加入到缓存等待提交,如果不想它加入到缓存,可以用git reset HEAD把缓存重新清除到最初状态,这个时候文件就是changed but not staged而等待添加了。

git reset

不同于git revert,这个命令主要用来将当前的代码库重新设定为某次提交所在的状态。而这个提交之后的所有提交记录都会丢失。这个命令的好处就在于如果有一个错误的提交是无效的,我们可以用git reset HEAD^回到之前一个提交状态;如果有好几个提交都错了,那么就直接重置到这几个提交操作之前。

注意,这里有个选项--soft|mixed|hard,其中soft表示只将提交记录表重置为目标提交状态,而真实的物理文件不作修改,即保持为未被跟踪或更新未被加入缓存,这样我们就可以查看当前文件与我们回到的提交状态时文件有哪些差异了。而如果选项为hard,则物理文件也全部进行了修改为缓存中所在的状态了。有时候,这个操作会比较危险,因为它无法回到后面的提交状态。

git branch

Git与其它版本控制工具最大的差异(或者说相对于其它的工具最大的优点)就是其对分支的优异支持。分支是什么?当多人合作或多个模块同时进行开发的时候,我们不希望它们之间相互干扰。可以分别从主分支(master)中衍生出多个从分支,然后各分支(假设分别为A,B)由于是不同的模块,因此可以独立进行开发,当某个从分支A的模块完成后,可以将该A分支与主分支(master)合并。而其它的模块继续开发,等到完成后再与主分支合并。另外,如果B分支想要把A已经完成的功能添加进来,只需要将master分支与B合并,这样B分支就包含了它自身修改的那部分内容,也包含了A已经完成的部分了。另外,如果B修改了A所涉及的那部分内容而引起合并冲突,这需要我们手动地去把这些冲突给解决掉(在逻辑上,这是非常合理的,如果A删除了某个文件中的某一行,而B添加了那一行,那到底谁对谁错呢,计算机当然无法判断,就得交给人来做这件事情了)。所以说,分支对于版本控制工具来说,是一个极其重要的功能。

下面列举的是常用的一些关于分支的命令,注意,这里没有涉及到关于远程分支的概念,也就没有所谓的rebasedpull等命令了。

git checkout -b branch-name   // 新建一个包含与当前分支内容相同的分支并切换到新建分支
git branch -v                 // 显示所有分支及其最后一次提交信息的一个列表
git checkout branch-name      // 切换到一个分支
git branch branch-name        // 新建一个包含与当前分支内容相同的分支,命令1等价于命令3加4
git branch -d branch-name     // 删除分支,如果这个分支还有没有提交的修改内容,将会提示删除失败。
git merge branch-name         // 合并某一分支到当前分支

一旦我们处于某一分支下,我们就可以对这个分支里的内容进行修改了,而且这些修改对其它的分支是不影响的。

所以,一般情况下,可以维护一个develop分支,日常的开发可以在这个分支中进行,而master主分支中用来维护稳定的代码。一旦develop中某个模块已经稳定,可以将它合并到master分支中等待发布。

git remote

大部分情况下,我们会把代码放在Githubbitbucket上,为了参与到别人已经建立好的工程中,我们需要先把代码仓库下载到本地(注意,Git是一个分布式的版本控制系统,因此所有的操作可以离线,这就需要本地保存整个的代码仓库了)。这时可以使用git clone git-url.git,具体第三个参数的url地址,网站上会有说明,而如果是clone本地的一个代码仓库,则会发现git-url.git正是本地代码仓库的地址(有一个.git文件夹)。也有一种情况是,我们本地的代码仓库建立好了,然后在远程也新建了一个代码仓库,如何将本地代码仓库提交到远程让远程服务器识别,并对这二个仓库作一个自动的映射呢,这时就需要用到git remote命令了。

// 与远程服务器连接
git remote add origin git-url.git
git push origin master

// 修改远程服务器
git remote set-url origin git://new.url.here

Q & A

Q:如果我们需要将一个分支A里的某一个commit合并到分支B中,需要如何操作呢?
A:我们可以使用git cherry-pick命令,假定当前分支在B中,知道A的commit唯一标识符就可以使用git cherry-pick commit-hash-id

Q:如何在git log中显示修改的文件名?
A:可以加--name-only这个参数,事实上git log -4 --name-only会显示最近的四次提交记录,包含每一次修改的文件名。

/* ---EOF--- */

UPDATE:2014.8.18