最近开始工作了,果然必然需要用到git,正好研究生刚刚入校就写过git总结,正好拿出来用用。
参考廖雪峰老师的git教程
目录
1、创建本地版本库
第一步,选择一个合适的地方,创建一个空目录:
第二步,通过git init
命令把这个目录变成Git可以管理的仓库:
当前目录下多了一个 .git
的目录,这个目录是Git来跟踪管理版本库的,千万不要手动修改这个目录里面的文件,不然改乱了,就把Git仓库给破坏了。
如果没有看到.git
目录,那是因为这个目录默认是隐藏的,用ls -ah
命令就可以看见。
2、把文件添加到版本库
一定要放到learngit
目录下(子目录也行),因为这是一个Git仓库,放到其他地方Git再厉害也找不到这个文件。把一个文件放到Git仓库只需要两步。
第一步,用命令git add
告诉Git,把文件添加到仓库:
执行上面的命令,没有任何显示,这就对了,Unix的哲学是“没有消息就是好消息”,说明添加成功。
第二步,用命令git commit
告诉Git,把文件提交到仓库:
git commit
命令,-m
后面输入的是本次提交的说明,可以输入任意内容,当然最好是有意义的,这样你就能从历史记录本地操作里方便地找到改动记录。
git commit
命令执行成功后会告诉你,1 file changed:
1个文件被改动(我们新添加的readme.txt文件);107 insertions:
插入了107行内容。
因为commit
可以一次提交很多文件,所以可以多次add
不同的文件。
3、工作区和暂存区
就是在电脑里能看到的目录,比如learngit
文件夹就是一个工作区。
版本库(Repository)
工作区有一个隐藏目录.git
,这个不算工作区,而是Git的版本库。
Git的版本库里存了很多东西,其中最重要的就是称为stage
(或者叫index)的暂存区,还有Git为我们自动创建的第一个分支master
,以及指向master的一个指针叫HEAD
。
把文件往Git版本库里添加的时候,是分两步执行的:
第一步是用git add
把文件添加进去,实际上就是把文件修改添加到暂存区;
第二步是用git commit
提交更改,实际上就是把暂存区的所有内容提交到当前分支。
因为创建Git版本库时,Git自动创建了唯一一个master
分支,所以,现在,git commit
就是往master
分支上提交更改。
可以理解为,需要提交的文件修改通通放到暂存区,然后,一次性提交暂存区的所有修改。
4、版本修改
当已经成功地添加并提交了一个文件,继续修改文件,添加内容aaaaaaaaaa
,运行git status
命令看看结果:
git status
命令可以让我们时刻掌握仓库当前的状态,上面的命令输出告诉我们,readme.txt
被修改过了,但还没有准备提交的修改。并且存在备份文件。
虽然Git告诉我们readme.txt
被修改了,但是如果记不清上次怎么修改的readme.txt
,所以,需要用git diff
这个命令看看:
知道了对readme.txt
作了什么修改后,再把它提交到仓库就放心多了,提交修改和提交新文件是一样的两步,第一步是git add
,同样没有任何输出。再执行第二步git commit
。
5、忽略特殊文件
当对文件进行修改时,会产生一个.v~
的特殊文件:
第一步是用vim .gitignore
进入.gitignore
文本:
按i编辑,输入*v~
忽略备份文件,sec
退出编辑,:wq
保存并退出。
第二步是git add
,同样没有任何输出。再执行第三步git commit
。
将.gitignore
文本内容上传到版本库。
6、版本回退
每当你觉得文件修改到一定程度的时候,就可以“保存一个快照”,这个快照在Git中被称为commit
。一旦你把文件改乱了,或者误删了文件,还可以从最近的一个commit
恢复,然后继续工作,而不是把几个月的工作成果全部丢失。
版本控制系统用git log
命令可以告诉我们历史记录,在Git中,我们用git log
命令查看:
git log
命令显示从最近到最远的提交日志,我们可以看到3次提交。
如果嫌输出信息太多,看得眼花缭乱的,可以试试加上--oneline
参数:
如果准备把文本回退到上一个版本,首先,Git必须知道当前版本是哪个版本,在Git中,用HEAD
表示当前版本,也就是最新的提交,上一个版本就是HEAD^
,上上一个版本就是HEAD^^
,当然往上100个版本写100个^
比较容易数不过来,所以写成HEAD~100
。
现在,我们要把当前版本回退到上一个版本,就可以使用git reset
命令:
用git log
再看看现在版本库的状态:
最新的那个版本已经看不到了,如果想看见最新的版本,办法其实还是有的,只要上面的命令行窗口还没有被关掉,你就可以顺着往上找,找到那个版本的commit id
,于是就可以指定回到未来的某个版本:
版本号没必要写全,前几位就可以了,Git会自动去找。当然也不能只写前一两位,因为Git可能会找到多个版本号,就无法确定是哪一个了。
在Git中,用$ git reset --hard HEAD^
回退版本时,再想恢复到最新版本,就必须找到最新版本的commit id
。Git提供了一个命令git reflog
用来记录每一次命令:
7、管理修改
第一步,对top_tb.v
做一个修改,比如加一行内容bbbbbbbbb
,然后添加:
然后,再修改,添加ccccccccccc
:
提交:
提交后,再看看状态:
第二次的修改没有被提交。
回顾一下操作过程:
第一次修改 -> git add
-> 第二次修改 -> git commit
Git管理的是修改,当你用git add
命令后,在工作区的第一次修改被放入暂存区,准备提交,但是,在工作区的第二次修改并没有放入暂存区,所以,git commit
只负责把暂存区的修改提交了,也就是第一次的修改被提交了,第二次的修改不会被提交。
提交后,用git diff HEAD -- top_tb.v
命令可以查看工作区和版本库里面最新版本的区别:
可见,第二次修改确实没有被提交。
那怎么提交第二次修改呢?你可以继续git add
再git commit
,也可以别着急提交第一次修改,先git add第二次修改,再git commit,就相当于把两次修改合并后一块提交了:
第一次修改 -> git add
-> 第二次修改 -> git add
-> git commit
8、撤销修改
在readme.txt
中添加了一行dddddddddddddd
:
可以删掉最后一行,手动把文件恢复到上一个版本的状态。如果用git status
查看一下:
可以发现,Git会告诉你,git checkout -- file
可以丢弃工作区的修改:
命令git checkout -- top_tb.v
意思就是,把top_tb.v
文件在工作区的修改全部撤销,这里有两种情况:
一种是top_tb.v
自修改后还没有被放到暂存区,现在,撤销修改就回到和版本库一模一样的状态;
一种是top_tb.v
已经添加到暂存区后,又作了修改,现在,撤销修改就回到添加到暂存区后的状态。
总之,就是让这个文件回到最近一次git commit
或git add
时的状态。
现在,看看top_tb.v
的文件内容:
文件内容果然复原了。
git checkout -- file
命令中的--
很重要,没有--
,就变成了“切换到另一个分支”的命令。
如果还git add
到暂存区了,庆幸的是,在commit
之前,你发现了这个问题。用git status
查看一下,修改只是添加到了暂存区,还没有提交:
Git同样告诉我们,用命令git reset HEAD <file>
可以把暂存区的修改撤销掉(unstage),重新放回工作区:
git reset
命令既可以回退版本,也可以把暂存区的修改回退到工作区。当用HEAD
时,表示最新的版本。
再用git status
查看一下,现在暂存区是干净的,工作区有修改:
用git checkout -- file
丢弃工作区的修改:
9、删除文件
在Git中,删除也是一个修改操作,先添加一个新文件test.txt
到Git并且提交:
通常直接在文件管理器中把没用的文件删了,或者用rm
命令删了,这个时候,Git知道你删除了文件,因此,工作区和版本库就不一致了,git status
命令会立刻告诉你哪些文件被删除了:
现在有两个选择,一是确实要从版本库中删除该文件,那就用命令git rm
删掉,并且git commit
:
现在,文件就从版本库中被删除了。
另一种情况是删错了,因为版本库里还有呢,所以可以很轻松地把误删的文件恢复到最新版本:
$ git checkout -- xxx
git checkout
其实是用版本库里的版本替换工作区的版本,无论工作区是修改还是删除,都可以“一键还原”。
10、分支管理
每次提交,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
参数表示创建并切换,相当于以下两条命令:
$ git branch dev
$ git checkout dev
Switched to branch 'dev'
然后,用git branch
命令查看当前分支:
git branch
命令会列出所有分支,当前分支前面会标一个*
号。
然后,就可以在dev
分支上正常提交,比如对top_tb.v
做个修改,加上一行eeeeeeeee
,然后提交:
现在,dev
分支的工作完成,就可以切换回master
分支:
切换回master
分支后,再查看一个top_tb.v
文件,刚才添加的内容不见了。因为那个提交是在dev
分支上,而master
分支此刻的提交点并没有变:
现在,把dev
分支的工作成果合并到master
分支上:
git merge
命令用于合并指定分支到当前分支。合并后,再查看top_tb.v
的内容,就可以看到,和dev
分支的最新提交是完全一样的。
注意到上面的Fast-forward
信息,Git告诉我们,这次合并是“快进模式”,也就是直接把master
指向dev
的当前提交,所以合并速度非常快。
当然,也不是每次合并都能Fast-forward
。
合并完成后,就可以放心地删除dev
分支了:
删除后,查看branch
,就只剩下master
分支了:
因为创建、合并和删除分支非常快,所以Git鼓励使用分支完成某个任务,合并后再删掉分支,这和直接在master分支上工作效果是一样的,但过程更安全。
11、解决冲突
合并分支往往也不是一帆风顺的。
准备新的feature1
分支,继续新分支开发:
修改top_tb.v
最后一行,改为Creating a new branch
,
在feature1
分支上提交:
切换到master
分支:
Git还会自动提示当前master
分支比远程的master
分支要超前1个提交。
在master
分支上把top_tb.v
文件的最后一行改为Creating a master branch
,提交:
现在,master
分支和feature1
分支各自都分别有新的提交,变成了这样:
这种情况下,Git无法执行“快速合并”,只能试图把各自的修改合并起来,但这种合并就可能会有冲突:
Top_tb.v
文件存在冲突,必须手动解决冲突后再提交。git status
也可以告诉我们冲突的文件:
我们可以直接查看top_tb.v
的内容:
Git用<<<<<<<
,=======
,>>>>>>>
标记出不同分支的内容,修改如下后保存:
Creating a master branch
再提交:
现在,master
分支和feature1
分支变成了下图所示:
用带参数的git log --graph
也可以看到分支的合并情况:
最后,删除feature1
分支:
当Git无法自动合并分支时,就必须首先解决冲突。解决冲突后,再提交,合并完成。
解决冲突就是把Git合并失败的文件手动编辑为我们希望的内容,再提交。
用git log --graph命令可以看到分支合并图。
12、分支管理策略
通常,合并分支时,如果可能,Git会用Fast forward
模式,但这种模式下,删除分支后,会丢掉分支信息。
如果要强制禁用Fast forward
模式,Git就会在merge
时生成一个新的commit
,这样,从分支历史上就可以看出分支信息。
下面我们实战一下--no-ff
方式的git merge
:
首先,仍然创建并切换dev
分支:
修改top_tb.v
文件,并提交一个新的commit
:
现在,切换回master
:
准备合并dev
分支,请注意--no-ff
参数,表示禁用Fast forward
:
因为本次合并要创建一个新的commit
,所以加上-m
参数,把commit
描述写进去。
合并后,用git log--graph
看看分支历史:
可以看到,不使用Fast forward
模式,merge后就像这样:
13、Feature分支
软件开发中,总有无穷无尽的新的功能要不断添加进来。
添加一个新功能时,肯定不希望因为一些实验性质的代码,把主分支搞乱了,所以,每添加一个新功能,最好新建一个feature
分支,在上面开发,完成后,合并,最后,删除该feature
分支。开发一个新feature
,最好新建一个分支;如果要丢弃一个没有被合并过的分支,可以通过git branch -D <name>
强行删除。
14、克隆远程仓库
用命令git clone
克隆一个本地库:输入git clone用户名(例:jqyao19)@192.168.3.21:/data/project/newrepo
注:最后这个newrepro是根据项目会变化的
第一次输入 yes
输入密码,注:密码不显示光标
15、本地库推送到远程库
如果没有从远程克隆仓库到本地,则用git remote add origin git@192.168.3.21:/data/project/newrepo
命令,把一个已有的本地仓库与远程仓库关联:
用git push origin master
命令,实际上是把当前分支master
推送到远程,输入密码。
由于远程库是空的,第一次推送master
分支时,加上了-u
参数,Git不但会把本地的master
分支内容推送的远程新的master
分支,还会把本地的master
分支和远程的master
分支关联起来,在以后的推送或者拉取时就可以简化命令。
本地库抓取远程库最新提交
用git pull
把最新的提交从origin/dev
抓下来:
16、多人协作
当你从远程仓库克隆时,实际上Git自动把本地的master
分支和远程的master分支对应起来了,并且,远程仓库的默认名称是origin
。
要查看远程库的信息,用git remote
:
或者,用git remote -v
显示更详细的信息:
上面显示了可以抓取和推送的origin
的地址。如果没有推送权限,就看不到push
的地址。
17、推送分支
推送分支,就是把该分支上的所有本地提交推送到远程库。推送时,要指定本地分支,这样,Git就会把该分支推送到远程库对应的远程分支上:
如果要推送其他分支,比如dev
,就改成:
$ git push origin dev
但是,并不是一定要把本地分支往远程推送
master
分支是主分支,因此要时刻与远程同步;
dev
分支是开发分支,团队所有成员都需要在上面工作,所以也需要与远程同步;
feature
分支是否推到远程,取决于你是否和你的小伙伴合作在上面开发。
18、抓取分支
多人协作时,大家都会往master
和dev
分支上推送各自的修改。
现在,模拟一个成员,可以在另一台电脑(注意要把SSH Key
添加到GitHub)或者同一台电脑的另一个目录下克隆:
当成员从远程库clone
时,默认情况下,成员只能看到本地的master分支。
$ git branch
* master
现在,要在dev分支上开发,就必须创建远程origin的dev分支到本地,于是他用这个命令创建本地dev分支:
$ git checkout -b dev origin/dev
现在,他就可以在dev上继续修改,然后,时不时地把dev分支push到远程:
$ 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分支推送了他的提交,而碰巧另一个成员也对同样的文件作了修改,并试图推送:
推送失败,因为成员的最新提交和另一个成员试图推送的提交有冲突,解决办法也很简单,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
成功,但是合并有冲突,需要手动解决,解决的方法和分支管理中的解决冲突完全一样。解决后,提交,再push
:
因此,多人协作的工作模式通常是这样:
首先,可以试图用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>
。
这就是多人协作的工作模式,一旦熟悉了,就非常简单。
19、标签管理
发布一个版本时,我们通常先在版本库中打一个标签(tag),这样,就唯一确定了打标签时刻的版本。将来无论什么时候,取某个标签的版本,就是把那个打标签的时刻的历史版本取出来。所以,标签也是版本库的一个快照。
Git的标签虽然是版本库的快照,但其实它就是指向某个commit
的指针(跟分支很像对不对?但是分支可以移动,标签不能移动),所以,创建和删除标签都是瞬间完成的。
20、创建标签
在Git中打标签非常简单,首先,切换到需要打标签的分支上。然后,敲命令git tag <name>
就可以打一个新标签:
可以用命令git tag
查看所有标签:
默认标签是打在最新提交的commit
上的。有时候,如果忘了打标签,比如,现在已经是周五了,但应该在周一打的标签没有打,怎么办?
方法是找到历史提交的commit id
,然后打上就可以了:
比方说要对123这次提交打标签,它对应的commit id
是dacdbcd
,敲入命令:
再用命令git tag
查看标签:
注意,标签不是按时间顺序列出,而是按字母排序的。可以用git show <tagname>
查看标签信息:
可以看到,v0.9
确实打在123
这次提交上。
还可以创建带有说明的标签,用-a
指定标签名,-m
指定说明文字:
用命令git show <tagname>
可以看到说明文字:
21、操作标签
如果标签打错了,也可以删除:
因为创建的标签都只存储在本地,不会自动推送到远程。所以,打错的标签可以在本地安全删除。如果要推送某个标签到远程,使用命令git push origin <tagname>
:
或者,一次性推送全部尚未推送到远程的本地标签:
$ git push origin --tags
Total 0 (delta 0), reused 0 (delta 0)
To 192.168.3.21:/data/project/newrepo
* [new tag] v0.9 -> v0.9
如果标签已经推送到远程,要删除远程标签就麻烦一点,先从本地删除:
然后,从远程删除。删除命令也是push
,但是格式如下:
要看看是否真的从远程库删除了标签,可以从远处仓库查看。
22、忽略特殊文件例子
.gitignore 文件的例子:
# 此为注释 – 将被 Git 忽略
# 忽略所有 .a 结尾的文件
.a
# 但 lib.a 除外
!lib.a
# 仅仅忽略项目根目录下的 TODO 文件
# 不包括 subdir/TODO
/TODO
# 忽略 build/ 目录下的所有文件
build/
# 忽略 doc 目录下的所有 .txt 文件
# 会忽略 doc/notes.txt
# 但不包括 doc/server/arch.txt
doc/.txt