R Bootcamp: Day 3 PM


Version control with git and GitHub



Eric Manning

September 1, 2026



princeton-ddss.github.io/r-bootcamp

This session



1) Why version control

  • Will become obvious

2) Setup, once

  • git, a GitHub account, and IDE integration

3) The main operations

  • Commit, diff, push — and what not to commit
  • Comments and commit messages

4) Website

  • Publishing a Quarto site with GitHub Pages

Some sources



Your version control system



paper.docx
paper_v2.docx
paper_v2_advisor_comments.docx

Or a file or folder on Dropbox or Google Drive.


  • Doesn’t work for code
  • Doesn’t track line changes
  • Doesn’t really work for collaboration or experimentation

Three questions



What changed?

Who changed it?

Why did they change it?

“Committing” changes with Git(Hub)



Every time you commit, git records:

  • the exact state of every file in the project
  • a message from you saying why you made this change
  • who you are, and when
  • a unique id, like a1b2c3d


You can return to any commit as needed or compare any two commits.

But I already have Dropbox



Dropbox / OneDrive / Drive git + GitHub
Tracks a file a change across files
Versions are automatic, timed, unnamed deliberate, named, explained
History kept ~30 days, per file forever, whole project
Both of you edit “conflicted copy (Eric’s Mac)” a merge you can resolve line-by-line
Try something risky copy the whole folder a branch
Share it publicly a link a repo, fork, or website
Big binary data good at this bad at this
Restricted data Princeton-managed, has a DUA your problem

You will use both



Code and text -> git. Medium-to-large data -> somewhere else. Fetch as needed.

Why GitHub?



  • A backup not on your laptop.
  • A distribution channel.
  • Collaboration. Changes with authorship, history, and review.
  • A free website. We’ll build one.
  • A public record of what you can do. People do look.

Patience



  • Relatively steep learning curve
  • The vocabulary is bad.
  • Some people will refuse to use it. Oh well.

Setup, once

Do you have git?



In a terminal — RStudio’s Terminal tab (next to Console), or Positron’s Terminal panel:

git --version
git version 2.55.0


If not:

  • macOSxcode-select --install, or install from git-scm.com
  • WindowsGit for Windows; take the defaults
  • Linuxsudo apt install git or your distribution’s equivalent

Setup for git (with an email you’ll use for GitHub)



Git stamps every commit with a name and an email. Set them once, globally:

git config --global user.name "Jane Doe"
git config --global user.email "jdoe@gmail.com"


Or, without leaving R:

usethis::use_git_config(
  user.name  = "Jane Doe",
  user.email = "jdoe@gmail.com"
)


Now check what you set:

git config --global --list

Register a GitHub account



github.com/join — but think for ten seconds about the username first.


Happy Git’s advice:

  • Pick something you would put on a CV. This is semi-permanent and public.
  • Shorter is better. All lowercase.
  • Some connection to your actual name.
  • Reuse it on other platforms if you can.


Email. Add your Princeton address as a second, verified email so GitHub links your university identity.
Vice versa is also okay.


Two-factor authentication is required. Set it up now. Use your phone. Use a biometric passkey (your face) if you can.

Make a token



usethis::create_github_token()


This opens GitHub’s token page with the form already filled in:

  • Scopes already checked: repo, user, workflow
  • Note — name it after the machine: positron-laptop-2026
  • Expiration — It will expire.


Copy it. This is the only time you will see it, but you can always make a new one.

Store it



Back in R:

gitcreds::gitcreds_set()
? Enter password or token: ghp_A1b2C3d4E5f6G7h8I9j0K1l2M3n4O5p6Q7r8
-> Adding new credentials...
-> Removing credentials from cache...
-> Done.


This saves the token to your operating system’s credential store.


Treat the token exactly like a password. Never write it or commit it.

Check that it all works



usethis::git_sitrep()


── Git global (user) ─────────────────────
• Name: 'Jane Doe'
• Email: 'jdoe@gmail.com'
• Default initial branch name: 'main'
── GitHub user ───────────────────────────
• Default GitHub host: 'https://github.com'
• Personal access token for 'https://github.com': '<discovered>'
• GitHub user: 'janedoe'
• Token scopes: 'gist, repo, user, workflow'

The main operations

Vocabulary, all at once



