Version control with git

RaukR 2026 • Data Science With R

Work reproducibly and together
Author

Nina Norgren

Published

18-Aug-2026

0.1 Install git

If you don’t have git installed on your computer, go here and follow the installation instructions:

0.2 Configure git

Before your first commit, git needs to know who you are. It stamps every commit with a name and email — this is not a login, just the author label baked into the commit. Set it once, globally:

git config --global user.name "Your Name"
git config --global user.email "you@example.org"

If you prefer to do it the R way, there’s of course a package for this: usethis

usethis::use_git_config(
  user.name  = "Your Name",
  user.email = "you@example.org"
)
Tip

Use the same email you use (or will use) on GitHub — it’s how GitHub links your commits to your account later.

If you want, you can also update your default branch name. Git uses master as default name, while GitHub has switched to main,

git config --global init.defaultBranch main

Check what you’ve set at any time with:

git config --list

There are different ways to use git locally on your computer. Below we will introduce two ways, the command line or through Positron. Try either or both. You can also mix using them. Alls settings you have configured are global, and are applied regardless if you interact with git through the commandline or through Positron.

1 The basics - Command line

1.1 Initiate an empty git repository

git init

You can see that git has added a hidden folder called .git in the root folder where you initiated the git repository. To check on the status of the repository, you can run:

git status

As we have no files in this folder yet, git will just tell you something like this:

On branch main

No commits yet

nothing to commit (create/copy files and use "git add" to track)
Tip

If you try to run git status in a non-Git directory, it will say that it is not a git repository. Git looks for the hidden directory .git/ where all information is stored. This hidden directory contains all information and settings Git needs in order to run and version track your files. This also means that your Git-tracked directory is self-contained, i.e. you can simply delete it and everything that has to do with Git in connection to that directory will be gone.

1.2 Create a file

Create a file with some random content, either in an editor or just using the command line:

echo "Content to my file" > random.txt

Once you have done that, run git status again. It will tell you that there are files in the directory that are not version tracked by Git.

1.3 Add and commit changes

We will now commit the untracked files. A commit is essentially a set of changes to a set of files. Preferably, the changes making out a commit should be related to something, e.g. a specific bug fix or a new feature.

  • Our first commit will be to add the copied files to the repository. Run the following (as suggested by git status):
git add random.txt
  • Run git status again! See that we have added random.txt to our upcoming commit (listed under “Changes to be committed”). This is called the staging area, and the files there are staged to be committed.

  • We are now ready to commit! Run the following:

git commit -m "Add random file"

The -m option adds a commit message. This should be a short description of what the commit contains. If you omit the -m flag it will open a text editor where you can write the message in.

  • Run git status again. It should tell you “nothing to commit, working directory clean”.

1.4 Change a file

Let’s repeat this process by editing a file!

  • Open up random.txt in your favourite editor and add a bit more text to the file.

  • Run git status. It will tell you that there are modifications in one file (random.txt) compared to the previous commit. This is nice! We don’t have to keep track of which files we have edited, Git will do that for us.

  • Run git diff random.txt. This will show you the changes made to the file. A - means a deleted line, a + means an added line. A few lines before and after the changes are also shown for context.

  • Now let’s add and commit the changes to this file.

You can track all changes made to files that are version controlled using git log:

git log
NoteQuick recap

These are the git commands we have used this far:

  • git init tells Git to track the current directory.
  • git status is a command you should use a lot. It will tell you, amongst other things, what files are changed, if they are staged or commit.
  • git add <file> adds a file to the staging area.
  • git commit commits the file to git.
  • git diff <file> shows the difference between the commited file and unstaged file.
  • git log show a log of all commit details.

2 The basics - Positron

2.1 Start a new project in Positron

Click on Initialize Git repository. Creates a hidden .git folder in your project and a README.md file.

2.2 Create a file

Add a new file to the folder and write something in it.

2.3 Add and commit changes

Now you want to first add your changes to the staging area. Do this by going to the Source Control tab in on the left hand side in Positron. There you will see a list of all files with or without changes. The small U to the right of the file means the changes have not been staged.

Click on the small plus next to the file to stage the file. Do this with all the files you want to later commit. Once it’s staged it will be marked with a small A. Now you can add a short but informative commit message, and commit your changes. They are now saved in git.

2.4 Change a file

Try adding a few more files and change some existing ones. If you change a file you can click on the file in the source control tab, and see the difference to the file that is already committed, and the new changes.

