Git 学习记录
工作里习惯了通过 Fork(app)使用 Git,这两个月的时间里开始熟悉 Git 的命令行客户端。虽然命令行在视觉上不如 UI 直观,但命令行可以保持双手持续输入不离开键盘,熟悉命令行也更利于寻找最适合自己的 GUI。
另外,界面的直观指的是个人使用上的直观。界面上的信息琳琅满目,鼠标指向四面八方,当某个人不熟悉界面的时候,我只能当面告诉他如何执行这些 git 指令:“点这里,这里,这里,往上一点,点顶部菜单栏左侧第二个按钮”,这不如命令行直观。
下面的内容包括了 Git 常用的命令,以及各命令或详或简但有必要的解释说明,其中也包括了很多相关的引用链接用于扩展阅读。
推送和拉取
推送:
1 | # 推送远程,并设置当前分支的 upstream 为 main,设置之后每次推送当前分支只需要 git push 即可 |
拉取:
1 | git checkout -b <local-branch> origin/<remote-branch> # 拉取远程指定分支并检出 |
git push
的默认行为:默认行为根据 git 配置里的push.default
的值决定,在 git 2.0 之前的默认值是matching
,之后的是simple
,所以在 2.0 之后如果没有设置上游分支 upstream 就会报错。
push.default
的可选值:
- nothing,禁止
git push
,必须显式指定推送分支,比如git push origin master
; - current,把本地分支推送至远程,远程分支名和本地分支名相同;
- upstream,推送到 upstream 上;
- tracking,同上一个
upstream
,不推荐使用; - simple,推送到 upstream 上,但是要保证 upstream 的分支名和本地分支名相同;
- matching,推送所有的本地和远程相同名称的分支。
设置local
(仓库)级别的默认推送行为:
1 | git config push.default nothing |
相关链接:
- Git push与pull的默认行为:解释了什么是
upstream
、downstream
; push.default
:社区文档的解释;- What is the difference between ‘git pull’ and ‘git fetch’?:
git pull
和git fetch
的区别,git pull = git fetch + git merge FETCH_HEAD
。
分支
查看分支:
1 | # 查看所有分支 |
删除分支:
1 | # 删除本地分支(删除不了未合并过的分支) |
检出远程分支:
1 | git checkout -b <local-branch> origin/<remote-branch> # 拉取远程指定分支并检出 |
重命名分支(本地):
1 | # 重命名当前分支 |
克隆
1 | git clone git@github.com:wswmsword/git-learning.git |
克隆所有分支,并切换到指定分支:
1 | git clone -b main git@github.com:wswmsword/git-learning.git # 克隆所有分支,并切换到 main |
克隆指定分支:
1 | git clone -b main --single-branch git@github.com:wswmsword/git-learning.git # 克隆 main |
克隆指定文件夹(git 2.25+ (Q1 2020)):
1 | mkdir git-sparse-checkout && cd git-sparse-checkout # 创建并进入文件夹 |
旧版本的 git 克隆指定文件夹:
1 | mkdir git-sparse-checkout-old && cd git-sparse-checkout-old |
相关链接:
- How to disable sparse checkout after enabled?:打开稀疏检出后如何关闭?回答中有使用老版本的 git 关闭答案。回答中提到了一个脚本。
- sparse_checkout.sh:第一条中提到的脚本。
- sparse-checkout:git 的稀疏检出社区文档。
提交
暂存:
1 | git add . # 暂存所有改动文件 |
提交:
1 | # 提交,添加信息 |
检出某一条提交记录:
1 | git checkout <commit-hash> |
git add -A
、git add .
、git add -u
的区别(Git 2.x):
命令(Git 2.x) | 新文件 | 修改的文件 | 删除的文件 | 描述 |
---|---|---|---|---|
git add -A | √ | √ | √ | 暂存所有(新的、修改的、删除的)文件 |
git add . | √ | √ | √ | 暂存所有(新的、修改的、删除的)文件 |
git add -u | × | √ | √ | 暂存修改的和删除的文件,不暂存未跟踪的新文件 |
git add –ignore-removal . | √ | √ | × | 暂存新文件和修改的文件,不暂存删除的文件 |
在 Git 2.0 之前的版本,git add .
不能暂存已删除的文件。
相关链接:
- Git 2.0, git add -A is default:git 源码里的文档,提到了
git add <path>
在 Git 2.0 版本之后,执行的效果和git add -A <path>
相同; - What’s the difference between
git add .
andgit add -u
?。 - git add .与git add -A的区别(版本不同时候的区别):1.x 和 2.x 版本之间 add 命令的区别。
合并
1 | # 指定分支合并到当前分支 |
合并其它分支的一个文件:
1 | git checkout --patch testing-merge-file features/album/audio-formats # 合并 testing-merge-file 分支的 features/album/audio-formats 文件 |
执行完上面的命令后进入优选项的交互界面,选择 e(manually edit the current hunk),进行文件的冲突编辑,最后保存,文件会在暂存区内。
如果命令中不加--patch
,执行完命令的行为是当前分支的文件被指定分支的文件覆盖:
1 | git checkout testing-merge-file features/album/audio-formats # 当前分支的文件 features/album/audio-formats 被 testing-merge-file 分支的覆盖 |
关于git merge <branch>
在fast-forward
情况下和git rebase <branch>
的区别:
目标分支超过主分支,并且主分支没有新的提交记录,
1 | *E-*F-*G-*H-*I BRANCH |
,fast-forward
和git rebase
合并的效果一样,
1 | *A-*B-*C-*D-*E-*F-*G-*H-*I MAIN | BRANCH |
,当主分支有了新的提交(J、K),
1 | *E-*F-*G-*H-*I BRANCH |
,就无法使用fast-forward
,合并之后必须有一个单独的合并节点,但是git rebase
仍然可以合并,并且能保持提交线的整洁,
1 | *A-*B-*C-*D-*J-*K-*E'-*F'-*G'-*H'-*I' MAIN | BRANCH |
,有冲突的节点因为解决冲突会生成新的commit-id
,有相同提交的记录会被目标分支的节点覆盖,rebase
的节点也不一定是连续的,可能会穿插到主分支已经提交的节点中(按提交先后)。
关于三方合并:
1 | master |
在这样的分支结构中,要把iss53
合并到master
,要使用C4
、C5
和公共祖先C2
,这三个节点做一个三方合并。
三方合并有利于自动合并:例如,节点 C2 有文件 F 包含内容
1 | 一 |
,节点 C4 的文件 F 包含内容
1 |
|
,节点 C5 的文件 F 包含内容
1 | 一 |
,现在把iss53
合并至master
,因为三方合并,文件 F 的内容变成了
1 |
|
,因为 C4 在 C2 的基础上清空了第一行,C5 在 C2 的基础上填补了第三行,C2、C4、C5 都有第二行,所以合并的结果是清空第一行、保留第二行和填补第三行,这其中需要公共祖先 C2 的参与。
提示:当合并解决完冲突后,修改提交信息,“添加一些细节给未来检视这个合并的读者一些帮助,告诉他们你是如何解决合并冲突的,以及理由是什么”。
相关链接:
- 合并 (版本控制):维基的合并解释,包括三方合并;
- 怎么理解Git里的 “three-way merge” ? - Lazykid的回答 - 知乎:三方合并解释;
- How to merge specific files from Git branches:stackoverflow 的推荐回答;
- 3.2 Git 分支 - 分支的新建与合并:社区文档;
- Git Merge Fast-Forward vs Git Rebase:StackOverflow 上关于
fast-forward
和rebase
区别的答案。
变基 rebase
首先要提一下 rebase 的意思,我擅自的直譯是「重新 (re-) 定義某個 branch 的參考基準 (base)」。把這個意思先記起來,比較容易理解 rebase 的運作原理。就好比移花接木那樣(稼接),把某個樹枝接到別的樹枝。——Git-rebase 小筆記
利用变基合并几条提交记录:
1 | git rebase -i <commit_id_start> <commit_id_end> # 前开后闭 |
下面是利用变基合并 5 条记录的例子:
合并22bafc6
以上的记录至4f4a863
共 5 条,执行上面的命令后(这里是git rebase -i 22bafc6 4f4a863
),默认编辑器将打开类似下面的文件:
1 | pick 478a9ab add album/history |
编辑,将前 2-5 行的pick
改为s
(s 代表上面注释里的 squash),表示这 4 条记录将合并到第一条记录中,最后保存:
1 | pick 478a9ab add album/history |
然后,默认编辑器会进入第二个文件,用于修改合并这 5 条记录的提交信息。
进行编辑,在第一行添加提交信息(add album features
),最后保存:
1 | add album features |
完成上面的步骤之后,就生成了一条记录,这条记录包含了原分支从22bafc6
到4f4a863
的 5 条记录,git 的HEAD
(头指针)会指向这条提交记录上。但是现在,这条记录不属于任何分支,所以一旦切换分支,这条记录就会丢失。
如果要把这条记录合回原分支:
1 | git checkout -b rebased-album-features # 从 HEAD 检出分支 |
变基代替合并:
1 | git rebase feature-album # 分支 feature-album 变基到当前分支 |
相关链接:
文件的四种状态
Untracked
、Staged
、Unmodified
、Modified
- Untracked(未跟踪)
- 第一次新建的文件,没有
git add
过的文件,或者git rm --cached
过的文件;
- 第一次新建的文件,没有
- Staged(暂存)
git add
过的文件都是staged
状态;
- Unmodified(未修改)
git commit
后的文件是未修改状态;
- Modified(已修改)
git commit
之后,对文件进行修改,当前为已修改状态。
也有文档解释中多出第五种状态—:“已提交(committed)”,已提交相当于Unmodified
未修改状态。
日志
简洁日志,包括短 id 和提交信息:“git log --oneline
”。
图形形式的提交记录,包括短 id,提交信息,相对时间,作者名称:
1 | git log --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit |
用关键字过滤日志:
1 | git log --all --grep="microsoft" # 查询包含“microsoft”的提交记录 |
使用reflog
查看操作历史(reflog
是本地的,无法通过git pull
得到):
1 | git reflog |
1 | # 查看某一个文件的提交记录 |
git log
其它配置的命令:
1 | git log -p [<file-name> | <commit>]# 显示差异 |
format
的参数:
- %h:commit hash
- %ai: author date
- %an: author name
- %ci: commit date
- %cn: commit name
- %s: log message
前面提到的图形形式的提交记录命令很长,如果常用,为它设置别名会更方便:
1 | git config --global alias.gg "log --color --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit" |
,这样设置后再执行命令git gg
就相当于执行了git log --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit
。
相关链接:
忽略文件
有两种忽略文件或文件夹的方法,一,编辑项目内的.git/info/exclude
文件,在文件里添加需要忽略的文件或文件夹,二,在项目里添加.gitignore
文件,在文件里添加需要忽略的文件或文件夹。
第一种编辑.git/info/exclude
的方法适合忽略偏个人的、不普遍的文件。例如,如果项目中每位开发者的开发工具不同,而有的开发工具会生成各自的配置文件,这样的配置文件可能不适合放在.gitignore
中,否则多一个不同的开发工具就会多一条忽略记录。
第二种创建.gitignore
文件的方法适合忽略通用的文件。例如*.log
之类的日志文件。.gitignore
可以被创建在不同的文件夹里,同时也会被当作项目的一部分,被 Git 跟踪管理。
创建.gitignore
文件,在其中加入要忽略的文件或文件夹。
1 | .DS_Store |
相关链接:
比较差异
1 | # 对比暂存区差异 |
diff 工具
- 下载 diff 前端,这里是 Meld(其它 diff 工具:Beyond Compare、Araxis Merge、DiffMerge)
- 添加配置,编辑
~/.gitconfig
1 | [diff] |
这样设置后,执行git difftool
打开 diff 工具进行文件比较。
补丁
生成一个补丁文件,自己或者分享其他人使用,举例:
1 | git diff HEAD~ HEAD > patch-abc # 上次提交和倒数第二次提交的变更,生成补丁 |
这是生成的补丁文件,名称patch-abc
:
1 | diff --git a/.gitignore b/.gitignore |
1 | git checkout HEAD~ # 把头移动到倒数第二次提交 |
标签
标签分为“附注标签”和“轻量标签”,附注标签包含很多信息,例如标签的作者、邮箱、日期时间等,而轻量标签只是简单的引用,是指向标签所在 commit 的指针。
1 | # 查看本地标签 |
点击查看开源项目 React 创建的所有标签。
贮藏
1 | # 查看贮藏列表 |
旧版本的git stash save
从 Git2.16.0 起被弃用,点击查看。
相关链接:“What’s the difference between git stash save and git stash push?”。
遴选
1 | # 遴选一条记录 |
什么是遴选:在下面的分支图里,如果 MAIN 分支需要 BRANCH 分支的节点H
的改动,可以通过遴选
把节点H
添加到节点E
的后面。
1 | *F-*G-*H-*I BRANCH |
从 BRANCH 分支遴选节点H
至主分支:
1 | *F-*G-*H-*I BRANCH |
相关链接:
回滚、撤销、重置
git reset
:
1 | git reset HEAD@{1} # HEAD@{1} 指的是执行`git reflog`后看到的记录 id,HEAD@{1} 是倒数第二条,HEAD@{0} 是倒数第一条,以此类推 |
git revert
:
1 | git revert <commit-id> # 反转并提交 |
撤销某个文件至指定 commit 时的状态:
1 | git checkout <commit-hash> -- <path/to/file> |
关于git reset
中的soft
、mixed
和hard
:
--soft
:当前(还没reset
)工作区和暂存区的改动不变,还在原位,reset
之后有了差异,差异在暂存区中;--mixed
:默认类型,reset
前后所有改动都移到工作区中,包括原来在暂存区的,也移到工作区中;--hard
:清空工作区和暂存区的改动,直接将 HEAD 指向reset
的指向,使用时要注意。
关于工作区、暂存区、History:工作区修改,add
至暂存区,commit
至 History。
一些名词:
- 工作区:Working directory,Working Copy;
- 暂存区:Index,索引,Staging area,Stage/Index;
- History:HEAD 指的地方,提交记录的地方,Repository。
相关链接:
责怪
1 | git blame <file_name> # 查看文件每一行的修改记录,包括作者、日期和内容 |
类似 VSCode 上安装插件 GitLens 之后的效果,鼠标点击某一行后,会在那一行后面出现最近一次该行的提交信息,有时候会把这个信息当成代码注释。
GitLens 第 26 行的git blame -L 26,26 .internal/baseUniq.js
的效果:
配置
查看配置:
1 | git config --global --list # 查看配置表 |
修改用户名和邮箱:
1 | git config user.name "wswmsword" # 设置用户名(local 级别) |
配置文件的级别:system
、global
、local
。
配置默认编辑器:
1 | # 常用编辑器:emacs / nano / vim / vi |
编辑配置文件:
1 | git config -e # 等价 vim .git/config |
设置文件大小写敏感:
1 | git config --global core.ignorecase false # git 默认忽略大小写 |
别名
设置命令的别名:
1 | git config --global alias.haha "status -sb" # 相当于 git status -sb |
删除命令的别名:删除.git/config
里对应别名所在行,如果是全局的别名,删除~/.gitconfig
里对应别名所在行,或者:
1 | git config --global --unset alias.haha # unset |
自定义简短 log msg:
1 | git config --global --replace-all alias.lg "log --pretty=format:'%h %ad %an %s' --date=local" |
子模块
1 | git submodule add https://github.com/wswmsword/git-submodule lyrics # 添加一个子模块,路径是 ./lyrics |
1 | git submodule add ./submarines # 本地仓库添加子模块 |
1 | git submodule foreach "<git command>" # 对每个子模块执行 git 命令 |
1 | git clone --recursive https://github.com/wswmsword/git-learning # 克隆项目,递归克隆子项目 |
删除子模块:
1 | # step 1 |
相关链接:
- How to git submodule tutorial—— YouTube 视频;
- Git子模块—— CSDN 文章,提到了子模块更新父模块为更新的情况;
- 7.11 Git 工具 - 子模块—— Git 社区文档。
二分查找
使用git bisect
命令,用二分法找到第一条出错的提交记录。
1 | git bisect start <节点> <祖先节点> |
在最后一步标记后,git 会提示我们“
相关链接:
- Bisectercise——Github 仓库,克隆下来用于练习“git bisect”;
- git bisect 命令教程;
- git-bisect——社区文档。
GUI
修改文件名称的大小写
1 | git mv SONG song2 |
两个相同名称的文件夹:
如果通过git config --global core.ignorecase false
把文件名设置为大小写敏感后,修改本地文件夹song
为Song
,然后上传,远程仓库将存在两个名称相同、大小写不同的(song 和 Song)文件夹(git version 2.34.1, macOS 12.5 (21G72))。
相关链接:
- 被文件名大小写的问题搞晕了:V2EX 上关于 Git 文件名大小写修改的讨论。
其它
git show
,显示最近提交的信息,与之相关的其它命令:
1 | git show <commit> |
git restore --staged <file>
取消暂存。
Github 新创建仓库的 Git 拉取提示:
1 | echo "# git-learning" >> README.md # 创建一个文件,往里面加入文本 |
获取提交的数量:
1 | git rev-list --count HEAD |
获取所有分支数量:
1 | git branch --all | wc -l |
切换上一次的分支:
1 | git checkout - # 只需添加一个短杠,其它命令兴许也能用 |
查看文件的 SHA1 值:
1 | git ls-files --stage # 查看所有文件的 SHA1 值 |
git rm
git rm <file>
,撤销跟踪,并且删除指定文件。如果加上-r
参数(git rm -r <file>
),则删除文件夹。
git rm --cached <file>
,撤销跟踪,保留工作区的文件内容。git rm -f <file>
,强制删除文件。
不是所有的文件都需要 git 跟踪,例如项目编译之后的产包。如果产包因为误操作导致被跟踪了,就使用git rm
命令来取消跟踪,并把产包添加到.gitignore
来忽略跟踪。已跟踪的文件再添加到.gitignore
中不会生效。
相关链接:
- git忽略已经被提交的文件——segmentfault 的答案,除了使用
git rm --cached
还提供了其它方法取消跟踪已跟踪的文件。
git remote
1 | git remote -v # 显示所有远程仓库 |
引用
资料:
- Git 飞行指南
- Git Book
- Git 聊天入门
- Pro Git
- Git常用命令参考手册
- 廖雪峰 git 教程
- 跟我一起学 Git
- Git 权威指南
- Oh Shit, Git!?!
- Git常用命令整理
- Awesome Git
- 图解Git
- git-tips
其它链接:
- Git 实验仓库 - git-learning——我创建的用于执行本学习记录里每一条命令的 git 仓库;
- Setting up and using Meld as your git difftool and mergetool on a Mac;
- git-fire——在紧急情况下保存代码;
- Generating a new SSH key and adding it to the ssh-agent——GitHub 创建 SSH 文档。