A practical reference for the most common Git workflows when contributing to open-source projects or working across forks. Each section covers a real scenario with the exact commands you need.
Scenario 1 — Fork, Branch, Stage, Commit & Push (With Ignored Files)
Use this when you want to contribute a new feature to an upstream project while keeping local tool files (AI configs, editor settings, etc.) out of your commits.
1. Fork the repository
Via GitHub CLI:
gh repo fork <OWNER/REPO> --clone=false
Or go to github.com/OWNER/REPO and click Fork. You may then directly clone your fork locally to work on and skip to 3.
2. Add your fork as a remote
git remote add myfork https://github.com/YOUR-USERNAME/REPO.git
git remote -v
3. Create a feature branch
git checkout -b <my-feature-branch>
4. Stage only your feature files
Never use git add . when you have local-only files you want to keep out. Add files explicitly instead: (Or do it from the source control tab on VScode)
git add <path/to/changed-file.cpp>
git add <path/to/new-file.h>
git status
5. Commit and push
git commit -m "<feature: describe what you added>"
git push myfork <my-feature-branch>
6. Keep local files out permanently
Add entries to .git/info/exclude — this is your personal ignore file and won’t be committed or affect other contributors:
.claude/
openspec/
CLAUDE.md
AGENTS.md
.env.local
Scenario 2 — Clean PR With One Commit Per Submodule (Squash Your Commits)
Use this when you have multiple messy work-in-progress commits and want to squash them into clean, logical commits before opening a pull request. Maintainers love this. Two methods are shown below — pick whichever fits your situation.
Quick method: soft reset and re-commit
If you just want to collapse everything into a single commit, this is the fastest way. No editor, no rebase.
1. See how many commits ahead you are
git log origin/master..HEAD --oneline
2. Soft-reset back to where upstream is
This un-commits your changes but keeps them staged:
git reset --soft origin/master
3. Commit everything as one clean commit
git commit -m "feature: describe what you added"
4. Force-push the cleaned branch to your fork
git push myfork my-feature-branch --force-with-lease
That’s it — your branch now has one commit on top of master.
Interactive rebase method
Use this when you want more control — for example, to keep multiple logical commits (one per subsystem) while squashing the noise.
1. See how many commits ahead you are
git log origin/master..HEAD --oneline
2. Start an interactive rebase
Replace N with the number of commits you want to rewrite:
git rebase -i HEAD~N
In the editor that opens, leave the first commit as pick and change the rest to squash (or s) to fold them in. For example:
pick a1b2c3 driver: initial skeleton
squash d4e5f6 driver: add read method
squash g7h8i9 driver: fix typo
squash j0k1l2 driver: add to build system
Save and close. Git will prompt you to write a combined commit message — write something clean and descriptive.
3. Force-push the cleaned branch to your fork
Since rebase rewrites history, a force push is required. --force-with-lease is safer than --force — it refuses if someone else has pushed to the branch since you last fetched.
git push myfork my-feature-branch --force-with-lease
Open the pull request
After using either method above:
gh pr create --base master --head YOUR-USERNAME:my-feature-branch --title "feature: short description" --body "Describe what this changes and why."
Or open it in the browser — GitHub will show a banner on your fork prompting you to create a PR.
Tip: One commit per logical area
If your PR touches multiple submodules or subsystems, aim for one commit per area rather than one giant commit. Use the interactive rebase method to achieve this. For example:
pick a1b2c3 AP_RangeFinder: add DTS6012M driver
pick b2c3d4 SITL: add DTS6012M rangefinder simulation
pick c3d4e5 Tools: autotest: add DTS6012M rangefinder test
Scenario 3 — Update Your Fork From Upstream (Stay in Sync With Main)
Use this regularly to keep your fork up to date with the original project so your feature branches don’t drift too far behind and create merge conflicts.
1. Add the upstream remote (first time only)
git remote add upstream https://github.com/OWNER/REPO.git
git remote -v
2. Fetch the latest changes from upstream
git fetch upstream
3. Update your local master
--ff-only ensures a fast-forward merge — it will refuse if your local master has diverged, protecting you from accidental merge commits.
git checkout master
git merge upstream/master --ff-only
4. Push the updated master to your fork
git push myfork master
5. Rebase your feature branch onto the updated master
This replays your changes on top of the latest upstream code — cleaner than merging:
git checkout my-feature-branch
git rebase master
If there are conflicts, Git will pause at each one. After resolving the affected files:
git add path/to/resolved-file.cpp
git rebase --continue
To abort and go back to where you started:
git rebase --abort
6. Push the rebased branch
git push myfork my-feature-branch --force-with-lease
Scenario 4 — Resolving Merge Conflicts
Use this when Git stops mid-merge or mid-rebase because the same lines were changed in two different places and Git can’t decide which version wins.
1. See which files have conflicts
git status
Conflicted files show as both modified. Open each one — Git marks the conflicting sections like this:
<<<<<<< HEAD
your version of the code
=======
incoming version of the code
>>>>>>> upstream/master
Edit the file to keep what’s correct — remove the conflict markers and leave only the final intended code.
2. Mark each file as resolved
git add path/to/resolved-file.cpp
3. Continue or complete the operation
If you were in a rebase:
git rebase --continue
If you were in a merge:
git merge --continue
If you want to bail out entirely and restore to the state before the conflict:
git rebase --abort
git merge --abort
Tip: Use a visual merge tool
If you prefer a visual diff, configure VS Code as your merge tool once and use it any time conflicts appear:
git config --global merge.tool vscode
git config --global mergetool.vscode.cmd "code --wait $MERGED"
git mergetool
Scenario 5 — Cherry-Picking Commits Between Branches
Use this when you want to copy one specific commit from another branch into your current branch — without merging the whole branch. Common when a bug fix lives on a feature branch and you need it on master too.
1. Find the commit hash you want
Look at the log of the source branch to identify the commit:
git log other-branch --oneline
2. Switch to the destination branch
git checkout master
3. Cherry-pick the commit
git cherry-pick a1b2c3
Git applies that commit as a new commit on your current branch. The original commit on the other branch is untouched.
4. Cherry-pick a range of commits
To apply multiple consecutive commits (oldest first, newest last):
git cherry-pick a1b2c3^..z9y8x7
5. If conflicts occur
Resolve the conflicted files, then:
git add path/to/resolved-file.cpp
git cherry-pick --continue
To abort and undo the cherry-pick entirely:
git cherry-pick --abort
6. Push when done
git push myfork master
Quick Reference
| Task | Command |
|---|---|
| Fork via CLI | gh repo fork OWNER/REPO --clone=false |
| Add upstream remote | git remote add upstream URL |
| New branch | git checkout -b branch-name |
| Stage specific files | git add path/to/file |
| Quick squash (soft reset) | git reset --soft origin/master && git commit -m "msg" |
| Squash commits (interactive) | git rebase -i HEAD~N |
| Safe force push | git push --force-with-lease |
| Fetch upstream | git fetch upstream |
| Fast-forward merge | git merge upstream/master --ff-only |
| Rebase onto master | git rebase master |
| Open PR via CLI | gh pr create --base master --head USER:branch |
| Resolve then continue | git add file && git rebase --continue |
| Cherry-pick a commit | git cherry-pick a1b2c3 |
| Cherry-pick a range | git cherry-pick A^..Z |
