English
Software is built by groups, and the group is the unit that determines what gets built. Even the rare system written by one person depends on libraries written by others, runs on infrastructure maintained by others, and is eventually handed to others to maintain. The overwhelming majority of professional software is built by teams whose members must coordinate their changes, integrate their work, agree on what good code looks like, and transfer knowledge among themselves. The skills of collaboration — using version control to coordinate parallel work, and reviewing code to maintain quality and share knowledge — are therefore as central to professional software development as the skills of writing code itself. A brilliant programmer who cannot collaborate effectively is, on most teams, less valuable than a competent one who can.
The two pillars of code collaboration are version control and code review, and they are deeply connected. Version control is the infrastructure that makes parallel work possible: it lets many people change the same codebase concurrently, integrates their changes, records the full history of how the code reached its current state, and allows any past state to be recovered. Code review is the social-technical practice built on top of version control: changes are proposed, examined by other people, discussed, improved, and approved before they become part of the shared codebase. Together they form the system through which a team’s individual contributions become a coherent whole — and through which the team maintains shared standards, catches problems early, and transmits knowledge among its members.
These skills are practiced daily by professionals and, like the other skills in this chapter, are taught poorly or not at all. Most programmers learn Git as a set of memorized command incantations without a mental model of what those commands do, which leaves them helpless when something goes wrong. Most learn code review by osmosis, picking up whatever norms happen to prevail on their first team, good or bad. Both skills repay deliberate study: version control because the mental model makes the difference between fluency and fear, and code review because it is one of the highest-leverage activities in software — the point where quality is maintained, knowledge is shared, and the culture of a team is enacted.
Prerequisites: Programming (§2.1) and reading code (§8.3) — code review is reading code with a purpose. Writing code (§8.4) — the standards applied in review are the craft standards. Software process (§7.3) — collaboration practices are part of the development process.
The Shape of the Skill: Coordinating Parallel Work and Shared Judgment
Collaboration in software has two distinct but connected dimensions, and understanding the shape of each clarifies what the skills consist of.
The first dimension is coordination: enabling many people to work on the same codebase concurrently without their changes destroying each other. This is fundamentally a concurrency problem — the same problem, in a different domain, as the concurrency of §4.2 and §4.6 — and version control is the system that solves it. The naive approach (everyone edits the same files, last save wins) loses work and is unusable beyond one person. Version control solves the coordination problem by tracking changes rather than just current state, by allowing changes to be developed in isolation and then merged, by detecting and surfacing conflicts when two changes touch the same code, and by recording the complete history so that any change can be understood, attributed, and if necessary reverted. The skill of using version control well is the skill of managing this coordination: structuring work into coherent changes, integrating frequently enough to avoid divergence, and using the history as the resource it is.
The second dimension is shared judgment: a team agreeing on what good code is and maintaining that standard across everyone’s contributions. This is what code review provides. A codebase touched by many people, with no shared review, fragments into a collection of individual styles, accumulates the mistakes that no second pair of eyes caught, and loses the coherence that makes it comprehensible as a whole. Code review maintains shared standards by having changes examined before they are integrated; it catches problems — bugs, security issues, design flaws, readability problems — while they are cheap to fix; and it spreads knowledge, both of the specific change and of the standards the team holds. The skill of code review is the skill of examining a change effectively and of giving feedback that improves the code and the author without damaging the collaboration.
The mental model of version control is what separates fluency from fear, and Git — the dominant version control system since its creation by Linus Torvalds in 2005 for Linux kernel development — rewards a correct mental model and punishes its absence. Git’s surface (its command-line interface) is notoriously inconsistent and confusing, which leads most people to memorize command sequences without understanding them. But underneath the confusing surface is a simple and elegant model: a Git repository is a directed acyclic graph of commits, where each commit is a snapshot of the entire project plus a pointer to its parent commit(s), and branches and tags are just named pointers into this graph. Almost every Git operation, however cryptic its command, is a simple manipulation of this graph: a commit adds a node, a branch creates a pointer, a merge creates a commit with two parents, a rebase replays commits onto a new base. The programmer who holds this graph model in mind can reason about what any operation does and recover from mistakes; the programmer who only memorized commands is lost the moment something deviates from the memorized sequence. The single highest-return investment in version control is learning the graph model, after which the commands become comprehensible as operations on the graph.
The history of collaboration tooling is brief but consequential. Early version control (RCS, then CVS, then Subversion) was centralized: a single server held the canonical history, and developers checked out and committed to it, which made branching expensive and offline work impossible. Git’s distributed model (every clone is a full repository with the complete history) made branching cheap and local, which enabled the branch-per-feature workflow that dominates today, and made the distributed open-source collaboration model possible. The rise of hosting platforms — GitHub (2008), then GitLab and Bitbucket — added the pull request (or merge request): a structured mechanism for proposing a change, reviewing it, discussing it, and merging it, which turned code review from an ad hoc practice into a standard part of the workflow. The pull request is now the dominant unit of collaboration in software, and the practices around it — what makes a good pull request, how to review one well — are core professional skills. The most recent development is AI-assisted review, where language models examine proposed changes and surface potential issues, addressed below.
Version Control, Code Review, and Collaborative Judgment
The Mental Model of Version Control
Fluency with version control rests on the graph model, and a few concepts elaborate it into practical capability.
A commit is a snapshot of the entire project at a moment, identified by a hash of its contents, with a pointer to its parent. The chain of parent pointers is the history; following them backward reconstructs how the project reached its current state. Because each commit is identified by a hash of its contents (including its history), the identity of a commit captures the entire history leading to it — which is what makes Git’s integrity guarantees work and what makes commits stable references. Understanding that a commit is a snapshot-plus-parent, not a diff, clarifies many operations that are confusing when commits are imagined as diffs.
A branch is a movable pointer to a commit, nothing more. Creating a branch creates a pointer; committing on a branch moves the pointer forward to the new commit; switching branches points your working directory at a different commit. This is why branching in Git is cheap — a branch is just a small pointer, not a copy of anything. The branch-per-feature workflow (each unit of work developed on its own branch, then merged) follows from branches being cheap: you make a branch for each piece of work, develop it in isolation, and merge it when done, without disturbing the main line until the work is ready.
Merging and rebasing are the two ways to integrate work from one branch into another, and understanding their difference is important. A merge creates a new commit with two parents, joining two lines of history while preserving both — the history shows that the branches diverged and rejoined. A rebase replays the commits from one branch onto the tip of another, producing a linear history as if the work had been done sequentially — the history is cleaner but the actual chronology is rewritten. Each has appropriate uses: merging preserves the true history and is safe for shared branches; rebasing produces a cleaner history and is appropriate for tidying a local branch before sharing it, but rewriting history that others have already based work on causes problems. The choice between them is a matter of team convention and of understanding what each does to the graph.
The history is a resource, not just a record. git log explores it; git blame shows when and by whom each line was last changed; git bisect (§8.6) searches it to find where a bug was introduced; git revert undoes a change by creating a new commit that reverses it. The commit messages are part of the resource: a good commit message explains why a change was made, which is information available nowhere else and which the future reader (recovering intent, §8.3) depends on. Writing good commit messages — a concise summary, then a body explaining the motivation and context for non-trivial changes — is a craft skill that pays off every time someone later tries to understand why the code is the way it is.
What Makes a Good Change
The unit of collaboration is the change — the commit, or the pull request comprising one or more commits — and structuring changes well is a skill that makes both integration and review work better.
A good change is atomic: it does one thing, completely. A change that mixes several unrelated modifications — a bug fix, a refactoring, and a new feature in one pull request — is hard to review (the reviewer must untangle the concerns), hard to understand in the history (the commit does several things), and hard to revert (reverting the bug fix also reverts the unrelated feature). The discipline of making each change do one thing — separate commits for separate concerns, separate pull requests for separate features — makes the history clean, the review tractable, and reversion precise. This is the version-control expression of the single-responsibility principle.
A good change is appropriately sized. Large changes are hard to review well — a thousand-line pull request gets a cursory review because thorough review is exhausting, so large changes get less scrutiny precisely when they need more. Small changes get thorough review, integrate easily, and surface problems early. The discipline of keeping changes small — breaking a large feature into a sequence of small, individually reviewable changes — is one of the most effective ways to improve both review quality and integration smoothness. It connects to the small-batch principle of §7.3 and §7.4: small changes, integrated frequently, are safer than large changes integrated rarely.
A good change is accompanied by the context a reviewer needs: a clear description of what it does and why, the reasoning behind non-obvious decisions, and whatever the reviewer needs to evaluate it. The pull request description is where this context lives, and writing it well — explaining the motivation, summarizing the approach, flagging the parts that need careful attention — is part of the craft of collaboration. A change submitted with no explanation forces the reviewer to reconstruct the intent, which is slower and more error-prone than the author simply stating it.
Code Review as Social-Technical Practice
Code review is where individual contributions become team-quality code, and doing it well requires both technical skill (reading the change, evaluating it) and social skill (giving feedback that improves the code and the collaboration). It is one of the highest-leverage activities in software because it serves several purposes at once: catching problems, maintaining standards, and transferring knowledge.
The technical side of reviewing is reading the change with specific questions: Is it correct, including edge cases? Does it do what it claims? Is it clear and well-structured? Does it fit the codebase’s conventions and architecture? Does it introduce security issues, performance problems, or technical debt? Are there tests, and do they cover the important cases? The reviewer reads the change as a critic and as a future maintainer, looking for what will cause problems. Effective reviewing requires the reading skills of §8.3 — understanding the change in the context of the surrounding code — and the craft standards of §8.4 — recognizing what makes code good or bad.
The social side is at least as important and is where review most often goes wrong. Feedback on someone’s code is feedback on their work, and the way it is given determines whether it improves the code and strengthens the collaboration or breeds resentment and defensiveness. Effective review feedback is specific (pointing to the exact issue, not a vague complaint), is about the code rather than the person (saying “this function is hard to follow” rather than “you write unclear code”), distinguishes between what must change (correctness, security) and what is preference (style choices the reviewer would make differently), and explains the reasoning so the author learns rather than just complies. The reviewer who masters this gives feedback that authors welcome because it makes their code better and teaches them something; the reviewer who has not makes review an ordeal that authors dread.
Review is also the primary channel for knowledge transfer on a team. Through review, knowledge of each change spreads beyond its author (so the team does not have single points of failure where only one person understands a piece of code), the team’s standards are enacted and taught (junior members learn the standards by having their code reviewed and by reviewing others’), and the codebase’s coherence is maintained (review catches changes that diverge from the established patterns). A team that reviews well has more shared knowledge, more consistent code, and more resilience than a team that does not, beyond the bug-catching that is review’s most visible function. This is why the review culture of a team is one of the strongest determinants of its effectiveness — and why establishing good review norms is one of the highest-leverage things a team can do.
The receiving side of review is also a skill. Receiving feedback on your code without defensiveness, treating it as information rather than attack, distinguishing feedback worth accepting from feedback worth discussing, and using review as a learning opportunity rather than an ordeal — these make the author better and the collaboration smoother. The psychological safety that §7.6 identifies as central to team effectiveness is enacted, in large part, in how code review is conducted: a team where review is a collaborative improvement process has safety; a team where review is adversarial criticism does not.
AI in Code Review
AI has entered code review from two directions, and both matter for the practice.
First, AI as a reviewer: language models integrated into the review process examine proposed changes and surface potential issues — bugs, security vulnerabilities, style inconsistencies, missing edge cases. As an automated first pass, this is genuinely useful: it catches a class of routine issues before human review, freeing human reviewers to focus on the things that require human judgment (design appropriateness, whether the change is the right solution, architectural fit). The AI reviewer is a complement to human review, not a replacement: it is good at the mechanical checks (does this match a known vulnerability pattern? is this consistent with the style guide?) and poor at the judgment calls (is this the right approach? does this fit where the system is going?). Used as an automated first pass that handles the routine, it makes human review more focused; treated as a substitute for human review, it misses everything that requires understanding the change’s purpose and context.
Second, reviewing AI-generated code: as more code is generated by AI (§8.4), review becomes the critical checkpoint where AI-generated code is evaluated by a human before entering the codebase. This raises the stakes of review, because AI-generated code has specific failure modes — subtle correctness errors, security vulnerabilities, generic solutions that do not fit the specific context, inconsistency with codebase conventions — that review must catch. The reviewer of AI-generated code needs the same critical reading skills as for human-written code, applied with awareness of AI’s specific failure modes: AI code is fluent and plausible whether correct or not, so the reviewer cannot rely on the surface impression of competence that human-written code’s fluency might (imperfectly) signal. As AI generates more code, the review skill becomes more important, not less — it is the human judgment that ensures AI-generated code meets the standard before it becomes part of the shared codebase.
The combination — AI reviewing code (including AI-generated code) and humans reviewing both — is the emerging shape of the practice. The durable human skills are the judgment calls that AI cannot reliably make: whether a change is the right solution, whether it fits the system’s direction, whether the abstraction is sound, whether the tradeoffs are appropriate. These are exactly the parts of review that require understanding the change’s purpose and the system’s context, which is where human judgment remains essential.
What Changes With Mastery
Collaboration skills change how effective a developer is on a team and how much a team can accomplish together.
The first change is fluency and fearlessness with version control. The developer who holds the graph model can structure their work into clean changes, integrate frequently without fear, use the history as a resource, and recover from the mistakes that paralyze developers who only memorized commands. The fear that many developers have of Git — the worry that a wrong command will lose work or corrupt the repository — dissolves when the graph model makes operations comprehensible and recovery routine. This fearlessness enables the workflows (frequent integration, liberal branching, history archaeology) that make collaboration smooth.
The second change is the ability to participate in and improve a review culture. The developer who reviews well — catching problems, giving feedback that improves code and teaches, maintaining standards without breeding resentment — raises the quality of the codebase and the capability of the team. The developer who receives review well — without defensiveness, as a learning opportunity — improves faster and makes review a better experience for everyone. Together, these enact the review culture that is one of the strongest determinants of team effectiveness.
The third change is the recognition that software is a collaborative product, with the habits that follow. The developer who has internalized this writes code for the team (clear, conventional, reviewable), structures their work for collaboration (atomic changes, good commit messages, clear pull requests), and treats the shared codebase as a commons to be maintained rather than a personal artifact. These habits make the developer a multiplier on the team rather than a brilliant individual contributor whose work no one else can build on.
The fourth change is the judgment about AI’s role in collaboration. The developer who understands where AI review helps (routine checks, first-pass screening) and where human judgment remains essential (design, fit, the rightness of the approach) uses AI to make review more focused without abdicating the judgment that review exists to apply. As AI generates more of the code, this developer’s review skill — the human judgment that ensures AI-generated code meets the standard — becomes a central contribution.
Resources
Books and Texts
Scott Chacon and Ben Straub’s Pro Git (2nd ed., Apress, free at git-scm.com/book) is the definitive Git reference and is freely available. Its early chapters teach the practical workflow; its crucial Chapter 10 (“Git Internals”) teaches the object model — the graph of commits, trees, and blobs — that is the mental model everything else rests on. Reading the internals chapter early, rather than treating it as advanced material, is the key to genuine Git fluency: once the object model is understood, the rest of Git becomes comprehensible as operations on it.
For the conceptual model specifically, several free resources teach Git through its graph model rather than through command memorization. “Git from the Bottom Up” (John Wiegley, free online) builds understanding from the object model upward. “Think Like (a) Git” (free online) is specifically aimed at developing the mental model that makes Git comprehensible. These are worth more than any number of command cheat-sheets, because they teach the model from which the commands follow.
On code review, Google’s engineering practices documentation (“How to do a code review” and “The CL author’s guide,” free at google.github.io/eng-practices) is the most useful concise treatment of review as a practice: what reviewers should look for, how to give feedback, how to write changes that are easy to review, and how to navigate the social dynamics. It distills the review practices of an organization that does enormous amounts of code review, and it is short and directly applicable.
For the broader human dynamics of collaboration, the books on team effectiveness from §7.6 (especially on psychological safety) apply directly, since code review is where much of a team’s interpersonal dynamic is enacted.
| Book | Role | Type |
|---|---|---|
| Chacon & Straub, Pro Git (2nd ed., free) | Definitive Git reference; read the internals chapter | Reference |
| Wiegley, “Git from the Bottom Up” (free) | Graph model from the ground up | Depth |
| “Think Like (a) Git” (free) | The mental model for Git fluency | Entry |
| Google “How to do a code review” (free) | Code review practice, concise and applicable | Practice |
Courses and Interactive Tools
Learn Git Branching (learngitbranching.js.org, free) is the most effective interactive resource for building the graph mental model. It visualizes the commit graph and lets you manipulate it with Git commands, making the abstract graph concrete and the effect of each command visible. Working through it is the fastest way to develop the model that makes Git comprehensible, because it shows the graph that the commands operate on.
The MIT Missing Semester (§8.2 reference) includes a lecture on version control that teaches Git through its data model rather than through command memorization, consistent with the approach that produces genuine fluency.
| Course | Platform | Type |
|---|---|---|
| Learn Git Branching (free) | learngitbranching.js.org | Practice |
| MIT Missing Semester: Version Control (free) | missing.csail.mit.edu | Entry |
| GitHub Skills (free) | github.com/skills | Practice |
| Atlassian Git Tutorials (free) | atlassian.com/git/tutorials | Entry |
Practice, Tools, and Projects
The graph model is best learned by making it visible: configure your tools to show the commit graph (git log --graph --oneline --all, or a GUI that visualizes it), and watch how each operation — commit, branch, merge, rebase — changes the graph. Performing the operations while watching their effect on the graph builds the model in a way that reading cannot.
Contributing to open source (§8.3) is the most complete practice for collaboration skills: it exercises version control (branching, committing, rebasing, resolving conflicts), the craft of good changes (atomic commits, clear pull requests, good commit messages), and code review (your changes are reviewed by maintainers, and you can review others’ changes). The experience of having a pull request reviewed by an exacting open-source maintainer is among the most efficient ways to learn what good changes and good review look like.
Reviewing code regularly — whether on a team, in open source, or in study groups — develops the review skill. Reviewing is reading code (§8.3) with the added dimensions of evaluating it against standards and articulating feedback, and like all skills it improves with deliberate practice. Reviewing code written by programmers better than yourself, and reading their reviews of others’ code, teaches the standards and the craft of feedback.
| Resource | Platform | Type |
|---|---|---|
| Visualizing the commit graph (git log –graph) | Git | Practice |
| Pull request preparation and review workflow | GitHub / GitLab | Practice |
| Regular code review practice | Team / open source | Practice |
| Conventional Commits specification (free) | conventionalcommits.org | Reference |
Traps
| Trap | Why it misleads | Better response |
|---|---|---|
| Memorizing Git commands without the model | The most common way to “learn” Git — memorizing command sequences for common operations — produces fluency only as long as nothing deviates from the memorized cases. The moment something unexpected happens (a conflict, a mistaken commit, a need to undo something), the command-memorizer is lost, because they have no model from which to reason about what to do, and they often make the situation worse with more half-understood commands. | Learn the graph model: commits as snapshots with parent pointers, branches as movable pointers, operations as manipulations of the graph. Read Pro Git’s internals chapter and work through Learn Git Branching. Once the model is in place, the commands become comprehensible as operations on the graph, unexpected situations become reasonable-about, and recovery from mistakes becomes routine. The one-time investment in the model eliminates the perpetual fear. |
| Large, unfocused changes | A pull request that mixes several concerns — a feature, a refactoring, a bug fix, some formatting — or that is simply very large, gets a worse review than it needs: the reviewer cannot untangle the concerns or cannot sustain attention across a thousand lines, so the change gets cursory scrutiny precisely when it needs careful scrutiny. Large unfocused changes also pollute the history and make reversion imprecise. | Make changes atomic and small: each does one thing, completely. Separate concerns into separate commits and separate pull requests. Break large features into sequences of small, individually reviewable changes. Small focused changes get thorough review, integrate smoothly, and produce a clean, useful history. This is the single most effective thing an author can do to improve the review their code receives. |
| Review feedback that attacks the person | Feedback framed as criticism of the author rather than the code (“you always write unclear code,” “this is sloppy”) breeds defensiveness and resentment, damages the collaboration, and makes review an ordeal that people dread and rush through. It undermines the psychological safety that effective teams depend on. | Give feedback about the code, not the person: “this function is hard to follow” rather than “you write unclear code.” Be specific, explain the reasoning so the author learns, and distinguish what must change (correctness, security) from what is preference. The aim is to improve the code and teach, while strengthening rather than damaging the collaboration. Praise good work in review too, not only flag problems. |
| Rubber-stamp review | Approving changes without actually reviewing them — because review feels like a formality, because the author is trusted, because reviewing is tedious — defeats the purpose of review entirely. The problems review exists to catch pass through; the knowledge review exists to spread does not spread; the standards review exists to maintain erode. Rubber-stamp review is worse than no review, because it provides the appearance of quality control without the substance. | Review changes genuinely: read the code, understand what it does, evaluate it against the questions that matter (correctness, clarity, fit, tests). If a change is too large to review properly, ask for it to be broken up rather than approving it unread. If reviewing is consistently rushed, address the cause (too much review load, too little time allocated) rather than degrading review into a formality. |
| Treating AI review as a substitute for human review | AI reviewers catch a class of routine issues (known vulnerability patterns, style inconsistencies, some bugs) but cannot make the judgment calls that are review’s most important function: whether the change is the right solution, whether it fits the system’s direction, whether the design is sound. A team that treats AI review as sufficient misses everything that requires understanding the change’s purpose and context. | Use AI review as an automated first pass that handles the routine checks, freeing human reviewers to focus on the judgment that AI cannot provide. Keep human review for the design and fit questions. As AI generates more code, maintain rigorous human review of AI-generated changes specifically, applied with awareness of AI’s failure modes — fluent plausibility regardless of correctness, generic solutions, convention inconsistency. |
| Bad commit messages | Commit messages that say nothing useful (“fix,” “update,” “changes,” “wip”) throw away the one opportunity to record why a change was made — information available nowhere else and essential to the future reader recovering intent. The cost is paid later, repeatedly, by everyone who tries to understand why the code is the way it is and finds only “fix” in the history. | Write commit messages that explain the why: a concise summary line, and for non-trivial changes a body explaining the motivation and context. The future reader — including yourself — recovering the intent behind a piece of code depends on these messages. The small cost of writing a good message is repaid every time someone later needs to understand the change, which for important code is many times. |
中文
软件是由群体构建的,而群体才是决定最终构建出什么的单位。即使是少数由一个人写成的系统,也依赖他人写成的库,运行在他人维护的基础设施上,并最终交给他人维护。绝大多数职业软件由团队构建,团队成员必须协调彼此的修改,整合各自的工作,就什么是好代码达成共识,并在成员之间传递知识。因此,协作能力——使用版本控制协调并行工作,以及通过代码审查维持质量、共享知识——和写代码能力本身一样,是职业软件开发的核心。一个无法有效协作的天才程序员,在大多数团队中,不如一个能够协作的合格程序员有价值。
代码协作的两大支柱是版本控制和代码审查,而且二者深度相连。版本控制是让并行工作成为可能的基础设施:它允许多人同时修改同一个代码库,整合他们的改动,记录代码如何到达当前状态的完整历史,并允许恢复任何过去状态。代码审查则是建立在版本控制之上的社会—技术实践:变更被提出,由其他人检查、讨论、改进,并在成为共享代码库的一部分之前获得批准。二者合在一起,形成一个系统:团队中个人贡献通过它变成一个连贯整体;团队也通过它维持共同标准,早期发现问题,并在成员之间传递知识。
这些能力是职业程序员每天都在实践的东西,但像本章其他技能一样,它们要么没有被教,要么教得很差。多数程序员学习 Git 时,只是记住一组命令咒语,却没有理解这些命令做了什么;这会让他们一旦出错就束手无策。多数程序员学习代码审查,则是靠环境浸泡,直接吸收自己第一个团队里碰巧存在的规范,无论好坏。这两项能力都值得刻意学习:版本控制值得学,是因为心智模型决定了流利与恐惧之间的差别;代码审查值得学,是因为它是软件开发中杠杆最高的活动之一——质量在这里被维护,知识在这里被共享,团队文化也在这里被 enact 出来。
前置知识:编程(§2.1)和阅读代码(§8.3)——代码审查是带着目的阅读代码。写代码(§8.4)——审查中应用的标准就是代码技艺标准。软件过程(§7.3)——协作实践是开发过程的一部分。
这项能力的形状:协调并行工作与共享判断
软件协作有两个不同但相互连接的维度;理解每个维度的形状,有助于澄清这些能力到底包含什么。
第一个维度是协调:让许多人能够同时在同一个代码库中工作,而不会让彼此的改动相互破坏。这本质上是一个并发问题——与 §4.2 和 §4.6 中的并发问题属于同一种问题,只是发生在不同领域——而版本控制就是解决它的系统。朴素方法是所有人编辑同一批文件,最后保存者获胜;这种方法会丢失工作,一旦超过一个人就无法使用。版本控制通过跟踪变化而不只是当前状态来解决协调问题:它允许改动在隔离环境中开发,然后合并;当两个改动触及同一段代码时,它会检测并暴露冲突;它还记录完整历史,使任何改动都可以被理解、归因,并在必要时回滚。良好使用版本控制的能力,就是管理这种协调的能力:把工作组织成连贯的改动,足够频繁地整合以避免分叉过远,并把历史当作一种资源来使用。
第二个维度是共享判断:团队要对什么是好代码形成共识,并在每个人的贡献中维持这一标准。这正是代码审查所提供的东西。一个由多人修改、却没有共同审查的代码库,会碎裂成一组个人风格的集合,积累那些本可以被第二双眼睛发现的错误,并失去作为整体可理解的连贯性。代码审查通过在改动整合之前进行检查来维持共享标准;它在问题仍然便宜的时候捕捉问题——bug、安全问题、设计缺陷、可读性问题;它也传播知识,包括关于具体改动的知识,以及团队所持有的标准。代码审查的能力,就是有效检查一个改动,并给出既改善代码、也改善作者,同时不破坏协作关系的反馈。
版本控制的心智模型,是区分流利与恐惧的东西。Git 自 2005 年由 Linus Torvalds 为 Linux 内核开发创建以来,已经成为主导性的版本控制系统;它奖励正确心智模型,也惩罚缺少模型的人。Git 的表层,也就是它的命令行界面,出了名地不一致且令人困惑,这导致多数人只是记住命令序列,而不理解它们。但在混乱表层下面,是一个简单而优雅的模型:一个 Git 仓库是由 commit 组成的有向无环图,每个 commit 都是整个项目的一个快照,并带有指向其父 commit 的指针;branch 和 tag 只是指向这张图中某个位置的命名指针。几乎每个 Git 操作,无论命令多么晦涩,都是对这张图的简单操作:commit 添加一个节点,branch 创建一个指针,merge 创建一个有两个父节点的 commit,rebase 把 commit 重新播放到新的基底上。能够把这张图模型放在脑中的程序员,可以推理任何操作做了什么,也能从错误中恢复;只记住命令的人,一旦情况偏离记住的序列,就会迷失。版本控制中回报最高的一项投入,就是学习图模型;之后命令就会变成对图进行操作的可理解动作。
协作工具的历史很短,但后果重大。早期版本控制系统,例如 RCS、CVS 和 Subversion,是集中式的:一台服务器保存权威历史,开发者从中 checkout 并向它 commit。这使得分支成本高昂,离线工作也不可能。Git 的分布式模型——每个 clone 都是带有完整历史的完整仓库——使分支变得便宜且本地化,从而让今天占主导地位的 feature branch 工作流成为可能,也让分布式开源协作模式成为可能。托管平台的兴起——GitHub(2008),随后是 GitLab 和 Bitbucket——增加了 pull request(或 merge request):一种结构化机制,用来提出改动、审查它、讨论它并合并它。这把代码审查从临时实践变成了工作流的标准部分。Pull request 现在是软件协作的主导单位,而围绕它的实践——什么构成好的 pull request,如何良好审查一个 pull request——都是核心职业技能。最新的发展是 AI 辅助审查,即语言模型检查拟议改动并指出潜在问题,下文会讨论。
版本控制、代码审查与协作判断
版本控制的心智模型
版本控制的流利度建立在图模型之上,而几个概念会把这个模型展开成实践能力。
一个 commit 是某一时刻整个项目的快照,由其内容的哈希标识,并带有指向父 commit 的指针。父指针形成的链条就是历史;沿着它们向后追踪,就可以重建项目如何到达当前状态。因为每个 commit 都由其内容的哈希标识,包括其历史,所以一个 commit 的身份捕捉了通向它的全部历史——这正是 Git 完整性保证能够成立的原因,也是 commit 能作为稳定引用的原因。理解 commit 是“快照加父指针”,而不是 diff,可以澄清许多在把 commit 想象成 diff 时会变得混乱的操作。
一个 branch 只是指向某个 commit 的可移动指针,仅此而已。创建分支就是创建一个指针;在分支上提交,会把这个指针向前移动到新的 commit;切换分支,则是把你的工作目录指向另一个 commit。这就是 Git 中分支成本低的原因——分支只是一个小指针,不是任何东西的副本。每个功能一个分支的工作流,也就是每个工作单元在自己的分支上开发,然后再合并,正是由分支便宜这一事实推导出来的:你为每项工作创建一个分支,在隔离中开发,完成后合并,在工作准备好之前不扰动主线。
Merge 和 rebase 是把一个分支上的工作整合进另一个分支的两种方式,理解它们的差别很重要。Merge 创建一个有两个父节点的新 commit,把两条历史线连接起来,同时保留二者——历史显示分支曾经分叉,又重新汇合。Rebase 则把一个分支上的 commit 重新播放到另一个分支的最新位置上,产生线性历史,看起来仿佛工作是顺序完成的——历史更干净,但真实时间顺序被重写了。二者各有合适用途:merge 保留真实历史,并且对共享分支安全;rebase 产生更干净的历史,适合在共享之前整理本地分支,但如果重写了别人已经基于其开展工作的历史,就会造成问题。选择哪个,是团队约定问题,也是理解每种操作如何改变图的问题。
历史是一种资源,不只是记录。git log 用来探索历史;git blame 显示每一行最后一次在何时、由谁修改;git bisect(§8.6)搜索历史以找出 bug 在哪里被引入;git revert 通过创建一个反向 commit 来撤销某个改动。提交信息也是这种资源的一部分:好的提交信息解释为什么做出改动,而这是一种其他地方没有的信息,未来读者在恢复意图时(§8.3)会依赖它。写好提交信息——一个简洁摘要,再加上一段正文解释非平凡改动的动机和语境——是一项技艺能力;它会在后来有人试图理解代码为什么是现在这样时不断产生回报。
什么构成一个好的改动
协作的单位是改动——一个 commit,或者由一个或多个 commit 组成的 pull request。良好组织改动,是让整合和审查都更顺畅的能力。
一个好改动是原子化的:它完整地做一件事。把几个无关修改混在一起的改动——一个 bug 修复、一次重构和一个新功能都放在同一个 pull request 中——很难审查,因为审查者必须把不同关切拆开;也很难在历史中理解,因为一个 commit 做了好几件事;更难精确回滚,因为回滚 bug 修复也会同时回滚无关功能。让每个改动只做一件事——不同关切用不同 commit,不同功能用不同 pull request——会让历史干净,让审查可处理,让回滚精确。这是单一职责原则在版本控制中的表达。
一个好改动大小合适。大改动很难被良好审查——一个一千行的 pull request 往往只会得到粗略审查,因为彻底审查太疲惫;因此,大改动恰恰在最需要仔细审查的时候得到更少关注。小改动更容易得到彻底审查,更容易整合,也更早暴露问题。保持改动小——把一个大功能拆成一系列小的、可单独审查的改动——是同时改善审查质量和整合平滑度的最有效方式之一。这连接到 §7.3 和 §7.4 的小批量原则:频繁整合的小改动,比很少整合的大改动更安全。
一个好改动会附带审查者所需的语境:清楚说明它做了什么、为什么这样做,解释非显而易见决策背后的理由,并提供审查者评估它所需的信息。Pull request 描述就是承载这些语境的地方;把它写好——解释动机,总结方法,标出需要特别注意的部分——是协作技艺的一部分。没有任何说明就提交改动,会迫使审查者自己重建意图,这比作者直接说明要更慢,也更容易出错。
代码审查作为社会—技术实践
代码审查是个人贡献变成团队质量代码的地方;做好它同时需要技术能力——阅读并评估改动——和社会能力——给出能够改善代码与协作关系的反馈。它是软件开发中杠杆最高的活动之一,因为它同时服务多个目标:发现问题、维持标准、传递知识。
审查的技术侧,是带着具体问题阅读改动:它是否正确,包括边界情况?它是否做了自己声称要做的事?它是否清楚且结构良好?它是否符合代码库的约定和架构?它是否引入安全问题、性能问题或技术债?有没有测试?测试是否覆盖重要情况?审查者既作为批评者,也作为未来维护者阅读改动,寻找将来会造成问题的地方。有效审查需要 §8.3 的阅读能力——在周围代码语境中理解改动——也需要 §8.4 的代码技艺标准——识别什么让代码好或坏。
社会侧至少同样重要,也是审查最常出问题的地方。对某人代码的反馈,就是对其工作的反馈;反馈方式会决定它是改善代码并加强协作,还是制造怨气和防御。有效的审查反馈是具体的:指出确切问题,而不是模糊抱怨;它针对代码而不是人,比如说“这个函数很难跟上”,而不是说“你写代码不清楚”;它区分必须修改的问题,例如正确性和安全性,以及只是偏好的问题,例如审查者个人会做不同风格选择;它还会解释理由,使作者学习,而不是只是服从。掌握这种能力的审查者,会给出作者欢迎的反馈,因为它让代码变好,也让作者学到东西;没有掌握的人,则会让审查变成作者害怕的折磨。
审查也是团队知识传递的主要渠道。通过审查,每个改动的知识会传播到作者之外,从而避免团队出现只有一个人理解某段代码的单点故障;团队标准会被执行和教授,初级成员通过自己的代码被审查、以及审查他人的代码来学习标准;代码库的连贯性也会被维持,审查能捕捉偏离既有模式的改动。一个良好审查的团队,比没有良好审查的团队拥有更多共享知识、更一致的代码和更强韧性;这超出了审查最明显的功能,也就是发现 bug。正因为如此,团队的审查文化是决定团队有效性的最强因素之一;建立良好的审查规范,也是团队能做的最高杠杆事项之一。
接受审查也是一项能力。能够在接收代码反馈时不防御,把它当作信息而不是攻击,区分哪些反馈值得接受、哪些值得讨论,并把审查当作学习机会而不是折磨——这些都会让作者变得更好,也让协作更顺畅。§7.6 指出的心理安全,对团队有效性至关重要;而它在很大程度上正是在代码审查如何被进行中体现出来的:一个把审查看作协作改进过程的团队拥有安全感;一个把审查看作敌对批评的团队则没有。
AI 在代码审查中的作用
AI 从两个方向进入了代码审查,而二者都影响实践。
第一,AI 作为审查者:集成到审查流程中的语言模型会检查拟议改动,并指出潜在问题——bug、安全漏洞、风格不一致、缺失边界情况。作为自动化的第一遍检查,这确实有用:它能在人类审查之前捕捉一类常规问题,把人类审查者释放出来,让他们专注于需要人类判断的事情,例如设计是否合适、这个改动是否是正确解法、是否符合架构。AI 审查者是人类审查的补充,不是替代品:它擅长机械性检查,例如这是否匹配已知漏洞模式,是否符合风格指南;但不擅长判断性问题,例如这是不是正确方法,是否符合系统未来方向。把它用作处理常规问题的自动第一遍,会让人类审查更聚焦;把它当作人类审查的替代品,则会漏掉所有需要理解改动目的和语境的东西。
第二,审查 AI 生成代码:随着越来越多代码由 AI 生成(§8.4),审查成为 AI 生成代码进入代码库之前由人类评估的关键检查点。这提高了审查的重要性,因为 AI 生成代码有特定失败模式——细微正确性错误、安全漏洞、不适合具体语境的通用解法、与代码库约定不一致——这些都必须由审查捕捉。审查 AI 生成代码的人,需要与审查人类代码相同的批判性阅读能力,同时要意识到 AI 的特定失败模式:无论正确与否,AI 代码都可以流畅而貌似合理,因此审查者不能依赖表面的熟练感;人类代码的流畅性也许能不完美地暗示能力,但 AI 代码的流畅性不能。随着 AI 生成更多代码,审查能力变得更重要,而不是更不重要——正是人类判断确保 AI 生成代码在进入共享代码库之前达到标准。
二者结合——AI 审查代码,包括 AI 生成的代码;人类也审查二者——正在成为这种实践的新形态。持久的人类能力,是 AI 无法可靠做出的判断:一个改动是否是正确解法,是否符合系统方向,抽象是否健全,取舍是否合适。这些正是审查中需要理解改动目的和系统语境的部分,也正是人类判断仍然必要之处。
熟练之后会发生什么变化
协作能力会改变开发者在团队中的有效性,也会改变团队共同完成工作的能力。
第一种变化,是对版本控制的流利和无惧。掌握图模型的开发者能够把工作组织成干净改动,频繁整合而不恐惧,把历史作为资源使用,并从那些会让只记住命令的开发者瘫痪的错误中恢复。许多开发者对 Git 的恐惧——担心一个错误命令会丢失工作或损坏仓库——会在图模型让操作变得可理解、恢复变成常规之后消散。这种无惧使得协作顺畅所需的工作流成为可能:频繁整合、自由创建分支、进行历史考古。
第二种变化,是能够参与并改善审查文化。善于审查的开发者——能发现问题,给出改善代码和教育作者的反馈,维护标准而不制造怨气——会提高代码库质量和团队能力。善于接受审查的开发者——不防御,把审查看作学习机会——会进步更快,也让审查对所有人来说体验更好。二者共同 enact 出一种审查文化,而这种文化是决定团队有效性的最强因素之一。
第三种变化,是认识到软件是协作产品,并形成相应习惯。内化这一点的开发者会为团队写代码——清楚、符合约定、易于审查;会为协作组织自己的工作——原子化改动、良好提交信息、清楚的 pull request;也会把共享代码库看作需要维护的公地,而不是个人作品。这些习惯使开发者成为团队的倍增器,而不是一个代码别人无法继续建设的天才个人贡献者。
第四种变化,是对 AI 在协作中角色的判断。理解 AI 审查在哪些地方有帮助——常规检查、第一遍筛查——以及人类判断在哪些地方仍然必要——设计、适配性、方法是否正确——的开发者,会用 AI 让审查更聚焦,而不会放弃审查本来存在的目的:应用判断。随着 AI 生成更多代码,这类开发者的审查能力——确保 AI 生成代码达到标准的人类判断——会成为核心贡献。
资源
书籍与文本
Scott Chacon 和 Ben Straub 的 Pro Git(第 2 版,Apress,可在 git-scm.com/book 免费获取)是权威 Git 参考书,并且免费。前几章教授实践工作流;关键的第 10 章 “Git Internals” 则教授对象模型——由 commit、tree 和 blob 构成的图——这是其他一切所依托的心智模型。应当早读内部原理章节,而不是把它当成高级材料;这是获得真正 Git 流利度的关键。一旦对象模型被理解,Git 的其他部分都会变成对它的可理解操作。
专门关于概念模型,也有几个免费资源通过图模型而不是命令记忆来教授 Git。John Wiegley 的 “Git from the Bottom Up”(网上免费)从对象模型开始向上建立理解。“Think Like (a) Git”(网上免费)则专门面向建立让 Git 变得可理解的心智模型。这些资源比任何命令速查表都更有价值,因为它们教授的是命令背后的模型。
关于代码审查,Google 的 engineering practices documentation,包括 “How to do a code review” 和 “The CL author’s guide”(可在 google.github.io/eng-practices 免费获取),是对审查实践最有用的简洁处理:审查者应该看什么,如何给出反馈,如何写出容易审查的改动,以及如何处理社会动态。它提炼了一个进行海量代码审查的组织的审查实践,篇幅短,且可以直接应用。
关于协作中更广泛的人际动态,§7.6 中关于团队有效性的书籍,尤其是心理安全相关内容,可以直接应用,因为代码审查正是团队大量人际动态被体现出来的地方。
| 书籍 | 作用 | 类型 |
|---|---|---|
| Chacon & Straub,Pro Git(第 2 版,免费) | 权威 Git 参考;阅读内部原理章节 | 参考 |
| Wiegley,“Git from the Bottom Up”(免费) | 从底层建立图模型 | 深入 |
| “Think Like (a) Git”(免费) | Git 流利度所需的心智模型 | 入门 |
| Google “How to do a code review”(免费) | 简洁且可应用的代码审查实践 | 实践 |
课程与交互工具
Learn Git Branching(learngitbranching.js.org,免费)是建立图模型最有效的交互资源。它会可视化 commit 图,并允许你用 Git 命令操作这张图,使抽象图变得具体,也让每条命令的效果可见。完成它是建立 Git 可理解性的最快路径,因为它展示的正是命令所操作的那张图。
MIT Missing Semester(§8.2 参考)包含一节关于版本控制的课,它通过 Git 数据模型而不是命令记忆来教授 Git,与真正产生流利度的方法一致。
| 课程 | 平台 | 类型 |
|---|---|---|
| Learn Git Branching(免费) | learngitbranching.js.org | 实践 |
| MIT Missing Semester: Version Control(免费) | missing.csail.mit.edu | 入门 |
| GitHub Skills(免费) | github.com/skills | 实践 |
| Atlassian Git Tutorials(免费) | atlassian.com/git/tutorials | 入门 |
实践、工具与项目
学习图模型的最佳方式,是让它可见:配置你的工具显示 commit 图,例如 git log --graph --oneline --all,或者使用可视化图的 GUI,并观察每个操作——commit、branch、merge、rebase——如何改变图。一边执行操作,一边观察其对图的影响,会建立一种阅读本身无法建立的模型。
参与开源(§8.3)是协作能力最完整的练习:它会训练版本控制能力,例如创建分支、提交、变基、解决冲突;也会训练良好改动的技艺,例如原子化 commit、清楚的 pull request、良好提交信息;还会训练代码审查,因为你的改动会被维护者审查,而你也可以审查他人的改动。让一位严格的开源维护者审查你的 pull request,是学习什么是好改动和好审查的最高效方式之一。
定期审查代码——无论是在团队中、开源中,还是学习小组中——会发展审查能力。审查是阅读代码(§8.3)加上额外维度:按照标准评估代码,并清楚表达反馈。和所有能力一样,它会通过刻意练习提升。审查比自己更优秀的程序员写的代码,以及阅读他们对其他代码的审查,会教授标准和反馈技艺。
| 资源 | 平台 | 类型 |
|---|---|---|
可视化 commit 图(git log --graph) |
Git | 实践 |
| Pull request 准备与审查工作流 | GitHub / GitLab | 实践 |
| 定期代码审查实践 | 团队 / 开源 | 实践 |
| Conventional Commits 规范(免费) | conventionalcommits.org | 参考 |
陷阱
| 陷阱 | 为什么会误导 | 更好的回应 |
|---|---|---|
| 记忆 Git 命令而不理解模型 | 最常见的“学习” Git 的方式,是为常见操作记住命令序列。只要情况不偏离记忆场景,这看似流利;一旦发生意外,例如冲突、错误提交、需要撤销某件事,只记命令的人就会迷失,因为他们没有可用于推理下一步该做什么的模型,而且常常会用更多半懂不懂的命令让情况变得更糟。 | 学习图模型:commit 是带有父指针的快照,branch 是可移动指针,操作是对图的操纵。阅读 Pro Git 的内部原理章节,并完成 Learn Git Branching。一旦模型建立,命令就会成为对图的可理解操作,意外情况也能被推理,错误恢复也会变成常规。对模型的一次性投入,会消除长期恐惧。 |
| 大而不聚焦的改动 | 一个 pull request 如果混合多个关切——一个功能、一次重构、一个 bug 修复、一些格式修改——或者只是非常大,就会得到比它应得更差的审查:审查者无法拆开不同关切,或无法在一千行代码中维持注意力,因此改动恰恰在需要仔细审查时只得到粗略审查。大而不聚焦的改动也会污染历史,并让回滚不精确。 | 让改动小而原子化:每个改动完整做一件事。把不同关切拆成不同 commit 和不同 pull request。把大功能拆成一系列小的、可单独审查的改动。小而聚焦的改动会得到彻底审查,整合更顺畅,也产生干净有用的历史。这是作者能做的、最能改善代码审查质量的一件事。 |
| 审查反馈攻击人而不是代码 | 如果反馈被表述成对作者的批评,而不是对代码的批评,例如“你总是写不清楚的代码”“这太潦草了”,它会制造防御和怨气,破坏协作,并让审查变成人人害怕、想快点结束的折磨。它会削弱有效团队所依赖的心理安全。 | 反馈应该针对代码,而不是人:说“这个函数很难跟上”,而不是“你写代码不清楚”。反馈要具体,解释理由使作者能够学习,并区分必须修改的问题,例如正确性和安全性,与只是偏好的问题。目标是改善代码并教学,同时加强而不是破坏协作。在审查中也要赞扬好的工作,不要只指出问题。 |
| 盖章式审查 | 在没有真正审查的情况下批准改动——因为审查感觉只是形式,因为信任作者,因为审查很烦——会完全破坏审查的目的。审查本应发现的问题会通过;审查本应传播的知识不会传播;审查本应维护的标准会被侵蚀。盖章式审查比没有审查更糟,因为它提供了质量控制的外观,却没有实质。 | 真正审查改动:阅读代码,理解它做什么,并根据关键问题进行评估,例如正确性、清晰性、适配性和测试。如果一个改动太大,无法恰当审查,就要求拆分,而不是在未读的情况下批准。如果审查总是被匆忙完成,就解决原因,例如审查负荷过重、没有分配足够时间,而不是把审查降级为形式。 |
| 把 AI 审查当成人类审查的替代品 | AI 审查者能够捕捉一类常规问题,例如已知漏洞模式、风格不一致、某些 bug,但它不能做出审查中最重要的判断:这个改动是否是正确解法,是否符合系统方向,设计是否健全。把 AI 审查当成足够的团队,会漏掉所有需要理解改动目的和语境的东西。 | 把 AI 审查用作自动化第一遍,处理常规检查,从而让人类审查者专注于 AI 无法提供的判断。设计与适配性问题仍应保留给人类审查。随着 AI 生成更多代码,尤其要对 AI 生成改动保持严格的人类审查,并带着对 AI 失败模式的认识进行审查:无论正确与否都流畅貌似合理、通用解法、约定不一致。 |
| 糟糕的提交信息 | 提交信息如果没有提供有用信息,例如 “fix”“update”“changes”“wip”,就浪费了记录为什么做出改动的唯一机会——这些信息在其他地方没有,而未来读者恢复意图时极其需要。代价会在以后被反复支付:每个试图理解代码为什么是现在这样的人,在历史中只看到一个 “fix”。 | 写解释为什么的提交信息:一行简洁摘要;对于非平凡改动,再用正文解释动机和语境。未来读者——包括你自己——恢复某段代码背后的意图时,会依赖这些信息。写好信息的小成本,会在后来每次有人需要理解这个改动时收回;对于重要代码,这会发生很多次。 |