Stage and commit the changes. Now, let’s look at the history. In Positron you can look at the git graph and see all changes made to files at certain commits:

All those files are now version controlled!

3 Keeping order

So far we’ve only added things. Now let’s practise taking them back.

3.1 Unstage a file (but keep the edit)

  • Open random.txt, add a line, and stage it:
git add random.txt
  • Run git status and confirm it’s listed under “Changes to be committed”.

  • Now unstage it:

git restore --staged random.txt
  • Predict before you run: is your new line gone, or just unstaged? Run git status to check.

The line is still there, the file is simply back to “modified but not staged”. git restore --staged restored the staging area from the last commit; it never touched your working file.

TipIn Positron

Hover the staged file in the Source Control panel and click the (minus) to unstage it — same as git restore --staged. Your edit stays put.

3.2 Discard an unstaged change (this one bites)

  • With that edit still unstaged, discard it:
git restore random.txt
  • Run git status, then open the file.

This time the edit is gone. The bare git restore restored the working file from the staging area and there was nothing staged, so it reverted to the last commit.

Important

Same command, two trees. git restore --staged <file> rewinds the staging area (your edit survives in the file). git restore <file> rewinds the working file (your edit is discarded). The flag decides which one you’re undoing. There’s no undo for the second one, the change wasn’t committed, so git has nothing to bring back.

TipIn Positron

Hover the file and click the Discard Changes (revert) icon to throw the edit away, same as git restore. Positron asks you to confirm, because, as on the command line, there’s no getting it back.

4 Telling git what to ignore

Some files should never be tracked: editor junk, rendered output, large data, secrets, etc. Git keeps a full copy of every version of every tracked file, so ignoring the right things keeps your history clean and your repo small.

4.1 Create some junk, then ignore it

  • Make a couple of files you would never want in version control:
echo "old session" > .Rhistory
echo "rendered" > report.html
  • Run git status. Git wants to track both.

  • Create a file called .gitignore in the repository if you don’t already have it, with these contents:

# R session junk
.Rhistory
.RData
.Rproj.user/

# Rendered output
*.html
  • Run git status again.

The junk has vanished from the list. Git now ignores anything matching those patterns. (Note: you do want to commit .gitignore itself, it’s part of the project.)

5 Working on parallel lines: branches

A branch lets you work on something without disturbing your main line of work. The mental model that makes everything else click: a branch is just a movable pointer to a commit. Making a commit moves the current pointer forward one step. That’s why branching is instant, git writes a tiny pointer, it doesn’t copy your files.

Useful commands for branches from terminal:

  • git branch <name of branch> : Create a new branch.
  • git branch : See list of branches. Current branch marked with *.
  • git switch <name of branch> : Move to branch.
  • git merge <name of branch> : Merge the branch you are currently on with the branch named in command.

5.1 See where you are now

  • Draw the current history:
git log --oneline --graph --all

Using the --onelin flag compresses the commit history to just one line for readability. Note the labels — main and HEAD are both pointing at your latest commit. HEAD means “the branch I’m currently on”.

5.2 Make a branch and commit on it

  • Create a branch and move onto it in one step:
git switch -c experiment
  • Run git status (it tells you which branch you’re on) and git branch (the * marks the current one).

  • Make a change to random.txt and commit it.

  • Draw the graph again:

git log --oneline --graph --all

Watch what moved: experiment advanced to the new commit, main stayed put. You just saw the pointer move.

5.3 Switch back and merge

  • Go back to main:
git switch main
  • Open random.txt. Your experiment change isn’t here, main never moved.

  • Merge the branch in:

git merge experiment
  • Draw the graph once more.

Because main hadn’t moved on its own, git just slid its pointer forward to catch up with experiment. This is a fast-forward merge, no new commit needed.

TipIn Positron

The current branch name sits in the status bar (bottom of the window). Click it to create or switch branches. The Git Graph view shows the same picture as git log --graph. Everything in this block works through those, but doing it on the command line first makes it obvious what the buttons are doing.

5.4 When git can’t decide: merge conflicts

A conflict isn’t an error. It’s git refusing to guess when two branches changed the same lines. You, not git, decide which version wins. This is the single most useful thing to have done once in a safe sandbox before it happens for real.

5.5 Set up two conflicting edits

  • Make sure you’re on main and that random.txt has a known first line. Commit if needed so you have a clean starting point.

  • Create a branch, change the first line one way, and commit:

git switch -c feature
# edit random.txt - change the first line to, say, "Hello from feature"
git add random.txt
git commit -m "Change greeting on feature"
  • Switch back to main and change the same line differently:
