以前一直没有系统的学习一遍git,导致每次使用都会有各种奇怪的问题。这次一定要把git学明白了。
学习资料主要参考廖雪峰的git教程,git官方文档
其中git官方文档有手册也有书,非常适合查看
git是一个分布式的版本控制系统,每个人的机器都可以当做一个代码仓库。git保存的是文件的修改,而不是每次修改后的文件,所以非常适合回溯到之前的版本。git只能管理文本文件的修改,像视频,图片,word文档之类的都只能显示出二进制编码的改变,不能显示内容的修改。
创建git仓库
首先需要在通过git init
命令将一个目录变成git可以管理的仓库。
之后会出现一个.git
的目录,这是跟踪管理版本库的目录,可以看一下里面有啥
这里面的内容还没有仔细研究过,之后会仔细的研究一下,本篇仅介绍使用和理解。
代码的提交
在执行过git init
之后的目录称为工作区,也就是存放你代码的地方。
git的提交分为两阶段:
//第一阶段
git add [file]
会将你指定的文件(可以是单个文件,也可以是.,整个目录)存到暂存区stage。
//第二阶段
git commit -m "first commit"
-m
参数后面要加本次commit的相关信息
会将暂存区里面的修改提交到版本库。这样在你本地的版本库里就可以存多个版本的代码的修改。
工作区和暂存区
借用廖雪峰的教程中的图
图中有git自动为我们创建的第一个分支
master
,以及一个指向master
的指针HEAD
。
我们修改文件是在工作区操作,git add
之后修改会被放到暂存区,git commit
之后修改会被提交到当前分支,并且清空暂存区,HEAD
会指向当前分支的最新提交。
查看工作区和暂存区的状态
有一个命令可以查看当前本地代码仓库的情况:
git status
会显示你当前是否有修改,修改是否放进暂存区,是否已经提交到分支上。非常重要并且好用的命令!
远程仓库
Github是专门为git设计的远程代码仓库,上面都是开源代码。也可以把自己的代码保存到Github上。
首先需要将你本地用户的ssh公钥传到Github上,因为本地仓库连接远程仓库主要通过的是ssh协议。
之后我们只需要在本地的代码仓库(即存代码的目录)下运行命令指定远程仓库即可:
git remote add origin [email protected]:MiracleMa/[].git
这样就把本地仓库和远程仓库联系起来了,远程仓库名字默认为origin,也可以自己指定,可以和多个远程仓库连接。
可以通过相关命令查看远程库的信息:
git remote //查看远程库的名字
git remote -v //查看远程库的详细信息
git remote show name //查看指定name的远程库的详细信息
如果你需要修改远程仓库地址,也是使用git remote
相关的命令进行修改,详情可以查看git remote -h
提交代码到远程仓库
如何把本地的代码提交到远程仓库呢?首先本地工作区的修改需要被commit到版本库,然后通过以下命令推送到远程仓库:
git push [-u] origin master
origin
表示远程仓库名称
master
表示本地仓库分支,完整表示应该是<本地分支>:<远程分支>
,省略:<远程分支>
意味着push
到和本地分支一样名字的分支
-u
参数表示会将本次的远程仓库和分支当做默认的,之后执行git push
就默认推送到这个仓库和分支(不太建议使用)
克隆远程仓库的代码
git clone [email protected]:MiracleMa/[].git
就可以通过ssh
协议把远程的仓库clone
到本地,也可以使用https
协议,用参数-b
或者-t
也可以指定相关的分支或者标签,可以查看git clone -h
。
clone
过程中,git自动的把远程的master
分支和本地的master
分支对应起来,并且远程仓库默认为origin
。(如果clone
的是xxx
分支,就会自动的在本地创建xxx
分支,然后对应起来)
版本回退
每次commit
都有一个commit_id
,是一个十六进制编号,因为这是分布式的版本管理系统,为了防止不同用户的commit_id
发生冲突,所以需要用哈希方法计算。
可以通过下面这个命令来查看曾经的修改:
mashaonan@mashaonan:~/Desktop/gti$ git log
commit 4114e6eec347fb4dddecd007150932ffa1423fb3
Author: MiracleMa
Date: Sat Apr 14 14:08:43 2018 +0800
a
commit 40a198e833a36598257ea0ae672e57c1006cf406
Author: MiracleMa
Date: Sat Apr 14 14:07:29 2018 +0800
merge
很长的数字串就是commit_id
,还包括commit
时的相关信息。
git log
可以用相关参数来更好的表示commit
的情况,比如--pretty=oneline
输出到一行,--graph
输出图状commit
,用于观察分之合并。
通过以下命令可以回退到某个以前提交的版本:
git reset --hard [commit_id]
只需要输入commit_id
的前几位,git就会自动去寻找,也看用HEAD^
代替commit_id
,HEAD
表示当前版本,HEAD^
就表示前一个版本,每多一个^
就表示往前一个,也可以写成HEAD~100
。
通过commit_id
可以版本回退,也可以恢复之后的版本(如果有的话)。
查看所有执行过的命令
还有个命令可以查看所有执行过的命令:
mashaonan@mashaonan:~/Desktop/gti$ git reflog
4114e6e HEAD@{0}: commit: a
40a198e HEAD@{1}: commit: merge
2451d7e HEAD@{2}: checkout: moving from master to dev
08a6450 HEAD@{3}: checkout: moving from dev to master
2451d7e HEAD@{4}: commit: c.txt
08a6450 HEAD@{5}: checkout: moving from master to dev
08a6450 HEAD@{6}: checkout: moving from dev to master
也可以查到每次操作的id
值。
撤销修改
git还可以通过命令来撤销对工作区以及暂存区的修改。
git checkout -- file
命令可以取消工作区中file文件的操作。
git reset HEAD file
可以取消暂存区中的修改,回退到工作区,还需要执行上一句命令才可以取消这个修改。
如果已经commit
到了本地分支,就要使用上面的版本回退了。
分支管理
HEAD
主要是指向当前分支,我们可以创建其他的分支,新创建的分支会指向当前分支的最新节点。
创建分支
主要使用的命令是:
//创建并且切换到dev分支
git checkout -b dev
//创建dev分支
git branch dev
//切换到dev分支
git checkout dev
第一条命令是下面两条命令结合起来的效果
合并与删除分支
所以我们一般都在dev
分支上做开发,然后提交之后,切换到master
分支,然后git merge dev
去合并dev
分支,然后git branch -d dev
删除dev
分支,如果dev
分支没有被merge
,删除的时候会出现错误提示,需要git branch -D dev
才能删除。
合并分支的时候会出现冲突,也就是,两个分支都在同一个文件上提交了修改,但是修改不同,然后合并的时候就无法增量的合并,也就是无法Fast-Forward
合并,这是只增加,不修改原来的数据的合并方式,所以就会出现冲突。
出现冲突之后,需要手动修改相关文件,然后commit
到当前分支。
错误情况如下:
mashaonan@mashaonan:~/Desktop/gti$ git merge dev
Auto-merging d.txt
CONFLICT (add/add): Merge conflict in d.txt
Removing 6.828
Automatic merge failed; fix conflicts and then commit the result.
相关文件中的输出结果为:
mashaonan@mashaonan:~/Desktop/gti$ cat d.txt
<<<<<<< HEAD
ddd
=======
jkbfdf df
dff
>>>>>>> dev
解决冲突后再提交后的图如下:
并且可以通过git log
的命令来查看分支合并情况:
mashaonan@mashaonan:~/Desktop/gti$ git log --graph --pretty=oneline --abbrev-commit
* 1c92543 a
|\
| * 463966f conflicted
| * 8328f90 add
| * 9b62157 rm
| * 4114e6e a
| * 40a198e merge
| * 2451d7e c.txt
* | 7653ac7 ddd
|/
* 08a6450 stash
--graph
参数会显示出合并的log
图。
禁止使用Fast forward模式
使用--no-ff
参数来合并分支,会强行给当前分支增加一次提交,命令如下:
git merge --no-ff -m "merge with no-ff" dev
-m
参数是这次提交附带的信息。
两种merge
方式如图所示:
为什么要禁止Fast forward
模式,因为禁止了这个模式,可以从git log --graph
中查看到曾经合并过分支,否则看不出分支合并的迹象。
stash
如果一条分支上提交了修改,另一条分支是看不到的,但是如果没有修改了但是没有提交,保留在工作区或者暂存区,别的分支是看得到的。
这样可能会导致一些错误,所以有了stash
这个功能。比如你正在一个分支上干活,但是突发情况不能让你干完,你必须先去别的分支上干活,这时候就需要用git stash
这个命令暂时储存你之前的修改。
这个命令有些trick,就是如果你创建一个文件,但是不写它,git stash
是无法发现它的,必须要git add file
到暂存区之后才能发现它,但是如果是修改,只要在工作区里修改了,不需要放到暂存区,这个命令也能发现。
可以通过命令git stash list
来查看当前存储的工作区和暂存区的状态,在每个分支下都能看到,因为工作区和暂存区的状态每个分支下都能看到。然后可以用git stash pop
来恢复第一个stash
,类似栈一样,如果用git stash apply
的话,还需要使用git stash drop
来弹出这个stash
。
如果多次工作区或者暂存区操作用stash
存储,然后恢复的时候没有提交,多次一起恢复,如果是同一个文件的话,则会发生merge
冲突。
多人协作
如果你从远程库clone
的是master
分支,但是你想切换到dev
分支,由于你没有clone
,所以直接git checkout dev
是不行的。
你需要创建dev
分支并且是用的是origin/dev
分支的内容,命令如下:
git checkout -b dev origin/dev
就会把远程库的dev
分支也弄下来,然后可以在上面愉快的操作了。
如果别人也往这个远程库的某个分支上提交代码,我们也正巧往这个分支上提交代码,就很有可能会发生冲突,比如:
mashaonan@mashaonan:~/Desktop/egnahc-hpec$ git push origin dev
To [email protected]:MiracleMa/egnahc-hpec.git
! [rejected] dev -> dev (fetch first)
error: failed to push some refs to '[email protected]:MiracleMa/egnahc-hpec.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
会告诉你应该怎么做,比如使用git pull
,把远程库分支的修改pull
下来,然后手动合并之后再提交再推送到远程库。
在git pull
的情况下可能会失败,因为本地的dev
分支没有和远程的origin/dev
分支链接,所以需要根据提示链接一下:
git branch --set-upstream dev origin/dev
Branch dev set up to track remote branch dev from origin.
标签
标签和分支非常类似,但是分支可以移动,标签不能移动,标签是和一个commit
绑定的。
标签有一些常用的命令:
git tag //查看所有标签名字
git tag [name] [commit_id]//对于指定的commit_id创建一个标签,默认为最新的commit
git show [name] //显示name标签的详细信息
git tag -a [name] -m "version" [commit_id] //创建标签另外附带信息,用-m参数,-a参数后面跟tag的名字
git tag -d [name] //删除tag
也可以被推送到远程库,推送命令为:
git push origin [tag] //推送指定名字的tag到远程库
git push origin --tags //一次性推送全部尚未推送到远程库的本地标签
如果要删除远程库的标签,首先需要本地删除
git tag -d [name]
然后从远程删除
git push origin :refs/tags/[name]
git配置
git的配置文件我们来看一下。
首先是当前版本库的配置文件,在./git/config
里,打开看到是这样的:
mashaonan@mashaonan:~/Desktop/gti/.git$ cat config
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
[remote "origin"]
url = [email protected]:MiracleMa/egnahc-hpec.git
fetch = +refs/heads/*:refs/remotes/origin/*
这是当前版本库的配置,还有当前用户全局版本库的配置,在~/.gitconfig
里:
mashaonan@mashaonan:~/Desktop/gti$ cat ~/.gitconfig
[user]
email = [email protected]
name = MiracleMa
如何修改配置呢?可以直接修改配置文件,可以用命令git config
来添加配置,加上参数--global
就是当前用户的全局配置,如果不加就是当前版本库的配置。
.gitignore
很多时候我们的工程在本地工作区都编译运行了,但是提交的时候不要编译运行后的执行文件,就需要.gitignore
文件来帮忙忽略掉那些不要的文件,但是.gitignore
不需要自己手写,github
上有各种配置文件了:https://github.com/github/gitignore
,直接拿过来用就行了。
我在本地操作如下:
mashaonan@mashaonan:~/Desktop/gti$ git status
On branch master
Untracked files:
(use "git add ..." to include in what will be committed)
a.c
nothing added to commit but untracked files present (use "git add" to track)
mashaonan@mashaonan:~/Desktop/gti$ vim .gitignore
mashaonan@mashaonan:~/Desktop/gti$ git status
On branch master
Untracked files:
(use "git add ..." to include in what will be committed)
.gitignore
nothing added to commit but untracked files present (use "git add" to track)
mashaonan@mashaonan:~/Desktop/gti$ git add .
mashaonan@mashaonan:~/Desktop/gti$ git commit -m "gitignore"
[master 7774969] gitignore
1 file changed, 1 insertion(+)
create mode 100644 .gitignore
可以看到只有一个文件被commit
了,也就是.gitignore
,可以看下远程库里的内容。
mashaonan@mashaonan:~/Desktop/gti$ ll
total 32
drwxrwxr-x 3 mashaonan mashaonan 4096 Apr 15 19:54 ./
drwxr-xr-x 23 mashaonan mashaonan 4096 Apr 14 16:04 ../
-rw-rw-r-- 1 mashaonan mashaonan 5 Apr 15 19:50 a.c
-rw-rw-r-- 1 mashaonan mashaonan 45 Apr 14 13:42 a.txt
-rw-rw-r-- 1 mashaonan mashaonan 4 Apr 14 16:35 c.txt
-rw-rw-r-- 1 mashaonan mashaonan 51 Apr 14 16:58 d.txt
drwxrwxr-x 8 mashaonan mashaonan 4096 Apr 15 19:53 .git/
-rw-rw-r-- 1 mashaonan mashaonan 4 Apr 15 19:50 .gitignore
submodule
子模块可以让你在一个项目中使用别的项目当做子模块,同时还能保持两个模块提交的独立。
如何为你的项目添加一个子模块呢?
git submodule add [email protected]:MiracleMa/6.828.git
就可以为你的项目添加一个子模块,查看此时工作区的状态:
mashaonan@mashaonan:~/Desktop/gti$ git status
On branch master
Changes to be committed:
(use "git reset HEAD ..." to unstage)
new file: .gitmodules
new file: 6.828
首先可以看到多了一个.gitmodules
文件,里面写的是子模块的相关信息:
mashaonan@mashaonan:~/Desktop/gti$ cat .gitmodules
[submodule "6.828"]
path = 6.828
url = [email protected]:MiracleMa/6.828.git
然后可以通过git diff --cache [6.828|--submodule]
的命令来显示相关子模块和原先的差别,--submodule
命令可以输出更多内容。
提交的时候,会发现远程库里的情况是这样的:
发现
6.828
这个文件夹,并没有真的被上传到我的远程库,那个目录其实是一个链接,链接到了6.828
原来的远程库。
大型的项目一般都需要用到子模块,比如ceph
,当时一直不明白为啥有些目录点进去就到了别的库,下载下来是空的。
可以看下ceph
的子模块:
[submodule "ceph-object-corpus"]
path = ceph-object-corpus
url = https://github.com/ceph/ceph-object-corpus.git
[submodule "src/civetweb"]
path = src/civetweb
url = https://github.com/ceph/civetweb
[submodule "src/erasure-code/jerasure/jerasure"]
path = src/erasure-code/jerasure/jerasure
url = https://github.com/ceph/jerasure.git
branch = v2-ceph
[submodule "src/erasure-code/jerasure/gf-complete"]
path = src/erasure-code/jerasure/gf-complete
url = https://github.com/ceph/gf-complete.git
branch = v3-ceph
这只是一小部分,每个都写了path
是存放到本仓库的哪个目录下,url
是对应的远程库地址,branch
是对应的分支。
如何克隆带有子模块的项目呢?
git clone xxx //克隆项目
git submodule init //子模块初始化
git submodule update //克隆子模块
但是这样只能把当前项目的子模块克隆下来,并不能克隆子模块的子模块,所以需要--recursive
参数,递归地克隆子模块。
git clone --recursive [url]
//或者
git clone [url]
git submodule update --init --recursive
其余还有很多在子模块上开发的操作,我暂时应该还用不上,以后用上了再查看吧。
pull request
pull request
是你要给某个开源项目做共享的时候需要用到的操作。
你需要把那个开源项目fork
过来到你的远程库,然后clone
到你本地,在某个分支上做一些修改,然后push
到你的远程库。
在github
上切换到你push
的分支,在边上有个New pull request
的按钮,可以把修改提交给作者,如果作者认可了,就会merge
到他的项目里。
这个方面主要参考GitHub 的 Pull Request 是指什么意思? - beepony的回答 - 知乎