Git gives you the mechanics of version control, but a workflow defines how a team actually uses those mechanics. On GitHub, one common workflow is:
This structure helps teams collaborate safely without pushing unfinished changes directly to main.
A fork is your own server-side copy of someone elseâs repository on GitHub. A clone is your local copy on your machine.
If you contribute to an open source project, you often fork first, then clone your fork:
git clone https://github.com/your-user/project.git
cd projectThen add the original repository as upstream:
git remote add upstream https://github.com/original-owner/project.git
git remote -vThis lets you fetch changes from the source project while pushing to your own fork.
Tip: In many company repositories, you may not need a fork because you already have direct branch access. The rest of the workflow still looks very similar.
Start from an updated main branch:
git switch main
git fetch upstream
git pull upstream mainCreate a branch for your work:
git switch -c feature/improve-git-docsFeature branches keep your work isolated and make pull requests easier to review.
After editing files, inspect and commit your work:
git status
git add content/tutorials/git/git-basics.mdx
git commit -m "Improve Git basics examples"If the feature takes multiple steps, create multiple logical commits rather than one giant commit.
Useful commit messages make code review easier. Compare:
update stuffAdd examples for git diff and git logPush your feature branch to your fork or shared remote:
git push -u origin feature/improve-git-docsNow GitHub can display the branch and offer a button to open a pull request.
A pull request (PR) is a request to merge your branch into another branch, usually main.
A strong pull request usually includes:
Example PR summary:
git diffThis saves reviewers time and reduces back-and-forth.
Code review is not only about catching bugs. It is also about clarity, maintainability, and shared understanding.
As the author:
As the reviewer:
Note: Good reviews improve team quality and knowledge sharing. They are not personal criticism.
If main changes while your PR is open, you may need to update your branch.
One option is to rebase:
git fetch upstream
git rebase upstream/main
git push --force-with-leaseAnother option is to merge main into your branch. Team preferences vary.
GitHub often offers Squash and merge, which combines all commits from the PR into one commit when merging.
Why teams like squash merging:
main history cleanerA squash merge is especially helpful when a branch contains many small fixup commits.
Your branch may have these commits:
Add Git basics examples
Fix typo in quiz
Adjust command formattingAfter squash merge, main gets one clean commit such as:
Improve Git basics tutorial contentgit clone https://github.com/your-user/project.git
cd project
git remote add upstream https://github.com/original-owner/project.git
git switch main
git fetch upstream
git pull upstream main
git switch -c feature/add-git-stash-guide
# edit files
git add content/tutorials/git/git-stash.mdx
git commit -m "Add Git stash tutorial"
git push -u origin feature/add-git-stash-guideThen open a pull request on GitHub, respond to review feedback, and squash merge when approved.
A GitHub workflow adds process around Git so teams can collaborate safely. Fork when needed, clone locally, work on feature branches, push to a remote, and open pull requests for review. Code review improves quality, and squash merge keeps history easier to read.
If you build one habit early, let it be this: never do meaningful team work directly on main.
Scenario: You cloned your own fork of an open source project and want to stay updated with the original repository.
Question: Why did the tutorial have you add an upstream remote?
<MCQExercise
exercise={{
id: 'git-github-workflow-q1',
title: 'Exercise 1: Keeping your fork current',
description: 'Choose the reason upstream is added after cloning a fork.',
options: [
{ id: 'a', text: 'To replace origin so your fork can no longer receive pushes.' },
{ id: 'b', text: 'To fetch changes from the original repository while still pushing to your own fork.' },
{ id: 'c', text: 'To turn every pull request into a squash merge automatically.' },
{ id: 'd', text: 'To store local stashes on GitHub for backup.' },
],
correctId: 'b',
explanation: 'The workflow in the lesson used origin for your fork and upstream for the original repository. That setup lets you pull in the latest project changes without losing the ability to push your work to your own fork.',
wrongExplanations: {
a: 'origin usually remains the remote for your fork so you can push your feature branches.',
c: 'Remote naming does not determine merge strategy on GitHub.',
d: 'Stashes are local by default and are unrelated to upstream remotes.',
},
}}
/>
Question: In the workflow described, why should meaningful team work happen on a feature branch instead of directly on main?
<MCQExercise
exercise={{
id: 'git-github-workflow-q2',
title: 'Exercise 2: Protecting main with feature branches',
description: 'Pick the workflow reason feature branches are preferred for normal team changes.',
options: [
{ id: 'a', text: 'Because GitHub can only open pull requests from branches named feature/*.' },
{ id: 'b', text: 'Because feature branches automatically resolve code review comments.' },
{ id: 'c', text: 'Because branches delete commit history until the pull request is merged.' },
{ id: 'd', text: 'Because they isolate work, keep main stable, and make review easier before merging.' },
],
correctId: 'd',
explanation: 'The tutorial emphasized that feature branches keep unfinished work away from the shared stable branch. That isolation supports pull requests, review, and safer collaboration before changes reach main.',
wrongExplanations: {
a: 'GitHub pull requests are not limited to branches with that naming convention.',
b: 'Review still requires people to evaluate and respond to comments.',
c: 'Branches preserve history; they do not delete it.',
},
}}
/>
Scenario: A pull request contains three small commits: Add Git basics examples, Fix typo in quiz, and Adjust command formatting.
Question: What is the main effect of using GitHub's squash and merge option here?
<MCQExercise
exercise={{
id: 'git-github-workflow-q3',
title: 'Exercise 3: Understanding squash and merge',
description: 'Identify what squash and merge does to the pull request commits on the target branch.',
options: [
{ id: 'a', text: 'It combines the pull request work into one cleaner commit on the target branch.' },
{ id: 'b', text: 'It creates several extra merge commits to preserve each review comment.' },
{ id: 'c', text: 'It removes the need for any code review before merging.' },
{ id: 'd', text: 'It permanently disables rebasing for the repository.' },
],
correctId: 'a',
explanation: 'The lesson described squash merge as a way to turn many small branch commits into one final logical commit on main. Teams often like that because it keeps the shared history easier to read and reason about.',
wrongExplanations: {
b: 'Squash merge reduces commit noise instead of adding more merge commits.',
c: 'It is a merge strategy choice, not a replacement for review and approval.',
d: 'Using squash merge on one pull request does not disable other history management techniques.',
},
}}
/>
git fetch origin then either git rebase origin/main for a linear history or git merge origin/main to keep a merge commit. Rebase is the usual choice on your own feature branch before review, since it makes the pull request read as though written against current code. Remember it needs git push --force-with-lease afterwards, because the hashes change. Merge is the right choice if others are working on the same branch, as rebasing shared work strands them.
Squash merge collapses every commit in the branch into one on main. The tidiest history and the easiest to revert, at the cost of losing the individual commits. Merge commit keeps all commits and adds one joining them, preserving the full record and showing the branch in the graph. Rebase merge replays each commit onto main individually, giving a linear history that keeps them all but rewrites their hashes. Pick one, set it in repository settings, and stop thinking about it.
Small enough to review properly in one sitting. A few hundred changed lines is a reasonable ceiling. Review quality falls off sharply beyond that, and a large pull request tends to receive approval rather than scrutiny, which is the opposite of the point. If a change is genuinely large, split it: a mechanical refactor as one pull request, the behaviour change as another. Separating a rename or reformat from real logic changes is the single most effective thing you can do for reviewability.
It is fine and often necessary, after a rebase onto main, or after squashing review fixes, provided you use git push --force-with-lease, which refuses if the remote has commits you have not fetched. What it costs is review continuity: a force-push can detach existing review comments from the lines they referred to. A common compromise is to add plain commits during review so reviewers can follow the changes, then squash on merge.
Correctness first, then whether the change is understandable without the author present, then tests covering the behaviour rather than the implementation. Leave formatting to a linter. A review that spends its attention on spacing has spent it on the thing a tool should have handled. Be specific and direct about problems, ask rather than assert when you may have missed context, and distinguish blocking concerns from suggestions so the author knows what actually needs to change.
Learn advanced Git: cherry-pick a commit, find a regression with git bisect, recover lost work with reflog and trace changes with blame, in learning order.
Automate checks at commit and push time with Git hooks: where they live, why they never reach your team, client versus server hooks, --no-verify and Husky.
Compare GitFlow, GitHub Flow and trunk-based development honestly, see which suits which team, why long-lived branches hurt and how feature flags help.