IT加油站

Git 中级命令指南:10 个你希望早点知道的实用技巧

21浏览 1天前 软件教程 MA122772

原文:https://dev.to/sylwia-lask/10-git-commands-youll-wish-you-knew-earlier-4fcp(作者 @sylwia-lask)

这些命令算是中级水平,但非常实用。是你在工作中真正需要的,但还没到那种高级 Git 魔法级别:

在终端里敲三行命令,你的仓库里就会出现一艘宇宙飞船。

如果你还有其他这个级别的实用命令,欢迎在评论区分享!

1. 和 2. git mergegit rebase

哈哈,第一个要点就包含两个命令。我把它们放在一起讲,是因为很多开发者,甚至是有经验的开发者,几乎把 mergerebase 当成同义词来用。

当然,它们的高层目的确实相似:你想把一个分支的历史整合到另一个分支中。很多时候这意味着用 maindevelop 或其他功能分支的更新来同步你的功能分支。

但它们的实现方式差别很大。假设你有这样一个提交历史:

A---B---C main
     \
      D---E feature


你一直在 feature 分支上愉快地工作,而其他人往 main 分支添加了提交 C。现在你想把这些改动带到你的分支里。

Merge

如果你在 feature 分支上运行:

git merge main


Git 会合并两条历史。你通常会得到类似这样的结果:

A---B---C------M
     \        /
      D---E---


M 是一个合并提交。

原始历史保持不变。Git 会记住你的 feature 分支是从 main 分叉出来的,两个分支各自独立演进了一段时间,然后被合并在一起。

这可能是件好事,因为它保留了真实的历史。缺点在于,如果你反复把 main 合并进 feature 分支,历史最终可能会变得像意大利面一样乱。

Rebase

现在假设你运行的是:

git rebase main


Git 会取出提交 DE,暂时移除它们,把你的分支移到最新的 main 上,然后把你的提交重新应用到顶部。

你会得到:

A---B---C---D'---E'


干净多了。

但注意那些撇号。D'E' 严格来说并不是原来的 DE。Git 创建了带有新哈希值的新提交。这意味着 rebase 会重写历史

这就是为什么有一条非常有用的经验法则:

可以 rebase 自己的工作。对共享历史进行 rebase 要格外小心。

如果你独自在一个功能分支上工作,rebase 通常完全没问题。如果已经有三个其他开发者在你的提交基础上工作,rebase 这些提交会让每个人的生活都变得相当刺激——而且不是好的那种刺激。

冲突怎么办?

merge 和 rebase 都可能产生冲突。

使用 merge 时,Git 会尝试在一次操作中合并两条历史。你解决冲突文件,暂存它们:

git add .


然后继续:

git merge --continue


使用 rebase 时,情况可能稍微烦人一些,因为 Git 会逐个重新应用你的提交。

这意味着你可能解决了一个冲突,继续 rebase...

git add .
git rebase --continue


...然后在下一个提交中又遇到另一个冲突。接着再来一个。再来一个。到这个时候,你会开始质疑自己当初为什么会选择这个职业。

但这里也有一个小福利。如果你把一切搞得一团糟,只想停止整个闹剧,你可以运行:

git merge --abort


或者:

git rebase --abort


Git 会尝试把你恢复到 merge 或 rebase 开始之前的状态。这个功能极其有用。


3. git commit --amend

这是我在初级开发时期很快就喜欢上的命令之一。想象一下,你刚刚创建了一个漂亮、完美的小提交。然后你看了看代码,发现忘了点东西。就我而言,通常是这样:

console.log("WTF");


你现在有两个选择。选择一:再创建一个光荣的提交:

Remove console log


说实话,这算不上 Git 历史美学的巅峰。

或者,你可以简单地把遗漏的改动添加到上一个提交中。例如:

git add forgotten-file.ts
git commit --amend --no-edit


--no-edit 的意思是:

把这些改动添加到上一个提交,但保留现有的提交信息。

如果你想同时修改提交信息,只需运行:

