ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

用Git仓库管理笔记:从Markdown到自研App的实践

用Git仓库管理笔记:从Markdown到自研App的实践 1. 为什么我最后把所有笔记塞进了一个 Git 仓库说出来你可能不信我手机上现在打开频率最高的 App不是聊天软件也不是短视频而是我自己写的一个笔记客户端。这一切的起因是四年前我把三千多篇笔记从各种笔记软件里导出来全部塞进了一个 Git 仓库。今天聊的这套东西不是什么大厂产品也不是什么高深架构就是一条很朴素的思路笔记的真源是本地 Markdown 文件版本历史交给 Git检索和编辑交给一个我自研的小 App。如果你也正在被笔记软件绑架或者手里攒了很多 md 文件不知道该怎么管这篇文章应该能给你一些可以直接抄的作业。1.1 从“笔记搬家”到“不再搬家”我大概从大学开始就养成随手记东西的习惯最早用的是某国产笔记软件后来换成印象笔记再后来被 Markdown 吸引又转战过一阵子 Bear、Notion、Obsidian。每次换工具都要经历一次痛苦的内容迁移。最夸张的一次我从一个笔记软件导出 html 压缩包再用脚本转回 Markdown结果发现里面有大量代码块被转义、图片链接失效、附件散落在一堆乱七八糟的文件夹里。那一刻我才意识到笔记软件从来不是你的资产笔记本身才是。所以我当时的想法很简单能不能让所有笔记都变成纯文本文件放在我自己的目录里用 Git 做版本管理这样以后不管什么软件倒闭、什么格式过时我都能随时把所有内容一股脑迁走。这个想法听起来很极客但做起来其实没有想象中难。真正难的是后面“管理”这一步——文件多了之后纯靠目录和文件名根本没法快速找到某条笔记手机上也很难直接编辑 Git 仓库里的文件。于是才有了那个 App。1.2 Git 做笔记后端到底解决了什么问题Git 本身就是为纯文本设计的版本管理工具而绝大多数笔记的核心内容恰好是文本。只要你的笔记能落到文件系统里Git 就能给每个文件、每次改动记录完整的历史。这意味着你不需要信任任何一家云服务商的“历史版本”功能也不需要担心同步 App 把数据弄丢因为每个版本都躺在本地仓库里。我最看重的其实是三个能力。第一可追溯改错了能回滚删掉了能恢复甚至能精确看到某段话是哪一天加入的。第二可迁移一个git clone就能把整个知识库复制到新电脑、新硬盘、新服务器上没有任何平台依赖。第三可自动化只要文件是普通文本就能用脚本批量处理比如批量给旧笔记加标签、统一格式、生成索引。这些能力对于我这种喜欢折腾的人来讲比任何花哨的“双链”“图谱”都更有安全感。1.3 这套方案的边界在哪里先泼一盆冷水不是所有人都适合把笔记塞进 Git 仓库。Git 擅长处理文本文件但不擅长处理大型二进制文件。如果你的笔记大量含有多媒体素材、复杂的数据库视图、多人实时协同表格或者你压根不想碰命令行那 Git 仓库方案大概率会让你抓狂。我的使用场景是个人知识库以文字为主偶尔插入截图和 PDF 附件总量可控。在这个前提下Git 仓库方案几乎是完美答案。它不需要联网也能随时读不会因为服务商停服而消失也不会因为产品改版而强制你迁移。当然代价是你得有一定动手能力至少理解 branch、commit、push、pull 这些基本概念。下面我展开讲讲仓库内部是怎么组织的。2. 笔记仓库的规范化目录、文件格式与提交策略把三千多个文件一股脑放进去是灾难但如果目录结构合理这套系统就能长期跑下去。我花了不少时间设计仓库的骨架期间推翻过好几次最终定型成下面这样。你会发现它跟一个开源代码仓库很像因为本质就是一个“内容仓库”。2.1 目录结构按主题拆不要按年份拆我试过按年份分目录比如 2023/01/xxx.md结果每次找一篇老笔记都要先回想起大致的日期非常痛苦。后来改成按主题分类第一层就是几个固定大类比如tech/、life/、reading/、work/、inbox/。每个大类下面再按实际需要嵌套子目录但层级尽量不超过三层。notes-repo/ ├── README.md ├── .gitignore ├── tech/ │ ├── git/ │ ├── dev-tools/ │ └── algorithm/ ├── life/ │ ├── travel/ │ └── journal/ ├── reading/ │ ├── books/ │ └── articles/ ├── work/ │ └── projects/ └── inbox/ └── 2025-01-15-tmp.mdinbox是我专门留的“收集箱”平时在路上想到什么或者看到一段不错的文字先随手丢进去等到周末再统一整理到对应目录。这样做的好处是记录时不需要思考应该放哪整理时又有明确的地方可以处理不会把临时文件混进正式笔记里。目录名我坚持用小写字母加连字符避免中文路径和空格带来的脚本兼容问题。2.2 文件格式Markdown 正文加 YAML front matter每篇笔记都是独立的.md文件文件头部用一段 YAML front matter 记录元信息。这样做的原因很简单正文归正文元信息归元信息机器可以很容易地解析人也能一眼看懂。--- title: 用 Git 管理笔记的一些思考 tags: [git, productivity] created: 2024-11-02 updated: 2025-01-18 status: published --- 这里是正文。标题不一定跟文件名一致因为文件名可能因为路径迁移而变动而标题是笔记内容的一部分。tags用来做标签聚合created和updated是为了排序和检索status可以标记这个笔记是草稿、已发布还是已经失效。附件方面我单独建了一个assets/目录每篇笔记的附件按assets/2025/01/文件名这样的形式放正文里用相对路径引用。这里有个容易被忽略的细节front matter 里的时间最好是手动维护不要依赖文件系统时间。因为当你用 Git 把仓库 clone 到新机器后所有文件的时间戳都会变成 checkout 时间如果拿文件时间做排序那整个笔记时间线就乱了。2.3 Git 提交策略把提交信息当日记写很多人的笔记仓库只会偶尔提交一次提交信息写“update”遇到问题想回退时根本不知道这个版本和上个版本有什么区别。我的约定是每完成一次“有意义的改动”就提交一次提交信息用一句话描述这次改动的内容。比如“给阅读笔记新增《置身事内》摘要”或者“把五篇关于网络协议的笔记归档到 tech/network 目录”。对于多端编辑我的做法是尽量每天手动提交一次如果当天有大量零散记录就分几次提交保持提交历史的可读性。我不建议做那种每分钟自动提交的“疯狂快照”否则你的 Git 历史会变成一锅粥真正想找回某段内容时反而找不到。.gitignore也很重要。我把系统生成文件、临时交换文件、编辑器配置都忽略掉避免仓库里出现.DS_Store、*.tmp、.obsidian/workspace之类的噪音。这样每次 clone 下来的都是干净内容不会因为本地环境不同导致各种无意义 diff。2.4 为什么我坚持用单仓库而不是多仓库有人会建议按主题拆成多个仓库比如读书笔记一个仓库、生活记录一个仓库。我试过后来放弃了。多仓库的问题在于笔记之间的边界经常是模糊的一篇技术文章可能既是项目记录又是学习笔记跨仓库引用和搜索非常痛苦。手机 App 管理多个仓库也很麻烦需要处理多套同步状态。单仓库则简单很多一次 clone全量下载一次搜索全库命中一次提交可以同时覆盖多个主题的改动。虽然仓库体积会变大但只要你不是疯狂塞视频纯文本加一些图片压缩文件几百 MB 对于现代设备完全不是问题。我现在的仓库全量包含历史版本大约 220MB日常操作依然很流畅。3. 自研 App 的核心设计不是 Git 客户端而是笔记客户端这是整个项目最容易被误解的地方。很多人一听到“用 Git 管笔记 写 App”第一反应是“在手机 App 里内嵌一个 Git 实现”。但实际上我写的 App 几乎不直接碰 Git。它更像是一个笔记客户端Git 的脏活累活由服务端来完成。3.1 为什么不让 App 直接操作 Git移动端操作 Git 是一件很痛苦的事。Android 和 iOS 上虽然都有兼容库但依赖、权限、网络稳定性都会带来一大堆问题。如果你的 App 要直接在手机上 clone、pull、push还得处理 SSH 密钥、大文件传输、冲突合并开发成本会成倍上涨。更关键的是笔记这个场景根本不需要 App 理解 Git 的整个模型。用户需要的是“浏览目录、看文章、编辑文章、保存修改”至于底层的 commit、push应该被封装成一个黑盒。所以我选择了另一种架构App 只通过 HTTP API 跟一个轻量同步服务通信同步服务在服务器上执行 git pull/push。这样 App 端代码简单服务端又能完全复用 Git 的成熟能力。3.2 三角架构App、同步服务、Git 仓库整个系统的结构可以这么理解Git 仓库存放在我的私有 Git 服务上用的是 Gitea因为它是开源的、可以脚本化、有完整的 REST API。同步服务是一个跑在 Linux 小主机上的 Python 服务负责接收 App 请求然后对仓库目录执行 Git 命令。App 是 Flutter 写的负责把仓库内容呈现成可阅读、可编辑的笔记列表。每次 App 启动时会先请求同步服务的/api/sync接口服务端收到请求后先git pull更新到最新再返回仓库文件的元数据列表。App 拿到元数据后跟本地缓存做 diff只拉取变化过的文件内容。整个过程中App 不需要知道 Git 的存在它只需要处理“文件路径、内容、更新状态”这些业务概念。选择 Gitea 而不是直接用 GitHub是因为我把仓库放在自己的服务器上更安心。你也可以用 GitLab、Gogs甚至一台只开 SSH 的普通机器只要你的同步服务能连上远程仓库就行。关键是所有 Git 操作都集中在服务端便于统一处理冲突、权限和日志。3.3 App 的功能清单和数据模型App 最初能做到“能看、能搜、能改、能传”就够了后面再逐渐迭代。我目前保留的核心功能只有五个目录树浏览、标签筛选、全文搜索、Markdown 编辑、附件上传。听起来很基础但恰恰是这些基础功能占掉了大部分开发时间。对应的数据模型其实也不复杂我在 SQLite 里建了一张notes表字段包括路径、文件名、标题、标签、正文快照、更新时间、本地状态。另外建了一个 FTS5 全文索引表用来支持中文全文搜索。SQLite 的 FTS5 对中文分词不算特别好但配合我常用的关键词和标签组合已经够用了。CREATE TABLE notes ( path TEXT PRIMARY KEY, title TEXT, content TEXT, tags TEXT, updated_at INTEGER, local_status TEXT DEFAULT synced ); CREATE VIRTUAL TABLE notes_fts USING fts5(path, title, content);本地状态字段local_status非常重要它记录了每篇笔记是“已同步”“本地修改待上传”还是“服务器已更新需合并”。App 每次渲染列表时就是根据这个字段决定是直接显示在线内容还是需要给用户一个“冲突/待提交”的提示。3.4 选型Flutter、SQLite、Python FastAPIApp 端我用了 Flutter主要是看中它的跨平台能力和文本渲染表现。我的主力设备是 Android 手机和 iPad如果写两套原生 App维护成本太大。Flutter 的 Markdown 编辑器生态也算成熟虽然不能跟桌面端比如 Typora 比但配合自带的小键盘工具栏写纯文字笔记没有太大问题。同步服务端我用了 Python FastAPI理由很简单我熟悉 PythonFastAPI 天生就是异步的处理文件读写和调用 subprocess 执行 git 命令都很方便。服务端每个接口只做很薄的一层封装核心逻辑就是“收到请求 - 操作本地仓库文件 - git commit - git push”。我不建议把同步逻辑写到 App 里哪怕你想省一台服务器。因为如果你只有手机和电脑这两个端电脑上可以装桌面 Git 工具手机用 App 同步中间那个同步服务就是天然的协调者。它负责保证多个端不会互相覆盖。没有它你就得自己实现多端同步协议那个坑比写 App 大得多。3.5 一次完整的笔记同步流程我以一个日常场景来演示整个链路。手机解锁打开 App首页会显示所有笔记目录。此时 App 尚未请求网络看到的是本地缓存的旧数据。App 在后台调用/api/sync同步服务收到请求后执行git pull。如果远端有新提交就更新仓库文件并重新生成本次变更的文件列表。同步服务把变更列表返回给 AppApp 对比 SQLite 里的旧快照对更新过的笔记重新拉取内容。我点开一篇昨天写的技术笔记在编辑器里修改几段文字。点击保存App 把新的 Markdown 内容发给/api/notes/{path}。同步服务保存文件然后依次执行git add、git commit -m 更新技术笔记...、git push。下次在电脑上打开这个仓库时git pull就能看到这次修改整条链路结束。这里面最关键的一步是第 6 步的提交信息。我的同步服务会从 front matter 里读标题再拼上“更新”或“新建”作为 message这样即使我在手机上快速保存Git 历史里也会留下清晰的记录而不是一堆“update”。4. 踩过的坑冲突、丢失、权限与同步风暴这套系统跑了两三年并不是一路顺畅。中间踩过几个比较深的坑都跟 Git 本身的机制、多端同步的时序、以及我自己的粗心有关。写出来给你省点试错时间。4.1 多端编辑同一个文件冲突是怎么发生的有一次我在外面用手机离线模式改了某篇读书笔记回到家打开电脑用命令行git pull准备更新仓库。结果电脑提示 auto merge 失败因为手机上的 App 在恢复网络后把同一个文件的修改 push 了上来而电脑本地也有一份旧改动没提交。两边改了同一段文字Git 不知道听谁的。当时我的 App 还没有任何冲突处理遇到这种情况只能去服务器上手动解决。后来我明确了两个原则。第一App 端保存前先对比服务端最新版本如果服务端文件比本地缓存新就提醒用户“远端已更新需要先拉取”。第二就算真的发生冲突也不要在手机上处理保留完好的两个版本把它们都提交到仓库里比如生成xxx.conflict.md然后在电脑上合并。4.2 一次误删笔记的恢复过程最惊险的一次是我清理旧目录时在服务器上执行了一条rm -rf命令后来发现当时有一个分支上的工作目录还没完全 push 到远端里面有几十篇最近一周的日记和随笔。看到目录变成空的时候我整个人是慌的。好在那台服务器上的仓库还有 reflog 记录我通过git reflog找到误删前的最新 HEAD然后用git reset --hard把仓库恢复到了删除前的状态。这算是不幸中的万幸。但如果我没有及时处理reflog 记录可能会在仓库 GC 后消失那就真的找不回来了。这件事给我的直接教训是就算本地仓库有 Git 兜底也一定要设置远端自动备份。我给服务器加了一个每天凌晨的定时任务做git bundle全量备份到另一块硬盘。同时对于重要笔记仓库我要求每个端在 push 之前检查 ahead 状态如果发现自己有一堆未推送的提交先停下来想一下再操作。4.3 自动提交带来的历史噪音有一段时间我想偷懒在同步服务里加了一个定时器每半小时就自动执行一次git add -A git commit -m auto sync。运行了一个星期后仓库里多了三百多条自动提交记录几乎都是无意义的“auto sync”。真正想查某篇笔记的编辑历史时根本分不清哪个提交是有价值的。后来我把同步策略改成了三档。正常编辑走手动提交流程提交信息由 App 生成如果某篇笔记超过一天没有手动提交同步服务在下次启动时会自动补一个快照提交只有在做批量移动文件、修改 front matter 这类操作时才允许脚本生成批量提交。这样 Git 历史干净多了也更像一个有思考的提交记录而不是流水账。4.4 移动端网络与 Token 认证App 和同步服务通信必须考虑网络身份认证。一开始我图省事直接在 App 里存了 Gitea 的用户名密码每次请求都带上。后来发现这非常不安全一旦手机丢失别人拿到这个请求就能直接修改你的整个仓库。我改成了在 Gitea 里生成一个只读/写笔记仓库的 access tokenApp 把 token 存在系统安全存储里服务端收到请求后用 token 去 Gitea API 校验。还有一个容易踩的坑是 HTTPS 证书。如果你用自签名证书App 默认会拒绝连接必须在开发环境手动信任证书线上环境则用正式证书。我的方案是用一台小主机配合域名和免费的证书这样 App 不需要额外处理证书问题。如果你没有域名也可以用 frp 或 Tailscale 之类的工具把服务暴露出来但要记得不要裸奔到公网记得加访问控制和防火墙。5. 这套笔记系统用了两年后我的真实感受Talk is cheap直接看数据。目前我的仓库里有 4300 多个文件包含 150 万字左右的笔记正文和近千张图片附件Git 仓库全量 220MB 出头。App 冷启动时间在 1.5 秒左右全文搜索关键词平均返回结果不超过 0.3 秒一次全量同步只拉变更通常在 1 秒内完成。这个性能表现已经足够让我每天都愿意用它记录。5.1 最值钱的回报内容不再被平台锁死最让我安心的一点是我所有的笔记现在都可以用一条git clone命令整体迁走没有任何一个 App 能绑架我的内容。就算未来 Flutter 不再维护我的笔记仍然是一堆纯文本文件就算 Gitea 跑不动了我也可以在本地直接继续用 Git 管理。于我而言这种“数据自主权”比任何酷炫功能都重要。你记了十年的日记不应该因为某个 App 关停而一夜消失。把笔记放在 Git 仓库里相当于给自己的知识上了保险。5.2 你完全可以简化方案如果你也想复刻这套系统但不想像我一样花几个月写 App我建议分两步走。第一步先把所有笔记统一成 Markdown 文件放进一个 Git 仓库桌面端用 IDE 或 Obsidian 配合 Git 插件管理。这一步成本极低但已经能享受到版本管理和跨设备同步的红利。第二步如果手机上编辑的需求越来越强烈再考虑写一个只读浏览器或者用现有的 Git Web 客户端先把“手机查看”解决掉别一上来就开发完整编辑器。我个人是从“写一个能读笔记的 App”开始迭代到“能编辑、能搜索、能传附件”的这个过程有很多乐趣但也确实花了不少周末时间。如果你只是想解决问题而不是享受写代码的过程那先用别人做好的工具再把数据用 Git 管理起来是性价比更高的路径。5.3 最后分享一点小偏好如果你真的决定写 App我强烈建议在第一天就把“同步状态”这个概念想清楚什么情况下显示“已同步”什么情况下显示“待上传”什么情况下显示“冲突”。这个状态机设计好了后面所有功能都会顺畅很多。至于手机端用什么框架、服务端用什么语言反倒是最不重要的决策。我现在每天打开这个 App 的时机很固定早上起来看一遍 inbox 里的临时记录通勤路上补两篇阅读笔记晚上在家整理当天的项目和写作草稿。它不像那些商业笔记软件一样有很多让我分心的功能但每一分钟打开它我知道我写下的东西会被安全地存进 Git 仓库永远不会莫名其妙地消失。这件事本身就非常值。
返回列表