跳转至

2025

约 1 个字 预计阅读时间不到 1 分钟

OSPP2025 经验分享

约 6079 个字 1 张图片 预计阅读时间 20 分钟

了解到 OSPP 的契机是通过学校工作室,工作室的学长姐在去年的 OSPP 申请中成果颇丰,看着学长姐中选的项目一个个都非常的高大上,让我非常眼红,但是当时的我并不知道应该怎么去参加开源,怎么去找 issue 去做,可以说在今年正式参加 OSPP 之前,我是一位完全零经验的开源小白,只有 git 和 github 的基础使用经验。

在今年的 5 月份,OSPP 的项目开始逐步放出,我选项目的主题是跟 AI Agent 有关的项目,主要是从功利的角度去选的,因为今年 AI Agent 非常的火爆,初步锚定的是海立老师的项目,项目目标是用 langgraph 构建 k8s 的问答智能体,但是在沟通了两周之后,我觉得这个项目其实不太适合我,首先因为项目主力语言是 python 去搭 agent,同时还要写前端页面,我一直以来都是写 java 的,去从零学习两个新的领域并在一个月内写出方案对于我来说难度有点太大了,而且在写申请书的同时还在找实习,实在顶不住压力,就决定换项目了。

大概是 5 月中旬的时候,打算将目标项目更换为 java 中的 ai 项目,因为我本身并没有开源项目的贡献经验,所以并不考虑 Apache 相关的项目,卷度太高,最终选择了 LiteFlow 这个社区作为我的申请项目。

5 月可以说是我相对最忙的一月,一边忙着找实习,一边撰写 OSPP 的开源申请书,同时还要兼顾学校的课程。不过最终的结果是比较喜人的,在 5 月中下旬拿到了一家 AI 初创的 offer,在拿到 offer 后就不打算继续找了,一方面是这家初创给的实在太多了,比大厂后端最高档的还要高一档,另一方面是可以全身心投入到 OSPP 申请书的撰写中,事实证明我的选择还挺正确的。

接下来一起看看我的申请全过程

5.09 - 6.16 套磁与申请书撰写

首先必须要明确的一点是,套磁很重要!必须积极的和导师进行邮件沟通,如果能加到微信就更好了。

申请书撰写我使用的是飞书文档,在飞书文档上书写完毕后,使用飞书自带的导出成 pdf 功能导出即可。

今年 OSPP 的项目申请只能选择一个了,很多项目到后期甚至没人选,导致主办方将项目申请时间延长了两周,当时看着我申请的项目热度一点一点提高,给我吓得不轻。

下面是我的套磁邮件与导师回复:

  1. 2025-05-19
Text Only
我看了你的实现思路文档。


对于LiteFlow应该是看过一些源代码,并且对Dify是有过自己的实践和思考的。这个是正确的方向。


我说下我的想法和一些重点,你可以自行看下你的申请书还缺失了哪些


1. LF的核心就四大块, 规则如何解析,脚本如何解析,如何执行的,存储插件如何做到热更新规则/脚本(存储插件只看SQL即可),这部分需要细看,原理要阐述。这相当于LF的整个骨架。阐述要精炼,不用贴大量代码,关键部分指出来即可。这也是考量学生对LF源代码理解的能力。

2. LF和大模型结合,其实更多的还是如何调用大模型的能力,来更方便的提供开发者开箱即用的能力,不用开发者自己去写大模型组件。LF官方就为之封装好一些现成的插件可以使用。

3. 关于LF如何调用大模型,其实无非就是LF封装一些与大模型交互的插件。如何封装,涉及如何选型大模型的Client,选型是一个很重要的方面。比如说spring-ai, langchain4j等等,你可以详细了解下后者。选型定了之后,其实实现无非就是调用这些工具去和大模型平台进行交互。很多能力都是大模型平台直接提供的,我们先直接对接大模型平台的能力。不考虑自己本地去向量化,对接本地的向量数据库,也不考虑自己去存储记忆体。

4. 需要对接哪些大模型,以及哪些大模型平台。这个需要思考下,当然也要和第3点结合起来思考。当然不用大而全,大而全不一定是好的,骨架打结实,后面扩展也是非常容易的 。毕竟我们是一个开源框架,开源就意味着需要不断迭代。

5. 假设我们已经定义好了各种AI组件,那么使用者如何使用。我希望的是开箱即用,这意味着开发者无需自己再去写类去集成。就如同dify一样,dify是拖一个组件到画布上,然后在画布上选择模型,填写参数。LF是后端版本的dify,虽然没有界面。但是使用方面应该和dify应该如出一辙,直接在编排中写已经定义好的组件。那最关键的一点来了:如何去定义各个组件上的参数,以及如何标准化这些参数,哪些参数的scope为公共参数,哪些参数的scope为每一个组件的参数?现有LF体系中的规则参数设定有哪些?满不满足这次课题中的ai组件的需求?如果不满足,应该在LF原有体系中做出什么样的改变?(这一个大点才是这次课题中最重要最重要的设计,直接关系着使用者该如何使用,以及最终和现有LF体系结合的形态)

6. 考虑完了第5点之后,其实也就是入参设计,那么就要考虑每个ai组件的出参设计。LF天然的有上下文,而且上下文功能特别强大。如何加以运用?并且如何在表达式上实现后一个ai组件的入参引用前一个ai组件的出参。这就是出入参绑定设计。

7. 最后则需要考虑,现有的编排方式是否满足这次课题,需不需要增加新的表达式,我举例说明:有些大模型平台有种ai组件叫做意图分类的。那么意图分类在LF上用什么表达式来表达?第一想到的肯定是SWITCH,但是SWITCH是单选,而很多大模型的意图是多选,那么是不是要支持SWITCH的多选特性呢?这只是一个例子,需要自己思考这次课题有没有其他需要扩展表达式的场景。



其实这次课题需要考虑的点非常多,要很好的完成这次课题需要前期投入大量的思考和设计。很多功能需要和LF本身结合。所以需要花大量的精力去研读源码。

LF本身设计的非常饱满,而且也在不停的迭代。


既然是和AI有关,你也可以结合AI来辅助你分析源码,去弄懂它。但是得确保,你是真的理解了源码并拥有对于本次课题的设计大致思想。


加油~~源码中碰到不懂的也可以直接来问我。邮件一般24小时内会回。周末除外


---


> 导师您好,我已将自己对该项目的初步实现思路以及过程中遇到的一些疑问整理在了一份飞书文档中,方便您查阅。如您有空,还请帮忙看看,并给予一些指导意见,万分感谢!

> 飞书链接:
  1. 2025-05-28
Text Only
我仔细看了你的文档,能看出来是花了时间思考的。

