Git学习总结——简单易懂的教程

安装Git

Git的下载地址:Git官网下载地址

Git本地仓库和命令

配置用户

下载完Git后,右键会有一个Git Bash here的选项,点击后会弹出一个类似于命令行的窗口:

Git界面

在此输入此命令配置用户名和邮箱:

$ git config --global user.name "Your Name"
$ git config --global user.email "email@example.com"

注意,--global参数,表示你这台机器上所有的Git仓库都会使用这个配置,当然也可以对某个仓库指定不同的用户名和Email地址。

创建版本库

什么是版本库呢?版本库又名仓库,英文名repository,你可以简单理解成一个目录,这个目录里面的所有文件都可以被Git管理起来,每个文件的修改、删除,Git都能跟踪,以便任何时刻都可以追踪历史,或者在将来某个时刻可以“还原”。

1、选择一个目录,使用命令创建一个目录:

Administrator@XXX MINGW64 /e/git
$ mkdir learngit

Administrator@XXX MINGW64 /e/git
$ cd learngit

Administrator@XXX MINGW64 /e/git/learngit
$ pwd
/e/git/learngit

其中,mkdir命令用于创建目录,pwd命令用于显示当前目录。

注意: 目录中最好不要有中文。

2、使用命令git init,将此目录变成一个仓库:

Administrator@XXX MINGW64 /e/git/learngit
$ git init
Initialized empty Git repository in E:/git/learngit/.git/

当前目录下多了一个.git的目录,这个目录是Git来跟踪管理版本库的,没事千万不要手动修改这个目录里面的文件,不然改乱了,就把Git仓库给破坏了。
如果没有看到.git的目录,输入命令ls -ah就能看见。

3、添加文件到版本库

所有的版本控制系统,其实只能跟踪文本文件的改动,图片、视频、Word这些二进制文件无法有效控制,因此建议以纯文本的形式编写。

learngit目录下,新建一个文本readme.txt,内容如下:

We don't talk anymore.
Like we used to do.

(1)使用命令git addreadme.txt添加到仓库:

Administrator@XXX MINGW64 /e/git/learngit (master)
$ git add readme.txt

(2)使用明明git commitreadme.txt提交到仓库:

Administrator@XXX MINGW64 /e/git/learngit (master)
$ git commit -m "create a readme file"
[master (root-commit) 93b4ff1] create a readme file
 1 file changed, 2 insertions(+)
 create mode 100644 readme.txt

其中,-m后面是本次提交的说明,1 file changed表示有一个文件发生改动,2 insertions表示插入了两行内容。

如果我们不小心直接使用了git commit操作,而不是git commit -m "XXX"操作的话,会弹出这样一个窗口提示我们输入为什么要合入本次修改:

直接使用git commit命令

此时,我们可以按i键,进入输入修改的解释(图中黄色部分),输入完后按Esc退出修改,再输入:wq按回车键就可以了。

修改文件

1、首先,将我们的readme.txt文件的第一行修改一下,修改后如下:

We don't talk anymore.  Yes.
Like we used to do.

可以看见,添加了一个yes

2、使用git status查看当前仓库的修改状态:

$ git status
On branch master
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:   readme.txt

no changes added to commit (use "git add" and/or "git commit -a")

其中,modified: readme.txt告诉我们,readme.txt被修改了。

3、如果我们要看具体修改的是什么内容,使用git diff命令来查看:

$ git diff
diff --git a/readme.txt b/readme.txt
index d27965f..cd08cf5 100644
--- a/readme.txt
+++ b/readme.txt
@@ -1,2 +1,2 @@
-We don't talk anymore.
+We don't talk anymore.  Yes.
 Like we used to do.
\ No newline at end of file

其中,可以看出readme.txt被修改了,修改内容是We don't talk anymore.这一句被改成了+We don't talk anymore. Yes.

4、知道了修改内容,认为没有问题就可以将它提交到仓库里面了,也是同样的步骤git add <file>git commit

(1)执行$ git add readme.txt后,没有任何提示;

(2)执行git commit之前,我们再看一下仓库的状态,执行git status

$ git status
On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

        modified:   readme.txt

其中,Changes to be committed:modified: readme.txt这两句代码告诉我们,将要提交的修改包括readme.txt

(3)接下来就可以执行git commit命令去提交修改的文件了:

$ git commit -m "add Yes"
[master 6a32611] add Yes
 1 file changed, 1 insertion(+), 1 deletion(-)

(4)接下来,我们再使用git status命令查看当前的仓库状态:

$ git status
On branch master
nothing to commit, working tree clean

表示没有要提交的修改,其中,working tree clean表示当前仓库是干净的。

版本回退

为了说明版本回退,我再修改提交一次readme.txt文件,将它修改成如下这样并提交:

We don't talk anymore.  Yes.
Like we used to do. No.

提交的代码如下:

$ git commit -m "add No."
[master 0440b0f] add No.
 1 file changed, 1 insertion(+), 1 deletion(-)

现在,我们的版本库里面就有了三个版本的readme.txt文件:

  1. create a readme file
  2. add Yes
  3. add No

如果版本太多,怎么看我们到底修改了多少次呢?使用git log命令,可以看到我们历史提交的数据:

$ git log
commit 0440b0f8a14578b1efb38fbf3ca95f312d44db2f (HEAD -> master)    
Author: XXX <XXX@qq.com>
Date:   Sat Jul 28 14:31:51 2018 +0800

    add No.

commit 6a326110a8adc7d7856c72eb2e1c9fad97504fc8
Author: XXX <XXX@qq.com>
Date:   Sat Jul 28 14:24:01 2018 +0800

    add Yes

commit 93b4ff11e85f94a16ba6d9b0cff1ea2cdf226b60
Author: XXX <XXX@qq.com>
Date:   Fri Jul 27 22:32:43 2018 +0800

    create a readme file