Term What it means
repository a folder git is tracking (a “repo”). The history lives in .git/
commit a saved snapshot, with a message (also verb)
stage choose which changes go into the next commit
diff a line-by-line difference between two versions
remote a copy of the repo somewhere else, usually GitHub
origin the default name for your remote
clone download a repo, with all of its history
push / pull send commits to the remote / get commits from it
branch a named line of development. The default one is main (or master)


The names are bad.

The three places



Working directory
the files on disk,
as you’re editing them
git add
Staging area
what you’ve chosen
to put in the next commit
git commit
Repository
permanent history,
stored in .git/

Why is there a staging area?



A commit should be one idea instead of “everything I happened to have saved.”


Staging lets you make two commits instead of one commit called stuff:

a1b2c3 Handle counties missing from the ACS extract
z0y9x8 Fix typo in README


Future you or somebody else will read this. Be brief, but clear.

Two ways to start a repo



GitHub first — make the repo on github.com, then clone it locally.

Better. The remote is wired up correctly when you start.


Local first — you already have a folder of work. Put it under git, then create the GitHub repo and connect them.

Slides for this at the end of the section.

Step 1) Make the repo on GitHub



github.com → the + menu → New repository

  • Name — lowercase, hyphens, no spaces: first-repo
  • Public or private — public is fine for today
  • Add a READMEcheck this. An empty repo is more annoying to clone
  • Add .gitignore — choose the R template. More on this later
  • License — skip for now


Then Create repository.

Step 2) Clone it – RStudio



  1. On GitHub, click the green Code button, copy the HTTPS URL
  2. File → New Project → Version Control → Git
  3. Paste the URL into Repository URL
  4. Pick a parent folder — somewhere sensible, not ~/Downloads
  5. Create Project


RStudio clones the repo, makes an .Rproj file, and opens the project. The Git pane appears in the top-right.

Clone it — Positron



  1. Copy the same HTTPS URL from GitHub
  2. Command Palette — Cmd/Ctrl + Shift + P — and type Git: Clone (or use Clone Repository on the Welcome page)
  3. Paste the URL
  4. Choose a parent folder
  5. Open when it offers


Positron clones the repo and opens the folder as your workspace. No .Rproj file is created, and none is needed.

Clone it — terminal



Both IDEs are running this for you:

git clone https://github.com/janedoe/first-repo.git
cd first-repo

Make something worth committing



New R script, analysis.R:

# analysis.R ---------------------------------------------------
# First look at county income.

county <- read.csv("data/county_data.csv")

summary(county$income)


Save it. Saving is not committing. Git has no idea you did anything yet.

git status



git status


On branch main
Your branch is up to date with 'origin/main'.

Untracked files:
  (use "git add <file>..." to include in what will be committed)
    analysis.R

nothing added to commit but untracked files present

Stage and commit — three ways



RStudio — Git pane → tick the Staged box next to analysis.RCommit → type a message → Commit

Positron — Source Control → hover analysis.R+ (Stage Changes) → type a message in the box → ✓ Commit

Terminal

git add analysis.R
git commit -m "Add first look at county income"


[main 9f8e7d6] Add first look at county income
 1 file changed, 6 insertions(+)
 create mode 100644 analysis.R

Staging everything at once



git add .      # stages everything here and below: new files, edits, deletions


Command Stages
git add . everything at or below where you’re standing
git add -A everything in the whole repo, wherever you are
git add -u only files git already tracks — no new ones


RStudio — tick the box in the Git pane’s header row.

Positron — the + on the Changes heading, not on a file.


git add . is only as safe as your .gitignore. It also coalesces all changes.

Now change something



Edit analysis.R — replace the summary() line:

# analysis.R ---------------------------------------------------
# First look at county income.

county <- read.csv("data/county_data.csv")

mean(county$income, na.rm = TRUE)


Save. Then look at the Git pane / Source Control — the file is now Modified rather than untracked.

The diff — this is the whole point



  • RStudio — Git pane → Diff
  • Positron — click the file in Source Control; it opens side-by-side
  • Terminalgit diff


@@ -3,4 +3,4 @@
 county <- read.csv("data/county_data.csv")

-summary(county$income)
+mean(county$income, na.rm = TRUE)

Commit the change



git add .
git commit -m "Use mean rather than summary for county income"


[main a1b2c3d] Use mean rather than summary for county income
 1 file changed, 1 insertion(+), 1 deletion(-)


git add . is safe here — you just looked at the diff, and it’s one file.


THE LOOP: Edit → look at the diff → commit.

Optional: View the history



git log --oneline