但是对于这个课题的思考,需要抓住重点。以下是一些建议:



1. 关于三级参数配置体系,从参数的scope来说,有些需要全局配置,比如用对接什么平台,也就是base-url,api-key这些。但是有些参数只是组件层面的,比如这个组件用什么模型,组件分类,温度等参数。其实并没有三层,甚至于都算不上层的概念。只是不同的参数配在不同地方而已。在chain中配置完全没必要,因为有可能出现,一个chain中2个ai模型是不同的,参数也不一样。

2. AI组件设计,你不能完全照搬Dify,要结合LF,比如说start和end,在LF里没有这回事。还有什么变量赋值,发送http请求,现有LF就能完成这些。还是一个结合的问题。

3. AI组件设计,方案一,预先开发AI节点,的确会引起组件id冲突问题,如果去映射。这方式太死板。方案二,用户还要自己实现方法,那基本上违背了开箱即用这个诉求,至少在使用层面上不是太方便。提示:是否能用Annotation的声明式方式让用户去自定义ai组件呢?这个不需要类,只需要定义接口即可,接口如何变成AI组件,这需要靠动态代理,动态代理如何做?这样一来,同时解决了组件上的入参问题和NodeId冲突问题,那么prompt如何定义,动态化的入参出参怎么做?

    1. 当然我只是提出个概念,你也可以不按照我说的来设计,设计讲究自圆其说,相对优点最多,同时设计的缺陷控制在可以容忍的范围内。

4. 出入参的绑定问题,你文档上也有提到。你是采用定义<ai-nodes>这种方式来做的。

    1. 首先你的组件定义和出入参是分开来的,开发者需要定义两块内容。这种设计太割裂

    2. 其次出入参完全采用了xml的方式来定义,需要热更吗?如果需要,那热更怎么做?如果开发者想使用存储插件,比如sql,redis,etcd等,这整个的一套东西又该如何适配。适配需要有多大的复杂度,需要花多少时间?

5. Streaming怎么做,你完全没提到

6. Function Calling和MCP大概的方向是什么,你也没提到。



其实AI组件类型定义有几个是不重要的,你可以先考虑最基础的几个,大模型组件,知识库搜索组件,分类组件。主要就这3种,大模型组件同时还有function calling和mcp的变种,加上一个streaming。



设计才是最重要的,尤其是组件定义,出入参设计。



如果你要针对于原来的解析做改动,需要考虑的非常周全(这当然也需要对源码更深层次的理解),以及对各个其他大功能块的影响评估。以及复杂度的评估。



---



> 导师您好,我根据您上次的回复,写了一份初版申请书。
>
> 有一些问题想请教一下,首先我发现要是想要实现 AI 组件的一些扩展功能,需要对原有框架代码增加解析逻辑,这样做是否合理呢?
>
> 还有就是导师您对 dify 节点看法如何?需要全部实现吗,还是只要实现部分重要的节点即可?
>
> 最后,如果申请书有任何不足之处,还望导师指出,我会改正。
>
>
>
> 申请书飞书文档:
  1. 2025-06-3
Text Only
以上是你组件设计,包括出入参的一个大致的示意。



1. 对于一个AI组件来说,需要最基本的参数,比如模型选择,systemPrompt,userPrompt,history,temperature和maxTokens,这些你用一个@AIChat标签去聚合,没太大问题(可能少了几项)。

    1. prompt当中引用的变量用双花括号表示,这没啥问题。但是考虑过prompt这种需要输入长文本,尤其是带markdown格式的文本的情况吗?这种怎么解决?现实场景中很多prompt都是非常长的。

2. 输入参数这种结构也没啥问题,从context中绑定一些数据作为ai模型的输入参数,这部分其实是一个表达式,在LF有相应的解析公共方法。

3. 对于一个大模型返回,无非就两种,文本或者对象(对象对应着JSON),那你的@AIOutPut是否也应该提供相应选项?

    1. 你选择了输出参数也绑定到原有上下文中。这部分没啥问题

4. 看起来以上所有的定义和你定义的方法没有任何关系,你返回ChatAIResult是什么意思呢,是想说明绑定的对象是ChatAIResult类型吗?那你做到@AIOutPut标签里去不就好了么。


这个接口只是用来定义大模型,最后反正要被动态代理,所以你所有的标注定义在类上看起来也完全成立。这个接口就是一个标识接口。




那知识检索组件和意图分类组件呢?只在你的文档中见到了标注的定义,但是没有见到最终的呈现方式

该如何设计,如何又和上面达成统一,或者不统一也行,你只要能自圆其说就ok




流式输出最大的点在于LF的FlowExecutor是同步返回,流式输出怎么在一个同步返回的里面持续向外streaming呢。注解层面其实提一下就可以了,不是太重要。



---



> 导师您好,我已按照您的反馈对项目申请书进行了修改,完成了第二版。麻烦您在百忙之中审阅一下
>
>
>
> 飞书文档链接:
  1. 2025-06-4- 12:33
Text Only
给你举个例子,以上是coze官方的其中一个示例。



{{}}在prompt中的确是占位的作用,表示动态参数,而整个prompt模版是静态的。整个prompt模版就是一个长文本。



在你的使用模式中,红框部分本身就是一个prompt模版。如果这部分太长了,怎么办。而且写在这里面会丢失格式信息。如果这部分直接用{{}}来占位。每次调用的时候让用户自己拼成大prompt自己传吗?



所以需要考虑到实际使用便捷度。



FlowExecutor本身就有返回CompletableFuture的接口。不需要额外加方法,多从异步和SseEmitter,以及langChain4J的流式处理方向去结合思考。



申请书按照ospp要求上传,具体看官网。排版没太大要求,章节清晰就可以了。等所有同学都提交了,我们会综合评定来确定人选



---



> 导师您好,看了您的邮件回复后有一些疑惑想跟您交流一下。
>
>
>
> 首先是长文本问题,这个我认为直接使用占位符解析似乎没什么问题?将文本直接解析到占位符就行吧
>
>
>
> 流式输出的问题,我看到FlowExecutor有一个方法是execute2Future,那么用户在需要流式输出的时候应该调用这个异步方法,这样主线程就不会阻塞了,但是Future 接口的结果获取是同步且阻塞的,如果我们用户想要直接返回一个 SseEmitter 到前端同时需要在流程结束的时候处理相关的业务逻辑,那么因为 Future 接口会阻塞主线程,就导致流式输出已经输出完了,这个请求才结束,所以这样做似乎不太合理。
>
>
>
> 那么,能不能在FlowExecutor加一个执行方法,这个方法返回的是 CompletableFuture,那么用户就可以通过编写回调函数去处理流程完成之后的业务逻辑,然后主线程直接返回 SseEmitter 就可以。
>
> 然后,我突然意识到我之前的流式输出方案是不合理的,因为如果是在注解层面设置对应的 Spring Bean ID 的话,那么用户的处理逻辑在进程启动前就是定死的,如果说需要在流式传输中用到一些当前请求的局部变量的话,那么是获取不到的。所以,我觉得应该将 StreamHandler 放在上下文中,那么用户在执行一个流程之前,都可以对处理逻辑进行自定义,相对是更加灵活的。
>
>
>
> 申请书的话,最后是上传 pdf 就行吗?然后排版上是否有特殊的要求?
  1. 2025-06-5