git switch main
# edit random.txt - change the same first line to "Hello from main"
git add random.txt
git commit -m "Change greeting on main"

Both branches now have a different version of the same line.

5.6 Trigger and resolve the conflict

  • Merge:
git merge feature

Git stops and tells you there’s a conflict. Run git status, it lists the file under “Unmerged paths” and, helpfully, tells you what to do next.

  • Open random.txt. Git has written both versions in, marked like this:
<<<<<<< HEAD
Hello from main
=======
Hello from feature
>>>>>>> feature
  • Edit the file by hand: keep the version you want (or combine them), and delete all three marker lines (<<<<<<<, =======, >>>>>>>). If you use Positron it will remove them for you.

  • Tell git you’ve resolved it, and complete the merge:

git add random.txt
git commit
  • Draw the graph:
git log --oneline --graph --all

This time merging the two diverged lines of work produced a real merge commit, you can see the two branches joining back together.

6 Additional reading and exercises

Want more? There’s tons of git resources out there. Below are a few of them with topics that might come in handy:

How to undo things
Tagging your work
Deep dive into branching

7 Sharing your work: remotes and GitHub

Everything so far has been local. A remote is a copy of your repo somewhere else, usually GitHub, so you can back it up, share it, and collaborate.

7.1 Create your Personal access token (PAT)

ImportantAuthentication

GitHub stopped accepting passwords for git in 2021, so your first push will ask for credentials. Two options:

  • HTTPS + Personal Access Token (PAT): easiest to start with.
  • SSH keys: set up once, nothing to paste afterwards.

R users have a gentle path for all of this, see below.

If you don’t already have SSH keys set up for GitHub, create PATs using the usethis and gitcreds packages. Install them if you haven’t already:

install.packages(c("usethis", "gitcreds"))

usethis::create_github_token()  # walks you through making a PAT
gitcreds::gitcreds_set()  # Helps you set up the PAT

You can verify that everything worked by running usethis::git_sitrep().

7.2 Connect your repo to GitHub

  • On GitHub, create a new empty repository (no README, no .gitignore. We want it empty so it doesn’t immediately diverge from yours). Copy its URL.

  • Back in your local repo, connect it. The conventional name for your main remote is origin:

git remote add origin <url-you-copied>
git remote -v        # check it's registered
  • Push your commits up, setting origin/main as the default for next time:
git push -u origin main
  • Refresh the GitHub page, your files and full history should now appear.

7.3 git the R way: usethis

Now that you know what each step does underneath, here’s how to do the whole setup from the R console. The usethis package wraps the same git operations you’ve been running by hand.

install.packages("usethis")

usethis::use_git()        # init a repo + make the first commit
usethis::use_github()     # create a GitHub repo and connect + push to it
usethis::use_git_ignore(  # add entries to .gitignore
  c(".Rhistory", ".RData", ".Rproj.user")
)

7.4 Pull changes down

  • On GitHub, edit a file directly in the browser and commit it there. Now your remote is one commit ahead of your local copy.

  • Bring that commit down:

git pull

git pull is really git fetch (download) + git merge (combine), which means a pull can produce a merge conflict exactly like the one you just resolved.

7.5 Make edits to your local files

  1. Edit random.txt locally.
  2. Stage and commit the change.
  3. Check git status.
git status

Push the new commit up to GitHub:

git push

7.6 Create a branch and push to GitHub

Create a new branch locally, switch to it, and make a commit:

git switch -c new-feature
# add new file
git add newfile.txt
git commit -m "Add new feature"

Push the branch to GitHub, here you will again have to specify the remote and the branch name:

git push -u origin new-feature

7.7 Make a Pull Request in GitHub

After pushing a branch to GitHub, you can create a Pull Request (PR) to merge your changes into the main branch. Go to your repository on GitHub, and you should see a prompt to create a pull request for your new branch. Click on “Compare & pull request”, add a description of your changes, and submit the pull request.

GitHub will show you the differences between your branch and the main branch, and you will see if there are any conflicts before merging.

Merge the pull request once it has been reviewed and approved. You can do this by clicking the “Merge pull request” button on GitHub.

Now your changes from the new-feature branch will be merged into the main branch, and you can delete the feature branch if you no longer need it.

As GitHub is now ahead of your local repository, you will need to pull the changes to your local repository to keep it up to date:

git switch main
git pull

