Git学习

Git学习

[参考网站:廖雪峰的官方网站--Git教程]

1. Git安装

  • Linux

    • Ubuntu和Debian可以直接通过sudo apt-get install git对git进行安装
    • 其他版本的Linux先从Git官网下载源码,然后解压,依次输入:./configmakesudo make install这几个命令安装就好了
  • Mac

    • 安装homebrew,然后通过homebrew安装Git
    • 直接从AppStore安装Xcode,Xcode集成了Git,不过默认没有安装,你需要运行Xcode,选择菜单“Xcode”->“Preferences”,在弹出窗口中找到“Downloads”,选择“Command Line Tools”,点“Install”就可以完成安装了。
  • Windows上可以直接通过官网下载

1.初始化配置

$ git config --global user.name "Your Name"
$ git config --global user.email "Your Email"

其中git-config 命令的--global参数表示这台机器上所有的Git仓库都使用这个配置

2.创建版本库

版本库简单理解为一个文件目录,目录中的所有文件都被Git统一管理.一边时刻查看历史.

创建版本库

$ mkdir learngit //创建一个文件目录,作为仓库
$ cd learngit //打开版本库
$ pwd //用于显示当前目录
$ git init //将文件目录变成Git仓库,变成仓库的同时,会新建一个.git的目录,这个目录用于存储版本历史信息
$ ls -ah   //查看显示仓库中所有目录

在目录下新建文件,并将文件提交到版本库进行版本控制

$ git add readme.txt  //将文件添加到版本库
$ git commit -m "wrote a readme file(提交内容介绍)"

git commit命令 -m后面输入的是本次提交的说明,后边可以输入任何内容

同时,git 可以一次添加多个文件,然后统一提交

$ git add file1.txt
$ git add file2.txt file3.txt
$ git commit -m "add 3 files"

小结

添加文件到Git仓库,分两步:

  1. 使用命令git add <file>
  2. 使用命令git commit -m <message>

2.时光机穿梭

1.版本控制

运行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")

上边这个命令的输出信息告诉我们,readme.txt被修改过了,但是还没有准备提交的修改.

此时,我们有可能需要查看具体修改了什么内容,可以使用git diff这个命令进行显示

$ git diff readme.txt                                          手动灰色
diff --git a/readme.txt b/readme.txt                           手动白色
index d8036c1..013b5bc 100644                                  手动白色
--- a/readme.txt                                               手动白色
+++ b/readme.txt                                               手动白色
@@ -1,2 +1,2 @@                                                手动蓝色
-Git is a version control system.                              手动红色
+Git is a distributed version control system.                  手动绿色
 Git is free software.                                         手动灰色
\ No newline at end of file                                    手动灰色

可以通过显示标志和提示信息很明确的看清楚对文件的修改,而且git Bash上边还有颜色提示

在查看了文件作出什么修改后就可以放心的将它提交到仓库了

第一步:首先将文件添加到仓库git add

$ git add readme.txt

在提交之前先通过git status查看一下文件的状态,显示将要被提交的修改包括readme.txt文件

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

        modified:   readme.txt

第二步:添加之后通过git commit对文件进行提交,显示一个文件被改变,添加一行,删除一行

$ git commit -m "add distributed"
[master c547aae] add distributed
 1 file changed, 1 insertion(+), 1 deletion(-)

然后提交后查看一下仓库当前状态,显示没有需要提交的修改,而且工作目录是干净的.

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

小结

  • 要随时掌握工作区的状态,使用git status命令。
  • 如果git status告诉你有文件被修改过,用git diff可以查看修改内容。

2.版本回退

现在,已经学会了修改文件,然后开始进行多次练习,修改readme.txt文件.

经过文件的多次修改提交后,现在可以通过git log命令查看历史日志

$ git log
commit 3a05f4be13377870f52d4f245175cac70b368c37 (HEAD -> master)
Author: chris <1151211841@qq.com>
Date:   Sun Mar 3 16:36:08 2019 +0800

    append GPL

commit c547aae2d26364c4d288bf8f5bd43ffce55d9aad
Author: chris <1151211841@qq.com>
Date:   Sat Mar 2 12:52:46 2019 +0800

    add distributed

commit 57f1e2773ce411f108beb5c18bb7d7f970dc610b
Author: chris <1151211841@qq.com>
Date:   Wed Feb 27 12:57:23 2019 +0800

    wrote a readme file

命令小时从最近到最远的提交日志,我们可以看到三次提交,一次append GPL,上一次是add distributed,最早的一次是wrote a readme.txt

如果一次输出信息太多的话,可以试试加上--pretty=oneline参数,可以将一次提交的信息输出到一行上

$ git log --pretty=oneline
3a05f4be13377870f52d4f245175cac70b368c37 (HEAD -> master) append GPL
c547aae2d26364c4d288bf8f5bd43ffce55d9aad add distributed
57f1e2773ce411f108beb5c18bb7d7f970dc610b wrote a readme file

需要注意的是,你看到的一大串类似3a05f4be13377870f52d4f245175cac70b368c37这种是commit id(版本号),和SVN不同的是,Git的版本号不是1,2,3...等递增的数字,而是一个SHA1计算出来的一个非常大的数字,用十六进制表示,而且每个人的版本号也都不同,至于版本号为什么事这么一大串数字,因为Git是分布式的版本控制系统,这样可以避免版本号冲突.

好了,现在基本的添加修改知道了,现在开始准备将版本回退到上一个版本,也就是add distributed

首先,必须知道当前是哪个版本,Git使用HEAD表示当前版本,上一个版本就是HEAD^,上上个版本是HEAD^^,但是,向前回退100个不能写100个^,所以可以写为HEAD~100

现在,我们可以使用git reset命令回退到上一个版本,信息提示版本已经改变.

$ git reset --hard HEAD^
HEAD is now at c547aae add distributed

此时这里有个疑问,--hard参数有什么作用?这个问题稍后揭晓.

提示已经改变了版本,可以通过cat命令查看文件内容,再检测一遍结果

$ cat readme.txt
Git is a distributed version control system.
Git is free software.

果然文件内容已经被改变.

此时还可以继续回退到上一个版本,不过且慢,我们先通过git log看看版本库的状态

$ git log
commit c547aae2d26364c4d288bf8f5bd43ffce55d9aad (HEAD -> master)
Author: chris <1151211841@qq.com>
Date:   Sat Mar 2 12:52:46 2019 +0800

    add distributed

commit 57f1e2773ce411f108beb5c18bb7d7f970dc610b
Author: chris <1151211841@qq.com>
Date:   Wed Feb 27 12:57:23 2019 +0800

    wrote a readme file

此时,最新的那个版本append GPL已经找不到了,想再回去已经回不去了,怎么办?

办法其实还是有的,只要上面的命令行窗口还没有被关掉,还是可以向上翻记录,找到之前版本的commit id,还是可以指定回到未来的某个版本的

$ git reset --hard 3a05f4be13377870f52d4f245175cac70b368c37
HEAD is now at 3a05f4b append GPL

果然,最新版还是回来了,同时,commit id没必要写全,只要写到前几位,Git就可以自动去找.

Git的版本回退速度非常快,因为Git内部有一个指向当前版本HEAD的指针,当你回退版本的时候,Git仅仅把HEAD指向目标版本就好了,然后顺便将工作区的文件更新了,所以HEAD指向哪个版本号,你就把当前版本定位在哪.