git log 显示的是从最近到最远的提交记录,如上,可以看出我们提交了三次修改。

其中,上面第二行的commit 0440b0f8a14578b1efb38fbf3ca95f312d44db2f中的0440b...表示的这次提交的commit id,HEAD -> master表示add No这次提交是当前的版本,上次的版本就是HEAD^表示,上上次就是HEAD^^,如果版本太久远,就会用类似HEAD~100这种形式来表示。

现在,我们要把add No版本回退到add Yes版本,我们使用命令git reset实现:

$ git reset --hard HEAD^
HEAD is now at 6a32611 add Yes

现在,我们看看readme.txt中的内容是否被还原了:

We don't talk anymore.  Yes.
Like we used to do.

确实变成了add Yes的版本。
我们再来查看当前版本历史记录:

$ git log
commit 6a326110a8adc7d7856c72eb2e1c9fad97504fc8 (HEAD -> master)
Author: XXX <XXX@qq.com>
Date:   Sat Jul 28 14:24:01 2018 +0800

    add Yes

commit 93b4ff11e85f94a16ba6d9b0cff1ea2cdf226b60
Author: XXX <XXX@qq.com>
Date:   Fri Jul 27 22:32:43 2018 +0800

    create a readme file

可以看到,add No的版本已经不见了。那么如果我想回退到新版本,应该如何操作呢?有两种办法:

1、如果你还记得add No的commit id,也就是上面所说的0440b...那一串数字,那么执行如下命令:

$ git reset --hard 0440b
HEAD is now at 0440b0f add No.

--hard后面输入了0440b,这是add No的commit id的前几位,只需要输入commit id的前几位就行了,git会自动去寻找对应的id。

现在,再看一下readme.txt的内容,发现果然还原到了add No的版本:

We don't talk anymore.  Yes.
Like we used to do. No.

2、如果你记不住commit id了,也没关系,git提供了一个命令行,来记录我们每次的命令git reflog

$ git reflog
6a32611 (HEAD -> master) HEAD@{0}: reset: moving to HEAD^
0440b0f HEAD@{1}: reset: moving to 0440b
6a32611 (HEAD -> master) HEAD@{2}: reset: moving to HEAD^
0440b0f HEAD@{3}: commit: add No.
6a32611 (HEAD -> master) HEAD@{4}: commit: add Yes
93b4ff1 HEAD@{5}: commit (initial): create a readme file

其中,从0440b0f HEAD@{3}: commit: add No.这一句中,我们可以看出来,add No的版本的commit id是0440b0f
找到了commit id,再次使用git reset命令,就能回退到指定的版本了:

$ git reset --hard 0440b
HEAD is now at 0440b0f add No.

工作区和暂存区

工作区(Working Directory)

工作区就是我们能看到的目录,比如learngit目录就是工作区。

版本库(Repository)

工作区有一个隐藏目录.git,这个不算工作区,而是Git的版本库。
Git的版本库里存了很多东西,其中最重要的就是称为stage(或者叫index)的暂存区,还有Git为我们自动创建的第一个分支master,以及指向master的一个指针叫HEAD

工作区和版本库

如图,我们可以知道,我们在工作区修改的内容,经过git add命令,会将修改的内容存储到stage区域,也就是暂存区,然后再经过git commit命令,才会将我们的修改内容合入到master分支上。

我们来试验一下,先给readme.txt新增加一行内容:

We don't talk anymore.  Yes.
Like we used to do. No.
Now You See Me.

然后,我们在learngit目录下,新增一个文本文件LICENSE.txt,内容随便填写。

先用git status命令查看一下当前目录的状态:

$ git status
On branch master
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:   readme.txt

Untracked files:
  (use "git add <file>..." to include in what will be committed)

        LICENSE.txt

no changes added to commit (use "git add" and/or "git commit -a")

其中,modified: readme.txt可以知道,readme.txt的内容被修改了,而LICENSE.txt还未被添加,所以状态是Untracked
关于git status的状态种类,可以看这里:Git教学篇3-文件状态之git status与git diff

现在,使用命令git add readme.txt和命令git add LICENSE.txt后,使用git status命令查看当前仓库状态:

$ git status
On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

        new file:   LICENSE.txt
        modified:   readme.txt

现在,暂存区的状态就变成了:


暂存区状态

然后执行git commit命令,将暂存区的文件提交到master分支上:

$ git commit -m "understand how stage works"
[master 7126739] understand how stage works
 2 files changed, 3 insertions(+), 1 deletion(-)
 create mode 100644 LICENSE.txt

此时,我们用git status查看仓库状态:

$ git status
On branch master
nothing to commit, working tree clean

此时,仓库变成这样:

仓库状态

补充:
git diff是工作区和暂存区的比较,git diff --cached是暂存区和master的比较。
git status是比较本地工作区的变更。

管理修改

Git最重要的一个特性是,git管理的是文件的修改,而不是文件本身。比如你新增了一行,这就是一个修改,删除了一行,也是一个修改,更改了某些字符,也是一个修改,删了一些又加了一些,也是一个修改,甚至创建一个新文件,也算一个修改。

举例说明,首先,我们给readme.txt新增加一行:

We don't talk anymore.  Yes.
Like we used to do. No.
Now You See Me.
Add new Line.

然后使用git add命令添加到暂存区,然后再次修改readme.txt

We don't talk anymore.  Yes.
Like we used to do. No.
Now You See Me.
Add new Line.
Add new new Line.

然后git commit这次提交:

$ git commit -m "add two lines"
[master ea3fc04] add two lines
 1 file changed, 2 insertions(+), 1 deletion(-)

此时,我们看一下提交状态:
$ git status

On branch master
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:   readme.txt

no changes added to commit (use "git add" and/or "git commit -a")

