「Git」从被动到主动
记录Git的常用命令及使用技巧。
常用命令
详细教程参考
初始化
1. 用户初始化
# system: `/etc/gitconfig`@linux, `path-to-install-git/gitconfig`@windows
git config --system user.name "yirami"
git config --system user.email "[email protected]"
# (✓) global: `~/.gitconfig`
git config --global user.name "yirami"
git config --global user.email "[email protected]"
# project: `path-to-project/.gitconfig`
cd `path-to-project`
git config user.name "yirami"
git config user.email "[email protected]"
# don't check file mode change
git config --add core.filemode false
# keep password
git config --global credential.helper store # forever
git config --global credential.helper cache # 15m by default
git config credential.helper 'cache --timeout=3600' # keep 1 hour- 自动切换用户
在不同的 Git 项目可以通过其下专有的 .gitconfig 文件来控制切换,但是如果项目很多,且都在某个文件夹下,那么也可以通过 includeIf 来控制切换配置。
假设有目录 ~/gerrit/,其下均是来自同一远程端的项目,需要统一的用户信息,则可以:
# 创建新配置并初始化用户信息
cat > ~/.gitconfig-gerrit << EOF
[user]
name = <your_special_name>
email = <your_special_email>
EOF
# 更新用户配置,使其在指定目录加载特定配置文件
# 注意:`~/gerrit/` 中结尾 `/` 号不能丢,否则可能误匹配以此开头的文件
cat >> ~/.gitconfig << EOF
[includeIf "gitdir:~/gerrit/"]
path = ~/.gitconfig-gerrit
EOF2. 密钥初始化
# 针对不同用途创建不同的密钥
ssh-keygen -t rsa -f ~/.ssh/id_rsa -C "[email protected]"
ssh-keygen -t rsa -f ~/.ssh/id_rsa_yirami -C "[email protected]"
ssh-keygen -t rsa -f ~/.ssh/id_rsa_singulato -C "[email protected]"
# 多密钥管理方式:
# 1) (×)ssh-agent:该方法为一次性的,重启shell需重新添加
# 2) (✓)所有平台使用同一个密钥,哈哈
# 3) 通过配置文件管理:
vim ~/.ssh/config
Host *
IdentityFile ~/.ssh/id_rsa
IdentitiesOnly yes
Host github
HostName github.com
User git
IdentityFile ~/.ssh/id_rsa_yirami
IdentitiesOnly yes
Host gitlab
HostName 10.2.2.11
User git
Port 8000
IdentityFile ~/.ssh/id_rsa_singulato
IdentitiesOnly yes
ssh -T [email protected] # testing
ssh -vT [email protected]3. 仓库初始化
mkdir project_name
cd project_name
git init
git init --bare # 裸仓库,仅存储历史本地操作
git stash | 暂存改动到草稿
git stash list # 查看当前的草稿栈 git stash push -m "desc" git stash pop # 弹出第一个,及stash@{0} git stash pop stash@{num} # 弹出第num+1个git worktree | 新建链接工作树
当需要同步开发多个分支时,可以创建多个工作树
- 相比使用 git stash,来回切换更加便利
- 相比使用 git clone,更加节省资源、速度更快,同时无需推送到远程仓库即可在多个工作树间同步改动
注意:
- 截止 $2025$ 年,包含
submodule的仓库,在使用git worktree时仍存在一些限制
- 每个
worktree中都需要独立初始化git submodule update --init- 在
submodule中不能再使用worktree- 无法在多个
worktree之间共享submodule的更新
- 如果各
worktree的submodule的checkout保持一致,可以共享- 否则,需要使用吸收命令(
git submodule absorbgitdirs) ,将子模块数据移动到各子模块目录中,此时可以避免worktree冲突,但也不再共享submodule的更新
``` shell
git worktree add ../folder_name branch_name # 在主仓库同级创建一个新的工作树并指向给定分支
git worktree list # 列出当前仓库关联的链接工作树
git worktree remove ../folder_name [--force] # 在主仓库中移除工作树
git worktree prune # 清理失效的工作树元数据,如工作树被强制删除文件目录、崩溃后留下的脏数据
# init pipline with submodule
cd ../folder_name
git submodule update --init
git submodule absorbgitdirs [submodule_name1 [submodule_name2] [...]]
```
git add | 将工作区文件添加到暂存区(stage)
git add . # 添加所有改动到暂存区 git add file_name # 添加某改动文件到暂存区git commit | 提交暂存区改动
git commit -m "..."git diff | 比较差异
git diff [file] # 比较工作区与暂存区的差异 git diff --staged [file] # 比较暂存区与HEAD的差异 git diff [first-branch]...[second-branch] # 比较两次提交的差异git show | 查看提交的改动
git show commit_hash # 查看commit_hash有什么改动 git show file # 查看当前提交中file有什么改动git branch | 编辑分支
git branch # 显示本地仓库所有分支 git branch -r # 显示远程仓库所有分支 git branch name # 创建name分支 git branch -d name # 删除name分支 git branch -m old new # 重命名分支 git branch -r -d remote_name/branch_name # 删除本地的远程分支引用git checkout | 切换或创建分支
因为
git checkout既能切分支,也能切文件,甚至还能创建新分支,功能复杂,因此Git团队将其功能拆出两个专门的命令:git switch和git restore。
``` shell
git checkout branch_name # 切换到分支branch_name
git checkout -b branch_name # 创建branch_name分支并切换过去
git checkout --orphan branch_name # 创建空分支
git checkout -- file_name # 撤销对file_name的修改(注意`--`与切换分支区分)
```
git switch | 分支切换操作
git switch local_branch # 切换到指定本地分支 git switch -c local_branch # 创建并切换到指定本地分支 git switch -C local_branch repo_name/remote_branch # 强制切换(若无则创建)本地分支到指定远程分支git restore | 工作区恢复
git restore filegit reset | 版本回退
git reset --hard HEAD # 强行恢复到HEAD提交(所有文件均改动) git reset --hard HEAD^^ # 强行恢复到HEAD之前2个提交(所有文件均改动) git reset --hard HEAD~2 # 强行恢复到HEAD之前2个提交(所有文件均改动) git reset --hard commit_hash # 强行恢复到commit_hash提交(所有文件均改动) git reset --soft commit_hash # HEAD指向commit_hash提交(但文件不变)git merge | 合并分支
git rebase | 变基(避免通过分支合并的方式合并修改)
git rebase other_branch # 将当前分支从 other_branch 上分叉出的提交节点改变到最新提交,看起来就像把当前分支中新的提交强行挪到了 other_branch 最新提交之后 git rebase -i # 交互式提交编辑,通过修改为`s`进行提交合并因为
git rebase默认不会保留 merge 节点,即它会将提交“摊平”,重写提交历史,使其线性化,因此merge节点通常会被丢弃或转为普通提交。但有一种特殊方式可以让git rebase尽量保留合并节点:git rebase --rebase-merges other_branch # 将当前分支变基到指定分支,且尽量保留 merge 节点git status | 查看上次提交(commit)后工作区的文件改动
git log | 查看仓库提交(commit)历史
git log git log --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%Cblue%cn%Cgreen, %cr)%Creset' --abbrev-commit --date=relativegit reflog | 查看参考历史(常用于恢复)
git clean | 删除未跟踪文件
# 删除未跟踪文件 git clean -f # 删除未跟踪文件(含目录) git clean -fd # 删除未跟踪文件(含目录及被ignore排除的部分) git clean -xfd # 删除未跟踪文件(预先展示将删除哪些文件) git clean -nf
与远程端互动
- 特殊符号
| 符号 | 形式 | 含义 |
|---|---|---|
: | src:dst | 把来源引用(src)更新到目标引用(dst)若 src 为空,则表示删除目标引用 |
+ | +src:dst | 强制更新(允许非快进) 默认的 git push/fetch 都会拒绝非快进更新,使用 + 号可以覆盖本地或远程的引用 |
- 常见引用
| 引用路径 | 类型 | 含义 | 典型用例 |
|---|---|---|---|
refs/heads/* | 本地分支 | 本地工作分支 如 master、dev 等 | git checkout master 本质上就是切换到 refs/heads/master |
refs/remotes/origin/* | 远程跟踪分支 | 本地记录的远程分支状态 如 origin/master | git fetch 默认更新这里 |
refs/tags/* | 标签 | 用于标记版本 如 v1.0 | git tag v1.0 会创建 refs/tags/v1.0 |
refs/stash | 收藏 | 保存临时修改 | git stash 会在这里建引用 |
- git clone | 从远程仓库克隆
git clone [-b master] <repo> [dir] # 从仓库克隆master分支到本地dir目录- git remote | 连接远程仓库
git remote -v # 显示本地仓库连接的所有远程仓库
git remote add repo_name repo_url # 新增远程仓库连接
git remote set-url --add repo_name repo_url # 远程仓库新增多个地址,push时都推送,pull时仅第一个有效
git remote remove repo_name # 删除远程仓库连接
git remote prune repo_name --dry-run # 删除本地失效的远程仓库repo_name的跟踪- git fetch | 拉取远程分支或标记到本地
git fetch # 拉取所有远程端的所有远程分支到本地
git fetch --tags # 额外拉取所有远程标记到本地
git fetch --tags --prune # 并同步清理本地失效的远程分支、标记(但不处理冲突)
git fetch repo_name remote_branch:local_branch # 从指定远程端拉取指定远程分支到指定本地分支- git push | 推送本地分支到远程分支
git push # 推送当前分支到所有远程端(远程没有会创建)
git push repo_name local_branch:remote_branch # 推送指定本地分支到指定远程端的指定远程分支
git push -f repo_name local_branch:remote_branch # 强制推送(远程仓库需有对应权限)
git push repo_name -d remote_branch # 删除远程分支- git pull | 拉取远程分支到本地分支(一般为当前分支)
git pull repo_name remote_branch # 从指定远程端拉取指定分支并合并到当前分支- git tag | 操作标签
git tag --list # 列出标签
git ls-remote --tags repo_name # 列出repo_name的标签
git tag tag_name # 创建标签
git tag -a tag_name -m 'tag_description' # 创建带注释的标签
git show tag_name # 查看标签信息
git tag -d tag_name # 删除标签
git push repo_name tag_name # 推送标签到远程
git push -d repo_name tag_name # 删除(远程)标签
git push repo_name --tags # 推送所有标签到 repo_name
git fetch repo_name --tags # 从repo_name拉取所有标签(增量拉取,不覆盖指向不同的 tag)
git fetch repo_name --prune '+refs/tags/*:refs/tags/*' # 从 repo_name 拉取所有 tag(覆盖与远程冲突的本地 tag,并清理本地多余的 tag)高级操作
.gitconfig
git submodule
在一个项目中包含另一个项目,可以使用子模块。
常用操作
# 添加子模块(可指定跟踪分支及存放路径)
git submodule add [-b <branch_name>] https://github.com/pybind/pybind11 [third_party/pybind11]
# 检查仓库变化
git status
git diff --cached --submodule
# 克隆包含子模块的项目
git clone xxx
git submodule init
git submodule update
# 或直接递归克隆包含子模块的项目(简单)
git clone --recurse-submodules xxx
# 克隆或切换后递归更新子模块到记录
git submodule update --init --recursive
# 或切换时直接递归更新
git checkout --recurse-submodules xxx
# 查看当前子模块状态
git submodule status
# 查看父仓库指定节点的子模块状态(或直接从父仓库 .git/modules/path/to/submodule/HEAD 文件直接查看)
git ls-tree HEAD path/to/submodule递归更新
在包含子模块的仓库中,若觉得每个命令都需要加递归指令较为麻烦,可以调整全局或项目配置。
# 用户全局配置
git config --global submodule.recurse true
git config --global --unset submodule.recurse
# 仓库配置
git config submodule.recurse true
git config --unset submodule.recurse注意这种方式可能存在一定的副作用:
- 性能开销
- 执行速度变慢
- 频繁网络请求(如需远程更新)
- 行为变化
git checkout:自动切换子模块可能覆盖子模块的本地修改- …
如需在开启自动递归的基础上临时禁用递归,可以:
git -c submodule.recurse=false <command>更新方式
前面的代码检出操作都是跟踪到指定的提交,这有利于精确固定版本。但协作开发中有时也需要跟踪分支的最新状态,此时可以使用命令:
# 跟踪指定或默认分支
git submodule update --remote
# 或临时指定跟踪其它分支
git submodule update --remote --branch your_branch_name上述命令会始终联网检查 .gitmodules 文件中设置的跟踪分支或默认分支(若文件中未设置)并检出最新的提交。
注意:检出后父仓库中仍需显式提交,可以认为该命令仅是一种追踪子模块到最新状态的简便方式。
分支跟踪
前面介绍了一种基于分支来跟踪子模块状态的方式,下面介绍设置、修改该跟踪分支的方式。
- 添加子模块时直接设置
git submodule add -b <branch_name> <repo_url> <store_path> - 对于已添加的子模块
git config -f .gitmodules submodule.<path>.branch <branch_name> # 或直接编辑 `.gitmodules` 文件,添加或修改对应子模块的 `branch` 属性,然后同步 git submodule sync
验证设置是否成功可以直接查看 .gitmodules,或通过命令:
git config -f .gitmodules submodule.<path>.branch如果想要取消设定的跟踪分支,可以编辑 .gitmodules 文件,删除其中 branch 属性行,然后运行
git submodule sync
git submodule update --force # 检出回退到指定记录屏蔽更新
用户可能对某些子模块不具备访问权限,此时可以通过配置来屏蔽这些模块,这样在更新时该模块会被跳过。如想屏蔽 path/to/submodule 这个子模块,可以运行:
# 仅本地配置
git config submodule.path/to/submodule.update none
# 配置到 `.gitmodules` 文件并版本跟踪
git config --file .gitmodules submodule.path/to/submodule.update none子模块禁用
某些环境下,用户可能完全不需要某些子模块,可以直接禁用该模块。仍以子模块 path/to/submodule 为例,可以运行:
git config submodule.path/to/submodule.active falsePS:
- 对于没有读权限的子模块,若需要在
git submodule update时跳过,需要配合前述屏蔽更新中的设置
子模块目录迁移
git mv path/to/old_module_name path/to/new_module_name
git submodule update --init --recursive删除子模块
git rm --cached path/to/submodule # 将同时触发 deinit
rm -rf .git/modules/path/to/submodule
# 亦可直接编辑 `.gitmodules` 文件,手动删除对应条目
git submodule sync子模块合并
子模块的合并类似于二进制文件的合并,即基于当前的 merge strategy 选择一个子模块的 commit 。如果没有冲突,这个过程是自动的,否则 Git 会报告冲突并提示手动选择 commit 。而 .gitmodules 这个文件的变化(如路径变化)会被作为普通文本文件进行合并,当然如果有冲突,按照文本文件的冲突处理方式进行处理。
有些时候可能想要指定子模块的合并方式为采用某一方的版本,则可以如下操作:
git merge dev_branch -Xsubmodule=theirs # theirs or ours有些时候,在 merge 后可能还想补充一些修改,此时若想让这部分提交合并到之前的 merge 节点上,可以执行:
git commit --amend --no-edit上述命令会将当前已暂存( staged )的改动合并到前一笔提交,生成一笔新提交,且保留之前的提交信息。并且对于 merge 节点,可以保留合并节点结构信息。它并不是直接修改前一笔提交,而是生成一笔新提交替代原提交,而原提交会 悬挂 在对象数据库中。
当然,使用 git rebase 的高级版本也可以实现上述需求:
git rebase --rebase-merges -i HEAD~2但是,该命令本质上是重放了合并过程,因此之前的合并如有冲突,需要再次解决。
git hook
git hook 类似于回调函数,可以在发生某些事件时触发自定义脚本。自定义脚本需没有扩展名,且具备可执行权限。
脚本存储在
.git/hooks/(git init)或hooks(git init --bare)下。Hook脚本不会被管理同步
1. 本地端
- pre-commit | 在Git请求你输入提交日志或者生成提交对象前触发
没有参数传递给pre-commit脚本,并且非零状态时候退出会暂停整个提交
- prepare-commit-msg | 在pre-commit后操作输入提交信息的文本编辑器后触发
这是为压缩(squashed)或者合并提交时修改自动生成提交信息的最佳时机
- commit-msg | 在用户输入提交日志时触发
可以提示提交的日志不符合标准
- post-commit | 在commit-msg后触发,不能改变git commit操作的结果
可以用来发送通知
- post-checkout | 切换分支后触发
可以用于清理工作目录生成的文件
pre-rebase | 在git rebase调用前触发
pre-push | 发起git push前触发
2. 服务端
pre-receive | 接受客户端git push前触发
update | 在pre-receive后但真正更新之前触发
post-update |
post-receive | 接受客户端git push成功后触发