0. Basic Git workflow and vocabulary
Before jumping to demos and exercises on Github, it is important to understand the basic workflow and vocabulary of Git.
Git works with branches and commits. Let’s leave branches aside for now, all you need to know is that by default Git will create a branch called main.
Commits
A commit is a snapshot that records all changes compared to the previous commit. It allows us to divide our work in logical steps and to easily go back in time because we can tell Git to restore a project exactly how it was at a specific commit.
When we start using Git on an existing project, we can put all our changes in a single commit first. From this point in time, we can add future changes in other commits. For instance, our commit history could look like this:
%%{init: {"gitGraph": {"rotateCommitLabel": false}}}%%
gitGraph
commit id: "start using\n git"
commit id: "add a plot to \n view time bins"
commit id: "remove \nfunction `foo()`"
A commit doesn’t have to be large: a simple typo fix can be an entire commit. Instead of their size, what matters more is the content of the commit. It is completely acceptable to make one large commit that contains code to make a regression and to export these results in a table. However, such a commit shouldn’t contain independent changes, such as a new analysis script that is unrelated to the regression. Dividing commits like this makes it easier to go back in time: it is easier to find bugs or to revert unwanted changes if each commit focuses on one thing only.
In the commit history above, all commits are authored by the same person. In many cases however, several people will collaborate on the same git project. Anyone that has the rights to modify your repository (by default, this is just the repository owner) could push new commits. For instance, if you decide to grant this right to a collaborator, your commit history could look like this:
%%{init: {"gitGraph": {"rotateCommitLabel": false}}}%%
gitGraph
commit id: "start using\n git"
commit id: "add a plot to \n view time bins"
commit id: "remove \nfunction `foo()`" type: HIGHLIGHT
commit id: "fix typo \nin the README"
Circles are your commits and squares are your collaborator’s.
One thing we haven’t mentioned above is that to be able to make a new commit, your clone of the repository (local) must be up to date with the repository that is stored on the server (remote). If you lag behind, you will have to pull changes from the server before being able to push your own changes (this introduces the notion of conflicts that we will explore later).
When you work on your own on only one laptop, this is usually not a problem because you are the only one who can push changes anyway, so your local clone should never be out of sync with the server. However, when someone else can make changes as in the example above, you need to be aware of the changes they are making and you need to pull these from time to time to keep your local clone in sync.
Branches
So far, we used self-contained commits as examples, e.g. a commit that creates a plot. Sometimes, the changes we want to make rely on several commits. For example, we may want to add a new function in a package, but we work on this over several days and we need to address feedback from collaborators on the design of this function.
Pushing a lot of intermediate commits to the main branch would have three downsides. First, it would make the commit history more complex because we now have commits that are not self-contained anymore. Second, regularly pushing commits means that all our collaborators would have to pull these (incomplete) changes. Finally, it makes it easier to leave an incomplete feature in the main branch without ever finishing it.
To address this issue, we can use branches. A branch is a sort of parallel environment where we can push our commits. Anyone can create a branch and a git project can have as many branches as you want.
Creating and merging a branch
Let’s take our example above and imagine that we want to add a new function argument called my_arg:
%%{init: {"gitGraph": {"rotateCommitLabel": false}}}%%
gitGraph
commit id: "start using\n git"
commit id: "add a plot to \n view time bins"
commit id: "remove \nfunction `foo()`" type: HIGHLIGHT
commit id: "fix typo \nin the README"
branch create-my-arg
commit id: "basic \nimplementation"
commit id: "add docs"
commit id: "add tests"
We have created a branch create-my-arg and added three commits there to create this new argument, and to add documentation and tests. As you can see, our changes related to my_arg are not on main so none of the concerns raised above exist here. Once everyone is happy with the changes in our branch, we can merge it to our main branch.
%%{init: {"gitGraph": {"rotateCommitLabel": false}}}%%
gitGraph
commit id: "start using\n git"
commit id: "add a plot to \n view time bins"
commit id: "remove \nfunction `foo()`" type: HIGHLIGHT
commit id: "fix typo \nin the README"
branch create-my-arg
commit id: "basic \nimplementation"
commit id: "add docs"
commit id: "add tests"
checkout main
merge create-my-arg id: "Add arg `my_arg`"
Now, anyone working on the project can pull the unique commit Add arg `my_arg` and benefit from the complete implementation of this new argument without having to pull several intermediate commits.
Updating a branch
The scenario above was ideal for us, the developer of my_arg: we took the main branch at a given point, created a branch, made several commits there, and then merged it back into main. This is the easiest branch workflow that exists and it is enough in some cases.
However, if several people can modify the main branch, or if we open several branches to work on independent changes in parallel, then our branch could become outdated!
%%{init: {"gitGraph": {"rotateCommitLabel": false}}}%%
gitGraph
commit id: "start using\n git"
commit id: "add a plot to \n view time bins"
commit id: "remove \nfunction `foo()`" type: HIGHLIGHT
commit id: "fix typo \nin the README"
branch create-my-arg
commit id: "basic \nimplementation"
commit id: "add docs"
commit id: "add tests"
checkout main
commit id: "fix typo \nin docs" type: HIGHLIGHT
The commit fix typos in docs, created by a collaborator, now exists on main but not in our branch. This is an issue because our branch currently refers to main as it was when we created the branch. Ignoring this new commit might cause trouble, for instance if this commit modifies some documentation that our branch also modifies. We need to resolve this by updating our branch to the latest version of main.
There are two ways to do that: rebasing, or merging.
Rebasing is an operation where we change the base of our branch, so that the base no longer is the commit fix typo in the README but instead is fix typos in docs:
%%{init: {"gitGraph": {"rotateCommitLabel": false}}}%%
gitGraph
commit id: "start using\n git"
commit id: "add a plot to \n view time bins"
commit id: "remove \nfunction `foo()`" type: HIGHLIGHT
commit id: "fix typo \nin the README"
commit id: "fix typo \nin docs" type: HIGHLIGHT
branch create-my-arg
commit id: "basic \nimplementation"
commit id: "add docs"
commit id: "add tests"
After rebasing, it looks like we had originally created the branch from the commit fix typos in docs right from the start.
Merging is essentially the same operation we had done before but in the other direction. Instead of incorporating our changes into main, we will incorporate the last commits on main in our branch, and this appears as a commit in our branch:
%%{init: {"gitGraph": {"rotateCommitLabel": false}}}%%
gitGraph
commit id: "start using\n git"
commit id: "add a plot to \n view time bins"
commit id: "remove \nfunction `foo()`" type: HIGHLIGHT
commit id: "fix typo \nin the README"
branch create-my-arg
commit id: "basic \nimplementation"
commit id: "add docs"
commit id: "add tests"
checkout main
commit id: "fix typo \nin docs" type: HIGHLIGHT
checkout create-my-arg
merge main id: "merge main\nin our branch"
Should we choose merging or rebasing? This is one of the few debates among git users, but generally it is not worth fighting about this. Both have pros and cons, starting with the ease of use. Merging is in general easier to manage when there are conflicts to resolve, e.g. if the commit from main that we want to integrate in our branch modifies some files that our branch also modifies.
No matter which approach you use, if everything went well then we can merge our branch into main once it is ready.
After rebasing:
%%{init: {"gitGraph": {"rotateCommitLabel": false}}}%%
gitGraph
commit id: "start using\n git"
commit id: "add a plot to \n view time bins"
commit id: "remove \nfunction `foo()`" type: HIGHLIGHT
commit id: "fix typo \nin the README"
commit id: "fix typo \nin docs" type: HIGHLIGHT
branch create-my-arg
commit id: "basic \nimplementation"
commit id: "add docs"
commit id: "add tests"
checkout main
merge create-my-arg id: "Add arg `my_arg`"
After merging:
%%{init: {"gitGraph": {"rotateCommitLabel": false}}}%%
gitGraph
commit id: "start using\n git"
commit id: "add a plot to \n view time bins"
commit id: "remove \nfunction `foo()`" type: HIGHLIGHT
commit id: "fix typo \nin the README"
branch create-my-arg
commit id: "basic \nimplementation"
commit id: "add docs"
commit id: "add tests"
checkout main
commit id: "fix typo \nin docs" type: HIGHLIGHT
checkout create-my-arg
merge main id: "merge main\nin our branch"
checkout main
merge create-my-arg id: "Add arg `my_arg`"
Another “good practice debate” between git users is about the moment when we merge a branch into main. As we will see in other sections, we have the option of “squashing” commits before merging. This affects the commit history on the main branch:
- if we do not squash, then all commits from our branch (potentially hundreds of commits) will also appear in the history of
main; - if we squash, all the commits from our branch will be condensed into a single commit, and only this commit will appear in the history of
main.
The former can make it easier to find when a bug was introduced because we still have all these small commits so it can be easier to see which one contains the bug. The downside is that the author must be careful that all commits in the merged branch have a good name and description, otherwise it can make the commit history very hard to parse.
In the examples above, we chose to follow the “squashing” approach so that the final commit history contains one commit per new feature, or per bug fix for instance.
Conclusion
This section briefly presented two of the most important concepts in the git workflow: commits and branches. The next sections will focus on GitHub, a platform where we can store our code and that provides tools to facilitate collaborative coding with interfaces to create branches, open pull requests, review code, and more.
Session info
─ Session info ───────────────────────────────────────────────────────────────
setting value
version R version 4.6.2 (2026-10-29)
os Ubuntu 26.04.1 LTS
system x86_64, linux-gnu
ui X11
language (EN)
collate C.UTF-8
ctype C.UTF-8
tz UTC
date 2026-11-11
pandoc 3.7.0.2 @ /usr/bin/pandoc
quarto 1.11.9 @ /usr/local/bin/quarto
──────────────────────────────────────────────────────────────────────────────