分类: git

  • “我的代码被同事覆盖了!”——帮群里的同学解决 Git 问题

    “我的代码被同事覆盖了!”——帮群里的同学解决 Git 问题

    本站 2024 年招商持续进行中,欢迎各位老板前来投放:

    https://blog.meathill.com/ad/advertise-on-this-site.html

    刚才,群里有位同学提问:

    我把我的分支合到 B 分支上了,然后另外一个同事将他的代码合到 B 分支将我的代码覆盖了,然后 B 分支又有很多提交记录,不能回退了,我的分支再合到 B 分支不显示冲突和修改了

    大佬们,这种情况怎么解决呀

    可能大家都有过被同事误操作丢失代码的经验吧,群里群情激愤,纷纷谴责起这位同事。也有不少同学七嘴八舌的开始支招,有说找责任人的,有说一个一个还原的,有说 rebase 的。大家很热心,可惜都没说到点子上。

    要用好 Git,绝对不能靠背套路,更不能瞎试,成了也不知道咋成的,败了也不知道为啥不行。要理解 Git 的设计和实现,遇到问题先分析,再寻找最佳方案,然后小心谨慎的实施。

    我们先分析一下问题的原因:

    1. 最大的可能是,同事合并代码时,遇到冲突,他在解决冲突时,选择保留自己的代码,丢弃了同学的代码(最恶心的情况)。
    2. 我们不考虑归责。同学现在要提交自己的代码,但是因为他的代码版本比较“旧”,所以 git 拒绝合并。也无法提交新的 PR。
    3. 如果前面推测没错,那么同事就不是用的 push -f
    4. 所以同学的代码应该有一部分在库里,只是冲突的部分可能没了。

    如此一来,除了一个文件一个文件比对(效率太低),那么就是把所有修改全部找出来,然后做成新的提交。因为部分代码已经在库里,这部分代码大概率会直接被 git 接受;只有少部分之前冲突的代码需要解决之后才能提交,总量应该不多。所以,这该会是最快的方法。

    想清楚该怎么做之后,就该制定方案了:

    1. 先备份代码。
    2. 回退到启动开发时,切出来的分支。假设 commit 是 b1:git reset b1 --mixed。这一步会将 b1 之后的变更,作为未提交的部分,集中起来,排除版本信息。
    3. 保留这部分的代码到暂存区:git stash
    4. 切到开发分支的最新版本:git checkout B
    5. 弹出暂存的代码 git stash pop
    6. 可能会失败,因为存在冲突。那就解决冲突。
    7. 解决冲突后,重新提交 PR,然后合并分支。

    总结一下。Git 原理很简单:

    1. 按行对比。
    2. 每次提交都会产生新版本。
    3. 版本是单向的,Git 负责维护版本历史。
    4. 两个无法分辨先后的版本修改了同一行,就有冲突。Git 无法解决冲突,会交给人处理。
    5. 人处理后,产生新版本。
    6. 分支是指针,指向某个版本,并随提交自动前进。

    在此基础上,可能衍生出各种问题,比如开头的那一幕。但是不管问题如何,都离不开根本原理。所以我们面对 Git 问题,不要背套路,要认真分析前因后果,然后选择解决方案,再根据方案制定流程。

    希望这次分享对大家有帮助。如果你对 Git 操作还有什么不清楚不明白的,欢迎提问。如果我上文有错误,也欢迎指出。希望 2024,大家共同进步,再没有会乱搞别人代码的同事。

  • 2023 Git 必备知识(三):小知识推荐配置与小技巧

    2023 Git 必备知识(三):小知识推荐配置与小技巧

    (本文好像内容较少,我会继续完善。)

    Git 系列文章

    “我们有多个客户,交付给每个客户的功能不一样,要维护几个分支?”

    这是一个很常见的问题,也是大家经常出错的地方。比如我之前待过的某厂,给客户交付部署了 a0 版本。然后我们继续开发,搞出来了 bcd 等功能。客户想增加功能,但是不想多付钱;我们收不到钱,又不想得罪客户,于是跟他们协商,只交付 c 功能。于是,负责版本的同学就 cherry-pick c 功能的每次提交,交付给客户。

    第一次交付比较顺利,后来就越来越麻烦,交付分支和开发分支的差异越来越大,难以维护。

    git 设计用来管理代码的版本,方便我们并行开发、快速迭代。于是它是线性的,基本上是个单向链,我们很难往分支中段插入一个 commit,或者将某个 commit 轻松发布到某几个特定的版本当中。故而,功能模块化和热插拔跟 git 无关,不应该放在一个问题里讨论。

    类似的工作,我们需要使用模块化控制,比如构建脚本等。大部分脚手架工具,比如 webpack、vite 都提供有 define 功能,即定义变量,并注入到最终代码。我们可以利用这个功能,配合环境变量,给功能添加开关,然后在编译阶段进行调整,针对不同的用户,输出包含不同功能的代码。

    以 Vite 为例,大概是这个样子:

    export default defineConfig({
      define: {
        __CLIENT__: process.env.CLIENT,
      },
    });
    const routes = [
      // 通用路由
    ];
    
    // 只有客户是 meathill,才能看到某些路由,配合路由 lazyload,实现打包区分
    if (__CLIENT === 'meathill') {
      routes.push({
        path: '/pay-before-use-feature',
        component: () => import('./pay-before-use-feature.vue');
      })
    } else {
      routes.push({
        path: '/pay-before-use-feature',
        component: SorryYouAreNotAllowed,
      });
    }

    如果你有不同意见的话,可以到 这个问答 里讨论。

    推荐配置

    git config --global pull.ff only

    全局配置 git pull 的时候只使用 fast forward only 模式,即只会在采用快进模式,不会将远程分支通过创建 merge commit 的方式合并到本地。可以避免产生多余的 merge commit

    git config --global core.autocrlf true

    如果你使用 Windows 系统作为主力开发系统,那最好把自动转换换行符的选项钩上,以便和主要用其它系统的同事一起工作。

    Tips

    • GUI 推荐 GitHub Desktop,GitHub 集成很好,其它平台也能用
    • 分支名不要太长,可能导致构建工具失败
    • 依赖变更,翻译更新尽量单独开分支来做,即使跟功能相关的翻译,也要单开分支,这样可以大大减轻 code reviewer 的负担,并且更不容易产生冲突
    • VS Code 处理冲突比较好用,比 JetBrains 系列 IDE 好用
    • rebase 时如果 lock 文件产生冲突,可以直接 git checkout --theirs 回退开发分支的修改,rebase 结束后重新安装即可。

    总结

    至此,我现阶段对 Git 的理解就分享完毕,希望对大家有帮助。未来我收获新知也会继续分享。如果你对 Git 或者版本管理有什么问题或者想法,欢迎留言交流。

  • 2023 Git 必备知识(二):常见问题解决

    2023 Git 必备知识(二):常见问题解决

    Git 系列文章

    处理 hotfix

    有时候,我们在迭代中间,会发现 master 存在一些 bug,于是我们就需要修复这个 bug。但此时 dev 通常不能合并到 master,于是我们应该在 master 上修复这个 bug。

    1. 此时不需要对 master 与 dev 做合并操作
    2. 我们从 master 上开启新分支,并修复这个 bug
    3. 采用和之前一样的策略完成代码合并
    4. 将 master 上线
    5. 如果此 bug 同时影响 master 与 dev,我们应该在 dev 分支 cherry-pick 这个修复
    6. 迭代发版时,先将 dev rebase master,与 master 同步
    7. 然后切换到 master,快进 git merge dev --ff-only
    8. 如此就完成两个分支的合并,同时保留单线历史记录

    git rebase dev -i

    有时候,我们开发过程中会产生一些冗余 commit。比如,我原先要用 A 方案,
    后来又改成 B 方案。于是,开发 A 方案时产生的 commit 就没有必要存在代码仓库中。

    此时,我们可以使用 git rebase dev -i,启动交互式 rebase,大概会看到这样的输出:

    pick 2662533e feature: plan A
    pick 7af8e1ef feature: plan B

    现在两个 commit 前面都是 pick,表示这两个 commit 我都要。但其实我们只想要第二个,
    就可以把第一个的 pick 改成 squash,或者简写成 s,这样就只会保留第二个 commit。

    s 2662533e feature: plan A
    pick 7af8e1ef feature: plan B

    保存退出,再 git log,就会只剩下第二个 commit 了。

    Note: 强烈建议大家在每次 merge PR 之前,都这样操作一次。删掉所有不必要的 commit,比如自己的尝试、测试同学发现的 bug,等。

    git reset --hard COMMIT

    使用 git reset 的时候,我建议大家轻易不要 git reset --hard COMMIT
    因为这样一不小心代码就丢了。损失难以估量。

    建议大家这样做:

    1. git checkout -b xxx_backup 先备份到新分支。
    2. git reset --mixed COMMIT,回退版本,但是不清除代码修改。
    3. git stash 将修改放入暂存区。
    4. 如果后续操作无误,清理掉暂存区 git stash drop,在删掉没用的备份分支即可

    万一 git reset --hard 失误

    实际上 git reset 只是重置分支头的位置,并不会真的去动 commit,所以那些 commit 其实都还在我们本地,只不过这些 commit 不太容易找到。

    此时我们可以使用 git reflog 查看分支的变更记录,找到之前的 commit 信息,然后再次使用 git reset 把分支指向对的 commit。

    注意:这个方法只适用于修复最近的失误。如果你的代码没有提交(生成 commit),或者 git reset 发生在很久之前,那也就回天乏力。所以日常操作还是要小心。


    上面这些问题都是我日常工作中实际遇到并解决过的。如果你有其它问题不知道怎么解决,欢迎留言提问;如果你对上面的解决方案有不同意见,也欢迎指教。

    下篇会分享一下小技巧和一些问题讨论,敬请期待。

  • 2023 Git 必备知识(一):常用团队规范

    2023 Git 必备知识(一):常用团队规范

    我一直以为,时至今日,Git 知识应该已经足够普及,常用操作大家都知道,应该不需要分享。没想到换过几份工作之后,发现还有很多同学不清楚、乱用;一些新出的 git 分享文也写得乱七八糟,槽点满满。于是我决定结合之前在 Code.fun 建立的 Git 规范,和分享过的常见操作整理出来,作为虎年兔年承上启下的文章,贡献给大家。

    Git 分享计划

    本系列计划分成三篇文章:

    Git 使用原则

    1. 保障分布式开发:每个人都在自己的分支上开发;可以随时切换不同分支。
    2. 保障历史记录有价值:只保留有价值的 commit,不必要的 commit 应该在合并前 squash。可以通过回溯 commit message,了解到某行代码的意图,做出合理推断。
    3. 减少未来操作成本:只保留一条历史树,不使用 merge 造成混乱的图谱。以便在发版时不因为和代码耽误时间。

    分支设计

    Code.fun 是一家小厂,采用敏捷开发模式,所以不需要维护太多分支。大体上,我们只维护两大类分支:发布分支:master、dev;开发分支:每个人根据需求切出自己的分支进行开发。

    • 正式版本代码放在 master 分支,并且公开发布上线。
    • 迭代中的代码合并到 dev 分支,可能也会提供 dev 环境给部分喜欢尝鲜的用户。
    • 迭代结束后,将 dev 合并回 master,并发布正式版。
    • 启动新迭代,产生新的 dev 分支。

    单线分支原则

    为了保证版本管理的有效性,我们应尽力保持 只有一条线 的版本历史。为此
    有以下注意事项:

    1. 保证本地 dev 与中心仓库一致。
    2. 开发时,从 dev 分支创建开发分支。
      • 如果需求较大、开发时间较长,建议大家经常 rebase,保持和 dev 同步。
    3. 合并前:
      1. 更新本地的 dev 分支,与中心仓库一致。
      2. 将开发分支 rebase 到最新 dev 分支,即在开发分支上,运行 git rebase dev
    4. 拉代码时,尽量使用 git pull --ff-only,避免出现 merge commit。

    开发规范

    1. 新需求启动时,需要创建一个分支进行开发。如果有多个参与者,可以根据实际情况,选择大家在同一个分支下进行开发,或者基于此分支创建更多的子分支进行开发。
    2. 分支名应该包含用户名,与分支名用 / 分隔,形如:{用户名}/{分支名称}
    3. 其中分支名称应该符合 {前缀}-{需求描述},如 feature-新功能
    4. 常见的前缀有:
      • feat(ure): 新功能
      • (bug)fix: 修复 bug
      • chore:构建过程或辅助工具的变更
      • docs: 文档的变更
      • style: 代码风格的变更
      • ref(actor): 重构
      • test: 测试的变更
      • ver(sion): 版本更新
      • text(ure): 文本的变更
      • deps: 依赖变更,即为适配依赖产生的变更
    5. 提交时,commit message 也需添加合适的前缀,如:feature: 支持点击
    6. 分支应随时推入公司仓库。
    7. 需求开发过程中,应尽早创建 PR。PR 完成前,加 [WIP] 前缀。
    8. 开发完成后,部署到测试环境自测。
      • 请结合各公司不同的基础设施,进行部署和自测。之前我厂主要是 k8s、GitHub Actions 等。
    9. 自测后,邀请产品同学体验,邀请测试同学测试。
    10. 体验+测试通过,请其他同事帮忙做 code review。
    11. 修复问题,code review 通过后,再次更新本地 dev 分支,然后使用 git rebase dev -i,将开发分支变基到最新 dev 分支,并 squash 掉价值较小的 commit,只保留有回顾价值的 commit。
    12. 接下来,建议引入自动化测试,进行回归测试。
    13. 使用 git push -f 将变基后的分支再次推入仓库,准备合并。
    14. 合并分支到 dev。此过程若在 GitHub 上操作,只可以使用“Squash and merge“或”Rebase and merge“,不可以使用“Create a merge commit”。如果只打算保留 一个 commit,就用”Squash“。
    15. 迭代周期结束后,合入 master,等待发版。合入后之前所有的开发分支均作废。

    可以说,Git 是现代化软件开发的基础,各种操作规范都建立在 Git 规范之上,比如 CI/CD、自动化测试、Code Review 等等。学会 Git,采用合理的 Git 流程对稳定发布有很大帮助,建议大家都学好 Git、选用简单有效的 Git 规范构建更好的团队。

    上面的规范不一定适应所有团队,但在我长期的开发实践中,这套规则很好的提升了开发效率、保证了代码质量,推荐给大家参考。

    如果你对上文有问题,或者对 Git 有不同理解,欢迎留言评论。

  • Git 操作特定分支的小技巧

    Git 操作特定分支的小技巧

    加入新的开发团队之后,难免有一些东西不适应。比如,我团队要求把所有代码,不分前后端放到一个仓库里;而且,还都不删分支,于是现在代码仓库有 3k+ 分支……

    我很受不了,clone 一次代码大几个 G,我感觉我的固态硬盘在呻吟……

    于是简单研究了一下,只 clone 特定分支的技巧,记录如下。

    clone 特定分支最近一次提交

    git clone -b 分支名 --single-branch --depth=1 仓库地址

    上面这段命令可以只 clone 一个分支、最近一次提交,速度非常快。缺点是会损失代码提交记录,如果你要回顾历史代码,可能会比较麻烦。不过具体到我负责的这个项目,它的历史记录乱成一张蜘蛛网,git blame 出来全是各种 merged branch,实在没啥看头。

    fetch 特定分支最近一次提交

    只 clone 一个分支可能不太够,比如我要看别人的提交,就要把别人的分支拉下来。这个时候,如果你用上面的方式 clone 代码,再执行 git fetch 是拉不到其它分支的,因为本地 git 配置里就不存在别的分支。

    所以这时候要:

    git fetch --depth 1 origin 远程分支:本地分支
    git checkout 本地分支

    修改 .git/config

    除了上面的方法,你还可以修改 .git/config。打开 .git/config,你会看到这样的配置:

    [remote "origin"]
            url = git@github.com:username/repo.git
            fetch = +refs/heads/master:refs/remotes/origin/master

    如果直接 git clone 不加参数得到的仓库,“master”那里会是 *。此时,你可以把想要增加的分支列在后面,然后照常 git fetchgit checkout 即可。修改后的配置文件大约是:

    [remote "origin"]
            url = git@github.com:username/repo.git
            fetch = +refs/heads/master:refs/remotes/origin/master
            fetch = +refs/heads/bugfix:refs/remotes/origin/bugfix
            fetch = +refs/heads/release:refs/remotes/origin/release

    此时你就可以只操作 masterbugfixrelease 三个分支了,其它分支不会干扰你。