git commit --amend


Git 会打开你配置的编辑器,让你修改。

这里有一个重要的细节。amend 并不是字面意义上编辑现有的提交。它会创建一个包含更新内容的新提交。由于提交哈希取决于提交的内容和元数据,哈希值会改变。

如果你在本地工作,还没有把分支推送到任何远程仓库,那没问题。但如果分支已经存在于远程,你的本地历史就不再匹配远程历史了。所以当你尝试:

git push


你可能会得到类似这样的结果:

rejected (non-fast-forward)


如果你非常确定没有其他人在那个分支上工作,你理论上可以运行:

git push --force


但如果你不确定呢?

这就很自然地引出了下一点。


4. git push --force-with-lease

在 rebase、amend、交互式 rebase、squash 或任何其他重写提交的操作之后,你的本地分支历史可能不再匹配远程存储的版本。

你可以用以下命令解决:

git push --force


--force 基本上是在说:

我的本地版本就是真相。用这个替换远程上的任何东西。

如果在此期间有人向该分支推送了内容,你可能会覆盖他们的工作。他们可能会生气,而且完全有理由生气。

一个安全得多的选项是:

git push --force-with-lease


简单来说,这告诉 Git:

强制推送我的版本,但前提是远程分支仍然和我预期的一样。

如果远程分支自你本地 Git 所知道的状态以来发生了变化,例如因为其他人推送了新提交,Git 会拒绝推送,而不是盲目覆盖。

所以,与其毁掉别人的下午,你只会得到一个错误。我通常建议:

--force-with-lease


而不是:

--force


只要有可能。


5. git rebase -i

对我来说,这是一个非常重要的命令。几乎是 Git 之王。交互式 rebase 让你几乎可以对最近的提交做任何想做的事。

换句话说,你可以每一行都单独提交,把所有东西改十遍,创建这样的提交:

Add validation
Fix validation
Actually fix validation
Fix validation again
Remove console.log
Please work now


然后稍后把整个烂摊子变成优雅的 Git 历史。

假设你想编辑最近五个提交。运行:

git rebase -i HEAD~5


Git 会打开你配置的编辑器——Vim、Nano、VS Code 或你用的任何编辑器——并显示类似这样的内容:

pick a111111 Add login form
pick b222222 Add validation
pick c333333 Fix typo
pick d444444 Fix validation
pick e555555 Remove debug log


现在有趣的部分来了。你可以用不同的命令替换 pick

pick

完全保持提交不变。

pick a111111 Add login form


reword

保留提交内容,但修改提交信息。

reword a111111 Add beautiful login form


edit

在提交处暂停 rebase,让你修改它。如果你想更改较旧提交的实际内容,这很有用。

squash

把提交与它前面的提交合并。

例如:

pick   a111111 Add login form
squash b222222 Add validation
squash c333333 Fix validation


Git 会把这些提交合并成一个,并让你编辑最终的提交信息。

所以,与其得到:

Add login form
Add validation
Fix validation


你可以最终得到:

Add login form with validation


漂亮。圆润。完美。

fixup

fixup 类似于 squash,但它会丢弃被 fixup 的提交的提交信息。

例如:

pick  a111111 Add login form
fixup b222222 Fix typo
fixup c333333 Remove console.log


你可能并不关心保留这个的历史意义:

Remove console.log


所以 fixup 在这里是完美的。

drop

完全移除提交。

drop c333333 Terrible idea


再见。

当然,关于你应该多大程度地清理提交,有不同的观点。一些资深开发者说,提交一份整理干净的提交历史供审查只是基本的礼貌。另一些人则完全不在乎,认为这没有必要,因为 GitHub、GitLab 或你使用的任何平台都可以在合并 PR 时直接 squash 所有内容。

但是!如果你不想点击 Squash and merge 呢?如果你的功能逻辑上包含两个提交,而你实际上想保留这两个提交呢?这种情况确实存在。

所以,无论你是一周用一次交互式 rebase,还是六个月用一次,我仍然认为值得认识这位王者:

git rebase -i



6. git stash

这个命令经常派上用场。你开始开发一个不错的小功能。或者,如今,Kiro 或 Claude Code 在你看着的时候非常努力地工作。突然有人报告了一个生产环境 bug。

不幸的是,你现在必须暂时放弃你漂亮的功能,切换到别的事情上。问题是你当前的工作完全没有完成。到处都是调试日志,一半的文件被修改了,项目甚至无法构建,你绝对不想提交这堆烂摊子。

不过,当然,现在你知道了 amend,所以你最终可以清理它。

但有一个更好的解决方案:

git stash


Git 会临时存储你未提交的更改,并把你的工作目录恢复到干净状态。

现在你可以切换分支:

git switch main


修复你的生产环境灾难,然后再回来。

一个重要的细节:默认情况下,git stash 会暂存已跟踪的文件,但不会暂存新的未跟踪文件。如果你还想包含新创建的文件,使用:

git stash -u


或者,更好的做法是,给你的 stash 起一个有用的名字:

git stash push -u -m "WIP login feature"


因为一旦你有几个基本上都叫"WIP"的 stash,未来的你会恨现在的你。

git stash list

查看所有 stash:

git stash list


你可能会得到:

stash@{0}: On feature/login: WIP login feature
stash@{1}: On feature/cart: experiment


git stash show

查看某个 stash 包含什么:

git stash show stash@{0}


如果你想要完整的 diff:

git stash show -p stash@{0}


git stash apply

恢复 stash:

git stash apply stash@{0}


更改被恢复,但 stash 仍然保留在列表中。

git stash pop

你也可以使用:

git stash pop


这会应用 stash,如果成功,会把它从 stash 列表中移除。

所以,大致来说:

apply = 恢复
pop   = 恢复 + 从列表中移除


非常简单,非常有用。


7. git cherry-pick

我得承认,这是我最常用的中级 Git 命令之一。想象一下,另一个分支上有一个特定的提交,你想要把它放到当前分支。

你不想合并整个分支。你不想 rebase 到它上面。你只需要一个东西。例如,最近在我的项目中,我需要一个包含新创建环境配置的提交。完美的用例。

你只需运行:

git cherry-pick <commit-hash>


例如:

git cherry-pick a1b2c3d


Git 会取出该提交引入的更改,并把它们应用到当前分支。

重要的细节:它会创建一个新提交。所以最终的提交包含基本相同的更改,但会有一个新的哈希值。想象这个历史:

main
A---B---C

feature
     \
      D---E---F


你在 main 上,但你只想要 E

运行:

git cherry-pick E


之后你会得到类似这样的结果:

A---B---C---E'


简单。

但我得承认,作为一个懒惰的生物,我也会用 cherry-pick 做一些不太高尚的事情。有时候我的分支上有些东西完全搞砸了。rebase 进行得很糟糕,历史看起来可疑,冲突开始成倍增加,过了一会儿我决定:我会从树顶的正确位置创建一个全新的分支。

而且——因为多亏了前面的命令——我的提交已经漂亮圆润了,我只需把它们一个个 cherry-pick 到新分支上。

我相信有些 Git 高手现在正在对我摇头,但让我这么说吧:这对我很有效。


8. git reset --soft--mixed--hard

三种回退历史的方式,每种对以下问题的回答都不同:

我的更改应该怎么办?

因为我们有多少次开始做某件事,写了一些代码,然后意识到:不,这个想法完全是错的。让我们回去吧。

理解 reset 最简单的方法是考虑三个层次:

提交历史
暂存区
工作目录


现在我们有三个戏剧性程度递增的 reset 级别。如果你用错了,每一个都比前一个更容易引发小规模心脏病发作。

git reset --soft

假设你想撤销最后一次提交:

git reset --soft HEAD~1


Git 会把 HEAD 回退一个提交,但被移除提交的所有更改仍然保持暂存状态。

所以如果你的历史是:

A---B


