赞
踩
版本控制是一种记录一个或若干文件内容变化,以便将来查阅特定版本修订情况的系统。 如果你是位图形或网页设计师,可能会需要保存某一幅图片或页面布局文件的所有修订版本,采用版本控制系统(CVS)是个明智的选择。 有了它你就可以将选定的文件回溯到之前的状态,甚至将整个项目都回退到过去某个时间点的状态,可以比较文件的变化细节,查出最后是谁修改了哪个地方,从而找出导致怪异问题出现的原因,又是谁在何时报告了某个功能缺陷等等。
本地版本控制系统
本地版本控制系统,大多都是采用某种简单的数据库来记录文件的历次更新差异。其中最流行的一种叫做 RCS,现今许多计算机系统上都还看得到它的踪影。RCS的工作原理是在硬盘上保存补丁集(补丁是指文件修订前后的变化);通过应用所有的补丁,可以重新计算出各个版本的文件内容。
集中化的版本控制系统
如何让在不同系统上的开发者协同工作? 于是,集中化的版本控制系统(Centralized Version Control Systems,CVCS)应运而生。 这类系统,诸如 CVS、Subversion 以及 Perforce 等,都有一个单一的集中管理的服务器,保存所有文件的修订版本,而协同工作的人们都通过客户端连到这台服务器,取出最新的文件或者提交更新。 多年以来,这已成为版本控制系统的标准做法。
集中式版本控制系统最大的毛病就是必须联网才能工作,如果在局域网内还好,带宽够大,速度够快。可如果在互联网上,遇到网速慢的话,可能提交一个10M的文件就需要5分钟。如果中央服务器宕机一小时,那么在这一小时内,谁都无法提交更新,也就无法协同工作。 如果中心数据库所在的磁盘发生损坏,又没有做恰当备份,毫无疑问你将丢失所有数据——包括项目的整个变更历史,只剩下人们在各自机器上保留的单独快照。 本地版本控制系统也存在类似问题,只要整个项目的历史记录被保存在单一位置,就有丢失所有历史更新记录的风险。
分布式版本控制系统
于是分布式版本控制系统(Distributed Version Control System,DVCS)面世了。 在这类系统中,像 Git、Mercurial、Bazaar 以及 Darcs 等,客户端并不只提取最新版本的文件快照, 而是把代码仓库完整地镜像下来,包括完整的历史记录。 这么一来,任何一处协同工作用的服务器发生故障,事后都可以用任何一个镜像出来的本地仓库恢复。 因为每一次的克隆操作,实际上都是一次对代码仓库的完整备份。
更进一步,许多这类系统都可以指定和若干不同的远端代码仓库进行交互。籍此,就可以在同一个项目中,分别和不同工作小组的人相互协作。 你可以根据需要设定不同的协作流程,比如层次模型式的工作流,而这在以前的集中式系统中是无法实现的。
Git工作区域:
(1)工作区:添加、编辑、修改文件等操作 (自己能看到的本地目录)
(2)暂存区:暂存已修改的文件,最后会统一提交到Git仓库中 (git add所在的区域)
(3)Git仓库:最终确定的文件保存到Git仓库成为一个新的版本 (git push到的区域)
在本地还有开发者的工作区 Workspace 和暂存区 Index / Stage。工作区是你能看到的目录:
工作区有一个隐藏目录.git,这个不算工作区,而是Git的版本库。
暂存区是存放在".git"文件夹下的index文件中,会记录 git add 添加文件的相关信息,可以用 git status 查看暂存区状态。
Git的版本库里存了很多东西,其中最重要的就是称为stage(或者叫index)的暂存区,还有Git为我们自动创建的第一个分支master,以及指向master的一个指针叫HEAD。 
把文件往Git版本库里添加的时候,是分两步执行的:
git add把文件添加进去,实际上就是把文件修改添加到暂存区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文本文件
用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
Untracked files:
(use "git add <file>..." to include in what will be committed)
LICENSE
no changes added to commit (use "git add" and/or "git commit -a")
Git非常清楚地告诉我们,readme.txt被修改了,而LICENSE还从来没有被添加过,所以它的状态是Untracked。
现在,使用两次命令git add,把readme.txt和LICENSE都添加后,用git status再查看一下:
31156@DESKTOP-XK MINGW64 ~/Desktop/mygit (master)
$ git add readme.txt LICENSE
31156@DESKTOP-XK MINGW64 ~/Desktop/mygit (master)
$ git status
On branch master
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: LICENSE
modified: readme.txt
现在,暂存区的状态就变成这样了: 
所以,git add命令实际上就是把要提交的所有修改放到暂存区(Stage),然后,执行git commit就可以一次性把暂存区的所有修改提交到分支。
$ git commit -m "understand how stage works"
[master 0db95d5] understand how stage works
2 files changed, 3 insertions(+), 1 deletion(-)
create mode 100644 LICENSE
一旦提交后,如果你又没有对工作区做任何修改,那么工作区就是“干净”的:
$ git status
On branch master
nothing to commit, working tree clean
现在版本库变成了这样,暂存区就没有任何内容了: 

Git管理的文件三种状态对应Git工作流程:
Git 有三种状态,你的文件可能处于其中之一: 已提交(committed)、已修改(modified) 和 已暂存(staged)
Git工作流程
(1)在工作区目录中添加、修改、删除文件
(2)将需要进行版本管理的文件放入暂存区**.git**
(3)将暂存区的文件提交到Git仓库中
如果 Git 目录中保存着特定版本的文件,就属于 已提交 状态。 如果文件已修改并放入暂存区,就属于 已暂存 状态。 如果自上次检出后,作了修改但还没有放到暂存区域,就是 已修改 状态。
在Linux上安装
$ sudo apt install git-all
要了解更多选择,Git 官方网站上有在各种 Unix 发行版的系统上安装步骤:
Git 官方网站:https://git-scm.com/download/linux
在 Windows 上安装
在Windows上使用Git,可以从Git官网直接下载:
https://git-scm.com/downloads
配置用户信息
安装完 Git 之后,要做的第一件事就是设置你的用户名和邮件地址。 这一点很重要,因为每一个 Git 提交都会使用这些信息,它们会写入到你的每一次提交中,不可更改:
git config --global user.name "ldf2022"
git config --global user.email "1304018570@qq.com"
如果使用了 **–global **选项,那么该命令只需要运行一次,因为之后无论你在该系统上做任何事情, Git 都会使用那些信息。 当你想针对特定项目使用不同的用户名称与邮件地址时,可以在那个项目目录下运行没有 --global 选项的命令来配置。
检查配置信息
** **如果想要检查你的配置,可以使用 **git config --list 命令来列出所有 Git 当时能找到的配置。 **
git config -l

