Git & GitHub#
This guide is a practical, end-to-end reference for using Git and GitHub in a
research and machine-learning setting. It is written for researchers, students, and
engineers who produce code, configurations, notebooks, and papers, and who need to
collaborate without losing work, overwriting one another, or corrupting a shared history.
The emphasis throughout is on building an accurate mental model first and then layering commands on top of it. Most confusion with Git comes not from the commands but from not knowing what state the repository is in; once you have a clear mental model, the commands become much more intuitive and easier to recall.
Tip
You do not have to read this top to bottom. Many people already have a working knowledge of Git — use the section headers (and the “On this page” list below) to jump straight to the topics you want to level up on, and skip anything you already know.
Scope. This document covers version control with Git and collaboration on GitHub. It deliberately stops at the point where automation begins: automated testing and continuous integration and deployment (CI/CD with GitHub Actions) are substantial topics in their own right and are out of scope here.
Note
Hands-on practices are marked throughout with a Practice callout, and a dedicated Hands-On Exercises section at the end ties them together into short labs. You only need a terminal and a free GitHub account to follow along.
Why Version Control#
Research and machine-learning work is, at bottom, a sequence of experiments whose results
must be trustworthy and reproducible. Without disciplined version control this breaks
down quickly: code is emailed around, directories named final, final_v2, and
final_REALLY_final accumulate, and six months later nobody can say which version of the
code produced which figure in the paper.
Git is a distributed version-control system. It records snapshots of your project
over time, lets many people work in parallel, and lets you move backward and forward
through that history with confidence. Because it is distributed, every clone is a
complete copy of the entire history — you can commit, branch, inspect, and diff completely
offline.
GitHub is a hosting platform built on top of Git. It stores a shared copy of a
repository in the cloud and adds the collaboration layer that Git itself lacks: pull
requests, code review, issue tracking, access control, and automation.
For a research group specifically, version control delivers four things that are otherwise very hard to guarantee:
Provenance — every line of code is traceable to who wrote it, when, and why.
Reproducibility — any past result can be regenerated by checking out the exact commit (and, with discipline, the exact environment) that produced it.
Parallelism — several people can develop features and experiments simultaneously without stepping on each other.
Safety — committed and pushed work is extraordinarily hard to lose, which makes bold refactoring and experimentation low-risk.
Warning
That safety only applies to work you have pushed to a remote. A branch that lives only on your laptop is one hard-drive failure — or one bad rebase — away from being gone for good. If you have made substantial local changes, push them (even to a work-in-progress branch) so a copy exists on the server.
Important
Git and GitHub are not the same thing. Git is the tool that versions your files and runs entirely on your machine. GitHub is one of several websites (others include GitLab and Bitbucket) that host Git repositories and add collaboration features. Git is fully usable with no GitHub account at all.
Installation and First-Time Setup#
Install Git#
Use your distribution’s package manager:
sudo apt update && sudo apt install git # Debian / Ubuntu
sudo dnf install git # Fedora / RHEL
Install via Homebrew (recommended) or the Xcode command-line tools:
brew install git
# or trigger Apple's installer:
xcode-select --install
Download and install Git for Windows, which bundles Git Bash — a terminal that provides the same commands shown throughout this guide.
Verify the installation:
git --version
Configure Your Identity#
Every commit is permanently stamped with an author name and email. Set these once so your contributions are attributed correctly, both locally and on GitHub.
git config --global user.name "Your Name"
git config --global user.email "you@example.com"
Important
Use the same email that is registered on your GitHub account. If the two differ, your commits will not be linked to your profile and will not appear in contribution graphs — a common and confusing source of “missing” work.
A few defaults are worth setting immediately; they prevent whole classes of beginner friction:
git config --global init.defaultBranch main # name the first branch "main", not "master"
git config --global pull.ff only # refuse surprise merge commits on pull
git config --global push.autoSetupRemote true # first push auto-creates the upstream branch
git config --global core.editor "nano" # editor used for commit messages
Inspect your configuration at any time:
git config --list --show-origin # every setting and which file it came from
git config user.email # show a single value
Note
Configuration lives at three levels, each overriding the one above it: system
(--system, the whole machine), global (--global, your user account), and
local (--local, a single repository). Use a local override when one project needs
a different identity — for example, a personal email on a work machine.
How Git Works (a Brief Look Inside)#
You do not need to know Git’s internals to use it, but a small, accurate mental model prevents the majority of confusion. This section is deliberately compact.
The Three Areas#
Every Git project has three places where a version of a file can live. Almost every Git command is, in effect, moving content between them:
Area |
What it is |
|---|---|
Working tree |
The actual files on disk that you edit. |
Staging area (the index) |
A holding zone where you assemble exactly what your next commit will contain. |
Repository (the |
The permanent, committed history of the project. |
The everyday loop moves changes from left to right: you edit files in the working tree,
stage the ones you want with git add, and commit the staged snapshot into the
repository.
working tree --( git add )--> staging area --( git commit )--> repository
^ |
+-----------------------( git restore / checkout )-----------------+
The staging area is the feature that distinguishes Git from simpler tools: it lets you craft a commit out of some of your changes while leaving the rest for a later, separate commit — the basis of clean, atomic history.
The Object Model, Commits, and Branches#
Under the hood, Git is a content-addressed store of four object types. You rarely touch them directly, but knowing they exist demystifies almost everything:
A blob stores the contents of a file; a tree stores a directory listing (names plus the blobs and sub-trees it contains); a commit points to one tree (a full snapshot of the project) plus metadata and its parent commit(s); a tag is a named pointer to a commit.
From this a few important properties follow:
A commit is a complete snapshot, not a diff. It records the whole project state, the author and date, a message, and a link to its parent. Chaining parents forms the history — more precisely a Directed Acyclic Graph (DAG) once branches and merges exist.
Every object is named by a SHA-1 hash of its content (e.g.
a04dbce…). Because the hash covers the content and the parent, history is tamper-evident: change anything and every downstream hash changes. (This is also why rewriting history — see Merge vs. Rebase — is possible: it produces new commits with new hashes.)A branch is nothing more than a lightweight, movable pointer to a commit. Creating one writes a single small file; this is why branching is cheap and encouraged. Committing advances the current branch pointer to the new commit.
HEAD is a pointer to where you are now — normally the branch you currently have checked out. A “detached HEAD” (discussed later) simply means HEAD points directly at a commit rather than at a branch.
Tip
Hold on to one idea above all: branching is cheap and local. A large share of Git’s power comes from creating short-lived branches freely and integrating them back.
Working with GitHub#
Everything so far has been entirely local. GitHub introduces a remote: a shared copy of the repository that you and your collaborators push to and pull from.
Authenticating: SSH vs. HTTPS#
GitHub no longer accepts an account password on the command line; you must use one of two mechanisms:
Method |
How it works |
Notes |
|---|---|---|
SSH |
Add your machine’s public SSH key to GitHub, then clone with |
Best for a machine you control; no prompts after setup. The same key concept used to access the clusters. |
HTTPS + token |
Clone with |
Useful where SSH is blocked by a firewall. Store the token with a credential helper to avoid re-entering it. |
To set up an SSH key (the same procedure used for cluster access):
ssh-keygen -t ed25519 -C "you@example.com" # generate a key pair
cat ~/.ssh/id_ed25519.pub # copy this PUBLIC key into GitHub → Settings → SSH keys
ssh -T git@github.com # test the connection
Important
Only ever upload your public key (the .pub file). Your private key must never be
shared, copied to a shared machine, or committed to a repository.
Remotes: Connecting Local to GitHub#
A remote is a named URL pointing at a hosted repository. By convention the primary
remote is called origin.
git remote -v # list configured remotes
git remote add origin git@github.com:you/repo.git # link a local repo to GitHub
git push -u origin main # first push; -u sets the upstream
Once the upstream is set, plain git push and git pull operate on the correct branch
automatically.
Push, Fetch, and Pull#
Command |
What it does |
|---|---|
|
Upload your local commits to the remote. |
|
Download remote changes without touching your working files — for inspection first. |
|
|
Tip
When collaborating, prefer git fetch followed by a review of git log origin/main
before integrating. A reflexive git pull can trigger a surprise merge or conflict at
an inconvenient moment.
Visualising Branch History#
Reading the commit graph is a skill, and good visual tools make it effortless:
The GitHub network graph. Every repository has an interactive branch/commit graph at
…/network— for example, the public github.com/Amii-Engineering/amii-tools/network. It is the quickest way to see how branches diverged and merged.The GitHub Desktop app and the repository’s “Insights → Network” view show the same history graphically.
In your editor (VS Code, Cursor, …), the GitLens extension is invaluable: it draws the branch graph, and — most useful day to day — when you hover over any line of code it shows the last commit that changed it, with a click-through link to view that commit on GitHub. It surfaces a great deal of “who changed this and why” without leaving the file.
Practice — publish a repository
Create a new, empty repository on GitHub (no README).
In a local project, run
git remote add origin <url>andgit push -u origin main.Edit a file through GitHub’s web UI and commit it there, then run
git pulllocally and watch the change arrive. Finally, open the repo’s/networkgraph to see it.
The Solo Workflow#
This is the core loop you will use every day, long before any collaboration enters the picture.
Starting a Repository#
You either create a new repository or copy an existing one:
# Option A: start a brand-new repository in the current folder
git init
# Option B: copy an existing repository (SSH URL preferred — see "Working with GitHub")
git clone git@github.com:Amii-Engineering/amii-docs.git
Checking Status and Staging#
git status is the single most useful command in Git: it reports what has changed, what
is staged, and which branch you are on. Run it liberally — before and after every other
command — until the repository’s state is always clear to you.
git status # what changed? what is staged? which branch?
git add experiment.py # stage one specific file
git add -u # stage changes to tracked files only (no new files)
git add -A # stage everything: modifications, new files, and deletions
git restore --staged file # unstage (keep the edits, just remove from the next commit)
Tip
Use git add -p to stage changes hunk by hunk. This is invaluable for splitting a
messy working tree into several small, coherent commits instead of one sprawling one.
Committing#
A commit records the staged snapshot together with a message.
git commit -m "Add learning-rate sweep to training script"
# stage all tracked, modified files AND commit in one step:
git commit -am "Fix off-by-one error in data loader"
Note
The commit summary is where you — and every future developer, including yourself — come
to understand why a change was made. A conventional style keeps that history readable:
write the summary in the imperative mood (“Add”, “Fix”, “Refactor”), keep it short, and
add a body when the reasoning is not obvious. Many teams adopt
Conventional Commits (feat:, fix:,
docs: …); see Commit Message Conventions.
Inspecting History and Differences#
git log --oneline --graph --decorate --all # compact, visual history of all branches
git show <commit> # exactly what one commit changed
git diff # unstaged changes (working tree vs index)
git diff --staged # staged changes (index vs last commit)
git blame <file> # who last changed each line, and in which commit
Ignoring Files with .gitignore#
Not everything belongs in version control. A .gitignore file lists patterns that Git
should never track. Getting this right from the first commit is critical for research
repositories, which otherwise fill up with large data files, model checkpoints, and — most
dangerously — secrets.
# Python
__pycache__/
*.pyc
.venv/
# Data & model artifacts (keep these out of Git — see "Large Files and Datasets")
data/
*.csv
*.ckpt
*.pt
# Secrets & environment
.env
*.key
# Editor / OS noise
.vscode/
.DS_Store
Important
.gitignore only affects files Git is not already tracking. If a file was already
committed, adding it to .gitignore does nothing until you stop tracking it with
git rm --cached <file> (this keeps the file on disk but removes it from the index).
Practice — your first repository
Create a folder, run
git initinside it, and add aREADME.md.Stage and commit it, then run
git log --onelineto see your commit.Create a
.envfile, add.envto.gitignore, and confirm withgit statusthat Git now ignores it.
Branching and Merging#
Branches let you develop a feature, experiment, or fix in isolation without disturbing a known-good branch. They are the foundation of both safe solo experimentation and team collaboration.
Creating and Switching Branches#
git branch # list local branches (current one marked with *)
git switch -c feature/new-loss # create AND switch to a new branch
git switch main # switch back to main
git branch -d feature/new-loss # delete a branch once it is merged
Note
git switch (change branches) and git restore (discard file changes) are the
modern, purpose-built commands. The older git checkout still does both jobs —
git checkout -b feature/new-loss is equivalent to git switch -c feature/new-loss
— and you will see it throughout older tutorials and answers.
Tip
Adopt a branch naming convention and use it consistently, e.g. feature/…,
fix/…, experiment/…, docs/…. Prefixes make the branch list self-documenting
and easy to filter.
Merging#
When the work on one branch is ready, you integrate it into another with git merge. You
switch to the branch that should receive the work, then merge the other branch into it. For
example, to fold a small fix branch into a larger feature branch:
git switch feature/binary-classification # the branch that will receive the work
git merge fix/new-loss # merge the fix branch into it
There are two possible outcomes:
Fast-forward: if the receiving branch has not moved since you branched, Git simply advances its pointer to your commits. No new commit is created, and the fact that a separate feature branch ever existed disappears from the history.
Merge commit: if both branches have new commits, Git combines them and records a new merge commit with two parents, preserving the fact that the work happened in parallel.
git merge --no-ff fix/new-loss
The --no-ff flag forces Git to always create a merge commit, even when a fast-forward
would be possible. This deliberately preserves the historical existence of the feature
branch and groups its commits together under one merge — which makes the DAG far easier to
read and a feature far easier to revert as a unit. Many teams standardise on --no-ff
merges into shared branches for exactly this reason (see the
git-flow model).
Note
Branches can branch off branches. Nothing forces every branch to start from main
or staging. It is common to branch a small fix/… off a larger feature/…,
merge it back in as above, and let several such sub-branches accumulate on the feature
until it is a coherent unit of work. That whole unit is then rolled up into a release
and pull-requested into a shared branch (staging/main) in one go.
git merge vs. a Pull Request — when to use which
A git merge on your own machine and a Pull Request on GitHub both integrate one
branch into another, but they serve different purposes:
A local merge is the right tool for integrating your own branches together — folding a fix into your feature, or keeping a private branch current. It is immediate and leaves no record on GitHub.
A Pull Request is the right tool for landing work in a shared branch (
main,staging, a QA/production environment). It exists to get the change reviewed before it lands, and it leaves a durable, searchable record on GitHub: every closed PR is a self-contained “here is a chunk of work that shipped, and why.” Reviewing the list of merged PRs is far easier than archaeology through the raw commit graph hunting for where individual branches were merged in.
A good habit — even when you work alone — is to always land work on main/staging
through a PR rather than a direct local merge. It keeps the same flow familiar, and it
gives you the same reviewable history you would want on a team. (In practice most teams
also enable branch protection on these branches, which requires a PR anyway.)
Merge vs. Rebase#
Both integrate one branch’s work into another, but they produce very different histories. This distinction is one of the biggest “level-ups” in day-to-day Git, so it is worth studying the pictures below.
Approach |
What it does |
When to use |
|---|---|---|
Merge |
Joins histories with a merge commit, preserving the true, branching shape of events. |
Integrating a finished branch into a shared branch; anything already public. |
Rebase |
Replays your commits on top of another branch, producing a clean, linear history. |
Updating and tidying your own feature branch before you open or update a PR. |
The problem rebase solves. Imagine your feature branch falls behind staging
while you work. If you keep merging staging into your feature to stay current, the
history fills with back-and-forth merge commits:
* Merge staging into feature (again)
|\
| * work on staging
* | your commit
|/
* Merge staging into feature
|\
| * work on staging
* | your commit
|/
* branch point
When this branch is finally merged back, the DAG is a tangle of “in and out” merges that is
hard to follow. Rebasing instead lifts your commits and replays them on top of the
latest staging:
git fetch origin
git rebase -i origin/staging # replay MY commits on top of the latest staging
Before rebase (feature was branched off E):
A---B---C feature
/
D---E---F---G staging
After rebase (A B C are replayed on top of G, the tip of staging):
A'--B'--C' feature
/
D---E---F---G staging
Now your work sits in a single straight line on top of staging. When the PR is merged,
the DAG shows one clean branch-off and one clean merge-back instead of a jumble — the
history becomes genuinely easy to read. (The interactive -i flag additionally lets you
reword, squash, or reorder your own commits along the way, e.g. collapsing five
“work in progress” commits into one.)
Warning
Rebasing rewrites history. Because A', B', C' are new commits with new
hashes, they are no longer the commits anyone else may have based work on. If a teammate
has already checked out your branch, pulled A B C, and added their own commit, then
after you rebase and force-push they will end up with both the old and the new copies
of your commits, plus conflicts, when they next pull:
Teammate's local branch after pulling your rebased branch:
...--A---B---C---(their work) <- the original commits, still here
\
A'--B'--C' <- your rebased copies, now also here
This is confusing and painful to untangle. Only rebase commits that are yours and unshared. If in doubt, don’t.
Pushing after a rebase. Because you have rewritten the branch, a normal git push is
rejected — your local history no longer matches the remote. You must force the push:
git push --force-with-lease # preferred: refuses if someone else updated the branch
git push -f # blunt force: overwrites the remote unconditionally
Prefer --force-with-lease: it still overwrites your feature branch, but aborts if the
remote has commits you have not seen (a sign someone else is working on it).
Important
Rebasing is powerful but unforgiving, so build muscle memory on a throwaway branch first,
then use it for real. And remember the escape hatch: at any point during a rebase you
can run git rebase --abort to return to exactly where you started. If you get lost or
the state stops making sense, abort — nothing is lost.
A Branching Model for Team & Production Work#
The commands above are neutral about how a team organises its branches. Adopting a shared model turns Git from a personal tool into a release process — and it is what separates hobby repositories from ones that ship reliably to clients. The model below is a lightly simplified git-flow and is a good default for product and client work.
Branch |
Role |
|---|---|
|
Production. This is what is served to customers — the clients your work ships to. It must always be the latest stable release, with forward-facing documentation. Nothing lands here until it is ready to go live. |
|
The next release. An integration branch that accumulates finished features and is
stabilised until it is ready, at which point it is merged into |
|
Work in progress. Each feature or fix branches off |
The everyday flow is therefore:
Branch
feature/xoffstaging.Do the work; keep the branch current by rebasing onto
origin/staging(see above).Open a PR from
feature/xintostaging. In practice the merge itself happens on GitHub: once the review comments are addressed and checks pass, you click the green Merge button (the merge-commit option corresponds to--no-ff) and you are done.When
stagingis ready to release, mergestagingintomainand tag the release.
Tip
Treating main as sacred, always-deployable production is the single habit that most
improves stability for the client. It also prepares staff for how the wider software
industry works. Read the original
“A successful Git branching model”
for the full picture, including hotfix and release branches.
Undoing Things#
Almost nothing that has been committed is truly lost, but the correct command depends
entirely on the situation. Choosing the wrong one — especially reset --hard — is the
most common way beginners lose work, so it is worth internalising this table.
You want to… |
Command |
|---|---|
Discard unstaged changes to a file |
|
Unstage a file (but keep the edits) |
|
Amend the last commit’s message or contents |
|
Undo the last commit but keep the changes staged |
|
Undo the last commit and unstage the changes (keep the files) |
|
Undo the last commit and discard the changes entirely |
|
Undo a commit that is already pushed / shared |
|
Note
HEAD~1 means “one commit before HEAD”; HEAD~2 is two before, and so on. So
git reset HEAD~3 rewinds the branch pointer back by three commits. You can also name
a commit directly by its hash.
Important
git reset moves your branch pointer and rewrites history — it is only safe on
commits you have not shared. git revert instead creates a new commit that cancels
out an old one, leaving history intact; this is the correct choice for anything already
pushed.
Note
In day-to-day team work, much of this “undo a shared change” happens through the GitHub
web UI rather than the command line. Once work is merged you are usually in review mode
rather than code-writing mode, and a merged PR has a one-click Revert button that
opens a ready-made PR backing the change out — the frontend equivalent of git revert.
See Working in a Team.
Warning
git reset --hard permanently discards uncommitted changes in your working tree. There
is no undo for work that was never committed. When in doubt, git stash first (below)
to set changes aside safely.
Stashing#
git stash parks your uncommitted changes so you can switch context (for example, to a
quick fix on another branch) with a clean working tree, then restores them afterward.
git stash # set aside all uncommitted (tracked) changes
git stash -u # include untracked files as well
git stash list # see stashed entries
git stash pop # reapply the most recent stash and drop it from the list
Recovering “Lost” Commits with the Reflog#
Git records every movement of HEAD in the reflog. Even after a mistaken reset
or a deleted branch, the commit almost always still exists and can be recovered.
git reflog # list recent HEAD positions with their hashes
git switch -c recovered <hash> # bring a "lost" commit back on a new branch
Tip
The reflog is your safety net. Before assuming work is gone, run git reflog — the
commit is typically still reachable for at least 30 days.
Collaborating in a Team#
This is where Git delivers the most value — and where most problems occur. The workflow below, the feature-branch + pull-request model, is what this documentation repository itself uses, and it scales from two people to hundreds.
The Pull-Request Workflow#
Sync your local base branch (
stagingin the model above, ormainin simpler repos) with the remote:git switch staging git pull
Branch for your task, using a descriptive, prefixed name:
git switch -c feature/add-fsdp-example
Work, making focused commits as you go.
Push the branch to GitHub:
git push -u origin feature/add-fsdp-example
Open a Pull Request (PR) from your branch into the base branch. Describe what changed and why, and link any related issue. Open it as a draft if it is not yet ready for review.
Review: teammates comment, request changes, or approve. Push additional commits to the same branch to address feedback — the PR updates automatically.
Merge the PR once it is approved and any required checks pass.
Clean up: delete the merged branch and update your local base branch:
git switch staging git pull git branch -d feature/add-fsdp-example
Fork vs. Branch#
There are two collaboration models; choose based on whether you have write access to the repository:
Model |
When to use |
|---|---|
Shared repository (branches) |
You are a member of the team or organisation with write access. Everyone pushes branches to the same repository and opens PRs internally. This is the typical internal-team setup. |
Fork & pull |
You are an outside contributor without write access. You fork (create your own server-side copy), push branches to your fork, and open a PR from your fork back to the original (“upstream”) repository. This is the standard way to contribute to open-source projects. |
When you fork, keep your copy current by adding an upstream remote:
git remote add upstream git@github.com:Amii-Engineering/amii-docs.git
git fetch upstream
git switch main
git merge upstream/main # bring your fork's main up to date
Keeping a Branch Up to Date#
Long-lived branches drift from their base. Integrate regularly to keep each reconciliation small. For a personal feature branch, rebasing onto the latest base keeps history linear (see Merge vs. Rebase):
git switch feature/my-work
git fetch origin
git rebase origin/staging # linear history; or `git merge origin/staging` to preserve merges
Tip
Integrate from the base branch frequently and in small steps. Ten small conflicts spread across a week are far easier to handle than one enormous conflict at merge time.
Reviewing Code Well#
Code review is where a team’s quality and shared understanding are built. A few habits make it effective:
Keep PRs small and focused. A reviewer can reason carefully about 200 lines; a 1,000-line PR gets a rubber-stamp. Small PRs merge faster and hide fewer bugs.
Review the *why*, not just the *what*. The PR description should let a reviewer judge whether the approach is sound, not only whether the code compiles.
Be specific and kind. Comment on the code, not the author; prefer questions and suggestions over demands.
Reviewing in the age of generative coding tools
A growing share of code now originates from AI assistants and agents (Claude, Copilot, Gemini, Codex, …), and some teams even have agents perform a first pass of PR review. The industry is still working out the norms here, but the centre of gravity of review has shifted:
Did it build the right thing? Modern agents are very good at producing code that runs and is locally correct, so review is less about hunting for subtle syntax bugs and more about verifying that what was built actually matches what was desired — the right feature, the right output, the intended behaviour and edge cases.
Does the change fit the whole? Earlier agents lacked a picture of the broader codebase and would duplicate logic or bolt on approaches that did not fit; that gap has largely closed, but it is still worth confirming the change belongs where it landed and is consistent with the surrounding architecture.
Commit Message Conventions#
A consistent commit-message format makes history readable and machine-parseable. A widely used standard is Conventional Commits, in which each message begins with a type:
<type>: <short summary in the imperative mood>
[optional body explaining the what and why]
Common types:
Type |
Meaning |
|---|---|
|
A new feature or section. |
|
A bug fix. |
|
Documentation, restructuring, formatting, tests, and other supporting changes. |
Note
The convention can be enforced automatically so it does not rely on everyone
remembering it. A commitlint hook (wired in through
pre-commit or Husky, and run in CI) checks each commit message
as it is created and rejects anything that does not match the <type>: <summary> shape.
Practice — a realistic pull request
Fork or clone a sandbox repository and create
feature/add-greeting.Add a line to a file, commit it as
feat: add greeting to readme, and push.Open a PR on GitHub, request a review (from yourself or a teammate), then merge it.
Delete the branch and run
git pullon the base branch to retrieve the merge.
Resolving Merge Conflicts#
A conflict arises when two branches change the same lines of a file in different ways. Git cannot know which version is correct, so it pauses and asks you to decide. Conflicts are a normal part of collaboration, not an error, and resolving them calmly is an essential skill.
When a merge or pull stops with a conflict, Git marks the affected region inside the file:
<<<<<<< HEAD
learning_rate = 0.001 # your version (current branch)
=======
learning_rate = 0.0005 # their version (incoming branch)
>>>>>>> feature/tune-lr
To resolve it:
List the conflicted files with
git statusand open each one.Edit the region into the desired final result, deleting the
<<<<<<<,=======, and>>>>>>>marker lines entirely.Stage the resolved file and complete the merge:
git add config.py git commit # completes the merge (the message is pre-filled)
To abandon the merge and return to the pre-merge state:
git merge --abort
Important
Editor buttons like VS Code’s “Accept Current / Incoming / Both” are convenient for trivial conflicts, but for complex projects prefer resolving conflicts by hand. Real conflicts are often too nuanced to settle by picking one side wholesale — the correct result is frequently a careful blend of both changes, and only reading the code closely will get it right.
Common Problems and How to Fix Them#
This section collects the situations researchers and collaborators hit most often, working both alone and in a team.
Working Alone#
- “Detached HEAD” state.
You ran
git checkout <commit-hash>and Git warns about a detached HEAD. You are viewing an old commit rather than a branch, and new commits made here can be lost. To keep any work, create a branch:git switch -c my-branch. To simply leave,git switch main.- Committed to the wrong branch.
Which fix you need depends on the situation:
If you intended to start a new feature branch anyway — you simply committed on
mainbefore creating it — branch off, then rewindmain:git switch -c feature/intended # the new branch includes your commit git switch main git reset --hard origin/main # rewind main (only if main is not yet pushed!)
If the feature branch already exists and you committed to
mainby accident, copy the commit over withcherry-pickand then rollmainback:git switch feature/existing git cherry-pick <the-stray-commit> # copy it onto the right branch git switch main git reset --hard origin/main # remove it from main (only if main is not yet pushed!)
- Accidentally committed a large data file or a secret.
Removing it in a new commit is not enough — it remains in history. For the most recent commit,
git rm --cached bigfile && git commit --amend. For deeper history, use git filter-repo or the BFG tool. If a secret was committed, rotate it immediately — assume it is compromised the moment it was pushed.- Your branch and origin/main have diverged.
Both you and the remote have new commits. Integrate them with
git pull(which creates a merge) orgit pull --rebase(which replays your commits on top for a linear history), then push.- Orphan / disconnected branches.
Occasionally someone starts a “clean slate” — often to try a radically different approach — with
git checkout --orphanor by importing an unrelated history. This creates a second, disconnected DAG inside the same repository: a branch that shares no common ancestor withmainand can never be cleanly merged back. Avoid this. It makes the project’s history impossible to follow, breaks CI/CD assumptions, and leaves it unclear what is actually delivered to the customer. To explore a very different approach, branch off the existing history like anything else (or use a separate repository) — an orphan branch is almost never what you want.
Working in a Team#
- Push rejected (``! [rejected] … fetch first``).
Someone pushed before you, so your local branch is behind. Run
git pull(resolving any conflicts), thengit pushagain. Do not force-push a shared branch to sidestep this.- The temptation to force-push.
git push --forceoverwrites the remote branch and can erase teammates’ commits. If you genuinely must (for example, after rebasing your own feature branch), use the safergit push --force-with-lease, which refuses to push if someone else has updated the branch since your last fetch. Never force-push shared branches such asmainorstaging— and in practice most repositories enable branch protection, which explicitly forbids force-pushing (and often direct pushing) to them at all.- A merged PR introduced a bug.
Use
git revertto add a new commit that undoes the change — safe for shared history — rather than rewriting the branch. Many people find this easiest from the GitHub web UI: open the merged PR and click the “Revert” button, which opens a ready-made PR that backs the change out.- Line-ending noise (Windows vs. Linux/macOS).
Whole files appear “changed” with no real edits. Normalise line endings with a
.gitattributesfile containing* text=auto, and setgit config --global core.autocrlf input(Linux/macOS) ortrue(Windows).
Large Files and Datasets#
For AI/ML work this deserves special attention: datasets and model checkpoints are large, binary, and constantly changing — exactly what plain Git handles worst.
Why not just commit them? Git stores the full history of every file forever. Commit a 2 GB checkpoint, change it ten times, and every single clone now carries ~20 GB that can never be removed without rewriting history. Binaries also cannot be diffed or merged, so they bloat the repository while providing none of Git’s benefits. Keep data and checkpoints out of Git (via Ignoring Files with .gitignore) and store the payload on appropriate storage — an object store, a shared filesystem, or the cluster’s project space.
When large files genuinely must be versioned, use Git LFS (Large File Storage). LFS keeps a tiny text pointer in the Git history while the real bytes live on a separate LFS server, so clones stay small and history stays clean:
git lfs install # one-time setup per machine
git lfs track "*.pt" "*.ckpt" # choose which patterns LFS should handle
git add .gitattributes # LFS records the tracked patterns here — commit it
git add model.pt
git commit -m "Add trained checkpoint via LFS"
git push # the pointer goes to Git; the bytes go to LFS storage
Cloning and pulling then work as usual — git clone / git pull fetch the pointers and
transparently download the real files from LFS. For heavier, pipeline-style data versioning
(datasets tied to experiments), consider a dedicated tool such as
DVC.
Using Git with Generative Coding Tools#
AI coding assistants and agents (Claude Code, the Gemini CLI, Codex, and others) now write, edit, and even commit code directly. Git is what keeps that productive rather than dangerous — it is the safety layer that lets you accept the good and discard the rest.
Keep agent work on a branch. Point assistants at a
feature/…branch, never straight atmainorstaging. A branch is a sandbox you can throw away.Commit in small, reviewable steps. Frequent commits let you inspect each change with
git diffand cleanlygit revertorgit resetanything the agent got wrong, instead of untangling one giant change.Always review the diff before you push. Treat generated code exactly like a teammate’s PR: the code is usually locally correct, so focus on whether it built the right thing and fits the surrounding codebase (see Reviewing Code Well).
Guard secrets and large files. Agents can accidentally stage a
.envfile or a checkpoint; a solid.gitignoreand a quickgit statusbefore committing are your guardrails.Let them help with Git itself. These tools are genuinely useful for drafting commit messages, explaining a confusing diff, or talking you through a merge conflict — but you remain responsible for what actually gets committed.
Best Practices for Researchers#
Commit early, commit often, in logical units. A commit should capture one coherent change, not a day’s worth of unrelated edits. Atomic commits are easier to review, revert, and understand later.
Write meaningful messages. “Fix bug” is useless in six months; state what changed and why. Follow Commit Message Conventions.
Never commit secrets or data. API keys, credentials, and datasets do not belong in Git. Use
.gitignoreand environment variables, and if a secret leaks, rotate it.Branch per task. Keep production branches always working; do experiments and features on branches.
Integrate frequently. Pull before you push, and rebase/merge from the base branch often to keep conflicts small.
Push work you care about. A local-only branch is not backed up — see the warning under Why Version Control.
Version the environment, not just the code. Commit your
pyproject.toml/ lockfile /requirements.txtalongside the code so the exact dependencies behind a result can be reproduced.Keep notebooks reviewable. Notebook outputs and execution counts create large, noisy diffs and frequent conflicts. Strip outputs before committing (e.g. with
nbstripout) or pair each notebook with a plain-text script (e.g. withjupytext).
Tag Releases and Paper Submissions#
A tag marks a permanent, named point in history — ideal for “the exact code behind this result.” Annotated tags are best because they carry a message and author:
git tag -a v1.0 -m "NeurIPS 2026 submission"
git push origin v1.0 # tags are not pushed by default; push them explicitly
Once pushed, the tag appears on the repository’s main page under Releases/Tags. From
there — or by turning the tag into a GitHub Release — anyone can view that snapshot and
download a .zip or .tar.gz of the repository exactly as it was at that commit.
This is a wonderful habit for reproducibility: link the release (or its commit hash) in your
paper, and reviewers or future readers can retrieve precisely the code that produced your
numbers.
Tips & Tricks#
A grab-bag of high-leverage habits, especially useful once the fundamentals are second nature:
Make an alias for the graph. You will use it constantly. A richly formatted version colours the hash, relative date, subject, and author, which makes the history far easier to scan:
git config --global alias.graph "log --graph --oneline --decorate --all --format=format:'%C(bold blue)%h%C(reset) - %C(bold green)(%ar)%C(reset) %C(white)%s%C(reset) %C(dim white)- %an%C(reset)%C(auto)%d%C(reset)'" # then just: git graph
Stage interactively with
git add -pto build small, coherent commits.Fix an earlier commit cleanly with
git commit --fixup <sha>. This makes a normal commit tagged to say “this belongs to<sha>”. Later, runninggit rebase -i --autosquash <sha>~1reorders the fixup commit next to its target and marks it to be squashed in, so the correction folds silently into the original commit with no new entry in the history. It is the clean way to amend a commit that is not the most recent one (where a plaingit commit --amendwould do). Setgit config --global rebase.autosquash trueto make--autosquashthe default.Grab a single commit from another branch with
git cherry-pick <sha>.Find the commit that introduced a bug with
git bisect— a guided binary search across history that pinpoints the culprit in a handful of steps. For a quick hunt, many people instead reach for blame on the GitHub frontend: open the file, click Blame, and step back through the revisions of a line to find when a change was introduced.git bisectshines when the bug is not tied to one obvious line and you need to binary-search behaviour across the history instead.Work on two branches at once with
git worktree add ../other-branch other-branch. A worktree checks a second branch out into its own folder that shares the same repository, so you can have two branches “live” on disk at the same time — no stashing or committing half-done work to switch. It is handy when, say, a build or long training run is going on one branch and you want to start a fix on another without disturbing it, or to compare two branches side by side in two editor windows.Always prefer
git push --force-with-leaseovergit push --force.Let Git remember conflict resolutions with
git config --global rerere.enabled trueso you do not re-solve the same conflict twice during a long rebase.Use GitLens (or your editor’s Git integration) to see per-line commit history on hover — see Visualising Branch History.
Do not commit large files — use Git LFS instead (see Large Files and Datasets).
Hands-On Exercises#
Work through these in order; together they exercise the full workflow.
Lab 1 — Local fundamentals
Initialise a repository, make three commits to a README.md, then use
git log --oneline, git diff, and git show to inspect them. Finally, use
git restore to discard an uncommitted edit.
Lab 2 — Branching, merging, and rebasing
From main, create feature/x and feature/y. Add a different line to the same
file on each branch. Merge feature/x, then merge feature/y and resolve the
resulting conflict by hand. Bonus: on a fresh branch, add two commits, then
git rebase -i onto main and squash them into one — compare the graph before and
after with git graph.
Lab 3 — Undo and recover
Make a commit, then git reset --hard HEAD~1 to “lose” it. Recover it using
git reflog and git switch -c recovered <hash>. Separately, practise
git revert on a pushed commit.
Lab 4 — End-to-end collaboration
Push a repository to GitHub, open a pull request from a feature branch, request a review,
merge it, delete the branch, and sync your local base branch. Then open the repo’s
/network graph and find your merge. Bonus: fork a public repository, add an
upstream remote, and keep your fork up to date.
Quick Reference#
Command |
Purpose |
|---|---|
|
Show changed and staged files, and the current branch. |
|
Stage a file / stage changes interactively, hunk by hunk. |
|
Stage tracked changes only / stage everything including new files. |
|
Record the staged snapshot. |
|
View compact, visual history. |
|
Create and switch to a new branch. |
|
Merge a branch, always creating a merge commit. |
|
Replay your commits on top of the base branch for a linear history. |
|
Download / download+integrate / upload commits. |
|
Safely push a rebased branch (aborts if others pushed). |
|
Discard unstaged changes to a file. |
|
Undo the last commit, keeping the changes staged. |
|
Safely undo a pushed commit with a new commit. |
|
Copy a single commit onto the current branch. |
|
Park / restore uncommitted changes. |
|
Recover “lost” commits and branch tips. |
|
Mark a reproducible point in history. |
Further Resources#
Pro Git (free book) — the canonical, thorough reference.
A successful Git branching model — the git-flow
main/staging/ feature model referenced above.GitHub Docs — pull requests, forks, and platform features.
GitHub Skills — interactive, hands-on learning modules.
Conventional Commits — the commit-message spec.
Oh Sh*t, Git!?! — concise recipes for getting out of trouble.
Learn Git Branching — an interactive, visual tutorial for branching and merging.