安装Git
Git的下载地址:Git官网下载地址
Git本地仓库和命令
配置用户
下载完Git后,右键会有一个Git Bash here
的选项,点击后会弹出一个类似于命令行的窗口:
在此输入此命令配置用户名和邮箱:
$ 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 add
将readme.txt
添加到仓库:
Administrator@XXX MINGW64 /e/git/learngit (master)
$ git add readme.txt
(2)使用明明git commit
将readme.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"
操作的话,会弹出这样一个窗口提示我们输入为什么要合入本次修改:
此时,我们可以按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
文件:
create a readme file
add Yes
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 add
和git 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_rsa
和id_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地址:
然后我们在本地的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
文件。
新的仓库建完之后,我们可以在此仓库的页面找到一个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 -> master
中HEAD
是表示当前的版本,这是因为我们只有一个分支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 checkout
和git 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 revert
。git 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 add
和git 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 remote
或git 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
分支的链接,根据提示,设置dev
和origin/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