〇、引言

    这篇博客介绍关于Git除git add/ git push/ git commit(以及git pull与git clone)以外的有效工具,其一是笔者用于记录学习,其二是避免此博客被荒废掉。

    在进行协同开发亦或使用仓库储存代码时,均少不了使用Git,但是笔者在正式使用后却发现,日常似乎一直在不停git add .,git commit -m,git push original main并且近乎安于此,于是近日重新对其他的指令进行了学习,试图以此增进工作效率。


一、时间回溯⌚

1.1 不小心提交了不得了的commit?git reset一键回撤

     git reset 是 Git 中用于移动 HEAD 指针重置暂存区(index) 和/或 工作目录 的命令,常用于撤销提交、取消暂存文件或丢弃本地更改。它的行为取决于所使用的选项(--soft--mixed(default)--hard)以及指定的提交(commit)

标准写法如下:

1
git reset [command] <commit>

    三个选项对应着三种不同的强度,其中soft为撤回上一次提交,但是保留修改在暂存区;mixed保留修改,但是清空暂存区,也是reset指令的默认模式;hard指令将代码和提交都恢复到上一次提交时的状态,彻底丢弃。示例如下:

1
2
3
git reset --soft HEAD~1    //HEAD~1表示上一个提交,~2表示上两个,以此类推
git reset                  //mixed模式,保存修改,修改不在暂存区,HEAD不变
git reset --hard HEAD~2    //与代码一起回到两次提交前

需要注意,reset可回溯的有限,一般建议不要HEAD~后面太大

    当然,reset指令也可以不更改HEAD指针直接针对暂存区的文件,常用如下代码:

1
git reset -- <filename>    //取消暂存一个文件,但不更改HEAD

git reset如果想用于共享分支需要强制推送,建议使用revert

注意⚠️:reset虽好,也不要贪多哦

1.2 带着现代的记忆回到过去? revert一键生成“反向commit”

    这个指令与git reset有相似之处,但是git revert的使用场景与git reset完全不同,git revert是用于安全撤销某次提交的指令,不删除历史,而是创造一个相反的commit用于抵消之前的commit,基本写法如下:

1
git revert <commit>

    其中比较常见的用法有以下两个

  • -n--no-commit:只应用反向更改到工作区/暂存区,不自动提交(适合组合多次 revert 后统一提交)
  • --edit / -e:强制编辑提交信息(默认会自动生成如 “Revert ‘xxx’”)

    commit可以使用HEAD指针或者commit hash,举例如:

1
2
3
git revert HEAD            //创建一个新提交与HEAD完全相反
git revert n0tv4l1d0     //Git计算HASH并生成反向补丁
git revert -n n0tv4l1d0    //应用于工作区但是不commit

1.3 COMMIT打错字好丢人?amend提供“后悔药”

    好不容易写完代码提交,push之前却发现不小心漏交文件或者发现打错字?不想再重新写一个垃圾提交污染纯净的信息?git commit 提供了这样一个指令:

1
git commit --amend <command>

    举例而言,不妨假设我们现在写了commit.c提交

1
2
3
4
git add commit.c                //提交commit.c
git commit -m "Update Commot"   //不好,没交commit.h还打错了,好羞耻
git add commit.h                //提交commit.h
git commit -m "Update Commit"   //这样就好了

需要注意,git resetgit commit --amend仅限于push之前的补救,push了还是自求多福罢…

1.3 reset的好帮手:reflog输出变更历史

        git reset指令打错了,soft HEAD~1一下成为了hard HEAD~10rebase失败,似乎自己的时间突然被挖空了一块。不用着急,git 也提供了git reflog指令用来查看历史编辑:只需要

1
git reflog

    输出实例如下:

1
2
n0tv4lid0 HEAD@{0}: reset: moving to HEAD~2    //hash + HEAD + command
...

    之后只需要找到对应操作的hash,之后使用git reset --hard <commit-hash>即可轻松恢复。


二、建立支部🌲

2.1 main裸奔施展不开?开分支放心搞事!

    很多时候写东西总有后顾之忧,担心写坏了还没有备份,每次都时间回溯又嫌弃浪费时间,git checkout解决了这个问题。git checkout的基本指令如下:

1
git checkout [command] <branch-name>

    比较常用的便是拉取新的分支与切换分支,分别对应:

1
2
git checkout -b <new-branch-name>    //在当前分支拉取一个新的分支
git checkout feature/n0tv4l1d0       //切换到feature/n0tv4l1d0这个分支

    这里checkout -b可替代为下列指令

1
2
git branch <new-branch-name>          //可替换为branch指令
git checkout <new-branch-name>        //直接写也可以

在Git23新推出了git switch指令,关于二者不同参考此篇博客

2.2 我还没改完怎么让我拉别的分支? Stash暂存未提交修改

    git stash的语法很简单,基本上只有两个

1
2
git stash            //暂存未提交的修改
git stash pop        //取回未提交的修改

    这个十分简单,使用场景也很纯粹:双分支忙不过来不想提交半成品commmit,此指令暂时不作过多讲解。


三、合成大分支🍉

