用Git仓库管理笔记:从Markdown到自研App的实践
2026/9/19 14:08:34 网站建设 项目流程

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.md

inbox是我专门留的“收集箱”,平时在路上想到什么或者看到一段不错的文字,先随手丢进去,等到周末再统一整理到对应目录。这样做的好处是:记录时不需要思考应该放哪,整理时又有明确的地方可以处理,不会把临时文件混进正式笔记里。目录名我坚持用小写字母加连字符,避免中文路径和空格带来的脚本兼容问题。

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用来做标签聚合,createdupdated是为了排序和检索,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 FastAPI

App 端我用了 Flutter,主要是看中它的跨平台能力和文本渲染表现。我的主力设备是 Android 手机和 iPad,如果写两套原生 App,维护成本太大。Flutter 的 Markdown 编辑器生态也算成熟,虽然不能跟桌面端比如 Typora 比,但配合自带的小键盘工具栏,写纯文字笔记没有太大问题。

同步服务端我用了 Python FastAPI,理由很简单:我熟悉 Python,FastAPI 天生就是异步的,处理文件读写和调用 subprocess 执行 git 命令都很方便。服务端每个接口只做很薄的一层封装,核心逻辑就是“收到请求 -> 操作本地仓库文件 -> git commit -> git push”。

我不建议把同步逻辑写到 App 里,哪怕你想省一台服务器。因为如果你只有手机和电脑这两个端,电脑上可以装桌面 Git 工具,手机用 App 同步,中间那个同步服务就是天然的协调者。它负责保证多个端不会互相覆盖。没有它,你就得自己实现多端同步协议,那个坑比写 App 大得多。

3.5 一次完整的笔记同步流程

我以一个日常场景来演示整个链路。

  1. 手机解锁,打开 App,首页会显示所有笔记目录。此时 App 尚未请求网络,看到的是本地缓存的旧数据。
  2. App 在后台调用/api/sync,同步服务收到请求后执行git pull。如果远端有新提交,就更新仓库文件,并重新生成本次变更的文件列表。
  3. 同步服务把变更列表返回给 App,App 对比 SQLite 里的旧快照,对更新过的笔记重新拉取内容。
  4. 我点开一篇昨天写的技术笔记,在编辑器里修改几段文字。
  5. 点击保存,App 把新的 Markdown 内容发给/api/notes/{path}
  6. 同步服务保存文件,然后依次执行git addgit commit -m "更新技术笔记:...git push
  7. 下次在电脑上打开这个仓库时,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 token,App 把 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 仓库,永远不会莫名其妙地消失。这件事本身就非常值。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询