如上,提示我readme.txt被修改了,但是还没有add到暂存区,原因是因为我们的操作顺序是修改文件->git add->修改文件->git commit,我们再第一次修改文件后执行了add操作,将文件add到暂存区,而第二次没有,因此,git commit命令只是将我们第一次的修改提交到了本地仓库中,如果我们需要第二次的修改也提交到仓库中,那么我们需要对第二次的操作也进行git addgit commit操作。或者,我们可以在第一次修改文件后不做git add操作,而是在修改完所有内容后,再一次性的git add

撤销修改

我们先给readme.txt文件添加一行内容:

We don't talk anymore.  Yes.
Like we used to do. No.
Now You See Me.
Add new Line.
Add new new Line.
I am a error.

我们在最后一行添加了一句I am a error.,这时,我们用git status看一下当前的本地状态:

$ git status
On branch master
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:   readme.txt

no changes added to commit (use "git add" and/or "git commit -a")

其中,有一句(use "git checkout -- <file>..." to discard changes in working directory告诉我们可以丢弃工作区的修改:

$ git checkout -- readme.txt

此时,我们的readme.txt变成了这样:

We don't talk anymore.  Yes.
Like we used to do. No.
Now You See Me.
Add new Line.

可以看见,最后两行都不见了,这是因为最后两行的内容都没有被添加到暂存区(没有执行git commit命令),因此,它们都被丢弃了。
那么,如果我添加I am a error后又将此修改add到了暂存区后,那么git checkout会变成什么样呢?

首先,我们先说明一下git checkout的作用范围。
我们修改一下readme.txt文件:

We don't talk anymore.  Yes.
Like we used to do. No.
Now You See Me.
Add new Line.
I am an error.

然后使用git add命令将此修改添加到暂存区,然后使用git ckeckout命令后,查看readme.txt文件:

We don't talk anymore.  Yes.
Like we used to do. No.
Now You See Me.
Add new Line.
I am an error.

最后一句依然在,那么,我们对readme.txt内容做一次修改:

We don't talk anymore.  Yes.
Like we used to do. No.
Now You See Me.
Add new Line.
I am an error.
I am an anotner error.

然后再git checkout

We don't talk anymore.  Yes.
Like we used to do. No.
Now You See Me.
Add new Line.
I am an error.

可以看到,readme.txt变成了上次git add后的内容(readme.txt添加了I am an error.这一句后的git add操作),也就是说,git checkout操作,可以将工作区的内容回退到最近一次git add或者git commit后的状态。

但是,如果我已经将I am an error.这句话add到了暂存区,那么,如何才能撤销本次暂存区的内容呢?
我们先将readme.txt的内容修改成这样,并add到暂存区:

We don't talk anymore.  Yes.
Like we used to do. No.
Now You See Me.
Add new Line.
I am an error.

然后使用git reset HEAD <file>命令,将暂存区的内容撤销到工作区(也就是将本次git add的内容撤销到工作区):

$ git reset HEAD readme.txt
Unstaged changes after reset:
M       readme.txt

其中,当我们用HEAD时,表示最新的版本。
再使用git status查看工作区状态,可以看见工作区的内容没有被添加到暂存区:

$ git status
On branch master
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:   readme.txt

no changes added to commit (use "git add" and/or "git commit -a")

然后,我们再使用git checkout命令,将本次修改移除掉。使用命令后,我们的readme.txt文件变成了这样:

We don't talk anymore.  Yes.
Like we used to do. No.
Now You See Me.
Add new Line.

版本回退和撤销修改场景总结

假如readme.txt原内容为A,修改后为A+B:

1、此时没有执行git add操作,工作区想要恢复为A,则执行:

$ git checkout -- readme.txt

执行前,工作区内容为A+B,暂存区内容为A,版本库内容为A。
执行后,工作区内容变为A,暂存区内容为A,版本库内容为A。

2、此时执行了git add操作,想要撤回本次git add操作(工作区恢复为A+B,暂存区恢复为A)则执行:

$ git reset readme.txt

此命令等同于命令:

$ git reset --mixed readme.txt

--mixed表示重置HEAD指针和暂存区,但是工作区内容保持不变。

执行前,工作区内容为A+B,暂存区内容为A+B,版本库内容为A。
执行后,工作区内容为A+B,暂存区内容为A,版本库内容为A。

3、此时执行了git add操作,想要撤回本次git add操作(工作区恢复为A,暂存区恢复为A),则执行:

$ git reset readme.txt
$ git checkout -- readme.txt

或者执行如下命令:

$ git reset --hard head

执行前,工作区内容为A+B,暂存区内容为A+B,版本库内容为A。
执行后,工作区内容为A,暂存区内容为A,版本库内容为A。

4、此时执行了git add操作和git commit操作,想要撤回本次操作(工作区恢复为A+B,暂存区恢复为A+B,版本库恢复为A),则执行:

$ git reset --soft HEAD^

--soft表示仅仅重置HEAD指针,不重置工作区和暂存区的内容。

执行前,工作区内容为A+B,暂存区内容为A+B,版本库内容为A+B。
执行后,工作区内容为A+B,暂存区内容为A+B,版本库内容为A。

5、此时执行git add操作和git commit操作,想要撤回本次操作(工作区恢复为A,暂存区恢复为A,版本库恢复为A),则执行:

$ git reset --hard HEAD^

--hard表示重置HEAD指针、暂存区、工作区内容。

执行前,工作区内容为A+B,暂存区内容为A+B,版本库内容为A+B。
执行后,工作区内容为A,暂存区内容为A,版本库内容为A。

  • --mixed:重置HEAD指针和暂存区,工作区保持不变。
    说明:--mixed后面可以接文件名或者指定的指针比如HEAD^,接文件名时表示仅仅重置当前的暂存区(暂存区的内容还是HEAD的),接指针比如HEAD^时,表示重置当前的指针到HEAD^,暂存区内容变为HEAD时的内容(也就是空的)。

  • --soft:后面只能接指针,仅仅重置版本(比如将当前版本HEAD改为HEAD^,但是暂存区和工作区的内容还是HEAD的)。

  • --hard:后面只能接指针,重置版本、暂存区和工作区内容(比如将当前版本HEAD改为HEAD^,暂存区和工作区的内容都变成HEAD^的)。

版本回退和撤销修改还有一种命令git revert,这个我们在后面再介绍。

删除文件

我们先在learngit目录下新建一个test.txt文本文件,并commit到本地版本库中。
当我们在本地将此文件删除后,
1、如果是确实要删除,则使用命令删除此文件:

$ git rm test.txt
rm 'test.txt'

然后在删除后做git commit操作更新本地仓库就行。
2、如果是误删,那么也没关系,我们的本次仓库中还有这个文件,我们只需要从仓库中取出就行了:

$ git checkout -- test.txt

此时,我们工作区的test.txt就又回来了。

Git远程仓库

在本地仓库对文件的修改我们已经学习的差不多了,那么,下面就到了和远程仓库如何交互的学习了。

远程仓库我们没有也没有关系,可以利用GitHub这个神奇的网站来实现。

添加远程仓库

1、生成秘钥

首先,由于GitHub和本地的仓库关联是通过SSH加密的,所以,我们需要先在本地添加一下公钥和私钥。

创建SSH Key的命令是:

$ ssh-keygen -t rsa -C "youremail@example.com"

不用设置密码,一路回车就行。
然后在我们的用户目录下会生成一个.ssh的文件夹,此文件夹下面会生成id_rsaid_rsa.pub两个文件。id_rsa是私钥,不能泄露出去,id_rsa.pub是公钥,可以告诉任何人。

2、配置秘钥

登录GitHub官网,点击头像 - Settings - SSH and GPG keys - New SSH key
id_rsa.pub文件中的内容粘贴进去,然后点击添加即可。

为什么GitHub需要SSH Key呢?因为GitHub需要识别出你推送的提交确实是你推送的,而不是别人冒充的,而Git支持SSH协议,所以,GitHub只要知道了你的公钥,就可以确认只有你自己才能推送。
当然,GitHub允许你添加多个Key。假定你有若干电脑,你一会儿在公司提交,一会儿在家里提交,只要把每台电脑的Key都添加到GitHub,就可以在每台电脑上往GitHub推送了。
最后友情提示,在GitHub上免费托管的Git仓库,任何人都可以看到喔(但只有你自己才能改)。所以,不要把敏感信息放进去。

3、关联本地仓库

登录GitHub后,点击头像旁边的+,选择new repository,在Repository name里面填入learngit后,其余保持默认后点击Create repository

生成后会看到如图所示的一个页面,点击SSH后会看到我们的git地址:

Git远程仓库地址

然后我们在本地的learngit仓库下打开git bash,直接运行图中标红的那部分的命令(其实应该是下面那个 or oush an existing ...那部分):

git remote add origin git@github.com:XXX/learngit.git

此时本地仓库就和远程仓库关联上了,然后我们运行标红框下面那一句命令,将本地的文件推送到远程仓库中:

$ git push -u origin master
Counting objects: 25, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (18/18), done.
Writing objects: 100% (25/25), 2.13 KiB | 0 bytes/s, done.
Total 25 (delta 5), reused 0 (delta 0)
remote: Resolving deltas: 100% (5/5), done.
To github.com:XXX/learngit.git
 * [new branch]      master -> master
Branch master set up to track remote branch master from origin.

问题解决
我在执行git push -u origin master时,爆出如下错误:

The authenticity of host 'github.com (52.74.223.119)' can't be established.
RSA key fingerprint is SHA256:nThbg6kXUpJWGl7E1IGOCspRomTxdCARLviKw6E5SY8.
Are you sure you want to continue connecting (yes/no)? 

此时不要选择直接回车,而是要输入yes后再回车,原因是我们本地.ssh文件夹中只有两个秘钥,缺少了一个known_hosts的文件,选择yes后此文件会自动生成。
接着,又爆出了如下错误:

Warning: Permanently added 'github.com,52.74.223.119' (RSA) to the list of known hosts.
packet_write_wait: Connection to 52.74.223.119 port 22: Broken pipe
fatal: Could not read from remote repository.

Please make sure you have the correct access rights
and the repository exists.

此时不要慌张,这只是表示我们已经把GitHub的Key添加到本机的一个信任列表里了。只要再次执行一次git push -u origin master命令,即可推送成功。

由于远程库是空的,我们第一次推送master分支时,加上了-u参数,Git不但会把本地的master分支内容推送的远程新的master分支,还会把本地的master分支和远程的master分支关联起来,在以后的推送或者拉取时就可以简化命令,推送命令:

$ git push origin master

从远程仓库克隆

如果我们已经有了一个远程库,如何将它拉取到本地呢?

我们先在GitHbu上创建一个仓库gitskills,并且勾选图中红框部分,表示给我们的项目添加一个README.md文件。

新建gitskills仓库

新的仓库建完之后,我们可以在此仓库的页面找到一个clone or download的图标,点击后会有一个我们仓库的地址,类似于git@github.com:XXX/gitskills.git,这时,我们只要在本地使用如下命令(最好不要在learngit目录下执行此命令),则就可以将远程仓库拉取到本地并关联了:

$ git clone git@github.com:XXX/gitskills.git
Cloning into 'gitskills'...
remote: Counting objects: 3, done.
remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
Receiving objects: 100% (3/3), done.

执行完后在当前目录就会有一个gitskills的git仓库了。

分支管理

如果在多人协作的项目中,我们有一个新的功能要去实现,当前我们已经实现了50%,如果此时我们将代码合入master分支,就有可能影响其他人无法工作,如果我们不合入master分支,则会有代码丢失的风险。
那么,针对这种情况,我们就可以新建一个只有自己能看见的分支,别人看不到,还继续在原来的分支上正常工作,而你在自己的分支上干活,想提交就提交,直到开发完毕后,再一次性合并到原来的分支上,这样,既安全,又不影响别人工作。

创建与合并分支

前面版本回退的时候,我们说了HEAD -> masterHEAD是表示当前的版本,这是因为我们只有一个分支master,其实实际上,指向版本是master这个指针,我们提交的内容也是通过master指向的指针版本去更新内容,而HEAD其实是指向master的指针。每次你的提交master都会向前移一步,而HEAD会永远跟随master移动。
如果我们现在创建一个新的分支dev,Git就会新建一个指针dev,并指向master指向的内容,并且把HEAD指向dev,表示dev是当前使用的分支。那么此时,我们在工作区的修改和提交就会在dev分支上进行,每一次提交,dev分支就会向前移动一步,而master分支不变。
如果dev分支上的内容开发完毕,就需要合并两个分支,最简单的方法就是直接将master的指针指向dev分支指向的内容。

实战

首先,我们在learngit上创建一个新的分支:

$ git checkout -b dev
Switched to a new branch 'dev'

然后我们用git branch命令查看当前分支:

$ git branch
* dev
  master

其中,*表示的是当前的分支。

然后,我们修改readme.txt文件,然后提交:

We don't talk anymore.  Yes.
Like we used to do. No.
Now You See Me.
Add new Line.

Add a new Branch.

然后,我们切换回master分支:

$ git checkout master
Switched to branch 'master'
Your branch is up-to-date with 'origin/master'.

此时,再打开readme.txt文件,可以看到,我们刚才提交的内容不见了:

We don't talk anymore.  Yes.
Like we used to do. No.
Now You See Me.
Add new Line.

现在,我们把刚才提交的内容合并到master分支上:

$ git merge dev
Updating 563fbc4..4fb19e4
Fast-forward
 readme.txt | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

其中,Fast-forward表示快进模式,表示当前的合并非常快。

此时,readme.txt的内容变成了:

We don't talk anymore.  Yes.
Like we used to do. No.
Now You See Me.
Add new Line.

Add a new Branch.

此时,dev分支的任务完成了,我们就可以删除dev分支了:

$ git branch -d dev
Deleted branch dev (was 4fb19e4).

此时,查看剩余分支,只剩下了master分支了:

$ git branch
* master

关于分支的一些命令:

  • 查看分支:git branch

  • 创建分支:git branch <name>

  • 切换分支:git checkout <name>

  • 创建+切换分支:git checkout -b <name>

  • 合并某分支到当前分支:git merge <name>

  • 删除分支:git branch -d <name>

解决冲突

上述情况是在理想的情况下可以进行,那么如果master分支和dev分支都对readme.txt文件进行了修改,那么如何解决冲突呢?

首先,我们新建一个分支feature1:

$ git checkout -b feature1
Switched to a new branch 'feature1'

然后,我们修改readme.txt文件如下,并提交:

We don't talk anymore.  Yes.
Like we used to do. No.
Now You See Me.
Add new Line.

Add a new Branch.

I am FEATURE1 branch.

然后,切换到master分支上:

$ git checkout master
Switched to branch 'master'
Your branch is ahead of 'origin/master' by 1 commit.
  (use "git push" to publish your local commits)

Your branch is ahead of 'origin/master' by 1 commit.这一句是git提示我们,我们本地的版本比远程仓库的版本还要新一个版本。

然后,我们把readme.txt最后一行也改动如下,并提交:

We don't talk anymore.  Yes.
Like we used to do. No.
Now You See Me.
Add new Line.

Add a new Branch.

I am MASTER branch.

此时,我们尝试着将两个分支合并:

$ git merge feature1
Auto-merging readme.txt
CONFLICT (content): Merge conflict in readme.txt
Automatic merge failed; fix conflicts and then commit the result.

此时,Git告诉我们,有冲突发生了!我们可以通过 git status查看发生冲突的文件:

On branch master
Your branch is ahead of 'origin/master' by 2 commits.
  (use "git push" to publish your local commits)
You have unmerged paths.
  (fix conflicts and run "git commit")
  (use "git merge --abort" to abort the merge)

Unmerged paths:
  (use "git add <file>..." to mark resolution)

        both modified:   readme.txt

no changes added to commit (use "git add" and/or "git commit -a")

如上提示,readme.txt发生了冲突。

现在,我们查看readme.txt的内容:

We don't talk anymore.  Yes.
Like we used to do. No.
Now You See Me.
Add new Line.

Add a new Branch.

<<<<<<< HEAD
I am MASTER branch.
=======
I am FEATURE1 branch.
>>>>>>> feature1

Git用<<<<<<<=======>>>>>>>标记出不同分支的内容,我们修改如下后保存:

$ git checkout master
Switched to branch 'master'
Your branch is ahead of 'origin/master' by 4 commits.
  (use "git push" to publish your local commits)

此时,合并工作完成了,我们可以删除feature1分支了。

远程仓库版本回退

前面介绍了git checkoutgit reset两种回退方法,其中git checkout是用来撤销本地工作区的内容,而git reset可以撤销本地暂存区的内容和本地版本库的内容。
其实git reset也同样可以用来撤销远程仓库的版本,我们只需要在本地将版本回退到我们需要的版本,然后git push到远程仓库就行,但是这样做的时候,往往会报如下的错误提示:

$ git push origin master
To github.com:XXX/learngit.git
 ! [rejected]        master -> master (non-fast-forward)
error: failed to push some refs to 'git@github.com:XXX/learngit.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Integrate the remote changes (e.g.
hint: 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

其中,Updates were rejected because the tip of your current branch is behind提示我们本地代码的版本比远程的版本要低,我们通过git diff 本地分支名 远程仓库名/远程分支名git diff master origin/master)就能看出区别,这时,我们只要使用强制推送就可以成功了:

