开发环境
名称 | 版本 |
---|---|
操作系统 | Windows 10 X64 |
Git | 2.26.2 |
来源
重要说明
本文的内容全部来自 廖雪峰-Git教程 结合自己的实操所作的记录,不是我写的。里面的文字描述都来自 廖雪峰-Git教程。
Git 简介
版本控制工具
1.集中管理型版本控制
集中管理型版本控制可能会遇到的几个问题
(1)版本管理的服务器一旦崩溃,硬盘损坏,代码如何恢复?
(2)程序员上传到服务器的代码要求是完整版本,但是程序 员开发过程中想做小版本的管理,以便追溯查询,怎么破?
(3)系统正在上线运行,时不时还要修改bug,要增加好几个功能要几个月,如何管理几个版本?
(4)如何管理一个分布在世界各地、互不相识的大型开发团队?
2.分布式版本控制
相关工具地址
1.命令行工具:Git for windows 下载地址:https://git-for-windows.github.io/
2.操作系统中可视化工具:TortoiseGit 下载地址: https://tortoisegit.org/
3.Eclipse插件: Egit Eclipse自带,插件市场搜索最新版
4.GitHub网站 http://www.github.com
在Windows 上安装 Git
下载 https://git-scm.com/downloads
运行 Git-2.26.2-64-bit.exe
,然后按默认选项安装即可。
安装完成后,在开始菜单里找到 Git
->Git Bash
出现一个类似命令行窗口的东西,就说明 Git 安装成功!
初始化用户名和邮箱设置
安装完成后,还需要最后一步设置,在命令行输入:
git config --global user.name "luoma"
git config --global user.email "luoma_haowei@qq.com"
因为 Git 是分布式版本控制系统,所以,每个机器都必须自报家门:你的名字和 Email 地址。你也许会担心,如果有人故意冒充别人怎么办?这个不必担心,首先我们相信大家都是善良无知的群众,其次,真的有冒充的也是有办法可查的。
注意 git config
命令的 --global
参数,用了这个参数,表示你这台机器上所有的 Git 仓库都会使用这个配置,当然也可以对某个仓库指定不同的用户名和 Email 地址。
打开对应用户下的 C:\Users\Administrator\.gitconfig
文件,可以看到如下信息
[user]
name = luoma
email = luoma_haowei@qq.com
创建版本库
什么是版本库呢?版本库又名仓库,英文名 repository
,你可以简单理解成一个目录,这个目录里面的所有文件都可以被 Git 管理起来,每个文件的修改、删除,Git 都能跟踪,以便任何时刻都可以追踪历史,或者在将来某个时刻可以“还原”。
所以,创建一个版本库非常简单,首先,选择一个合适的地方,创建一个空目录 D:\v_hwhao\GitTest
:
$ cd d
$ cd v_hwhao
$ cd GitTest
$ pwd
pwd
命令用于显示当前目录。在我的电脑上,这个仓库位于 D:\v_hwhao\GitTest
。
为了避免遇到各种莫名其妙的问题,请确保目录名(包括父目录)不包含中文。
通过 git init
命令把这个目录变成 Git 可以管理的仓库:
$ git init
Initialized empty Git repository in D:/v_hwhao/GitTest/.git/
瞬间 Git 就把仓库建好了,而且告诉你是一个空的仓库(empty Git repository),细心的读者可以发现当前目录下多了一个 .git
的目录,这个目录是 Git 来跟踪管理版本库的,没事千万不要手动修改这个目录里面的文件,不然改乱了,就把 Git 仓库给破坏了。
如果你没有看到 .git
目录,那是因为这个目录默认是隐藏的,用 ls -ah
命令就可以看见。
也不一定必须在空目录下创建 Git 仓库,选择一个已经有东西的目录也是可以的。
把文件添加到版本库
首先这里再明确一下,所有的版本控制系统,其实只能跟踪文本文件的改动,比如 TXT 文件,网页,所有的程序代码等等,Git 也不例外。版本控制系统可以告诉你每次的改动,比如在第5行加了一个单词“Linux”,在第8行删了一个单词“Windows”。而图片、视频这些二进制文件,虽然也能由版本控制系统管理,但没法跟踪文件的变化,只能把二进制文件每次改动串起来,也就是只知道图片从 100KB 改成了 120KB,但到底改了啥,版本控制系统不知道,也没法知道。
不幸的是,Microsoft 的 Word 格式是二进制格式,因此,版本控制系统是没法跟踪 Word文件的改动的。
因为文本是有编码的,比如中文有常用的 GBK 编码,日文有 Shift_JIS 编码,如果没有历史遗留问题,强烈建议使用标准的 UTF-8 编码,所有语言使用同一种编码,既没有冲突,又被所有平台所支持。
千万不要使用 Windows 自带的记事本编辑任何文本文件。原因是 Microsoft 开发记事本的团队使用了一个非常弱智的行为来保存 UTF-8 编码的文件,他们自作聪明地在每个文件开头添加了 0xefbbbf(十六进制)的字符,你会遇到很多不可思议的问题,比如,网页第一行可能会显示一个“?”,明明正确的程序一编译就报语法错误,等等,都是由记事本的弱智行为带来的。建议你下载 Notepad++ 代替记事本,不但功能强大,而且免费!记得把 Notepad++ 的默认编码设置为 UTF-8 without BOM 即可:
现在我们编写一个 readme.txt
文件,内容如下:
11111
22222
一定要放到 D:\v_hwhao\GitTest
目录下(子目录也行),因为这是一个 Git 仓库,放到其他地方 Git 再厉害也找不到这个文件。
和把大象放到冰箱需要3步相比,把一个文件放到 Git 仓库只需要两步。
第一步,用命令 git add
告诉Git,把文件添加到仓库:
git add readme.txt
执行上面的命令,没有任何显示,这就对了,Unix 的哲学是“没有消息就是好消息”,说明添加成功。
第二步,用命令 git commit
告诉 Git,把文件提交到仓库:
$ git commit -m "第一次提交"
[master (root-commit) 023a5e2] 第一次提交
1 file changed, 2 insertions(+)
create mode 100644 readme.txt
简单解释一下 git commit
命令,-m
后面输入的是本次提交的说明,可以输入任意内容,当然最好是有意义的,这样你就能从历史记录里方便地找到改动记录。
嫌麻烦不想输入 -m "xxx"
行不行?确实有办法可以这么干,但是强烈不建议你这么干,因为输入说明对自己对别人阅读都很重要。实在不想输入说明的童鞋请自行 Google,我不告诉你这个参数。
git commit
命令执行成功后会告诉你,1 file changed
:1个文件被改动(我们新添加的 readme.txt 文件);2 insertions
:插入了两行内容(readme.txt 有两行内容)。
为什么 Git 添加文件需要 add
,commit
一共两步呢?因为 commit
可以一次提交很多文件,所以你可以多次 add
不同的文件,比如:
$ git add file1.txt
$ git add file2.txt file3.txt
$ git commit -m "add 3 files."
小结
初始化一个Git仓库,使用 git init
命令。
添加文件到 Git 仓库,分两步:
使用命令 git add <file>
,注意,可反复多次使用,添加多个文件;
使用命令 git commit -m <message>
,完成。
版本回退
现在,你已经学会了修改文件,然后把修改提交到 Git 版本库,现在,再练习一次,修改 readme.txt 文件如下:
33333
44444
然后尝试提交:
v_hwhao@v_hwhao-PC3 MINGW64 /d/v_hwhao/GitTest (master)
$ git add readme.txt
v_hwhao@v_hwhao-PC3 MINGW64 /d/v_hwhao/GitTest (master)
$ git commit -m "第二次修改"
[master a28592f] 第二次修改
1 file changed, 2 insertions(+), 2 deletions(-)
像这样,你不断对文件进行修改,然后不断提交修改到版本库里,就好比玩 RPG 游戏时,每通过一关就会自动把游戏状态存盘,如果某一关没过去,你还可以选择读取前一关的状态。有些时候,在打 Boss 之前,你会手动存盘,以便万一打 Boss 失败了,可以从最近的地方重新开始。Git 也是一样,每当你觉得文件修改到一定程度的时候,就可以“保存一个快照”,这个快照在 Git 中被称为 commit。一旦你把文件改乱了,或者误删了文件,还可以从最近的一个 commit 恢复,然后继续工作,而不是把几个月的工作成果全部丢失。
现在,我们回顾一下 readme.txt 文件一共有几个版本被提交到Git仓库里了:
版本1:第一次提交
11111
22222
版本2:第二次修改
33333
44444
当然了,在实际工作中,我们脑子里怎么可能记得一个几千行的文件每次都改了什么内容,不然要版本控制系统干什么。版本控制系统肯定有某个命令可以告诉我们历史记录,在 Git 中,我们用 git log
命令查看:
$ git log
commit a28592fe51d5b1040894bb81d054f17f944e1f7f (HEAD -> master)
Author: v_hwhao <v_hwhao@qq.com>
Date: Thu May 7 20:13:37 2020 +0800
第二次修改
commit 023a5e2a6b1c1efbabfc71fb91edabf392cb9117
Author: v_hwhao <v_hwhao@qq.com>
Date: Thu May 7 19:54:43 2020 +0800
第一次提交
git log
命令显示从最近到最远的提交日志,我们可以看到 3 次提交,最近的一次是 第二次修改
,上一次是 第一次提交
。
如果嫌输出信息太多,看得眼花缭乱的,可以试试加上 --pretty=oneline
参数:
$ git log --pretty=oneline
a28592fe51d5b1040894bb81d054f17f944e1f7f (HEAD -> master) 第二次修改
023a5e2a6b1c1efbabfc71fb91edabf392cb9117 第一次提交
需要友情提示的是,你看到的一大串类似 a2859...
的是 commit id(版本号)
,和 SVN 不一样,Git 的 commit id 不是1,2,3……递增的数字,而是一个 SHA1
计算出来的一个非常大的数字,用十六进制表示,而且你看到的 commit id 和我的肯定不一样,以你自己的为准。
为什么 commit id 需要用这么一大串数字表示呢?因为 Git 是分布式的版本控制系统,后面我们还要研究多人在同一个版本库里工作,如果大家都用 1,2,3……作为版本号,那肯定就冲突了。
好了,现在我们启动时光穿梭机,准备把 readme.txt
回退到上一个版本,也就是 第一次提交
的那个版本,怎么做呢?
首先,Git 必须知道当前版本是哪个版本,在Git中,用 HEAD 表示当前版本,也就是最新的提交 a2859...
(注意我的提交ID和你的肯定不一样),上一个版本就是 HEAD^
,上上一个版本就是 HEAD^^
,当然往上 100 个版本写 100 个 ^ 比较容易数不过来,所以写成HEAD~100
。
现在,我们要把当前版本 第二次修改
回退到上一个版本 第一次提交
,就可以使用 git reset
命令:
$ git reset --hard HEAD^
HEAD is now at 023a5e2 第一次提交
--hard
参数有啥意义?这个后面再讲,现在你先放心使用。
看看 readme.txt 的内容是不是版本 第一次提交
:
11111
22222
用 git log
再看看现在版本库的状态:
$ git log
commit 023a5e2a6b1c1efbabfc71fb91edabf392cb9117 (HEAD -> master)
Author: v_hwhao <v_hwhao@qq.com>
Date: Thu May 7 19:54:43 2020 +0800
第一次提交
最新的那个版本 第二次修改
已经看不到了!好比你从 21 世纪坐时光穿梭机来到了 19 世纪,想再回去已经回不去了,肿么办?
办法其实还是有的,只要上面的命令行窗口还没有被关掉,你就可以顺着往上找啊找啊,找到那个 第二次修改
的 commit id 是 a28592fe51d5b1040894bb81d054f17f944e1f7f
,于是就可以指定回到未来的某个版本:
$ git reset --hard a2859
HEAD is now at a28592f 第二次修改
版本号没必要写全,前几位就可以了,Git 会自动去找。当然也不能只写前一两位,因为 Git 可能会找到多个版本号,就无法确定是哪一个了。
再小心翼翼地看看 readme.txt 的内容:
33333
44444
果然,我胡汉三又回来了。
Git的版本回退速度非常快,因为 Git 在内部有个指向当前版本的 HEAD 指针,当你回退版本的时候,Gi t仅仅是把 HEAD 从指向 第二次修改
。
然后顺便把工作区的文件更新了。所以你让 HEAD 指向哪个版本号,你就把当前版本定位在哪。
现在,你回退到了某个版本,关掉了电脑,第二天早上就后悔了,想恢复到新版本怎么办?找不到新版本的 commit id
怎么办?
在 Git 中,总是有后悔药可以吃的。当你用 $ git reset --hard HEAD^
回退到 第二次修改
版本时,再想恢复到 第一次提交
,就必须找到 第一次提交
的 commit id
。Git提供了一个命令 git reflog
用来记录你的每一次命令:
$ git reflog
a28592f (HEAD -> master) HEAD@{0}: reset: moving to a2859
023a5e2 HEAD@{1}: reset: moving to HEAD^
a28592f (HEAD -> master) HEAD@{2}: commit: 第二次修改
023a5e2 HEAD@{3}: commit (initial): 第一次提交
终于舒了口气,从输出可知, 第一次提交
的 commit id
是 023a5e2
,现在,你又可以乘坐时光机回到未来了。
再总结一下:
HEAD 指向的版本就是当前版本,因此,Git 允许我们在版本的历史之间穿梭,使用命令 git reset --hard commit_id
。
穿梭前,用 git log
可以查看提交历史,以便确定要回退到哪个版本。
要重返未来,用 git reflog
查看命令历史,以便确定要回到未来的哪个版本。
管理修改
为什么Git比其他版本控制系统设计得优秀,因为Git跟踪并管理的是修改,而非文件。
你会问,什么是修改?比如你新增了一行,这就是一个修改,删除了一行,也是一个修改,更改了某些字符,也是一个修改,删了一些又加了一些,也是一个修改,甚至创建一个新文件,也算一个修改。
为什么说Git管理的是修改,而不是文件呢?我们还是做实验。第一步,对 readme.txt 做一个修改,比如加一行内容:
$ cat readme.txt
33333
44444
55555
66666
然后,添加:
v_hwhao@v_hwhao-PC3 MINGW64 /d/v_hwhao/GitTest (master)
$ git add readme.txt
v_hwhao@v_hwhao-PC3 MINGW64 /d/v_hwhao/GitTest (master)
$ git status
On branch master
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: readme.txt
然后,再修改 readme.txt:
$ cat readme.txt
33333
44444
55555
66677
提交:
$ git commit -m "第四次提交"
[master 44c4b3c] 第四次提交
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 restore <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 9c729e5..336df88 100644
--- a/readme.txt
+++ b/readme.txt
@@ -1,4 +1,4 @@
33333
44444
55555
-66666
\ No newline at end of file
+66677
\ No newline at end of file
可见,第二次修改确实没有被提交。
那怎么提交第二次修改呢?你可以继续 git add
再 git commit
,也可以别着急提交第一次修改,先 git add
第二次修改,再 git commit
,就相当于把两次修改合并后一块提交了:
第一次修改 -> git add
-> 第二次修改 -> git add
-> git commit
好,现在,把第二次修改提交了,然后开始小结。
小结
现在,你又理解了 Git 是如何跟踪修改的,每次修改,如果不用 git add
到暂存区,那就不会加入到 commit
中。
撤销修改
自然,你是不会犯错的。不过现在是凌晨两点,你正在赶一份工作报告,你在 readme.txt 中添加了一行:
$ cat readme.txt
33333
44444
55555
66677
错误的行
在你准备提交前,一杯咖啡起了作用,你猛然发现了 stupid boss 可能会让你丢掉这个月的奖金!
既然错误发现得很及时,就可以很容易地纠正它。你可以删掉最后一行,手动把文件恢复到上一个版本的状态。如果用 git status
查看一下:
$ git status
On branch master
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <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 checkout -- file
可以丢弃工作区的修改:
$ git checkout -- readme.txt
命令 git checkout -- readme.txt
意思就是,把 readme.txt 文件在工作区的修改全部撤销,这里有两种情况:
一种是 readme.txt 自修改后还没有被放到暂存区,现在,撤销修改就回到和版本库一模一样的状态;
一种是 readme.txt 已经添加到暂存区后,又作了修改,现在,撤销修改就回到添加到暂存区后的状态。
总之,就是让这个文件回到最近一次 git commit
或 git add
时的状态。
现在,看看 readme.txt
的文件内容:
$ cat readme.txt
33333
44444
55555
66677
文件内容果然复原了。
git checkout -- file
命令中的 --
很重要,没有 --
,就变成了“切换到另一个分支”的命令,我们在后面的分支管理中会再次遇到 git checkout
命令。
现在假定是凌晨 3 点,你不但写了一些胡话,还 git add
到暂存区了:
$ cat readme.txt
33333
44444
55555
66677
吃火锅唱歌
$ git add readme.txt
庆幸的是,在 commit
之前,你发现了这个问题。用 git status
查看一下,修改只是添加到了暂存区,还没有提交:
$ git status
On branch master
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: readme.txt
Git 同样告诉我们,用命令 git reset HEAD <file>
可以把暂存区的修改撤销掉(unstage),重新放回工作区:
$ 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 restore <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
整个世界终于清静了!
现在,假设你不但改错了东西,还从暂存区提交到了版本库,怎么办呢?还记得版本回退一节吗?可以回退到上一个版本。不过,这是有条件的,就是你还没有把自己的本地版本库推送到远程。还记得Git是分布式版本控制系统吗?我们后面会讲到远程版本库,一旦你把 stupid boss 提交推送到远程版本库,你就真的惨了……
小结
又到了小结时间。
场景1:当你改乱了工作区某个文件的内容,想直接丢弃工作区的修改时,用命令 git checkout -- file
。
场景2:当你不但改乱了工作区某个文件的内容,还添加到了暂存区时,想丢弃修改,分两步,第一步用命令 git reset HEAD <file>
,就回到了场景1,第二步按场景1操作。
场景3:已经提交了不合适的修改到版本库时,想要撤销本次提交,参考版本回退一节,不过前提是没有推送到远程库。
删除文件
在 Git 中,删除也是一个修改操作,我们实战一下,先添加一个新文件 test.txt 到 Git 并且提交:
v_hwhao@v_hwhao-PC3 MINGW64 /d/v_hwhao/GitTest (master)
$ git add test.txt
v_hwhao@v_hwhao-PC3 MINGW64 /d/v_hwhao/GitTest (master)
$ git commit -m "添加 test.txt"
[master 7fd0411] 添加 test.txt
1 file changed, 1 insertion(+)
create mode 100644 test.txt
一般情况下,你通常直接在文件管理器中把没用的文件删了,或者用rm命令删了:
v_hwhao@v_hwhao-PC3 MINGW64 /d/v_hwhao/GitTest (master)
$ rm test.txt
这个时候,Git 知道你删除了文件,因此,工作区和版本库就不一致了,git status
命令会立刻告诉你哪些文件被删除了:
v_hwhao@v_hwhao-PC3 MINGW64 /d/v_hwhao/GitTest (master)
$ git status
On branch master
Changes not staged for commit:
(use "git add/rm <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
deleted: test.txt
no changes added to commit (use "git add" and/or "git commit -a")
现在你有两个选择,一是确实要从版本库中删除该文件,那就用命令 git rm
删掉,并且 git commit
:
v_hwhao@v_hwhao-PC3 MINGW64 /d/v_hwhao/GitTest (master)
$ git rm test.txt
rm 'test.txt'
v_hwhao@v_hwhao-PC3 MINGW64 /d/v_hwhao/GitTest (master)
$ git commit -m "删除 test.txt"
[master 2a4d9dd] 删除 test.txt
1 file changed, 1 deletion(-)
delete mode 100644 test.txt
现在,文件就从版本库中被删除了。
小提示:先手动删除文件,然后使用 git rm <file>
和 git add<file>
效果是一样的。
另一种情况是删错了,因为版本库里还有呢,所以可以很轻松地把误删的文件恢复到最新版本:
$ git checkout -- test.txt
git checkout
其实是用版本库里的版本替换工作区的版本,无论工作区是修改还是删除,都可以“一键还原”。
注意:从来没有被添加到版本库就被删除的文件,是无法恢复的
小结
命令 git rm
用于删除一个文件。如果一个文件已经被提交到版本库,那么你永远不用担心误删,但是要小心,你只能恢复文件到最新版本,你会丢失最近一次提交后你修改的内容。
工作区+暂存区+本地库
使用 GitHub
GitHub 是一个 Git 项目托管网站,主要提 供基于 Git 的版本托管服务。
我们一直用 GitHub 作为免费的远程仓库,如果是个人的开源项目,放到 GitHub上是完全没有问题的。其实 GitHub 还是一个开源协作社区,通过 GitHub,既可以让别人参与你的开源项目,也可以参与别人的开源项目。
在 GitHub 出现以前,开源项目开源容易,但让广大人民群众参与进来比较困难,因为要参与,就要提交代码,而给每个想提交代码的群众都开一个账号那是不现实的,因此,群众也仅限于报个 bug,即使能改掉 bug,也只能把 diff 文件用邮件发过去,很不方便。
但是在 GitHub 上,利用 Git 极其强大的克隆和分支功能,广大人民群众真正可以第一次自由参与各种开源项目了。
如何参与一个开源项目呢?比如人气极高的 bootstrap 项目,这是一个非常强大的 CSS 框架,你可以访问它的项目主页 https://github.com/twbs/bootstrap,点“Fork”就在自己的账号下克隆了一个 bootstrap 仓库,然后,从自己的账号下 clone:
一定要从自己的账号下 clone 仓库,这样你才能推送修改。如果从 bootstrap 的作者的仓库地址 git@github.com:twbs/bootstrap.git 克隆,因为没有权限,你将不能推送修改。
Bootstrap 的官方仓库 twbs/bootstrap、你在 GitHub 上克隆的仓库 my/bootstrap,以及你自己克隆到本地电脑的仓库,他们的关系就像下图显示的那样:
如果你想修复 bootstrap 的一个bug,或者新增一个功能,立刻就可以开始干活,干完后,往自己的仓库推送。
如果你希望 bootstrap 的官方库能接受你的修改,你就可以在 GitHub 上发起一个 pull request。当然,对方是否接受你的 pull request 就不一定了。
如果你没能力修改 bootstrap,但又想要试一把 pull request,那就 Fork 一下我的仓库:https://github.com/luoma-haowei/gitskills.git,创建一个 your-github-id.txt 的文本文件,写点自己学习 Git 的心得,然后推送一个 pull request 给我,我会视心情而定是否接受。
小结
在 GitHub 上,可以任意 Fork 开源仓库;
自己拥有 Fork 后的仓库的读写权限;
可以推送 pull request 给官方仓库来贡献代码。
添加远程库
现在的情景是,你已经在本地创建了一个 Git 仓库后,又想在 GitHub 创建一个 Git 仓库,并且让这两个仓库进行远程同步,这样,GitHub 上的仓库既可以作为备份,又可以让其他人通过该仓库来协作,真是一举多得。
首先,登陆 GitHub,然后,在右上角找到“new repository”按钮,创建一个新的仓库:
在 Repository name *
填入 GitTest
,其他保持默认设置,点击 Create repository
按钮,就成功地创建了一个新的 Git 仓库:
目前,在 GitHub 上的这个 GitTest
仓库还是空的,GitHub 告诉我们,可以从这个仓库克隆出新的仓库,也可以把一个已有的本地仓库与之关联,然后,把本地仓库的内容推送到 GitHub 仓库。
现在,我们根据 GitHub 的提示,在本地的 GitTest
仓库下运行命令:
v_hwhao@v_hwhao-PC3 MINGW64 /d/v_hwhao/GitTest (master)
$ git remote add origin https://github.com/luoma-haowei/GitTest.git
请千万注意,把上面的 luoma-haowei
替换成你自己的GitHub账户名,否则,你在本地关联的就是我的远程库,关联没有问题,但是你以后推送是推不上去的,因为你的 SSH Key 公钥不在我的账户列表中。
添加后,远程库的名字就是 origin
,这是 Git 默认的叫法,也可以改成别的,但是 origin
这个名字一看就知道是远程库。
下一步,就可以把本地库的所有内容推送到远程库上:
v_hwhao@v_hwhao-PC3 MINGW64 /d/v_hwhao/GitTest (master)
$ git push -u origin master
提示登录
登录后再推送一次
$ 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% (11/11), done.
Writing objects: 100% (20/20), 1.57 KiB | 401.00 KiB/s, done.
Total 20 (delta 2), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (2/2), done.
To https://github.com/luoma-haowei/GitTest.git
* [new branch] master -> master
Branch 'master' set up to track remote branch 'master' from 'origin'.
把本地库的内容推送到远程,用 git push
命令,实际上是把当前分支 master
推送到远程。
由于远程库是空的,我们第一次推送 master
分支时,加上了 -u
参数,Git 不但会把本地的 master
分支内容推送的远程新的 master
分支,还会把本地的 master
分支和远程的 master
分支关联起来,在以后的推送或者拉取时就可以简化命令。
推送成功后,可以立刻在 GitHub 页面中看到远程库的内容已经和本地一模一样:
从现在起,只要本地作了提交,就可以通过命令:
$ git push origin master
把本地 master 分支的最新修改推送至 GitHub,现在,你就拥有了真正的分布式版本库!
小结
要关联一个远程库,使用命令 $ git remote add origin https://github.com/luoma-haowei/GitTest.git
关联后,使用命令 git push -u origin master
第一次推送 master
分支的所有内容;
此后,每次本地提交后,只要有必要,就可以使用命令 git push origin master
推送最新修改;
分布式版本系统的最大好处之一是在本地工作完全不需要考虑远程库的存在,也就是有没有联网都可以正常工作,而 SVN 在没有联网的时候是拒绝干活的!当有网络的时候,再把本地提交推送一下就完成了同步,真是太方便了!
从远程库克隆
上次我们讲了先有本地库,后有远程库的时候,如何关联远程库。
现在,假设我们从零开发,那么最好的方式是先创建远程库,然后,从远程库克隆。
首先,登陆GitHub,创建一个新的仓库,名字叫 gitskills
我们勾选 Initialize this repository with a README
,这样 GitHub 会自动为我们创建一个 README.md 文件。创建完毕后,可以看到 README.md 文件:
现在,远程库已经准备好了,下一步是用命令 git clone
克隆一个本地库:
$ git clone https://github.com/luoma-haowei/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
Receiving objects: 100% (3/3), done.
远程库已经可以看到了,注意把 Git库 的地址换成你自己的,然后进入 gitskills 目录看看,已经有 README.md 文件了:
v_hwhao@v_hwhao-PC3 MINGW64 /d/v_hwhao/GitTest (master)
$ cd gitskills
v_hwhao@v_hwhao-PC3 MINGW64 /d/v_hwhao/GitTest/gitskills (master)
$ ls
README.md
如果有多个人协作开发,那么每个人各自从远程克隆一份就可以了。
你也许还注意到,GitHub给出的地址不止一个,还可以用 https://github.com/luoma-haowei/gitskills.git 这样的地址。实际上,Git支持多种协议,默认的 git://使用ssh, 但也可以使用 https 等其他协议。
使用 https 除了速度慢以外,还有个最大的麻烦是每次推送都必须输入口令,但是在某些只开放 http 端口的公司内部就无法使用 ssh 协议而只能用 https。
小结
要克隆一个仓库,首先必须知道仓库的地址,然后使用 git clone
命令克隆。
Git 支持多种协议,包括 https
,但 ssh
协议速度最快。
创建与合并分支
首先,我们创建 dev 分支,然后切换到 dev 分支:
v_hwhao@v_hwhao-PC3 MINGW64 /d/v_hwhao/GitTest (master)
$ 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
命令查看当前分支:
v_hwhao@v_hwhao-PC3 MINGW64 /d/v_hwhao/GitTest (dev)
$ git branch
* dev
master
git branch
命令会列出所有分支,当前分支前面会标一个 *
号。
然后,我们就可以在 dev
分支上正常提交,比如对 readme.txt
做个修改,加上一行:
dev 88888
然后提交:
v_hwhao@v_hwhao-PC3 MINGW64 /d/v_hwhao/GitTest (dev)
$ git add readme.txt
v_hwhao@v_hwhao-PC3 MINGW64 /d/v_hwhao/GitTest (dev)
$ git commit -m "dev 第一次提交"
[dev e4f543f] dev 第一次提交
1 file changed, 2 insertions(+), 1 deletion(-)
现在,dev 分支的工作完成,我们就可以切换回 master 分支:
v_hwhao@v_hwhao-PC3 MINGW64 /d/v_hwhao/GitTest (dev)
$ git checkout master
Switched to branch 'master'
Your branch is up to date with 'origin/master'.
现在,我们把 dev 分支的工作成果合并到 master 分支上:
v_hwhao@v_hwhao-PC3 MINGW64 /d/v_hwhao/GitTest (master)
$ git merge dev
Updating 2a4d9dd..e4f543f
Fast-forward
readme.txt | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
git merge
命令用于合并指定分支到当前分支。合并后,再查看 readme.txt
的内容,就可以看到,和 dev 分支的最新提交是完全一样的。
注意到上面的 Fast-forward
信息,Git告诉我们,这次合并是“快进模式”,也就是直接把 master 指向 dev 的当前提交,所以合并速度非常快。
当然,也不是每次合并都能 Fast-forward,我们后面会讲其他方式的合并。
合并完成后,就可以放心地删除 dev 分支了:
v_hwhao@v_hwhao-PC3 MINGW64 /d/v_hwhao/GitTest (master)
$ git branch -d dev
Deleted branch dev (was e4f543f).
删除后,查看 branch,就只剩下 master 分支了:
v_hwhao@v_hwhao-PC3 MINGW64 /d/v_hwhao/GitTest (master)
$ git branch
* master
因为创建、合并和删除分支非常快,所以 Git 鼓励你使用分支完成某个任务,合并后再删掉分支,这和直接在 master 分支上工作效果是一样的,但过程更安全。
switch
我们注意到切换分支使用 git checkout <branch>
,而前面讲过的撤销修改则是 git checkout -- <file>
,同一个命令,有两种作用,确实有点令人迷惑。
实际上,切换分支这个动作,用 switch 更科学。因此,最新版本的Git提供了新的 git switch
命令来切换分支:
创建并切换到新的 dev 分支,可以使用:
$ git switch -c dev
Switched to a new branch 'dev'
直接切换到已有的 master 分支,可以使用:
$ git switch master
使用新的git switch命令,比git checkout要更容易理解。
小结
Git鼓励大量使用分支:
查看分支:git branch
创建分支:git branch <name>
切换分支:git checkout <name>
或者 git switch <name>
创建+切换分支:git checkout -b <name>
或者 git switch -c <name>
合并某分支到当前分支:git merge <name>
解决冲突
删除分支:git branch -d
人生不如意之事十之八九,合并分支往往也不是一帆风顺的。
准备新的 feature1 分支,继续我们的新分支开发:
修改 readme.txt 最后一行,改为:
feature1 99999
在 feature1 分支上提交:
v_hwhao@v_hwhao-PC3 MINGW64 /d/v_hwhao/GitTest (feature1)
$ git add readme.txt
v_hwhao@v_hwhao-PC3 MINGW64 /d/v_hwhao/GitTest (feature1)
$ git commit -m "feature1 第一次修改"
[feature1 08ce9e2] feature1 第一次修改
1 file changed, 2 insertions(+), 1 deletion(-)
切换到 master 分支:
v_hwhao@v_hwhao-PC3 MINGW64 /d/v_hwhao/GitTest (feature1)
$ git switch 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 文件的最后一行改为:
master 分支的 99999
提交:
v_hwhao@v_hwhao-PC3 MINGW64 /d/v_hwhao/GitTest (master)
$ git commit -m "master 分支提交"
[master 744aa74] master 分支提交
1 file changed, 2 insertions(+), 1 deletion(-)
现在,master 分支和 feature1 分支各自都分别有新的提交,变成了这样:
这种情况下,Git 无法执行“快速合并”,只能试图把各自的修改合并起来,但这种合并就可能会有冲突,我们试试看:
v_hwhao@v_hwhao-PC3 MINGW64 /d/v_hwhao/GitTest (master)
$ 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
也可以告诉我们冲突的文件:
v_hwhao@v_hwhao-PC3 MINGW64 /d/v_hwhao/GitTest (master|MERGING)
$ 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
Untracked files:
(use "git add <file>..." to include in what will be committed)
gitskills/
no changes added to commit (use "git add" and/or "git commit -a")
我们可以直接查看 readme.txt 的内容:
33333
44444
55555
66677
dev 88888
<<<<<<< HEAD
master 分支的 99999
=======
feature1 99999
>>>>>>> feature1
Git用 <<<<<<<
,=======
,>>>>>>>
标记出不同分支的内容,我们修改如下后保存:
master feature1 分支的 99999
再提交:
v_hwhao@v_hwhao-PC3 MINGW64 /d/v_hwhao/GitTest (master|MERGING)
$ git add readme.txt
v_hwhao@v_hwhao-PC3 MINGW64 /d/v_hwhao/GitTest (master|MERGING)
$ git commit -m "master,feature1 合并提交"
[master bcdd674] master,feature1 合并提交
现在,master 分支和 feature1 分支变成了下图所示:
用带参数的 git log
也可以看到分支的合并情况:
v_hwhao@v_hwhao-PC3 MINGW64 /d/v_hwhao/GitTest (master)
$ git log --graph --pretty=oneline --abbrev-commit
* bcdd674 (HEAD -> master) master,feature1 合并提交
|\
| * 08ce9e2 (feature1) feature1 第一次修改
* | 744aa74 master 分支提交
|/
* e4f543f (dev) dev 第一次提交
* 2a4d9dd (origin/master) 删除 test.txt
* 7fd0411 添加 test.txt
* 3ed88fa 第五次修改
* 44c4b3c 第四次提交
* c0ee5d7 第三次提交
* a28592f 第二次修改
* 023a5e2 第一次提交
最后,删除 feature1 分支:
v_hwhao@v_hwhao-PC3 MINGW64 /d/v_hwhao/GitTest (master)
$ git branch -d feature1
Deleted branch feature1 (was 08ce9e2).
小结
当 Git 无法自动合并分支时,就必须首先解决冲突。解决冲突后,再提交,合并完成。
解决冲突就是把 Git 合并失败的文件手动编辑为我们希望的内容,再提交。
用 git log --graph
命令可以看到分支合并图。
分支管理策略
通常,合并分支时,如果可能,Git 会用 Fast forward 模式,但这种模式下,删除分支后,会丢掉分支信息。
如果要强制禁用 Fast forward 模式,Git 就会在 merge 时生成一个新的 commit,这样,从分支历史上就可以看出分支信息。
下面我们实战一下 --no-ff
方式的 git merge
:
首先,仍然创建并切换 dev 分支:
$ git switch -c div
Switched to a new branch 'div'
ps:这里写错了,暂时不管
修改 readme.txt 文件,并提交一个新的 commit:
$ git add readme.txt
$ git commit -m "div 分支提交"
[div 429a305] div 分支提交
1 file changed, 2 insertions(+), 1 deletion(-)
现在,我们切换回 master:
$ git switch master
Switched to branch 'master'
准备合并 dev 分支,请注意 --no-ff
参数,表示禁用 Fast forward:
$ git merge --no-ff -m "合并 with no-ff" div
Merge made by the 'recursive' strategy.
readme.txt | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
因为本次合并要创建一个新的 commit,所以加上 -m
参数,把 commit 描述写进去。
合并后,我们用 git log
看看分支历史:
$ git log --graph --pretty=oneline --abbrev-commit
* b81a5d9 (HEAD -> master) 合并 with no-ff
|\
| * 429a305 (div) div 分支提交
|/
* 69389dd 主分支提交
可以看到,不使用 Fast forward 模式,merge 后就像这样:
分支策略
在实际开发中,我们应该按照几个基本原则进行分支管理:
首先,master 分支应该是非常稳定的,也就是仅用来发布新版本,平时不能在上面干活;
那在哪干活呢?干活都在 dev 分支上,也就是说,dev 分支是不稳定的,到某个时候,比如 1.0 版本发布时,再把 dev 分支合并到 master上,在 master 分支发布 1.0 版本;
你和你的小伙伴们每个人都在 dev 分支上干活,每个人都有自己的分支,时不时地往 dev 分支上合并就可以了。
所以,团队合作的分支看起来就像这样:
小结
Git 分支十分强大,在团队开发中应该充分应用。
合并分支时,加上 --no-ff
参数就可以用普通模式合并,合并后的历史有分支,能看出来曾经做过合并,而 fast forward 合并就看不出来曾经做过合并。
Bug 分支
软件开发中,bug 就像家常便饭一样。有了 bug 就需要修复,在 Git 中,由于分支是如此的强大,所以,每个 bug 都可以通过一个新的临时分支来修复,修复后,合并分支,然后将临时分支删除。
当你接到一个修复一个代号 101 的 bug 的任务时,很自然地,你想创建一个分支 issue-101 来修复它,但是,等等,当前正在 dev 上进行的工作还没有提交:
$ git status
On branch master
Your branch is ahead of 'origin/master' by 18 commits.
(use "git push" to publish your local commits)
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: dev.txt
并不是你不想提交,而是工作只进行到一半,还没法提交,预计完成还需1天时间。但是,必须在两个小时内修复该 bug,怎么办?
幸好,Git 还提供了一个 stash 功能,可以把当前工作现场“储藏”起来,等以后恢复现场后继续工作:
$ git stash
Saved working directory and index state WIP on master: bda480f 1
现在,用 git status
查看工作区,就是干净的(除非有没有被Git管理的文件),因此可以放心地创建分支来修复 bug。
首先确定要在哪个分支上修复 bug,假定需要在 master 分支上修复,就从 master 创建临时分支:
v_hwhao@v_hwhao-PC3 MINGW64 /d/v_hwhao/GitTest (master)
$ git checkout master
Already on 'master'
Your branch is ahead of 'origin/master' by 18 commits.
(use "git push" to publish your local commits)
v_hwhao@v_hwhao-PC3 MINGW64 /d/v_hwhao/GitTest (master)
$ git checkout -b issue-101
Switched to a new branch 'issue-101'
现在修复 bug,需要把 在 readme.txt 最后一行添加 “Bug 修复”,然后提交:
v_hwhao@v_hwhao-PC3 MINGW64 /d/v_hwhao/GitTest (issue-101)
$ git add readme.txt
v_hwhao@v_hwhao-PC3 MINGW64 /d/v_hwhao/GitTest (issue-101)
$ git commit -m "bug 修复"
[issue-101 87070e6] bug 修复
1 file changed, 2 insertions(+), 1 deletion(-)
修复完成后,切换到 master 分支,并完成合并,最后删除 issue-101 分支:
v_hwhao@v_hwhao-PC3 MINGW64 /d/v_hwhao/GitTest (master)
$ git merge --no-ff -m "合并 bug 修复 101" issue-101
Merge made by the 'recursive' strategy.
readme.txt | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
太棒了,原计划两个小时的 bug 修复只花了 5 分钟!现在,是时候接着回到 dev 分支干活了!
v_hwhao@v_hwhao-PC3 MINGW64 /d/v_hwhao/GitTest (master)
$ git switch dev
Switched to branch 'dev'
v_hwhao@v_hwhao-PC3 MINGW64 /d/v_hwhao/GitTest (dev)
$ git status
On branch dev
Untracked files:
(use "git add <file>..." to include in what will be committed)
f
gitskills/
nothing added to commit but untracked files present (use "git add" to track)
工作区是干净的,刚才的工作现场存到哪去了?用 git stash list
命令看看:
v_hwhao@v_hwhao-PC3 MINGW64 /d/v_hwhao/GitTest (dev)
$ git stash list
stash@{0}: WIP on master: bda480f 1
工作现场还在,Git 把 stash 内容存在某个地方了,但是需要恢复一下,有两个办法:
一是用 git stash apply
恢复,但是恢复后,stash
内容并不删除,你需要用 git stash drop
来删除;
另一种方式是用 git stash pop
,恢复的同时把 stash
内容也删了:
$ git stash pop
On branch dev
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: dev.txt
Untracked files:
(use "git add <file>..." to include in what will be committed)
f
gitskills/
Dropped refs/stash@{0} (fd2630419376d25fd4c67cad0669ec4ff130fded)
再用 git stash list
查看,就看不到任何 stash 内容了:
$ git stash list
你可以多次 stash,恢复的时候,先用 git stash list
查看,然后恢复指定的 stash,用命令:
$ git stash apply stash@{0}
在 master 分支上修复了 bug 后,我们要想一想,dev 分支是早期从 master 分支分出来的,所以,这个 bug 其实在当前 dev 分支上也存在。
那怎么在 dev 分支上修复同样的 bug?重复操作一次,提交不就行了?
有木有更简单的方法?
有!
同样的 bug,要在 dev 上修复,我们只需要把 87070e6 bug 修复 101 这个提交所做的修改“复制”到 dev 分支。注意:我们只想复制 87070e6 bug 修复 101 这个提交所做的修改,并不是把整个 master 分支 merge 过来。
为了方便操作,Git 专门提供了一个 cherry-pick
命令,让我们能复制一个特定的提交到当前分支:
$ git branch
* dev
master
$ git cherry-pick 87070e6
[master 1d4b803] fix bug 101
1 file changed, 1 insertion(+), 1 deletion(-)
Git 自动给 dev 分支做了一次提交,注意这次提交的 commit 是 1d4b803,它并不同于 master 的 87070e6,因为这两个 commit 只是改动相同,但确实是两个不同的 commit。用 git cherry-pick,我们就不需要在dev分支上手动再把修 bug 的过程重复一遍。
有些聪明的童鞋会想了,既然可以在 master 分支上修复 bug 后,在 dev 分支上可以“重放”这个修复过程,那么直接在 dev 分支上修复 bug,然后在 master 分支上“重放”行不行?当然可以,不过你仍然需要 git stash 命令保存现场,才能从 dev 分支切换到 master 分支。
小结
修复 bug 时,我们会通过创建新的 bug 分支进行修复,然后合并,最后删除;
当手头工作没有完成时,先把工作现场 git stash
一下,然后去修复 bug,修复后,再 git stash pop
,回到工作现场;
在 master 分支上修复的 bug,想要合并到当前dev分支,可以用 git cherry-pick <commit>
命令,把 bug 提交的修改“复制”到当前分支,避免重复劳动。
Feature 分支
软件开发中,总有无穷无尽的新的功能要不断添加进来。
添加一个新功能时,你肯定不希望因为一些实验性质的代码,把主分支搞乱了,所以,每添加一个新功能,最好新建一个 feature 分支,在上面开发,完成后,合并,最后,删除该 feature 分支。
现在,你终于接到了一个新任务:开发代号为 Vulcan 的新功能,该功能计划用于下一代星际飞船。
于是准备开发:
$ git switch -c feature-vulcan
Switched to a new branch 'feature-vulcan'
5 分钟后,开发完毕:
$ git add vulcan.py
$ git status
On branch feature-vulcan
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: vulcan.py
$ git commit -m "添加 feature 分支 vulcan"
[feature-vulcan 131dec2] 添加 feature 分支 vulcan
1 file changed, 1 insertion(+)
create mode 100644 vulcan.py
切回 dev,准备合并:
$ git switch 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友情提醒,feature-vulcan 分支还没有被合并,如果删除,将丢失掉修改,如果要强行删除,需要使用大写的 -D
参数。
现在我们强行删除:
$ git branch -D feature-vulcan
Deleted branch feature-vulcan (was 131dec2).
终于删除成功!
开发一个新 feature,最好新建一个分支;
如果要丢弃一个没有被合并过的分支,可以通过 git branch -D <name>
强行删除。
多人协作
当你从远程仓库克隆时,实际上 Git 自动把本地的 master 分支和远程的 master 分支对应起来了,并且,远程仓库的默认名称是 origin。
要查看远程库的信息,用 git remote
:
$ git remote
origin
或者,用 git remote -v
显示更详细的信息:
$ git remote -v
origin https://github.com/luoma-haowei/gitskills.git (fetch)
origin https://github.com/luoma-haowei/gitskills.git (push)
推送分支
推送分支,就是把该分支上的所有本地提交推送到远程库。推送时,要指定本地分支,这样,Git 就会把该分支推送到远程库对应的远程分支上:
$ git push origin master
Everything up-to-date
如果要推送其他分支,比如 dev,就改成:
$ git push origin dev
但是,并不是一定要把本地分支往远程推送,那么,哪些分支需要推送,哪些不需要呢?
master 分支是主分支,因此要时刻与远程同步;
dev 分支是开发分支,团队所有成员都需要在上面工作,所以也需要与远程同步;
bug 分支只用于在本地修复 bug,就没必要推到远程了,除非老板要看看你每周到底修复了几个 bug;
feature 分支是否推到远程,取决于你是否和你的小伙伴合作在上面开发。
总之,就是在 Git 中,分支完全可以在本地自己藏着玩,是否推送,视你的心情而定!
抓取分支
多人协作时,大家都会往 master 和d ev 分支上推送各自的修改。
现在,模拟一个你的小伙伴,可以在另一台电脑(注意要把 SSH Key 添加到 GitHub)或者同一台电脑的另一个目录下克隆:
$ git clone https://github.com/luoma-haowei/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
Receiving objects: 100% (3/3), done.
当你的小伙伴从远程库 clone 时,默认情况下,你的小伙伴只能看到本地的 master 分支。不信可以用 git branch
命令看看:
$ git branch
* master
现在,你的小伙伴要在 dev 分支上开发,就必须创建远程 origin 的 dev 分支到本地,于是他用这个命令创建本地 dev 分支:
$ git checkout -b dev origin/dev
现在,他就可以在 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 分支推送了他的提交,而碰巧你也对同样的文件作了修改,并试图推送:
$ cat env.txt
env
$ git add env.txt
$ git commit -m "add new env"
[dev 7bd91f1] add new env
1 file changed, 1 insertion(+)
create mode 100644 env.txt
$ git push origin dev
To github.com:michaelliao/learngit.git
! [rejected] dev -> dev (non-fast-forward)
error: failed to push some refs to 'git@github.com:michaelliao/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.
推送失败,因为你的小伙伴的最新提交和你试图推送的提交有冲突,解决办法也很简单,Git 已经提示我们,先用 git pull 把最新的提交从 origin/dev 抓下来,然后,在本地合并,解决冲突,再推送:
$ git pull
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
git pull 也失败了,原因是没有指定本地 dev 分支与远程 origin/dev 分支的链接,根据提示,设置 dev 和 origin/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 env.txt
CONFLICT (add/add): Merge conflict in env.txt
Automatic merge failed; fix conflicts and then commit the result.
这回 git 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
因此,多人协作的工作模式通常是这样:
首先,可以试图用 git push origin <branch-name>
推送自己的修改;
如果推送失败,则因为远程分支比你的本地更新,需要先用 git pull
试图合并;
如果合并有冲突,则解决冲突,并在本地提交;
没有冲突或者解决掉冲突后,再用 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
,如果有冲突,要先处理冲突。
Rebase
在上一节我们看到了,多人在同一个分支上协作时,很容易出现冲突。即使没有冲突,后 push 的童鞋不得不先 pull,在本地合并,然后才能 push 成功。
每次合并再 push 后,分支变成了这样:
$ git log --graph --pretty=oneline --abbrev-commit
* 6d76a5e (HEAD -> master, origin/master, origin/HEAD) 合并 bug 修复 101
|\
| * 87070e6 bug 修复
|/
* bda480f 1
|\
| * 9497c83 2
* | d30df49 1
|\|
| * 24c7cbd dev 9
* | 5fd9434 merge
|\|
| * 2cc73c2 dev 8
* | 3e6b36c 1
* | 25cfa26 add merge
|\|
| * 1c891fe add merge
* | b215296 冲突解决
|\|
| * 8707c3a dev 分支提交
* | 6f87cc5 submit
* | b354fad submit
|\|
总之看上去很乱,有强迫症的童鞋会问:为什么 Git 的提交历史不能是一条干净的直线?
其实是可以做到的!
Git 有一种称为 rebase 的操作,有人把它翻译成“变基”。
先不要随意展开想象。我们还是从实际问题出发,看看怎么把分叉的提交变成直线。
现在我们尝试推送本地分支:
$ git push origin master
To https://github.com/luoma-haowei/GitTest.git
! [rejected] master -> master (fetch first)
error: failed to push some refs to 'https://github.com/luoma-haowei/GitTest.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.
很不幸,失败了,这说明有人先于我们推送了远程分支。按照经验,先 pull 一下:
$ git pull
remote: Counting objects: 3, done.
remote: Compressing objects: 100% (1/1), done.
remote: Total 3 (delta 1), reused 3 (delta 1), pack-reused 0
Unpacking objects: 100% (3/3), done.
From github.com:michaelliao/learngit
d1be385..f005ed4 master -> origin/master
* [new tag] v1.0 -> v1.0
Auto-merging hello.py
Merge made by the 'recursive' strategy.
hello.py | 1 +
1 file changed, 1 insertion(+)
再用 git status
看看状态:
$ git status
On branch master
Your branch is up to date with 'origin/master'.
nothing to commit, working tree clean
加上刚才合并的提交,现在我们本地分支比远程分支超前3个提交。
用 git log
看看:
$ git log --graph --pretty=oneline --abbrev-commit
* e0ea545 (HEAD -> master) Merge branch 'master' of https://github.com/luoma-haowei/GitTest.git
|\
| * f005ed4 (origin/master) set exit=1
* | 582d922 add author
* | 8875536 add comment
|/
* d1be385 init hello
...
对强迫症童鞋来说,现在事情有点不对头,提交历史分叉了。如果现在把本地分支push到远程,有没有问题?
有!
什么问题?
不好看!
有没有解决方法?
有!
这个时候,rebase就派上了用场。我们输入命令 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...
输出了一大堆操作,到底是啥效果?再用 git log
看看:
$ git log --graph --pretty=oneline --abbrev-commit
* 7e61ed4 (HEAD -> master) add author
* 3611cfe add comment
* f005ed4 (origin/master) set exit=1
* d1be385 init hello
..
原本分叉的提交现在变成一条直线了!这种神奇的操作是怎么实现的?其实原理非常简单。我们注意观察,发现Git把我们本地的提交“挪动”了位置,放到了 f005ed4 (origin/master) set exit=1 之后,这样,整个提交历史就成了一条直线。rebase 操作前后,最终的提交内容是一致的,但是,我们本地的 commit 修改内容已经变化了,它们的修改不再基于 d1be385 init hello,而是基于 f005ed4 (origin/master) set exit=1,但最后的提交 7e61ed4 内容是一致的。
这就是 rebase 操作的特点:把分叉的提交历史“整理”成一条直线,看上去更直观。缺点是本地的分叉提交已经被修改过了。
最后,通过 push 操作把本地分支推送到远程:
$ git push origin
Enumerating objects: 4, done.
Counting objects: 100% (4/4), done.
Delta compression using up to 4 threads
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 299 bytes | 149.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
To https://github.com/luoma-haowei/GitTest.git
6d76a5e..deda4bb master -> master
再用 git log
看看效果:
$ git log --graph --pretty=oneline --abbrev-commit
* 7e61ed4 (HEAD -> master, origin/master) add author
* 3611cfe add comment
* f005ed4 set exit=1
* d1be385 init hello
...
远程分支的提交历史也是一条直线。
小结
rebase 操作可以把本地未 push 的分叉提交历史整理成直线;
rebase 的目的是使得我们在查看历史提交的变化时更容易,因为分叉的提交需要三方对比。
标签管理
发布一个版本时,我们通常先在版本库中打一个标签(tag),这样,就唯一确定了打标签时刻的版本。将来无论什么时候,取某个标签的版本,就是把那个打标签的时刻的历史版本取出来。所以,标签也是版本库的一个快照。
Git 的标签虽然是版本库的快照,但其实它就是指向某个 commit 的指针(跟分支很像对不对?但是分支可以移动,标签不能移动),所以,创建和删除标签都是瞬间完成的。
Git 有 commit,为什么还要引入 tag?
“请把上周一的那个版本打包发布,commit 号是 6a5819e…”
“一串乱七八糟的数字不好找!”
如果换一个办法:
“请把上周一的那个版本打包发布,版本号是 v1.2”
“好的,按照 tag v1.2 查找 commit 就行!”
所以,tag 就是一个让人容易记住的有意义的名字,它跟某个 commit 绑在一起。
创建标签
在 Git 中打标签非常简单,首先,切换到需要打标签的分支上:
$ git branch
* dev
master
$ git checkout master
Switched to branch 'master'
然后,敲命令 git tag <name>
就可以打一个新标签:
$ git tag v1.0
可以用命令 git tag
查看所有标签:
$ git tag
v1.0
默认标签是打在最新提交的 commit 上的。有时候,如果忘了打标签,比如,现在已经是周五了,但应该在周一打的标签没有打,怎么办?
方法是找到历史提交的 commit id,然后打上就可以了:
$ git log --pretty=oneline --abbrev-commit
deda4bb (HEAD -> master, tag: v1.0, origin/master, origin/HEAD) 2.txt
6d76a5e 合并 bug 修复 101
87070e6 bug 修复
bda480f 1
9497c83 2
d30df49 1
24c7cbd dev 9
5fd9434 merge
2cc73c2 dev 8
3e6b36c 1
25cfa26 add merge
1c891fe add merge
b215296 冲突解决
8707c3a dev 分支提交
6f87cc5 submit
b354fad submit
6b800ce dev 分支提交
bcdd674 master,feature1 合并提交
744aa74 master 分支提交
08ce9e2 feature1 第一次修改
e4f543f dev 第一次提交
2a4d9dd 删除 test.txt
7fd0411 添加 test.txt
比方说要对 “bug 修复” 这次提交打标签,它对应的 commit id 是 87070e6,敲入命令:
$ git tag v0.9 87070e6
再用命令 git tag
查看标签:
$ git tag
v0.9
v1.0
注意,标签不是按时间顺序列出,而是按字母排序的。可以用 git show <tagname>
查看标签信息:
$ git show v0.9
commit 87070e6cc0eaa7446ca6562707c9fcd89ce90806 (tag: v0.9)
Author: v_hwhao <v_hwhao@tencent.com>
Date: Thu May 14 19:57:20 2020 +0800
bug 修复
diff --git a/readme.txt b/readme.txt
index c46537c..ae52ab5 100644
--- a/readme.txt
+++ b/readme.txt
@@ -3,4 +3,5 @@
55555
66666
dev 7
-dev 8
\ No newline at end of file
+dev 8
+Bug <D0><B4>
\ No newline at end of file
可以看到,v0.9 确实打在 “ bug 修复” 这次提交上。
还可以创建带有说明的标签,用 -a
指定标签名,-m
指定说明文字:
$ git tag -a v0.1 -m "version 0.1 released" e4f543f
用命令 git show <tagname>
可以看到说明文字:
$ git show v0.1
tag v0.1
Tagger: luoma-haowei <903464199@qq.com>
Date: Tue May 19 18:51:21 2020 +0800
version 0.1 released
commit e4f543f197146957abaa827daa7f5800a50a11a3 (tag: v0.1)
Author: v_hwhao <v_hwhao@tencent.com>
Date: Tue May 12 19:49:58 2020 +0800
dev 第一次提交
diff --git a/readme.txt b/readme.txt
index 336df88..28726f9 100644
--- a/readme.txt
+++ b/readme.txt
@@ -1,4 +1,5 @@
33333
44444
55555
-66677
\ No newline at end of file
+66677
注意:标签总是和某个 commit 挂钩。如果这个 commit 既出现在 master 分支,又出现在 dev 分支,那么在这两个分支上都可以看到这个标签。
小结
命令 git tag <tagname>
用于新建一个标签,默认为 HEAD,也可以指定一个 commit id;
命令 git tag -a <tagname> -m "blablabla..."
可以指定标签信息;
命令 git tag
可以查看所有标签。
操作标签
如果标签打错了,也可以删除:
$ git tag -d v0.1
Deleted tag 'v0.1' (was 8ffc496)
因为创建的标签都只存储在本地,不会自动推送到远程。所以,打错的标签可以在本地安全删除。
如果要推送某个标签到远程,使用命令 git push origin <tagname>
:
$ git push origin v1.0
Total 0 (delta 0), reused 0 (delta 0), pack-reused 0
To https://github.com/luoma-haowei/GitTest.git
* [new tag] v1.0 -> v1.0
或者,一次性推送全部尚未推送到远程的本地标签:
$ git push origin --tags
Total 0 (delta 0), reused 0 (delta 0), pack-reused 0
To https://github.com/luoma-haowei/GitTest.git
* [new tag] v0.9 -> v0.9
如果标签已经推送到远程,要删除远程标签就麻烦一点,先从本地删除:
$ git tag -d v0.9
Deleted tag 'v0.9' (was 87070e6)
然后,从远程删除。删除命令也是 push
,但是格式如下:
$ git push origin :refs/tags/v0.9
To https://github.com/luoma-haowei/GitTest.git
- [deleted] v0.9
要看看是否真的从远程库删除了标签,可以登陆 GitHub 查看。
小结
命令 git push origin <tagname>
可以推送一个本地标签;
命令 git push origin --tags
可以推送全部未推送过的本地标签;
命令 git tag -d <tagname>
可以删除一个本地标签;
命令 git push origin :refs/tags/<tagname>
可以删除一个远程标签。
自定义 Git
在 “安装Git” 一节中,我们已经配置了 user.name
和 user.email
,实际上,Git 还有很多可配置项。
比如,让 Git 显示颜色,会让命令输出看起来更醒目:
$ git config --global color.ui true
忽略特殊文件
有些时候,你必须把某些文件放到 Git 工作目录中,但又不能提交它们,比如保存了数据库密码的配置文件啦,等等,每次 git status
都会显示 Untracked files …,有强迫症的童鞋心里肯定不爽。
好在 Git 考虑到了大家的感受,这个问题解决起来也很简单,在 Git 工作区的根目录下创建一个特殊的 .gitignore
文件,然后把要忽略的文件名填进去,Git 就会自动忽略这些文件。
不需要从头写 .gitignore
文件,GitHub已经为我们准备了各种配置文件,只需要组合一下就可以使用了。所有配置文件可以直接在线浏览:https://github.com/github/gitignore
忽略文件的原则是:
忽略操作系统自动生成的文件,比如缩略图等;
忽略编译生成的中间文件、可执行文件等,也就是如果一个文件是通过另一个文件自动生成的,那自动生成的文件就没必要放进版本库,比如 Java 编译产生的 .class 文件;
忽略你自己的带有敏感信息的配置文件,比如存放口令的配置文件。
举个例子:
假设你在 Windows 下进行 Python 开发,Windows 会自动在有图片的目录下生成隐藏的缩略图文件,如果有自定义目录,目录下就会有 Desktop.ini 文件,因此你需要忽略 Windows 自动生成的垃圾文件:
# Windows:
Thumbs.db
ehthumbs.db
Desktop.ini
然后,继续忽略 Python 编译产生的 .pyc、.pyo、dist 等文件或目录:
# Python:
*.py[cod]
*.so
*.egg
*.egg-info
dist
build
加上你自己定义的文件,最终得到一个完整的 .gitignore
文件,内容如下:
# Windows:
Thumbs.db
ehthumbs.db
Desktop.ini
# Python:
*.py[cod]
*.so
*.egg
*.egg-info
dist
build
# My configurations:
db.ini
deploy_key_rsa
最后一步就是把 .gitignore
也提交到Git,就完成了!当然检验 .gitignore
的标准是 git status
命令是不是说 working directory clean
。
使用 Windows 的童鞋注意了,如果你在资源管理器里新建一个 .gitignore
文件,它会非常弱智地提示你必须输入文件名,但是在文本编辑器里“保存”或者“另存为”就可以把文件保存为 .gitignore
了。
v_hwhao@v_hwhao-PC3 MINGW64 /d/v_hwhao/Git/GitTest (master)
$ git add .gitignore
v_hwhao@v_hwhao-PC3 MINGW64 /d/v_hwhao/Git/GitTest (master)
$ git commit -m "添加 .gitignore"
[master 2993be1] 添加 .gitignore
1 file changed, 16 insertions(+)
create mode 100644 .gitignore
有些时候,你想添加一个文件到 Git,但发现添加不了,原因是这个文件被 .gitignore
忽略了:
$ git add so.so
The following paths are ignored by one of your .gitignore files:
so.so
hint: Use -f if you really want to add them.
hint: Turn this message off by running
hint: "git config advice.addIgnoredFile false"
如果你确实想添加该文件,可以用 -f
强制添加到 Git:
$ git add -f so.so
或者你发现,可能是 .gitignore
写得有问题,需要找出来到底哪个规则写错了,可以用 git check-ignore
命令检查:
$ git check-ignore -v egg.egg
.gitignore:9:*.egg egg.egg
Git 会告诉我们,.gitignore
的第 3 行规则忽略了该文件,于是我们就可以知道应该修订哪个规则。
小结
忽略某些文件时,需要编写 .gitignore
;
.gitignore
文件本身要放到版本库里,并且可以对 .gitignore
做版本管理!
配置别名
有没有经常敲错命令?比如 git status
?status 这个单词真心不好记。
如果敲 git st
就表示 git status
那就简单多了,当然这种偷懒的办法我们是极力赞成的。
我们只需要敲一行命令,告诉 Git,以后 st
就表示 status
:
$ git config --global alias.st status
好了,现在敲 git st
看看效果。
$ git st
On branch master
Your branch is ahead of 'origin/master' by 1 commit.
(use "git push" to publish your local commits)
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: so.so
当然还有别的命令可以简写,很多人都用 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'
当你敲入命令:
$ git unstage test.py
实际上 Git 执行的是:
$ git reset HEAD test.py
配置一个 git last
,让其显示最后一次提交信息:
$ git config --global alias.last 'log -1'
这样,用 git last
就能显示最近一次的提交:
$ git last
commit 2993be184ad10eb66e58bcfe128baa85e73ad3b3 (HEAD -> master)
Author: luoma-haowei <903464199@qq.com>
Date: Tue May 19 19:22:06 2020 +0800
添加 .gitignore
甚至还有人丧心病狂地把 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 = false
bare = false
logallrefupdates = true
symlinks = false
ignorecase = true
[remote "origin"]
url = https://github.com/luoma-haowei/GitTest.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
配置别名也可以直接修改这个文件,如果改错了,可以删掉文件重新通过命令配置。
小结
给 Git 配置好别名,就可以输入命令时偷个懒。我们鼓励偷懒。
搭建 Git 服务器
在远程仓库一节中,我们讲了远程仓库实际上和本地仓库没啥不同,纯粹为了 7x24 小时开机并交换大家的修改。
GitHub 就是一个免费托管开源代码的远程仓库。但是对于某些视源代码如生命的商业公司来说,既不想公开源代码,又舍不得给 GitHub 交保护费,那就只能自己搭建一台 Git 服务器作为私有仓库使用。
搭建 Git 服务器需要准备一台运行 Linux 的机器,强烈推荐用 Ubuntu 或 Debian,这样,通过几条简单的 apt 命令就可以完成安装。
假设你已经有 sudo 权限的用户账号,下面,正式开始安装。
第一步,安装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/passwd 文件完成。找到类似下面的一行:
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 就是这个工具。
这里我们也不介绍 Gitolite 了,不要把有限的生命浪费到权限斗争中。
小结
搭建 Git 服务器非常简单,通常 10 分钟即可完成;
要方便管理公钥,用 Gitosis;
要像 SVN 那样变态地控制权限,用 Gitolite。
使用 SourceTree
当我们对 Git 的提交、分支已经非常熟悉,可以熟练使用命令操作 Git 后,再使用 GUI 工具,就可以更高效。
Git 有很多图形界面工具,这里我们推荐 SourceTree,它是由 Atlassian 开发的免费 Git 图形界面工具,可以操作任何 Git 库。
首先从官网下载 SourceTree 并安装,然后直接运行 SourceTree。
第一次运行 SourceTree 时,SourceTree 并不知道我们的 Git 库在哪。如果本地已经有了 Git 库,直接从资源管理器把文件夹拖拽到 SourceTree上,就添加了一个本地 Git 库:
也可以选择“New”-“Clone from URL”直接从远程克隆到本地。
提交
我们双击 GitTest 这个本地库,SourceTree 会打开另一个窗口,展示这个 Git 库的当前所有分支以及文件状态。选择左侧面板的 WORKSPACE
-文件状态
,右侧会列出当前已修改的文件(Unstaged files):
选中某个文件,该文件就自动添加到 “已暂存文件”,实际上是执行了 git add so.so
命令:
然后,我们在下方输入 Commit 描述,点击“提交”,就完成了一个本地提交:
实际上是执行了 git commit -m "提交ddd"
命令。
使用 SourceTree 进行提交就是这么简单,它的优势在于可以可视化地观察文件的修改,并以红色和绿色高亮显示。
分支
在左侧面板的“分支”下,列出了当前本地库的所有分支。当前分支会加粗并用 ○
标记。要切换分支,我们只需要选择该分支,例如 添加 .gitignore
,然后点击右键,在弹出菜单中选择 “检出”,实际上是执行命令 git checkout master
:
要合并分支,同样选择待合并分支,例如 合并 bug 修复 101
,然后点击右键,在弹出菜单中选择“合并”,实际上是执行命令 git merge 合并 bug 修复 101
:
推送
在 SourceTree 的工具栏上,分别有 Pull 和 Push,分别对应命令 git pull
和 git push
,只需注意本地和远程分支的名称要对应起来,使用时十分简单。
注意到使用 SourceTree 时,我们只是省下了敲命令的麻烦,SourceTree 本身还是通过 Git 命令来执行任何操作。如果操作失败,SourceTree 会自动显示执行的 Git 命令以及错误信息,我们可以通过 Git 返回的错误信息知道出错的原因:
小结
使用 SourceTree 可以以图形界面操作 Git,省去了敲命令的过程,对于常用的提交、分支、推送等操作来说非常方便。
SourceTree 使用 Git 命令执行操作,出错时,仍然需要阅读 Git 命令返回的错误信息。
期末总结
终于到了期末总结的时刻了!
经过几天的学习,相信你对 Git 已经初步掌握。一开始,可能觉得 Git 上手比较困难,尤其是已经熟悉 SVN 的童鞋,没关系,多操练几次,就会越用越顺手。
Git 虽然极其强大,命令繁多,但常用的就那么十来个,掌握好这十几个常用命令,你已经可以得心应手地使用 Git 了。
友情附赠国外网友制作的 Git Cheat Sheet,建议打印出来备用:
现在告诉你Git的官方网站:http://git-scm.com。 英文自我感觉不错的童鞋,可以经常去官网看看。什么,打不开网站?相信我,我给出的绝对是官网地址,而且,Git 官网决没有那么容易宕机,可能是你的人品问题,赶紧面壁思过,好好想想原因。
如果你学了 Git 后,工作效率大增,有更多的空闲时间健身看电影,那我的教学目标就达到了。
谢谢观看!
配置 ssh
ssh 模式比 https 模式的一个重要好处就是,每次 push、pull、fetch 等操作时,不用重复 填写遍用户名密码。
前提是你必须是这个项目的拥有者或者合作者,且配好了 ssh key。
如何配置 SSH key
1.检查你的电脑上是否已经生成了 SSH Key 在 git bash 下执行如下命令
Administrator@V_HWHAO-PC4 MINGW64 /d/Git/JianFa (master)
$ cd ~
Administrator@V_HWHAO-PC4 MINGW64 ~
$ cd .ssh
bash: cd: .ssh: No such file or directory
如果已经有这个文件包,删除就行了
2.创建SSH Key: ssh-keygen -t rsa -C luoma_haowei@qq.com
成功的话会在 ~/
下生成 .ssh
文件夹,进去,打开id_rsa.pub
,复制里面的 key。
3.进入 .ssh
文件包 ``,打印 id_rsa.pub 的内容,复制全部内容
4.登录 Github 后,右上角点击 setting
5.在左侧菜单中选择 SSH and GPG keys
, 在右边点击 New SSH key
6.Title 随便写,Key 把之前 id_rsa.pub
的内容复制进去,点击 Add SSH key
,设置 ssh key 完成。
7.测试连通性,改用 ssh 连接
git@github.com:luoma-haowei/GitTest.git
切换到 GitTest 目录,建立新的远程代号 git remote add originssh git@github.com:luoma-haowei/GitTest.git
,并在此目录下新建文件 ssh.txt
,内容为 sshTest
Administrator@V_HWHAO-PC4 MINGW64 /d/Git/GitTest (master)
$ git remote add originssh git@github.com:luoma-haowei/GitTest.git
Administrator@V_HWHAO-PC4 MINGW64 /d/Git/GitTest (master)
$ git add ssh.txt
Administrator@V_HWHAO-PC4 MINGW64 /d/Git/GitTest (master)
$ git commit -m 'ssh set test'
[master aee40bc] ssh set test
1 file changed, 1 insertion(+)
create mode 100644 ssh.txt
8.推送到 GitHub,git push originssh master
,第一次使用会要求输入个 yes
Administrator@V_HWHAO-PC4 MINGW64 /d/Git/GitTest (master)
$ git push originssh master
Warning: Permanently added the RSA host key for IP address '192.30.255.112' to the list of known hosts.
Enumerating objects: 4, done.
Counting objects: 100% (4/4), done.
Delta compression using up to 8 threads
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 270 bytes | 90.00 KiB/s, done.
Total 3 (delta 1), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (1/1), completed with 1 local object.
To github.com:luoma-haowei/GitTest.git
640d977..71287fc master -> master
进入 GitHub 对应页面,刷新,可以看到新增的 ssh.txt
文件已经推送成功了。
Git 工作流
简单来说就是,一个项目的成员们在工作中统一使用 Git 的工作方式。
1.集中式工作流
像 SVN 一样,集中式工作流以中央仓库作为项目所有修改的单点实体。所有修改都提交到 Master 这个分支上。
这种方式与SVN的主要区别就是开发人员有本地库。Git 很多特性并没有用到。
2.GitFlow 工作流
Gitflow 工作流通过为功能开发、发布准备和维护设立了独立的分支,让发布迭代过程更流畅。严格的分支模型也为大型项目提供了一些非常必要的结构。
3.Git 工作流
分支种类 | 说明 |
---|---|
主干分支 master | 主要负责管理正在运行的生产环境代码。永远保持与正在运行的生产环境完全一致。 |
开发分支 develop | 主要负责管理正在开发过程中的代码。一般情况下应该是最新的代码。 |
bug修理分支 hotfix | 主要负责管理生产环境下出现的紧急修复的代码。 从主干分支分出,修理完毕并测试上线后,并回主干分支。并回后,视情况可以删除该分支。 |
发布版本分支 release | 较大的版本上线前,会从开发分支中分出发布版本分支,进行最后阶段的集成测试。该版本上线后,会合并到主干分支。生产环境运行一段阶段较稳定后可以视情况删除。 |
功能分支 feature | 为了不影响较短周期的开发工作,一般把中长期开发模块,会从开发分支中独立出来。 开发完成后会合并到开发分支。 |