DEV Community

Quinn Zhu
Quinn Zhu

Posted on

Your First Hour in a New Repo: Build a Reading Order from Git History

Conclusion first: do not read a new repo alphabetically

You join a repo on Monday. Nobody has time to walk you through it. So you open the file tree and start at the top.

That is the slowest path through a codebase. Files that change together belong together. Git already recorded which files move as one unit.

Read the coupled files first and the architecture appears faster. This workflow has three steps: extract co-change pairs, expand them into a reading order, then use an agent to cover what you cannot read in time. Setup costs about fifteen minutes.

The scripts below are templates. Run them in your own clone and treat the ranking as a hypothesis to verify.

Step 1 — Extract co-change pairs from commit history

A commit that touches checkout.ts and cart.ts is evidence of a relationship. Repeat that across a year of history and the strong edges stand out from the noise.

Limit the window to 12–18 months. Older commits describe code that no longer exists.

# Dump one commit per NUL byte, followed by the file names it touched.
git log --no-merges --since="18 months ago" \
  --pretty=format:'%x00' --name-only > /tmp/cochange.log

wc -l /tmp/cochange.log   # sanity check: is this file absurdly large?
Enter fullscreen mode Exit fullscreen mode

Now count the pairs.

# cochange.py — rank file pairs by how often they change together
import collections, itertools, subprocess

IGNORE = ("vendor/", "node_modules/", "dist/", "third_party/", ".lock")

raw = subprocess.run(
    ["git", "log", "--no-merges", "--since=18 months ago",
     "--pretty=format:%x00", "--name-only"],
    capture_output=True, text=True, check=True).stdout

pairs = collections.Counter()
touched = collections.Counter()

for chunk in raw.split("\x00"):
    files = {f.strip() for f in chunk.splitlines() if f.strip()}
    files = {f for f in files if not f.startswith(IGNORE)}
    if len(files) < 2:
        continue
    for f in files:
        touched[f] += 1
    for a, b in itertools.combinations(sorted(files), 2):
        pairs[(a, b)] += 1

for (a, b), n in pairs.most_common(20):
    print(f"{n:4d}  {a}  <->  {b}")
Enter fullscreen mode Exit fullscreen mode

Check two things before you trust the output. First, generated files and lockfiles dominate raw counts, so keep the ignore list honest. Second, squash-merged history turns every commit into a whole feature, which flattens the pair counts.

The --no-merges flag hides merge commits. That is usually what you want. If your team merges feature branches without squashing, drop the flag and compare the two outputs.

Step 2 — Expand one seed into a reading order

The top pair is a starting point, not a plan. Expand outward from the most frequently touched file and stop after roughly a dozen files.

# Continue in the same script: walk the coupling graph.
import collections

neighbors = collections.defaultdict(collections.Counter)
for (a, b), n in pairs.items():
    neighbors[a][b] += n
    neighbors[b][a] += n

seed = touched.most_common(1)[0][0]

order, seen = [seed], {seed}
while len(order) < 12:
    best, score = None, 0
    for recent in order[-3:]:            # weight the last few picks
        for cand, w in neighbors[recent].items():
            if cand not in seen and w > score:
                best, score = cand, w
    if best is None:
        break
    order.append(best)
    seen.add(best)

print("\nreading order:")
for i, f in enumerate(order, 1):
    print(f"{i:2d}. {f}")
Enter fullscreen mode Exit fullscreen mode

Greedy expansion is a heuristic. It can wander into a test-only cluster and stall there. If the list looks wrong, change the seed to a file you already know matters.

Then write the plan down with a stop condition for each block.

Time Target Stop when
0:00–0:20 top 3 coupled files you can name the entry point and its caller
0:20–1:00 full pair list you can draw five arrows between modules
1:00–2:00 one end-to-end test the test passes locally and you know what it asserts
2:00–3:00 agent summaries of the rest every claim is checked against code or a test

The third row matters most. A reading order you cannot run is still a guess.

Step 3 — Use an agent for the files you cannot reach

You will not read 200 files on day one. Hand the ranked list to a coding agent and ask for narrow, checkable answers.

MonkeyCode offers free model access and a free server option, which makes this step cheap to try from a work laptop. Disclosure: This article was prepared as part of MonkeyCode's product outreach.

Keep the prompt narrow. Broad questions produce broad guesses.

For each file listed below, answer in at most four lines:
1. Which files in this repo reference it? Give file:line, or "not found".
2. What does it return or mutate?
3. Which test exercises it? Give the test name, or "no test found".
4. What would break if this file were deleted?

Files:
<paste the reading order here>
Enter fullscreen mode Exit fullscreen mode

"Not found" and "no test found" are the most useful answers. They mark the edges of the repo's documented behavior.

Step 4 — Verify before you believe

Agent output is a hypothesis, not a fact. Three commands settle most of it.

# Does the caller really exist?
grep -rn "parseCheckout" --include="*.ts" src/ | head

# Is this file actually alive?
git log -1 --format='%h %ad %an' --date=short -- src/cart.ts

# Does the claimed test exist, and does it pass?
npm test -- --testPathPattern cart
Enter fullscreen mode Exit fullscreen mode

If the grep finds nothing, the agent invented the caller. If the last commit is four years old, the file is probably dead code, not core logic.

Record each verified fact in one note file. That note becomes the onboarding document for the next hire, which is a better gift than a Slack summary.

Your first PR comes from the gaps

The "no test found" list is your backlog. Pick one small function and add the missing test. It is a real contribution, it is low risk, and it forces you to run the code.

Do not refactor anything in week one. You do not yet know which odd-looking code is load-bearing.

Limitations and who should skip this

  • Repos under about 40 files: just read them all in one sitting.
  • Squash-merge-only history: co-change counts flatten, so pair ranking gets weak.
  • Heavy vendoring or generated code: your ignore list does most of the work.
  • Teams without a working local test command: verify by reading code instead.
  • Anyone who cannot review agent output critically: skip Step 3 entirely.

Do not hand security-critical paths to an agent on day one. Read those yourself, slowly.

The pair list also says nothing about runtime behavior. Two files can co-change because a formatter touches them together, not because they are coupled. Skim the commit messages for the top pairs before you build a mental model on them.

What to do next

Run Step 1 on the repo you joined this week. Paste the top ten pairs into your notes. You now have a reading order, a verification habit, and a first PR candidate.

If your laptop cannot spare the cycles for Step 3, MonkeyCode's free server option is one way to move that work off your machine.

Top comments (0)