$ git push origin master --force
Total 0 (delta 0), reused 0 (delta 0)
To github.com:TokyoAndroid/learngit.git
 + a1dcb7a...f631cdb master -> master (forced update)

但是,这样做非常不推荐。因为这样会直接将我们提交版本之后的所有提交都给删除,这样在多人协作开发的时候会给其他人带来很大的困惑!

因此,我们还可以使用另一种版本回退命令git revertgit revert也是将我们指定的某一个版本撤销,但不会删除该版本后续的提交信息,只是在最新的提交上面新建一个提交,这个新建提交的内容就是撤销掉我们指定版本的提交。
听起来有点绕,我们举例来说明:
首先,我们在readme.txt文件后面增加一句AAAAA并提交,然后我们在后面再增加一句BBBBB再提交,然后我们在后面再增加一句CCCCC再提交:

$ git add readme.txt
$ git commit -m "add AAAAA"

$ git add readme.txt
$ git commit -m "add BBBBB"

$ git add readme.txt
$ git commit -m "add CCCCC"

通过git log命令,我们可以看到当前我们有三次提交记录:

$ git log --oneline
fb3527b (HEAD -> master) add CCCCC
4369360 add BBBBB
c591acb add AAAAA
...

此时,如果我们想要撤销掉最后一次提交,也就是commit id为fb3527b的这次,我们可以使用命令git revert来实现:

$ git revert fb3527B
[master 9494e61] Revert "add CCCCC"
 1 file changed, 1 insertion(+), 2 deletions(-)

此时,我们再用git log查看提交记录:

$ git log --oneline
9494e61 (HEAD -> master) Revert "add CCCCC"
fb3527b add CCCCC
4369360 add BBBBB
c591acb add AAAAA
...

可以看到,fb3527b的这次提交信息还在,只是在此基础上新增了一个提交,然后,我们再通过命令git push便可以将这次修改推送到远程仓库中了。
如果你只想撤销本次提交,而不想再次提交的话,可以使用命令git revert <commit-id> --no-commit来实现。

如果我们想要撤销某一次的版本,也可以使用此命令来实现,比如,我们先回到fb3527b之前的版本,现在去撤销4369360的版本(注意,由于4369360这个提交是提交的add BBBBB的操作,因此撤销到这个提交之前的内容是只有AAAAA):

$ git reset --hard fb3527
HEAD is now at fb3527b add CCCCC

$ git log --oneline
fb3527b (HEAD -> master) add CCCCC
4369360 add BBBBB
c591acb add AAAAA
...

$ git revert 4369360
error: could not revert 4369360... add BBBBB
hint: after resolving the conflicts, mark the corrected paths
hint: with 'git add <paths>' or 'git rm <paths>'
hint: and commit the result with 'git commit'

