Writing
One GitHub Account, Four Developers: A Git Bundle Workflow for a Single Integrator
When only one person can push to GitHub, the other developers can still ship. The bundle-based setup I’d use for three developers and an integrator, with every command, a tracker, a review checklist and a first-day checklist.
By Ka Lun Chan · Engineering leadership · Delivery / Engineering judgment
When only one person can push to a GitHub repository, the other developers can still contribute. They work in ordinary local Git clones, send their commits as Git bundles through a file channel the company already approves, and the integrator reviews, merges and pushes. Nobody shares a GitHub password, token or SSH key, and every commit keeps its real author.
This is the setup I’d use for three developers and one integrator while access is waiting on a contract, an account review or a security approval. The commands use placeholder paths. I ran the bundle flow end to end on Git 2.54 in a throwaway repository before writing them down.
What does this setup assume?
- The repository owner has agreed in writing that these developers may receive the source code. Get that first. A temporary access problem doesn’t change who is allowed to see the code.
- There is an approved internal channel for files: a company file share, an internal drive folder or an encrypted transfer tool your security team already allows. I’ll call it the handoff folder.
- Everyone has Git installed locally, ideally the same minor version.
- The integrator can run the project’s tests locally. GitHub Actions only runs after the push, so local tests are the real gate here.
- The arrangement lasts weeks, not quarters. If it starts to look permanent, skip to the last section.
One risk to say out loud: the integrator is now a single point of failure. If they’re sick for two days, nothing ships, which is the same trap as a best engineer who becomes the bottleneck. Ask the repository owner to authorize a backup integrator early, even if they never push.
How does the integrator prepare a clean baseline?
Start from what is actually on GitHub, then tag it so everyone can refer to the same commit by name.
cd /path/to/project
git status --short # must print nothing
git switch main
git pull --ff-only origin main
git tag -a baseline-001 -m "Baseline 001"Before sharing anything, run a secret scanner such as Gitleaks or TruffleHog over the history you are about to hand out. A bundle carries every committed object in the range, including a key that was committed last year and deleted the next day. If the scan finds something, rotate the credential and talk to the owner before going further.
git bundle create /path/to/outbox/baseline-001.bundle main baseline-001
git bundle verify /path/to/outbox/baseline-001.bundle
shasum -a 256 /path/to/outbox/baseline-001.bundle > /path/to/outbox/baseline-001.bundle.sha256A bundle contains commits and refs only. Per the git bundle manual, it leaves out the working tree, stash, per-repository config and hooks, so the integrator’s origin URL and credential helper settings stay behind. Copy both files to the handoff folder.
How do developers receive it without GitHub access?
git bundle verify needs an existing repository, so on a fresh machine check the checksum and let the clone do the rest. Calling the remote baseline instead of origin makes it obvious that it points at a file from the integrator and not at GitHub.
cd /path/to/inbox
shasum -a 256 -c baseline-001.bundle.sha256
git clone --branch main /path/to/inbox/baseline-001.bundle /path/to/project
cd /path/to/project
git remote rename origin baselineHow does each developer set their commit identity?
git config user.name "Your Name"
git config user.email "[email protected]"Use the email you will add to your GitHub account later. GitHub links commits to accounts by email, so once you do get access, this period’s commits show up under your name.
How should the team divide the work?
One task, one branch, one owner. Name branches after the tracker ID, such as task/T-101-password-reset, and always branch from the latest baseline tag:
git switch -c task/T-101-password-reset baseline-001To reduce overlapping edits, divide work by area of the codebase rather than by layer. One person owns the billing module this week, another owns search. Conflicts come from the files everyone touches: lockfiles, route tables, database migrations and shared config. Give each of those a single owner per cycle, or let the integrator make those edits. If two tasks both need a migration, the integrator assigns the migration numbers.
A shared spreadsheet or a Markdown file in the handoff folder is enough for the tracker. It needs the owner, the branch, the baseline the work started from and any dependency on another task.
| Task | Owner | Branch | Baseline | Depends on | Area | Status |
|---|---|---|---|---|---|---|
| T-101 Password reset email | Dev A | task/T-101-password-reset | baseline-001 | none | auth/ | Submitted v1 |
| T-102 Invoice PDF export | Dev B | task/T-102-invoice-pdf | baseline-001 | none | billing/ | In progress |
| T-103 Search pagination | Dev C | task/T-103-search-pagination | baseline-001 | T-101 | search/ | Blocked |
Keep the status values fixed: To do, In progress, Blocked, Submitted vN, Changes requested, Accepted in baseline-NNN and Reverted. The baseline column tells the integrator which prerequisite commit the bundle should need.
Should you exchange Git bundles or patch files?
| Question | Git bundle | Patch files |
|---|---|---|
| What arrives | The exact commits, with the same IDs | Text diffs that become new commits |
| Authorship | Author and committer preserved | Author and date preserved; the integrator becomes the committer |
| Already merged? | Git can tell by commit ID | Only by comparing patch content with git cherry |
| Binary files and merges | Handled | Binary diffs work; merge commits don’t travel |
| Readable without Git | No | Yes, in any text viewer |
| Integrity check | git bundle verify checks the format and prerequisite commits | None built in |
| When main has moved | Still imports; conflicts show up at merge time | git am can stop partway; git am -3 helps |
Use bundles by default. Identical commit IDs make duplicate detection, acceptance tracking and reverts much simpler, and verify catches a wrong baseline before anyone wastes time on it. Patch files from git format-patch are fine for a one-commit fix that someone wants to read in the channel before it is applied.
How do contributions move between developers and the integrator?
Submitting a contribution
The developer checks exactly what is leaving their machine before creating anything:
git switch task/T-101-password-reset
git status --short # clean, nothing half-done
git log --format='%h %an <%ae> %s' baseline-001..HEAD
git diff --name-only baseline-001..HEAD # no .env, IDE folders, build outputIf an unrelated file or a secret shows up in that list, a later “remove file” commit won’t help, because the bundle still carries the earlier commit. Rewrite the branch with git rebase -i baseline-001, drop or edit the commit, then check again. A .gitignore that covers .env*, editor folders and build output prevents most of this.
git bundle create /path/to/outbox/T-101-v1.bundle baseline-001..task/T-101-password-reset
git bundle verify /path/to/outbox/T-101-v1.bundle
shasum -a 256 /path/to/outbox/T-101-v1.bundle > /path/to/outbox/T-101-v1.bundle.sha256The bundle, the checksum and a handoff note (template below) go into the handoff folder.
Importing a contribution
cd /path/to/project
shasum -a 256 -c /path/to/inbox/T-101-v1.bundle.sha256
git bundle verify /path/to/inbox/T-101-v1.bundle
git bundle list-heads /path/to/inbox/T-101-v1.bundle
git fetch /path/to/inbox/T-101-v1.bundle \
task/T-101-password-reset:refs/contrib/dev-a/T-101-v1Fetching into refs/contrib/<developer>/<task>-v<N> keeps every submission apart from the integrator’s own branches and gives each one a permanent name. A revision arrives as T-101-v2, and v1 stays where it was for comparison.
Sending and picking up a new baseline
After each integration round, the integrator tags the new state and sends only what changed. A full bundle is worth keeping too, for anyone who joins later or whose clone is missing an earlier baseline.
git tag -a baseline-002 -m "Baseline 002"
git bundle create /path/to/outbox/baseline-002.bundle baseline-001..main baseline-002
git bundle create /path/to/outbox/baseline-002-full.bundle main baseline-002git remote set-url baseline /path/to/inbox/baseline-002.bundle
git fetch baseline --tags
git branch --merged baseline/main # your accepted task branches appear hereA branch that is still in progress, or came back with changes requested, gets rebased onto the new baseline and submitted as the next version:
git switch task/T-103-search-pagination
git rebase baseline-002
git bundle create /path/to/outbox/T-103-v2.bundle baseline-002..task/T-103-search-paginationRebasing changes the commit IDs, which is why the version number matters. Never rebase a branch that has already been accepted.
How does the integrator review, merge and push?
Review and testing checklist
- The checksum matches and
git bundle verifypasses. - The bundle holds exactly one branch, and it matches the handoff note.
- Every commit’s author name and email are the developer’s own.
git diff --stat main...refs/contrib/dev-a/T-101-v1touches only the task’s area.- The diff has no secrets,
.envfiles, local config, build output or large binaries. - The change does what the tracker says. Read the diff as well as the summary.
- Any dependency named in the note is already accepted.
- Tests and the linter pass on the merged result, as well as on the branch.
- New migrations use the number the integrator assigned.
The integration loop
git switch main
git pull --ff-only origin main
git merge-base --is-ancestor refs/contrib/dev-a/T-101-v1 main && echo "already merged"
git merge --no-ff refs/contrib/dev-a/T-101-v1 -m "Merge T-101 v1 from Dev A"
# run tests and linters here
git push origin main--no-ff keeps one merge commit per contribution. That gives you one commit to point at when you announce acceptance and one commit to revert if something goes wrong. Merge one contribution at a time and run the tests between merges. When several land in one round, merge anything others depend on first, then the rest smallest first.
Resolving conflicts with the author
If the merge conflicts in a file or two and the right answer is obvious, resolve it in the merge commit and mention it in the acceptance note. For anything else, run git merge --abort and send it back. Mark the task Changes requested, cut a new baseline that includes whatever caused the conflict, and let the author rebase and submit the next version. The author understands the intent of the change better than the integrator does, and resolving conflicts in someone else’s code is how subtle bugs get in.
Avoiding the same contribution twice
Because bundle commits keep their IDs, Git can answer this directly. The merge-base --is-ancestor line in the loop above stops a repeat merge, and this lists every submission already in main:
git for-each-ref --merged main --format='%(refname)' refs/contrib/For a contribution that arrived as patch files, use git cherry -v main <branch>. Lines starting with - already exist upstream under a different commit ID.
Reverting a problematic contribution
Don’t rewrite main on GitHub. Revert the merge commit instead:
git log --merges --oneline -10 # find the merge commit
git revert -m 1 <merge-commit-sha>
# run tests
git push origin main-m 1 keeps the mainline side and undoes what the contribution brought in. Git’s own guide to reverting a faulty merge describes the catch: if the author fixes the branch and you merge it again, Git treats the original commits as already merged. Either revert the revert first, or have the author send the fix as new commits on a fresh baseline. Mark the task Reverted in the tracker with the reason.
What does daily coordination look like?
Every submission comes with the same short note, so the integrator never has to ask what a bundle contains:
Task: T-101 Password reset email
Developer: Dev A <[email protected]>
Branch: task/T-101-password-reset
Version: v1
Baseline commit: baseline-001 (<short SHA>)
Bundle: T-101-v1.bundle (sha256 in T-101-v1.bundle.sha256)
Included commits: <output of git log --oneline baseline-001..HEAD>
Tests: <commands run and result>
Known issues: <anything unfinished, risky or needing a decision>
Depends on: noneFor three developers I’d start with two integration windows a day, one late morning and one mid-afternoon. Whatever is in the handoff folder by the window gets reviewed in that window. A new baseline goes out after each window, even if only one contribution landed, so nobody drifts more than half a day. If reviews keep spilling past the window, the tasks are too big, and smaller stories and pull requests will help more than extra windows.
After each window, post one message in the team channel and update the tracker to match. Developers confirm on their side with git branch --merged baseline/main after fetching the new baseline.
baseline-004 is in the handoff folder (main at <short SHA>, pushed to GitHub)
Accepted: T-101 v1 (Dev A), T-104 v2 (Dev C)
Changes requested: T-102 v1 (Dev B): conflict in billing/routes, rebase on baseline-004
Reverted: noneHow do you move beyond the temporary setup?
The goal is to retire this runbook. Once the owner approves access:
- Each developer gets their own GitHub account, added to the organization or as a collaborator with Write permission, ideally through a team so access is easy to remove later.
- Turn on branch protection for
main: pull requests required, at least one approving review and required status checks. The integrator becomes a reviewer instead of a gatekeeper. - Each developer adds the commit email they set on day one to their GitHub account, so the history from this period links to them.
- Developers point their clones at GitHub with
git remote add origin <repository URL>, push any unaccepted task branches and open pull requests from them. - Delete the bundles from the handoff folder once everything is on GitHub.
An approved internal Git server makes more sense than bundles when the restriction will last more than a few weeks, when the team grows past four or five people, when the integrator spends more time moving files than reviewing code, or when policy rules out external accounts for these developers. A self-managed GitLab or Gitea instance gives you pull requests and permissions. For a short stretch, even a bare repository on an internal SSH host removes the file shuffling. Whichever you pick, the security team has to approve where the code lives.
A first-day checklist
Before anyone gets code
- Written approval from the repository owner naming the developers who may receive the source.
- The approved handoff folder exists, and only the four of you can open it.
- A backup integrator is named, or the risk is accepted in writing.
- The history being shared has been scanned for secrets, and anything found has been rotated.
Integrator
- A clean
main, taggedbaseline-001. - The baseline bundle and checksum created, verified and copied to the handoff folder.
- The tracker started with the first tasks, owners, areas and dependencies.
- Owners assigned for lockfiles, routes, migrations and shared config.
- Integration windows and the acceptance message format announced.
Each developer
- The checksum matches, the clone came from the bundle and the remote is renamed to
baseline. user.nameanduser.emailmatch the identity you’ll use on GitHub..gitignorecovers.env*, editor folders and build output.- The first task branch is created from
baseline-001. - The project builds and its tests pass locally before any changes.
By the end of day one
- At least one small contribution has gone through the whole loop: bundle, verify, review, merge, push, new baseline and acceptance message.
- Anything that felt awkward is written down and fixed before day two.
Sources
Short answers
Can developers contribute to a GitHub repository without GitHub access?
Yes, through an integrator who has access. Developers commit locally and send their commits as Git bundles or patch files through an approved channel. The integrator reviews, merges and pushes. Commits keep the developer as author, and nobody shares credentials.
What is a Git bundle?
A single file containing Git commits and references, created with git bundle create. You can clone or fetch from it like a remote. It carries committed history only, not the working tree, stash, configuration or hooks.
Should I use Git bundles or patch files?
Use bundles by default. They keep commit IDs identical, which makes duplicate detection and reverts straightforward, and git bundle verify checks that the receiver has the right base. Patch files suit small fixes someone wants to read as text before applying.
Does pushing someone else’s commits change the author on GitHub?
No. Merging a bundle keeps each commit’s author and committer. GitHub shows the author and links the commit to their account once that email is added to it. The integrator appears only on the merge commit.
How do I undo a merged contribution?
Run git revert -m 1 with the merge commit and push. Don’t force-push main. If the same branch is merged again later, revert the revert first or have the author send the fix as new commits.
Where to start
Push one small change through the whole loop on the first day, before anyone starts real work. Most of what goes wrong with this setup shows up in that first round.