Text Only
关于prompt,你的思考是正确的。
但是介于你的三个主要的标注参数比较多,需要从开发者的角度多去想想,怎么样简单开发我们就怎么去设计。
所以prompt不用把Resource的类型和直接写的分开。就systemPrompt和userPrompt两个可以包含所有情况,用户怎么定义,我们去适配,智能的识别。

关于sse的思考,我看了你的方案,的确CompletableFuture返回可能更好。传入回调函数可以解决streaming。

凡是用户需要直接定义的东西,希望要站在用户侧去思考。尤其这个课题,需要用户侧定义的东西还挺多,需要格外注意。

总体不错,看来是花了时间思考的。加油


---

导师您好,又修改了一版申请书,主要修改和添加的部分是 4.4,4.5,4.7 章节

然后申请书的 pdf 版本在附件中


长文本 prompt 我应该理解导师您的意思了。

但是流式输出的问题,我真没找到FlowExecutor有返回 CompletableFuture 对象的方法啊,我只看到 execute2Future 是返回 Future 接口的。

这是一个异步的执行流程,那么肯定是不会阻塞主线程的,但是要是主线程调用 furture.get()方法,这个获取结果的方法是同步且阻塞的,所以后续的操作不会执行,那么比方说有这样一个场景。

1. 请求进入
2. 在上下文设置流式输出处理器,调用 AI 相关节点
3. 调用execute2Future方法,获取 future 对象
4. future.get()获取执行结果,并对结果进行业务处理
5. return 流式输出处理器给前端

问题就在 4 -> 5 这个阶段,后端要是想要使用 SSE 协议对前端进行流式输出,那么应当直接返回相关的处理器,但是阶段 4 阻塞住了,所以就会导致 LLM 节点流式输出已经关闭了才返回相关的处理器,那么前端是收不到任何消息的。

当然解决上述问题可以靠用户自行完成,比如说我不调用 future.get()方法,那么就不会阻塞。比如说用户自己起一个 CompletableFuture 然后在里面写 future.get() ,再用回调逻辑做相关的业务处理。

但是我们为何不直接返回一个 CompletableFuture 对象呢?我觉得这样做是合理的。相关的讨论我放在申请书的 4.7 章节。

飞书链接:
  1. 2025-06-19- 12:20 导师微信通知中选

image.png

最终申请书 pdf 有 36 页,在申请书提交截止的两天后成功中选!!!

7.01 - 9.30 项目开发

在中选后的那股兴奋劲消退之后,我开始思考应该如何实现这个项目,此时的我并不知道写这鬼项目能这么折磨。

首先,申请书是我和 AI 一起写的,光是写申请书就要将我燃尽了,所以我并没有写什么 mvp 去验证我的方案可行性。

然后,中选之后我发现我的方案怎么这么像一坨 shit 呢,欸,要不不按方案写的来,我按我自己的想法写一个新的方案实现吧🤓

在 7 月初到 7 月中旬,我因为我的灵机一动一直在按我自己的想法写,我的想法大概就是底层调用 AI 模型不用开源的实现,而是自己尝试去写一套,那么在一顿自认为惊为天人的设计并用代码实现了一版之后,我信心满满地发微信跟导师说:“看看我写的怎么样🤓”

不出意外,那么要出意外了

导师把我狠狠喷了一顿,说,你自己写的再好能有开源的 langchain4j 写的好吗,不要把精力花在没必要的地方,能用开源的就用开源的

给我直接整抑郁了,不过也没办法,只能顺从了,那就用 langchain4j 去写吧!

在 7 月中旬到 7 月底,我用 langchain4j 的方案实现了一版,并且发给了导师,不出意外的话,这大概就是最终的版本了

然后出意外了= =

导师说,langchain4j 的方案也不行,首先 langchain4j 要求 jdk17 及以上,而 LiteFlow 需要支持 jdk8 及以上,这依赖就没法用,再者 langchain4j 的 api 封装的过于高层了,这个项目只需要一个非常基础的调用大模型的能力即可

又给我整抑郁了,我真是日了

但是项目还是要写的,毕竟是我自己选的,就算是史我也吃了

最后和导师沟通之后,最终敲定按我原来的方案实现,那么也就是自己去写一套底层实现。

好在这两版方案是可以互通的,总之也不算白写,那么在 8 月份一直在慢慢去实现相关的代码。

这里没有说导师不好的意思,相反我非常感谢导师对我的指导,我觉得自己真是运气爆棚,能遇到一个这么好的导师,每一次去问问题,导师都是一大段一大段的回复我,去讨论相关的方案实现,确实受益很大。

到 8 月下半的时候,感觉导师也算认可我了,也就开始讨论结项相关的事宜。

截止 8 月底,核心代码的代码量共计 1w5 行左右(不包含测试),但是还没有完全完善,需要慢慢的进行迭代。

导师这边非常的善解人意,允许直接合入我的 pr,并且告知我说,不需要担心 ospp 的时间节点,可以直接合入,但是后续需要进行相关的迭代工作

所以不出意外的话我这边其实已经可以开香槟了,后续只要跟着 ospp 的流程走就能顺利结项。

然后踏马又出意外了

ospp 的结项审核,因为我的 PR 的 commit 太多报错了,导致直接访问不了,其实也不多,就 87 个 commit,问客服之后,情况是这样的: "后台反馈是因为PR中包含的commit数量太多导致限制访问,被第三方平台解析禁止。建议精简commit数量,具体操作方法可与导师讨论决定。"

不是你告诉我怎么精简 commit?都已经合进去,还有这不是你们 OSPP 后台服务的设计问题吗?

不过跟导师反馈后,导师说能够帮我解决,实在不行就走人工审核,总之先这样吧

最后的最后,很感谢 OSPP 和 LiteFlow 社区能够给我一次参与开源的机会。我在一开始申请的时候,从来没想过自己也能够参与开源,甚至一度怀疑自己的能力是否能够胜任,不过真真正正投入进去之后,会发现曾经认为很难的事情或许并不可怕,重要的是让自己行动起来。

Missing Semester of MacOS - A Vim Enthusiast Programmer’s Perspective

