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

A Span in a File You Don't Own

~13 min · vendoring, gitignore, boundaries, deploy

Level 0Loose Parts
0 XP0/52 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete

A File the Kit Could Not Own

By November the kit delivered shared text in two shapes. A whole file, vendored with a birth certificate on its first line, for Python and TypeScript. A local package, for the layered Swift half. Both shapes assume the same thing: the file belongs to the kit, and the consumer's job is to hold an exact copy of it.

Then a rule set turned up that had to be identical in every repository and lived in a file no repository could give away. The family's monitoring app held eighteen warnings open about uncommitted paths, seventeen of them on one compute Mac, and not one of them was work at risk. They were Python bytecode, packaging residue, files a coding assistant writes into a project folder for one machine's sessions, and editor state. On the workbench Macs those paths were hidden by a per-user global ignore file. The compute Mac and a lab account had no such file, and the code fan-out carries a checkout's untracked files along with it, so the same checkouts read as dirty there and clean everywhere else. A checkout has to read the same on every Mac, and a per-user file cannot promise that.

The obvious repair is to vendor .gitignore like any other shared file. It fails at once, because half of every .gitignore is that repository's own. One repository tracks its built frontend on purpose, so a family rule ignoring build output would hide files it ships. Two Swift apps keep a source folder named Target, and on a case-insensitive disk a rule for target/ matches it. A whole-file copy would delete those choices; leaving every repository to paste the shared rules into its own file is the triplication this quest started with.

Own the Span, and Nothing Else

So the deployer learned a third shape, the managed block. A manifest entry names a block, and the kit owns exactly the lines from # >>> <name> to # <<< <name> inside a file the consumer owns:

  • Deploy replaces the span where it stands, or appends it once when the file has none. Every line outside the markers is kept byte for byte.
  • Check compares the span alone and reports DRIFT, or MISSING when the file has no block. A rule the repository adds outside the markers is never drift.
  • Broken markers are refused. If the file does not hold exactly one begin marker followed by one end marker, the deployer exits with an error and leaves the file alone. Replacing a guessed span would delete the repository's own rules, which is the one thing this shape exists to avoid.
  • There is no header line. The source is the block whole, markers included, and the begin marker itself names the kit source. Line 1 of the file belongs to the consumer — unless the file was empty, the block is appended below the repository's own lines — so the certificate travels inside the span it describes.

A repository that needs an exception writes it below the end marker. In an ignore file a later pattern wins over an earlier one, so the repository gets the last word without ever editing the kit's span. The order of the file does the work a fork would otherwise do.

When the file belongs to someone else, own a marked span of it and nothing more — and refuse to guess where that span is. Whole-file vendoring is honest only when the whole file is shared. When it is not, the boundary has to be written into the file itself, the check has to stop at that boundary, and a boundary that cannot be read has to stop the deploy rather than be inferred.

What Goes Inside, and What Stays Out

Only names that are junk in every stack go in the block: bytecode and tool caches, packaging residue, dependency trees, operating-system litter, one Mac's editor state. A name some stack tracks on purpose — build output, a release folder — stays in that repository's own section, because the block's whole claim is that it is right for everyone.

The block also does one thing in the other direction. It un-ignores the coding assistant's project settings file, because that file carries hooks and is part of the repository's contract, while it ignores what one Mac's sessions wrote beside it: permission grants, preview-server entries, scratch checkouts. With that, status reads the same whether or not a machine also ignores the folder globally. The kit itself took the block the same evening and, for the first time, committed its own project settings — a file the global ignore had been hiding from its own history.

The Check Found a Lie on Day One

For an ignore file, a byte comparison of the span is not the whole question. A rule that matches a tracked file does nothing for that file — git keeps tracking it — and it silently drops the next new file created beside it. So the check also asks git which tracked files the block would call junk, and reports each one as TRACKED BUT IGNORED. Either the path really is junk and should be untracked, or the rule is wrong for the family and should leave the block.

It found one on the first run. A repository had committed a per-machine preview-server entry that pointed at a different repository's development server. Nobody would have looked for that, and no byte comparison could have seen it: the span was identical, and the lie was in what the span meant for that tree.

Code

A managed block: replace the span in place, compare only the span, refuse a guess·python
from __future__ import annotations

import subprocess
from pathlib import Path


class BlockError(ValueError):
    """The markers cannot be read, so the file is left exactly as it was."""


def markers(name: str) -> tuple[str, str]:
    return f"# >>> {name}", f"# <<< {name}"


def find_span(lines: list[str], name: str) -> tuple[int, int] | None:
    begin, end = markers(name)
    starts = [i for i, ln in enumerate(lines)
              if ln.rstrip("\n") == begin or ln.startswith(begin + ":")]
    ends = [i for i, ln in enumerate(lines) if ln.rstrip("\n") == end]
    if not starts and not ends:
        return None                              # no block yet
    if len(starts) != 1 or len(ends) != 1 or ends[0] < starts[0]:
        # Replacing a guessed span would delete the repo's own rules.
        raise BlockError(f"{name}: expected one begin marker, then one end")
    return starts[0], ends[0]


def place(text: str, block: str, name: str) -> str:
    """Replace the span where it stands, or append it once.
    Every line outside the markers is kept byte for byte."""
    lines = text.splitlines(keepends=True)
    span = find_span(lines, name)
    if span is None:
        if text and not text.endswith("\n"):
            text += "\n"
        return text + ("\n" if text else "") + block
    first, last = span
    return "".join(lines[:first]) + block + "".join(lines[last + 1:])


def check(text: str, block: str, name: str) -> str | None:
    """Compare the span alone. A rule outside the markers is never drift."""
    lines = text.splitlines(keepends=True)
    span = find_span(lines, name)
    if span is None:
        return "MISSING"
    first, last = span
    return None if "".join(lines[first:last + 1]) == block else "DRIFT"


def tracked_but_ignored(repo: Path, block_file: Path) -> list[str]:
    """Tracked files the block calls junk. git keeps tracking them anyway,
    so the rule does nothing for them - either the path is junk (untrack
    it) or the rule is wrong for the family (take it out of the block)."""
    out = subprocess.run(
        ["git", "-C", str(repo), "ls-files", "--cached", "--ignored",
         f"--exclude-from={block_file.resolve()}"],
        capture_output=True, text=True,
    )
    return out.stdout.splitlines() if out.returncode == 0 else []

External links

Exercise

Find one file in your projects that every repository has but none can give away — an ignore file, an editor config, a CI workflow, a lint config. List the rules in it that are genuinely the same everywhere, then sketch the marked span that would hold them: its two markers, what a deploy does when the span is missing, and what it does when the markers are broken. Last, ask your version-control system which tracked files those shared rules would match.
Hint
The last step is the one that pays. A shared ignore rule that matches a tracked file in some repository is either the discovery of junk somebody committed or proof that the rule is not as universal as it looked — and you will not know which until you list them.

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 in — Please sign in to comment.

No comments yet — be the first.