前言
发现 git
这块所知甚浅,打算利用空闲时间好好学习一下;
git 整理
(DVCS Distributed version controll system 分布式版本控制系统)
git简介
- 直接记录快照,而非差异比较; 文件变化,保存一个指向这次快照的索引,文件没有变化,对上次保存的快照做一链接;
- 本地操作;
- SHA-1哈希值作为索引,校验保持数据完整性;
- 文件三种状态;
committed,modified,staged
;- committed: 已提交该文件已被安全的保存在本地数据库中;
- modified: 已修改修改了某个文件,还没有提交保存;
- staged: 已暂存已修改的文件放在下次提交时要保存的清单中;
git流转过程:
git的工作区域(modified),暂存区域(staged),本地仓库(committed)
git目录:保存元数据和对象数据库的地方
git clone 就是.git目录;
git clone --bare 新建的目录本身就是git目录;
基本工作流程:
工作目录中修改文件(modified)->对修改的文件进行快照,保存到暂存区域(staged)->提交更新,将保存在暂存区的文件快照永久转存到git目录中(commited);
git配置 .git/config
--global 表示更改的配置文件就是用户主目录下,以后所有的项目默认使用这里配置的用户信息,可去除表示特定的项目中运用;
git config --global user.name "John Doe"
git config --global user.email [email protected]
git config --list 查看配置信息;
git help 查看帮助信息;
git config可用于设置别名;
git config --global alias.br branch 设置branch的快捷名称;
即`git br 查看所有分支;`
git 基础
工作目录中初始化新仓库
git init : 对现有的某个项目开始用git管理,只需到此项目所有的目录执行;
git clone [url] newfolder:克隆url的文件;保存下载下来的所有版本记录,并从中取出最新版本的文件拷贝;
可自定义新建的项目目录;
自动使用默认的`master`和`origin`名字;
git add *.c 纳入版本控制,多功能命令;
记录每次更新到仓库
工作目录下的所有文件不外乎两种状态: 已跟踪
或未跟踪
;已跟踪指本来就被纳入控制的文件,状态可能为未更新,已修改,已暂存(unmodified,modified,staged)
;其他的所有文件属于未跟踪(untracked)
;
查看文件状态
git status
-
clean: 现在的工作目录是干净的,已跟踪文件没有修改过,没有出现未跟踪的新文件;
-
创建文件后:
untracked files
; 出现未跟踪文件; -
跟踪新文件: git add 文件 后,
Changes to be committed
new file
已被跟踪,暂存区,处于已暂存状态;提交后被留在历史记录中; -
暂存已修改文件: 修改已经跟踪的文件,会出现
Changes not staged for commit
modified
已跟踪文件的内容产生变化,但还没有暂存,需要运行git add 后出现:Changes to be committed
newfile or modified
表明已经暂存;此时再次修改后还需要git add;
添加至暂存区
git add
多功能命令,根据目标文件的状态不同,命令效果不同;
- 未跟踪过的文件标记为需要跟踪;
- 将已跟踪目标文件快照放入暂存区,git会暂存运行命令时的版本;
- 用于合并时把有冲突的文件标记为已解决状态;
忽略文件格式 .gitignore
- 所有空行或者以注释符号 # 开头的行都会被 Git 忽略。
- 可以使用标准的 glob 模式匹配 (shell 所使用的简化了的正则表达式)。
- *: 0个或多个任意字符; [abc]:匹配任意一个列在方括号中的字符,a,b,c; ?:只匹配一个任意字符;[0-9]两个字符范围内的都可以匹配;
- 匹配模式最后跟反斜杠(/)说明要忽略的是目录。
- 要忽略指定模式以外的文件或目录,可以在模式前加上惊叹号(!)取反。
# 此为注释 – 将被 Git 忽略
# 忽略所有 .a 结尾的文件
*.a
# 但 lib.a 除外
!lib.a
# 仅仅忽略项目根目录下的 TODO 文件,不包括 subdir/TODO
/TODO
# 忽略 build/ 目录下的所有文件
build/
# 会忽略 doc/notes.txt 但不包括 doc/server/arch.txt
doc/*.txt
# 忽略 doc/ 目录下所有扩展名为 txt 的文件
doc/**/*.txt
git diff
无参数比较的是修改之后还没有暂存起来的变化;
git diff --cached 查看已经暂存起来的文件和上次提交时的快照之间的差异;
git commit -m “msg”
提交时记录的是放在暂存区的快照,没有暂存的仍然保持已修改状态,可以下次提交时纳入版本管理; 每一次提交都是对项目做个快照,可以回到这个状态或者进行比较;
跳过使用暂存区域 git commit -a -m "msg"
自动将素有已经跟踪过的文件暂存起来一并提交;
git commit --amend 修改最后一次提交,将使用当前的暂存区域快照提交,可以用于添加额外的文件或者提交信息写错了,较少commit节点;
git rm 移出文件;
必须从暂存区移除,然后提交; 如果直接删除文件 会出现changes not staged for commit
deleted
;
删除git仓库,但保留在当前工作目录中,以便加入到gitignore中;使用 git rm --cached 文件
git rm log/*.log
git rm *.~ (删除所有以~结尾的文件;)
git mv
git mv oldfile newfile 相当于
- mv oldname newname
- git rm oldname
- git add newname
git log && gitk
显示历史记录和图形化记录
git reset HEAD 取消已经暂存的文件;
- 取消暂存区域的文件;(一个是add 的撤销操作)
git reset HEAD 文件名
将暂存区的文件回到已修改未暂存的状态;
- 取消工作目录中已修改的文件;(一个是commit的撤销操作)
git checkout -- 文件名
取消对文件的修改,需要回到已修改未暂存状态,会吧之前对改文件的修改取消了;
git remote 远程项目的管理
git remote
列出每个远程库的简短名字(origin);
git remote -v
显示对应的克隆地址;
git remote add [shortname] [url]
添加一个新的远程仓库,指定一个简单名称;使用shortname指代对应的仓库地址;
git remote show [remote-name]
查看远程仓库信息;
git remote rename oldname newname
远程仓库的重命名;
git remote rm [remote-name]
远程仓库的删除
git fetch 从远程仓库中抓取数据
git fetch [remote-name]
此命令会到远程仓库中拉取所有你本地仓库中还没有的数据; 只是将远端的数据拉取到本地仓库,不会自动合并到当前工作分支;
git pull
自动抓取数据,git fetch + git merge
git push 推送数据到远程仓库
git push [remote-name] [branch-name]
将本地的branchname分支推送到远程的origin服务器上;
git tag 打标签
git tag
显示现有的所有标签; 搜索加 (-l v1.4.2.*
)
- -l lightweight 轻量级的,指向特定提交对象的引用;
- -a annotated 含附注的,存储在仓库中的一个独立对象;
git tag v1.4
轻量级标记;
git tag -a v1.4 -m "my version 1.4"
打附注标签;
git tag -a v1.2 序列号
对某次提交打tag;
git push origin [tagname]
分享某个标签到远程;
git push origin --tags
推送所有的标签到远程;
git show
查看相关标签信息;
git tag -s v1.2 -m "msg"
签署标签,使用GPG来签署标签;
git tag -v [tag-name]
验证标签;
git分支的原理简介
因为git保存的是一系列文件快照;
git commit
新建一个提交对象前,git会对每一个文件计算校验和
(SHA1)将当前版本的文件快照保存在git仓库中(blob对象);对每一个子目录计算校验和
,然后在git仓库中将这些目录保存为tree对象;之后创建的提交对象,除了包含相关提交信息以外,还包含着指向这个tree对象的指针;
-
blob对象: 表示文件快照内容;
-
tree对象: 记录着目录树内容及其中各个文件对应blob对象索引;
-
commit对象: 包含指向tree对象(根目录)的索引和其他提交信息元数据的commit对象;
修改后再次提交,提交对象会包含一个指向上次提交对象的指针(parent对象)
分支 本质上是一个指向commit对象的可变指针;
git 使用master作为分支的默认名称;在若干次提交后,其实已经有一个指向最后一次提交对象的master分支,它在每次提交的时候都会自动向前移动;
git branch newbranchname
在当前commit对象上新建一个分支指针,但不切换至新分支;
HEAD 判断当前在哪个分支上工作
git保存了一个名叫HEAD
的特别指针; git中HEAD指向你正在工作中的本地分支的指针(当前分支的别名)
git checkout newbranchname
切换到其他分支上;
每次提交head都会随着分支一起向前移动,原master分支仍然指向原先git checkout时所在的commit对象;
若此时运行git checkout master
,head指向另一个分支;
将HEAD指针移回到master分支;
并把工作目录中的文件换成了master分支所指向的快照内容;
git commit 新建一个新的提交对象,向不同方向开发;
分支的新建和合并
git checkout -b newbranch
git checkout -d branchname
- -b : 新建一个分支,并切换head指向新建分支;(branch + checkout)
- -d : 删除一个分支;
clean 后的切换分支;
如果你的暂存区或者工作目录中,有没有提交的修改,它会和你即将检出的分支产生冲突从而阻止git为你切换分支;保持clean后,可切换分支;
hotfix 分支从master分支所在点分化出来的;master是线上版本,iss53分支是当前正在开发的feature功能;
git checkout -b hotfix
,git checkout master
,git merge hotfix
Fast-forward
: 向前合并,由于master分支所在的提交对象是要并入hotfix分支的直接上游,git只需把master分支指针直接右移,这种单线的历史分支不存在任何需要解决的分歧,这种合并方式可以称为Fast forward;
合并之后,master分支和hotfix分支指向同一位置;删除不需要的hotfix分支(指针);
切换回正在开发的分支iss53,不受影响;如果想要纳入hotfix的修改,需要git merge master
or 等iss53完成后将iss53分支中的更新并入master;
分支的合并
git checkout master
,git merge iss53
Auto-merging
: 这次合并并不同于hotfix的合并;因为开发历史从更早的地方开始分叉的;由于master分支所指向的提交对象并不是iss53分支的直接祖先,git需要用两个分支的末端(C4,C5)以及他们公共的祖先(C2)进行一次简单的三方合并,红框是需要合并的提交对象;
不同方向的分支合并,需要对三方合并后的结果重新做一个新的快照,自动创建一个指向它的提交对象C6,此提交对象有两个祖先(C4,C5); 删除feature功能分支即可完成开发;
遇到冲突后的分支合并;
git status
查看合并冲突;
Automatic merge failed
,unmerged paths
- 冲突标记
===========
隔开冲突;<<<<<<<<HEAD
开始冲突,>>>>>>>iss53
与分支结束冲突地方;- 上方是
HEAD
(即master分支,当前分支,运行merge命令时所切换的分支)的内容; - 下方是iss53分支中的内容;
解决冲突之后,git add
再来一次快照保存到暂存区域,一旦暂存表示冲突已解决;
git mergetool
运行可视化的合并工具并引导你解决所有冲突;
总结merge
合并分为两种合并:fast-forward
和非共同祖先的三方合并;
fast-forward
: 单线的提交历史,如果低节点的master分支指针的想去高节点的dev分支指针,使用merge
命令 fast-forward 合并,指向同一个commit 提交对象节点;- 非共同祖先的两分支的合并,使用两分支的末节点和共同祖先的节点进行三方合并,产生一个新的虚拟节点; 可使用的命令为三个,
merge
,rebase
😭cherry pick
遴选节点再重演)
分支的管理
git branch
查看所有分支-v
查看各个分支最后一次的提交信息;--no-merged
查看尚未合并的分支;删除未合并的分支使用get branch -D name
强制删除;--merged
查看哪些分支已被并入当前分支(哪些分支是当前分支的直接上游,父提交对象),此时可将没有*
的分支删除掉;
利用分支进行开发的工作流程;(git flow 雏形)
- 长期分支
随着提交对象不断的右移指针,稳定分支的指针总是在提交历史中落后于前沿特性开发的指针;展现不同层次的稳定新,当前沿指针分支进入到稳定阶段,将之合并到更高层的分支中去;
- 特性分支
特性(Topic)分支,短期的用来实现单一特性,通常创建特性分支,提交若干提交对象更新后,合并到主干分支,然后删除特性分支;
新建多个特性分支,针对于不同提交对象的节点进行新建分支的功能尝试;
具体情景: 决定使用iss91v2
特性分支尝试的解决方案,并抛弃原来的iss91
分支的内容(抛弃C5,C6提交对象),直接在主干中并入另外两个分支,这些分支都是本地分支,完全不涉及与服务器的交互;
远程分支
对远程仓库中的分支的索引; 使用(远程仓库名/(分支名))
形式表示远程分支,如origin/master
;一次克隆建立本地分支和远程分支,都指向origin上的master分支;
多人开发中,远程仓库master向前推进,本地master也朝不同方向推进;
此时运行git fetch origin
同步远程服务器上数据到本地;找到origin是哪个服务器,获取未拥有的数据,更新本地的数据库,将origin/master指针移到它最新的位置上;
多个远程仓库,添加另一个远程仓库和拉取另一个远程仓库,在本地提交中新建一个指针;
推动本地分支 (分支全面: refs/head/分支名)
git push (远程仓库名) (分支名)
: 取出我的本地分支,推送到远程仓库分支,git分支名扩展为refs/head/localbranchname:refs/head/remotebranchname
;
git push origin branchname
== git push origin branchname:branchname
git push [远程名] [本地分支名]:[远程分支名]
git push origin branchname:awebranchname
推送本地branchname分支到远程仓库创建awebranchname分支;
git merge origin/master
合并远程分支到当前分支
git checkout -b branchname origin/branchname
在远程分支的基础上,分化出一个新的分支并切换; git checkout -b [本地分支名] [远程名/分支名]
== git checkout --track [远程名/分支名]
跟踪远程分支
从远程分支checkout 出来的分支被称为跟踪分支,是一种和某个远程分支有直接联系的本地分支;在跟踪分支中使用git push
or git pull
,git会判断应该向哪个服务器的哪个分支推送or拉取数据;
克隆仓库clone时,git自动创建一个名为master 的分支来跟踪origin/master,可使用--track
简化;
删除远程分支
git push [远程名] : [远程分支名]
删除远程服务器上的某个分支;
分支的变基 (NICE)
将一个分支的修改整合到另一个分支上有两种方法: rebase
,merge
基本的变基操作
git rebase --abort
变基冲突,回退所有的变基操作;
git rebase --continue
合并冲突后的继续变基;
如果使用merge
,三方合并(C2基,C3快照,C4快照),会产生一个新的基于两个祖先的提交对象节点(C5);
rebase
可以将C3里产生的变化补丁 在C4的基础上重新打一遍,即一个分支里提交的变化移到另一个分支里重新放一遍,git中操作叫变基
;
git checkout experment
,git rebase master
首先回到两个分支最近的共同祖先,根据当前分支(也就是要进行变基的分支experiment)后续的历次提交对象(C3,)生成一系列文件补丁,然后以基底分支(也就是主干分支master)最后一个提交对象(C4)为新的出发点,逐个应用之前准备好的补丁文件,最后会生成一个新的合并提交对象(C3`),从而改变experiment的提交历史,使它成为master的直接下游;
回到master分支,进行一次Fast-Forward
合并;
变基的好处
变基可以产生一个更为整洁的提交历史;一般使用变基的目的,是想得到一个能在远程分支上干净应用的补丁,按照每行的修改次序重演一遍修改;(实际上是把解决分支补丁
同最新主干代码
之间冲突的责任,划转为由提交补丁的人来解决;这样维护者不需要做任何整合工作,只需要根据你提供的仓库地址做一次快进合并,或者直接采纳你提交的代码)
情景分析
决定将client分支中的修改(C8,C9)并到master中,跳过server分支直接放到master分支中重演一遍,--onto
指定新的基底分支;
git rebase --onto master server client
: 取出client分支,找出client分支和server分支的共同祖先之后的变化,然后把它们在master上重演一遍;
快进合并master分支,包含client分支的变化;git checkout master
,git merge client
将server分支的变化也包含过来,将server变基到master git rebase master server
git rebase [主干分支] [特性分支]
不需要切换分支的情况下,取出特性分支server,在master分支上重演;
再次快进合并,删除特性分支 git branch -d client
,git branch -d server
变基的风险
准则:
一旦分支中的提交对象发布到公共仓库,就千万不要对该分支进行变基操作
在进行变基的时候,实际上抛弃了一些现存的提交对象而创造了一些类似但不同的新的提交对象;如果你将原来分支中的提交对象发布出去,并且其他人更新下载后再其基础上开展工作,而稍后你又用git rebase
抛弃这些提交对象,把新的重演后的提交对象发布出去的话,合作者们就必须重新合并他们的工作,当你再次从他们那获取代码时,就会一团乱;
如:获取远程仓库的代码,并提交几个提交对象;
合作者提交变化C2,合并他自己的分支提交对象C5得到新的提交对象C6,推送到远程;当你抓取并合并到本地的开发分支,显示:
接下来,推送C6的人决定用变基取代之前的合并操作;继而又使用git push --force
覆盖了服务器上的历史,得到C4`,而之后你再从服务器上下载最新提交后,得到:
你获取远程数据合并后,此时C4`和之前的C4的sha1值完全不同,git会当做新的提交对象处理,而C7中已经包含了C4的修改内容,于是合并操作会将C7和C4`合并成C8;
所以把变基当成一种在推送之前清理提交历史的手段,而且仅仅变基那些尚未公开的提交对象,就没问题;
绕过 clean 的切换分支;
- stashing commit amending;
git stash
git工作流
集中式工作流
一个中心库,开发者pull,push;
集成管理员工作流
github网站的开发流,每个开发者有自己的公共仓库,pull request由维护者处理你提交的更新;
司令官和副官工作流
大型项目使用,dictator(司令官)合并lieutenant(副官)的分支,副官合并零散的特性分支,司令将集成后的分支推送到共享仓库中,其他开发者以共享仓库为基础衍合;
衍合与挑拣(cherry-pick)的流程(对应于sourcetree的遴选)
查看不同分支diff命令
git diff master...contrib
...
语法,加在原始分支(拥有共同祖先)和当前分支之间;
cherry-pick
也可以保持线性的提交历史;类似于针对某次特定提交的衍合,首先提取某次提交的补丁,然后试着应用在当前分支上;如果某个特性分支上有多个commit,你只想引入其中之一就可以使用这种方法;
master中git cherry-pick e43a6fd3e94888d76779ad79fb568ed180e5fcdf
引入另一个分支挑拣一些提交对象进行应用;
交互式暂存
交互式命令,帮助你方便的构建至包含特定组合和部分文件的提交对象,确保你的提交在逻辑上划分为相应的变更集;
git add -i
or git add -interactive
git进入一个交互式的shell模式;
显示暂存区,类似与sourcetree中的挑选某些提交对象暂存;
储藏(Stashing)
-
git stash
不想提交你正在进行中的工作,所以储藏这些变更,往堆栈中推送一个新的储藏;执行后工作目录就clean了,可以方便的切换到其他分支上;你的变更都保存在栈上; -
git stash list
查看现有的储藏; -
git stash apply
默认应用最近储藏,git stash apply stash@{2}
应用指定的储藏;git stash apply --index
对文件的变更被重新应用,但是被暂存的文件没有重新被暂存,带–index 来重新应用被暂存的变更;
-
git stash drop
移除储藏; -
git stash pop
移除储藏,并应用储藏;
获取你工作目录的中间状态,也就是你修改过的被追踪的文件和暂存的变更(modified类型),并将它保存到一个未完结变更的堆栈中,随时可以重新应用;
取消储藏
git stash show -p stash@{0} | git apply -R
取消某个stash的应用;
git stash show -p | git apply -R
没有指定具体的储藏,会选择最近的储藏;
从储藏中创建分支
git stash branch branchname
创建一个新的分支,检出你储藏工作时的所处的提交, 重新应用你的工作,如果成功,将会丢弃储藏;可以恢复储藏的工作然后在新的分支上继续当前的工作;
重写历史
改变最近一次提交
- 改变提交说明
- 改变快照内容;
git commit --amend
修改最后一次提交说明
修改多个提交说明
可以使用交互式rebase
来衍合一系列的提交到它们原来所在的HEAD上而不是移到新的;
git rebase -i HEAD~3
指明你想要的修改的提交的父提交,衍合命令(HEAD~3…HEAD范围内的每一个提交都会被重写,不要涵盖已经推送到远程的提交,因为其他开发者可能基于之前提交作为基础开发;);
rebase顺序为 1,2,3,从上到下,最底部为最后一次提交对象;不同于log的记录顺序,最近的位于最上方;
-
p
pick: 使用当前commit; -
e
edit: 使用当前commit,停止修改; -
s
squash(压扁): 使用当前commit,和先前的commit合并;
将某次commit 前方的 pick 改为 edit ,可多次修正提交内容;之后多次
git commit --amend
, git rebase --continue
;完成对多个提交的修改;
重排提交和删除提交
修改commit的顺序和删除某些commit对象;直接使用vim 将多个文件交换位置,和删除某些节点;
squash压扁提交
交互式的衍合可以将一系列的提交压为单一提交; 指定某一提交前为squash
,git会同时应用那个变更和它之前的变更并将提交说明归并;
拆分提交
撤销一次提交,多次部分的暂存或提交直到结束;rebase -i
脚本中对commit对象使用edit
;
那里你可以用git reset HEAD^
对那次提交进行一次混合的重置,这将撤销那次提交并且将修改的文件撤回。此时可暂存并提交文件,直到你拥有多次提交,结束后,git rebase --continue
;
原历史记录
pick f7f3f6d changed my name a bit
edit 310154e updated README formatting and added blame
pick a5f4a0d added `cat-file`
退出命令后,先应用第一次提交f7f3f6d,后进入edit;
git操作;
$ git reset HEAD^
$ git add README
$ git commit -m 'updated README formatting'
$ git add lib/simplegit.rb
$ git commit -m 'added blame'
$ git rebase --continue
应用后最后一次提交a5f4a0d,提交历史:
$ git log -4 --pretty=format:"%h %s"
1c002dd added `cat-file`
9b29157 added blame
35cfb2b updated README formatting
f3cc40e changed my name a bit
与rebase一样,确保不包含已经推送到共享仓库的提交;
核弹级选项filter-branch
使用脚本的方式修改大量的提交;
从所有提交中删除一个文件
git filter-branch --tree-filter 'rm -f passwords.txt' HEAD
--tree-filter
每次检出项目时先执行指定的命令然后重新提交结果;
将一个子目录设置为新的根目录
git filter-branch --subdirectory-filter 子目录名 HEAD
全局性地更换电子邮件地址
$ git filter-branch --commit-filter '
if [ "$GIT_AUTHOR_EMAIL" = "schacon@localhost" ];
then
GIT_AUTHOR_NAME="Scott Chacon";
GIT_AUTHOR_EMAIL="[email protected]";
git commit-tree "$@";
else
git commit-tree "$@";
fi' HEAD
最后
项目还是推荐使用sourceTree
等图形化工具吧;
两个重点:
- git要常用
分支
; - 使用
rebase
-i
交互性的变基 可产生线性的提交历史,更直观;