但是刚才的问题还没有解决,如果回退完版本之后,关闭电脑,第二天又后悔了,想恢复到最新的版本怎么办?此时已经找不到最新版本的commit id了.

在Git中,总归是有后悔药可以吃的.当你用$ git reset --hard HEAD^回退到add distributed版本时,再想恢复到append GPL,就必须找到append GPLcommit id,所以Git暖心的给了一个命令git reflog,用来记录你的每一次命令:

$ git reflog
3a05f4b (HEAD -> master) HEAD@{0}: reset: moving to 3a05f4be13377870f52d4f245175cac70b368c37
c547aae HEAD@{1}: reset: moving to HEAD^
3a05f4b (HEAD -> master) HEAD@{2}: commit: append GPL
c547aae HEAD@{3}: commit: add distributed
57f1e27 HEAD@{4}: commit (initial): wrote a readme file

终于舒了口气,从信息可以看出append GPLcommit id3a05f4b,现在,又可以乘坐时光机回到未来了.

小结

现在总结一下:

  • HEAD指向的版本就是当前版本,因此,Git允许我们在版本的历史之间穿梭,使用命令git reset --hard commit_id
  • 穿梭前,用git log可以查看提交历史,以便确定要回退到哪个版本。
  • 要重返未来,用git reflog查看命令历史,以便确定要回到未来的哪个版本。

3.工作区和暂存区

Git和其他版本控制系统如SVN的一个不同之处就是有了暂存区的概念

先看一下名词解释

  1. 工作区(Working Directory)

    从英文释义上就能看出,工作中的文件目录,也就是电脑中能看到的目录,比如开始创建的learngit文件夹,就是一个工作区.

  2. 版本库(Repository)

    工作区有一个隐藏目录.git,这个不算工作区,而是Git的版本库.

    Git版本库里存了很多东西,其中最重要的就是stage(或者叫index)的暂存区 ,还有Git为我们自动创建的第一个分支master,以及指向master的一个指针叫HEAD.

    分支和HEAD的概念之后再讲

    前面讲了我们把文件往Git版本库里添加的时候是分两步进行的:

    1. 第一步是用git add把文件添加进去,实际上就是讲文件添加到暂存区;
    2. 第二步是用git commit提交更改,实际上就是把暂存区的所有内容提交到当前分支.

因为我们创建Git版本库时,Git自动为我们创建了唯一一个master分支,所以现在git commit就是往master分支上提交更改.

这里可以简单的理解为,需要提交的文件通通放到暂存区,然后,一次性提交暂存区所有修改.

俗话说的好,实践出真知.现在,我们再联系一遍,先对readme.txt做个修改,比如加上一行内容

Git is a distributed version control system.
Git is free software distributed under the GPL
Git has a mutable index called stage.

然后,在工作区新增一个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")

Git非常清楚的告诉我们,readme.txt文件被修改了,而license.txt文件还从来没有被添加过,所以他的状态是Untracked

现在,使用连词命令git add,把readme.txtlicense.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 add命令实际上就是把要提交的所有修改都放到了暂存区(stage),然后执行git commit就可以一次性把暂存区的所有修改提交到master分支.

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

一旦提交成功后,如果没有对工作区做出任何修改,那么工作区就是"干净"的:

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

4.管理修改

现在,假定你已经掌握了暂存区的概念.

下面要讨论的是,为什么Git比其他版本控制系统设计的优秀,因为Git跟踪并管理的是修改,而非文件.

这时候,你会问,什么是修改?新增了一行,删除了一行,修改了一个字符,新建了一个文件,这些都算作修改.

为什么说Git管理的是修改而非文件呢?我们还是做一个实验.

首先,我们对readme.txt做一个修改,比如添加一行内容

Git is a distributed version control system.
Git is free software distributed under the GPL
Git has a mutable index called stage.
Git tracks changes.

然后,添加到暂存区

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

        modified:   readme.txt

然后,再修改readme.txt文件

Git is a distributed version control system.
Git is free software distributed under the GPL
Git has a mutable index called stage.
Git tracks changes of files.

然后在不添加git add的情况下直接git commit提交:

$ git commit -m "git tracks changes"
[master f5e16fa] git tracks changes
 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")

这个时候,提示第二次的修改没有被提交.

这里,我们回顾一下操作工程:

第一次修改 -> git add -> 第二次修改 -> git commit

这里就证实了我们前边所说,Git管理的是修改,当git add命令后,在工作区的第一次修改被放入暂存区,但是第二次修改没有被放入暂存区,所以,git commit只负责把暂存区的修改提交了,也就是第一次的修改被提交了,但是第二次的修改没有被提交.

提交后,可以用git diff HEAD -- readme.txt命令可以查看工作区和版本库里面最新版本的区别:

$ git diff HEAD -- readme.txt
diff --git a/readme.txt b/readme.txt
index 5d2dfa2..f6b4816 100644
--- a/readme.txt
+++ b/readme.txt
@@ -1,4 +1,4 @@
 Git is a distributed version control system.
 Git is free software distributed under the GPL
 Git has a mutable index called stage.
-Git tracks changes.
\ No newline at end of file
+Git tracks changes of files.
\ No newline at end of file

这时,可以知道,第二次的修改确实没有被提交.

那么怎么提交第二次修改呢?你可以继续git addgit commit,也可以别急着提交第一次修改,可以先git add两次修改,再git commit,就相当于把两次修改合并后一块提交

  1. 第一次修改 -> git add -> git commit -> 第二次修改 -> git add -> git commit
  2. 第一次修改 -> git add -> 第二次修改 -> git add -> git commit

5.撤销修改

现在遇见了一种情况,在修改准备提交前,发现了一个错误,幸亏错误发现的及时,就很容易纠正他,可以直接删除一行,手动把文件恢复到上一个版本,此时用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告诉了你现在可以进行两种操作:一种就是前面提到的git add <file>提交操作,第二种就是可以使用git checkout -- readme.txt操作,丢弃对工作区的修改:

$ git checkout -- readme.txt

执行完成没有任何提示,但是,打开文件可以看到,文件内容已经被还原到之前提交的内容.

命令git checkout -- readme.txt意思就是,把readme.txt文件在工作区的修改全部撤销,这里有两种情况:

  • 一种是readme.txt自修改后没有被放到暂存区,现在撤销修改就回到和当前版本库一模一样的状态;
  • 一种是readme.txt已经添加到暂存区后,又做了修改,现在,撤销修改就回到了添加暂存区后的状态.

总之,就是让这个文件回到最近一次git add或者git commit的状态.

注意:这里git checkout -- readme.txt中的--很重要,没有--就变成了"切换到另一个分支"的命令,我们在后续的分支管理会再次遇到git checkout

现在,有出现了一个问题,在写错了之后,不但没有发现,还手欠的执行了git add,提交到了暂存区

庆幸的是,在commit之前发现了问题,用git status查看一下,修改只是添加到了暂存区,还没有被提交:

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

        modified:   readme.txt

这个时候,Git有告诉我们了,用git reset HEAD <file>命令,可以将暂存区的修改撤销掉,重新放回到工作区.

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

这里git reset命令既可以回退版本,也可以把暂存区的修改回退到工作区,当我们用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

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

现在,整个世界都安静了!

