Git基本用法

目前版本控制系统比较流行的就是SVN和Git了,相比较而言,Git有分布式的优势,对网络依赖性更低,但SVN简单,这一条就有很强的生存能力了。用Git已经好几年了,不过很长一段时间只是在用clone pull add commit push这些类Ctrl+C/V的命令(Office中),连操作Head指针实现Ctrl+Z/Y都没怎么用,想起去年收到了Leancloud的10X程序员笔记本,里面附页还写着几行Git命令,突然觉得有些陌生了。

也只是突然想到,回忆一下,当是补上多年前未肯作的笔记了。

基本文件操作

检查文件状态

Git检查文件状态可以使用git status,可以看到已经提交的修改和未提交的修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
On branch master
Your branch is up to date with 'origin/master'.
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
modified: source/_drafts/git.md
modified: source/talks/index.md
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: source/_drafts/git.md

使用git diff可以查看尚未暂存的文件的修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@@ -1,4 +1,26 @@
...
+Git检查文件状态可以使用`git status`,可以看到已经提交的修改和未提交的修改:
+On branch master
+Your branch is up to date with 'origin/master'.
+
+Changes to be committed:
+ (use "git reset HEAD <file>..." to unstage)
+
+ modified: source/_drafts/git.md
+ modified: source/talks/index.md
+
+Changes not staged for commit:
+ (use "git add <file>..." to update what will be committed)
+ (use "git checkout -- <file>..." to discard changes in working directory)
+
+ modified: source/_drafts/git.md
+
+使用`git diff`可以勘察尚未暂存的文件的修改:
\ No newline at end of file

另外加上--cached或者--staged(新版支持)参数,可以直接查看已暂存的和上次提交时的差异。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
git diff --staged
diff --git a/source/_drafts/git.md b/source/_drafts/git.md
index b220f55b..ce996f92 100644
--- a/source/_drafts/git.md
+++ b/source/_drafts/git.md
@@ -1 +1,4 @@
-title: Git用法
+title: Git基本用法
+
+目前版本控制系统比较流行的就是SVN和Git了,相比较而言,Git有分布式的优势,对网络依赖性更低,但SVN简单,这一条就有很强的生存能力了。用Git已经好几年了,不过很长一段时间只是在用`clone pull add commit push`这些类`Ctrl+C/V`的命令(Office中),连操作Head
指针实现`Ctrl+Z/Y`都没怎么用,想起去年收到了Leancloud的`10X`程序员笔记本,里面附页还写着几行Git命令
+#
\ No newline at end of file
diff --git a/source/talks/index.md b/source/talks/index.md
index d13db982..63515f47 100644

基本文件操作

除去系统自带的mv或者rm命令,Git也有自己的git mvgit rm命令,在Git仓库中,后者不仅仅是对文件做了前者的操作,也在工作目录中做了前者的操作。
git rm在删除文件后,也从跟踪文件清单中删除了该文件(使用--cached只是从暂存区中删除,使用-f同时也删除文件),以后不会再跟踪该文件,而rm命令的操作记录依然会被记录在跟踪文件清单中。
一个简单的例子,先创建一个文件:

1
touch test

