Reproducibility with renv and git
In Lesson 1, Maria fixed her sales-analysis project so the file paths work on any computer. The script is portable now: a teammate can open the project and the paths just resolve. Job done?
Not quite. Eighteen months later a colleague clones the project onto a fresh laptop, runs it, and a chart comes out different. The code is identical. The data is identical. What changed is everything underneath the code: a newer version of a package, a newer version of R. And there is no record of how the project got to where it is, so when an edit breaks something, there is no clean way back.
This lesson closes those two gaps for good. You will pin your package versions with renv so the analysis computes the same answer next year, and track every change with git so you always have a labeled snapshot to return to.
By the end of this lesson you will be able to:
- Explain why an analysis that ran last year can give a different result today, even with the same code and the same data
- Use renv to record your project's exact package versions and rebuild them on any machine
- Use git to save labeled snapshots of your project and see its history
Prerequisites: you have a portable RStudio Project from Lesson 1, you can install and load a package, and you can run R and call a function. The journey below is the whole lesson.
Same code is not the same result
We tend to think of an analysis as just its code and its data. But the answer also depends on the environment: the exact version of R you ran, and the exact version of every package you loaded. That environment is invisible, and it drifts: packages get updated, R gets updated, and a function that behaved one way last year can behave differently today.
Here is a concrete, famous case. In April 2019, R 3.6.0 changed the default method R uses to draw random numbers. After that release, the same two lines, set.seed(1) then sample(1:6), return a different ordering than they did before. Nothing in the code changed. The version of R underneath it did, and any analysis that relied on that draw silently gave a new answer.
Run this to see your current environment. Every number it prints is something your results can depend on:
The exact version numbers you see are whatever this session happens to have, and that is precisely the point: they are facts about one environment.
renv records the exact versions in a lockfile
The renv package solves the version-drift problem in two moves. First, it gives each project its own private package library, so installing or upgrading a package in one project never disturbs another. Second, and this is the part that makes you reproducible, it writes a lockfile named renv.lock that records the exact version of R and of every package the project uses.
A lockfile is not magic. It is a plain text file in JSON format, a list of records, one per package, each saying which package, which exact version, and where it came from. We can prove that by writing a tiny lockfile by hand and reading the versions back out of it:
The renv workflow: init, snapshot, restore
You drive renv with three commands, and the flow below is the whole loop. You init once to give the project its private library, snapshot whenever you want to record the current versions into renv.lock, and restore on any other machine to rebuild that exact set of versions.
In Maria's project she runs these once, then commits the resulting renv.lock:
library(renv)
# Run these in the project. renv writes a project-local library plus renv.lock:
renv::init() # start renv for this project; it finds the packages you use
renv::snapshot() # record the exact version of every package into renv.lock
renv::restore() # on another machine: reinstall those exact versions from renv.lock
Which command rebuilds the versions?
A colleague clones Maria's project onto a fresh laptop that already has the newest version of every package installed. Maria's renv.lock records the older versions her analysis was written against. Which command gives the colleague Maria's exact versions?
Read a pinned version
Reading a version straight out of the lockfile is a handy habit (it answers "what was this project built with?" without guessing). The lock object from earlier is still loaded. It has the same shape as lock$Packages$dplyr$Version that you saw. Pull out the exact ggplot2 version this project pinned.
Show answer
lock$Packages$ggplot2$Version # the exact ggplot2 version, read from the lockfile
#> [1] "3.5.1"git saves labeled snapshots you can return to
renv handles the environment. The other gap is history: as Maria edits her analysis, there is no record of what she changed or when, and no safe way to undo a bad edit. git fixes that. It is a version-control tool that lets you save commits: a commit is a labeled snapshot of your whole project at a moment in time, with a message saying what changed.
The loop has one wrinkle worth understanding. You do not commit straight from your edits. You first stage the changes you want to include (the staging area lets you choose what goes into the next snapshot), then commit them together with a message. After that, the history is a list of those snapshots, oldest to newest, each one a point you can return to.
These are command-line steps (RStudio's Git pane runs the same commands behind buttons):
git init # start tracking this project folder
git add renv.lock analysis.R # stage the files for the next snapshot
git commit -m "First working analysis" # save a labeled snapshot
git log --oneline # see the history of snapshots
renv.lock, not for large data files or the package library, so you keep those out of it.Commit renv.lock so the versions travel
Here is where the two tools click together. git tracks your files as they change. renv writes one special file, renv.lock, that captures your environment. So if you commit renv.lock into git alongside your scripts, then every snapshot in your history carries the exact package versions that went with that version of the code. Roll the project back to last March's commit and renv::restore() rebuilds last March's environment.
That is the full reproducibility story, and it is three legs holding up one stool:
What is the one more file to commit?
Maria has been committing her scripts to git as she works. To make the project fully reproducible for a teammate, the lesson says she should commit one more file. Which file, and why?
References
A few authoritative, free places to take this further:
- Introduction to renv - the official walk-through of the init / snapshot / restore workflow and what
renv.lockcontains. - The renv package site - the full reference, including how renv interacts with git and how it ignores the library folder.
- Happy Git and GitHub for the useR - Jenny Bryan's complete, R-focused guide to using git in real projects.
- Pro Git (free book) - the canonical reference for commits, the staging area, and history.
Lesson 2 complete
You closed the last two reproducibility gaps. renv captures the invisible part of an analysis, the exact R and package versions, into a small renv.lock file, and rebuilds them anywhere with restore(). git records your project's history as a series of labeled commits you can always return to. Commit renv.lock into git and the two combine: every point in your history carries the environment that produced it. With the portable project from Lesson 1, that is the full three-legged stool of a reproducible analysis.
Next, Lesson 3: The Modern R Toolchain (2026). You have a reproducible project; now meet the tools that make working in it fast and pleasant, from the Positron editor to talking to a database with duckdb and even to an LLM from R.