以前在学校的时候用git基本上就那几个命令,一般是用于push到github上做备份。比较容易,最近涉及到了git的一些别的用途,多人合作,创建分支等,发现自己有些命令还是不熟悉,需要老是查文档,所以重头开始学习,并做记录方便以后查阅,下面是我的整理。
Git的优点
集中式 vs 分布式
集中式版本控制系统,版本库是集中存放在中央服务器,必须联网工作。因此如果是局域网带宽大,尚可。如果是在互联网上,可能会导致花费时间长。
分布式版本控制系统,每个人电脑里都有完整的版本库, 安全性高,一个人电脑坏了,可以复制别的人的即可。不需要联网工作。a和b改了相同的文件,互相推送到对方的电脑,就可以看到修改的内容。
在实际使用分布式版本控制系统的时候,其实很少在两人之间的电脑上推送版本库的修改,因为可能你们俩不在一个局域网内,两台电脑互相访问不了,也可能今天你的同事病了,他的电脑压根没有开机。因此,分布式版本控制系统通常也有一台充当“中央服务器”的电脑,但这个服务器的作用仅仅是用来方便“交换”大家的修改,没有它大家也一样干活,只是交换修改不方便而已。 –from 廖雪峰的git网站
那这个时候有人会问,包括我自己最开始也觉得奇怪:
分布式的版本控系统如果要在多个人之间协作不也是需要一个像github一样的的远程版本库吗,这与集中式的有什么区别呢?
区别在于,Git不仅包含代码库还包含了历史库,你每一次clone代码都是一个完整的和中央仓库一模一样的库。意味着你本地与中央仓库一样都有所有的提交修改记录。而SVN的只有代码库是在本地,其历史库实在中央仓库,因此每次当我们提交和比对代码的时候都要连接中央仓库才可以。这也是我们为什么说Git不需要联网的一个原因。
所以主要差别就在于历史版本维护的位置。一句话总结就是:分布式版本控制的每个节点都是完整仓库。这里的每个节点就是我们的终端的意思。
Git 原理-工作区和暂存区
有过一点点认识的童鞋都知道一些基本操作,比如 git add , git commit
为什么我们不一次性进行 commit 操作,反而要执行这么多步呢? 所以我们一定要了解 Git 工作原理,才能够理解下面的命令的一些意思。
上面这张图中,我们可以认识到:
工作区可以当成你写代码的地方。
在你通过 git init
命令之后,会在你的文件夹下生成一个 .git 文件,这个我们称之为 git 的版本库。 它不算做工作区,在这个版本库里又包括两个部分,第一个是暂存区 Stage ,第二个是 Git 为我们自动创建的第一个分支 master ,以及指向 master 的一个指针叫 HEAD 。我们一般不要去动这个 .git 文件,否则可能破坏我们的版本库。
那么当我们通过 git add 的时候,其实是把工作区的更改,提交到了暂存区 stage, 然后我们再通过 git commit 提交到 master 主分支。
通过下面一张图,我们可以看的更清楚。
仓库/版本库-Repository
创建版本库
如何创建版本库?
//初始化一个git仓库
git init
//添加文件到git仓库,可多次使用
git add <file1>
git add <file2>
git commit -m "说明"
//掌握仓库当前的状态
git status
//查看修改内容
git diff <file1>
//提交日志
git log (--pretty=oneline)
-m 后面输入的是本次提交的说明,最好有意义。通过 commit 我们可以一次提交很多文件,所以可以 add 多次。
首先这里再明确一下,所有的版本控制系统,其实只能跟踪文本文件的改动,比如TXT文件,网页,所有的程序代码等等,Git也不例外。版本控制系统可以告诉你每次的改动,比如在第5行加了一个单词“Linux”,在第8行删了一个单词“Windows”。而图片、视频这些二进制文件,虽然也能由版本控制系统管理,但没法跟踪文件的变化,只能把二进制文件每次改动串起来,也就是只知道图片从100KB改成了120KB,但到底改了啥,版本控制系统不知道,也没法知道。 – from 廖雪峰的git网站
版本回退
上面提到了可以使用 git log
来查看提交日志,我们可以看到
上面的 commit 后面有一串用 sha1 计算出来的串,就是 commit_id 了。通过这 样一个个 commit_id 就可以看出我们提交历史的一条线了。那如果我们要回退版本怎么办呢?
首先 Git 肯定要知道你要回退到哪个版本,很明显,我们不可能再去根据写 commit_id 来判断,因为这个很长的一串让我们重复是很费脑子的事情。
在 Git 中,我们使用 HEAD 来表示当前的版本, 也就是上面图片里看到的 13d436c4a928e3db2bfd08e15c9a8ce06780da91
这个。 那么上一个版本我们可以使用 HEAD^ , 上上一个用 HEAD^^, 第一百个就是 HEAD~100 。
具体的回退的命令,我们使用 git reset
。
1 | git reset --hard HEAD^ |
但是此时我们再往前看, 刚刚第一个 HEAD 版本已经不在了, 这可咋办?
如果我们没有关闭刚刚 git log
的窗口, 是可以的。
//版本号不用写全,git会自动去找
git reset --hard 13d436c4a...
那如果我关闭了呢?
Git 提供了一个命令记录你的每一次命令.
git reflog
git reset --hard commit_id
这下我们再在 git reflog
里面查看,就可以看到第一个版本的 commit_id了,然后我们再用 reset 命令即可。
这里我们看到一个现象, 我们使用 git reset 做版本回退的时候,速度非常快,这是为什么呢? 因为 Git 在内部有个指向当前版本的HEAD指针,当你回退版本的时候, Git 仅仅是把 HEAD 从指向 了新的 commit_id 并将工作区的文件更新。
撤销修改
可能有下面几种情形:
- 修改了工作区的内容,但是并没有使用 git add 添加到暂存区, 可以使用
git checkout -- file
。这样就直接丢弃工作区的修改。这种情况使用git status
会看到下面的提示(not staged)的:
1 | # Changes not staged for commit: |
不但修改了工作区的文件,而且还使用 git add 添加到了暂存区,如果想要丢弃更改两步走。第一步用命令git reset HEAD file,就回到了第一种情况,第二步按第一种情况来操作。
git reset HEAD file
git checkout –file
这种情况使用 git status
会看到下面的提示(to be committed)的:
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
- 如果已经
git commit
了,也就是已经提交到了 master 分支上,那么应该庆幸还没有 push 到远程,这时候我们利用我们上面一个小节提到的撤销版本即可。 如果已经 push 了的话,就没办法喽 。。
1 | git reset --hard HEAD^ |
删除文件
如果我们执行下面的操作:
git add test.txt
git commit -m "add test.txt"
rm test.txt
意思比较明白,就是我们将 test.txt 添加到了 master 分支,但是我们后来不想要这个文件,就删除了。于是你利用 git status
就会发现如下的信息:
# Changes not staged for commit:
# (use "git add/rm <file>..." to update what will be committed)
# (use "git checkout -- <file>..." to discard changes in working directory)
#
# deleted: test.txt
#
告诉我们已经删除了 test.txt ,要做修改了。我们可以用下面的命令来改变我们的更改:
git add test.txt
git rm test.txt
git commit -m "delete test.txt"
那如果我们执行了 rm test.txt
之后,发现我们实际还是想要这个文件的怎么办呢? 没关系,因为我们版本库里面确实是有这个文件的啊。所以我们利用:
git checkout -- test.txt
即可恢复到我们最新版本了。git checkout
非常好用,注意一定要加 --
因为如果不加就是切换分支了。它其实就是利用版本库里面的版本替换工作区的版本,这样不管是添加,修改,删除都可以瞬间恢复。
Git 远程操作
关联并向远程推送分支
通过以下命令,我们可以关联本地库和远程库并且推送分支。
//关联库
git remote add origin git@server-name:path/repo-name.git
//将本地分支 master 推送到远程分支
git push -u origin master
注意,这里的 orgin 代表的就是远程仓库的名字, Git 默认这么做,如果改成别的当然也是可以的啦。
然后就是这里的 -u 是什么意思呢?
由于远程库是空的,我们第一次推送master分支时,加上了-u参数,Git不但会把本地的master分支内容推送的远程新的master分支,还会把本地的master分支和远程的master分支关联起来,在以后的推送或者拉取时就可以简化命令。–from 廖雪峰
好,那我们以后如果再推送还要做写什么呢?
git push origin master
以后每次推送就用这个就可以了。就不需要做 remote add
了。
这里要强调一下,有很多人都是看着这么用,就这么用了,但是不知道 origin master 分别代表的什么意思,就导致以后比如我要把本地的 develope 分支和远程的 master 分支推送怎么写呢?所以强调如下:
1 | git push <远程主机名> <本地分支名>:<远程分支名> |
如果省略远程分支名,则表示将本地分支推送与之存在”追踪关系”的远程分支(通常两者同名),如果该远程分支不存在,则会被新建。如果不同名字,我们可以用下面的命令使之关联
1 | git branch --set-upstream master origin/next |
上面命令指定 master
分支追踪 origin/next
分支。
1 | git push origin master |
上面的简写命令就表示的是:将本地的master分支推送到origin主机的master分支。如果后者不存在,则会被新建。
千万要注意的是,上面的命令是省略了远程分支名,这是因为一般我们可以认为远程分支和本地分支名相同的原因。那如果我们省略了本地分支名呢?
//删除 origin 主机上的 master 分支
git push origin :master
# 等同于
git push origin --delete master
如果省略本地分支名,则表示删除指定的远程分支,因为这等同于推送一个空的本地分支到远程分支。是不是很可怕。。所以一定要注意了。你还可能见过下面三种用法:
//将当前分支推送到origin主机的对应分支
git push origin
git push
git push -u origin master
git push --all origin
git push --force origin
git push origin
如果当前分支与远程分支之间存在追踪关系,则本地分支和远程分支都可以省略。
git push
如果当前分支只有一个追踪分支,那么主机名都可以省略。
git push -u origin master
如果当前分支与多个主机存在追踪关系,则可以使用-u选项指定一个默认主机,这样后面就可以不加任何参数使用git push。(此命令表示将master分支推送到origin主机的master分支,同时指定了 orgin 为默认分支,以后可以直接使用 git push
了。)
git push --all origin
表示将所有本地分支都推送到origin主机。如果你远程主机上的版本比本地的要新,你可以使用 git push --force origin
这样导致远程主机上更新的版本被覆盖。所以要慎用。
git clone/pull/fetch/remote
刚刚讲了最重要的 git push
我们下面来看看稍微简单一点的 其他命令。
git clone
1 | git clone <版本库的网址> |
后面的网址有很多情况,比如以 http, https, ssh, ftp等等。其中 Git 协议的下载速度会稍微快一些,然后 SSH就是要认证了。
git clone https://github.com/Christine95/Blog.git
git clone git@github.com:Christine95/Blog.git
git clone git@github.com:Christine95/Blog.git myBlog
git clone -o Blog https://github.com/Christine95/Blog.git
如果不指定后面的本地目录名(myBlog)的话,就是与远程库相同的名字。
如果不指定 -o 选项,我们通过 git remote -v
查询时,就是名字默认为 origin, 指定就是刚刚的 Blog 啦。
git remote
//只有名字
git remote
//有名字有路径
git remote -v
//远程主机详情
git remote show <主机名>
//添加远程主机
git remote add <主机名> <网址>
//删除远程主机
git remote rm <主机名>
//重命名
git remote rename <原主机名> <新主机名>
我感觉这个就没啥说的了。经常用就好了。
git fetch
当我们远程主机有了更新后,可以使用 git fetch
取回这些更新至本地。
1 | git fetch |
git fetch
表示将某个远程主机的更新,全部取回本地。git fetch origin develope
表示只更新远程主机的 develope 分支。
注意:取回了更新后,必须用”远程主机名/分支名”的形式读取。如我们上面的例子,就必须用 origin/develope
读取。比如:
git branch -r
//会得到类似:
origin/HEAD -> origin/master
origin/develope
origin/html
origin/master
origin/test_d002
git branch -a
//会得到类似:
remotes/origin/HEAD -> origin/master
remotes/origin/develope
remotes/origin/html
remotes/origin/master
remotes/origin/test_d002
git branch
命令的 -r 选项,可以用来查看远程分支,-a 选项查看所有分支。
这里注意: git pull
= git fetch
+ git merge
git pull
1 | git pull <远程主机名> <远程分支名>:<本地分支名> |
注意不要和 git push
的<本地>:<远程> 弄混淆了。有例子如下:
//将orgin主机上的next分支取回并和本地master分支合并
git pull origin next:master
//将origin主机的next分支取回并和当前分支合并
git pull origin next
//上面一句命令等于下面两条命令
git fetch origin
git merge origin/next
有一个地方需要注意:
当我们使用某些命令的时候,Git会自动在本地分支和远程分支之间,建立一种追踪关系,也就是说关联起来啦。比如你使用 git clone 的时候,会默认将远程主机和本地分支建立相同的名字。比如你克隆的是远程的 develope 分支,那么你本地克隆下来也会是 develope 分支,然后自动追踪 origin/develope分支。
但是,比如说有这样一种情况,我就遇到过,同事让我看一个项目,我很自然的去 clone master 分支了。这个时候发现 master 分支并不对,我应该去 clone develope 分支的。因为同事有些代码在 develope 分支上,不再 master 分支上。这个时候,我当然可以去利用 git pull origin develope master
,但是为了方便,我最好这么做:
1 | git branch --set-upstream master origin/develope |
这样就把本地的 master 分支和远程的 develope 分支关联啦。这样我们就可以用:1
git pull origin
来 pull 代码啦。这样连主机名都省略了。只用告诉从那个远程主机 pull 就可以。
总结
本来想这一篇文章能总结完的,结果连分支管理都没讲完。还是决定下一篇文章再将啦。要不然篇幅太长,就不适合阅读了。感谢下面两篇文章,让我清晰了很多,也准备小额资助感谢下廖雪峰老师的网站啦,毕竟是花心血写这么好的教程:
最后贴一张阮一峰老师这个文字里的一幅图,我觉得太棒了。