约 1679 个字 14 行代码 26 张图片 预计阅读时间 6 分钟

前言

当我经历了使用 MacBook Air 开启 docker + n 个 idea 项目 + cursor + vscode + 一堆浏览器标签页 卡到爆炸的体验之后,决定迁移我的开发环境到 MacBook Pro 上,在购买前了解到 mac 有非常"先进"的 Migration Assistant,能无缝复制所有的配置到新的 MacBook 上(内心 os: 哎呀这真是个好东西,看来不用自己再配置一遍了)

然后... 不出意外地出了意外,我的 mba 的 Migration Assistant 抽风了,程序一进入到选择页面就是彻底卡死->黑屏->等待几小时依然黑屏->重启->重新启动程序->彻底卡死->....

我甚至没有在网上搜索到任何一位有跟我类似的卡死受害者,一些方法比如将系统版本升级到最新版都试过了,依然无效。

总之放弃了,正好一直想跟别人推荐我的 MacOS 配置,而且之前使用的过程中还是拉了很多史在自己的电脑里,那么就借此记录一下我是如何从零配置我的 MacOS 的吧。

系统设置

开启三指拖拽

image.png

开启轻点

image.png

去掉动画效果

image.png

自动隐藏 dock

image.png

禁用根据最近使用重排序窗口

image.png

finder 默认打开在 user 文件夹 (打开 finder 窗口,左上角 settings)

image.png

finder sidebar 配置,按自己喜欢的来

image.png

Mac 长按连续输入 (重启生效)

Bash
defaults write -g ApplePressAndHoldEnabled -bool false

禁用键盘自动加句号

image.png

image.png

键盘按键 repeat 拉满

image.png

打开 Terminal,输入

Bash
1
2
3
cd ~/Documents
mkdir {Code,Dev-Tools,docker,Privacy,Resources}
mkdir Code/{github.com,Java,Go,Python,vscode,FrontEnd,CS\ Course}

这是我喜欢的 mac 文件管理方式,我的 mac 的桌面上不会有任何文件

  • Code: 存放代码文件
    • github.com
    • Java
    • Go
    • Python
    • vscode
    • FrontEnd
    • CS Course
  • Dev-Tools:开发工具,比如 maven
  • docker:docker-compose 文件
  • Privacy:隐私文件
  • Resources:学习资料

CS Course.png

keyboard shortcuts 中打开快捷键切换桌面,使用 option+数字键切换 (需要创建多个桌面才会有选项)

image.png

网络

这里使用 Clash Verge

你跟我说要 VPN 才能去 github 下 clash?那你下个 Clash 啊 ()

HomeBrew

执行以下代码,跟着配置就行

Bash
/bin/zsh -c "$(curl -fsSL https://gitee.com/cunkai/HomebrewCN/raw/master/Homebrew.sh)"

Arc

神中神!下载 Arc 浏览器

Download Arc

image.png

一些快捷键

  • cmd + t :打开搜索框,搜索框内输入不同 space 的名称,找到 focus on space名称 这个选项,即可实现切换 space 效果
  • cmd + shift + c :复制当前网站链接
  • cmd + shift + option + c :以 md 格式复制当前网站链接,名字+链接
  • cmd + s :打开/关闭侧边栏

Raycast

神中神!吊打 spotlight

Raycast - Your shortcut to everything

安装完毕之后,在系统设置的 keyboard shortcuts 中关闭 spotlight

image.png

更改 raycast 快捷键

image.png

image.png

后续下载别的软件后会进一步配置

微信输入法

神!比原生输入法好用,选它主要是我要配置一些东西 + 跨设备复制同步

微信输入法-简洁好用打字快

image.png

现在使用 control + space 或者 fn 切换输入法

Karabiner-Elements

神中神!改键位无敌,真的好用

Karabiner-Elements

先把 mac 内置键盘的 fn 和 control 调换一下位置 (我说 mac 键盘设计真是一坨,control 放那个位置)

image.png

接着进行复杂的键位映射

image.png

  1. 映射 cmd + shiftcontrol + space,现在切换输入法可以使用 cmd + shift
  2. 映射大写键到 cmd + shift + option + control ,大写键现在没用了,设置为辅助键位
  3. 映射大写键 + hjkl 到方向键,vim 用户应该懂这个的含金量,无敌!

相关的配置代码如下

Text Only
{
    "description": "Command + Shift to control + spacebar",
    "manipulators": [
        {
            "from": {
                "key_code": "left_shift",
                "modifiers": { "mandatory": ["left_command"] }
            },
            "to": [
                {
                    "key_code": "spacebar",
                    "modifiers": ["left_control"]
                }
            ],
            "type": "basic"
        }
    ]
}
Text Only
{
    "description": "Change LeftShift+LeftCommand+LeftControl+LeftOption+hjkl to arrow keys",
    "manipulators": [
        {
            "from": {
                "key_code": "h",
                "modifiers": {
                    "mandatory": ["left_shift", "left_command", "left_control", "left_option"],
                    "optional": ["any"]
                }
            },
            "to": [{ "key_code": "left_arrow" }],
            "type": "basic"
        },
        {
            "from": {
                "key_code": "j",
                "modifiers": {
                    "mandatory": ["left_shift", "left_command", "left_control", "left_option"],
                    "optional": ["any"]
                }
            },
            "to": [{ "key_code": "down_arrow" }],
            "type": "basic"
        },
        {
            "from": {
                "key_code": "k",
                "modifiers": {
                    "mandatory": ["left_shift", "left_command", "left_control", "left_option"],
                    "optional": ["any"]
                }
            },
            "to": [{ "key_code": "up_arrow" }],
            "type": "basic"
        },
        {
            "from": {
                "key_code": "l",
                "modifiers": {
                    "mandatory": ["left_shift", "left_command", "left_control", "left_option"],
                    "optional": ["any"]
                }
            },
            "to": [{ "key_code": "right_arrow" }],
            "type": "basic"
        }
    ]
}
Text Only
{
    "manipulators": [
        {
            "description": "Change caps_lock to command+control+option+shift.",
            "from": {
                "key_code": "caps_lock",
                "modifiers": { "optional": ["any"] }
            },
            "to": [
                {
                    "key_code": "left_shift",
                    "modifiers": ["left_command", "left_control", "left_option"]
                }
            ],
            "type": "basic"
        }
    ]
}

Karabiner-Elements 还可以进行其他配置,比如禁用 mac 的内置键盘,然后你的外置键盘就能直接放在内置键盘上了

image.png

Raycast 再配置

在刚刚进行了 Karabiner-Elements 的配置之后,我们已经将大写键作为辅助键位映射到了 cmd + shift + option + control ,所以我可以 100% 保证不可能有键位冲突

