跳到正文
DEV Community· Eugene Yakhnenko·· 3 小時前AI 評分46

TTT 維護者為何以快速合併取代漫長審查週期

The Optimistic Maintainer: Why Fast Merges Beat Endless Review Cycles

AI 導讀

TTT 維護者採用「樂觀合併」,快速合併通過 CI 並附有測試的 PR,未完成的小部分則另開後續工單處理。項目有逾 3,500 項自動化測試,累計超過 1,750 次提交及 70 名社羣貢獻者,約每週推出一次穩定版本。項目歷史中只有 3 次還原提交。

正文

If you have ever contributed to open source, you probably know this feeling.

You find a bug in a tool you use every day. You clone the repo, spend a Saturday afternoon digging through unfamiliar code, write a clean 40-line fix, verify it works, and open a pull request. More recent, you ask your agent to find the fix and open the pull request. You feel great.

Then the waiting begins.

Two weeks pass. A maintainer appears, leaves three comments on variable naming and asks if you can rebase against a newly refactored branch. You make the changes. Another three weeks pass. A second reviewer asks why you didn't rewrite the surrounding function to use a different pattern. By month two, the enthusiasm that carried you through that Saturday afternoon is completely gone. You close the tab and never open a PR to that project again.

I've been on that side of the fence. Now, as the maintainer of TTT—a terminal text editor and IDE written in Go—I'm on the other side.

Over the last few months, the project has crossed 1,750 commits, 185,000 lines of code, and more than 70 unique community contributors. We ship a new stable release roughly every week.

Someone looking at our Git log recently pointed out an interesting pattern:

"The maintainer seems to merge almost everything quickly and just creates follow-up tickets for the rest."

They were spot on. That is not an accident or sloppy rubber-stamping. It is a deliberate strategy: optimistic merging. It is the single most important reason our community has grown this fast without burning me out.

Here is why it works, how we do it, and the engineering safety net that makes it possible.


The Trap of the Adversarial Gatekeeper

Most maintainers start out with good intentions. They care about their project, so they treat their repository like a fortress and themselves as the gatekeeper. Every incoming pull request is viewed with skepticism: What could this break? Did they follow all my implicit rules?

The problem is that open source contribution is discretionary effort. A contributor doesn’t work for you. When someone gives you a PR, they are giving you a gift of their free time. And even in modern times when opening a PR can be automated with an agent, they are opening the PR on your repository instead of somewhere else.

If you meet that gift with four rounds of nitpicking over inconsequential details, you aren't "maintaining high standards"—you are imposing friction. The contributor burns out, and you burn out from reading the same 30 lines of code across six weeks of back-and-forth.

Worse, when maintainers get paranoid about breaking things, they freeze. PRs pile up in the queue because the maintainer is afraid to merge them until they have a free weekend to manually test every edge case.

I wanted TTT to feel different: if you submit a working fix with tests, your code should be in main within hours, not months.


Step 1: You Need a Safety Net You Actually Trust

Here is the catch: you cannot practice optimistic merging if you don't trust your test suite. If merging a PR requires you to open the editor and manually poke at things to see if it feels right, you will inevitably become a bottleneck.

In previous posts, I wrote about how we built our --exec testing harness and how we used differential testing for our TextMate engine and Vim emulation.