获取帮助
** **若你使用 Git 时需要获取帮助,有三种等价的方法可以找到 Git 命令的综合手册(manpage):
$ git help <verb>
$ git <verb> --help
$ man git-<verb>
$ git help config
Git 的使用——提交避免输入用户名和密码
以上的配置会导致每次远程提交的时候,都要登录账号和密码,非常麻烦。这里介绍以下SSH公钥免密的方法,当然也还有其他的方法,可以网上自己寻找。这边贴一个:https://zhuanlan.zhihu.com/p/358721423
git ssh 方式免密提交方式需要将 ssh-keygen 生成的公钥放到服务器上
全局用户名密码配置
git config --global user.name "ldf2022"
git config --global user.email "1304018570@qq.com"
项目初始化,生成 .git 目录,配置 ssh 远程项目地址。
$ git remote add origin git@gitee.com:ldf2022/tiny-httpd.git
生成公钥和私钥
1、首先需要检查你电脑是否已经有 SSH key
$ cd ~/.ssh
$ ls

如果不是第一次使用,已经存在 id_rsa.pub 或 id_dsa.pub 文件。请执行下面的操作,清理原有 ssh 密钥。
$ mkdir key_backup
$ cp id_rsa* key_backup
$ rm id_rsa*
2、执行生成公钥和私钥的命令,生成新的密钥:
$ ssh-keygen -t rsa -C "ldf"
代码参数:
按默认为空,直接按回车3下,生成 id_rsa 和 id_rsa.pub 两个秘钥文件。
执行查看公钥信息:
$ cat ~/.ssh/id_rsa.pub

3、复制公钥信息,打开 gitee,我的账户-设置-SSH 公钥,如下图所示,把公钥粘贴到公钥文本框中,标题自己定义,然后点击确定按键,输入密码。


然后,提交时就不再需要用户名和密码了
通常有两种获取 Git 项目仓库的方式:
两种方式都会在你的本地机器上得到一个工作就绪的 Git 仓库。
1.创建全新的仓库,需要用git管理的项目的根目录执行:git init
2.另一种方法就是克隆远程目录,由于是将远程服务器上的仓库完全镜像一份至本地:git clone [url]
工作目录下的每一个文件都不外乎这两种状态:已跟踪 或 未跟踪。 已跟踪的文件是指那些被纳入了版本控制的文件,在上一次快照中有它们的记录,在工作一段时间后, 它们的状态可能是未修改,已修改或已放入暂存区。简而言之,已跟踪的文件就是 Git 已经知道的文件。
工作目录中除已跟踪文件外的其它所有文件都属于未跟踪文件,它们既不存在于上次快照的记录中,也没有被放入暂存区。 初次克隆某个仓库的时候,工作目录中的所有文件都属于已跟踪文件,并处于未修改状态,因为 Git 刚刚检出了它们, 而你尚未编辑过它们。
编辑过某些文件之后,由于自上次提交后你对它们做了修改,Git 将它们标记为已修改文件。 在工作时,可以选择性地将这些修改过的文件放入暂存区,然后提交所有已暂存的修改,如此反复。
跟踪新文件
** **在项目下编写一个readme.txt文件,内容如下:
Git is a version control system.
使用命令 git add开始跟踪一个文件运行:
$ git add readme.txt
git add命令使用文件或目录的路径作为参数;如果参数是目录的路径,该命令将递归地跟踪该目录下的所有文件。
可以用 git status 命令查看哪些文件处于什么状态:
$ git status
On branch master
No commits yet
Changes to be committed:
(use "git rm --cached <file>..." to unstage)
new file: readme.txt
只要在 Changes to be committed这行下面的,就说明是已暂存状态。 如果此时提交,那么该文件在你运行 git add 时的版本将被留存在后续的历史记录中。
用命令git commit告诉Git,把文件提交到仓库:
$ git commit -m "wrote a readme file"
[master (root-commit) 0a48055] wrote a readme file
1 file changed, 1 insertion(+)
create mode 100644 readme.txt
-m后面输入的是本次提交的说明,可以输入任意内容,当然最好是有意义的,这样你就能从历史记录里方便地找到改动记录。
git commit命令执行成功后会告诉你,1 file changed:1个文件被改动(我们新添加的readme.txt文件);1 insertions:插入了一行内容(readme.txt有一行内容)。
继续修改readme.txt文件,改成如下内容:
Git is a distributed version control system.
Git is free software.
运行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")
上面的命令输出告诉我们,readme.txt被修改过了,但还没有准备提交的修改。
用git diff这个命令看看具体修改了什么内容:
$ git diff readme.txt
diff --git a/readme.txt b/readme.txt
index 7950452..013b5bc 100644
--- a/readme.txt
+++ b/readme.txt
@@ -1 +1,2 @@
-Git is a version control system.
\ No newline at end of file
+Git is a distributed version control system.
+Git is free software.
\ No newline at end of file
提交到仓库:
$ git add readme.txt
31156@DESKTOP-XK MINGW64 ~/Desktop/mygit (master)
$ git commit -m "add distributed"
[master 98d9024] add distributed
1 file changed, 2 insertions(+), 1 deletion(-)
提交后,再用git status命令看看仓库的当前状态:
$ git status
On branch master
nothing to commit, working tree clean
Git告诉我们当前没有需要提交的修改,而且,工作目录是干净(working tree clean)的
先对test.txt进行修改,然后提交记录
git add test.txt
git commit -m "test"
随后可以通过git log来查看提交记录
git log

git log命令显示从最近到最远的提交日志,可以看到3次提交,最近的一次是append GPL,上一次是add distributed,最早的一次是wrote a readme file。
加上–pretty=oneline参数,减少输出信息: 
一大串类似dc740…的是commit id(版本号),和SVN不一样,Git的commit id不是1,2,3……递增的数字,而是一个SHA1计算出来的一个非常大的数字,用十六进制表示。
把readme.txt回退到上一个版本,也就是add distributed的那个版本,怎么做呢?
git reset --hard HEAD^

此时的test.txt的内容就回退到之前了,也就是空
用git log再看看现在版本库的状态: 
最新的那个版本append GPL已经看不到了!回不去了?
只要上面的命令行窗口还没有被关掉,就可以顺着往上找啊找啊,找到那个append GPL的commit id是dc74…,于是就可以指定回到未来的某个版本:
$ git reset --hard dc74
HEAD is now at dc74077 append GPL
版本号没必要写全,前几位就可以了,Git会自动去找 

