Git原理&常用命令
工作原理
常用配置&操作
工作原理
git 维护三棵树:
- working directory:工作目录
- stage(index):缓存
- history:commit 历史
可以参考这个 图解 Git 命令
获取/创建项目
init
1 | git init |
1 | git init <directory> |
1 | git init --bare <directory> |
初始化一个裸的(没有工作目录的) Git 仓库,中央仓库应该总是用 –bare 标记创建,因为向非裸仓库推送分支有可能会覆盖已有的代码变动。将 bare 看作是将仓库标记为储存设施而不是开发环境,中央仓库是裸仓库,开发者的本地仓库是非裸仓库。
clone
1 | # 将位于 <repo> 的仓库克隆到本地机器 |
LF CRLF
clone 下来的东西报 lf crlf 的错误,是由于 git clone 的时候自动进行了转换,可以如下设置关闭
1 | git config --global core.autocrlf false |
submodule
可以通过 git submodule 配置子模块,git 会在父级目录中用 .gitmodules 文件记录
配置
config
常见配置
1 | git config user.name <name> |
git 的配置项存储位置:
- /.git/config – 特定仓库的设置。
- ~/.gitconfig – 特定用户的设置。这也是
--global标记的设置项存放的位置。 - $(prefix)/etc/gitconfig – 系统层面的设置。
忽略特定文件
1 | git update-index --assume-unchanged PATH |
基本快照操作
add
将工作目录中的变化添加到缓冲区,缓存更改
1 | git add <file> |
commit
将缓存的快照提交到项目历史
1 | # 运行文本编辑器,等待输入提交信息 |
status
1 | git status |
文件状态:
- modified, to be commited:已缓存
- not staged for commit:未缓存
- untracked:未追踪
rm
1 | # = rm 且默认(-f)add file |
mv
1 | # 重命名一个文件 |
通过 git mv 去移动相当于自动 add 到 stage 中,不用手动 git add
reset
改变 HEAD 指向的 commit,并有选择地变动 working directory 和 index;也用来在从 commit 历史中复制文件到 index,而不动 working directory
reset 可以理解为重设,而 revert 是撤销:reset 移除掉后面所有的 commit,如果想恢复只能自己重新提交,这个重设是永远的,改了就再也找不回来了,所以要小心,最好只用在本地修改
1 | # 当前 HEAD 指向 <commit> |
restore
可以通过 git reflog 查看日志,找到 reset 前的 commit,然后用 restore 将 working directory 恢复(但是commit 不会恢复,要重新提交)
分支与合并
branch
1 | # 列出(本地)仓库中所有分支 |
Git 的分支实际上是指向 commit 的指针,代表了一系列 commit 的顶端,而不是 commit 的容器。分支历史通过提交之间的关系来推断
所以 Git 的合并其实是将两个独立的 commit 历史连接起来,而不是基于文件的操作
因为分支本质是个指针,所以创建/删除分支都不会影响到仓库历史
删除分支时 Git 会检查这个分支是否已经被合并,如果还没被合并就删除,那就相当于丢失了这段开发线的入口(指针),所以 Git 会做出错误提示
checkout
从 commit history 或 index 中拷贝文件到 working directory;或切换分支
1 | # 从指定的 commit 中拷贝文件到 working directory |
项目中可能出现的情况
由于常用的npm run dev 一般是增量编译配置,所以有时候在一个分支编译完 checkout 到另一个分支,但 /dist 里的东西由于被 ignore 所以还保留着原分支的一些信息,最好 checkout 之后 rm /dist 再重新编译
merge
1 | # 将指定分支并入当前分支 |
当前分支会被更新,但目标分支完全不受影响
合并算法:
- 快速向前合并:当当前分支顶端到目标分支路径是线性时,Git 只需要将当期分支顶端(快速向前地)移动到目标分支顶端,即可整合两个分支的历史,而不需要“真正”合并分支。在效果上合并了历史,因为目标分支上的 commit 现在在当前分支可以访问到
- 三路合并:使用一个专门的 commit 来合并两个分支的历史(Git 实际上使用三个 commit 来生成它,分别是两个分支顶端和它们共同的祖先)
通常用快速向前合并搭配 rebase 来合并微小的功能或者修复 bug,使用三路合并来整合长期运行的功能
解决冲突
手动解决:merge 发现错误 -> 自己修复 -> git add -> git commit 生成合并提交
stash
1 | git stash |
把现在的 working directory 和 index 存起来,但是不 commit 到 history 以免污染历史
找回不小心 pop/drop 掉的 stash
出处
1 | gitk --all $(git fsck --no-reflog | Select-String "(dangling commit )(.*) |
之后会打开一个 GUI 界面,在里面找到你丢失掉的那个 commit 值之后 git stash apply <commit> 即可
tag
通常在发布版本时打一个 tag,tag 会记录版本的 commi 号,方便后期回溯
1 | # 列出已有 tag |
分享更新项目
remote
管理与其他仓库之间的连接,用书签(别名)的方式去引用其他仓库的 url 连接
1 | # 列出远程连接列表 |
git clone 时,自动创建了一个名为 origin 的远程连接,指向被克隆的 repo(基本上都是中央仓库)
http 或 ssh 协议
fetch
将远程仓库的提交拉到本地仓库,拉下来的提交储存为远程分支,而不是我们一直用的普通的本地分支
1 | # 拉取仓库中的所有分支 |
查看拉下来的远程分支时,会像 checkout 一样处于分离 HEAD 状态(基本上可以视作只读)
如果接受远程分支包含的更改,可以用 git merge 将它并入本地分支
pull
= fetch + merge
1 | # = fetch + merge origin/. |
push
将提交导出到远程分支
1 | # 将指定的分支推送到 remote 上 |
push 其实相当于在远程仓库内部运行 git merge
只应该推送到那些用 –bare 初始化的仓库(裸仓库,没有工作目录)。因为 push 会弄乱远程分支结构,所以永远不要推送到其他开发者的仓库
开发标准做法
1 | git checkout master # 参考 checkout 中的解释,注意并不会造成工作丢失! |
审查比较
diff
行号
git diff 不能直接得到行号信息,而是会给出一个 unified-diff format 格式的东西:
1 | @@ -start,count +start,count @@ |
- 标记的代表原状态的信息,+ 标记的代表现状态的信息
start 代表原状态/现状态的开始行号,count 代表从 start 开始有多少行被修改了
实际数起来是这样的(例子来自 stack overflow 的这个回答)
1 | diff --git a/osx/.gitconfig b/osx/.gitconfig |
这里的 11 代表下面显示的这些代码从第 11 行开始,包括 7 行,所以原文件中的行号其实是这样的
1 | 11 [color "branch"] |
所以实际被修改的是第 14 行。
打补丁
rebase
将分支移到一个新的基 commit 的过程
rebase 这个“基”,指的是三路合并时的那个基 commit
从内容的角度看,rebase 只不过是将分支从一个 commit 移到了另一个;但是从内部机制来看, Git 是通过在选定的基上创建新 commit 来完成这件事的,事实上重写了项目历史
1 | # 将当前分支 rebase 到 base |
rebase 的主要目的是为了保持一个线性的项目历史
将自己的 feature 分支整合进 master 分支,有两个选择:
- 直接 merge,会产生一个三路合并和一个合并提交
- 先 rebase 到 master HEAD 再 merge,产生一个快速向前的合并以及完美的线性历史
rebase 是将上游更改合并进本地仓库的通常方法,“我想将我的更改建立在其他人的进展之上”的感觉
本质上是线性化的自动的 cherry-pick
revert
1 | # 生成一个撤销了 commit 引入的修改的新提交,然后应用到当前分支 |
应该用在想要在项目历史中移除一整个提交(通常是因为这个提交造成 bug)的时候
被撤销的提交依然在项目历史中,git revert 在后面增加了一个提交来撤销修改,而不是删除它
cherry-pick
“复制一个 commit 节点并在当前分支做一次完全一样的新提交
debug
blame
1 | git blame -- <file> |
显示该文件的修改,以及每个修改的作者
grep
打印出符合指定 pattern 的行数
1 | git grep <pattern> -- <files> |
bisect
1 | git bisect <subcommand> <options> |
用二分查找的方式找出 bug 来自于哪个 commit,具体见 官方文档
管理
reflog
查看日志