此时未放入暂存区,直接删除就可以,Git也不会记录,但是如果Git已经跟踪了该文件,则直接删除状态为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
git add test
rm test
git status
On branch master
Your branch is up to date with 'origin/master'.
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
modified: source/_drafts/git.md
new file: test
Changes not staged for commit:
(use "git add/rm <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: source/_drafts/git.md
deleted: test

如果使用git rm test,可以看到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
git rm test
git status
On branch master
Your branch is up to date with 'origin/master'.
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
modified: source/_drafts/git.md
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: source/_drafts/git.md

可以看到,test文件的记录已经被删除了。
同样,git mv也是一样的类型,git mv file1 file2相当于:

1
2
3
mv file1 file2
git rm file1
git add file2

查看提交历史

查看每次的提交历史可以直接使用git log,可以看到每次的提交记录。另外,加上-p参数可以展开每次提交的内容差异,加上-{d}可以指定显示最近次数的差异,如-2显示最近两次提交的差异。加上--since或者--until可以限制时间查询,如可以用git long --since=2.weeks显示最近两周的修改。加上--word-diff可以进行单词层面的对比,加上--graphASCII图形表示的分支合并历史。如果只想看每次提交的简略信息,可以加上-stat参数。另外,可以使用--pretty指定展示提交历史的格式,如用oneline将每个提交放在一行显示(--pretty常用参数有oneline,short,full,fuller和format(后跟指定格式))。

撤销操作

仅修改提交信息

如果提交信息写错了,或者有些文件漏掉了未添加到暂存区,可以使用amend指令重新提交:

1
2
3
git commit -m "add test"
git add test
git commit --amend

这样就完成了提交信息的修改。

取消暂存区文件

如果想取消暂存区的某个文件的暂存,有两种方法。一是上面的git rm --cached直接将文件从暂存区中删除,实际文件不受影响。另外一个是HEAD指针的操作。
HEAD可以理解为指向当前分支的指针,指向该分支最近一次的调用,操作HEAD指针即可实现版本回退等操作。
这里直接使用reset命令,将某个文件重置到最近一次提交时的状态:

1
git reset HEAD test

因为上次test未暂存,所以相当于从暂存区中取消该文件。

撤销对文件的修改

使用git checkout -- file可以撤销上次提交以来,对某个文件的所有修改,本质上是拷贝了上次提交时的该文件来覆盖它,因此对该文件做的任何修改都会消失。该命令需要谨慎使用,最好的方式是通过分支的保存进度来恢复。
Git中所有已经提交的东西基本上都是可以恢复的,但未暂存的就不属于Git恢复的范畴了。

远程仓库

Git主要是在本地修改好了再推送到远程仓库,实际上对远程仓库的操作比较少,就一些基本的推拉行为。

  1. 查看远程仓库。
    直接使用git remote即可查看当前的远程仓库,加上-v选项可以以详细模式查看。
  2. 添加远程仓库。
    直接使用git remote add <shortname> <url>,将仓库名和地址添加即可。
  3. 从远程仓库抓取数据。
    有两种需求,一种是只从远程仓库拉取数据,但并不合并到当前分支,可以使用git fetch <remotename>命令。
    另外,使用git clone获取的远程仓库会自动归于origin名下。
    另一种,需求是自动抓取并合并到当前分支,可以使用git pull命令。
  4. 推送数据到远程仓库。
    基本操作,git push <remotename> <branch>
  5. 查看远程仓库信息。

    1
    git remote show <remote-name>
  6. 远程仓库的删除和重命名。

  • 删除远程仓库: git remote rm <remotename>
  • 重命名远程仓库: git remote rename <orignname> <newname>

标签

Git可以给历史中的某个提交打上标签,以示其重要性,如v1.0等。

列出标签

列出已有标签,可以直接使用git tag命令,加上-l参数可以过滤选项。如

1
git tag -l 'v1.0.1*'

创建标签

标签分为轻量标签和附注标签,轻量标签如其名轻量,只是一个特定提交的引用,本质上是将提交校验和存储到一个文件中,没有保存其他任何信息,因此创建也比较简单。附注标签则是Git数据库中的一个完整对象,是可以被校验的。附注标签通常包含打标签者的姓名、邮件地址、日期、标签信息等,并可以使用GPG(GNU Privacy Guard)签名及验证。

  • 创建附注标签: 最简单的方式是使用tag-a选项:
1
git tag -a v1.1 -m "new test version"

查看标签:

1
2
3
git tag
v1.0
v1.1

其中,-m是存储在标签中的信息,是必填内容。使用git show也可以看到标签信息与对应的提交信息。

  • 创建轻量标签: 轻量标签的创建不需要任何选项,直接提供标签名字即可。
1
git tag v1.11

查看标签:

1
2
3
4
git tag
v1.0
v1.1
v1.11

此时用git show只能看到标签的提交信息,没有额外信息。

后期上标签

也可以对过去的提交上标签,使用git log --pretty=oneline时可以看到每次提交的校验和,如某次校验和是e0c29751bf13be3df3b5030cc589685752bd9fb6,则可以通过该校验和给该次提交打上标签:

1
git tag -a v0.8 e0c2975

实际只需要部分校验和即可。

分享标签

通常情况,git push并不会将标签推送到服务器上,需要通过显示命令才能分享标签到远程仓库。

1
git push origin <tagname>

如果要一次性推送所有本地新增标签到服务器上,则可以使用--tags参数:

1
git push origin --tags

删除标签

删除本地仓库的标签,可以使用:

1
git tag -d <tagname>

如果要同时删除远程标签,则需要使用git push <remotename> :refs/tags/<tagname>来更新远程仓库标签。

标签检出

可以使用git checkout命令查看某个标签指向的文件版本。但会使仓库处于头指针分离(“detacthed HEAD”)的状态:在”头指针分离“状态下,如果做了某些更改然后提交他们,标签不会发生变化,但新的提交不属于任何分支,也无法访问,除非确切的提交哈希。所以如果要进行更改,通常需要创建一个新分支:

1
2
git checkout -b newversion v1.12
Switched to a new branch 'newversion'

如果继续对newversion分支做改动,该分支的提交指针会继续向前移动,就不是原来的v1.12标签了。

分支

Git好用很大原因是其极具优势的分支模型,使得分支处理方式更为轻量。
在使用git commit新建一个提交对象前,Git会先计算每一个子目录的校验和,然后在Git仓库将这些目录保存为一个Tree对象,然后就可以创造一个提交对象,并包含了指向这个Tree对象的指针。Git使用blob类型的对象存储此次保存的快照。
关于Git的树结构,可以用Git官方仓库中的一张图说明:

这是首次提交后的结构图,此时Git仓库中有五个对象(五个校验和),最右侧的是三个存储文件快照的blob对象,中间是记录目录结构和blob对象索引的树对象,最左侧是包含指向书对象的指针和所有提交信息的提交对象。
此时因为是第一次提交,相当于祖先提交,提交对象中没有父对象,但之后的所有提交对象中,都会多一个父对象指针,指向上次提交。

Git分支在本质上是一个指向最新提交对象的指针,每次提交操作之后,指针都会更新到最新提交。

分支就是某个提交对象往回看的历史。

使用git branch可以列出所有的分支,加上--merged--no-merged可以显示已合并或未合并的分支。

分支创建

Git使用master作为默认的分支名,如果要创建分支,可以使用branch选项。

1
git branch <branchname>

但此时只是新建了一个分支,并未将当前工作分支切换过去。Git确定当前工作的分支是使用HEAD指针,HEAD指针指向哪个分支,当前就在哪个分支工作。

也可以使用git log -decorate命令查看各个分支当前所指的对象。

分支切换

切换分支即修改HEAD指针指向,可以使用chenkout命令实现。

1
git checkout <branchname>

在每次提交后,HEAD指针会随着当前分支一起向前移动以保证以后分支能正确切换回来。
或者直接使用命令:

1
git checkout -b <branchname>

可以在新建分支的同时切换到该分支,-b可以理解为branch,相当于:

1
2
git branch <branchname>
git checkout <branchname>

分支合并

在某个分支上进行操作,使得该分支指针向前移动后,如果要将该分支合并到其他分支,则可以切换到其他分支进行merge操作:

1
git merge <branchname>

当两个分支没有需要解决的分歧时,可以直接合并。

删除分支

当分支不再使用时,可以删除:

1
git branch -d <branchname>

对于未合并的分支,直接删除会失败,可以使用-D强制删除。

冲突合并

如果合并的两个分支,并不是直接祖先关系,两个分支在其共同祖先分支上都做了修改,如果修改没有冲突,如修改的都是不同的文件,则Git会自动新建一个提交,将共同祖先分支以及两个要合并的分支共同合并建立一个新的提交。此时Git会自行决定选取哪个提交作为最优的共同祖先。
但是如果两个不同分支都对同一个文件做了修改,在合并时就会引起冲突,因为Git不知道到底该对这个文件做如何操作。此时Git会先暂停下来,等待用户解决冲突。这种情况在平时也经常会遇到,如在本地对某个远程仓库做了修改,但是远程仓库在此之前已经在另一台电脑上做了push操作,这时使用pull操作就会自动抓取并合并到当前分支,如果存在冲突,pull时就会提示哪个文件修改冲突,并等待用户解决。此时,可以使用git status查看状态。
解决冲突后可以重新使用git add将其标记为冲突已解决。

远程分支

远程引用是指向远程仓库的指针,包括分支、标签等,可以通过git ls-remote <remotename>查看远程引用的完整列表,或者通过git remote show <remote>查看远程分支的更多信息。
远程跟踪则是指向远程分支状态的引用,只有当与远程仓库通信时,它们会自动移动。用户无法手动修改其状态。
可以使用git fetch命令将远程仓库中的内容拉取到本地,同事远程跟踪会更新到新的远程分支状态。当本地与远程的工作出现分叉之后,合并到本地分支时,依然会考虑是否有冲突的问题,解决方式和其他冲突分支合并一样。

推送本地分支

使用git push将本地分支推送到远端:

1
git push origin test

等价于

1
git push origin test:test

Git会自动将test名字展开为refs/heads/test:refs/heads/test

跟踪分支

使用checkout可以实现对分支的跟踪:

1
git checkout --track origin/test

通常可以新建一个本地分支来跟踪拉取的远程分支:

1
git checkout -b sf origin/test

也可以使用-u--set-upstream-to选项来直接设置已有的本地分支来跟踪拉取的远程分支:

1
git branch -u origin/test

另外,可以使用git branch -vv命令查看设置的所有跟踪分支。

合并分支

可以使用git fetch拉取分支后再使用git merge合并到本地分支,也可以直接使用git pull拉取并合并到本地分支。但是有时候git pull会显得有些佛性,难以理解,最简单的方式是fetchmerge的组合。

删除分支

删除远程分支可以使用:

1
git push origin --delete test

或者直接将空分支推送到远端覆盖远端分支即可:

1
git push origin :<remotebranch>

变基

这个是个有趣的用法,自从有了变基,Github就变成了Gayhub (逃 stuck_out_tongue_winking_eye )。
啊呸!当然不是这个原因。
变基是一种整合分支的方法,通常整合分支有两种方法:合并和变基。
合并(merge)之前已经经常用到了,主要就是将一个分支合并到另一个上。而变基(rebase)则是将一个分支里提交的修改在另一个分支上重放一边,也就是走别人的路,让别人说去吧。
一个基本的例子如下:

1
2
git checkout branch1
git rebase branch2

此时,Git会先找到这两个分支的分叉点(即最近共同祖先),然后从分叉点开始,将branch1所经历的操作,给branch2也体验一下。然后回到branch2,进行一次快进合并:

1
2
git checkout branch2
git merge branch1

其实就这个例子来看,变基和合并没有任何区别,但这样可以保证在向远程分支推送时保持提交历史的简洁。
另外,变基可以放到其他分支进行,并不一定非得依据分化之前的分支。可以从一个特性分支里再分出一个特性分支,然后跳过前面的特性分支,将后者与主分支进行变基,可以使用--onto选项。

1
git rebash --onto master branch1 branch2

即取出branch2分支,找到branch1branch2的分离点,然后在master分支上重放其共同祖先之后的修改。
然后就可以将变基后的分支快进合并到master分支上:

1
2
git checkout master
git merge branch2

剩下的也可以将branch1合并到master中:

1
git rebase master branch1

然后快进合并master分支:

1
2
git checkout master
git merge branch1

之后就可以删除无用的分支了。

变基风险

因为人人都可以编辑,所以一旦分支中的对象提交发布到公共仓库,就千万不要对该分支进行变基,不然其他人不得不重新将手里的工作和你的提交进行整合,接下来你也要重新拉取他们的提交进行整合,引入太多不必要的麻烦。
总之用官方一句加粗的话说:

不要对在你的仓库外有副本的分支执行变基。

其他操作

别名

和Linux的alias命令一样的意思,也是方便在git中快速操作。

1
2
3
4
$ git config --global alias.co checkout
$ git config --global alias.br branch
$ git config --global alias.ci commit
$ git config --global alias.st status

设置别名后,通过 git co即可实现git checkout命令。

储藏

当不想提交现在的工作状态,又想切换到别的分支进行工作,可以先将当前状态出藏起来。储藏(Stash)可以获取工作目录的中间状态——也就是修改过的被追踪的文件和暂存的变更——并将它保存到一个未完结变更的堆栈中,随时可以重新应用。
使用git stash list可以查看当前储藏的列表。
如果之后要恢复储藏的状态,可以使用:

1
git stash apply

Git则会默认恢复最近一次的储藏,如果想应用更早的储藏,则可以通过名字指定,如:

1
git stash apply stash@{2}

此时对文件的变更被重新应用,但是被暂存的文件没有重新被暂存。可以通过运行git stash apply命令时带上一个--index的选项来告诉命令重新应用被暂存的变更。
apply选项只尝试应用储藏的工作,但储藏的栈上仍然有该储藏。可以通过运行git stash drop,加上希望移除的储藏的名字来移除该储藏,或者直接通过git stash pop来重新应用储藏并在此之后快速删除栈上的储藏。

取消储藏

如果要取消之前所应用的储藏的修改,可以通过取消该储藏的补丁达到该效果:

1
git stash show -p stash@{0} | git apply -R

如果没有指定储藏名称,则会自动选择最近的储藏:

1
git stash show -p | git apply -R

从储藏中创建分支

在储藏一个工作状态后,继续在该分支上工作,最后还原储藏的时候可能会引起合并冲突,此时可以新建一个储藏分支简化工作。

1
git stash branch <branchname>

此时Git会创建一个新的分支,检出储藏工作时的所处的提交,重新应用,如果成功,则丢弃储藏。

分享
匿名评论