Git学习-Git工具
Git的修订版本
Git 每一次 commit 都会生成一个版本号,也成为单个修订版本,它由 40 个字符的完整 SHA-1 散列值组成。
Git 支持只提供 4 个 SHA-1 字符即可获得对应的那次提交,但前提是在没有冲突的情况下,一般 8 到 10 个字符就已经足够在一个项目中避免 SHA-1 的冲突。
关于 SHA-1 的简短说明
许多人觉得他们的仓库里有可能出现两个不同的对象其 SHA-1 值相同。 然后呢?
如果你真的向仓库里提交了一个对象,它跟之前的某个 不同 对象的 SHA-1 值相同, Git 会发现该对象的散列值已经存在于仓库里了,于是就会认为该对象被写入,然后直接使用它。 如果之后你想检出那个对象时,你将得到先前那个对象的数据。
但是这种情况发生的概率十分渺小。 SHA-1 摘要长度是 20 字节,也就是 160 位。 2^80 个随机哈希对象才有 50% 的概率出现一次冲突 (计算冲突机率的公式是
p = (n(n-1)/2) * (1/2^160))
)。 2^80 是 1.2 x 10^24,也就是一亿亿亿,这是地球上沙粒总数的 1200 倍。举例说一下怎样才能产生一次 SHA-1 冲突。 如果地球上 65 亿个人类都在编程,每人每秒都在产生等价于整个 Linux 内核历史(650 万个 Git 对象)的代码, 并将之提交到一个巨大的 Git 仓库里面,这样持续两年的时间才会产生足够的对象, 使其拥有 50% 的概率产生一次 SHA-1 对象冲突。
交互式暂存
用途:当在修改了大量文件后,希望这些改动能拆分为若干提交而不是混杂在一起成为一个提交时。
通过 git add -i
进入交互式暂存。
输入数字下标或者前缀字母即可进入下一步,例如 status ,输入 1 或 s 。
上述的所有命令解释如下:
- status 表示查看当前 Git 已跟踪且变更过的文件状态。
- update 表示进行暂存操作。(即 git add xxx 操作)
- revert 表示取消暂存操作。 (即 git restore xxx 操作)
- add untracked 表示将未跟踪的文件进行跟踪。
- patch 表示暂存文件的特定部分。
- diff 表示查看已暂存内容的区别。
- quit 表示退出。
- help 表示帮助。
进入以上命令后,想要返回上一步,只需要不输入任何东西的情况下按回车即可。
贮藏与清理
有时,当你在项目的一部分上已经工作一段时间后,所有东西都进入了混乱的状态, 而这时你想要切换到另一个分支做一点别的事情。 问题是,你不想仅仅因为过会儿回到这一点而为做了一半的工作创建一次提交。 针对这个问题的答案是 git stash
命令。
贮藏(stash)会处理工作目录的脏的状态——即跟踪文件的修改与暂存的改动——然后将未完成的修改保存到一个栈上, 而你可以在任何时候重新应用这些改动(甚至在不同的分支上)。
贮藏相关命令如下:
-
git stash:贮藏当前改动的文件。
默认情况下,
git stash
只会贮藏已修改和暂存的 已跟踪 文件。 如果指定--include-untracked
或-u
选项,Git 也会贮藏任何未跟踪文件。 然而,在贮藏中包含未跟踪的文件仍然不会包含明确 忽略 的文件。 要额外包含忽略的文件,请使用--all
或-a
选项。 -
git stash list:查看贮藏记录
-
git stash apply:应用贮藏记录的内容
git stash apply 默认应用最新一条记录,如果想要应用指定记录,可根据记录前的下标修改命令为 git stash apply stash@{n} ,n 表示下标。
-
git stash drop:删除贮藏记录
默认删除第一条贮藏记录,也可以跟 apply 一样,指定删除。
清理(clean)用于从工作目录中移除未被追踪的文件。
清理相关命令如下:
-
git clean -n:查看将要移除的文件有哪些,并不是真的移除。
-
git clean -f:移除 -n 中显示的将要移除的文件。
-d 选项,表示未追踪的子目录。默认情况下 -n 或 -f 不会扫码移除子目录,需要增加 -d 参数才行。
-x 选项,表示包含移除与
.gitignore
或其他忽略文件中的模式匹配的文件。
签署工作
Git 虽然是密码级安全的,但它不是万无一失的。 如果你从因特网上的其他人那里拿取工作,并且想要验证提交是不是真正地来自于可信来源, Git 提供了几种通过 GPG 来签署和验证工作的方式。
GPG 使用方法:
-
gpg --list-keys:查看 GPG 密钥。
示例如下:
-
gpg --gen-key:生成 GPG 密钥。
-
git config --global user.signingkey 0A46826A:配置 GPG 的公钥。
搜索
无论仓库里的代码量有多少,你经常需要查找一个函数是在哪里调用或者定义的,或者显示一个方法的变更历史。 Git 提供了两个有用的工具来快速地从它的数据库中浏览代码和提交。
第一个工具是 git grep <搜索字符串或正则表达式>
命令,主要是用于搜索代码在哪里。
如果传递 -n
参数,将会输出 Git 找到的匹配行的行号。
如果传递 -c
参数,输出的信息仅包括那些包含匹配字符串的文件,以及每个文件中包含了多少个匹配。
如果传递 -p
参数,则会显示搜索字符串的上下文。
第二个工具是 Git 日志搜索,主要是用于搜索代码是何时存在或引入的。
通过 git log -S <搜索字符串或正则表达式>
命令,搜索代码的提交记录,展示提交日志。
其中 -S
参数表示显示新增和删除该字符串的提交记录。
如果传递 --oneline
参数,则主要显示提交的修订版本和提交备注。
重写历史
在学习重写历史之前,需要牢记一个开发准则:在满意之前不要推送工作内容。
Git 的基本原则之一是,由于克隆中有很多工作是本地的,因此你可以 在本地 随便重写历史记录。 然而一旦推送了你的工作,那就完全是另一回事了,除非你有充分的理由进行更改,否则应该将推送的工作视为最终结果。 简而言之,在对它感到满意并准备与他人分享之前,应当避免推送你的工作。
修改最后一次提交
顾名思义,只需要对最后一次提交做修改,此时使用 git commit --amend
命令就好。它会用新的提交版本来替换旧的最后一次提交。
如果提交信息不需要修改,可以使用 --no-edit
参数。
修改多个提交信息
Git 本身是没有直接改变历史的工具的,可以使用变基工具来变基这一系列提交,通过交互式变基工具,可以在任何想要修改的提交后停止,然后修改信息、添加文件或做任何想做的事情。
需要注意的是,git rebase -i <版本区间>
命令会将这个区间的所有提交信息进行重写,所以尽量不要涉及任何已经推送到中央服务器的提交——这样做会产生一次变更的两个版本,因而使他人困惑。
filter-branch
有另一个历史改写的选项,如果想要通过脚本的方式改写大量提交的话可以使用它——例如,全局修改你的邮箱地址或从每一个提交中移除一个文件。 这个命令是 filter-branch
,它可以改写历史中大量的提交,除非你的项目还没有公开并且其他人没有基于要改写的工作的提交做的工作,否则你不应当使用它。
git filter-branch
有很多陷阱,不再推荐使用它来重写历史。 请考虑使用git-filter-repo
,它是一个 Python 脚本,相比大多数使用filter-branch
的应用来说,它做得要更好。它的文档和源码可访问 https://github.com/newren/git-filter-repo 获取。
从每一个提交中移除一个文件
有人粗心地通过 git add .
提交了一个巨大的二进制文件,你想要从所有地方删除。 可能偶然地提交了一个包括一个密码的文件,然而你想要开源项目。 filter-branch
是一个可能会用来擦洗整个提交历史的工具。 为了从整个提交历史中移除一个叫做 passwords.txt
的文件,可以使用 --tree-filter
选项给 filter-branch
:
1 | git filter-branch --tree-filter 'rm -f passwords.txt' HEAD |
--tree-filter
选项在检出项目的每一个提交后运行指定的命令然后重新提交结果。 在本例中,你从每一个快照中移除了一个叫作 passwords.txt
的文件,无论它是否存在。
使一个子目录做为新的根目录
假设已经从另一个源代码控制系统中导入,并且有几个没意义的子目录(trunk
、tags
等等)。 如果想要让 trunk
子目录作为每一个提交的新的项目根目录,filter-branch
也可以帮助你那么做:
1 | git filter-branch --subdirectory-filter trunk HEAD |
现在新项目根目录是 trunk
子目录了。 Git 会自动移除所有不影响子目录的提交。
全局修改邮箱地址
另一个常见的情形是在你开始工作时忘记运行 git config
来设置你的名字与邮箱地址, 或者你想要开源一个项目并且修改所有你的工作邮箱地址为你的个人邮箱地址。 任何情形下,你也可以通过 filter-branch
来一次性修改多个提交中的邮箱地址。 需要小心的是只修改你自己的邮箱地址,所以你使用 --commit-filter
:
1 | git filter-branch --commit-filter ' |
这会遍历并重写每一个提交来包含你的新邮箱地址。 因为提交包含了它们父提交的 SHA-1 校验和,这个命令会修改你的历史中的每一个提交的 SHA-1 校验和, 而不仅仅只是那些匹配邮箱地址的提交。
如果提交记录已经推送到远程仓库,需要使用 git push origin branch_name
命令更新。
使用它来修改提交者(作者和提交者)信息可能导致与远程仓库的历史不一致,从而导致推送被拒绝。这是因为修改历史会改变每个提交的 SHA-1 校验和,而远程仓库已经有了不同的历史。
因此建议使用git push --force-with-lease origin branch_name
方式安全的强制推送,它会检查远程分支是否和你的本地分支一致。如果远程分支的历史已经被其他人修改,推送会被拒绝。
重置揭密
主要讲解 git reset
命令的强大之处,以及与 git checkout
的区别。
更多详情在:https://blog.itwray.com/2023/11/22/git-commands-reset/
高级合并
Git 的哲学是聪明地决定无歧义的合并方案,但是如果有冲突,它不会尝试智能地自动解决它。
所以,在合并前,建议将正在做的工作,要么提交到一个临时分支要么储藏(stasg)它。
如果已经执行了 git merge
合并操作并出现了合并冲突,可以通过 git merge --abort
来简单地退出合并。
如果这个不想要的合并提交只存在于你的本地仓库中,最简单且最好的解决方案是移动分支到你想要它指向的地方。 大多数情况下,如果你在错误的 git merge
后运行 git reset --hard HEAD~
。
Rerere
git rerere
功能是一个隐藏的功能。 正如它的名字“重用记录的解决方案(reuse recorded resolution)”所示,它允许你让 Git 记住解决一个块冲突的方法, 这样在下一次看到相同冲突时,Git 可以为你自动地解决它。
有几种情形下这个功能会非常有用。 在文档中提到的一个例子是想要保证一个长期分支会干净地合并,但是又不想要一串中间的合并提交弄乱你的提交历史。 将 rerere
功能开启后,你可以试着偶尔合并,解决冲突,然后退出合并。 如果你持续这样做,那么最终的合并会很容易,因为 rerere
可以为你自动做所有的事情。
Git 调试
使用 git blame
标注文件,可以知道一个文件的每一行具体在何时引入的,显示每行最后一次修改的提交记录。
例如:git blame -L 69,82 Makefile
表示用 git blame
确定了 Linux 内核源码顶层的 Makefile
中每一行分别来自哪个提交和提交者, 此外用 -L
选项还可以将标注的输出限制为该文件中的第 69 行到第 82 行。
使用git bisect
命令会对提交历史进行二分查找,以找到是哪一个提交引入了问题。
子模块
子模块的应用场景如下:某个工作中的项目需要包含并使用另一个项目。 也许是第三方库,或者你独立开发的,用于多个父项目的库。 现在问题来了:你想要把它们当做两个独立的项目,同时又想在一个项目中使用另一个。
首先,确定自己的主项目,然后在主项目的仓库下通过git submodule add
命令后面加上想要跟踪的项目的相对或绝对 URL 来添加新的子模块。
例如,在 git-study
项目下添加 git-study-submodule
子项目:
git submodule add git@github.com:wangfarui/git-study-submodule.git
查看git-study
项目目录结构:
首先应当注意到新的 .gitmodules
文件。 该配置文件保存了项目 URL 与已经拉取的本地目录之间的映射:
如果有多个子模块,该文件中就会有多条记录。 要重点注意的是,该文件也像 .gitignore
文件一样受到(通过)版本控制。 它会和该项目的其他部分一同被拉取推送。 这就是克隆该项目的人知道去哪获得子模块的原因。
添加子模块后,git status
查看主项目仓库状态:
可以发现子模块的项目内容已经被跟踪到主项目中,执行git commit
将其提交到主项目中,再执行git push
将主项目推送到远程仓库。
在 GitHub 下,可以看到 GitHub 会自动识别到子项目,项目链接到子项目地址。
但是!!!需要注意链接地址:
https://github.com/wangfarui/git-study-submodule/tree/ea6b9f6afef267dc897376df3b98b879aa5ec0fb
可以再看看git-study-submodule
项目的真正 Git 仓库地址:
https://github.com/wangfarui/git-study-submodule
可以发现从主项目进入的子项目显示的是主项目的版本,所以如果直接在子项目做提交变更,主项目是感知不到的,需要手动刷新并推送。
由此可见,子项目是独立存在的,只不过主项目将其引入进来,可以直接使用源码。同理,子项目的开发工作可以是独立项目开发,也可以是直接在主项目的子模块下开发。在子模块下执行 git remote show origin
可以看到当前指向的 Git 远程仓库为子项目的仓库地址。
在子项目下做编辑并提交操作后,在主项目通过git add
跟踪子模块,再提交和推送后,子模块的远程仓库才会同步更新。
如果子项目已经独立开发并更新到远程仓库了,可以使用 git submodule update --remote git-study-submodule
命令拉取最新信息,此时 git-study-submodule
子项目的内容已经与远程仓库同步了,不需要再 merge 。
更新完子项目后,主项目需要通过git add
跟踪子模块,再提交和推送后,主项目下的子项目版本才会同步变更。
打包
Git 可以将它的数据“打包”到一个文件中。 这在许多场景中都很有用。 有可能你的网络中断了,但你又希望将你的提交传给你的合作者们。 可能你不在办公网中并且出于安全考虑没有给你接入内网的权限。 可能你的无线、有线网卡坏掉了。 可能你现在没有共享服务器的权限,你又希望通过邮件将更新发送给别人, 却不希望通过 format-patch
的方式传输 40 个提交。
这些情况下 git bundle
就会很有用。 bundle
命令会将 git push
命令所传输的所有内容打包成一个二进制文件, 你可以将这个文件通过邮件或者闪存传给其他人,然后解包到其他的仓库中。
替换
Git 对象数据库中的对象是不可改变的, 然而 Git 提供了一种有趣的方式来用其他对象 假装 替换数据库中的 Git 对象。
replace
命令可以让你在 Git 中指定 某个对象 并告诉 Git:“每次遇到这个 Git 对象时,假装它是 其它对象”。 在你用一个不同的提交替换历史中的一个提交而不想以 git filter-branch
之类的方式重建完整的历史时,这会非常有用。
凭证存储
如果你使用的是 SSH 方式连接远端,并且设置了一个没有口令的密钥,这样就可以在不输入用户名和密码的情况下安全地传输数据。 然而,这对 HTTP 协议来说是不可能的 —— 每一个连接都是需要用户名和密码的。 这在使用双重认证的情况下会更麻烦,因为你需要输入一个随机生成并且毫无规律的 token 作为密码。
Git 拥有一个凭证系统来处理这个事情。 下面有一些 Git 的选项:
- 默认所有都不缓存。 每一次连接都会询问你的用户名和密码。
- “cache” 模式会将凭证存放在内存中一段时间。 密码永远不会被存储在磁盘中,并且在15分钟后从内存中清除。
- “store” 模式会将凭证用明文的形式存放在磁盘中,并且永不过期。 这意味着除非你修改了你在 Git 服务器上的密码,否则你永远不需要再次输入你的凭证信息。 这种方式的缺点是你的密码是用明文的方式存放在你的 home 目录下。
- 如果你使用的是 Mac,Git 还有一种 “osxkeychain” 模式,它会将凭证缓存到你系统用户的钥匙串中。 这种方式将凭证存放在磁盘中,并且永不过期,但是是被加密的,这种加密方式与存放 HTTPS 凭证以及 Safari 的自动填写是相同的。
- 如果你使用的是 Windows,你可以安装一个叫做 “Git Credential Manager for Windows” 的辅助工具。 这和上面说的 “osxkeychain” 十分类似,但是是使用 Windows Credential Store 来控制敏感信息。 可以在 https://github.com/Microsoft/Git-Credential-Manager-for-Windows 下载。
你可以设置 Git 的配置来选择上述的一种方式
1 | git config --global credential.helper cache |
总结
Git 工具非常之多,有些工具在日常开发中可能基本用不到,但有些工具还是非常实用的。这里主要列举一下我觉得比较有用的:贮藏(git stash)、重写最后一次提交(git commit --amend)、重置揭密(git reset)、搜索(git grep)。