让我们输入 cmd + , 打开 raycast 的 settings,找到 extensions

这里以我 mba 上的配置为例,仅供参考,你可以按你喜欢的来

image.png

现在切换窗口已然变得非常丝滑,应用之间使用 CapsLock+字母 进行切换,如果想切换到桌面,比如访问 finder,那么只需要输入 option+数字 即可

Arc 再配置

改一个键位即可,映射 CapsLock + N 到打开 little arc window

image.png

什么是 little arc window?

这东西灰常好用啊,比如我现在全屏的 obsidian 窗口,我希望查询一个东西,那么就可以打开小窗口,查完 cmd + w 关闭即可,非常的方便

rt

image.png

iterm2

iTerm2 - macOS Terminal Replacement

shell 相关的配置请看 oh-my-zsh

or 你可以直接到我的 dotfiles 仓库 LunaY-dotfiles 找到 zsh 文件夹的 .zshrc

把文件 mv 到 ~ 下

记得安装字体,然后去 iterm2 的 settings 中选择 hack-nerd-font

Bash
brew install font-hack-nerd-font

tmux

tmux 插件管理器

Bash
git clone https://github.com/tmux-plugins/tpm ~/.tmux/plugins/tpm
Bash
brew install tmux
tmux

dotfiles 仓库 LunaY-dotfiles 找到 tmux 文件夹下的 .tmux.conf

然后 mv 到 ~ 文件夹下

输入

Bash
tmux source ~/.tmux.conf

键位映射很久之前配的了,说下我常用的

  • prefix: control+z
  • 水平切分窗口prefix + |
  • 垂直切分窗口: prefix + -

Lazygit

神中神!用了都说好,终端里的 git 图形化界面

Bash
brew install lazygit

Snipaste

神!截图软件,苹果自带的截图也是一坨

Snipaste - 截图 + 贴图

这边把截图快捷键映射到大写键+1,因为我的键盘的 f1 键要用 fn+1 才能按出来很不方便

image.png

如何使用?

  • 大写键+1:开始截图
  • ,: 上一张截图
  • :下一张截图

Input Source Pro

神!自动切换输入法

Bash
brew install --cask input-source-pro

或者去官网下载

Input Source Pro - 自动切换输入法加上适时的提示,让每一次输入都游刃有余

一些配置

image.png

image.png

DropOver

一般。相当于给你一个 buffer 复制粘贴文件

但是!这软件免费版移动文件到 buffer 里要先等 3 秒,呃呃

不过还是很好用的

Dropover - Easier Drag and Drop on your Mac.

RunCat

哈基米!挂右上角看系统信息的

总之可爱就行

PicGo

神中神!无需多言

Bash
brew install picgo --cask

Scroll Reverser

翻转鼠标滚轮用的

Scroll Reverser for macOS

idea

我的 ideavim 配置:ideavimrc

vscode

Bash
brew install --cask visual-studio-code

我的 vscode + vscode vim 配置:vscode

neovim

我的 neovim 配置:neovim

最长回文子串

约 623 个字 38 行代码 1 张图片 预计阅读时间 3 分钟

题目链接: 5. 最长回文子串

题解链接:5. 最长回文子串 - 力扣(LeetCode)

中心扩散法

从每一个位置出发,向两边扩散即可。遇到不是回文的时候结束。举个例子,str=acdbbdaa 我们需要寻找从第一个 b(位置为 3)出发最长回文串为多少。怎么寻找?

  1. 首先往左寻找与当期位置相同的字符,直到遇到不相等为止。
  2. 然后往右寻找与当期位置相同的字符,直到遇到不相等为止。
  3. 最后左右双向扩散,直到左和右不相等。如下图所示:

image.png

每个位置向两边扩散都会出现一个窗口大小(cur_len)。如果 cur_len > length(用来表示最长回文串的长度)。则更新 length 的值。

因为我们最后要返回的是具体子串,而不是长度,因此,还需要记录一下 length 时的起始位置(start),即此时还要 start = left。

Python
class Solution:
    def longestPalindrome(self, s: str) -> str:
        if s is None or len(s) == 0:
            return ''
        n = len(s)
        start = length = 0
        for i in range(n):
            left, right, cur_len = i - 1, i + 1, 1
            while left >= 0 and s[left] == s[i]:
                left -= 1
                cur_len += 1
            while right < n and s[right] == s[i]:
                right += 1
                cur_len += 1
            while left >= 0 and right < n and s[left] == s[right]:
                left -= 1
                right += 1
                cur_len += 2
            if cur_len > length:
                length = cur_len
                start = left + 1
        return s[start:start+length]

动态规划

中心扩散的方法,其实做了很多重复计算。动态规划就是为了减少重复计算的问题。动态规划听起来很高大上。其实说白了就是空间换时间,将计算结果暂存起来,避免重复计算。作用和工程中用 redis 做缓存有异曲同工之妙。

我们用一个 boolean dp[l][r] 表示字符串从 i 到 j 这段是否为回文。

试想如果 dp[l][r]=true,我们要判断 dp[l-1][r+1] 是否为回文,只需要判断字符串在 (l-1) 和(r+1) 两个位置是否为相同的字符。

进入正题,动态规划关键是找到初始状态和状态转移方程。

初始状态,l=r 时,此时 dp[l][r]=true。

状态转移方程,dp[l][r]=true 并且 (l-1) 和(r+1) 两个位置为相同的字符,此时 dp[l-1][r+1]=true。

Python
class Solution:
    def longestPalindrome(self, s: str) -> str:
        if s is None or len(s) == 0:
            return ''
        n = len(s)
        start = end = 0
        length = 1
        dp = [[False] * n for _ in range(n)]
        for r in range(n):
            for l in range(r):
                if s[l] == s[r] and (r - l <= 2 or dp[l + 1][r - 1]):
                    dp[l][r] = True
                    if r - l + 1 > length:
                        length = r - l + 1
                        start, end = l, r
        return s[start:end+1]

if s[l] == s[r] and (r - l <= 2 or dp[l + 1][r - 1]): 这个判断条件的含义是

1. s.charAt(l) == s.charAt(r)

这一部分表示:当前字符串左右两端的字符是否相等

  • 如果 s[l] 和 s[r] 不相等,那么从 l 到 r 之间的子串显然不可能是回文串,因此直接跳过后续判断。

  • 如果相等,就进一步判断该子串中间部分是否是回文,从而确定整个子串是否是回文串。

2. (r - l <= 2 || dp[l + 1][r - 1])

这一部分表示:在两种情况下,从位置 l 到 r 的子串可以被确认是回文串

(1) r - l <= 2

