Development

Replaying two years of commits in Docker: a time machine for a repository

Rebuilding a 2024 commit today, getting it to start and photographing it: this is not nostalgia, it is documentation that does not lie. The six-stage pipeline we wrote for BuyStep, the pitfalls we had to solve, and why counting remains Git's job.

Contents (9 sections)
  1. 1. Sample: which commits deserve to be told
  2. 2. Rebuild: the problem is not the code, it is the era
  3. The four pitfalls that cost the most time
  4. 3. Photograph: the internal scrolling trap
  5. 4. Counting is Git's job, not a model's
  6. 5. Narrate, then verify
  7. What execution found that review had missed
  8. The milestone that never started
  9. What it costs, what it brings

There is one question almost no team can answer with evidence: what did the product look like eighteen months ago? The mock-ups have been overwritten, the screenshots are scattered across chat threads, the acceptance environment has been recreated three times. Yet the information exists, intact, a few metres away: in the Git repository.

We wrote a tool that makes use of it. It walks through the history, picks milestones, rebuilds each one in containers with the SDK and database of its time, starts the application, photographs its pages, counts the facts with Git, then assembles a document. It ran over two years of history of the BuyStep platform and produced twenty-five real screenshots. Here is how it is built, and above all what we had to solve.

Diagram of the six-stage pipeline: sample, rebuild, photograph, count, narrate, compose, and the era detection table
The six stages and their outputs. Each stage can be rerun on its own, for one milestone or the whole series; intermediate results are files on disk, not state in memory.

1. Sample: which commits deserve to be told #

Replaying 1,700 commits would make no sense. The rule we chose is deliberately simple-minded: one milestone per month, plus one milestone per declared event (a framework version change, a database engine migration, the birth of a portal). A monthly milestone less than seven days from an event is dropped, so the same week is not told twice.

Two details make all the difference in use. First, the analysis windows are contiguous and non-overlapping: every commit in the history belongs to exactly one milestone, otherwise the figures get counted twice. Second, each milestone carries several rebuild candidates, ranked by how likely they are to compile: pull request merges first, then first-parent commits, then the rest. The first one that starts wins.

2. Rebuild: the problem is not the code, it is the era #

Checking out a commit is trivial. Getting it to start is not: the SDK of the time is no longer the one installed, the database engine has changed, and the referenced packages do not all install the same way. The sequence for each candidate:

Bash
git worktree add --detach <tmp>/<sha> <sha>   # disposable workspace, the repository is never modified
# era detection: TargetFramework of the .csproj files, referenced EF Core provider, presence of an apps/ folder
docker compose -f compose.<moteur>.yml up -d   # fresh database, anonymous volume, dedicated port
docker run --rm -v nuget-cache:/root/.nuget <sdk-de-l-epoque> dotnet publish
dotnet ef database update                      # the migrations of the time, not today's
# schema-tolerant seeding, then the runtime container, then a readiness probe

Era detection is the heart of the system, and it is inferred from the commit itself, never from a hard-coded date. On BuyStep, it distinguishes four periods: .NET 8 on MySQL, .NET 9 on MySQL, .NET 9 with two separate databases (the application and the files), then .NET 10 on PostgreSQL with a Node portal from April 2026 onwards.

Data seeding has to be schema-tolerant: the 2026 data set does not fit into the 2024 tables. In practice, each insert is attempted and then skipped if the column does not exist yet. It is ugly, and it is the only thing that works across twenty-five different schemas.

The four pitfalls that cost the most time #

  • Secrets that became mandatory. An application that started without a payment key in 2024 refuses to start in 2026, because a service began requiring the variable. The answer is not to supply a real secret but a value in the right format: the pages display, and only a real call would fail.
  • Package licences. A mapping library that went paid checks a token at startup. In the window concerned, a dummy value is not enough: you need the real key, or those milestones do not start. Name it explicitly in the error messages, or you will spend an hour searching.
  • Dependencies that kill the process. An indexing service that stops the application when its search engine is missing turns a rebuildable milestone into a silent failure. You either start the service or neutralise its initialisation.
  • Ports. A development database already running on the machine, and the rebuilt stack connects to the wrong one. Dedicated, configurable ports, and never the project's own.

3. Photograph: the internal scrolling trap #

Screenshots are taken while the stack is running, at 1440 × 900 at double density, full page, animations disabled, with a full scroll beforehand to trigger lazy loading of images. Each file comes with a sibling .meta.json: status, actual page height, length of the document text, notes.