Git提供了git reflog命令来记录每一次命令: 方便对版本的跳转
为什么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.
然后,添加:
31156@DESKTOP-XK MINGW64 ~/Desktop/mygit (master)
$ git add readme.txt
31156@DESKTOP-XK MINGW64 ~/Desktop/mygit (master)
$ git status
On branch master
Changes to be committed:
(use "git restore --staged <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 commit -m "git tracks changes"
[master cd3f0b2] 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 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 db28b2c..295b239 100644
--- a/readme.txt
+++ b/readme.txt
@@ -1,4 +1,5 @@
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.
+Git tracks changes of files.
\ No newline at end of file
可见,第二次修改确实没有被提交。
怎么提交第二次修改呢?可以继续git add再git commit,也可以别着急提交第一次修改,先git add第二次修改,再git commit,就相当于把两次修改合并后一块提交了:
第一次修改 -> git add -> 第二次修改 -> git add -> git commit
假设在readme.txt中错误的添加了一行:My stupid boss still prefers SVN.
$ 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.
Git tracks changes of files.
My stupid boss still prefers SVN.
在准备提交前,发现了错误。
很容易地纠正它,可以删掉最后一行,手动把文件恢复到上一个版本的状态。如果用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 restore 可以丢弃工作区的修改:
$ git restore readme.txt
命令的意思就是,把readme.txt文件在工作区的修改全部撤销,这里有两种情况:
readme.txt自修改后还没有被放到暂存区,现在,撤销修改就回到和版本库一模一样的状态readme.txt已经添加到暂存区后,又作了修改,现在,撤销修改就回到添加到暂存区后的状态总之,就是让这个文件回到最近一次git commit或git add时的状态。现在,看看readme.txt的文件内容:
$ 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.
Git tracks changes of files.
假定不但添加了错误的一行,还git add到暂存区了:
庆幸的是,在commit之前,发现了这个问题。用git status查看一下,修改只是添加到了暂存区,还没有提交:
$ git status
On branch master
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: readme.txt
Git同样告诉我们,用命令**git restore --staged **可以把暂存区的修改撤销掉(unstage),重新放回工作区:
$ git restore --staged readme.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 restore <file>..." to discard changes in working directory)
modified: readme.txt
no changes added to commit (use "git add" and/or "git commit -a")
丢弃工作区的修改:
31156@DESKTOP-XK MINGW64 ~/Desktop/mygit (master)
$ git restore readme.txt
31156@DESKTOP-XK MINGW64 ~/Desktop/mygit (master)
$ git status
On branch master
nothing to commit, working tree clean
在Git中,删除也是一个修改操作,先添加一个新文件fileGit并且提交: 
这个时候,Git知道你删除了文件,因此,工作区和版本库就不一致了,git status命令会立刻告诉你哪些文件被删除了: 
现在你有两个选择:
git rm删掉,并且git commit:
现在,文件就从版本库中被删除了。
rm命令就是在工作区删文件git rm就是删文件,并且把删文件的修改提交到暂存区。相当于rm删文件后,git add 提交,保存修改$ git checkout -- file
git checkout其实是用版本库里的版本替换工作区的版本,无论工作区是修改还是删除,都可以“一键还原”。
file文件又回来了/!
Git是分布式版本控制系统,同一个Git仓库,可以分布到不同的机器上。怎么分布呢?
最早,肯定只有一台机器有一个原始版本库,此后,别的机器可以“克隆”这个原始版本库,而且每台机器的版本库其实都是一样的,并没有主次之分。
可以自己搭建一台运行Git的服务器,不过好在这个世界上有个叫GitHub的神奇的网站,从名字就可以看出,这个网站就是提供Git仓库托管服务的,所以,只要注册一个GitHub账号,就可以免费获得Git远程仓库。
不过Github的登录需要VPN才可登录,国内强者也不容落后,创造出了Gitee网站.
我们先以gitee为例子

目前,可以从这个仓库克隆出新的仓库,也可以把一个已有的本地仓库与之关联,然后,把本地仓库的内容推送到Gitee仓库。
根据Gitee的提示,在本地的mygit仓库下运行命令:
$ git remote add origin https://gitee.com/ldf2022/tiny-httpd.git
添加后,远程库的名字就是origin,这是Git默认的叫法,也可以改成别的,但是origin这个名字一看就知道是远程库。
下一步,就可以把本地库的所有内容推送到远程库上:
$ git push -u origin master
Enumerating objects: 24, done.
Counting objects: 100% (24/24), done.
Delta compression using up to 16 threads
Compressing objects: 100% (18/18), done.
Writing objects: 100% (24/24), 1.89 KiB | 646.00 KiB/s, done.
Total 24 (delta 7), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (7/7), done.
To https://github.com/xk1201333/mygit.git
* [new branch] master -> master
branch 'master' set up to track 'origin/master'.
把本地库的内容推送到远程,用git push命令,实际上是把当前分支master推送到远程。
由于远程库是空的,第一次推送master分支时,加上了-u参数,Git不但会把本地的master分支内容推送的远程新的master分支,还会把本地的master分支和远程的master分支关联起来,在以后的推送或者拉取时就可以简化命令。
从现在起,只要本地作了提交,就可以通过命令:
git push origin master
把本地master分支的最新修改推送至Gitee,现在,你就拥有了真正的分布式版本库!
如果添加的时候地址写错了,或者就是想删除远程库,可以用git remote rm 命令。使用前,建议先用git remote -v查看远程库信息:
$ git remote -v
origin https://github.com/xk1201333/mygit.git (fetch)
origin https://github.com/xk1201333/mygit.git (push)
此处的“删除”其实是解除了本地和远程的绑定关系,并不是物理上删除了远程库。远程库本身并没有任何改动。要真正删除远程库,需要登录到GitHub,在后台页面找到删除按钮再删除。
假设从零开发,那么最好的方式是先创建远程库,然后,从远程库克隆。克隆仓库的命令是 git clone ,要克隆 Git 的链接库 libgit2,可以用下面的命令:
$ git clone https://github.com/libgit2/libgit2
这会在当前目录下创建一个名为 “libgit2” 的目录,并在这个目录下初始化一个 .git 文件夹, 从远程仓库拉取下所有数据放入 .git 文件夹,然后从中读取最新版本的文件的拷贝。 如果你进入到这个新建的 libgit2 文件夹,你会发现所有的项目文件已经在里面了,准备就绪等待后续的开发和使用。
如果想在克隆远程仓库的时候,自定义本地仓库的名字,你可以通过额外的参数指定新的目录名:
$ git clone https://github.com/libgit2/libgit2 mylibgit
这会执行与上一条命令相同的操作,但目标目录名变为了 mylibgit
在GitHub出现以前,开源项目开源容易,但让广大人民群众参与进来比较困难,因为要参与,就要提交代码,而给每个想提交代码的群众都开一个账号那是不现实的,因此,群众也仅限于报个bug,即使能改掉bug,也只能把diff文件用邮件发过去,很不方便。
但是在GitHub上,利用Git极其强大的克隆和分支功能,广大人民群众真正可以第一次自由参与各种开源项目了。
如何参与一个开源项目呢?比如人气极高的bootstrap项目,这是一个非常强大的CSS框架,你可以访问它的项目主页https://github.com/twbs/bootstrap,点“Fork”就在自己的账号下克隆了一个bootstrap仓库,然后,从自己的账号下clone:
git clone git@github.com:xk1201333/bootstrap.git
一定要从自己的账号下clone仓库,这样你才能推送修改。如果从bootstrap的作者的仓库地址git@github.com:twbs/bootstrap.git克隆,因为没有权限,你将不能推送修改。
Bootstrap的官方仓库twbs/bootstrap、你在GitHub上克隆的仓库my/bootstrap,以及你自己克隆到本地电脑的仓库,他们的关系就像下图显示的那样: 
如果你想修复bootstrap的一个bug,或者新增一个功能,立刻就可以开始干活,干完后,往自己的仓库推送。
如果希望bootstrap的官方库能接受你的修改,可以在GitHub上发起一个pull request。当然,对方是否接受你的pull request就不一定了。
分支在实际中有什么用呢?假设你准备开发一个新功能,但是需要两周才能完成,第一周你写了50%的代码,如果立刻提交,由于代码还没写完,不完整的代码库会导致别人不能干活了。如果等代码全部写完再一次提交,又存在丢失每天进度的巨大风险。
现在有了分支,就不用怕了。你创建了一个属于你自己的分支,别人看不到,还继续在原来的分支上正常工作,而你在自己的分支上干活,想提交就提交,直到开发完毕后,再一次性合并到原来的分支上,这样,既安全,又不影响别人工作。
每次提交,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的当前提交,就完成了合并: 
所以Git合并分支也很快!就改改指针,工作区内容也不变!
合并完分支后,甚至可以删除dev分支。删除dev分支就是把dev指针给删掉,删掉后,就剩下了一条master分支: 
创建dev分支,然后切换到dev分支:
$ git checkout -b dev
Switched to a new branch 'dev'
# 可以分成两步
git branch dev #新建分支
git checkout dev #切换分支
然后,用git branch命令查看当前分支:
$ git branch
* dev
master
git branch命令会列出所有分支,当前分支前面会标一个*号。
然后,就可以在dev分支上正常提交,比如对readme.txt做个修改,加上一行:Creating a new branch is quick.
然后提交:
31156@DESKTOP-XK MINGW64 ~/Desktop/mygit (dev)
$ git add readme.txt
31156@DESKTOP-XK MINGW64 ~/Desktop/mygit (dev)
$ git commit -m "branch test"
[dev d0d2fe2] branch test
1 file changed, 2 insertions(+), 1 deletion(-)
现在,dev分支的工作完成,切换回master分支:
$ git checkout master
Note: switching to 'master'.
切换回master分支后,再查看一个readme.txt文件,刚才添加的内容不见了!因为那个提交是在dev分支上,而master分支此刻的提交点并没有变: 
现在,把dev分支的工作成果合并到master分支上:
$ git merge dev
Updating ec2d7e6..d0d2fe2
Fast-forward
readme.txt | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
git merge命令用于合并指定分支到当前分支。合并后,再查看readme.txt的内容,就可以看到,和dev分支的最新提交是完全一样的。
注意到上面的Fast-forward信息,Git告诉我们,这次合并是“快进模式”,也就是直接把master指向dev的当前提交,所以合并速度非常快。
合并完成后,就可以放心地删除dev分支了:
$ git branch -d dev
Deleted branch dev (was d0d2fe2).
$ git branch
* master
最新版本的Git提供了新的git switch命令来切换分支
git checkout <name>或者git switch <name>git checkout -b <name>或者git switch -c <name>准备新的feature1分支,继续新分支开发:
$ git switch -c feature1
Switched to a new branch 'feature1'
修改test.txt最后一行,改为:Git is test txt.->branch 3 is created.
在feature1分支上提交:
git add test.txt
git commit -m "b3_test.txt"

切换到master分支:
git switch master

Git还会自动提示我们当前master分支比远程的master分支要超前1个提交。
在master分支上把test.txt文件的最后一行改为:Creating a new branch is quick & simple.
提交:
git add test.txt
git commit -m "master_test.txt"

现在,master分支和feature1分支各自都分别有新的提交:
这种情况下,Git无法执行“快速合并”,只能试图把各自的修改合并起来,但这种合并就可能会有冲突:
git merge b3


Git告诉我们,test.txt文件存在冲突,必须手动解决冲突后再提交。git status也可以告诉我们冲突的文件:
git status

查看test.txt的内容: 
Git用<<<<<<<,=======,>>>>>>>标记出不同分支的内容,修改如下后保存:Creating a new branch is quick and simple.
再提交:
$ git add test.txt
$ git commit -m "conflict fixed"
[master aa6fb38] conflict fixed
现在,master分支和feature1分支变成了下图所示: 
用带参数的git log也可以看到分支的合并情况:
$ git log --graph --pretty=oneline --abbrev-commmit
# 以图的形式展示每次的提交记录

最后,删除b3分支:v
$ git branch -d b3
已删除分支 b3(曾为 529c4ba)。
通常,合并分支时,如果可能,Git会用Fast forward模式,但这种模式下,删除分支后,会丢掉分支信息。
如果要强制禁用Fast forward模式,Git就会在merge时生成一个新的commit,这样,从分支历史上就可以看出分支信息。 (这也是与rebase的区别)
仍然创建并切换dev分支:
$ git switch -c dev
切换到一个新分支 'dev'
** **修改test.txt文件,并提交一个新的commit: 添加:new dev branch.
$ git add test.txt
$ git commit -m "dev_test"

现在,切换回master:
$ git switch master

准备合并dev分支,**请注意–no-ff参数,表示禁用Fast forward: **
$ git merge --no-ff -m "merge with no-ff" dev
Merge made by the 'recursive' strategy.
test.txt | 2 ++
1 file changed, 2 insertions(+)
因为本次合并要创建一个新的commit,所以加上-m参数,把commit描述写进去。
合并后,用git log看看分支历史:
$ git log --graph --pretty=oneline --abbrev-commit
#--abbrev-commit 省略版本号,只取前小部分的版本号

可以看到,不使用Fast forward模式,merge后就像这样: 
分支策略
** 在实际开发中,应该按照几个基本原则进行分支管理: **
首先,master分支应该是非常稳定的,也就是仅用来发布新版本,平时不能在上面干活;
那在哪干活呢?干活都在dev分支上,也就是说,dev分支是不稳定的,到某个时候,比如1.0版本发布时,再把dev分支合并到master上,在master分支发布1.0版本;你和你的小伙伴们每个人都在dev分支上干活,每个人都有自己的分支,时不时地往dev分支上合并就可以了。
所以,团队合作的分支看起来就像这样: 
软件开发中,bug就像家常便饭一样。有了bug就需要修复,在Git中,由于分支是如此的强大,所以,每个bug都可以通过一个新的临时分支来修复,修复后,合并分支,然后将临时分支删除。
当你接到一个修复一个代号101的bug的任务时,很自然地,你想创建一个分支issue-101来修复它,但是,等等,当前正在dev上进行的工作还没有提交:
$ git status
On branch dev
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
new file: hello.py
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: test.txt
并不是你不想提交,而是工作只进行到一半,还没法提交,预计完成还需1天时间。但是,必须在两个小时内修复该bug,怎么办?
幸好,**Git还提供了一个stash功能,可以把当前工作现场“储藏”起来,等以后恢复现场后继续工作: **
$ git stash
Saved working directory and index state WIP on dev: f52c633 add merge

现在,用git status查看工作区,就是干净的(除非有没有被Git管理的文件),因此可以放心地创建分支来修复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 bug-101
Switched to a new branch 'issue-101'
现在修复bug,需要把“Git is free software …”改为“Git is a free software …”,然后提交: 
修复完成后,切换到master分支,并完成合并,最后删除bug-101分支: 
现在,是时候接着回到dev分支干活了!
$ git switch dev
$ git status

工作区是干净的,刚才的工作现场存到哪去了?用git stash list命令看看:
$ git stash list

工作现场还在,Git把stash内容存在某个地方了,但是需要恢复一下,有两个办法:
一是用git stash apply恢复,但是恢复后,stash内容并不删除,你需要用git stash drop来删除;
另一种方式是用git stash pop,恢复的同时把stash内容也删了:
$ git stash pop

再用git stash list查看,就少了一个stash内容了: 
在master分支上修复了bug后,我们要想一想,dev分支是早期从master分支分出来的,所以,这个bug其实在当前dev分支上也存在。
那怎么在dev分支上修复同样的bug?重复操作一次,提交不就行了?有木有更简单的方法?
同样的bug,要在dev上修复,只需要把4c805e2 fix bug 101这个提交所做的修改“复制”到dev分支。注意:只想复制4c805e2 fix bug 101这个提交所做的修改,并不是把整个master分支merge过来。
为了方便操作,Git专门提供了一个cherry-pick命令,让我们能复制一个特定的提交到当前分支:
$ git branch
* dev
master
$ git cherry-pick 071e8f3
[master 1d4b803] fix bug 101
1 file changed, 1 insertion(+), 1 deletion(-)
Git自动给dev分支做了一次提交,注意这次提交的commit是1d4b803,它并不同于master的4c805e2,因为这两个commit只是改动相同,但确实是两个不同的commit。用git cherry-pick,我们就不需要在dev分支上手动再把修bug的过程重复一遍。
既然可以在master分支上修复bug后,在dev分支上可以“重放”这个修复过程,那么直接在dev分支上修复bug,然后在master分支上“重放”行不行?当然可以,不过你仍然需要git stash命令保存现场,才能从dev分支切换到master分支。
软件开发中,总有无穷无尽的新的功能要不断添加进来。
添加一个新功能时,你肯定不希望因为一些实验性质的代码,把主分支搞乱了,所以,每添加一个新功能,最好新建一个feature分支,在上面开发,完成后,合并,最后,删除该feature分支。
现在,接到了一个新任务:开发代号为Vulcan的新功能,该功能计划用于下一代星际飞船。于是准备开发:
$ git switch -c feature-vulcan
Switched to anew branch 'feature-vulcan'
5分钟后,开发完毕:
$ git add vulcan.c
$ git status
On branch feature-vulcan
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
new file: vulcan.c
$ git commit -m "add feature vulcan"
[feature-vulcan 287773e] add feature vulcan
1 file changed, 2 insertions(+)
create mode 100644 vulcan.c
切回dev,准备合并:
$ git switch dev
一切顺利的话,feature分支和bug分支是类似的,合并,然后删除。
但是!就在此时,接到上级命令,因经费不足,新功能必须取消!
虽然白干了,但是这个包含机密资料的分支还是必须就地销毁:
$ git branch -d feature-vulcan
error: 分支 'feature-vulcan' 没有完全合并。
如果您确认要删除它,执行 'git branch -D feature-vulcan'。
销毁失败。Git友情提醒,feature-vulcan分支还没有被合并,如果删除,将丢失掉修改,如果要强行删除,需要使用大写的-D参数。。
现在强行删除:
$ git branch -D feature-vulcan
已删除分支 feature-vulcan(曾为 82b75f2)。
当你从远程仓库克隆时,实际上Git自动把本地的master分支和远程的master分支对应起来了,并且,远程仓库的默认名称是origin。
要查看远程库的信息,用git remote:
$ git remote
origin
或者,用git remote -v显示更详细的信息:
$ git remote -v
origin https://gitee.com/ldf2022/tiny-httpd.git (fetch)
origin https://gitee.com/ldf2022/tiny-httpd.git (push)
上面显示了可以抓取和推送的origin的地址。如果没有推送权限,就看不到push的地址。
推送分支,就是把该分支上的所有本地提交推送到远程库。推送时,要指定本地分支,这样,Git就会把该分支推送到远程库对应的远程分支上:
$ git push origin master
如果要推送其他分支,比如dev,就改成:
$ git push origin dev
但是,并不是一定要把本地分支往远程推送,那么,哪些分支需要推送,哪些不需要呢?
master分支是主分支,因此要时刻与远程同步dev分支是开发分支,团队所有成员都需要在上面工作,所以也需要与远程同步bug分支只用于在本地修复bug,就没必要推到远程了,除非老板要看看你每周到底修复了几个bug;feature分支是否推到远程,取决于你是否和你的小伙伴合作在上面开发。总之,就是在Git中,分支完全可以在本地自己藏着玩,是否推送,视你的心情而定!
多人协作时,大家都会往master和dev分支上推送各自的修改。
现在,模拟一个你的小伙伴,可以在另一台电脑(注意要把SSH Key添加到GitHub)或者同一台电脑的另一个目录下克隆:
$ git remote add origin https://github.com/xk1201333/mygit.git
当你的小伙伴从远程库clone时,默认情况下,你的小伙伴只能看到本地的master分支:
$ 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 compressionusing upto 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 $ gitcommit -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: failedto pushsome refsto 'git@github.com:michaelliao/learngit.git' hint: Updates were rejected because the tipof yourcurrent branchis 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 informationfor the current branch.
Please specify which branch you want to mergewith.
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 upto track remote branch 'dev'from 'origin'.
再pull:
$ git pull
Auto-merging env.txt
CONFLICT (add/add): Merge conflictin env.txt
Automatic merge failed; fix conflictsandthen 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 compressionusing upto 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 origin/。
这就是多人协作的工作模式,一旦熟悉了,就非常简单。
多人在同一个分支上协作时,很容易出现冲突。即使没有冲突,后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 addnew env | |/ * | 12a631b merged bug fix 101 |\ \ | * | 4c805e2 fix bug 101 |/ / * | e1e9c68 merge with no-ff |\ \ | |/ | * f52c633 add merge |/ * cf810e4 conflict fixed
总之看上去很乱,有强迫症的童鞋会问:为什么Git的提交历史不能是一条干净的直线?
其实是可以做到的!Git有一种称为rebase的操作,有人把它翻译成“变基”。
在和远程分支同步后,我们对hello.py这个文件做了两次提交。用git log命令看看:
$ git log --graph --pretty=oneline --abbrev-commit
* 582d922 (HEAD -> master) add author
* 8875536 add comment
* d1be385 (origin/master) init hello
* e5e69f1 Merge branch 'dev'
|\
| * 57c53ab (origin/dev, dev) fix env conflict
| |\
| | * 7a5e5dd add env
| * | 7bd91f1 add new env
...
注意到Git用(HEAD -> master)和(origin/master)标识出当前分支的HEAD和远程origin的位置分别是582d922 add author和d1be385 init hello,本地分支比远程分支快两个提交。
现在尝试推送本地分支:
$ git push origin master
To github.com:michaelliao/learngit.git
! [rejected] master -> master (fetch first)
error: failed to push some refs to 'git@github.com:michaelliao/learngit.git'
hint: Updates were rejected because the remote contains work that youdo
hint:not have locally. Thisis usually causedby another repository pushing
hint:to the same ref. You may wanttofirst 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 ahead of 'origin/master' by 3 commits.
(use "git push" to publish your local commits)
nothing tocommit, working tree clean
加上刚才合并的提交,现在我们本地分支比远程分支超前3个提交。
用git log看看:
$ git log--graph --pretty=oneline --abbrev-commit
* e0ea545 (HEAD -> master) Merge branch 'master' of github.com:michaelliao/learngit
|\
| * f005ed4 (origin/master)set exit=1
* | 582d922add author
* | 8875536add 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 baseand 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 baseand 3-way merge...
Auto-merging hello.py
输出了一大堆操作,到底是啥效果?再用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 master
Counting objects: 6, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (5/5), done.
Writing objects: 100% (6/6), 576 bytes | 576.00 KiB/s, done.
Total 6 (delta 2), reused 0 (delta 0)
remote: Resolving deltas: 100% (2/2), completed with 1 local object.
To github.com:michaelliao/learngit.git
f005ed4..7e61ed4 master -> master
再用git log看看效果:
$ git log--graph --pretty=oneline --abbrev-commit
* 7e61ed4 (HEAD -> master, origin/master) add author
* 3611cfe add comment
* f005ed4set exit=1
* d1be385 init hello
...
远程分支的提交历史也是一条直线。
发布一个版本时,通常先在版本库中打一个标签(tag),这样,就唯一确定了打标签时刻的版本。将来无论什么时候,取某个标签的版本,就是把那个打标签的时刻的历史版本取出来。所以,标签也是版本库的一个快照。
Git的标签虽然是版本库的快照,但其实它就是指向某个commit的指针(跟分支很像对不对?但是分支可以移动,标签不能移动),所以,创建和删除标签都是瞬间完成的。
Git有commit,为什么还要引入tag?tag就是一个让人容易记住的有意义的名字,它跟某个commit绑在一起。
在Git中打标签非常简单,首先,切换到需要打标签的分支上:
$ git branch
* dev
master
$ git checkout master
Switched to branch 'master'
然后,敲命令git tag 就可以打一个新标签:
$ git tag v1.0
可以用命令git tag查看所有标签:
$ git tag
v1.0
默认标签是打在最新提交的commit上的。有时候,如果忘了打标签,怎么办?
方法是找到历史提交的commit id,然后打上就可以了:
$ git log --pretty=oneline --abbrev-commit 12a631b (HEAD -> master, tag: v1.0, origin/master) merged bug fix 101 4c805e2 fix bug 101 e1e9c68 merge with no-ff f52c633 add merge cf810e4 conflict fixed 5dc6824 & simple 14096d0 AND simple b17d20e branch test d46f35e remove test.txt b84166e add test.txt 519219b git tracks changes e43a48b understand how stage works 1094adb append GPL e475afc add distributed eaadf4e wrote a readme file
比方说要对add merge这次提交打标签,它对应的commit id是f52c633,敲入命令:
$ git tag v0.9 f52c633
再用命令git tag查看标签:
$ git tag
v0.9
v1.0
注意,标签不是按时间顺序列出,而是按字母排序的。可以用git show 查看标签信息:
$ git show v0.9

还可以创建带有说明的标签,用-a指定标签名,-m指定说明文字:
$ git tag -a v0.1 -m "version 0.1 released" 1094adb
用命令git show 可以看到说明文字:
$ git show v0.1
tag v0.1
Tagger: Michael Liao <askxuefeng@gmail.com>
Date: Fri May 18 22:48:43 2018 +0800
version 0.1 released
commit 1094adb7b9b3807259d8cb349e7df1d4d6477073 (tag: v0.1)
Author: Michael Liao <askxuefeng@gmail.com>
Date: Fri May 18 21:06:15 2018 +0800
append GPL
diff --git a/readme.txt b/readme.txt
...
注意:标签总是和某个commit挂钩。如果这个commit既出现在master分支,又出现在dev分支,那么在这两个分支上都可以看到这个标签。
如果标签打错了,也可以删除:
$ git tag -d v0.9
Deleted tag 'v0.9' (was f15b0dd)
因为创建的标签都只存储在本地,不会自动推送到远程。所以,打错的标签可以在本地安全删除。
如果要推送某个标签到远程,使用命令git push origin :
$ git push origin v1.0
Total 0 (delta 0), reused 0 (delta 0)
To github.com:michaelliao/learngit.git
* [new tag] v1.0 -> v1.0
或者,一次性推送全部尚未推送到远程的本地标签:
$ git push origin --tags
Total 0 (delta 0), reused 0 (delta 0)
To github.com:michaelliao/learngit.git
* [new tag] v0.9 -> v0.9
如果标签已经推送到远程,要删除远程标签就麻烦一点,先从本地删除:
$ git tag -d v0.9
Deleted tag 'v0.9' (was f52c633)
然后,从远程删除。删除命令也是push,但是格式如下:
$ git push origin :refs/tags/v0.9
To github.com:michaelliao/learngit.git
- [deleted] v0.9
要看看是否真的从远程库删除了标签,可以登陆GitHub查看。 
安装Git时,我们已经配置了user.name和user.email,实际上,Git还有很多可配置项。
比如,让Git显示颜色,会让命令输出看起来更醒目:
$ git config --global color.uitrue
有些时候,你必须把某些文件放到Git工作目录中,但又不能提交它们,比如保存了数据库密码的配置文件啦,等等,每次git status都会显示Untracked files …
好在Git考虑到了大家的感受,这个问题解决起来也很简单,在Git工作区的根目录下创建一个特殊的.gitignore文件,然后把要忽略的文件名填进去,Git就会自动忽略这些文件。
不需要从头写.gitignore文件,GitHub已经为我们准备了各种配置文件,只需要组合一下就可以使用了。所有配置文件可以直接在线浏览:https://github.com/github/gitignore
忽略文件的原则是:
.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。
有没有经常敲错命令?比如git status?status这个单词真心不好记。
如果敲git st就表示git status那就简单多了。只需要敲一行命令,告诉Git,以后st就表示status:
$ git config --globalalias.st status
当然还有别的命令可以简写,很多人都用co表示checkout,ci表示commit,br表示branch:
$ git config --globalalias.co checkout
$ git config --globalalias.ci commit
$ git config --globalalias.br branch
以后提交就可以简写成:
$ git ci -m "bala bala bala..."
-global参数是全局参数,也就是这些命令在这台电脑的所有Git仓库下都有用。
配置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
配置别名也可以直接修改这个文件,如果改错了,可以删掉文件重新通过命令配置。
搭建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来管理公钥
有很多不但视源代码如生命,而且视员工为窃贼的公司,会在版本控制系统里设置一套完善的权限控制,每个人是否有读写权限会精确到每个分支甚至每个目录下。因为Git是为Linux源代码托管而开发的,所以Git也继承了开源社区的精神,不支持权限控制。不过,因为Git支持钩子(hook),所以,可以在服务器端编写一系列脚本来控制提交等操作,达到权限控制的目的。Gitolite就是这个工具。
在github上创建了一个公开的代码仓:https://github.com/tinygyro/git_rampup.git
在文件夹的目录下点击鼠标右键,再选择 git bash 即可进入:
克隆Remote代码的命令:
$ git clone https://github.com/tinygyro/git_rampup.git
完成代码克隆后,我们进入到项目文件夹,发现和远程仓库一样的结构:
在第一次commit之前,我们要来配置git用户的姓名和邮箱。在项目的根目录下运行:
$ git config --global user.name "Tiny Gyro"
$ git config --global user.email "tinygyro@github.com"
我们运行命令 git status 查看暂存区状态:所在分支和工作区的状态。
$ git status

创建一个新文件 happy_summer.txt
改动只发生在工作区 
运行 git status ,显示新添加的happy_summer.txt 是新文件不受git的追踪
运行命令 git add 添加到暂存区
git add happy_summer.txt
再次运行 git status 
我们的更改已经推进到了暂存区
代码修改提交到本地仓库:
$ git commit -m "first commit"

此时更改已经推进到本地仓库区 
** 本地仓库推送到远程仓库**
$ git push

至此,我们便完成了本地到云端的全部修改:
远程仓库更新:
命令 说明 git diff 比较文件的不同,即暂存区和工作区的差异 git commit -m "注解" 提交暂存区到本地仓库 git reset --hard [版本号] 回退到此版本号,也可以回退到未来 git checkout -- [filename] 回退到上次的修改 git reflog 显示从开始到当前提前的所有文件 git log 查看git提交历史记录 git log --oneline 可以查看简约版历史提交记录 git mv 旧文件名 新文件名 来将工作区和暂存区文件重命名 git rm --cached [filename] 删除远程上的文件,执行完命令之后要commit与push 分支: 列出本地分支: git branch 列出远程分支: git branch -r 新建一个分支,但仍停留在当前分支: git branch [branch-name] 分支之间的切换: git checkout [branch-name] 新建一个分支,并切换到该分支: git checkout -b [branch] 合并指定分支到当前分支 : git merge [branch-name] 把dev分支提交到origin中: git push origin dev 在branch分支下创建新的分支child_branch: git branch [child_branch] [branch] 拉取分支branch_name: git pull origin [branch-name] 删除本地分支: git branch -d [branch-name] 可以删除远程分支: git push origin --delete [branch-name]
# 第一次初始化(方式1):
git init
git add .
git commit -m 'first commit'
git remote add origin git@github.com:帐号名/仓库名.git
git pull origin master
git push origin master # -f 强推
# 第一次初始化(方式2): git clone git@github.com:git帐号名/仓库名.git # 平时工作基本操作: git checkout master # 切到主分支 git fetch origin # 获取最新变更 git checkout -b dev origin/master # 基于主分支创建dev分支 git add . # 添加到缓存 git commit -m 'xxx' # 提交到本地仓库 git fetch origin # 获取最新变更 git rebase dev origin/master # 合并到主分支 git push origin dev # 推送到远程分支 git chekout master # 切到主分支 git merge dev # 合并开发分支 git clone -b 远程分支 仓库地址 # 本地不存在仓库 拉取远程分支代码 git checkout -b 远程分支 origin/远程分支 # 本地存在仓库,拉取远程分支
git相关配置
git config --global user.name “用户名” # 设置用户名
git config --global user.email “用户邮箱” #设置邮箱
git config --global user.name # 查看用户名是否配置成功
git config --global user.email # 查看邮箱是否配置
初始化仓库
创建全新的仓库
查看仓库当前状态
将文件添加到仓库
git add 文件名 # 将工作区的某个文件添加到暂存区
git add . # 将当前工作区的所有文件都加入暂存区
git add -u # 添加所有被tracked文件中被修改或删除的文件信息到暂存区,不处理untracked的文件
git add -A # 添加所有被tracked文件中被修改或删除的文件信息到暂存区,包括untracked的文件
git add -i # 进入交互界面模式,按需添加文件到缓存区
将暂存区文件提交到本地仓库
**git commit -m “提交说明” **# 将暂存区内容提交到本地仓库
git commit -a -m “提交说明” # 跳过缓存区操作,直接把工作区内容提交到本地仓库
比较文件异同
git diff # 工作区与暂存区的差异
git diff 分支名 #工作区与某分支的差异,远程分支这样写:remotes/origin/分支名
git diff HEAD # 工作区与HEAD指针指向的内容差异
git diff 提交id 文件路径 # 工作区某文件当前版本与历史版本的差异
**git diff --stage **# 工作区文件与上次提交的差异(1.6 版本前用 --cached)
**git diff 版本TAG **# 查看从某个版本后都改动内容
git diff 分支A 分支B # 比较从分支A和分支B的差异(也支持比较两个TAG)
git diff 分支A…分支B # 比较两分支在分开后各自的改动
查看历史记录
git log # 查看所有commit记录(SHA-A校验和,作者名称,邮箱,提交时间,提交说明)
git log -p -次数 # 查看最近多少次的提交记录
git log --stat # 简略显示每次提交的内容更改
**git log --name-only **# 仅显示已修改的文件清单
git log --name-status # 显示新增,修改,删除的文件清单
git log --oneline # 让提交记录以精简的一行输出
git log –graph –all --online # 图形展示分支的合并历史
**git log --author=作者 ** # 查询作者的提交记录(和grep同时使用要加一个–all–match参数)
git log --grep=过滤信息 # 列出提交信息中包含过滤信息的提交记录
**git log -S查询内容 **# 和–grep类似,S和查询内容间没有空格
git log fileName # 查看某文件的修改记录,找背锅专用
代码回滚,撤销变更
git reset 通过把分支记录回退几个提交记录来实现撤销改动。你可以将这想象成“改写历史”。git reset 向上移动分支,原来指向的提交记录就跟从来没有提交过一样。
git reset HEAD^ # 恢复成上次提交的版本
git reset HEAD^^ # 恢复成上上次提交的版本,就是多个^,以此类推或用~次数
git reflog git reset --hard 版本号
–soft:只是改变HEAD指针指向,缓存区和工作区不变;
–mixed:修改HEAD指针指向,暂存区内容丢失,工作区不变;
–hard:修改HEAD指针指向,暂存区内容丢失,工作区恢复以前状态;
主要有两种方法用来撤销变更 —— 一是 git reset,还有就是 git revert
git reset HEAD~1

Git 把 main 分支移回到 C1;现在我们的本地代码库根本就不知道有 C2 这个提交了
为了撤销更改并分享给别人,我们需要使用 git Revert。
git revert HEAD

奇怪!在我们要撤销的提交记录后面居然多了一个新提交!这是因为新提交记录C2’引入了更改 —— 这些更改刚好是用来撤销C2这个提交的。也就是说C2’的状态与C1是相同的。revert 之后就可以把你的更改推送到远程仓库与别人分享啦。
同步远程仓库
git rm 文件名 删除版本库文件
注意:在 Git 2.23 版本中,引入了一个名为 git switch 的新命令,最终会取代 git checkout,因为 checkout 作为单个命令有点超载(它承载了很多独立的功能)。
切换分支
git checkout – test.txt #版本库里的版本替换工作区的版本
git checkout -b dev //-b表示创建并切换分支
上面一条命令相当于一面的二条:
git branch dev //创建分支
git checkout dev //切换分支
git add .
git commit -m “dev0903”
git push -u origin dev //提交到分支上
查看远程库信息
git remote add origin git@github.com:帐号名/仓库名.git #本地仓库内容推送到远程仓库
git clone git@github.com:git帐号名/仓库名.git # 从远程仓库克隆项目到本地
创建分支,查看分支
git branch newImage //创建分支
git branch -d dev //删除分支
早建分支!多用分支!
案例:
**newImage** 的分支git branch newImage
创建分支就是这么容易!新创建的分支 newImage 指向的是提交记录 C1。
为什么 main 分支前进了,但 newImage 分支还待在原地呢?!这是因为我们没有“在”这个新分支上,看到 main 分支上的那个星号(*)了吗?这表示当前所在的分支是 main。
git checkout newImage
git commit
让我们在提交修改之前先切换到新的分支上
有个更简洁的方式:如果你想创建一个新的分支同时切换到新创建的分支的话,可以通过 **git checkout -b <your-branch-name>** 来实现。
git checkout -b newImage
合并分支
git merge dev #用于合并指定分支到当前分支
git merge --no-ff -m “merge with no-ff” dev
#加上–no-ff参数就可以用普通模式合并,合并后的历史有分支,能看出来曾经做过合并
**分支与合并:**就是说我们新建一个分支,在其上开发某个新功能,开发完成后再合并回主线。
案例:
准备了两个分支,每个分支上各有一个独有的提交。这意味着没有一个分支包含了我们修改的所有内容。咱们通过合并这两个分支来解决这个问题。
1. 我们要把 **bugFix** 合并到 **main** 里
git merge bugFix
首先,**main**** 现在指向了一个拥有两个父节点的提交记录。**假如从 main 开始沿着箭头向上看,在到达起点的路上会经过所有的提交记录。这意味着 main 包含了对代码库的所有修改。↓↓↓
2. 再把 **main** 分支合并到 **bugFix**:
因为 main 继承自 bugFix,Git 什么都不用做,只是简单地把 bugFix 移动到 main 所指向的那个提交记录。表明每一个分支都包含了代码库的所有修改
git checkout bugFix
git merge main

对当前分支的记录做一个拷贝,然后连接在主之路的记录的后面。
第二种合并分支的方法是 git rebase。
Rebase 的优势就是可以创造更线性的提交历史,这听上去有些难以理解。如果只允许使用 Rebase 的话,代码库的提交历史将会变得异常清晰。还是准备了两个分支;注意当前所在的分支是 bugFix(星号标识的是当前分支)
咱们这次用 git rebase 实现此目标
git rebase main
现在 bugFix 分支上的工作在 main 的最顶端,同时我们也得到了一个更线性的提交序列。
注意,提交记录 C3 依然存在(树上那个半透明的节点),而 C3’ 是我们 Rebase 到 main 分支上的 C3 的副本。
main 上。把它 rebase 到 bugFix 分支上……由于 bugFix 继承自 main,所以 Git 只是简单的把 main 分支的引用向前移动了一下而已。
在提交树上移动
在接触 Git 更高级功能之前,我们有必要先学习在你项目的提交树上前后移动的几种方法。

git checkout c1
git checkout main
git commit
git checkout c2
HEAD 指向了 main,随着提交向前移动
实际这些命令并不是真的在查看 HEAD 指向,看下一屏就了解了。如果想看 HEAD 指向,可以通过 cat .git/HEAD 查看
分离的 HEAD
分离的 HEAD 就是让其指向了某个具体的提交记录而不是分支名。在命令执行之前的状态
如下所示:
HEAD -> main -> C1
HEAD 指向 main, main 指向 C1
git checkout c1 //HEAD-》C1

目标:
git checkout c4
**git log** 来查查看提交记录的哈希值。fed2da64c0efc5293610bdd892f82a58e8cbc5d8。舌头都快打结了吧…fed2 而不是上面的一长串字符。使用相对引用的话,你就可以从一个易于记忆的地方(比如 bugFix 分支或 HEAD)开始计算。
相对引用非常给力,这里我介绍两个简单的用法
**^** 向上移动 1 个提交记录**~<num>** 向上移动多个提交记录,如 ****~3**1. 首先看看操作符 (^)。把这个符号加在引用名称的后面,表示让 Git 寻找指定提交记录的父提交。
所以 main^ 相当于“main 的父节点”。 main^^ 是 main 的第二个父节点
现在咱们切换到 main 的父节点
git checkout main^

你也可以将 HEAD 作为相对引用的参照。下面咱们就用 HEAD 在提交树中向上移动几次。
git checkout c3
git checkout HEAD^
git checkout HEAD^
git checkout HEAD^


目标
git checkout bugFix^
如果你想在提交树中向上移动很多步的话,敲那么多 ^ 貌似也挺烦人的,Git 当然也考虑到了这一点,于是又引入了操作符 ~。
**^** 相同,向上移动一次),指定向上移动多少次。用 ~<num> 一次后退四步。
git checkout HEAD~4