如果 r - l <= 2,说明 l 和 r 之间最多只有 0 个或 1 个字符,这种情况下:

  • s[l] == s[r] 就足以判断该子串是回文串了,因为中间的部分要么为空(r - l = 1),要么只有一个字符(r - l = 2)。

  • 示例:对于 aba、aa,只需要两端相等即可确认它们是回文。

(2) dp[l + 1][r - 1]

如果 r - l > 2,说明 l 和 r 之间至少隔着两个字符,那么子串 [l+1, r-1] 是否是回文就变得重要。

  • dp[l + 1][r - 1] 是一个动态规划数组,记录从位置 l+1 到 r-1 的子串是否是回文。

  • 如果 dp[l + 1][r - 1] == true,说明该子串是回文串,再加上 s[l] == s[r],可以确认 [l, r] 也是回文串。

  • 如果 dp[l + 1][r - 1] == false,那么中间部分不是回文,即使两端字符相等,也无法形成回文串。

手撕快速排序

约 267 个字 35 行代码 2 张图片 预计阅读时间 1 分钟

题目连接: 912. 排序数组

代码 (三路快排)

Python
class Solution:
    def sortArray(self, nums: List[int]) -> List[int]:
        self.quick_sort(nums, 0, len(nums) - 1)
        return nums

    def quick_sort(self, nums: List[int], left: int, right: int) -> None:
        if left >= right:
            return
        le, gt = self.partition(nums, left, right)
        self.quick_sort(nums, left, le - 1)
        self.quick_sort(nums, gt, right)

    def partition(self, nums: List[int], left: int, right: int):
        random_index = random.randint(left, right)
        nums[random_index], nums[left] = nums[left], nums[random_index]
        pivot = nums[left]

        # 循环不变量:
        # [left, le - 1] < pivot
        # [le, i] = pivot
        # [gt, right] > pivot
        le = left
        gt = right + 1
        i = left + 1
        while i < gt:
            if nums[i] < pivot:
                nums[i], nums[le] = nums[le], nums[i]
                le += 1
                i += 1
            elif nums[i] == pivot:
                i += 1
            else:
                gt -= 1
                nums[i], nums[gt] = nums[gt], nums[i]
        return le, gt

三路快排

思想:把整个数组分成三个部分,引入两个指针,le (less equal) 和 gt (great than)

image.png

此时有:

  1. nums[left, le - 1] 均小于 V
  2. nums[gt, right] 均大于 V

初始值:

  • le = left
  • gt = right + 1

遍历指针 i 从 left + 1 开始

  1. 遇到比 V 小的,交换 (le, i),le、i 均后移一位
  2. 遇到等于 V 的,不用交换,i 后移一位
  3. 遇到比 V 大的,gt 前移一位,交换 (gt, i)
    • 注意:此时 i 不能后移,因为交换过来的 nums[gt] 之前未看过

现在考虑退出循坏的条件,i 碰到 gt 即可

不要取 "=",因为 i 没看过,表示下一个要看,gt 的值都看过了,故 while i < gt

循环停止时

image.png

此时有:

  1. nums[left, le - 1] 均小于 V
  2. nums[gt, right] 均大于 V
  3. nums[le, gt - 1] 均等于 V

返回 le, gt, 并回到 quick_sort 函数

  • 递归调用左区间 [left, le - 1]
  • 递归调用右区间 [gt, right]
  • 直到排序完成

K 个一组翻转链表

约 138 个字 36 行代码 预计阅读时间 1 分钟

题目链接: 25. K 个一组翻转链表

Python
# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, val=0, next=None):
#         self.val = val
#         self.next = next
class Solution:
    def reverseKGroup(self, head: Optional[ListNode], k: int) -> Optional[ListNode]:
        '''
        pre 是这次翻转后的头节点
        p0 是上一次翻转的尾节点
        nxt = p0.next 是这次翻转前的头节点/这次翻转后的尾节点
        cur 是下一次翻转前的头节点
        在一次翻转完成之后
        nxt.next = cur 这一次翻转的尾节点应指向下一次的头节点
        p0.next = pre 上次翻转的尾节点应指向这次翻转的头节点
        p0 变为这次翻转后的尾节点
        '''
        n = 0
        cur = head
        while cur:
            cur = cur.next
            n += 1
        p0 = sentinel = ListNode(next=head)
        pre, cur = None, head
        while n >= k:
            n -= k
            for _ in range(k):  
                nxt = cur.next
                cur.next = pre
                pre = cur
                cur = nxt
            nxt = p0.next
            nxt.next = cur
            p0.next = pre
            p0 = nxt
        return sentinel.next

pre 是这次翻转后的头节点

p0 是上一次翻转的尾节点

nxt = p0.next 是这次翻转前的头节点/这次翻转后的尾节点

cur 是下一次翻转前的头节点


在一次翻转完成之后

nxt.next = cur 这一次翻转的尾节点应指向下一次的头节点

p0.next = pre 上次翻转的尾节点应指向这次翻转的头节点

p0 变为这次翻转后的尾节点

01.IdeaGit-Demo

约 198 个字 2 行代码 9 张图片 预计阅读时间 1 分钟

准备

  1. 创建一个github repo

  1. 创建一个idea项目

注意到,github repo中是存在一个预先定义的README文件,而idea项目中不存在

尝试提交并推送

找到左侧commit选项,选中全部并点击Commit

commit完成之后右键master分支,将其改名为main,目的是保证和github repo中的分支保持一致

右键main分支,尝试push

此时还未定义git remote,点击Define remote,复制github repo的链接粘贴进去,再次尝试推送

不出意外,应该要出意外了,idea会提示push reject,代表出现冲突

解决办法如下:

  1. 命令行
Bash
git fetch
git pull --rebase origin main
  1. idea手动拉取

首先进行fetch,然后进行pull

我这里选择命令行执行

此时 push 成功,rt

02.IdeaGit 基础操作

约 1713 个字 26 行代码 31 张图片 预计阅读时间 6 分钟

1. 初始化

Bash
git init

查看目录结构中,就可以看到包含有 .git 子目录,这就说明创建版本库成功

相当于idea中的 vcs----create git repository

2. 将文件添加到版本库

2.1 将文件添加到暂存区

Bash
git add 001.txt        // 添加单个文件到暂存区
git add .                // 将当前目录下所有修改添加到暂存区,除按照规则忽略的之外

相当于idea中的git --> add

不同之处是在idea中即便不进行add,依旧可以commit


2.2 将暂存区文件提交到仓库

Bash
git commit        // 如果暂存区有文件,则将其中的文件提交到仓库
git commit -m 'your comments'         // 带评论提交,用于说明提交内容、变更、作用等

这个和idea中的git -commit一致,不同点:注释必须写


2.3 查看仓库状态

Bash
git status

