Git笔记
创建版本库
选择一个合适的地方,创建一个空目录,并进入到该目录下,通过git init
命令初始化:
$ mkdir learngit
$ cd learngit
$ git init
Initialized empty Git repository in /Users/michael/learngit/.git/
.git
是用来跟踪管理版本库的,不可乱删,是隐藏文件,用ls -ah
就能看见。
所有的版本控制系统,只能跟踪文本文件的改动,像图片、视频这些二进制文件,虽然也能由版本控制系统管理,但是没法跟踪文件的具体变化。
文本文件最好用utf-8
编码。
添加文件到git仓库
在learngit
文件夹下新建一个文件readme.txt
,用git add
把文件添加到暂存区:
$ git add readme.txt
用git commit
把文件提交到仓库:
$ git commit -m "statements"
[master (root-commit) eaadf4e] wrote a readme file
1 file changed, 2 insertions(+)
create mode 100644 readme.txt
-m
后面的输入是本次提交的提交说明,最好每次提交都要有说明。
查看工作区状态和查看修改
用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 diff
查看不同:
$ git diff readme.txt
diff --git a/readme.txt b/readme.txt
index 46d49bf..9247db6 100644
--- a/readme.txt
+++ b/readme.txt
@@ -1,2 +1,2 @@
-Git is a version control system.
+Git is a distributed version control system.
Git is free software.
查看日志和版本回退
用git log
查看最近到最远的提交日志:
$ git log
commit 1094adb7b9b3807259d8cb349e7df1d4d6477073 (HEAD -> master)
Author: Michael Liao <askxuefeng@gmail.com>
Date: Fri May 18 21:06:15 2018 +0800
append GPL
commit e475afc93c209a690c39c13a46716e8fa000c366
Author: Michael Liao <askxuefeng@gmail.com>
Date: Fri May 18 21:03:36 2018 +0800
add distributed
commit eaadf4e385e865d25c48e7ca9c8395c3f7dfaef0
Author: Michael Liao <askxuefeng@gmail.com>
Date: Fri May 18 20:59:18 2018 +0800
wrote a readme file
若想简化输出信息,可加上--pretty=oneline
参数:
$ git log --pretty=oneline
1094adb7b9b3807259d8cb349e7df1d4d6477073 (HEAD -> master) append GPL
e475afc93c209a690c39c13a46716e8fa000c366 add distributed
eaadf4e385e865d25c48e7ca9c8395c3f7dfaef0 wrote a readme file
git用HEAD
表示当前版本,HEAD^
表示上一个提交的版本,HEAD^^
表示上上个提交的版本,以此类推,往上100个版本可用HEAD~100
表示。可用命令git reset
返回以前的版本:
$ git reset --hard HEAD^
HEAD is now at e475afc add distributed
此时再用git log
查看就看不到最新的版本了:
$ git log
commit e475afc93c209a690c39c13a46716e8fa000c366 (HEAD -> master)
Author: Michael Liao <askxuefeng@gmail.com>
Date: Fri May 18 21:03:36 2018 +0800
add distributed
commit eaadf4e385e865d25c48e7ca9c8395c3f7dfaef0
Author: Michael Liao <askxuefeng@gmail.com>
Date: Fri May 18 20:59:18 2018 +0800
wrote a readme file
如果想回到未来的版本,可以找到它的commit id
,指定回到未来的某个版本:
$ git reset --hard 1094a
HEAD is now at 83b0afe append GPL
版本号没必要写全,前几位就可以了,Git会自动去找。当然也不能只写前一两位,因为Git可能会找到多个版本号,就无法确定是哪一个了。
git还提供了git reflog
来查看记录下来的每一次命令:
$ git reflog
e475afc HEAD@{1}: reset: moving to HEAD^
1094adb (HEAD -> master) HEAD@{2}: commit: append GPL
e475afc HEAD@{3}: commit: add distributed
eaadf4e HEAD@{4}: commit (initial): wrote a readme file
工作区和暂存区
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JOnaeLzX-1574520291048)(img/2019-11-23-10-47-51.png)]
git add
命令实际上就是把要提交的所有修改放到暂存区(stage),然后,执行git commit
就可以一次性把暂存区的所有修改提交到分支。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hM8tHoIH-1574520291049)(img/2019-11-23-10-49-33.png)]
撤销修改
使用git checkout -- filename
命令丢弃该文件在工作区的修改:
$ git checkout -- readme.txt
命令git checkout -- readme.txt
意思就是,把readme.txt文件在工作区的修改全部撤销,这里有两种情况:
- 一种是
readme.txt
自修改后还没有被放到暂存区,现在,撤销修改就回到和版本库一模一样的状态; - 一种是
readme.txt
已经添加到暂存区后,又作了修改,现在,撤销修改就回到添加到暂存区后的状态,也就是不能丢弃暂存区的内容。
总之,就是让这个文件回到最近一次git commit或git add时的状态。
如果已经将修改添加到暂存区了,可以使用git reset HEAD filename
将暂存区的修改撤销掉(unstage),重新放回工作区:
$ git reset HEAD readme.txt
Unstaged changes after reset:
M readme.txt
现在暂存区是干净的,工作区有修改:
$ 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
如果已经将修改提交到了版本库,可以采用版本回退方法。
删除文件
删除也是一种修改操作,如果你直接在文件管理器中删除了文件,此时用git status
查看可知哪些文件被删除了:
$ git status
On branch master
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
no changes added to commit (use "git add" and/or "git commit -a")
此时有两个选择,一是要确定从版本库删除该文件,则用git rm
删除掉,并用git commit
提交:
$ git rm test.txt
rm 'test.txt'
$ git commit -m "remove test.txt"
[master d46f35e] remove test.txt
1 file changed, 1 deletion(-)
delete mode 100644 test.txt
另一种情况是误删,可以用git checkout -- filename
把误删的文件恢复到最新的版本:
$ git checkout -- test.txt
git checkout
其实是用版本库里的版本替换工作区的版本,无论工作区是修改还是删除,都可以一键还原。如果一个文件已经被提交到版本库,那么你永远不用担心误删,但是要小心,你只能恢复文件到最新版本,你会丢失最近一次提交后你修改的内容。
SSH Key设置
由于本地Git仓库和GitHub仓库之间的传输是通过SSH加密的,所以,需要一点设置:
第1步:创建SSH Key。在用户主目录下,看看有没有.ssh
目录,如果有,再看看这个目录下有没有id_rsa
和id_rsa.pub
这两个文件,如果已经有了,可直接跳到下一步。如果没有,打开Shell(Windows下打开Git Bash),创建SSH Key:
$ ssh-keygen -t rsa -C "youremail@example.com"
你需要把邮件地址换成你自己的邮件地址,然后一路回车,使用默认值即可,由于这个Key也不是用于军事目的,所以也无需设置密码。
如果一切顺利的话,可以在用户主目录里找到.ssh
目录,里面有id_rsa
和id_rsa.pub
两个文件,这两个就是SSH Key的秘钥对,id_rsa
是私钥,不能泄露出去,id_rsa.pub
是公钥,可以放心地告诉任何人。
第2步:登陆GitHub,打开“Account settings”,“SSH Keys”页面,然后,点“Add SSH Key”,填上任意Title,在Key文本框里粘贴id_rsa.pub文件的内容:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zVZIxSp8-1574520291050)(img/2019-11-23-14-22-41.png)]
点“Add Key”,你就应该看到已经添加的Key:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gZWaCLKq-1574520291050)(img/2019-11-23-14-25-46.png)]
GitHub允许你添加多个Key。假定你有若干电脑,你一会儿在公司提交,一会儿在家里提交,只要把每台电脑的Key都添加到GitHub,就可以在每台电脑上往GitHub推送了。
远程仓库
假设你已经在本地创建了一个Git仓库后,又想在GitHub创建一个Git仓库,并且让这两个仓库进行远程同步,这样,GitHub上的仓库既可以作为备份,又可以让其他人通过该仓库来协作。
首先在GitHub上创建一个新的仓库learngit
,此时这个仓库还是空的,根据GitHub的提示,在本地的learngit
仓库下运行命令:
$ git remote add origin git@github.com:JILLWONG/learngit.git
添加后,远程库的名字就是origin
,这是Git默认的叫法,也可以改成别的,但是origin
这个名字一看就知道是远程库。
下一步,就可以把本地库的所有内容推送到远程库上:
$ git push -u origin master
Counting objects: 20, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (15/15), done.
Writing objects: 100% (20/20), 1.64 KiB | 560.00 KiB/s, done.
Total 20 (delta 5), reused 0 (delta 0)
remote: Resolving deltas: 100% (5/5), done.
To github.com:JILLWONG/learngit.git
* [new branch] master -> master
Branch 'master' set up to track remote branch 'master' from 'origin'.
由于远程库是空的,我们第一次推送master
分支时,加上了-u
参数,Git不但会把本地的master
分支内容推送的远程新的master
分支,还会把本地的master
分支和远程的master
分支关联起来,在以后的推送或者拉取时就可以简化命令。
从现在起,只要本地做了提交,就可以通过以下命令把本地master
分支的最新修改推送到GitHub:
$ git push origin master
上述讲了先有本地库,后有远程库的时候,如何关联远程库。现在,假设我们从零开发,那么最好的方式是先创建远程库,然后,从远程库克隆。首先,登陆GitHub,创建一个新的仓库,名字叫gitskills
,接下来用命令git clone
克隆一个本地库:
$ git clone git@github.com:JILLWONG/gitskills.git
Cloning into 'gitskills'...
remote: Counting objects: 3, done.
remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 3
Receiving objects: 100% (3/3), done.
GitHub给出的地址不止一个,还可以用https://github.com/JILLWONG/gitskills.git
这样的地址。实际上,Git支持多种协议,默认的git://
使用ssh
,但也可以使用https
等其他协议。使用https
除了速度慢以外,还有个最大的麻烦是每次推送都必须输入口令,但是在某些只开放http
端口的公司内部就无法使用ssh
协议而只能用https
。
分支创建与合并
创建并切换到dev
分支:
$ git checkout -b dev
Switched to a new branch 'dev'
git checkout
命令加上-b
参数表示创建并切换分支,相当于下面两条命令:
$ git branch dev
$ git checkout dev
Switched to branch 'dev'
用git branch
查看当前分支:
$ git branch
* dev
master
在当前分支上进行开发,并切换回master
分支:
$ git checkout master
Switched to branch 'master'
再将dev
分支合并到master
:
$ git merge dev
Updating d46f35e..b17d20e
Fast-forward
readme.txt | 1 +
1 file changed, 1 insertion(+)
注意到上面的Fast-forward
信息,Git告诉我们,这次合并是“快进模式”,也就是直接把master
指向dev
的当前提交,所以合并速度非常快。
合并完成后就可以删除dev
分支了:
$ git branch -d dev
Deleted branch dev (was b17d20e).
实际上,切换分支这个动作,用switch
更科学。因此,最新版本的Git提供了新的git switch
命令来切换分支,创建并切换到新的dev
分支,可以使用:
$ git switch -c dev
直接切换到已有的master
分支:
$ git switch master
分支命令小结
- 查看分支:
git branch
- 创建分支:
git branch <name>
- 切换分支:
git switch <name>
或者git checkout <name>
- 创建并切换分支:
git checkout -b <name>
或者git switch -c <name>
- 合并某分支到当前分支:
git merge <name>
- 删除分支:
git branch -d <name>
解决冲突
当合并分支产生冲突时,必须先解决冲突,再提交,方可合并完成。
冲突文件内容如下:
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.
<<<<<<< HEAD
Creating a new branch is quick & simple.
=======
Creating a new branch is quick AND simple.
>>>>>>> feature1
手动修改后,再提交:
$ git add readme.txt
$ git commit -m "conflict fixed"
[master cf810e4] conflict fixed
用带参数的git log
可看到分支合并情况:
$ git log --graph --pretty=oneline --abbrev-commit
* cf810e4 (HEAD -> master) conflict fixed
|\
| * 14096d0 (feature1) AND simple
* | 5dc6824 & simple
|/
* b17d20e branch test
* d46f35e (origin/master) 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
Bug分支
当你接到一个修复一个代号101的bug的任务时,很自然地,你想创建一个分支issue-101
来修复它,但是,等等,当前正在dev
上进行的工作还没有提交,并不是你不想提交,而是工作只进行到一半,还没法提交,预计完成还需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
创建临时分支issue-101
,修复完成后,切换到master
分支,并完成合并,最后删除issue-101
分支。现在,是时候接着回到dev
分支干活了!
dev
分支的工作区是干净的,刚才的工作现场存到哪去了?用git stash list
命令看看:
$ git stash list
stash@{0}: WIP on dev: f52c633 add merge
工作现场还在,Git把stash
内容存在某个地方了,但是需要恢复一下,有两个办法:
- 一是用
git stash apply
恢复,但是恢复后,stash
内容并不删除,你需要用git stash drop
来删除; - 另一种方式是用
git stash pop
,恢复的同时把stash
内容也删了:
$ git stash pop
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: readme.txt
Dropped refs/stash@{0} (5d677e2ee266f39ea296182fb2354265b91b3b2a)
你可以多次stash
,恢复的时候,先用git stash list
查看,然后恢复指定的stash
,用命令:
$ git stash apply stash@{0}
在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 4c805e2
[master 1d4b803] fix bug 101
1 file changed, 1 insertion(+), 1 deletion(-)
Git自动给dev
分支做了一次提交,注意这次提交的commit
是1d4b803
,它并不同于master
的4c805e2
,因为这两个commit
只是改动相同,但确实是两个不同的commit
。
Feature分支
软件开发中,总有无穷无尽的新的功能要不断添加进来。添加一个新功能时,你肯定不希望因为一些实验性质的代码,把主分支搞乱了,所以,每添加一个新功能,最好新建一个feature
分支,在上面开发,完成后,合并,最后,删除该feature
分支。
当feature
分支没有合并时,如果删除,将丢失掉修改,因此使用参数-d
则提示销毁失败。如果要强行删除,需要使用大写的-D
参数:
$ git branch -D feature
Deleted branch feature (was 287773e).
多人协作
当你从远程仓库克隆时,实际上Git自动把本地的master
分支和远程的master
分支对应起来了,并且,远程仓库的默认名称是origin
。
要查看远程库的信息,用git remote
:
$ git remote
origin
要查看远程库的详细信息,可以git remote -v
:
$ git remote -v
origin git@github.com:michaelliao/learngit.git (fetch)
origin git@github.com:michaelliao/learngit.git (push)
上面显示了可以抓取和推送的origin
的地址。如果没有推送权限,就看不到push
的地址。
推送分支,就是把该分支上的所有本地提交推送到远程库。推送时,要指定本地分支,这样,Git就会把该分支推送到远程库对应的远程分支上:
$ git push origin master
如果要推送其他分支,比如dev
,则改成:
$ git push origin dev
当你的小伙伴从远程库clone
时,默认情况下,你的小伙伴只能看到本地的master
分支。现在,你的小伙伴要在dev
分支上开发,就必须创建远程origin
的dev
分支到本地,于是他用这个命令创建本地dev
分支并切换到dev
分支:
$ git checkout -b dev origin/dev
或者先创建dev
再切换到dev
:
$ git branch bug origin/dev
Branch 'dev' set up to track remote branch 'dev' from 'origin'.
$ git switch dev
Switched to branch 'dev'
Your branch is up to date with 'origin/dev'.
当多人合作开发时,两个人先分别从远程库克隆,各自修改提交后push
到远程库,此时后push
的人可能推送失败,因为先推送的人的最新提交和后推送的人的提交有冲突,解决办法也很简单,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
Auto-merging env.txt
CONFLICT (add/add): Merge conflict in env.txt
Automatic merge failed; fix conflicts and then 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 compression using up to 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中打标签非常简单,首先,切换到需要打标签的分支上:
$ git branch
* dev
master
$ git checkout master
Switched to branch 'master'
然后,敲命令git tag <name>
就可以打一个新标签:
$ 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 <tagname>
查看标签信息:
$ git show v0.9
commit f52c63349bc3c1593499807e5c8e972b82c8f286 (tag: v0.9)
Author: Michael Liao <askxuefeng@gmail.com>
Date: Fri May 18 21:56:54 2018 +0800
add merge
diff --git a/readme.txt b/readme.txt
...
还可以创建带有说明的标签,用-a
指定标签名,-m
指定说明文字:
$ git tag -a v0.1 -m "version 0.1 released" 1094adb
用命令git show <tagname>
可以看到说明文字:
$ 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.1
Deleted tag 'v0.1' (was f15b0dd)
因为创建的标签都只存储在本地,不会自动推送到远程。所以,打错的标签可以在本地安全删除。
如果要推送某个标签到远程,使用命令git push origin <tagname>
:
$ 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