强制修改分支位置
我使用相对引用最多的就是移动分支。可以直接使用 **-f** 选项让分支指向另一个提交。例如:
git branch -f main HEAD~3
上面的命令会将 main 分支强制指向 HEAD 的第 3 级父提交。
相对引用为我们提供了一种简洁的引用提交记录 C1 的方式, 而 -f 则容许我们将分支强制移动到那个位置。
拉取远程分支到本地仓库
git checkout -b 本地分支 远程分支 # 会在本地新建分支,并自动切换到该分支
git fetch origin 远程分支:本地分支 # 会在本地新建分支,但不会自动切换,还需checkout
git branch --set-upstream 本地分支 远程分支 # 建立本地分支与远程分支的链接
git init #初始化
git add #将文件放入暂存区
git commit -m "name" #从暂存区放到本地仓库
git push
#一般是不敢直接并到主分支上的,所有基本是有属于自己的分支
git clone // git pull #将远程仓库的文件拉下来
git branch -b branch1 #创建分支并打开
git add file
git commit -m "b1"
git push -u origin branch1 #提交到分支b1上
git checkout b2 #切换分支
git pull --rebase

区别:
1、rebase把当前的commit放到公共分支的最后面,merge把当前的commit和公共分支合并在一起;
2、用merge命令解决完冲突后会产生一个commit,而用rebase命令解决完冲突后不会产生额外的commit。
git是一个常用的分布式版本管理工具(DVCS)。它可以跟踪文件的更改,并允许你恢复到任何特定版本的更改。
一个主要优点是它不依赖于中央服务器来存储项目文件的所有版本。
所以git pull = git fetch + git merge
**push之前一定要进行本地更新操作。**使用git pull命令或者使用git fetch和git merge的命令组合。这时候,可能会出现版本冲突,如果出现的话,需要解决完冲突再进行代码push。
版本冲突多出现在合并操作(合并远程仓库代码或者合并分支代码)中。如果出现版本冲突,需要具体分析出现冲突的代码区,手动进行代码合并,然后再进行提交。
用于写入提交的命令是 git commit -a。
现在解释一下 -a 标志, 通过在命令行上加 -a 指示 git 提交已修改的所有被跟踪文件的新内容。还要提一下,如果你是第一次需要提交新文件,可以在在 git commit -a 之前先 git add 。
要创建存储库,先为项目创建一个目录(如果该目录不存在),然后运行命令 git init。通过运行此命令,将在项目的目录中创建 .git 目录。
下期将会讲解Gerrit的使用,,
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。