Skip to content

Git and GitHub Essentials

Read this when your team sets up its repository or hits its first merge conflict; it gives you the branching workflow and the settings that protect main.

Git is the version control system that tracks every change to your codebase. GitHub is the platform where your team hosts, reviews, and collaborates on that code. Together, they form the backbone of nearly every modern software project. Without version control:

  • There is no reliable way to undo a mistake or recover a deleted file.
  • Two people editing the same file at the same time creates conflicts nobody knows how to resolve.
  • There is no record of who changed what, when, or why.
  • Deploying code means copying files around and hoping nothing breaks.

If you are new to Git and GitHub, or need a refresher, start with the resources below before continuing with this guide.

Your project might use other platforms like GitLab or Bitbucket, but the concepts are the same. This guide focuses on Git and GitHub because they are the most widely used tools in industry and academia.

A branching workflow defines how and when the team creates branches, reviews changes, and merges code into the main codebase. Without a shared workflow, team members step on each other’s work, the main branch breaks regularly, and nobody is confident that the code they pulled is safe to build on.

Several well-known workflows exist. Each makes different tradeoffs between simplicity, safety, and flexibility.

GitHub Flow is the simplest workflow that supports team collaboration. It has one rule: the main branch is always in a deployable (or at least stable) state. All work happens on short-lived feature branches that are merged back through pull requests.

This is the recommended workflow for most Capstone projects. It is easy to learn, works well for small teams, and enforces code review without adding ceremony.

Git Flow, described by Vincent Driessen in 2010, uses long-lived develop and main branches, plus dedicated branches for releases and hotfixes. Driessen has since added a note to that post recommending against it for teams shipping continuously, which is worth reading as an example of an influential idea whose author narrowed its scope later. It provides more structure for projects with formal release cycles but adds complexity that most Capstone projects do not need. It is common in enterprise environments where multiple versions are maintained simultaneously.

In trunk-based development, everyone commits directly to main (or to very short-lived branches that are merged within hours). This requires strong CI, comprehensive test coverage, and high team discipline. It is used by large engineering organizations (Google, for example) and the DORA research consistently finds it associated with better delivery performance, but it is difficult to adopt without mature testing infrastructure. The disagreement between this row and the one above is real, and it is mostly about how good your tests are: trunk-based development without a trustworthy test suite is just committing to main.

The rest of this guide walks through GitHub Flow step by step, from creating a branch to merging a pull request. Each stage includes the exact commands and actions involved.

Before starting any new work, make sure your local main branch reflects the latest state of the remote repository. This prevents you from building on outdated code.

Terminal window
git checkout main
git pull origin main

Create a new branch for the piece of work you are about to do. The branch name should be short, descriptive, and follow whatever convention your team agreed on in the working agreement.

Terminal window
git checkout -b feature/add-login-endpoint

Common naming conventions:

  • feature/add-login-endpoint for new functionality
  • fix/null-pointer-dashboard for bug fixes
  • chore/update-dependencies for maintenance tasks

If the work corresponds to a GitHub issue, include the issue number in the branch name. This makes it easy to trace a branch back to the conversation that motivated it:

Terminal window
git checkout -b feature/42-add-login-endpoint

Each branch should represent a single, focused unit of work. A branch called feature/add-login-endpoint that also refactors the database layer and updates the README is doing too many things. Keep branches small and focused so they are easy to review and safe to merge.

As you implement the feature, commit your changes in logical increments. Each commit should represent a coherent step, not necessarily every save, but not an entire feature in one commit either.

Terminal window
git add src/auth/login.py src/auth/tests/test_login.py
git commit -m "Add login endpoint with email/password validation"

Write commit messages that explain what changed and why. A reviewer reading the commit history should be able to follow the progression of your work. Chris Beams’s seven rules of a great commit message is the short version most teams converge on, and the rule that matters most is the one people skip: the body explains why, because the diff already shows what.

If your team wants machine-readable history, Conventional Commits adds a type(scope): subject prefix that tools can parse to generate changelogs and pick version numbers. It costs a small amount of discipline per commit and pays off only if you actually run those tools, so adopt it if you plan to automate releases and skip it if you do not.

Good commit messages:

Add login endpoint with email/password validation
Fix token expiration check to use UTC consistently
Add integration tests for login with invalid credentials

Poor commit messages:

WIP
fix stuff
update files