在idea中,直接在commit界面可以看到改动的所有文件以及未add的文件

红色: 代表未add到暂存区
绿色: 代表已在暂存区但是有修改
无标记:代表无修改


2.4 查看仓库中的具体修改

Bash
git diff    // 查看版本库中所有的改动
git diff 001.txt        // 查看具体文件的改动

在idea中,可以在commit界面中双击文件查看,或者也可以通过右键标签--show diff with working tree查看


2.5 查看历史提交记录

Bash
git log     // 显示所有提交的历史记录
git log --pretty=oneline    // 单行显示提交历史记录的内容

相当于idea中的git窗口,同时commit id 位于右下角


2.6 版本回退

Bash
1
2
3
git reset --hard HEAD^        // 回退到上一个提交版本
git reset --hard HEAD^^        // 回退到上上一个提交版本
git reset --hard 'commit_id'    // 会退到 commit_id 指定的提交版本

如下图,某个git仓库中存在2个提交记录,需要版本回退到第一个记录

输入 git reset --hard f731

此时,如果反悔了,不希望回退,如何重新返回?

输入git log,发现仅存在一条提交记录,查询不到第2条提交记录

可以通过git reflog找到之前的提交记录

Bash
git reflog
git reset --hard 'commit_id'


git工作区域

git reset的方式不仅仅只有hard一种,还有softmixedkeep,而在介绍它们之间的异同前,首先需要理解git的工作区域

  • Workspace: 工作区,就是你平时存放项目代码的地方
  • Index / Stage: 暂存区,用于临时存放你的改动,事实上它只是一个文件,保存即将提交到文件列表信息
  • Repository: 仓库区(或版本库),就是安全存放数据的位置,这里面有你提交到所有版本的数据。其中HEAD指向最新放入仓库的版本
  • Remote: 远程仓库,托管代码的服务器,可以简单的认为是你项目组中的一台电脑用于远程数据交换

执行reset命令后还存在文件的区域

git reset


2.6.1 git reset --soft

操作

移动本地库HEAD指针

前置准备(其他演示类似不做过多赘述)

类似上文,某个git仓库中存在2个提交记录,需要版本回退到第一个记录

远程仓库如下图所示

输入下述命令,使用soft回滚,并推送到远程仓库

Bash
git reset --soft 'commitid'
git push --force origin master # 注意这里要强制推送

可以看到远程仓库中第2次提交不存在了,意味着版本库发生修改

运行 git statusll(alias for ls -l), 观察结果可知工作区和暂存区未发生修改

结论

soft只影响版本库,不影响工作区和暂存区


2.6.2 git reset --mixed

操作

移动本地库HEAD指针
重置暂存区

输入下述命令,使用mixed回滚,并推送到远程仓库

Bash
git reset --mixed 'commitid'
git push --force origin master # 注意这里要强制推送

可以看到远程仓库中第2次提交不存在了,意味着版本库发生修改

运行 git statusll(alias for ls -l), 观察结果可知工作区未发生修改,暂存区发生修改(丢失了对t2的追踪)

结论

mixed影响版本库和暂存区,不影响工作区


2.6.3 git reset --hard

操作

移动本地库HEAD指针
重置暂存区
重置工作区

输入下述命令,使用hard回滚,并推送到远程仓库

Bash
git reset --hard 'commitid'
git push --force origin master # 注意这里要强制推送

可以看到远程仓库中第2次提交不存在了,意味着版本库发生修改

运行 git statusll(alias for ls -l), 观察结果可知工作区发生修改,暂存区发生修改(丢失了对t2的追踪)

结论

hard影响版本库、暂存区和工作区


2.6.4 git reset --keep

操作

移动本地库HEAD指针
重置暂存区
重置工作区, 保留改动

keep模式和hard模式有些类似,但是对工作区的操作有些不同,具体如下

工作区中文件如果当前版本和退回版本之间没有发生过变动,则工作区的修改保持不便;如果发生了变动,并且工作区也进行了修改,需要自行合并(或者冲突解决)

1. 工作区文件未发生变动

Bash
git reset --keep 'commitid'

hard模式保持一致,版本库、暂存区和工作区均发生修改

2. 工作区文件发生变动

reset之前,将t1文件中的111改为311,并添加到暂存区

执行reset

Bash
git reset --keep 'commitid'

神奇的是,加入暂存区的对应文件发生的变动也被保留了下来

再来看变动的另一种情况,在reset之前,将t1文件中的111改为311,且将t2文件中的222改成322,并添加到暂存区

执行 reset

Bash
git reset --keep 'commitid'

发现出现报错,原因是f731版本中t2文件是不存在的,导致无法merge

结论

keep影响版本库、暂存区和工作区,可以撤销提交而不丢失文件的更改


接下来学习idea git中的版本回退

首先在项目中创建一个名为 GitResetTest.java 的文件

添加到暂存区并提交

修改文件,再次提交

右键希望回退到的版本,选择Reset

选择hard模式

如果希望撤销回滚,同样需要查询reflog, 打开terminal输入git reflog, 找到对应的commitid

通过右键项目的reset head进行回退


2.7 撤销修改

Bash
git checkout -- 004.txt   // 如果 004.txt 文件在工作区,则丢弃其修改
git checkout -- .            // 丢弃当前目录下所有工作区中文件的修改

idea中:

注意到在rollback右侧有一个静默搁置按钮

点击之后修改的代码也会消失,这个功能主要是用于暂存你修改后的代码,并把你工作区的代码回滚到最后commit的记录,恢复的时候通过:

那么这个操作具体使用场景是什么呢?

试想一下,在我们开发的时候,正在写某个需求的代码,然而此时项目经理要求你去做一个更加重要/紧迫的任务,那么现在这部分需求代码暂未写完,是不能提交的,此时就可以使用静默搁置,将代码暂时搁置,等到把项目经理的任务忙完后,选择取消静默,接着写即可


2.8 删除文件

在被 git 管理的目录中删除文件时,可以选择如下两种方式来记录删除动作:

  1. rm + git commit -am "abc"
  2. git rm + git commit -m "abc"

区别在于

  • 如果文件已经被git跟踪,那么 1、2 都可以正常工作
  • 如果文件未被git跟踪
    • rm + git commit -am "abc" 不会提交删除
    • git rm + git commit -m "abc" 可以确保文件删除被git记录并提交

03.IdeaGit 远程仓库

约 149 个字 5 行代码 2 张图片 预计阅读时间 1 分钟

1. 克隆仓库

Bash
git clone 你的仓库地址

idea中即创建项目的时候选择 from version control

2. 添加远程仓库

Bash
git remote add origin 你的仓库地址

3. 查看远程仓库信息

Bash
git remote [-v] // 显示远程仓库信息