小结

  • 场景1:当你改乱了工作区某个文件的内容,想直接丢弃工作区的修改时,用命令git checkout -- file
  • 场景2:当你不但改乱了工作区某个文件的内容,还添加到了暂存区时,想丢弃修改,分两步,第一步用命令git reset HEAD <file>,就回到了场景1,第二步按场景1操作。
  • 场景3:已经提交了不合适的修改到版本库时,想要撤销本次提交,参考版本回退一节,不过前提是没有推送到远程库。

6.删除文件

在Git中,删除也是一个修改操作,我们实战一下,先添加一个新文件test.txt到Git并且提交:

$ git add test.txt

$ git commit -m "add test.txt"
[master d62c822] add test.txt
 1 file changed, 1 insertion(+)
 create mode 100644 test.txt

一般情况下,你通常直接在文件管理器只能给把没有的文件删除,或者用rm命令删除:

$ rm test.txt

$ git status
On branch master
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)

        deleted:    test.txt

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

这个时候,Git知道你删除了文件,因此,工作区和版本库就不一致了,git status命令会立刻告诉你哪些文件被删除了.

现在你有两个选择,一是确实要从版本库中删除该文件,那就用命令git rm删掉,并且git commit:

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

$ git commit -m "remove test.txt"
[master bd3a1f6] remove test.txt
 1 file changed, 1 deletion(-)
 delete mode 100644 test.txt

现在文件已经从版本库中删除了.

还有一种情况就是删除错了,因为版本库中还有记录,措意可以很轻松的将文件恢复到最新版本:

$ git checkout -- test.txt

git checkout其实是用版本库里的版本替换工作区的版本,无论工作区是修改还是删除,都可以"一键还原".

小结

命令git rm用于删除一个文件。如果一个文件已经被提交到版本库,那么你永远不用担心误删,但是要小心,你只能恢复文件到最新版本,你会丢失最近一次提交后你修改的内容

3.远程仓库

首先假设已经拥有了GitHub账号,由于你的本地Git仓库和GitHub仓库之间的传输是通过SSh加密的,所以还需要提前进行一些设置:

  1. 第一步,创建SSH Key,在用户主目录下,看看有没有.ssh目录,如果有,再看看这个目录下有没有id_rsaid_rsa.pub这两个文件,如果有了,就可以直接跳到下一步,如果没有,打开Git bash,创建SSH Key.

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

    这里将邮件地址换成自己的,然后一路回车默认设置,由于key也不是用于军事目的,所以也无需密码.

    如果一切顺利的话,可以在用户主目录中找到.ssh目录,并且目录里有id_rsaid_rsa.pub两个文件,这两个就是SSH Key的密钥对,id_rsa是私钥,不能泄露,id_rsa.pub是公钥,不必在意.

  2. 第二步,登录GitHub,打开"Account setting","SSH Keys"页面:

    然后,点"Add SSH Key",填上想要的Title,在Key文本框里粘贴id_rsa.pub文件的内容.

1551683583430

这里在向Key文本框里粘贴id_rsa.pub文件的内容时,一定注意将换行符删掉,否则会报错

此时可以看到已经存在了新添加的SSH Key:

1551683817077

为什么GitHub需要SSH Key呢?因为GitHub需要识别出你推送的消息确实是你推送的,而非别人冒充的,而Git支持SSH协议,所以,GitHub只要知道了你的公钥,就可以确定确实是你的推送.

当然.GitHub允许你添加多个Key,假定你有若干电脑,一会在公司提交,一会在家提交,这是只要将每台电脑的Key都添加到GitHub中,就可以实现每台电脑都能进行远程推送了.

最后友情提示,在GitHub上免费托管的Git仓库,任何人都可以看到喔(但只有你自己才能改)。所以,不要把敏感信息放进去。

如果你不想让别人看到Git库,有两个办法,一个是交点保护费,让GitHub把公开的仓库变成私有的,这样别人就看不见了(不可读更不可写)。另一个办法是自己动手,搭一个Git服务器,因为是你自己的Git服务器,所以别人也是看不见的。这个方法我们后面会讲到的,相当简单,公司内部开发必备。

在这里需要提醒一句,在我写这个文章的时候,GitHub已经支持了私有空间权限的设置

1.添加远程库

现在的情景是,你已经在本地创建了一个Git仓库后,又想在GitHub创建一个Git仓库,并且让这个两个仓库进行远程同步,这样,GitHub上的仓库既可以作为备份,有可以让其他人通过仓库来协作,真是一举多得.

首先,登录GitHub,然后点击头像旁边的"+",在下拉裂变中选择"new repository",创建新的仓库,在repository name中填入learngit,其他保持默认,点击"Create repository"按钮,就成功地创建了一个新的Git仓库:

1551684892527

点击确认之后,自动创建仓库,显示如下:

1551684969808

目前,GitHub上的learngit仓库还是空的,GitHub告诉我们,可以从这个仓库克隆出新的仓库,也可以把一个已有的本地仓库与之关联,然后,把本地仓库的内容推送到GitHub仓库.

现在,我们根据GitHub的提示,在本地的learngit仓库中运行命令:

git remote add origin https://github.com/Chris-0-0/learngit.git

这里需要注意,这里的命令需要按照自己创建的GitHub的仓库中的提示书写,我这里的chris-0-0需要替换成自己的GitHub账户名,否则关联的是我的远程库,虽然能够关联上,但是以后无法推送成功,因为你的SSH Key不在我的账户列表中.

添加后,远程库的名字就是origin,这是Git默认的叫法,也可以改成别的,但是origin这个名字一看就知道是远程库。

下一步,就可以把本地库的所有内容推送到远程库上:

$ git push -u origin master

Enumerating objects: 20, done.
Counting objects: 100% (20/20), done.
Delta compression using up to 4 threads
Compressing objects: 100% (15/15), done.
Writing objects: 100% (20/20), 1.62 KiB | 236.00 KiB/s, done.
Total 20 (delta 4), reused 0 (delta 0)
remote: Resolving deltas: 100% (4/4), done.
To https://github.com/Chris-0-0/learngit.git
 * [new branch]      master -> master
Branch 'master' set up to track remote branch 'master' from 'origin'.


这里我遇到了一个问题,在执行这条语句的时候需要登录GitHub的账号和密码,才能继续向下执行,否则提交失败

把本地库的内容推送到远程,用git push命令,实际上就是把当前分支master推送到远程.

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

推送成功后,可以立刻在GitHub页面中看到远程库的内容已经和本地一致.

从现在起,只要本地做了提交,就可以通过命令

$ git push origin master
Everything up-to-date

将本地master分支的最新修改推送至GitHub.

SSH警告

当你第一次使用Git的clone或者push命令连接GitHub时,会得到一个警告:

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

这是因为Git使用SSH连接,而SSH连接在第一次验证GitHub服务器的Key时,需要你确认GitHub的Key的指纹信息是否真的来自GitHub的服务器,输入yes回车即可。

Git会输出一个警告,告诉你已经把GitHub的Key添加到本机的一个信任列表里了:

Warning: Permanently added 'github.com' (RSA) to the list of known hosts.

这个警告只会出现一次,后面的操作就不会有任何警告了。

如果你实在担心有人冒充GitHub服务器,输入yes前可以对照GitHub的RSA Key的指纹信息是否与SSH连接给出的一致。

小结

要关联一个远程库,使用命令git remote add origin git@server-name:path/repo-name.git

关联后,使用命令git push -u origin master第一次推送master分支的所有内容;

此后,每次本地提交后,只要有必要,就可以使用命令git push origin master推送最新修改;