reset 之后,你的分支指向:

A


B 引入的更改仍然可以提交。

当你提交得太早,想用不同的方式重新创建提交时,这很有用。

git reset --mixed

现在:

git reset --mixed HEAD~1


或者简单地:

git reset HEAD~1


因为 --mixed 是默认选项。

同样,Git 回退一个提交。

但这次更改会留在你的工作目录中,处于未暂存状态。

所以你的文件不会丢失任何东西,但在提交之前你需要重新 git add

git reset --hard

现在我们进入危险区域:

git reset --hard HEAD~1


这会回退 HEAD,并更新暂存区和工作目录以匹配该提交。换句话说,更改也会从你的文件中消失。

所以:

--soft  → 保留更改,保持暂存
--mixed → 保留更改,不暂存
--hard  → 从工作目录丢弃更改


使用最后一个命令时,要清楚自己在做什么。但如果你多走了一步,意外删除了你绝对不想删除的东西呢?

这就引出了...


9. git reflog

一个我很少使用的命令。但它救了我太多次了。

例如,有一次我运行了 git reset --hard,结果不是删除了我最后的更改,而是基本上删除了我工作了两天的分支。令人惊叹的体验。强烈推荐。

需要理解的重要一点是,git log 显示的是从你当前查看的历史可达的提交。如果你把分支向后 reset,一些提交可能会从 git log 中消失。

这并不一定意味着 Git 立即删除了它们。Git 还会维护一个本地日志,记录 HEAD 等引用的移动。你可以用以下命令查看:

git reflog


你可能会看到类似这样的内容:

e35fa12 HEAD@{0}: reset: moving to HEAD~2
821cd77 HEAD@{1}: commit: Add authentication
f992ab1 HEAD@{2}: commit: Add login page


啊哈!你丢失的提交就在这里。

现在你有几个选择。你可以把分支移回它:

git reset --hard 821cd77


但就我个人而言,如果我已经处于恐慌恢复模式,我更喜欢先做更安全的事情:

git branch rescue 821cd77


现在提交又可以从一个分支到达了,我可以冷静地检查发生了什么,而不必立即重写其他任何东西。

关键区别是:

git log


显示你可见的提交历史。

git reflog


显示你的本地引用,尤其是 HEAD,最近指向过哪里。

但有一个重要的限制。Reflog 并不是你输入过的每个字符的神奇备份。如果你的更改从未被提交、stash 或以其他方式存储为 Git 对象,reflog 无法神奇地复活它们。

所以是的,如果你工作了六个小时而没有提交任何东西,然后毁掉了那些更改……嗯,也许这会教你下次要更频繁地提交。


10. git revert

最后:

git revert


想象你把一个提交发布到了生产环境。或者,在这个场景的稍微不那么硬核的版本中,发布到某个共享的 develop 分支。

出了大问题。一切都崩了。那现在怎么办?你要在生产分支上运行:

git reset --hard


吗?

你要开始驱魔吗?

幸运的是,不需要。在保留仓库历史清晰记录的同时撤销提交的优雅方式是:

git revert <commit-hash>


想象你的历史是这样的:

A---B---C


C 引入了灾难。

你运行:

git revert C


Git 不会移除 C

相反,它会创建一个应用相反更改的新提交:

A---B---C---D


你最终可能会得到类似这样的结果:

C: Add new payment logic
D: Revert "Add new payment logic"


这在共享分支上极其有用,因为你没有重写公共历史。每个人都可以清楚地看到:

  1. 原始更改发生了,
  2. 它造成了麻烦,
  3. 它被回滚了。

对比一下 reset 分支并强制推送回退,后者会重写历史,并可能给所有在该分支上工作的人带来问题。

所以,作为一般规则:

共享分支 + 坏提交 → revert


通常比:

reset + force push


安全得多。

原文:https://dev.to/sylwia-lask/10-git-commands-youll-wish-you-knew-earlier-4fcp(作者 @sylwia-lask)

#Git #版本控制 #开发工具
2