Git and GitHub Essentials
Your team has one repository, four people who can push to it, and a demo coming up. Sooner or later two of you edit the same file on the same afternoon, or someone pushes a broken build straight to main the night before that demo. This page covers the way of working that prevents both: a branching workflow, the pull request cycle from branch to merge, merge conflicts, and the repository settings that enforce the workflow. It doesn’t teach Git from scratch (the two lectures linked below do), and it doesn’t cover writing a CI pipeline.
Git is a distributed version control system: every clone holds the project’s full history as a graph of commits, and a branch is a movable label that points at one of them. GitHub hosts Git repositories and adds the collaboration layer on top: pull requests, code review, issues, and the settings that control who can change what. If you’re new to either, or need a refresher, start with these two lectures and come back.
Your project might live on GitLab or Bitbucket instead. The concepts on this page transfer to both; the settings have different names and sit in different menus.
Branching Workflows
Section titled “Branching Workflows”A branching workflow is the team’s agreement on when to create a branch, how a change gets reviewed, and how it reaches main. Without one, teammates build on different ideas of which branch is current, main breaks, and nobody trusts that what they just pulled will run. Three workflows are common. They differ in how much structure they add and in how much they lean on automated tests.
GitHub Flow
Section titled “GitHub Flow”GitHub Flow, as Scott Chacon first described it, has one hard rule: anything on main is deployable. All work happens on branches that return to main through a pull request, where a teammate reviews the change and CI runs the tests before it merges. GitHub’s own documentation walks through the steps.
Use GitHub Flow on a small team shipping one product. It’s simple enough that everyone will actually follow it, and it still enforces the two practices that matter most on a small team: working on branches, and reviewing each other’s code before it merges.
Git Flow
Section titled “Git Flow”Git Flow, described by Vincent Driessen, keeps a long-lived develop branch next to main, plus dedicated branches for releases and hotfixes. It gives structure to projects with formal release cycles, at the cost of more branches to keep in sync. Driessen later added a note to the post that narrows its scope: for software delivered continuously, such as most web apps, he suggests a simpler workflow like GitHub Flow, and he says Git Flow may still fit software that is explicitly versioned or that has several versions in use at once. A library whose users pin old versions fits the second case; a web app deployed from main fits the first.
Trunk-Based Development
Section titled “Trunk-Based Development”In trunk-based development, everyone commits to main (the “trunk”), either directly or through short-lived branches, which the site limits to a couple of days. Google works this way at scale. Potvin and Levenberg (2016) describe a single repository where most engineers work at the latest version, development on branches is unusual, branches exist mainly for releases, and new features sit behind conditional flags instead of on development branches. It holds together because of what surrounds it: every change is tested before it lands, affected code is rebuilt on almost every commit, and a change that breaks the build widely is rolled back automatically.
Industry data on branch lifetime points the same way. The 2017 State of DevOps Report, an industry report by Puppet and DORA based on a survey of practitioners, restates the previous year’s findings that merging into trunk daily, keeping branches alive for less than a day, and having fewer than three active branches all contributed to higher software delivery performance, and confirms them with new data: high performers’ branches typically lived for hours where low performers’ lived for days (Forsgren et al., 2017). The report notes that some developers used to the GitHub-recommended workflow, which merges branches into trunk only periodically, remain skeptical, and replies that short-lived branches merged into trunk at least daily are consistent with commonly accepted continuous integration practice. The report’s own advice is to avoid keeping a branch alive more than a day (Forsgren et al., 2017). GitHub Flow with branches that merge within a day follows that advice and fits the site’s definition of trunk-based development; GitHub Flow with branches that stay open for days has the branch lifetimes the report found among low performers.
GitHub Flow in Practice
Section titled “GitHub Flow in Practice”The steps below take one change from an up-to-date main to a merged pull request, with the commands for each.
1. Start from an up-to-date main branch
Section titled “1. Start from an up-to-date main branch”Before starting new work, update your local main to match the remote repository, so you build on what your teammates already merged rather than on last week’s code.
git checkout maingit pull origin main2. Create a feature branch
Section titled “2. Create a feature branch”Create a branch for the piece of work you’re about to do. Name it so a teammate can tell what it holds from the branch list alone, following whatever convention your team wrote into its working agreement.
git checkout -b feature/add-login-endpointCommon naming conventions:
feature/add-login-endpointfor new functionalityfix/null-pointer-dashboardfor bug fixeschore/update-dependenciesfor maintenance tasks
If the work corresponds to a GitHub issue, put the issue number in the branch name, so anyone can trace the branch back to the discussion that motivated it:
git checkout -b feature/42-add-login-endpointEach branch holds one focused change. A branch called feature/add-login-endpoint that also refactors the database layer and updates the README makes the reviewer untangle three changes to approve one, and none of the three can be reverted without losing the other two.
3. Make commits as you work
Section titled “3. Make commits as you work”Commit in logical increments: each commit a coherent step that makes sense on its own. That’s not every save, and it’s not a whole feature in one commit either.
git add src/auth/login.py src/auth/tests/test_login.pygit commit -m "Add login endpoint with email/password validation"A commit message has a subject line that says what changed and, when the change needs it, a body that says why. Chris Beams argues for seven rules, among them a subject of about 50 characters written in the imperative (“Add”, not “Added”) and a body that explains what and why rather than how. Treat the body as the part that matters, because the diff already shows what changed, and the reason is the one thing a reader six months later can’t reconstruct from the code.
If your team wants machine-readable history, Conventional Commits adds a type(scope): description prefix, such as feat(auth): add login endpoint, that tools parse to generate changelogs and pick the next version number. It costs a little discipline on every commit and pays off only if you run those tools, so adopt it if you plan to automate releases and skip it if you don’t.
Good commit subjects:
Add login endpoint with email/password validationFix token expiration check to use UTC consistentlyAdd integration tests for login with invalid credentialsA good commit with a body:
Fix token expiration check to use UTC consistently
The check compared the token's UTC timestamp with the server's localtime, so on a server set to Pacific time, tokens stayed valid for hoursafter they had expired.Poor commit messages:
WIPfix stuffupdate files4. Push your branch to GitHub
Section titled “4. Push your branch to GitHub”Push your branch so teammates can see your work and so it’s backed up. Until you push, the only copy of that work is on one laptop, and nothing else on this page can recover it.
git push -u origin feature/add-login-endpointThe -u flag, needed only the first time you push a branch, sets up tracking between your local branch and the remote one; after that, git push on its own is enough.
5. Open a pull request
Section titled “5. Open a pull request”On GitHub, open a pull request (PR) from your branch into main. The PR is where a teammate reviews the change and CI runs against it before anything merges.
A good pull request has:
- A title that says what the change does: “Add login endpoint with email/password validation”.
- A description that says what changed, why, and how to test it. If the PR resolves an issue, write
Closes #42in the description, and GitHub closes the issue when the PR merges into the default branch. - A size a reviewer can read in one sitting. If it’s growing past that, split it into smaller PRs that merge in sequence.
The description and the size help reviewers with their hardest task: understanding the change. In interviews with Microsoft developers, Bacchelli and Bird (2013) found that understanding the change, and in particular the reason for it, was reviewers’ main challenge, and developers said that when the author gave them context and direction, they could respond better and faster. Keep your pull requests small, because a reviewer who can hold the whole change in their head can check it, and one who can’t has to skim. Google’s process has converged on small changes: Sadowski et al. (2018) report a median of 24 lines changed per change, about 90% of changes touching fewer than 10 files, and a median of under four hours for the whole review.
6. Review and discuss
Section titled “6. Review and discuss”At least one teammate, or however many your working agreement names, reviews the pull request. A reviewer checks:
- 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 obvious bugs, security issues, or performance problems?
Reviewers comment on specific lines or on the PR as a whole. The author responds, makes changes where needed, and pushes more commits to the same branch, and the PR updates automatically.
# After addressing review feedbackgit add src/auth/login.pygit commit -m "Handle empty password field per review feedback"git pushMost people expect review to catch bugs. It catches some, but that’s not most of what it produces. In the same Microsoft study, finding defects was the top motivation developers and managers gave for reviewing, yet of 570 review comments only 78 (14%) were about defects, the fourth most common of nine categories, while 165 (29%) were code improvements: better practices, removing unused code, and readability (Bacchelli and Bird, 2013). The authors found that review also produces knowledge transfer, team awareness, and alternative solutions to problems. They also recommend spreading understanding of the code across the team, because when the author of a change is the only person who knows that code, nobody else is in a position to review it.
That doesn’t make review optional for quality. McIntosh et al. (2014) studied review participation in two open-source projects, Qt and ITK, and found that among components where every change was reviewed, those integrating a larger share of changes approved without any comment from another team member tended to have more post-release defects, in all three releases they modeled. Self-approved and hastily reviewed changes showed the same effect in one Qt release each.
7. Merge into main
Section titled “7. Merge into main”Once the PR is approved and the CI checks pass, merge the branch into main. GitHub offers three merge methods:
- Merge commit: keeps every commit from the branch in
main’s history, plus a merge commit that joins them. - Squash and merge: combines all the branch’s commits into a single commit on
main. - Rebase and merge: replays the branch’s commits on top of
mainwithout a merge commit, for a linear history.
Make squash and merge your default, because it keeps main readable as one commit per pull request while letting you commit freely on the branch, “fix typo” and “WIP” included. The cost is that the branch’s individual commits, their timestamps, and their authors don’t reach main, so the squashed commit’s message, which GitHub lets you edit when you merge, has to carry whatever a later reader needs.
After merging, delete the branch. GitHub can do this automatically. Don’t keep working on a branch after it’s been squashed and merged: its old commits reappear in the next pull request from it, and you’ll resolve the same conflicts again.
8. Pull the updated main branch
Section titled “8. Pull the updated main branch”After a merge, everyone else pulls the latest main before starting new work:
git checkout maingit pull origin mainIf you have a branch in progress, bring it up to date with main, so it doesn’t drift far enough to make the eventual merge painful:
git checkout feature/your-branchgit merge mainBecause squash and merge creates a new commit on main, git branch --merged won’t list your old local branch as merged. Once GitHub shows the PR as merged, delete the local branch with git branch -D feature/your-branch.
Handling Merge Conflicts
Section titled “Handling Merge Conflicts”A merge conflict happens when two branches change the same lines of the same file, and Git can’t decide which version to keep, so it stops and asks you (Pro Git covers the mechanics in Basic Merge Conflicts). The first one feels alarming because your files now contain markers that aren’t valid code. Git’s message, “Automatic merge failed; fix conflicts and then commit the result.”, tells you what to do next but not that you can back out: git merge --abort puts you back where you were before the merge, as long as you had no uncommitted changes when you started it.
When a conflict occurs, either during git merge or when GitHub can’t merge a PR on its own, Git marks the conflicting sections in the file:
<<<<<<< HEADdef authenticate(email, password): return check_credentials(email, password)=======def authenticate(user_email, user_password): return verify_user(user_email, user_password)>>>>>>> feature/refactor-authTo resolve it, edit the file to keep the correct version (or combine both), delete the markers, then stage and commit:
git add src/auth/login.pygit commit -m "Resolve merge conflict in authenticate function"The longer two branches live apart, the more lines both of them touch, so conflicts grow with branch lifetime. If your team keeps hitting painful conflicts, fix them with smaller, more frequent pull requests and a quick word with whoever works in the same area of the code, because that shrinks the overlap itself, where better resolution skills only make each collision cheaper.
If you find yourself resolving the same conflict on every rebase, turn on git rerere (“reuse recorded resolution”) with git config --global rerere.enabled true. Git then records how you resolved a conflict and replays that resolution the next time the same one appears.
Protecting the Main Branch
Section titled “Protecting the Main Branch”Everything above depends on people remembering to follow it, and GitHub can enforce it instead. A ruleset is a named list of rules that GitHub applies to matching branches on every push and merge. Rulesets are the newer mechanism and work alongside classic branch protection rules. Prefer them on a new repository, because several rulesets can apply to one branch at once, you can switch one off without deleting it, and anyone with read access can see which rules are active.
GitHub offers rulesets and classic protection on private repositories only with some of its plans, and it changes what each plan includes, so if your repository is private, check where rulesets are available before you rely on these settings.
For a small team, these rules are worth turning on (the full list is in Available rules for rulesets):
- “Require a pull request before merging”, with at least one required approval, so nothing reaches
mainwithout review. - “Require status checks to pass before merging”, so CI has to be green before the merge button works.
- “Block force pushes”, which stops anyone rewriting
main’s history under their teammates. - An empty bypass list. A ruleset applies to everyone who isn’t on that list, and on a small team’s repository everyone usually has admin access, so any role you add there exempts the whole team.
To create one, open the repository’s Settings, choose Rulesets in the left sidebar under “Code, planning, and automation”, and click New ruleset, then New branch ruleset. Target the default branch, and set the enforcement status to Active.
If the repository already has a classic branch protection rule, check whether its restrictions apply to administrators; if they don’t, turn on “Do not allow bypassing the above settings”, or convert the rule to a ruleset.
Best Practices
Section titled “Best Practices”- Commit early and often. Small commits are easier to review and to revert.
- Pull
mainbefore starting new work. - Keep branches short-lived: a day at most.
- Write the why into commit messages and pull request descriptions.
- Never commit secrets. API keys, passwords, and tokens belong in environment variables, and the files that hold them belong in
.gitignore. - Set up
.gitignorebefore the first commit, starting from GitHub’s templates for your language and framework, so build artifacts, IDE settings, and OS files never enter the history.
A History a Maintainer Could Inherit
Section titled “A History a Maintainer Could Inherit”The habits above are for you. These five are for whoever takes the repository over after you, and each one is cheap now and expensive to fix later:
- Nothing lands on the default branch except through a pull request, enforced by the ruleset above.
- No live secrets, build artifacts, or dependency directories in the history. A committed
.envstays in the history after you delete the file; if a secret got in, you revoked or rotated it, and the commit that removed it says so. - Commit messages say what changed and why, with noisy branches squashed on merge.
- Nobody force-pushes the default branch.
- Large binaries stay out of Git, or go in Git LFS, because every clone downloads every version of every file ever committed.
If a secret does get committed, revoke or rotate it at once: GitHub’s guidance on removing sensitive data makes that the first step. A revoked or rotated secret can no longer be used for access, and GitHub notes that rewriting the history to remove it may then not be warranted. Say in the pull request that removes the secret, and in its squashed commit message, that the secret was revoked or rotated, so a maintainer who runs git log -- .env finds that record on the commit that removed it. When what leaked can’t be revoked (personal data, or a credential the team doesn’t control), set the ruleset to Disabled for one rewrite coordinated with the whole team, following GitHub’s page, set it back to Active, and have every teammate re-clone.
Some Truths About Git
Section titled “Some Truths About Git”Git is hard to learn, and if you find it confusing, the problem is partly Git’s. Perez De Rosso and Jackson (2013) observe that Git puzzles even experienced developers, and they attribute many of its difficulties to flaws in its conceptual model, not only to its commands. “Tracked” and “staged” sound interchangeable and aren’t: a file you’ve committed stays tracked but is no longer staged. Neither one means Git follows your latest edits, either: edit a file after git add and a plain git commit records the version you added, not the one on disk. The fastest way through is to learn the model before memorizing commands, which Pro Git teaches in its first and third chapters. After that, most commands follow from the model and the rest are lookups.
Almost every mistake you can make locally with committed work is recoverable, which is worth knowing before the first time you panic. git reflog records every commit HEAD has pointed at, including commits no branch points at anymore, so a bad reset, a deleted branch, or a botched rebase is usually one git reset --hard HEAD@{n} away from undone (run it on a clean working tree, since --hard discards uncommitted changes). Two limits apply. Work you never committed isn’t in the reflog, which is why committing often is about safety as much as tidiness. And reflog entries expire, by default after 90 days, or 30 for commits no branch can reach (git gc). When something goes wrong, stop, read the error message, and look at the reflog before trying fixes, because a string of improvised fixes is what turns a recoverable mistake into a lost afternoon.
Review costs the author time, and Google’s published code review guidelines answer that cost with two rules. Their central standard is to approve a change once it definitely improves the overall health of the code, even if it isn’t perfect, rather than holding out for the version the reviewer would have written. Their page on review speed argues that slow reviews create pressure to accept weaker changes and make developers protest the process, says most of those complaints go away once reviews get faster, and sets one business day as the longest a review request should wait. Adopt both as written, because together they keep review from stalling on taste or on a review request nobody has opened.
Which workflow you choose matters less than whether everyone follows it. All three workflows above work for the teams they were designed for. What doesn’t work is a mix: two people open pull requests, one pushes straight to main, and nobody is sure which branch is current. Pick GitHub Flow, write it into the working agreement, and enforce it with a ruleset.
No set of rules fits every change. A one-character typo fix in the README doesn’t need the same scrutiny as a new authentication flow, and a team that makes the typo fix as slow to merge as the feature gives everyone a reason to batch unrelated changes into bigger pull requests, which are harder to review. Keep the protections on and make the small case cheap instead: a teammate can read and approve a typo fix in a minute.
Git and GitHub in Industry and Academia
Section titled “Git and GitHub in Industry and Academia”Git is the default in industry. The 2022 Stack Overflow Developer Survey, an industry report, found that 94% of the roughly 71,000 respondents who answered its version control question used Git and 5% used SVN, and GitHub was the most used host for both personal and professional work, with GitLab, Bitbucket, and Azure Repos more common at work than for personal projects (Stack Overflow, 2022). The respondents were recruited mostly through Stack Overflow’s own channels, so highly engaged users were more likely to take part, but the gap between Git and everything else is too large for that to change the ranking.
Large companies use the same building blocks, arranged differently. Google rarely uses feature branches, working trunk-based as described above (Potvin and Levenberg, 2016), but it doesn’t skip review: review has been required there since early in the company’s history, and the median change has a single reviewer (Sadowski et al., 2018). At Microsoft, the teams in one study reviewed code through an internal tool that more than 40,000 developers had used (Bacchelli and Bird, 2013).
In open source, the pull request is how outsiders contribute: you fork the repository, push a branch to your fork, and open a pull request against the original, where the maintainers review it (Contributing to a project, GitHub). That’s the flow on this page with a fork added, so practicing it on your own team is also how you learn to contribute to a project you don’t control.
In research, Ram (2013) argues for Git as a tool for reproducible science. He notes that sharing the data and code behind a publication is becoming more common, driven partly by journal requirements and funder mandates, while access to them is still not very common, and argues that a Git history keeps a complete provenance of a project’s data. He also warns that a hosting service isn’t an archive, since none guarantee permanence, and recommends submitting the project to an academic repository that belongs to the CLOCKSS archive network, such as figshare. GitHub documents how to archive a repository in Zenodo and get a DOI you can cite.
References
Section titled “References”- Potvin, R., and Levenberg, J. (2016). Why Google Stores Billions of Lines of Code in a Single Repository. Communications of the ACM. https://doi.org/10.1145/2854146
- Forsgren, N., Brown, A., Humble, J., Kersten, N., and Kim, G. (2017). 2017 State of DevOps Report. Puppet and DORA. https://services.google.com/fh/files/misc/state-of-devops-2017.pdf
- Bacchelli, A., and Bird, C. (2013). Expectations, Outcomes, and Challenges of Modern Code Review. 35th International Conference on Software Engineering (ICSE). https://doi.org/10.1109/ICSE.2013.6606617
- Sadowski, C., Söderberg, E., Church, L., Sipko, M., and Bacchelli, A. (2018). Modern Code Review: A Case Study at Google. 40th International Conference on Software Engineering: Software Engineering in Practice (ICSE-SEIP). https://doi.org/10.1145/3183519.3183525
- McIntosh, S., Kamei, Y., Adams, B., and Hassan, A. E. (2014). The Impact of Code Review Coverage and Code Review Participation on Software Quality: A Case Study of the Qt, VTK, and ITK Projects. 11th Working Conference on Mining Software Repositories (MSR). https://doi.org/10.1145/2597073.2597076
- Perez De Rosso, S., and Jackson, D. (2013). What's Wrong with Git? A Conceptual Design Analysis. ACM International Symposium on New Ideas, New Paradigms, and Reflections on Programming and Software (Onward!). https://doi.org/10.1145/2509578.2509584
- Stack Overflow (2022). Stack Overflow Developer Survey. Stack Overflow. https://survey.stackoverflow.co/2022/
- Ram, K. (2013). Git Can Facilitate Greater Reproducibility and Increased Transparency in Science. Source Code for Biology and Medicine. https://doi.org/10.1186/1751-0473-8-7
Additional Readings
Section titled “Additional Readings”Sources and further reading
- Git from the Bottom Up, John Wiegley: Git’s model of blobs, trees, and commits, built up from the object store.
- Think Like (a) Git, Sam Livingston-Gray: branches as labels on a graph, for readers who know the commands and still feel lost.
- How to Write the Perfect Pull Request, Keavy McMinn, GitHub: writing the pull request, then tone in giving and answering feedback.
- Small CLs, Google Engineering Practices: how to split a change into pieces a reviewer can read.
- Trunk-based development, DORA: the capability as DORA’s research program describes it, with ways to adopt it.
- Comparing Git workflows, Atlassian: centralized, feature branch, Git Flow, and forking workflows side by side.
- Semantic Versioning, Tom Preston-Werner: the version numbers Conventional Commits tooling produces.
- Ten Simple Rules for Taking Advantage of Git and GitHub, Perez-Riverol et al., PLOS Computational Biology: Git and GitHub for research code.
- Gitless, Santiago Perez De Rosso and Daniel Jackson: the redesign of Git that grew out of their analysis of its concepts.
Activities that exercise this
- Analyze an Existing Codebase: reads a repository’s review rules, CI configuration, and recent merged changes to learn how a change actually gets reviewed and released.
- Software Release and Versioning: what your branching model has to support at release time.
- Public Code Repository: gets one repository to where someone who didn’t build it can run it and a reviewer can read it.