分布式版本系统的最大好处之一是在本地工作完全不需要考虑远程库的存在,也就是有没有联网都可以正常工作,而SVN在没有联网的时候是拒绝干活的!当有网络的时候,再把本地提交推送一下就完成了同步,真是太方便了!

2.从远程库克隆

上次讲了现有本地库,后有远程库的时候,如何关联远程库.

现在,假设我们从零开始,那么最好的方式肯定是先创建远程库,然后,从远程库克隆.

首先登录GitHub,创建一个新的gitskills仓库,参照上一章节操作,但是在创建仓库的时候,我们勾选Initialize this repository with a README选项,这样,GitHub会自动为我们创建一个README.md文件,创建完毕后,可以在仓库中看到README.md文件.

现在,远程仓库已经准备完毕,下一步可以使用git clone克隆一个本地库:

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

同理,这里我的chris-0-0仍旧需要替换自己的GitHub名称

然后,进入gitskills目录看看,是否已经有README.md文件了:

$ cd gitskills
$ pwd
/c/Users/Chris/gitskills
$ ls
README.md

如果有多个人协作开发,那么每个人各自从远程克隆一份就可以了。

你也许还注意到,GitHub给出的地址不止一个,还可以用https://github.com/chris-0-0/gitskills.git这样的地址。实际上,Git支持多种协议,默认的git://使用ssh,但也可以使用https等其他协议。

使用https除了速度慢以外,还有个最大的麻烦是每次推送都必须输入口令,但是在某些只开放http端口的公司内部就无法使用ssh协议而只能用https

小结

要克隆一个仓库,首先必须知道仓库的地址,然后使用git clone命令克隆。

Git支持多种协议,包括https,但通过ssh支持的原生git协议速度最快。

4.分支管理

1.创建和合并分支

分支就相当于每个人都有自己的分支,自己在自己的分支上边干活,大家互不影响,直到开发完毕,在一次性合并到原来的分支上,这样既安全又互不影响.

其他版本控制系统如SVN等都有分支管理,但是用过之后你会发现,这些版本控制系统创建和切换分支比蜗牛还慢,简直让人无法忍受,结果分支功能成了摆设,大家都不去用。

但Git的分支是与众不同的,无论创建、切换和删除分支,Git在1秒钟之内就能完成!无论你的版本库是1个文件还是1万个文件。

通过之前的学习,现在我们已经知道了,每次提交,Git都把他们串成一个时间线,这个时间线就是一个分支(类似于数据结构中的链表)

在Git中,截止目前为止,只有一条时间线,在Git中,这个分支叫做主分支,也叫master分支.HEAD严格来说不是指向提交,还是指向master,master才是指向提交的,所以,HEAD指向的是当前分支的最新

一开始的时候,master分支是一条线,Git用master指向最新的提交,再用HEAD指向master,就能确定当前分支,及当前分支的提交点(这个时候类似于一个链表).每次提交,master都会向前移动一步(类似于链表中添加了一个节点),随着不断提交,master分支线也越来越长.

这个时候,我们创建一个新的分支dev,Git就自动创建了一个新的指针dev,指向master相同的提交,再把HEAD指向dev,就表示当前分支在dev上.

这里,就可以理解为什么Git创建分支很快了,因为除了增加一个dev指针,改改HEAD的指向,工作区的文件没有任何变化.

不过,从此开始,对工作区的修改和提交就是针对dev分支了,比如新提交一次后,dev指针往前移动一步,但master指针不变,假设我们在dev上的工作已经完成,就可以将dev合并到master上.

那么,我们就会有一个疑问,Git怎么合并呢?

最简单的方式,就是直接把master指向dev的当前提交,就完成了合并.合并完分支后,我们甚至可以删除dev分支,删除dev分支也就是吧dev指针删除掉,删掉后,我们就只剩下一条master分支.

现在,我们开始实际操作一下

  1. 首先,创建dev分支,然后切换到dev分支:

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

    这里的git checkout命令加上-b参数表示创建并切换,可以拆分成一下两条命令:

    $ git branch dev
    $ git checkout dev
    Switched to branch 'dev'
    
    

    然后,可以用git branch命令查看当前分支:

    $ git branch
    * dev
      master
    
    

    这个命令会列出所有分支,当前分支前面会标一个*号.

    然后,我们可以在dev分支上正常提交,比如对readme.txt做个修改,添加一行文字,然后提交:

    $ vi readme.txt
    $ git add readme.txt
    $ git commit -m "branch test"
    [dev a5147c2] branch test
     1 file changed, 2 insertions(+), 1 deletion(-)
    
    

    现在,dev分支的工作已经完成,我们可以切换回master分支:

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

    切换回master分支后,再查看readme.txt文件,刚刚添加的内容不见了!因为那个提交是在dev分支上,而master分支此刻的提交点并没有改变.

    现在我们将dev分支合并到master上:

    $ git merge dev
    Updating bd3a1f6..a5147c2
    Fast-forward
     readme.txt | 3 ++-
     1 file changed, 2 insertions(+), 1 deletion(-)
    
    

    git merge命令用于合并指定分支到当前分支(如果想将dev分支合并到master分支,需要先切换到master分支),合并后再查看readme.txt的内容,就可以看到,和dev分支的最新提交是完全一样的.

    注意到上面的Fast-forward信息,Git告诉我们,这次合并是"快进模式",也就是直接将master指向dev的当前提交,所以合并速度非常快.

    当然,并不是每次合并都可以Fast-forward,后边还会有其他方式.

    合并完成后,就可以放心地删除dev分支了:

    $ git branch -d dev
    Deleted branch dev (was a5147c2).
    
    

    删除之后,查看git branch,就只有master了:

    $ git branch
    * master
    
    

    小结

    Git鼓励大量使用分支:

    查看分支:git branch

    创建分支:git branch <name>

    切换分支:git checkout <name>

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

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

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

2.解决冲突

人生不如意之事十之八九,合并分支往往都不是一帆风顺的.

准备一个新的feature1分支,继续我们新分支的开发:

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

修改readme.txt最后一行,改为:

$ vi readme.txt
Creating a new branch is quick AND simple.

feature1分支上提交:

$ git add readme.txt
$ git commit -m "AND simple"
[feature1 de0aa25] AND simple
 1 file changed, 1 insertion(+), 1 deletion(-)

切换到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)

此时Git会自动提示,我们当前的master分支比远程的master分支要超前1个提交.

这个我们在master分支上把readme.txt文件的最后一行改为:

$ vi readme.txt
Creating a new branch is quick & simple.

$ git add readme.txt
$ git commit -m "& simple"
[master faf2012] & simple
 1 file changed, 1 insertion(+), 1 deletion(-)

这种情况下,masterfeature1两个分支同时做了修改,Git无法进行"快速合并",只能试图将各自的修改合并起来,但这种合并可能会有冲突,我们可以试试看:

$ 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告诉我们,readme.txt文件存在冲突,必须手动解决冲突后再提交.git status也可以告诉我们冲突的文件:

$ 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")

我们这里可以通过cat <file>查看文件内容:

$ cat readme.txt
Git is a distributed version control system.
Git is free software distributed under the GPL.
Git has a mutable index called stage.
Git tracks changes of files.
<<<<<<< HEAD
Creating a new branch is quick & simple.
=======
Creating a new branch is quick AND simple.
>>>>>>> feature1

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