3.1 分支总算写完了…然后呢?

    git merge这个指令十分常见,不应作过多解释,但是为了彰显与后续git rebase的区别,还是应当稍加完善。git merge的使用方式如下:

1
2
git checkout n0tv4l1d0        //切换分支
git merge main                //将当前分支合并在main中

    也可以在main分支反向合并,效果相同:

1
2
git checkout main             //切换到主分支
git merge n0tv4l1d0 //合并分支

3.2 唔唔啊…分支多了历史好混乱

    git rebase指令对个人开发十分友好,与merge的基本使用方式完全相同,但是rebase指令会重写生成线性历史结构,具体流程与基本指令如下:

  • 找到 feature 相对于 main 的新提交。
  • 临时移除这些提交,将 feature 分支“移到” main 的最新提交之后。
  • 重新逐个应用(replay)那些提交。
1
git rebase <branch-name>

需要注意⚠️: 对已经推送到远程的公共分支执行rebase会破坏他人历史,切忌在公共场所随地rebase

3.3 不想要整个分支?cherry-pick选定特定提交

    git cherry-pick提供了一种新的指令,允许用户不整合整条分支而是提取某(几)个特定提交,具体指令如下

1
git cherry-pick <commit-hash>         //后面可以包含多个hash

使用<hash_old>..<hash_new>时不包括<hash_old>对应版本,如欲包括需要使用<hash_old>^..<hash_new>


四、不做背锅侠🦸‍♀️

4.1 谁动了我的代码?blame一键找出

    git blame的这个名字十分奇怪,虽然无从得知Linus是在何种情形下写出这个指令的,但是这个代码的使用情景十分契合它的名字。git blame主要为找出代码每一行的修改详情,基本指令十分简单,只需要:

1
git blame n0tvalid0.c

    返回的详情包括每一行修改者,修改时间与对应commit;可以找到代码的家长和创造代码的原因(从commit)

4.2 我都干了什么?Diff一键获取修改

    git diff是一个十分实用的指令,用于在commit之前确认自己到底都更新了什么。具体指令如下:

1
2
git diff                                         //用于暂存区或上次提交与工作区的diff
git diff <branch-name-1> <branch-name-2> //用于比较分支区别

    git diff尤其应当作为团队开发的常客,知道自己有无不小心的改动也是为他人成果的负责。

4.3 防患于未然:使用Hook钩子防止一时糊涂

    hookGit提供的钩子系统,用于自动化运行所写的脚本。

    hook放于.git/hooks/目录下,Git提供了十余种规范的文件名,包括pre-commit,prepare-commit-msg,commit-msg,post-commit,pre-push,post-checkout,post-merge,举个例子而言,比如写一个pre-commit钩子,只需要在.git/hooks/目录下新建一个名为pre-commit的文件(无后缀),脚本内容示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!/bin/sh

# pre-commit hook: 检查暂存区中是否包含 console.log

# 获取暂存区中所有被修改的文件(过滤出 .js 或 .ts 文件)
files=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(js|ts)$')

if [ -z "$files" ]; then
# 没有相关文件,直接通过
exit 0
fi

# 检查这些文件中是否包含 console.log
found_logs=$(git diff --cached -G 'console\.log' --name-only)

if [ -n "$found_logs" ]; then
echo "错误:提交中包含 console.log,禁止提交!"
echo "包含 console.log 的文件:"
echo "$found_logs"
echo "请删除后再提交。"
exit 1 # 阻止提交
fi

exit 0 # 允许提交

    与基本.sh写法无异,写之后需要chmod开放执行权限

1
chmod +x .git/hooks/pre-commit

4.4 查看Log太丑了?美化的Log用不用?

    git log指令作为一个常见指令,因其不易读而令人生畏,原版指令如下:

1
git log

    但是其实可以通过加上其他功能使得其更易读

1
git log --oneline --graph --decorate --all

oneline:提交一行一个,避免拥挤

graph:带树形图

decorate:显示分支/tag

all:显示全部分支

    如果不用Linux而用Windows自带的git bash,无法自动补全上次输入的指令导致费时费力,可以选择.gitconfig添加别名(假如以后输入git lg)

1
2
[alias]
lg = log --oneline --graph --decorate --all

五、 清理仓库🧹

5.1 项目没有LTS版本十分混乱? 做好Tag以防后患

    Git里比较常用的push指令便是git push origin main,但是一直在main上push难免会导致项目如同Arch Linux一样(I use Arch, btw)容易崩坏,对于相对稳定的版本,发布新的Tags十分关键。基本指令如下:

1
git tag v0.0.1

    后续push只需要

1
git push origin v0.0.1

5.2 仓库填满饭桶与肥猪?写好ignore清除没用的东西

    .gitignore不是一个很少见的文件,至少如果在GitHub上新建一个repo的时候,一般都会有一个框问是否添加.gitignore。(虽然大部分新手会选择取消勾选)。

    .gitignore只需要在文件里加入不上传的CACHE.env等文件,比如

1
2
3
4
5
6
7
8
9
10
11
node_modules/
*.log
dist/
.env
venv/
__pycache__/
*.pyc
*.pyo
*.egg-info/
.DS_Store
electron/dist/

    写完之后放在仓库里即可避免垃圾文件堆满整个仓库。