撤销失败,因为有冲突,我们需要先解决冲突,打开readme.txt文件:

...

<<<<<<< HEAD
AAAAA
BBBBB
CCCCC
=======
AAAAA
>>>>>>> parent of 4369360... add BBBBB

如上可以看见,当前的内容是:

AAAAA
BBBBB
CCCCC

撤销到4369360之前的内容是:

AAAAA

此时,我们解决冲突,将文本改成这样:

AAAAA
after Revert 
BBBBB
CCCCC

然后重新git addgit commit来提交修改并推送到远程仓库:

$ git add readme.txt

$ git commit -m "Revert add BBBBB"
[master 233a610] Revert add BBBBB
 1 file changed, 2 insertions(+), 1 deletion(-)

$ git log --oneline
233a610 (HEAD -> master) Revert add BBBBB
fb3527b add CCCCC
4369360 add BBBBB
c591acb add AAAAA
...

$ git push origin master
Counting objects: 12, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (12/12), done.
Writing objects: 100% (12/12), 998 bytes | 0 bytes/s, done.
Total 12 (delta 8), reused 0 (delta 0)
remote: Resolving deltas: 100% (8/8), completed with 2 local objects.
To github.com:XXX/learngit.git
   f631cdb..233a610  master -> master

分支管理策略

通常,合并分支时,如果可能,Git会用Fast forward模式,但这种模式下,删除分支后,会丢掉分支信息。

如果要强制禁用Fast forward模式,Git就会在merge时生成一个新的commit,这样,从分支历史上就可以看出分支信息。

我们新建一个分支dev并修改后提交,然后切换到master分支,合并dev,此时我们禁用掉Fast forward模式:

$ git merge --no-ff -m "merge dev with no of" dev
Merge made by the 'recursive' strategy.
 readme.txt | 2 ++
 1 file changed, 2 insertions(+)

其中,--no-ff表示禁掉Fast forward模式,后面的-m "merge dev with no of"是由于我们本次的merge会生成一个新的提交,因此需要写上提交的信息。
这时,我们用git log命令查看本次的分支历史:

$ git log --graph --pretty=oneline --abbrev-commit
*   276bfb7 (HEAD -> master) merge dev with no of
|\
| * 41c356f (dev) test no off fast forward
|/
*   1cd720d  modify by master 1
|\
| * d49f6d7 modify by feature1
* | 1cf5452 modify by master
|/
* 4fb19e4 add a new branch
* 563fbc4 (origin/master) add test.txt 2 d^Z
* d91c29b delete text.txt
* 5188978 add test.txt
* 47045f4 eight modify
* 9eaa421 six modify
* ea3fc04 add two lines
* 7126739 understand how stage works
* 0440b0f add No.
* 6a32611 add Yes
* 93b4ff1 create a readme file

紧急情况(Bug分支)

假如我现在在dev分支上干活,工作进行了一半,还没有办法合入master分支中,但此时master上有一个紧急bug需要修改,如果此时合入我们未完成的内容,则可能导致master分支不稳定,而如果不合入我们的修改而直接切回master分支时,我们的修改内容也会跟着被带到master分支上,并且master分支提交时,也会将我们在dev分支上的内容提交:

$ git checkout master
Switched to branch 'master'
M       readme.txt

其中,M readme.txt中的M表示在dev分支上有修改并且没有提交的内容被带到了master分支上。

总之这两种方式都会对代码造成影响,那么此时如何解决呢?

还好,Git给我们提供了一个命令git stash,用来解决这种情况。

首先我们修改dev分支下的readme.txt文件,但是并不add,同时新建一个文件testStash.txt并add,这样,我们在dev分支有两个文件的修改,其中一个add了,另一个没有,这样,是否add的情况我们都考虑到了:

$ git status
On branch dev
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

        new file:   testStash.txt

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:   readme.txt

现在,我们需要在master分支上拉取一个紧急分支bug101来解决这个编号为101的Bug,那么我们先执行git stash将我们在dev分支上的工作现场给“隐藏”起来:

$ git stash
Saved working directory and index state WIP on dev: 276bfb7 merge dev with no of

通过命令git status查看当前dev分支上的工作内容确实已经被隐藏起来了:

$ git status
On branch dev
nothing to commit, working tree clean

并且learngit目录下的testStash.txt文件已经不见了,readme.txt上的修改也消失了。

然后我们切换到master分支上去拉取一个新的分支:

$ git checkout -b bug101
Switched to a new branch 'bug101'

现在,我们把readme.txt内的内容后面增加一行并提交:

We don't talk anymore.  Yes.
Like we used to do. No.
Now You See Me.
Add new Line.

Add a new Branch.

I am MASTER&FEATURE1 branch.

I am Fast forward.

Bug is over.

然后切换到master分支上,合并bug101分支上的内容,并删除bug101分支:

$ git merge --no-ff -m "merge bug101 resolve bug" bug101
Merge made by the 'recursive' strategy.
 readme.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

$ git branch -d bug101
Deleted branch bug101 (was c1e5ce5).

Bug修复完毕后,重新切换到dev分支完成任务,这时dev分支时干净的,我们需要恢复上次的工作现场,输入命令git stash list命令查看工作现场:

$ git stash pop
On branch dev
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

        new file:   testStash.txt

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:   readme.txt

Dropped refs/stash@{0} (e9413549106498377c06d20c48df660040ab260b)

可以看见,我们的testStash.txt又回来了!

同时,我们用命令git stash list查看隐藏的工作现场,发现已经没有了:

$ git stash list

这是git stash pop的用法,它会在恢复现场的同时,将隐藏的工作现场都给删除;还有一种用法是使用命令git stash apply来恢复,但是恢复后,隐藏的工作现场都不会删除,你需要调用git stash drop来删除。
首先用命令git stash apply stash@{0}恢复工作现场:

$ git stash apply stash@{0}
On branch dev
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

        new file:   testStash.txt

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:   readme.txt

然后用命令git stash list查看是否隐藏的工作现场还存在:

$ git stash list
stash@{0}: WIP on dev: 276bfb7 merge dev with no of

然后使用命令git stash drop来删除隐藏的工作现场:

$ git stash drop stash@{0}
Dropped stash@{0} (003664b3a57eb40941908e6a7fddffe7a7b3b1b9)

再用命令git stash list查看,发现隐藏的工作现场已经没有了。

强行删除分支

如果我们在dev分支上拉取一个新分支dev1去开发新功能,开发完毕并且提交后,切换回dev分支准备进行合并分支,此时如果突然不想要这个新功能了,那么我们就要去销毁这个分支,当我们执行销毁dev1分支命令时,会出现以下的情况:

$ git branch -d dev1
error: The branch 'dev1' is not fully merged.
If you are sure you want to delete it, run 'git branch -D dev1'.

删除失败,但是Git会友情提示我们,如果要删除,会丢失所有修改,并且使用-D参数,现在我们用此参数来删除dev1分支:

$ git branch -D dev1
Deleted branch dev1 (was a74fcdb).

多人协作

当我们本地仓库和远程仓库对应起来后,我们可以通过git remotegit remote -v来查看远程库的信息。

我们可以使用命令git push origin master来将本地master分支推送到远程仓库origin分支,如果要推送到其他分支,则可以使用命令git push origin dev推送到dev分支上。

至于哪些分支需要同步,则要视项目情况而论,一般master分支和dev分支一般都是必须要时刻保持同步的。

抓取分支

当另一个开发人员从我们的learngit仓库clone代码到本地时,他只能看到master分支,而无法看到dev分支。
然后,他需要在dev分支开发的话,就需要新建一个dev分支,现在,他新建了一个dev分支,并新建了一个newDev.txt文件,里面只有一句话:

James newDev.

并推送到了远程仓库:

$ git push origin dev
Counting objects: 2, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (2/2), 242 bytes | 0 bytes/s, done.
Total 2 (delta 1), reused 0 (delta 0)
remote: Resolving deltas: 100% (1/1), completed with 1 local object.
To github.com:XXX/learngit.git
 * [new branch]      dev -> dev

同时,碰巧我们在dev分支下也创建了一个newDev.txt文件,并且有一句话是Master newDev.
当我们将这个新建的文件也推送到远程仓库时就会因为冲突而报错:

$ git push origin dev
To github.com:TokyoAndroid/learngit.git
 ! [rejected]        dev -> dev (fetch first)