Creating a new branch is quick and simple.

再提交:

$ git add readme.txt 
$ git commit -m "conflict fixed"
[master cf810e4] conflict fixed

这里用git log --graph --pretty=oneline --abbrev-commit可以查看分支的合并情况:

$ git log --graph --pretty=oneline --abbrev-commit
*   5fd90f7 (HEAD -> master) conflict fixed
|\
| * de0aa25 (feature1) AND simple
* | faf2012 & simple
|/
* a5147c2 branch test
* bd3a1f6 (origin/master) remove test.txt
* d62c822 add test.txt
* f5e16fa git tracks changes
* aaa067c understand how stage works
* 3a05f4b append GPL
* c547aae add distributed
* 57f1e27 wrote a readme file

最后,删除feature1分支:

$ git branch -d feature1
Deleted branch feature1 (was de0aa25).

小结

当Git无法自动合并分支时,就必须先解决冲突.解决完冲突后,再提交,合并完成.

解决冲突就是把Git合并失败的文件手动编辑为我们希望的内容,再提交。

git log --graph命令可以看到分支合并图。

3.分支管理策略

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

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

那么如何强制禁用Fast forward模式?在Git中可以使用--no-ff进行git merge,这样就能禁用Fast forward,现在实际操作一下:

首先,任然切换到dev分支:

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

修改readme.txt文件,并提交一个commit:

$ vi readme.txt
$ git add readme.txt
$ git commit -m "and merge"
[dev 60352c2] and merge
 1 file changed, 1 insertion(+)

现在,切换回master:

$ git checkout master
Switched to branch 'master'

然后开始合并dev分支,这里注意使用--no-ff参数,表示禁用Fast forward

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

因为本次合并会在禁用Fast forward的同时创建一个新的commit分支,所以需要加上-m参数,添加commit描述信息.

合并后我们查看git log:

$ git log --graph --pretty=oneline --abbrev-commit
*   a3eb38b (HEAD -> master) merge with no-ff
|\
| * 60352c2 (dev) and merge
|/
*   5fd90f7 conflict fixed
|\
| * de0aa25 AND simple
* | faf2012 & simple
|/
* a5147c2 branch test
* bd3a1f6 (origin/master) remove test.txt
* d62c822 add test.txt
* f5e16fa git tracks changes
* aaa067c understand how stage works
* 3a05f4b append GPL
* c547aae add distributed
* 57f1e27 wrote a readme file

分支策略

在实际开发中我们应该按照几个基本原则进行分支管理:

首先,确保master分支是稳定的,也就是仅仅用来发布新版本,平时不能在上边干活.

其次,我们干活的应该都在dev分支上,也就是说dev分支是不稳定的,需要到特定的时间点,比如V1.0版本发布,再将dev分支合并到master上,再从master分支上发布V1.0版本.

这样为了方便,我们每一个开发的小伙伴又都可以在dev分支上建立自己的分支,在完成自己工作之后,将自己的分支在提交合并到dev分支,就可以了.

小结

Git分支十分强大,在团队开发中应该充分应用。

合并分支时,加上--no-ff参数就可以用普通模式合并,合并后的历史有分支,能看出来曾经做过合并,而fast forward合并就看不出来曾经做过合并。

4.bug分支

在软件开发中,bug就像家常便饭一样.有了bug就需要修复,在Git中,由于分支如此强大,所以,每个bug都可以通过一个新的临时分支来修复,修复后,合并分支,然后将临时分支删除.

现在可以想象一下,现在接到了一个代号为101的bug,很自然的,你就想创建一个分支issue-101来修复他,但是,当前正在dev上进行的工作还没有提交:

$ git status
On branch dev
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")

并非不想提交,而是不能提交,因为对应的任务还没有做完,但是bug必须及时修复,怎么办?

这个时候,Git提供了一个stash功能,就是可以吧当前的工作现场"贮藏"起来,等改完bug再恢复现场继续工作:

$ git stash
Saved working directory and index state WIP on dev: 60352c2 and merge

现在,git status就可以看到工作区已经没有修改了,因此可以放心的创建分支修复bug.

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

现在,保存完工作现场后,就可以开始修改bug了,首先确定bug需要在哪个分支上进行更改,假设需要在master分支上进行修改,就从master创建临时分支:

$ git checkout master
Switched to branch 'master'
Your branch is ahead of 'origin/master' by 6 commits.
  (use "git push" to publish your local commits)
  
$ git checkout -b issue-101
Switched to a new branch 'issue-101'

现在修复bug,然后进行提交:

$ vi readme.txt
$ git add readme.txt
$ git commit -m "fix bug 101"
[issue-101 d3475b2] fix bug 101
 1 file changed, 1 insertion(+)

修改完成后,切换到master分支,并合并分支,最后删除issue-101分支:

$ git checkout master
Switched to branch 'master'
Your branch is ahead of 'origin/master' by 6 commits.
  (use "git push" to publish your local commits)
  
$ git merge --no-ff -m "merged bug fix 101"
Already up to date.

$ git branch -D issue-101
Deleted branch issue-101 (was d3475b2).

现在,可以回到之前的工作现场继续工作了:

$ git checkout dev
Switched to branch 'dev'

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

但是工作现场还没有恢复,这里有两个办法:

  • 一是用git stash apply恢复,但是恢复后,stash需要通过git stash drop手动删除
  • 二是用git stash pop,恢复的同时自动删除stash
$ git stash pop
On branch dev
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")
Dropped refs/stash@{0} (40c2d562ac584761e59fffeafee792f883efc252)

这时,已经无法看到刚才创建的stash了:

$ git stash list

这里也可以多次stash,恢复的时候先用git stash list查看,然后恢复对应的stash:

$ git stash list
stash@{0}: WIP on dev: 60352c2 and merge
stash@{1}: WIP on dev: 60352c2 and merge

$ git stash apply stash@{0}

5.Feature分支

软件开发中,总有无穷无尽的新的功能要不断的添加进来.

添加新的功能时,肯定不希望因为一些实验形式的额代码,把主分支搞乱了,所以,每添加一个新的功能,最好新建一个feature分支,在上面进行开发,完成后,合并并删除分支.

现在,接到了一个开发代号为Vulcan的新功能,于是开始准备开发:

首先新建一个feature-vulcan分支:

$ git checkout -b feature-vulcan
Switched to a new branch 'feature-vulcan'

然后在开发完成后,进行提交:

$ git add feature.txt

Chris@DESKTOP-77TKV6B MINGW64 ~/learngit (feature-vulcan)
$ git status
On branch feature-vulcan
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

        new file:   feature.txt

$ git commit -m "add feature vulcan"
[feature-vulcan 78d4a69] add feature vulcan
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 feature.txt

提交完成后,切换回dev,准备合并:

$ git checkout dev
Switched to branch 'dev'

一切顺利的话,feature分支和bug分支是类似的,合并完成后直接删除.

但是,上级觉得没必要,功能被删除了,虽然活白干了,但是分支必须销毁.

这时如果我们直接删除,

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

Git会告诉我们报错,并且告诉了解决方法,可以用大写的-D强制删除:

$ git branch -D feature-vulcan
Deleted branch feature-vulcan (was 78d4a69).

小结

开发一个新feature,最好新建一个分支;

如果要丢弃一个没有被合并过的分支,可以通过git branch -D <name>强行删除。

6.多人协作