a1b2c3d Use mean rather than summary for county income
9f8e7d6 Add first look at county income
0011223 Initial commit


  • RStudio — Git pane → the clock icon (History)
  • Positron — Source Control → the Graph / commit history view
  • GitHub — the “N commits” link at the top of the file list

Send it to GitHub



RStudio — the green ↑ Push arrow in the Git pane

PositronSync Changes in Source Control, or the ↻ in the status bar

Terminal

git push


Enumerating objects: 8, done.
To https://github.com/janedoe/first-repo.git
   0011223..a1b2c3d  main -> main


Go look at your repo URL.

Remember:



git pull                    # bring in new stuff
# ... do work ...
git add analysis.R          # choose what belongs together
git commit -m "Why"         # explain it
git push                    # share it

When should I commit?



  • When you finish a thought
  • When something starts working
  • Before you try something that might not work
  • Before you stop for the day


Commit often. Write short, but clear commit messages.  (Looking at you, Claude.)

Undoing things



I want to… Do this
Throw away uncommitted edits to a file git restore analysis.R
Unstage something I staged git restore --staged analysis.R
Fix the message on my last commit git commit --amend
Undo a commit I already pushed git revert a1b2c3d — makes a new commit that reverses it
See an old version of a file git log -p analysis.R


If it’s really broken, move your new edits, delete the folder, clone it again, and put your new files back.

The second way: Local first, for a project you already have



You have a folder of work and want it on GitHub. From inside that folder:

usethis::use_git()

Initialises the repo, offers to commit everything currently there, and restarts the session so the IDE notices.


usethis::use_github()

Creates the matching repo on GitHub, wires it up as origin, and pushes.

Local first, by hand (without usethis::)



git init
git add .
git commit -m "Initial commit"


Then make an empty repo on GitHub — no README, no .gitignore — and:

git remote add origin https://github.com/janedoe/existing-project.git
git push -u origin main

Comment your code properly.

You think you’ll remember.

You won’t remember.

Judge these comments



# add one to i
i <- i + 1


# subset the data
county <- county[county$year != 2020, ]


# 2020 census enumeration was disrupted by COVID; county-level
# counts aren't comparable to adjacent years. Sensitivity check
# with 2020 included is in appendix.R.
county <- county[county$year != 2020, ]

Header comments



# analysis.R ---------------------------------------------------
# Difference in means: household income by census region.
#
# Inputs:  data/county_data.csv   (see data/README.md for source)
# Outputs: figures/income_by_region.png


You won’t remember the inputs and outputs.

Section comments



# Setup --------------------------------------------------------
library(dplyr)

# Load data ----------------------------------------------------
county <- read.csv("data/county_data.csv")

# Clean --------------------------------------------------------
county <- county |> filter(...) |> mutate(...) |> ...

# Model --------------------------------------------------------
first_model <- lm(y_var ~ x_var, data = county, ...)

# Figures ------------------------------------------------------
ggplot(...)


RStudio: Cmd + Shift + R

Delete commented-out code



# old version, keeping just in case
# model <- lm(income ~ region, data = county)
# model <- lm(income ~ region + year, data = county)
model <- lm(income ~ region + year + factor(state), data = county)


Delete it. Git remembers.

Writing code that produces readable diffs



county <- county |> filter(year != 2020) |> mutate(income_k = income / 1000) |> group_by(region) |> summarise(m = mean(income_k, na.rm = TRUE))

Change one thing here and the diff says this line changed. Useless.


county_summary <- county |>
    filter(year != 2020) |>
    mutate(income_k = income / 1000) |>
    group_by(region) |>
    summarise(m = mean(income_k, na.rm = TRUE))


Now the diff points at the step/line(s) you changed.

What not to commit…

Git is for code, not (most) data.

And Git never forgets.

Git never forgets



Deleting a file in a later commit does not remove it.


GitHub:

  • warns you above 50 MB per file
  • hard-rejects anything over 100 MB
  • asks that repositories stay under about 1 GB


A 40 MB CSV, committed ten times as you clean it, is 400 MB of history.

.gitignore



# R
.Rhistory
.RData
.Renviron
.Rproj.user/