error: failed to push some refs to 'git@github.com:TokyoAndroid/learngit.git'
hint: Updates were rejected because the remote contains work that you do
hint: not have locally. This is usually caused by another repository pushing
hint: to the same ref. You may want to first integrate the remote changes
hint: (e.g., 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

其中,hint: (e.g., 'git pull ...') before pushing again.提示我们先git pull远程代码后再git push
执行此操作,同样也报错:

$ git pull
remote: Counting objects: 5, done.
remote: Compressing objects: 100% (3/3), done.
remote: Total 5 (delta 2), reused 4 (delta 1), pack-reused 0
Unpacking objects: 100% (5/5), done.
From github.com:TokyoAndroid/learngit
 * [new branch]      dev        -> origin/dev
There is no tracking information for the current branch.
Please specify which branch you want to merge with.
See git-pull(1) for details.

    git pull <remote> <branch>

If you wish to set tracking information for this branch you can do so with:

    git branch --set-upstream-to=origin/<branch> dev

根据There is no tracking information for the current branch.我们知道这是因为没有指定本地dev分支与远程origin/dev分支的链接,根据提示,设置devorigin/dev的链接,同样最后一句也给出了链接的命令git branch --set-upstream-to=origin/<branch> dev

$ git branch --set-upstream-to=origin/dev dev
Branch dev set up to track remote branch dev from origin.

然后再一次pull

$ git pull
Auto-merging newDev.txt
CONFLICT (add/add): Merge conflict in newDev.txt
Automatic merge failed; fix conflicts and then commit the result.

这次pull成功,但是有冲突,我们需要解决冲突(解决方法和上面的解决方式完全一样),并且提交后,再git push

$ git push origin dev
Counting objects: 9, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (7/7), done.
Writing objects: 100% (9/9), 837 bytes | 0 bytes/s, done.
Total 9 (delta 3), reused 0 (delta 0)
remote: Resolving deltas: 100% (3/3), completed with 1 local object.
To github.com:XXX/learngit.git
   4018c1b..c5e7d93  dev -> dev

这次,就成功了。

补充:在本地创建和远程仓库对应的分支时,可以使用如下命令:

$ git checkout -b <branch-name> origin/<branch-name>

Git补充功能

设置命令颜色

给Git设置命令颜色,使我们的命令输出更醒目:

$ git config --global color.ui true

设置忽略文件

比如有些文件需要放在Git仓库目录下,但是又不需要提交到Git的,可以新建一个名称为.gitignore的文件,然后把你不想要被提交的文件添加进去。很多开发语言的忽略文件都已经写好了:忽略文件,我们只需要再往里面添加自己想要添加的就可以了。

设置别名

比如我觉得输入git status太麻烦,想简化成git st行吗?当然可以,给git status配置别名:

$ git config --global alias.st status

标签

当我们需要一个指定的版本的时候,通常需要这个版本的commit id,但是commit id我们一般记不住,则我们可以使用git tag给某个版本打上一个标签。commit id和tag的关系类似于网站的IP地址和域名的关系。

给当前版本打上tag:

$ git tag v1.0

给历史版本打上标签,先找到commit id,再打标签(给resolve bug101这个版本打上标签):

$ git log --pretty=oneline --abbrev-commit
5a92d6b (HEAD -> master, tag: v1.0, origin/master) merge dev stash
b19befc commit dev modify
9e374a8 merge bug101 resolve bug
c1e5ce5 resolve bug101
4bcf5a8 test dev no add
276bfb7 merge dev with no of
41c356f test no off fast forward
1cd720d  modify by master 1
1cf5452 modify by master
d49f6d7 modify by feature1
4fb19e4 add a new branch
563fbc4 add test.txt 2 d^Z
d91c29b delete text.txt
5188978 add test.txt
47045f4 eight modify
9eaa421 six modify
ea3fc04 add two lines
7126739 understand how stage works
0440b0f add No.
6a32611 add Yes
93b4ff1 create a readme file

$ git tag v0.9 c1e5ce5

查看标签:

$ git tag
v0.9
v1.0

查看具体标签对应的版本提交信息:

$ git show v0.9
commit c1e5ce59eccf9253d7adfab0facad8faab2c9919 (tag: v0.9)
Author: TokyoZ <344738736@qq.com>
Date:   Sat Jul 28 21:37:58 2018 +0800

    resolve bug101

diff --git a/readme.txt b/readme.txt
index 110244a..81560d3 100644
--- a/readme.txt
+++ b/readme.txt
@@ -9,4 +9,4 @@ I am MASTER&FEATURE1 branch.

 I am Fast forward.

-I am dev.
\ No newline at end of file
+Bug is over.
\ No newline at end of file

删除标签:

$ git tag -d v0.9
Deleted tag 'v0.9' (was c1e5ce5)

推送标签到远程仓库:

$ git push origin v0.9
Total 0 (delta 0), reused 0 (delta 0)
To github.com:XXX/learngit.git

  • [new tag] v0.9 -> v0.9

删除远程标签:
需要先删除本地标签,再用命令git push origin :refs/tags/<tag-name>删除远程标签:

$ git tag -d v0.9
Deleted tag 'v0.9' (was c1e5ce5)

$ git push origin :refs/tags/v0.9
To github.com:XXX/learngit.git
 - [deleted]         v0.9

参考资料

廖雪峰的官方网站-Git教程

推荐阅读更多精彩内容

  • 一、Git工作流程 以上包括一些简单而常用的命令,但是先不关心这些,先来了解下面这4个专有名词。 Workspac...
    LeiLv阅读 5,039评论 2 38
  • 1.git的安装 1.1 在Windows上安装Git msysgit是Windows版的Git,从https:/...
    落魂灬阅读 4,468评论 2 43
  • Git常用语法 [TOC] Git简介 描述 ​ Git(读音为/gɪt/。)是一个开源的分布式版本控制系统,...
    君惜丶阅读 1,101评论 0 13
  • 今天第一天开始上班,没有任务,于是开始学习Git这一程序猿必须掌握之技能,希望今天的积累过后,对与Git或者...
    CoderTung阅读 4,803评论 2 91
  • 1、一起小心肝!我国每年约有42.2万人死于肝癌,占全球肝癌死亡病例数的51%。(人民日报) 2、据中央气象台,受...
    財知道阅读 51评论 0 0