当你从远程仓库克隆时,实际上Git自动把本地的master分支和远程的master分支对应起来了,并且远程仓库的默认名称是origin.

要查看远程库的信息,用git remote:

$ git remote
origin

或者,用git remote -v显示更详细的的信息:

$ git remote -v
origin  https://github.com/Chris-0-0/learngit.git (fetch)
origin  https://github.com/Chris-0-0/learngit.git (push)

上面显示了可以抓取和推送的地址.如果没有推送权限,就看不到push地址.

推送分支

推送分支,就是把该分支上的所有本地提交推送到远程库.推送时,要指定本地分支,这样,Git就会把该分支推送到对应的远程分支上.

比如要推送master分支到远程origin分支:

$ git push origin master
Enumerating objects: 18, done.
Counting objects: 100% (18/18), done.
Delta compression using up to 4 threads
Compressing objects: 100% (16/16), done.
Writing objects: 100% (16/16), 1.37 KiB | 281.00 KiB/s, done.
Total 16 (delta 7), reused 0 (delta 0)
remote: Resolving deltas: 100% (7/7), completed with 1 local object.
To https://github.com/Chris-0-0/learngit.git
   bd3a1f6..a3eb38b  master -> master

如果要推送其他分支,比如dev,就改成:

$ git push origin dev
Total 0 (delta 0), reused 0 (delta 0)
remote:
remote: Create a pull request for 'dev' on GitHub by visiting:
remote:      https://github.com/Chris-0-0/learngit/pull/new/dev
remote:
To https://github.com/Chris-0-0/learngit.git
 * [new branch]      dev -> dev

但是,并不是一定要把本地分支往远程推送,那么,哪些分支需要推送,哪些不需要呢?

  • master分支是主分支,因此要时刻与远程同步;
  • dev分支是开发分支,团队所有成员都需要在上面工作,所以也需要与远程同步;
  • bug分支只用于在本地修复bug,就没必要推到远程了,除非老板要看看你每周到底修复了几个bug;
  • feature分支是否推到远程,取决于你是否和你的小伙伴合作在上面开发。

总之,就是在Git中,分支完全可以在本地自己藏着玩,是否推送,视你的心情而定!

抓取分支

多人协作时,大家都会往master分支和dev分支上推送各自的修改.

现在,模拟一个你的小伙伴,可以在另一台电脑(注意要把SSH Key添加到GitHub)或者同一台电脑的另一个目录下克隆:

$ git clone git@github.com:chris-0-0/learngit.git
Cloning into 'learngit'...
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
Warning: Permanently added 'github.com,52.74.223.119' (RSA) to the list of known hosts.
remote: Enumerating objects: 36, done.
remote: Counting objects: 100% (36/36), done.
remote: Compressing objects: 100% (20/20), done.
remote: Total 36 (delta 11), reused 36 (delta 11), pack-reused 0
Receiving objects: 100% (36/36), done.
Resolving deltas: 100% (11/11), done.

当你的小伙伴从远程库clone时,默认情况下,你的额小伙伴只能看到本地的master分支.不信可以用git branch查看:

$ git branch
* master

现在,小伙伴想要在dev分支上开发,就必须创建远程origindev分支到本地,于是他用这个命令创建本地dev分支:

$ git checkout -b dev origin/dev
Switched to a new branch 'dev'
Branch 'dev' set up to track remote branch 'dev' from 'origin'.

现在,他就可以在dev上继续修改,然后,时不时地吧dev分支push到远程:

$ git add env.txt

$ git commit -m "add env"
[dev 7a5e5dd] add env
 1 file changed, 1 insertion(+)
 create mode 100644 env.txt

$ git push origin dev
Counting objects: 3, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 308 bytes | 308.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0)
To github.com:michaelliao/learngit.git
   f52c633..7a5e5dd  dev -> dev

现在你的小伙伴已经向origin/dev分支推送了他的提交,而碰巧你也同样的文件作出修改,并试图推送:

$ touch env.txt

$ vi env.txt

$ cat env.txt
env

$ git add env.txt

$ git commit -m "add new env"
[dev 0ae46b6] add new env
 1 file changed, 1 insertion(+)
 create mode 100644 env.txt

$ git push origin dev
To https://github.com/Chris-0-0/learngit.git
 ! [rejected]        dev -> dev (fetch first)
error: failed to push some refs to 'https://github.com/Chris-0-0/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.

推送失败,因为你的小伙伴的最新提交和你试图推送的提交有冲突,解决办法也很简单,Git提示我们先用git pull吧最新的提交从origin/dev抓下来,然后,在本地合并,解决冲突,再推送:

$ git pull
remote: Enumerating objects: 4, done.
remote: Counting objects: 100% (4/4), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 0), reused 3 (delta 0), pack-reused 0
Unpacking objects: 100% (3/3), done.
From https://github.com/Chris-0-0/learngit
   60352c2..96b7c82  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

然后这里直接pull的话也失败了,原因是没有指定本地dev分支与远程origin/dev分支的连接,根据提示,设置devorigin/dev的连接:

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

现在再git pull:

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

这里输入完git pull之后,会弹出vi编辑,填写提交信息.

但是这里pull成功了,但是合并有冲突,需要手动解决,解决的方法和分支管理中的解决冲突完全一样.解决后,再提交,再push:

$ git commit -m "fix env conflict"
[dev 57c53ab] fix env conflict

$ git push origin dev
Counting objects: 6, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (4/4), done.
Writing objects: 100% (6/6), 621 bytes | 621.00 KiB/s, done.
Total 6 (delta 0), reused 0 (delta 0)
To github.com:michaelliao/learngit.git
   7a5e5dd..57c53ab  dev -> dev

因此,多人协作的工作模式通常是这样:

  1. 首先,可以试图用git push origin <branch-name>推送自己的修改;
  2. 如果推送失败,则因为远程分支比你的本地更新,需要先用git pull试图合并;
  3. 如果合并有冲突,则解决冲突,并在本地提交;
  4. 没有冲突或者解决掉冲突后,再用git push origin <branch-name>推送就能成功!

如果git pull提示no tracking information,则说明本地分支和远程分支的链接关系没有创建,用命令git branch --set-upstream-to <branch-name> origin/<branch-name>

这就是多人协作的工作模式,一旦熟悉了,就非常简单。

小结

  • 查看远程库信息,使用git remote -v
  • 本地新建的分支如果不推送到远程,对其他人就是不可见的;
  • 从本地推送分支,使用git push origin branch-name,如果推送失败,先用git pull抓取远程的新提交;
  • 在本地创建和远程分支对应的分支,使用git checkout -b branch-name origin/branch-name,本地和远程分支的名称最好一致;
  • 建立本地分支和远程分支的关联,使用git branch --set-upstream branch-name origin/branch-name
  • 从远程抓取分支,使用git pull,如果有冲突,要先处理冲突。

7.Rebase

在上一节我们看到了,多人在同一个分支上协作时,很容易出现冲突。即使没有冲突,后push的童鞋不得不先pull,在本地合并,然后才能push成功。

每次合并再push后,分支变成了这样:

$ git log --graph --pretty=oneline --abbrev-commit
* d1be385 (HEAD -> master, origin/master) init hello
*   e5e69f1 Merge branch 'dev'
|\  
| *   57c53ab (origin/dev, dev) fix env conflict
| |\  
| | * 7a5e5dd add env
| * | 7bd91f1 add new env
| |/  
* |   12a631b merged bug fix 101
|\ \  
| * | 4c805e2 fix bug 101
|/ /  
* |   e1e9c68 merge with no-ff
|\ \  
| |/  
| * f52c633 add merge
|/  
*   cf810e4 conflict fixed