Push your branch to the remote repository so others can see your work and so it is backed up. Until you push, the only copy of your work is on one laptop, which is a risk the rest of this page cannot protect you from.

Terminal window
git push origin feature/add-login-endpoint

If you are pushing the branch for the first time, Git will suggest using the -u flag to set up tracking:

Terminal window
git push -u origin feature/add-login-endpoint

After the first push with -u, subsequent pushes only need git push.

On GitHub, open a pull request (PR) from your feature branch into main. The pull request is where the team reviews your code before it is merged.

A good pull request includes:

  • A clear title that summarizes the change: “Add login endpoint with email/password validation”
  • A description that explains what changed, why, and how to test it. If the change relates to an issue, reference it (Closes #42).
  • A reasonable size. Pull requests that touch hundreds of lines across dozens of files are hard to review well. If your PR is getting large, consider splitting it into smaller, sequential PRs.

At least one teammate (or however many your team agreed on in the working agreement) reviews the pull request. Reviewers should check:

  • Does the code do what the PR description says it does?
  • Is the code readable and consistent with the project’s style?
  • Are there tests, and do they cover the important cases?
  • Does the change introduce any obvious bugs, security issues, or performance problems?

Reviewers leave comments on specific lines or on the PR as a whole. The author responds, makes changes if needed, and pushes additional commits to the same branch. The PR updates automatically.

Terminal window
# After addressing review feedback
git add src/auth/login.py
git commit -m "Handle empty password field per review feedback"
git push

Code review is not about gatekeeping. It is about catching mistakes early, sharing knowledge across the team, and maintaining a codebase that everyone can work in confidently.

Once the PR is approved and any CI checks pass, merge the branch into main. GitHub offers three merge options:

  • Merge commit: preserves the full branch history. Every commit on the branch appears in main’s history, plus a merge commit.
  • Squash and merge: combines all commits on the branch into a single commit on main. This keeps the main branch history clean, especially for branches with many small “fix typo” or “WIP” commits.
  • Rebase and merge: replays the branch commits on top of main without a merge commit. Produces a linear history.

For most teams, squash and merge is a good default. It keeps main’s history readable (one commit per feature or fix) while allowing developers to commit freely on their branches without worrying about messy history.

After merging, delete the feature branch. It has served its purpose.

After a merge, other team members should pull the latest main before starting new work:

Terminal window
git checkout main
git pull origin main

If you already have a feature branch in progress, you can bring it up to date with main:

Terminal window
git checkout feature/your-branch
git merge main

This prevents your branch from diverging too far from main, which makes merging easier later.

Merge conflicts happen when two branches modify the same lines in the same file. Git cannot decide which version to keep, so it asks you to resolve the conflict manually. They are not errors and they are not emergencies: they are Git declining to guess, which is the correct behavior. The reason they feel alarming the first time is that the repository enters a state where the working tree contains markers that are not valid code, and nothing tells you that this state is normal and reversible. It is, and git merge --abort returns you to where you started.

The real lesson conflicts teach is about batch size. Two people editing the same file for a week will conflict; two people merging daily rarely do. If your team hits painful conflicts repeatedly, the fix is smaller and more frequent pull requests, not better conflict-resolution skills. If you find yourself resolving the same conflict on every rebase, git rerere records your resolution and replays it.

When a conflict occurs (either during git merge or when merging a PR on GitHub), Git marks the conflicting sections in the file:

<<<<<<< HEAD
def authenticate(email, password):
return check_credentials(email, password)
=======
def authenticate(user_email, user_password):
return verify_user(user_email, user_password)
>>>>>>> feature/refactor-auth

To resolve: edit the file to keep the correct version (or combine both), remove the conflict markers, then stage and commit:

Terminal window
git add src/auth/login.py
git commit -m "Resolve merge conflict in authenticate function"

The best way to handle merge conflicts is to avoid them. Keep branches short-lived, merge main into your branch regularly, and coordinate with teammates when you are both working in the same area of the codebase.

GitHub allows you to add branch protection rules, now offered as repository rulesets, to prevent direct pushes to main and enforce quality checks before merging. Rulesets are the newer mechanism and the one to prefer on a new repository, because they can be layered and their enforcement status is visible. For a Capstone project, useful protections include:

  • Require pull request reviews: at least one approval before merging.
  • Require status checks to pass: CI tests must pass before the merge button is available.
  • Restrict who can push: prevent accidental direct pushes to main.
  • Do not allow bypassing the above settings: without this, administrators skip every rule above, and on a student repository everyone is usually an administrator.

Set these up in the repository settings under Branches > Branch protection rules. These protections codify your team’s working agreement into the repository itself, so the rules are enforced automatically rather than depending on everyone remembering to follow them.

  • Commit early and often. Small, frequent commits are easier to review, easier to revert, and less likely to cause conflicts.
  • Pull from main before starting new work. Always build on the latest code.
  • Keep branches short-lived. A branch that lives for two weeks accumulates conflicts and becomes painful to merge. Aim for branches that last a few days at most.
  • Write meaningful commit messages. Your future self (and your teammates) will thank you.
  • Never commit secrets. API keys, passwords, and tokens do not belong in the repository. Use environment variables and add sensitive files to .gitignore.
  • Use .gitignore from the start. Ignoring build artifacts, IDE configuration files, and OS-specific files keeps the repository clean.

The habits above are for you. These five are for whoever takes the repository over, and they are the most common thing that makes an otherwise good project unpleasant to inherit.

  • Nothing lands on the default branch except through a pull request. Turn on branch protection so this is structural rather than a promise.
  • No secrets, no build artifacts, no dependency directories in the history. A committed .env is a security incident even after you delete the file, because the history keeps it. Fix .gitignore before the first commit, not after.
  • Commit messages say what changed and why. “fix”, “update”, “asdf”, and forty commits called “wip” are the failure mode. Squash noisy branches before merging.
  • No force-pushing the default branch, ever.
  • Large binaries belong outside git or in Git LFS.

Git is genuinely confusing, and it is worth knowing that this is not a failure of yours. The command-line interface grew over years without a consistent design, so the same word means different things in different commands and the mental model you need (a directed graph of commits, with branches as movable labels) is not the model the commands present. The fastest way through is to learn the model rather than memorizing commands, which is what the first three chapters of Pro Git are for. After that most commands become obvious and the rest are lookups.

Almost every Git disaster you can create locally is recoverable, and not knowing this causes more damage than the original mistake. git reflog records every state HEAD has been in, including states no branch points at any more, so a bad reset, a dropped branch, or a botched rebase is usually one git reset --hard HEAD@{n} away from being undone. The exception is work you never committed, which is why the advice to commit early and often is about safety rather than tidiness. Panic and improvisation are what turn a recoverable mistake into a lost afternoon: stop, read the message, and look at the reflog.

Code review is where the short-term and long-term incentives diverge most sharply on a student team. Skipping it genuinely is faster this sprint, and the cost arrives later as a codebase nobody but the author understands, which turns into a bus factor of one right when that person has three finals. Google’s engineering practices documentation is the most useful public description of what reviewers should actually look for, and its central principle is worth adopting directly: approve a change once it definitely improves the codebase, rather than holding out for the version you would have written.

The branching workflow matters far less than following one consistently, and teams spend a surprising amount of time debating this. All three workflows above work. What does not work is a team where two people open pull requests, one pushes to main, and nobody is sure which branch is current. Pick GitHub Flow, write it in the working agreement, and enforce it with a ruleset so the decision does not depend on memory.

One thing this page cannot teach you is when to break the rules. A one-character typo fix on a docs page does not need a review round, and a team that insists it does will quietly start batching unrelated changes into bigger pull requests to avoid the ceremony, which is worse. Set the protections, and treat the rare override as a decision someone announces rather than one they hide.

Git is the dominant version control system in software engineering. GitHub, GitLab, and Bitbucket host the vast majority of both open-source and proprietary projects. Familiarity with Git workflows, pull requests, and code review is a baseline expectation for software engineering roles.

In open-source communities, the pull request model (fork, branch, PR, review, merge) is the standard contribution mechanism. Understanding this workflow is essential for contributing to FOSS projects, which many Capstone teams do.

In research settings, version control is increasingly recognized as essential for reproducibility. Journals and conferences expect computational work to be backed by a public repository with clear commit history. Git enables this, and platforms like GitHub and GitLab provide the infrastructure for sharing and collaborating on research code.

Git’s own documentation is unusually good and unusually free, so the first two entries below should be your default reference rather than a search-engine result of unknown vintage. The rest are the specific conventions and review practices that teams adopt on top of Git itself.

Sources and further reading

Activities that exercise this