# Data — too big and/or restricted; see data/README.md
data/*.csv
!data/README.md

# Quarto build output
/.quarto/
/_site/
*_files/

# OS junk
.DS_Store

You already have one



When you ticked the R template while creating the repo, GitHub gave you the first block: .Rhistory, .RData, .Renviron, .Rproj.user/.


usethis::use_git_ignore("data/*.csv")


Set up .gitignore before the first commit.

Secrets



API keys, tokens, passwords, database credentials. Never in a script.


usethis::edit_r_environ()

opens ~/.Renviron. Put the key there:

CENSUS_API_KEY=a1b2c3d4e5f6


Restart R, then in your script:

key <- Sys.getenv("CENSUS_API_KEY")


.Renviron is already in the R .gitignore template.

If you commit a secret



Bots scan public GitHub for credentials within seconds of a push.


In order:

  1. Revoke it and reissue a new key immediately.
  2. Then worry about the repository.

Getting a file out of history



First, the thing that does not work:

git rm --cached data/secret.csv
git commit -m "Remove secret"


Git records the deletion, but does not forget the file in previous commits.

Actually removing it from history



You have to rewrite every commit that ever touched the file.

pip install git-filter-repo      # or: brew install git-filter-repo


git-filter-repo --sensitive-data-removal \
  --invert-paths --path data/secret.csv


To scrub a string rather than a whole file, put the string in a file and use --replace-text ../passwords.txt instead.

Then force-push



filter-repo deliberately deletes your origin remote when it finishes, so you can’t push the rewrite by accident.

git remote add origin https://github.com/janedoe/first-repo.git
git push --force --mirror origin


Every commit hash after the removed file has changed. Collaborators must delete their copy and clone fresh.

If you commit something enormous



Early in a project, just start fresh:

  1. Copy your files out of the folder
  2. Delete the repo on GitHub, make a new one
  3. Clone it, add a proper .gitignore first
  4. Copy your files back and commit


Rewriting history with git-filter-repo works too.

Branches and conflicts 🙀

A branch



A branch is a label pointing at a commit.

main
try-state-fe
shared history

Why?



Once main is stable and large:

  • Experiment without breaking a working version
  • Keep main in a state you’d be willing to show someone else
  • Work (collaboratively?) on two things at once without entangling

Make one



git switch -c try-state-fe


  • RStudio — Git pane → the branch dropdown → New Branch
  • Positron — click the branch name in the status bar → Create new branch


New commits land in try-state-fe, not main.


We used to use checkout for this (and other things).

Moving between branches



Commit before you switch.


git switch main


The files on disk change. Your analysis.R reverts to whatever main says it is.

Merging



You’re happy with the branch. Bring it into main:

git switch main
git merge try-state-fe


Updating a1b2c3d..d4e5f6a
Fast-forward
 analysis.R | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)


Now you can delete the branch: git branch -d try-state-fe

Conflicts



When the same lines changed in two places. Git will not guess.


Commit both. Then, from main:

git merge try-state-fe


Auto-merging analysis.R
CONFLICT (content): Merge conflict in analysis.R
Automatic merge failed; fix conflicts and then commit the result.


And inside analysis.R:

<<<<<<< HEAD
mean(county$income, na.rm = TRUE)
=======
median(county$income, na.rm = TRUE)
>>>>>>> try-state-fe

Fix it



Both IDEs highlight this block and offer buttons: Accept Current, Accept Incoming, Accept Both, Compare.


Edit the file so it says what you actually want. Then delete all three marker lines.


Then stage and commit as normal:

git add analysis.R
git commit -m "Merge try-state-fe; use median income"


Resolving a conflict: edit, delete markers, save, commit.

Your first conflict



 ! [rejected]        main -> main (fetch first)
error: failed to push some refs to 'https://github.com/...'
hint: Updates were rejected because the remote contains work that you do
hint: not have locally. This is usually caused by another repository
hint: pushing to the same ref.


Fix it with:

git pull
# resolve anything that conflicts, exactly as we just did
git push


Start every work session with git pull.

Other people’s repositories

Clone versus fork



Clone — Your copy to read, run, and edit locally. You cannot push it back without write access.


Fork — A copy in your GitHub account. You can push to it freely (and easily offer changes to original authors).


usethis::create_from_github("someuser/replication-2024")


If you can’t push to that repo, this will:

  • fork it
  • clone your fork locally
  • set origin to your fork and upstream to the original
  • open the project

By hand



  1. Fork button, top right of their repo
  2. Clone your fork’s URL, exactly as in the last section
  3. Point upstream at the original:
git remote add upstream https://github.com/someuser/replication-2024.git
git remote -v
origin    https://github.com/janedoe/replication-2024.git (fetch)
origin    https://github.com/janedoe/replication-2024.git (push)
upstream  https://github.com/someuser/replication-2024.git (fetch)
upstream  https://github.com/someuser/replication-2024.git (push)

Getting updates



Your fork does not update itself.

git fetch upstream
git merge upstream/main

Contributing with pull requests



  1. Commit to your fork and push
  2. GitHub offers Compare & pull request
  3. Write what you changed and why
  4. The maintainer reads the diff and decides


Method packages. Found a bug in someone’s estimator? Fork, fix, open a PR. GitHub will credit your contributions.

Licenses



No license means all rights reserved. Nobody can use your code.


  • MIT — permissive, common default for research code
  • GPL — copyleft: anyone may reuse it, but their work must also be GPL
  • CC0 — effectively public domain


usethis::use_mit_license()

Or tick the box when you create the repo.

Publishing a website

(Seriously!)

GitHub Pages



Free static hosting attached to any GitHub repository.


<username>.github.io/<repo>


Static means HTML, CSS, and JavaScript. You render locally and publish the result.


Which is exactly what Quarto already does.

Two URLs



Project site — any repo:

janedoe.github.io/first-repo


User site — a repo named janedoe.github.io gets the bare address:

janedoe.github.io


The second one is your academic website.

The steps



Broadly,

  1. Create a new R project > Quarto Website > call the project <yourusername>.github.io
  2. Update the .gitignore
  3. Make an initial commit on main (or master) branch
  4. With usethis::, wire to a new GitHub respository
  5. Edit and/or preview content
  6. Publish!

1. Create new R project



In a fresh RStudio session, File > New Project > New Directory > Quarto Website


2. Update the .gitignore



Before publishing anything, add to .gitignore if not already there:

.quarto/
_site/

3. Make an initial commit on main (or master) branch



Commit everything as an initial commit as we’ve done previously. This will create a main (or master) branch.

4. With usethis::, wire to a new GitHub respository



usethis::use_github()


It will look something like:



It will open the GitHub repo in the browser. If this shows a 404 error, refresh the page.

5. Edit and/or preview content



You can call quarto preview at any time to render a local version of the website in your browser.


Alternatively, you can pull it up in the RStudio viewer by opening one of the .qmd files and clicking “Render” at the top. (In the adjoining gear box, make sure “Preview in Viewer Pane” is selected.)


Now is a good time to make, save, stage, and commit some edits before first publication, if desired. After making and saving, you can call quarto preview or click “Render” again at any time to view your changes.

6. Publish!



Once you’ve pushed any changes, in the terminal type quarto publish gh-pages. If prompted, type yes when asked if you want to publish using gh-pages. The printed output will end with something like the following:



Follow those directions. Click the link or go to “Settings” > “Pages” in the GitHub repo and change the “Branch” dropdown from None to gh-pages, then click Save. Wait a minute and your site will publish. (To view the status of the publication run, you can click on the “Actions” tab of the GitHub repo.)

What just happened to your repo



You now have two branches doing two different jobs:

Branch Holds You touch it
main your source: .qmd, .R, _quarto.yml constantly
gh-pages rendered HTML, managed by Quarto never


To update the site later: edit, preview (if you want), commit, push, then run quarto publish gh-pages again.

Adding content



The Quarto documentation is truly excellent. See https://quarto.org/docs/websites/ and its subcontent. You can easily swap between website templates, just as you can swap between document templates.


Claude is also quite good at helping with this.

Alternatives / custom domains



See here for another way to publish – and how to wire your website to a custom domain.

Things that exist, that we skipped



So you know the words when you meet them:

git stash · git rebase · tags and releases · submodules · GitHub Issues · Projects and boards · Actions and CI · Codespaces · renv for package versions · pre-commit hooks · signed commits


Learn each of these the first time you actually need them.

Git is not intuitive



Nobody knows all of git. Look it up or ask Claude/Codex to help you.

The cheatsheet



What Terminal RStudio Positron
See what changed git status Git pane Source Control (Ctrl/Cmd+Shift+G)
See the diff git diff Diff button click the file
Stage a file git add <file> tick Staged + on the file
Stage everything git add . header checkbox + on Changes
Commit git commit -m "..." Commit message box, then
Send git push ↑ Push Sync Changes
Get git pull ↓ Pull Sync Changes
History git log --oneline History (clock) Graph
New branch git switch -c name branch dropdown branch in status bar
Switch branch git switch name branch dropdown branch in status bar
Undo a file git restore <file> Revert discard changes (↩︎)