这时候,就有了新的需要,Git提交历史为什么不能是一条干净的直线?

其实是有的,Git有一种成为rebase的操作,有人把它翻译为"变基".

当我们的提交历史经过多个分支提交的时候,这个操作就派上用场了:

$ git rebase
First, rewinding head to replay your work on top of it...
Applying: add comment
Using index info to reconstruct a base tree...
M    hello.py
Falling back to patching base and 3-way merge...
Auto-merging hello.py
Applying: add author
Using index info to reconstruct a base tree...
M    hello.py
Falling back to patching base and 3-way merge...
Auto-merging hello.py

原本分叉的提交现在变成一条直线了!这种神奇的操作是怎么实现的?其实原理非常简单。我们注意观察,发现Git把我们本地的提交“挪动”了位置,放到了f005ed4 (origin/master) set exit=1之后,这样,整个提交历史就成了一条直线。rebase操作前后,最终的提交内容是一致的,但是,我们本地的commit修改内容已经变化了,它们的修改不再基于d1be385 init hello,而是基于f005ed4 (origin/master) set exit=1,但最后的提交7e61ed4内容是一致的。

这就是rebase操作的特点:把分叉的提交历史“整理”成一条直线,看上去更直观。缺点是本地的分叉提交已经被修改过了。

小结

  • rebase操作可以把本地未push的分叉提交历史整理成直线;
  • rebase的目的是使得我们在查看历史提交的变化时更容易,因为分叉的提交需要三方对比。

5.标签管理

发布一个版本时,我们通常先在版本库中打一个标签(tag),这样,就唯一确定了打标签时刻的版本。将来无论什么时候,取某个标签的版本,就是把那个打标签的时刻的历史版本取出来。所以,标签也是版本库的一个快照。

Git的标签虽然是版本库的快照,但其实它就是指向某个commit的指针(跟分支很像对不对?但是分支可以移动,标签不能移动),所以,创建和删除标签都是瞬间完成的。

Git有commit,为什么还要引入tag?

“请把上周一的那个版本打包发布,commit号是6a5819e...”

“一串乱七八糟的数字不好找!”

如果换一个办法:

“请把上周一的那个版本打包发布,版本号是v1.2”

“好的,按照tag v1.2查找commit就行!”

所以,tag就是一个让人容易记住的有意义的名字,它跟某个commit绑在一起。

1.创建标签

在Git上打标签非常简单,首先,切换到需要打标签的分支上:

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

然后,敲命令git tag <name>就可以打一个新标签:

$ git tag v1.0

然后可以通过git tag命令查看所有标签:

$ git tag
v1.0

默认标签是打在最新提交的commit上的,有时候,如果忘了打标签,比如,现在已经是周五了,但是应该是周一打的标签没有打,怎么办?

方法是找到历史提交的commit id,然后打上就可以了:

$ git log --pretty=oneline --abbrev-commit
a3eb38b (HEAD -> master, tag: v1.0, origin/master) merge with no-ff
60352c2 and merge
5fd90f7 conflict fixed
faf2012 & simple
de0aa25 AND simple
a5147c2 branch test
bd3a1f6 remove test.txt
d62c822 add test.txt
f5e16fa git tracks changes
aaa067c understand how stage works
3a05f4b append GPL
c547aae add distributed
57f1e27 wrote a readme file

假如要给add merge这次提交打标签,它对应的commit id是60352c2,可以这么操作:

$ git tag v0.9 60352c2

再查看一下标签:

$ git tag
v0.9
v1.0

注意,标签不是按照时间顺序列出,而是按照字母顺序排列的,可以用git show <tagname>查看标签信息:

$ git show v0.9
commit 60352c2528735b0d79420ab15400659b88d06973 (tag: v0.9)
Author: chris <1151211841@qq.com>
Date:   Tue Mar 5 13:59:02 2019 +0800

    and merge

diff --git a/readme.txt b/readme.txt
index 2ff7c03..f6a91dd 100644
--- a/readme.txt
+++ b/readme.txt
@@ -3,3 +3,4 @@ Git is free software distributed under the GPL
 Git has a mutable index called stage.
 Git tracks changes.
 Creating a new branch is quick and simple.
+and merge

可以看到,v0.9确实打在了add merge这次提交上.

还可以创建带有说明的标签,其中-a指定标签名,-m指定说明文字:

$ git tag -a v0.1 -m "version 0.1 released" 57f1e27

命令git show <tagname>可以查看说明:

$ git show v0.1
tag v0.1
Tagger: chris <1151211841@qq.com>
Date:   Thu Mar 7 15:59:40 2019 +0800

version 0.1 released

commit 57f1e2773ce411f108beb5c18bb7d7f970dc610b (tag: v0.1)
Author: chris <1151211841@qq.com>
Date:   Wed Feb 27 12:57:23 2019 +0800

    wrote a readme file

diff --git a/readme.txt b/readme.txt
new file mode 100644
index 0000000..d8036c1
--- /dev/null
+++ b/readme.txt
@@ -0,0 +1,2 @@
+Git is a version control system.
+Git is free software.
\ No newline at end of file

小结

  • 命令git tag <tagname>用于新建一个标签,默认为HEAD,也可以指定一个commit id;
  • 命令git tag -a <tagname> -m "blablabla..."可以指定标签信息;
  • 命令git tag可以查看所有标签。

2.操作标签

首先是标签的删除:

$ git tag -d v0.1
Deleted tag 'v0.1' (was 670741a)

因为创建的标签只存储本地,不会自动推送到远程,所以,标签可以直接在本地安全删除.

如果要推送某个标签到远程,使用命令git push origin <tagname>:

$ git push origin v1.0
Total 0 (delta 0), reused 0 (delta 0)
To https://github.com/Chris-0-0/learngit.git
 * [new tag]         v1.0 -> v1.0

或者,任性一点的话,可以一次性推送全部尚未推送到远程的本地标签:

$ git push origin --tags
Total 0 (delta 0), reused 0 (delta 0)
To https://github.com/Chris-0-0/learngit.git
 * [new tag]         v0.9 -> v0.9

但是,任性完了是需要负责任的,现在如果再想删除远程标签就麻烦了,需要先从本地删除:

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

然后你还需要为之前的任性擦屁股,在本地删除后,还需要从远程删除,命令也是push:

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

小结

  • 命令git push origin <tagname>可以推送一个本地标签;
  • 命令git push origin --tags可以推送全部未推送过的本地标签;
  • 命令git tag -d <tagname>可以删除一个本地标签;
  • 命令git push origin :refs/tags/<tagname>可以删除一个远程标签。

6.使用GitHub参与开源项目

如何参与一个开源项目呢?比如人气极高的bootstrap项目,这是一个非常强大的CSS框架,你可以访问它的项目主页https://github.com/twbs/bootstrap,点“Fork”就在自己的账号下克隆了一个bootstrap仓库,然后,从自己的账号下clone:

git clone git@github.com:michaelliao/bootstrap.git

一定要从自己的账号下clone仓库,这样你才能推送修改。如果从bootstrap的作者的仓库地址git@github.com:twbs/bootstrap.git克隆,因为没有权限,你将不能推送修改。

