Skip to content
C.W.K.
Stream
Lesson 04 of 05 · published

Choose Your Base Before You Build

~12 min · version-control, convergence, operations, design

Level 0Wet Clay
0 XP0/36 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete

A Push Rejection Is a Discovery Made Too Late

The default sequence is: do the work, commit it, push it, find out whether the remote moved. That last step is where most of the pain lives, because by the time you learn the answer the commit already exists on a base that turned out to be wrong. Unwinding it is a manual operation, under time pressure, while holding a lock everyone else is waiting on.

Fetch first instead. Ask where the remote is before deciding what to build on, and four cases appear — each with an obvious correct action, and the fourth of which is to stop.

The Four Cases

Level or no remote branch. Build on the local tip. Nothing to reconcile.

Behind. The remote has commits you do not. Build directly on the remote tip rather than on your stale local one. This is the case that quietly produces "why did my change disappear" if you get it wrong, and it has one extra check worth doing: if the remote's new commits touched the same paths you are about to land, stop and reconcile by hand. Two writers editing the same material is not something an automated rule should resolve.

Ahead. You have local commits the remote does not. Build on the local tip, keeping them, and push the lot.

Diverged. Both sides have commits the other does not. Refuse. There is no correct automated answer — a merge might be right, a rebase might be right, and which one depends on facts the tool does not have. Stopping here is the feature.

Refusal Is a Result

An automated operation that stops and says "history diverged, a human should look at this" is doing its job. The temptation is to make it clever — try a merge, fall back to a rebase, force if the merge is trivial — and every increment of cleverness moves a decision that needed a human into a code path nobody will read until it does the wrong thing quietly.

The bar to apply: automate the cases where the correct action is determined by the state, and refuse the cases where it is determined by intent.

What is good about that criterion is that it shortens the argument. When somebody proposes automating one more case there is only one question: is the answer determined by this state alone? If it is, do it. If it is not, name the additional fact you would need. If the tool can hold that fact, give it the fact first; if it cannot, you have found where the refusal belongs.

Turn "discover at push time" into "decide before build time" wherever a shared resource is involved. The information is available either way; the difference is entirely whether you have already produced something that has to be unwound when the answer comes back inconvenient.

Code

Four cases, three decisions, one refusal·python
def choose_base(repo) -> str:
    """Decide what to build on, BEFORE building anything."""
    branch = current_branch(repo)
    run(["git", "fetch", "origin"], cwd=repo)      # ask first

    local = rev(repo, branch)
    remote = rev(repo, f"origin/{branch}", missing_ok=True)

    if remote is None or local == remote:
        return local                                # level

    if is_ancestor(repo, local, remote):            # BEHIND
        touched = run(["git", "diff", "--name-only", local, remote,
                       "--", *land_paths], cwd=repo).stdout.strip()
        if touched:
            raise SystemExit(
                "the remote moved THIS unit's paths while the claim "
                f"was held - reconcile by hand:\n{touched}")
        return remote            # build on the remote tip, not the
                                 # stale local one

    if is_ancestor(repo, remote, local):            # AHEAD
        return local             # keep the unpushed local commits

    raise SystemExit(               # DIVERGED
        "local and remote have both moved. no automated answer is "
        "correct here - merge or rebase depends on intent. "
        "reconcile by hand, then re-run.")


# Four branches, three of them decided by state alone.
# The refusal is the feature. Every increment of cleverness here
# moves a decision that needed a human into a code path nobody
# reads until it quietly does the wrong thing.

External links

Exercise

Find an automated process of yours that discovers a conflict at the end — a push, a deploy, an upload, a write with a version check. Move the check to the beginning and enumerate the states it can find. Then, for each state, decide whether the correct action follows from the state alone. Every state where it does not is a refusal, and writing that refusal is usually a two-line change with a large payoff.
Hint
The state you will be tempted to automate is the one where the answer is right ninety percent of the time. That is exactly the one to refuse, because the other ten percent will be handled silently and wrongly, and nobody will be watching the run that hits it.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.