$ git init # 创建新仓库
$ git config --global user.email "[email protected]" # 设置全局email
$ git config --global user.name "Anderson" # 设置全局用户名
本地仓库由3个区域组成,工作区
持有你的源码等实际文件,暂存区
像个工作区和版本库之间的buffer,临时存储你的改动,本地版本库
记录着你每一次的提交,并维护若干分支。
所有文件都处在两个状态之一:untracked 和 tracked。untracked 的文件未被纳入git的版本控制,tracked状态又可被细分为以下三个状态:
暂存区
,坐等提交查看文件状态:
$ git status
根目录下的.gitignore
文件描述了哪些文件需要被git忽略:
# 此为注释 – 将被 Git 忽略
*.a # 忽略所有 .a 结尾的文件
!lib.a # 但 lib.a 除外
/TODO # 仅仅忽略项目根目录下的 TODO 文件,不包括 subdir/TODO
build/ # 忽略 build/ 目录下的所有文件
doc/*.txt # 会忽略 doc/notes.txt 但不包括 doc/server/arch.tx
将untracked或modified文件加入暂存区
(这个过程称为 stage):
$ git add
如果改了暂存区
内的文件,需要再次$ git add
。
$ git add .
:偷懒,慎用。递归把当前目录下所有untracked或modified文件加入暂存区
。
unstage某个文件(文件内容不会变):
$ git reset HEAD
撤销对某个文件的修改,文件恢复到unmodified状态,与版本库一致:
从暂存区
提交到本地版本库
:
$ git commit -m 'description'
branch 和 HEAD
简单地说,版本库是一个个commit连接起来的一张图,branch是指向某个commit的指针,从初始commit到该branch指向的commit的路径,形成了该分支的历史。
HEAD
是一个非常重要的概念,理解了HEAD
很多其他命令就很直观了。HEAD
本质上也是一个指针,它有两种状态:
通常情况下,HEAD
是一个branch的 引用/镜像,此时HEAD
和该branch绑定(attach)在一起,一起指向某个commit,HEAD
和该branch指针的移动会互相同步(除了用checkout
显式移动HEAD
);
attach HEAD
到某一分支的命令:
$ git checkout
HEAD
也可以不attach到branch,而是指向某个commit,这种状态称为 detached HEAD。注意,即使HEAD
与branch本质上指向同一commit,如果没有显式用1中命令attach HEAD
到分支则也是 detached HEAD 状态。
同样地,用 checkout
命令将HEAD
指向某个特定的 commit:
$ git checkout
HEAD
所指向commit的版本,是当前工作区内文件的基准。除此之外,HEAD
一个重要的作用是:
HEAD 为很多git命令提供基准位置
比如:
命令 | 含义 |
---|---|
git commit |
将新的commit链接在HEAD 后并更新HEAD (链表的插入),如果HEAD attach到了某个branch,该branch也会被更新 |
git branch |
创建一个新的branch,值与HEAD 一致,与之指向同一个commit |
git merge |
将指定branch多出来的commit合并为一个并提交到HEAD ,后续逻辑与git commit 一致 |
… | … |
举例说明,假如初始状态如下图(master* 表示HEAD
attach到了master分支):
用以下命令将HEAD
指向C0:
$ git checkout C0
在该状态下做一次commit(C2),C2链接在HEAD
后并移动HEAD
:
如果此时执行git checkout master
将HEAD
attach到master,我们会丢失C2的引用,因此在C2处建一个 dev 分支(其实就算丢失了也没关系,神奇的reflog
命令可以找到C2的id):
$ git branch dev # 以 HEAD 为基准建立 dev 分支
注意,这时依然是 detached HEAD 状态,再commit一次的话可以看到 dev 分支不受影响:
如果要在dev分支上提交,必须先将HEAD
attach到dev:
$ git checkout dev
现在将dev合并到master,根据之前描述的,merge
的操作对象是HEAD
,因此先将HEAD
attach到master再做merge:
$ git checkout master
$ git merge dev
如果只是单纯地让HEAD
指向C1而不attach到master上,结果会是这样:
$ git checkout C1
$ git merge dev
即操作的是HEAD
,master 分支不受影响。
由上述例子可知,在针对某个分支操作之前,通常先要将HEAD
attach到该branch上,因此HEAD
绑定的分支也被称为当前分支。
HEAD
的位置可以通过 .git/HEAD
文件查看:
$ cat .git/HEAD
e96c12854b77fe6f3dea81d593ddd2824eeaf9d6 #指向某个commit
$ git checkout develop
$ cat .git/HEAD
ref: refs/heads/develop #指向develop分支
分支合并
假设有两个branch:bugfix和master,初始状态如下:
现在要将 bugFix 合并到 master,我们有两种选择:
1. merge
git merge bugFix
merge
的动作如下:
HEAD
绑定的分支,即master)上;此时 bugFix 的 commit 被合并到了 master,master 含有了两个分支的提交信息(C3 + C4),但bugFix 却没有 master 分支的提交(即C3),如果想要让 bugFix 分支也含有全部提交,则可将master merge到bugFix:
$ git checkout bugFix
$ git merge master
由于 bugFix 指向的 commit 实际上是 master 的祖先,因此这里的 merge 只会将 bugFix 分支 fast-forward 到 master 分支,与其一同指向C4:
2. rebase
对上面的场景我们也可以用 rebase
来进行分支合并:
git rebase bugFix
rebase
的工作流程可以想象成:
就像把 master 分支上的 commit “append” 到了 bugFix 分支。注意 C3 依然存在,C3’只是 C3 的一份拷贝。
接下来也可以用rebase
让bugFix与master保持同步,同样也是做一次 fast-forward:
$ git checkout bugFix
$ git rebase master
3. 冲突解决
git merge
时如果出现文件冲突,合并将失败,冲突的详细信息会写入对应文件中,此时应修改文件,手动解决冲突,并用 git add
表明冲突已解决。最后 commit 即完可,该提交的 message 默认为 “Merge branch xxx into yyy”:
$ edit
$ edit
$ git add
$ git add
$ git commit
git rebase
实质上一个多个commit依次回放的过程,如果某次“回放”出现了冲突,可以修改文件手动解决冲突,并用git add
表明冲突已解决。冲突修改完毕后不需要commit,用下面命令继续 rebase:
$ git rebase --continue
如果中间遇到某个补丁不需要应用,可以用下面命令忽略:
git rebase --skip
如果想回到rebase执行之前的状态,可以执行:
git rebase --abort
4. 优缺点
rebase
的优点是分支树会很干净,不会出现分叉,也不会有一个多余的Merge From XXX 的commit;缺点是commit的顺序会错乱。
相对路径^
和~
我们已经看到了两种修改commit指针的方式:
git checkout
移动HEAD
指针git branch -f <目标commit>
如果目标地址不是某个branch而是一个commit,我们必须找到其ID,这通常比较麻烦。我们可以用 ^
和 ~
相对某个指针进行定位:
HEAD^ # HEAD之前的commit
HEAD^^ # HEAD回退两步的commit
master~5 # master指针回退5步的commit
撤销提交
1. reset
reset 的作用是移动当前分支。
$ git reset HEAD^ #将当前分支往回退一步
它的效果和git branch
移动分支是一样的:
$ git branch -f master HEAD^ # master是当前分支
注意,文件的内容会被保留,你会发现多了很多未暂存的文件修改,利用这一点我们可以用git reset
整理commit历史,将多个commit合并为1个。如果想让所有文件彻底回到目标commit的状态,可以再次$ git checkout .
丢弃所有文件修改;或者给reset
加上--hard
参数,这样你的本地修改都会被抹杀,大招慎用。
2. revert
revert 为指定的(多个)提交生成逆操作,并依次形成新的commit:
$ git revert HEAD
比reset
好在可以把撤销动作push到远程分支,共享给其他开发者。
随意移动commit
1. cherry-pick
这命令名字很长但其实很简单,就是把指定的几个commit拷贝一份,append到HEAD后面,比如:
$ git cherry-pick C2 C4
2. rebase -i
该命令提供了一个交互式的界面(说是界面其实用VI打开个文本文件)让你整理最近的若干次提交,你可以在这里对 commit 进行删除、合并、重排序,是整理commit历史的利器。
比如你现在有3次提交,分别是first/second/third,third是最后一次提交。输入以下命令:
git rebase -i HEAD~3
HEAD~3
表明修改对象是从HEAD~3
开始(不包括自己)到HAED
的3个commit。
然后出来一个文本文件让你编辑,怎么编辑有注释告诉你:
pick 65bc434 first
pick 86253cb second
pick b2756fa third
# Rebase ab14c02..b2756fa onto ab14c02 (3 command(s))
#
# Commands:
# p, pick = use commit
# r, reword = use commit, but edit the commit message
# e, edit = use commit, but stop for amending
# s, squash = use commit, but meld into previous commit
# f, fixup = like "squash", but discard this commit's log message
# x, exec = run command (the rest of the line) using shell
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
# Note that empty commits are commented out
假如我们想把second和third合并为一个commit new ,再把first丢在new的后面,应该这么搞:
pick 86253cb second #选择second
s b2756fa third #s:合并到上一个commit
pick 65bc434 first #选择first
整个过程其实是 先将分支回退到指定位置(这里是HEAD~3
)然后依次执行上面的命令:第一二行把second和third合并为一个提交,并让你编辑这个新提交的commit message:
第三行提交first,同样也能编辑commit信息,截图就不放了。
远程分支是远程版本库中分支在本地的镜像,反应了远程库中分支的状态,是本地库与远程库交互的桥梁。它们和本地分支没什么不同,只不过在某种意义上远程分支是“只读”的:你通常不会手动去操作远程分支,只会通过与服务器的“推送”和“拉取”动作来更新它们;而且也无法把HEAD
指针和远程分支绑定(attach)在一起。
git clone
从远程主机中克隆一个版本库到本地,git 将做以下动作:
远程主机名/分支名
,如origin/master
);可以用git branch -r
查看远程分支,-a
查看所有分支:
> git branch -a
* master
remotes/origin/HEAD -> origin/master
remotes/origin/Task_Description_Display
remotes/origin/Task_Description_Edit
remotes/origin/master
以一个远程分支为基准创建本地分支的方式和之前一样:
> git checkout origin/dev #移动HEAD指针到远程分支(进入detached HEAD模式)
> git branch dev #创建新分支dev
git remote
用来管理远程库,不太常用,一般用默认的 origin 就够了:
> git remote show <远程库> #看地址
> git remote add <远程库> <地址> #加
> git remote rm <远程库> #删
> git rename <远程库> <新名字> #重命名
如果远程库有了更新,可以用git fetch
将更新拉取到本地,并将对应的远程分支(即 origin/master 等指针)移动到最新位置。
> git fetch <远程库> # 取所有分支的更新,如果远程库有新的分支,本地会创建对应的远程分支
> git fetch -p <远程库> # 同上,但会删除远程库中已不存在的远程分支,p==prunge
> git fetch <远程库> <分支名> # 取特定分支的更新
git fetch
只会更新远程分支,不会影响本地分支。
git pull
== git fetch
+ git merge
:
先从远程库拉取数据更新某个远程分支,再与指定本地分支进行merge。完整格式:
> git pull <远程库> <远程分支>:<本地分支>
> git pull origin next:master # 更新origin/next,与master分支合并
如果省略<本地分支>
(git pull origin <远程分支>
),则与当前分支合并。如果当前分支与远程分支是 tracking关系,则git pull
即可,省略所有参数。
Tracking Branch
本地分支和远程分支间可以建立一种跟踪(Tracking)关系,这样的本地分支被称为 Tracking Branch。在跟踪分支上进行
git pull
或git push
,Git会自动判断应向远程库中的哪个分支拉取/推送数据。
git clone
时会自动为 新建的本地分支 与 对应远程分支 之间建立跟踪关系,这也是为什么克隆完成后git pull/push
直接可用的原因。可以用下面的命令手动建立跟踪关系:
> git branch -u <远程分支> <本地分支> 或者 > git branch --set-upstream-to=<远程分支> <本地分支>
如果合并远程分支时不想用默认的merge
而是rebase
,可以加上--rebase
参数:
> git pull --rebase <远程库> <远程分支>:<本地分支>
将某个本地分支上的更新推送到远程库中的某个分支,完整格式:
> git push <远程库> <本地分支>:<远程分支>
如果省略<本地分支>
(git push origin <远程分支>
),则把当前分支推送到远程库中的指定分支,无则新建。如果当前分支有且只有一个跟踪的远程分支,不带参数的git push
即可。
git push
还用来删除远程库中的分支,方法是将一个空白的分支推送到指定分支:
> git push origin :master #把远程库中的master分支删掉
等同于
> git push origin --delete master
这个需求很常见而命令又很奇怪,很容易忘记。
不带参数的git push
默认只推送当前分支,这成为“simple”方式;还有一种“matching”方式推送所有分支。2.0后默认是simple,可以用如下命令更改:
> git config --global push.default simple
如果远程库中分支的版本比本地更新,push时会报错,必须先在本地fetch,解决冲突并合并后再push。
加-f
(force)选项可以用本地分支强制覆盖远程库中的分支。在整理提交历史时这个选项很有用,比如你刚做了两次提交并把它们push到了远程库中,现在你想把它们合并为一次,对本地分支可以用git rebase -i
或者git reset
达到目的,但你无法把合并后的commit推送到远程库,这时可用-f
把你整理后的本地分支强制推送过去。
和传统SVN类似,只有一个远程库,开发者把库克隆到本地,进行修改再推送回去。库有更新了则先拉取下来进行合并,有冲突则解决冲突。
简单实用,小型团队这么干就够了。
在Forking工作流下,通常有一个由专人维护的官方仓库,开发者们没有官方仓库的push权限,而是先从官方仓库fork一份私有库,开发工作都在该库上进行,开发完毕后向官方仓库发起一个pull request,请求从自己的私有库中拉取更新合并到官方库。
Forking工作流的一个主要优势是不用开放官方仓库的push权限给第三方。开发者push到自己fork出来的私有库,只有项目维护者才能push到正式仓库。这样项目维护者可以接受任何开发者(包括不受信的第三方)的提交,而不用开放官方仓库的写权限给他。
Step1:维护者建立官方仓库,假设只有一个分支master
Step2:开发者fork一个自己的私有库,一般会在fork时勾选同步,这样Git服务器(github或stash)会自动将官方仓库的更新同步过来:
Step3:开发者clone自己fork的库,并在新分支上(如dev)进行开发工作,开发完毕将其push到私有库的dev分支,私有库的master分支用于同步官方仓库,不直接修改:
Step4:开发者创建一个pull request,通知维护者将自己私有库中的分支合并到官方仓库中:
Stash 中创建pr的表单如下,你需要指定要将自己库中的哪个分支推送到官方库中的哪个分支,本例中是dev推送到master:
Step5:官方仓库的维护者收到pr后决定是否接受。Stash提供了GUI界面,维护者可以直接在网站中查看pr的代码修改、与开发者进行讨论,最后执行合并。接受pr的效果相当用git pull
命令拉取开发者仓库分支(dev)并merge到官方仓库分支(master),这也是 “pull request” 名称的由来。如果出现了冲突,维护者必须在本地从开发者仓库中fetch分支,合并到本地分支master,并解决冲突,最后将代码push到官方仓库:
# 拉取开发者仓库中的分支
> git fetch https://bitbucket.org/user/repo feature-branch
# 查看变更
> git checkout master
# 解决冲突并合并
> git merge FETCH_HEAD
> git push origin master
Step6:官方仓库的master分支向前走了,开发者私有仓库中的master分支会自动同步,开发者将最新代码pull到本地:
nfig --global color.ui true # git status等命令自动着色
git config --global color.status auto
git config --global color.diff auto
git config --global color.branch auto
git config --global color.interactive auto
git clone git+ssh://[email protected]/VT.git # clone远程仓库
git status # 查看当前版本状态(是否修改)
git add xyz # 添加xyz文件至index
git add . # 增加当前子目录下所有更改过的文件至index
git commit -m 'xxx' # 提交
git commit --amend -m 'xxx' # 合并上一次提交(用于反复修改)
git commit -am 'xxx' # 将add和commit合为一步
git rm xxx # 删除index中的文件
git rm -r * # 递归删除
git log # 显示提交日志
git log -1 # 显示1行日志 -n为n行
git log -5
git log --stat # 显示提交日志及相关变动文件
git log -p -m
git show dfb02e6e4f2f7b573337763e5c0013802e392818 # 显示某个提交的详细内容
git show dfb02 # 可只用commitid的前几位
git show HEAD # 显示HEAD提交日志
git show HEAD^ # 显示HEAD的父(上一个版本)的提交日志 ^^为上两个版本 ^5为上5个版本
git tag # 显示已存在的tag
git tag -a v2.0 -m 'xxx' # 增加v2.0的tag
git show v2.0 # 显示v2.0的日志及详细内容
git log v2.0 # 显示v2.0的日志
git diff # 显示所有未添加至index的变更
git diff --cached # 显示所有已添加index但还未commit的变更
git diff HEAD^ # 比较与上一个版本的差异
git diff HEAD -- ./lib # 比较与HEAD版本lib目录的差异
git diff origin/master..master # 比较远程分支master上有本地分支master上没有的
git diff origin/master..master --stat # 只显示差异的文件,不显示具体内容
git remote add origin git+ssh://[email protected]/VT.git # 增加远程定义(用于push/pull/fetch)
git branch # 显示本地分支
git branch --contains 50089 # 显示包含提交50089的分支
git branch -a # 显示所有分支
git branch -r # 显示所有原创分支
git branch --merged # 显示所有已合并到当前分支的分支
git branch --no-merged # 显示所有未合并到当前分支的分支
git branch -m master master_copy # 本地分支改名
git checkout -b master_copy # 从当前分支创建新分支master_copy并检出
git checkout -b master master_copy # 上面的完整版
git checkout features/performance # 检出已存在的features/performance分支
git checkout --track hotfixes/BJVEP933 # 检出远程分支hotfixes/BJVEP933并创建本地跟踪分支
git checkout v2.0 # 检出版本v2.0
git checkout -b devel origin/develop # 从远程分支develop创建新本地分支devel并检出
git checkout -- README # 检出head版本的README文件(可用于修改错误回退)
git merge origin/master # 合并远程master分支至当前分支
git cherry-pick ff44785404a8e # 合并提交ff44785404a8e的修改
git push origin master # 将当前分支push到远程master分支
git push origin :hotfixes/BJVEP933 # 删除远程仓库的hotfixes/BJVEP933分支
git push --tags # 把所有tag推送到远程仓库
git fetch # 获取所有远程分支(不更新本地分支,另需merge)
git fetch --prune # 获取所有原创分支并清除服务器上已删掉的分支
git pull origin master # 获取远程分支master并merge到当前分支
git mv README README2 # 重命名文件README为README2
git reset --hard HEAD # 将当前版本重置为HEAD(通常用于merge失败回退)
git rebase
git branch -d hotfixes/BJVEP933 # 删除分支hotfixes/BJVEP933(本分支修改已合并到其他分支)
git branch -D hotfixes/BJVEP933 # 强制删除分支hotfixes/BJVEP933
git ls-files # 列出git index包含的文件
git show-branch # 图示当前分支历史
git show-branch --all # 图示所有分支历史
git whatchanged # 显示提交历史对应的文件修改
git revert dfb02e6e4f2f7b573337763e5c0013802e392818 # 撤销提交dfb02e6e4f2f7b573337763e5c0013802e392818
git ls-tree HEAD # 内部命令:显示某个git对象
git rev-parse v2.0 # 内部命令:显示某个ref对于的SHA1 HASH
git reflog # 显示所有提交,包括孤立节点
git show HEAD@{5}
git show master@{yesterday} # 显示master分支昨天的状态
git log --pretty=format:'%h %s' --graph # 图示提交日志
git show HEAD~3
git show -s --pretty=raw 2be7fcb476
git stash # 暂存当前修改,将所有至为HEAD状态
git stash list # 查看所有暂存
git stash show -p stash@{0} # 参考第一次暂存
git stash apply stash@{0} # 应用第一次暂存
git grep "delete from" # 文件中搜索文本“delete from”
git grep -e '#define' --and -e SORT_DIRENT
git gc
git fsck