Now the changes from the pull request are also reflected in your local repository. It is good practice to remove the feature branch after the pull request has been merged, both locally and on GitHub:

git branch -d new-feature  # delete local branch
git push origin --delete new-feature  # delete remote branch

8 Forks and pull requests

Forking a repository on GitHub means creating a copy of the repository on your GitHub user. This can be very useful if you want to make some changes to a repository that you do not have push access to, for example. It is also a great way collaborate because it adds more backups of the repository and because of pull requests, which are a way of implementing your local changes to the source repository where the owner of the repository has the power to review and choose whether to accept them or not.

For example, in this RaukR course we have a repository for all the teaching materials that all teachers have forked, made changes to and then submitted a pull request back to the source repository.

To fork a repository you can either use the GitHub website of the repository you want to fork, or you can use usethis.

usethis::create_from_github(
  "https://github.com/<user name of owner>/<repository name>",
  destdir = "~/path/to/place/repo/",
  fork = TRUE
)

This command does a lot of things, from happygitwithr

  • Forks the source repo on GitHub.
  • Clones your fork to a new local repo. This configures your fork as the origin remote.
  • Configures the source repo as the upstream remote.
  • Sets the upstream tracking branch for main (or whatever the default branch is) to upstream/main.
  • Opens a new Positron instance in the new local repo.

Remotes are connections for your repository, and being upstream means being a repository closer to the source than your repository, which is downstream (as it is a fork). So the sentence “Configures the source repo as the upstream remote” translates to adding a connection specifying that the source repository is where your repository came from. This connection is used to update your fork with any future changes that occur in the source repository.

8.1 Let’s get real!

We’re going to try a “real-world setup” where there are several collaborators working on a project. The source repository is the one that is owned by the maintainer of the project, and you will fork it to your own GitHub account, clone it to your local computer, make some changes and then submit a pull request back to the source repository. The repository contains Quarto code that generates a website, and the maintainer of the source repository will review your changes and decide whether to accept them or not.

The website in question is this one: https://ninanorgren.github.io/raukr-awesome-website/. As you might have guessed, ChatGPT did most of the work here (hence the emoji bonanza). But the main point here is for you to try out a situation where you need to fork a repository, make some changes and submit a pull request. This is a common workflow in open source projects.

The GitHub repository you will be working with is https://github.com/ninanorgren/raukr-awesome-website. The descriptions for the exercise is in the website itself, so familiarize yourself with it before you start. The maintainer of the source repository will review your pull requests and see if we can merge them.

If you want to use R and usethis to fork and clone the repository, here are a few useful commands:

usethis::pr_init(branch = "branchName")  # create new branch and switch to it
usethis::pr_push()  # push changes and open browser to submit pull request

There is a whole family of pr-* functions for both the person submitting the pull the request and the maintainer of the source repository that will review and accept/decline the pull request. I really recommend you read here if you think this is of interest!

8.2 Disclaimer

This might crash and burn… We’ll see!

8.3 Session

Click here
sessionInfo()
R version 4.5.3 (2026-03-11)
Platform: x86_64-conda-linux-gnu
Running under: Ubuntu 26.04 LTS

Matrix products: default
BLAS/LAPACK: /home/roy/miniforge3/envs/r-4.5/lib/libopenblasp-r0.3.33.so;  LAPACK version 3.12.0

locale:
 [1] LC_CTYPE=en_US.UTF-8       LC_NUMERIC=C              
 [3] LC_TIME=en_US.UTF-8        LC_COLLATE=en_US.UTF-8    
 [5] LC_MONETARY=en_US.UTF-8    LC_MESSAGES=en_US.UTF-8   
 [7] LC_PAPER=en_US.UTF-8       LC_NAME=C                 
 [9] LC_ADDRESS=C               LC_TELEPHONE=C            
[11] LC_MEASUREMENT=en_US.UTF-8 LC_IDENTIFICATION=C       

time zone: Europe/Stockholm
tzcode source: system (glibc)

attached base packages:
[1] stats     graphics  grDevices utils     datasets  methods   base     

loaded via a namespace (and not attached):
 [1] htmlwidgets_1.6.4 compiler_4.5.3    fastmap_1.2.0     cli_3.6.6        
 [5] tools_4.5.3       htmltools_0.5.9   otel_0.2.0        yaml_2.3.12      
 [9] rmarkdown_2.31    knitr_1.51        jsonlite_2.0.0    xfun_0.59        
[13] digest_0.6.39     rlang_1.3.0       evaluate_1.0.5