Featured image of post Check the code you're reading is current

Check the code you're reading is current

I was a minute from filing a fix for a feature that had shipped the day before. The reason I had not noticed turned out to be a rule of my own, doing precisely what I told it to.

I nearly raised a merge request against one of my own libraries, at about six in the morning and one coffee in, to add a feature it already had.

It had shipped the day before. Tagged, released, tests, a paragraph in the getting-started guide… I’d have looked a proper wally, in public, on a repo with my own name across the top of it!

Pull, you clown

My fault, straightforwardly. I was reading a checkout that was twenty commits behind and I hadn’t checked, and there’s no version of that where I come out well.

What I wanted was for an HTTP server in go/transport to bind to one interface rather than every address on the box. So I went and looked. ServerSettings had a port on it. It did not have a host. I read the config, I read the server, I found nothing at all, and off I went, certain enough to start drafting the change and mildly pleased with myself for spotting a gap.

The field wasn’t there because the commit that added it had landed the previous afternoon and gone out in v0.2.0 the same day. Roughly four feet away from what I was reading, in a directory I had open.

So, yes. Pull, you clown.

But I’ve been chewing on it since, because “be more careful” is the sort of lesson that lasts about a fortnight, and I don’t think carelessness is what happened.

Why I never noticed

I work in worktrees. There’s a standing order about it and it’s a good one: when the change belongs in a repo you aren’t sat in, or when another session might be live on the same repo, you leave the shared checkout alone entirely. Cut a worktree off the target’s freshly fetched origin/main, work in there, tidy up after. It stops two sessions fighting over the same branch, and it stops one wandering into the other’s checkout mid-build (which I have watched happen, and would rather not again), and I’d not give it up for anything.

Have a look at what it takes away, though.

In an ordinary week you’d wander into a repository, pull, and start work. That pull is doing two jobs, and fetching code is only one of them. The other is telling you how far behind you’d got. It’s a little status report nobody asked for, delivered every time you sit down, and you stop noticing it’s there.

Work in worktrees and it simply stops arriving. You don’t check the main clone out. There’s no reason to go anywhere near it. So nothing pulls it, and it drifts a commit at a time, and the thing that used to tell you has been taken out of the room without anybody mentioning it.

The rule was also only ever about the repo I was changing. The sibling libraries I was reading, to answer a question, all sat outside it completely. That’s why twenty of them were behind at once, not twenty separate lapses. One gap nobody had thought about, me very much included.

What made it dangerous rather than annoying

A stale checkout doesn’t look stale, and that’s the whole difficulty (it took the second mistake of the morning before I saw it). It doesn’t warn you, or sulk, or leave a note. It sits there being perfectly agreeable and answering every question you put to it, accurately, about two weeks ago. Most other kinds of mistake announce themselves eventually: a typo fails, a bad merge goes red, a wrong assumption produces an answer someone queries. This one compiles. The signatures are sensible, the logic hangs together, and it hands you a conclusion that is confident, coherent and defensible about a version of the world that stopped existing a while back.

The evidence is even real. It’s just old…

And it goes wrong in one particular direction, which is what makes it worth writing down. Stale source rarely misleads you about what code does. Read a function from three weeks back and it almost certainly still does roughly that. What it misleads you about is what isn’t there.

“There’s no API for it.” “Upstream can’t do that.” “The library doesn’t support it.” Absence claims, the lot of them, and they’re just what a stale source manufactures, because absence is the one thing you can’t check by staring harder at what’s in front of you. There’s nothing there to stare at. That is the claim.

They’re also the expensive ones, and that isn’t a coincidence either. Decide a thing is missing and your very next move is to go and build it: a workaround for a bug fixed two releases back, or a wrapper round a gap that closed in March, or a merge request for a feature that shipped yesterday afternoon (the one I was ninety seconds from). The second mistake that morning was reading go/controls and concluding it had no HTTP support whatsoever, and that one would have had me build the thing rather than merely offer it.

What I do now

Stable door, horse long gone. It’s still the right door.

The rule that came out of it isn’t “remember to pull”. It’s the other half of the worktree rule, and it’s about reading rather than writing: before you draw a conclusion out of some code, confirm you’re looking at the current version of it. The line I typed into the global agent instructions that morning was this:

always check that the code we investigate is the latest version and not stale otherwise that leads to bad assumption

Global, rather than one project’s memory, because it was never a phpbotscout problem. It’s a working-pattern problem and the working pattern is everywhere.

In practice it’s four small things, all of them dull. Fetch, then count, because git rev-list --count HEAD..@{u} gives you the distance to upstream as one number and if it isn’t nought you don’t know what you’re looking at. Read origin/main directly rather than the working tree, which costs nothing and doesn’t need you fast-forwarding a checkout that might have your own mess in it. Remember that a module cache holds the pinned version, which is a fact about your go.mod and says nothing whatever about what the library can do this morning. And name the version you checked, whenever you tell someone their code can’t do something, because it costs you a clause and it lets anyone prove you wrong in ten seconds. Including you, in four months.

It went in at about six in the morning. By quarter past five that afternoon it had already caught one: another repo, ten behind, spotted before being read rather than after being concluded about.

That’s really all I wanted out of it, and there’s nothing clever in it anywhere, which is the point, really. Just a cheap habit parked in front of a whole category of confident error, which is more or less the only way I know to keep delegated work honest at any scale, because you can’t read every line (and if you can, you haven’t delegated anything).

A rule I’d still write again tomorrow had quietly taken something away without mentioning it, and the fix was never to tear it up. It was to work out what it had stopped doing for me, and go and write the other half.

I checked, while putting this together. go/controls on this machine is thirty commits behind.

Built with Hugo · Theme Stack designed by Jimmy