Those weren't just academic exercises. They are the exact safety net that gives me the confidence to merge fast:

  1. 3,500+ Automated Tests: Across core algorithms, JSON-RPC LSP client communication, and buffer undo transactions.
  2. Deterministic UI Snapshots: Using our in-memory SimScreen and --exec scripts, CI tests actual visual layouts, tree widget expansions, and cursor goal-column math without needing a physical terminal.
  3. Automated PR Benchmark Comparisons: Every pull request runs scripts/bench-compare.mjs against main. It opens 64 MiB code files and 1 GiB logs. If a community PR introduces an accidental allocation loop or slows down rendering, CI tells us before I even open the review.
  4. Architectural Zones (ARCHITECTURE.md): Domain packages (internal/core/*) are strictly decoupled from the UI. They are forbidden from starting processes or touching terminal state.

Because the machine ruthlessly verifies that the invariants haven't broken, I don't have to spend my mental energy acting like a human compiler. Across 1,750+ commits, we have had exactly three reverts in the entire history of the project.


Step 2: The 90% Rule

When a contributor opens a pull request that solves 90% of a problem, passes CI, and includes tests, what do you do with the remaining 10%?

The gatekeeper demands that the contributor spend another week figuring out that last 10%.

The optimistic maintainer says: Merge the 90%.

Here’s what that looks like in practice:

  • The fix is solid, but missed a secondary edge case? Merge the PR, thank the contributor, and file a follow-up ticket: "Follow-up on #1284: handle ancestor tree preservation on collapsed roots."
  • The feature works, but needs a benchmark comparison? Don't make the contributor write a complex benchmark suite from scratch. Cherry-pick their commit, add the sub-benchmark alongside it, tag them with Co-Authored-By, and merge both.
  • Variable naming or a comment could be cleaner? Spend 30 seconds pushing a polish commit directly to main after merging, rather than making the contributor wait three days for another review cycle.

When contributors see their PR merged the same day they submitted it, the psychological shift is immediate. They don't feel like an outsider seeking approval; they feel like part of the team. Many of our most active contributors—people like Hao Chen, Jay Hemnani, Nandaleio, and Lzcwbcgs—started with a single small PR and kept coming back because the turnaround was fast and respectful.


Step 3: Defend Scope, Not Syntax

Optimistic merging does not mean accepting every feature anyone dreams up. If you merge every feature request into core, your binary will quickly turn into bloatware.

The distinction is: Be generous with code quality and bug fixes, but be strict about architectural scope.

Whenever a PR proposes something that doesn't belong in the core binary, I don't reject the contributor's idea—I guide it into our Lua plugin system:

  • A contributor built a great Linux desktop launcher integration into the core binary (#683). Core isn't the right place for desktop session launchers, so we agreed to package it as a featured Lua plugin (#1267).
  • A contributor submitted a port of the Catppuccin Mocha theme as a built-in Go theme. Rather than bundling dozens of themes into the core executable, we asked them to register it as a plugin manifest from ttt-plugins (#1240).

By pushing niche features into plugins, the core binary stays lean (single static Go binary, ~14 MB), while the community still gets the features they care about.


Step 4: Culture Encoded in Code

Most open source projects have a CONTRIBUTORS.md file that rots in the repository root. Almost nobody reads it, and bug reporters are never mentioned at all.

I wanted the culture of TTT to reflect that an editor is only as good as the people using it and reporting bugs. So we wrote a small script:

#!/usr/bin/env bash
# scripts/update-contributors.sh
set -euo pipefail

REPO="eugenioenko/ttt"
OUTPUT="internal/app/contributors.json"

contributors=$(gh api "repos/$REPO/contributors?per_page=100" --paginate \
  -q '.[] | select(.type == "User") | .login' | jq -R . | jq -s .)

reporters=$(gh api "repos/$REPO/issues?state=all&per_page=100" --paginate \
  -q '.[] | select(.pull_request == null and .user.type == "User") | .user.login' |
  sort | uniq -c | sort -k1,1nr -k2,2 | awk '{print $2}' | jq -R . | jq -s .)

jq -n --argjson c "$contributors" --argjson r "$reporters" \
  '{contributors: $c, reporters: ($r - $c)}' > "$OUTPUT"

Before every release, this script fetches every person who contributed code and every person who opened an issue that led to a fix. It compiles them directly into contributors.json, which is embedded in the binary.

When you open Help > About inside TTT, you see this:

┌───────────────────────── About TTT Editor ─────────────────────────┐
│ Version: v1.10.0                                                   │
│ Website: https://tttedit.dev                                       │
│ GitHub:  https://github.com/eugenioenko/ttt                        │
│ ────────────────────────────────────────────────────────────────── │
│ ttt is great because of everyone who contributes code, reports     │
│ issues, and uses it:                                               │
│                                                                    │
│ HaoChen · JayHemnani · Nandaleio · prime120 · Lzcwbcgs · ...       │
│                                                                    │
│ Thank you!                                                         │
│                                                            [Close] │
└────────────────────────────────────────────────────────────────────┘

Every bug reporter and every code author is literally baked into the application they helped build. It runs offline, makes zero network calls, and permanently credits the community.


The Takeaway

If you are maintaining an open-source project and feeling overwhelmed by PRs, consider flipping the model:

  1. Invest in automated safety nets first. If you don't trust your CI, build harness tests and benchmarks until you do. You cannot be a fast maintainer if you are a fearful maintainer.
  2. Stop holding PRs hostage over formatting. If the logic works and passes tests, merge it. Fix the variable name yourself in two minutes or open a follow-up ticket.
  3. Protect your architectural boundaries, not your ego. Save your "no" for scope creep and core bloat. Say "yes" to fixes and performance wins as fast as humanly possible.

High rigor and community warmth are not mutually exclusive. When your automated testing is ruthless, your human maintainership can be optimistic.


TTT is open source at github.com/eugenioenko/ttt and available at tttedit.dev. Come open a PR—we’ll probably merge it.

來源:DEV Community · dev.to