如果你想修复bootstrap的一个bug,或者新增一个功能,立刻就可以开始干活,干完后,往自己的仓库推送。

如果你希望bootstrap的官方库能接受你的修改,你就可以在GitHub上发起一个pull request。当然,对方是否接受你的pull request就不一定了。

如果嫌弃GitHub的连接太慢,也可以使用中国的码云,与GitHub的操作是相同的.

关键

如果这个时候,用了码云之后,现在又想用一下GitHub,这个时候也是可以关联两个远程库的:

首先,先关联GitHub的远程库:

git remote add github git@github.com:chris-0-0/learngit.git

注意:这里的远程库的名称就不叫origin了,而是叫github.

接着关联码云的远程库:

git remote add gitee git@gitee.com:Chris_0/learngit.git

同样这里的远程库的名称叫gitee,而非origin.

然后可以用git remote -v查看远程库信息:

git remote -v
gitee    git@gitee.com:chris_0/learngit.git (fetch)
gitee    git@gitee.com:chris_0/learngit.git (push)
github    git@github.com:chris-0-0/learngit.git (fetch)
github    git@github.com:chris-0-0/learngit.git (push)

现在的推送命令就需要修改了:

推送到GitHub:

git push github master

推送到码云:

git push gitee master

7.自定义Git

在安装Git中,我们配置了user.nameuser.email,实际上,Git还有很多可配置项.

比如,让Git显示颜色,会让命令输出看起来更醒目:

$ git config --global color.ui true

1.忽略特殊文件

有些时候,你必须把某些文件放到Git工作目录中,但又不想提交,这样每次git status都会显示Untracked files ...,就会很不舒服,好在Git帮忙解决了这个问题,在Git工作区的根目录下创建一个特殊的.gitignore文件,然后把要忽略的文件名填进去,Git就会自动忽略这些文件.

不需要从头写.gitignore文件,GitHub已经为我们准备了各种配置文件,只需要组合一下就可以使用了。所有配置文件可以直接在线浏览:https://github.com/github/gitignore

忽略文件的原则是:

  1. 忽略操作系统自动生成的文件,比如缩略图等;
  2. 忽略编译生成的中间文件、可执行文件等,也就是如果一个文件是通过另一个文件自动生成的,那自动生成的文件就没必要放进版本库,比如Java编译产生的.class文件;
  3. 忽略你自己的带有敏感信息的配置文件,比如存放口令的配置文件。

只要编辑好gitignore文件后,将它提交到Git,Git就能自动忽略了.

同时假如这个文件已经被忽略了但是你就是想任性的非要提交他也是可以的,只要加上-f就可以强制提交了:

$ git add -f App.class

同时也可以查看.gitignore文件是否配置了忽略文件:

$ git check-ignore -v App.class
.gitignore:3:*.class    App.class

小结

  • 忽略某些文件时,需要编写.gitignore
  • .gitignore文件本身要放到版本库里,并且可以对.gitignore做版本管理!

2.配置别名

接下来就是懒人福利了.

如果感觉git status命令复杂,想要将它编程git st?只需一行命令:

$ git config --global alias.st status

当然,别的命令也都是可以的:

比如:co表示checkout,ci表示commit,br表示branch:

$ git config --global alias.co checkout
$ git config --global alias.ci commit
$ git config --global alias.br branch

提交以后就可以变成:

$ git ci -m "bala bala bala..."

--global参数是全局参数,也就是这些命令在这台电脑的所有Git仓库都可用.

撤销修改一节中,我们知道,命令git reset HEAD file可以把暂存区的修改撤销掉(unstage),重新放回工作区。既然是一个unstage操作,就可以配置一个unstage别名:

$ git config --global alias.unstage 'reset HEAD'

甚至还有人丧心病狂地把lg配置成了:

git config --global alias.lg "log --color --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit"

配置文件

配置Git的时候,加上--global是针对当前用户起作用的,如果不加,那只针对当前的仓库起作用。

配置文件放哪了?每个仓库的Git配置文件都放在.git/config文件中:

$ cat .git/config 
[core]
    repositoryformatversion = 0
    filemode = true
    bare = false
    logallrefupdates = true
    ignorecase = true
    precomposeunicode = true
[remote "origin"]
    url = git@github.com:michaelliao/learngit.git
    fetch = +refs/heads/*:refs/remotes/origin/*
[branch "master"]
    remote = origin
    merge = refs/heads/master
[alias]
    last = log -1

别名就在[alias]后面,要删除别名,直接把对应的行删掉即可。

而当前用户的Git配置文件放在用户主目录下的一个隐藏文件.gitconfig中:

$ cat .gitconfig
[alias]
    co = checkout
    ci = commit
    br = branch
    st = status
[user]
    name = Your Name
    email = your@email.com

配置别名也可以直接修改这个文件,如果改错了,可以删掉文件重新通过命令配置。

3.搭建Git服务器

终于到了重点环节,搭建属于自己的Git服务器:

首先需要一台Linux服务器,最好是Ubuntu或Debian,这样可以直接通过简单的apt命令完成.

第一步,安装Git:

$ sudo apt-get install git

第二步,创建一个git用户,用来运行git服务:

$ sudo adduser git

第三步,创建证书登录:

手机所有需要登录的用户的公钥,就是他们自己的id_rsa.pub文件,把所有的公钥导入到/home/git/.ssh/authorized_keys文件中,一行一个.

第四步,初始化Git仓库:

先选定一个目录作为Git仓库,假定是/srv/sample.git,在/srv目录下输入命令:

$ sudo git init --bare sample.git

Git就会创建一个裸仓库,裸仓库没有工作区,因为服务器上的Git仓库纯粹为了共享,所以不让用户直接登录到服务器上去改工作区,并且服务器上的Git仓库通常以.git结尾,然后,把owner改为git:

$ sudo chown -R git:git sample.git

第五步,禁用shell登录:

处于安全考虑,第二部创建的git用户不允许登录shell,这可以通过etc/password文件完成.找到类似下面一行:

git:x:1001:1001:,,,:/home/git:/bin/bash

改为:

git:x:1001:1001:,,,:/home/git:/usr/bin/git-shell

这样,git用户可以正常通过ssh使用Git了,但无法登陆shell,因为我们为git用户指定的git-shell每次登陆就自动退出.

第六步,克隆远程仓库:

现在可以通过git clone命令克隆远程仓库了,:

$ git clone git@server:/srv/sample.git
Cloning into 'sample'...
warning: You appear to have cloned an empty repository.

管理公钥

如果团队很小,把每个人的公钥收集起来放到服务器的/home/git/.ssh/authorized_keys文件里就是可行的。如果团队有几百号人,就没法这么玩了,这时,可以用Gitosis来管理公钥。

这里我们不介绍怎么玩Gitosis了,几百号人的团队基本都在500强了,相信找个高水平的Linux管理员问题不大。

管理权限

有很多不但视源代码如生命,而且视员工为窃贼的公司,会在版本控制系统里设置一套完善的权限控制,每个人是否有读写权限会精确到每个分支甚至每个目录下。因为Git是为Linux源代码托管而开发的,所以Git也继承了开源社区的精神,不支持权限控制。不过,因为Git支持钩子(hook),所以,可以在服务器端编写一系列脚本来控制提交等操作,达到权限控制的目的。Gitolite就是这个工具。

posted @ 2019-12-30 14:42  谁动了我的code  阅读(110)  评论(0)    收藏  举报