idea中

4. 推送本地内容到远程仓库

Bash
git push -u origin <branch>

-u 代表设置上游分支。

在执行 git push -u origin <branch> 后,Git 会将当前本地分支与远程仓库的 <branch> 分支关联。之后,你可以简单地使用 git pushgit pull,Git 会知道应该推送/拉取哪个远程分支,而不需要每次都指定远程分支的名称。

5. 修改本地仓库对应的远程仓库地址

Bash
git remote set-url origin url

04.IdeaGit 分支管理

约 1063 个字 29 行代码 25 张图片 预计阅读时间 4 分钟

1. 查看分支

Bash
1
2
3
git branch        // 查看本地分支信息
git branch -v     // 查看相对详细的本地分支信息
git branch -av     // 查看包括远程仓库在内的分支信息

注意:在 git branch 的输出内容中,有一个分支,前面带有 * 号,这标识我们当前所在的分支。

2. 创建分支

分支创建后,新分支基于当前分支。

Bash
git branch dev 

idea 中

3. 切换分支

Bash
git checkout dev

idea:

4. 创建并切换分支

Bash
git checkout -b dev # 新建 dev 分支,并切换到该分支上

5. 合并分支

Bash
git checkout master        # 切换回 master 分支
git merge dev            # 将 dev 分钟中的修改合并回 master 分支

idea:先check out 到指定标签,在把需要合并的标签右键合并即可。

6. 删除分支

Bash
git branch -d dev # 删除 dev 分支

7. 建立本地分支和远程分支的关联

7.1 本地分支存在,远程分支存在,但之间没有关联关系。

Bash
git push -u origin/remote_branch 命令推送。

7.2 本地分支存在,但远程分支不存在。

Bash
git push origin local_branch:remote_branch 

7.3 本地分支存在,远程分支存在,且关联关系已建立

使用 git push 命令进行推送即可

idea中本地push时直接修改远程分支名即可

8. 从远程仓库拉取

通过 git fetchgit pull 来获取远程仓库的内容。

Bash
git fetch origin master    
git pull origin master

git fetchgit pull 之间的区别:

8.1 git fetch

git fetch 是仅仅获取远程仓库的更新内容,并不会自动做合并。

需要先查看更新:

Bash
git log -p FETCH_HEAD

或者拉取到一个其他分支(目前还没见过分支创建)

如果没有问题,再进行合并:

Bash
git merge origin/master

新建一个本地分支,拉取远程到本地temp,查看不同,没问题则合并,合并后删除即可。

Bash
## 在本地新建一个temp分支,并将远程origin仓库的master分支代码下载到本地temp分支;
$ git fetch origin master:temp

## 比较本地代码与刚刚从远程下载下来的代码的区别;
$ git diff temp

## 合并temp分支到本地的master分支;
$ git merge temp

## 如果不想保留temp分支,删除;
$ git branch -d temp

实机演示:

在远程仓库手动修改t1111511, 在本地仓库的t1文件中添加666commit

1. git fetch origin master:temp

2. git diff temp

3. git merge temp

出现冲突,手动解决

选择全部接受(lazygitb),当然也可以只选择某一个hunk

最终commit log如下所示

4. git branch -d temp


8.2 git pull

git pull 在获取远程仓库的内容后,会自动做合并,可以看成 git fetch 之后 git merge

Bash
1
2
3
# 因为存在冲突故git pull会报错,以下命令二选一即可
git pull --no-rebase # merge冲突
git pull --rebase # rebase冲突


8.3 idea 拉取远程仓库

1. 前置准备

本地创建GitPullTest.java并添加f1函数,提交并推送

本地仓库添加f2函数,并提交

远程仓库添加f3函数,并提交


2. git fetch

找到idea上方git菜单,选择fetch (这里仅演示fetch,不演示pull,思想都是一致的)

选择remote下的main分支,查看更改

检查过后发现没有问题,右键remotemain分支,选择mergerebase合并

选择merge,发现出现冲突,需要解决

点击merge按钮,弹出冲突解决页面

左侧为本地仓库代码,右侧为远程仓库代码

有两种方式解决冲突

  1. 左下角 Accept LeftAccept Right,但只能选择一个仓库的代码作为最终合并文件
  2. 屏幕中间的>><<,可以同时保留本地和远程代码,也可以只选择其中一份

这里选择同时保留本地和远程代码

PS: 中间部分可以随意修改/调换位置,但两侧是无法修改的

最终效果如图所示(merge)

如果选择rebase,最终效果如下图所示


8.4 merge与rebase的区别(存在冲突)

merge:rebase

1. merge

  • 保留分支历史(会产生合并提交 merge commit)。
  • 适合多人协作,不会修改历史记录。

2. rebase

  • 不产生合并提交,而是将 feature 分支上的提交重新应用到 main 分支后面。
  • 历史会被重写(提交哈希值改变),适合保持提交历史整洁。

8.5 merge fast-forward(不存在冲突)

Fast-forward 是 Git 中的一种 合并方式,当你执行 git merge 时,如果当前分支没有新的提交(即该分支的 HEAD 没有分歧),Git 会直接将当前分支指向目标分支的最新提交,而不创建额外的合并提交。

什么时候会发生 Fast-Forward

  • 当目标分支没有新的提交,并且当前分支完全处于目标分支的前面(即目标分支是当前分支的祖先),Git 就可以简单地将当前分支 “快进” 到目标分支的最新提交,而不需要创建新的合并提交。

merge ff:rebase

上面的例子中,dev分支为目标分支,feature分支为当前分支

dev分支中不存在新的提交,且feature分支完全处于dev分支之前,此时进行merge将触发fast-forward,效果与rebase一致

05.IdeaGit 杂项

约 220 个字 10 行代码 9 张图片 预计阅读时间 1 分钟

Cherry-Pick

复用04.IdeaGit 分支管理8.3的前置准备

merge过程上略有不同,保留函数f2,去除函数f3

最终效果如下:

Java
public class GitPullTest {

    public void f1() {
        System.out.println("f1");
    }

    public void f2() {
        System.out.println("f2");
    }
}

此时,继续创建函数f4

问题来了,在实现函数f4时,发现其可以使用函数f3的逻辑简化代码

而想要重新获取之前已经放弃的函数f3,可以选中记录后点击cherry-pick按钮进行合并(可能需要解决冲突),之后会对函数f4在主干上重新提交,被抛弃的代码也可以重新获得。

解决冲突后,最终效果如下图所示

Undo Commit,Revert Commit,Drop Commit

undo:revert:drop

Tag

在项目完工准备发布时,可以对最后一个提交打上Tag

注意:push的时候一定要选中左下角Push tags,否则无法生效

查看远端仓库