That actual height is not decorative: it is a product indicator. On BuyStep, it goes from 11,620 pixels in August 2024 to 5,514 in April 2025, rises back to 13,624 a month later, reaches 15,110 in spring 2026, then drops to 8,302 in August 2026. The series of heights tells the editorial history of the site on its own.

4. Counting is Git's job, not a model's #

This is the design decision we are happiest with. Before any language model gets involved, a free, offline stage produces the numerical facts for each window:

Bash
# commits per author, busiest day, night-time commits, reverts, merged PRs
git log --no-merges --pretty=format:%H%x09%an%x09%aI%x09%s <depuis>..<jusqua>
# migrations added, dependency deltas, areas of the repository touched
git diff --name-status <depuis>..<jusqua>
git diff <depuis>..<jusqua> -- "*.csproj" "package.json"

The facts file produced this way is then handed to the agents. All they have to do is choose what deserves to be told and put it into words. A model asked to count commits in a log gets it wrong; a model given the count cannot get it wrong. The boundary is easy to hold: anything that can be checked mechanically must never pass through a model.

5. Narrate, then verify #

The writing stage is split into four roles, each given to the cheapest model that can do the job: extract the salient facts and link them to their commit hash; describe the visual change between two successive screenshots; summarise the technical side; write the continuous narrative. A fifth pass reviews the result against the facts counted in the previous stage and flags anything that is not sourced there.

Responses are cached on disk: rerunning the chain does not pay again for what has already been computed. On a two-year project, that is the difference between a tool you use and a tool you run once.

What execution found that review had missed #

The tool itself was written quickly, by several agents working in parallel on separate modules, with an interface contract written in advance and one reviewer per module. The code was reviewed, and the review found nothing serious. Then we ran it on real commits, and four defects appeared within minutes: the page truncated by its internal scrolling, a migration that required a factory that did not exist at the time, an indexing service that stopped the application, and a port already in use.

None of the four was a logic error. All of them were failures of contact with reality: an assumption about the environment, not about the code. That is exactly the category of bug that a review, human or not, does not find, and it is why we never call a feature finished before we have seen it run.

The milestone that never started #

Out of twenty-six milestones, only one resisted: early March 2025. Its set of migrations has real defects (a column referencing an identifier of another type, a constraint declared twice, data inserted before its parent row) and all four candidate commits share them.

We could have widened the search until we found a commit that compiled. We did not, because the repository itself had settled the matter: five days later, it completely reset those migrations. The failed rebuild is not a gap in the documentation, it is a line in it. A tool of this kind must be able to say “it did not start, and here is why”; if it covers things up, it is no longer of any use.

What it costs, what it brings #

Allow three to eight minutes of rebuild per milestone on the first pass, much less afterwards thanks to image layers and a shared package cache, and around fifteen gigabytes of disk. For twenty-six milestones, that is a night of computing. The counting stage is free; the writing stage costs a few euros in tokens.

In return: a case study illustrated with real images rather than stock photos, dated technical decisions when they need justifying, a handover that no longer relies on the memory of those who leave, and (the use we did not expect) an excellent way to reveal weak spots. A repository whose state from six months ago cannot be rebuilt is a repository whose environment is no longer under control.

This is the kind of tooling we build for our clients alongside their application: continuous integration, containerisation and reproducibility are not end-of-project extras. If your platform deserves this treatment, let us talk: first conversation free, quote within 48 h.

About
01/Read nextThree articles

Related articles

Reliability

Service levels, monitoring, on-call: what “reliable” really means

Uptime, RTO, RPO, a 99.9% service level, monitoring, tested backups, disaster recovery, guaranteed response and restoration times: what “reliable” means in practice, in figures and written commitments.

reliabilityservice levels
Method

93% of our code was never reviewed. We closed the door.

Over ninety days, 855 commits reached the main branch without going through a merge request, against 63 that did. And that branch triggers the release to production. Measurement, decision, and the four pitfalls we hit while closing the door.

continuous integrationcode review
Security

A stale lock file is a scan that lies

Dependency lock files are the source of truth for security scans. When a build pipeline quietly rewrites them instead of failing, the scan reads the wrong versions and returns a reassuring verdict. How we found out, and what we changed.

dependenciessoftware supply chain

What next

A .NET application to modernise?

Migration, rebuild or new application: we look at your code together, with no commitment.

Discuss your project

Reply within 24 working hours, from one of the two founders. Or by phone: +33 7 59 96 14 99.