a3ff56e570
Sync course wiki / sync-wiki (push) Has been cancelled
Co-authored-by: claude <claude@jpaul.io> Co-committed-by: claude <claude@jpaul.io>
494 lines
30 KiB
Markdown
494 lines
30 KiB
Markdown
# Module 8 — Remotes and Hosting: GitHub, the Alternatives, and Owning Your Repo
|
|
|
|
> **One repo on one laptop is one spilled coffee away from gone.** A remote gets your history
|
|
> off your machine and somewhere durable — and because every clone carries the full history, a
|
|
> working team backs itself up just by working.
|
|
|
|
---
|
|
|
|
## Prerequisites
|
|
|
|
- **Module 2** — you have a Git repo (`tasks-app`) with real commits, and you understand commits as
|
|
checkpoints and the repo as durable memory. This module gets that history *off the one disk it
|
|
lives on*.
|
|
- **Module 5** — you committed your agentic tool's instructions file into the repo. A remote is what
|
|
finally makes that config *shared*: push it once and every teammate (and every agent) pulls the
|
|
same setup.
|
|
- **Module 6** — you can work on branches. Pushing is per-branch, so knowing what a branch is matters
|
|
here.
|
|
|
|
Helpful but not required: **Module 7** (worktrees). Everything below works the same whether you have
|
|
one working directory or several.
|
|
|
|
---
|
|
|
|
## Learning objectives
|
|
|
|
By the end of this module you can:
|
|
|
|
1. Explain what a remote *is* — a named pointer to another copy of the same repo — and why "it's just
|
|
another copy" is the whole reason hosting is provider-neutral.
|
|
2. Add a remote, push your history to it, and pull changes back, on any forge, with the same commands.
|
|
3. Recover from the three failure modes that bite everyone on first push: authentication, a
|
|
non-empty remote, and a branch-name mismatch.
|
|
4. Choose a host deliberately — hosted vs. self-hosted — using a current, dated comparison instead of
|
|
defaulting to GitHub by reflex.
|
|
5. State precisely where "pushing to a remote" is and isn't a backup, and how a normal team workflow
|
|
accidentally satisfies most of the 3-2-1 rule.
|
|
|
|
---
|
|
|
|
## Key concepts
|
|
|
|
### A remote is just another copy
|
|
|
|
A **remote** is a named reference to *another copy of this same repository*, usually somewhere you
|
|
can reach over the network. That's it. `origin` is not a
|
|
GitHub concept, a GitLab concept, or a Gitea concept — it's a Git concept, and the copy it points at
|
|
is a full, equal Git repo that happens to live on a server.
|
|
|
|
This is the fact the entire rest of the module rests on, so sit with it: **because a remote is just
|
|
another copy, the commands you use to talk to it are identical no matter who hosts it.** `git push`
|
|
to GitHub is byte-for-byte the same operation as `git push` to a **forge** (a Git hosting platform —
|
|
GitHub, GitLab, Gitea, Forgejo, and the like) you run yourself in a locked-down rack. The provider is
|
|
a logistics decision — uptime, price, who can see it, where the servers sit — not a Git decision. We
|
|
lean on GitHub as the worked example below *only* because it's
|
|
the one you're most likely to hit first, not because the mechanics change anywhere else.
|
|
|
|
The local-to-remote vocabulary is small:
|
|
|
|
```bash
|
|
git remote add origin <URL> # register a remote named "origin" at this URL (once per repo)
|
|
git remote -v # list remotes and their URLs
|
|
git push -u origin main # send your "main" branch up; -u links local main to origin/main
|
|
git push # after the first -u push, this is all you need
|
|
git pull # fetch the remote's changes AND merge them into your branch
|
|
git fetch # fetch the remote's changes WITHOUT merging (look before you leap)
|
|
git clone <URL> # make a brand-new local copy from a remote (history and all)
|
|
```
|
|
|
|
`origin` is just the conventional name for "the place I push to." You can have more than one remote
|
|
(a personal fork *and* the team's repo, say), and they can live on different hosts entirely — one on
|
|
a SaaS forge, one on a box in your closet. Git doesn't care.
|
|
|
|
### Getting a remote: you create the empty repo first
|
|
|
|
The one piece the commands above assume is that a remote repo *exists* to push into. On every host
|
|
the shape is the same:
|
|
|
|
1. In the host's web UI (or its CLI/API), create a **new, empty** repository. Give it a name; do
|
|
**not** let it add a README, license, or `.gitignore` — you want it empty so your local history
|
|
is the first thing in it.
|
|
2. Copy the URL it gives you. You'll see two flavours:
|
|
- **HTTPS** — `https://host/you/tasks-app.git`. Authenticates with a username + a personal access
|
|
token (not your account password — password auth over Git is gone on essentially every modern
|
|
host).
|
|
- **SSH** — `git@host:you/tasks-app.git`. Authenticates with an SSH key you've added to your
|
|
account. More setup once, less friction forever.
|
|
3. Point your local repo at it and push:
|
|
|
|
```bash
|
|
cd ~/ai-workflow-course/tasks-app
|
|
git remote add origin <URL-you-copied>
|
|
git push -u origin main
|
|
```
|
|
|
|
That `-u` (short for `--set-upstream`) is worth understanding, not just copying: it records that your
|
|
local `main` *tracks* `origin/main`. After it, `git status` will tell you things like "your branch is
|
|
ahead of origin/main by 2 commits" — the ahead/behind report you met in Module 2, now meaningful
|
|
because there's finally a remote to be ahead *of*. And `git push` / `git pull` with no arguments know
|
|
where to go.
|
|
|
|
### The three failure modes of a first push
|
|
|
|
Everyone hits at least one of these. Recognizing them by their error text saves an afternoon.
|
|
|
|
**1. Authentication fails.** You push and get `Authentication failed`, `Permission denied
|
|
(publickey)`, or a `403`. Two different causes hide behind that wall, and they have different fixes.
|
|
The common one is *no usable credential at all* — you tried an account password (dead on every modern
|
|
host) or never set up a token / SSH key. The sneakier one is a credential that *exists but lacks the
|
|
right scope*: a token authenticates fine and then the push is refused with `403` because the token was
|
|
never granted write access to repositories. They look alike but you fix them differently — create a
|
|
credential vs. *edit the existing token's scopes* (don't regenerate it). For the no-credential case:
|
|
for HTTPS, generate a personal access token in the host's settings and use it as your password when
|
|
prompted; for SSH, generate a key (`ssh-keygen`) and paste the public half into the host's SSH-keys
|
|
settings. This is host-specific UI but the *concept* is identical everywhere — the callout below walks
|
|
the shape of getting one.
|
|
|
|
> ### Getting a credential (the shape)
|
|
>
|
|
> The exact menu names and scope labels drift per host, so treat these as the *shape*, not gospel
|
|
> (**Verify-before-publish** the specific UI wording for your forge):
|
|
>
|
|
> - **Scope is the gotcha — check it first.** In the host's **Settings → developer / access tokens →
|
|
> create token**, you must grant the token write access to repositories: usually a scope literally
|
|
> named `repo`, or a "read **and write**" toggle on the repositories resource. A token created
|
|
> *without* it authenticates and then `403`s on push — it looks like an auth failure, but the fix is
|
|
> to **edit the token's scopes**, not to delete and recreate it.
|
|
> - **The token is shown once.** Hosts reveal the value a single time at creation. Copy it the moment
|
|
> it appears; if you lose it you create a new one rather than recover the old.
|
|
> - **Pasting it is invisible, and only happens once.** When Git prompts for your "password," paste
|
|
> the token — most terminals show *nothing* as you paste a secret, which is normal, not a failure.
|
|
> A **credential helper** (`git config --global credential.helper …`, e.g. `store`, `cache`, or your
|
|
> OS keychain) remembers it after the first success so you aren't pasting it on every push.
|
|
> - **SSH is the alternative.** A key you've added to the host skips passwords entirely: more setup
|
|
> once, no token to scope or cache afterward.
|
|
|
|
**2. The remote isn't empty (non-fast-forward).** You let the host create the repo *with* a README,
|
|
then push, and get `! [rejected] ... (fetch first)` or `non-fast-forward`. The remote has a commit
|
|
your local history doesn't, so Git refuses to overwrite it. The simple fix is to **recreate the remote
|
|
empty** and push again. (The alternative you'll see online — `git pull --rebase origin main`, then
|
|
push — replays your commits on top of the remote's, but `rebase` is an advanced, history-rewriting
|
|
operation this course doesn't teach as a step here, so prefer the empty-remote fix for now. And note
|
|
that plain `git pull` won't rescue you against an auto-README remote — it refuses to merge unrelated
|
|
histories.) This is the same "someone else pushed before me" situation you'll hit constantly once
|
|
you're collaborating — Module 11 — except here the "someone else" was the host's auto-generated README.
|
|
|
|
**3. Branch-name mismatch.** Your local default branch is `master` but the host expects `main` (or
|
|
vice versa). `git push -u origin main` then errors with `src refspec main does not match any`. Fix:
|
|
check what you actually have with `git branch`, and either push the branch you have
|
|
(`git push -u origin master`) or rename it first (`git branch -m main`). If you initialized with
|
|
`git init -b main` back in Module 2, you're already on `main` and this one won't bite you here — but
|
|
it's the classic wall for any repo that started life on `master`, so it's worth recognizing.
|
|
|
|
### Pull, fetch, and the everyday loop
|
|
|
|
Once the remote exists, day-to-day work adds two moves to the Module 2 loop:
|
|
|
|
- **`git pull`** before you start, to get whatever the remote gained since you last looked. It's a
|
|
`fetch` (download) plus a merge into your current branch in one step.
|
|
- **`git push`** after you've committed, to send your new checkpoints up.
|
|
|
|
When you want to *see* what the remote has before you let it touch your working files, use
|
|
**`git fetch`** instead — it downloads the remote's commits into `origin/main` but leaves your branch
|
|
untouched, so you can `git log main..origin/main` to read exactly what's incoming before merging.
|
|
That "look before you leap" habit matters more the moment other contributors — human or agent — are
|
|
pushing to the same place.
|
|
|
|
### Choosing a host: the comparison
|
|
|
|
GitHub is the titan. It is by a wide margin the largest forge, it's where most open source lives, and
|
|
it's the one AI tooling integrates with *first* — when a new coding agent or MCP server ships, GitHub
|
|
support is usually in the first release and everything else trails. That makes it the sane default for
|
|
most people, and it's why this module uses it as the worked example. But "default" is not "only," and
|
|
for a team with on-prem, air-gapped, or data-control requirements — a real and common constraint for
|
|
this audience — it may be the wrong default. The genuine choice is between **hosted** (someone runs
|
|
the forge; you just use it) and **self-hosted** (you run the forge on your own infrastructure).
|
|
|
|
> ### Hosting comparison — as of 2026-06-22
|
|
>
|
|
> Pricing and feature claims drift fast. Everything in these two tables was checked on the date above
|
|
> and must be re-verified before you rely on it — see the **Verify-before-publish** checklist at the
|
|
> end. List prices are per-user/month at the entry paid tier, billed annually, in USD; promotional
|
|
> and volume discounts are common and not shown.
|
|
|
|
**Hosted forges (someone else runs it):**
|
|
|
|
| Platform | Pricing (entry → paid) | Built-in CI/CD | AI-tooling integration | Ease of operation |
|
|
|---|---|---|---|---|
|
|
| **GitHub** | Free; Team ~$4/user; Enterprise ~$21/user | GitHub Actions, built in (Free tier includes a monthly minutes allowance for private repos; unlimited for public) | **Deepest.** Most agents, MCP servers, and AI reviewers target GitHub first | Zero ops — pure SaaS |
|
|
| **GitLab** (SaaS) | Free (capped users/namespace, small CI allowance); Premium ~$29/user; Ultimate ~$99/user | GitLab CI/CD — among the most mature, deeply integrated pipelines | Strong; first-party AI assistant plus growing agent support | Zero ops as SaaS; also self-hostable (see below) |
|
|
| **Bitbucket** (Atlassian) | Free (≤5 users); Standard ~$3.65/user; Premium ~$7.25/user | Pipelines, built in (small free monthly build-minute allowance) | Growing; tightest value is deep Jira/Atlassian tie-in | Zero ops as SaaS; Data Center edition self-hostable (enterprise pricing) |
|
|
| **Azure DevOps** | First 5 users free; Basic ~$6/user beyond; pipelines ~$40/parallel job after a free job | Azure Pipelines, built in (one free parallel job + monthly minutes) | Good within the Microsoft ecosystem; Copilot integration | Zero ops as SaaS; Azure DevOps Server self-hostable |
|
|
| **Codeberg** | Free (FOSS projects only; soft repo/storage caps) | Forgejo Actions (it runs Forgejo) | Via API/MCP; not a first-tier agent target | Zero ops; nonprofit-run, no commercial/closed-source hosting |
|
|
| **SourceHut** | Paid to host: ~$5 / $10 / $15 (all tiers buy the *same* service — "pay what's fair"); reduced ~$2 rate / financial aid if the full price is a hardship; free to *contribute* | builds.sr.ht, built in | Minimal first-class AI tooling; reachable via API | Zero ops as SaaS; fully self-hostable (it's open source) |
|
|
|
|
**Self-hostable open-source forges (you run it):**
|
|
|
|
| Forge | License / cost | Built-in CI/CD | AI-tooling integration | Ease of operation |
|
|
|---|---|---|---|---|
|
|
| **Forgejo** | Free, open source (you pay infra + ops) | Forgejo Actions — runs GitHub-Actions-compatible workflow YAML | Full REST API; community MCP servers; agents work over git + API | **Easiest.** Single Go binary, runs on a tiny VPS (~256 MB RAM). Community/nonprofit governed |
|
|
| **Gitea** | Free, open source | Gitea Actions (GitHub-Actions-compatible YAML) | Full REST API; community MCP servers | Single Go binary, same light footprint as Forgejo; company-backed |
|
|
| **GitLab CE** | Free, open source | Full GitLab CI/CD + container registry + more, in one install | Same first-party AI direction as GitLab SaaS, self-hosted | **Heaviest.** Wants ~8 GB+ RAM (Postgres/Redis/Sidekiq/Gitaly); upgrades can't skip versions |
|
|
| **Gogs** | Free, open source | None built in | API only | Lightest of all; single binary, runs on a Raspberry Pi. Slower development; no CI |
|
|
| **OneDev** | Free, open source | Built-in CI/CD configured in the **UI** (little/no YAML) + Kanban + packages | API; less common as an agent target | Single deployment; all-in-one but a smaller ecosystem |
|
|
|
|
Two things to read out of those tables rather than memorize the numbers:
|
|
|
|
- **GitLab spans both camps.** It's a hosted SaaS *and* a self-hostable Community Edition from the
|
|
same project — useful if you want SaaS now and the *option* to bring it in-house later without
|
|
changing tools.
|
|
- **"Self-hosted" trades a per-user bill for an ops bill.** The license is free; your cost is the
|
|
server, the upgrades, the backups, and the on-call. Forgejo/Gitea make that bill small (a single
|
|
binary on a cheap box). GitLab CE makes it real (a stack to feed and water). That trade is the
|
|
whole decision.
|
|
|
|
### The self-hosted-forge track (optional)
|
|
|
|
If you're in the air-gapped/on-prem audience, you can run this module's lab against a forge you stand
|
|
up yourself instead of a SaaS account. The teaching point is precisely that **nothing changes** — you
|
|
create an empty repo on your forge, copy its URL, `git remote add origin <URL>`, and `git push`. The
|
|
lab below flags exactly where the only difference is (the URL and how you authenticate to your own
|
|
box). Standing the forge up is its own exercise — Forgejo or Gitea is a single binary and the fastest
|
|
path; the *git* half is identical to the hosted track.
|
|
|
|
### Backup thesis, part one: distribution is the backup
|
|
|
|
Module 2 left you with a sharp limitation: everything lived on one disk. Drop the laptop in a lake and
|
|
the repo, history and all, is gone. A single local repo gives you *recovery* (move between
|
|
checkpoints) but not *backup* (a copy that survives the disk dying).
|
|
|
|
Pushing to a remote is what closes that gap, and Git's design makes the win bigger than it looks.
|
|
Recall the standard **3-2-1 backup rule**: keep **3** copies of your data, on **2** different media,
|
|
with **1** offsite. Now look at what a normal team doing normal work ends up with, without anyone
|
|
"doing backups":
|
|
|
|
- Your laptop has a full copy — **complete history**, not just current files.
|
|
- The remote has a full copy — **offsite**, on someone else's hardware (or your other box).
|
|
- Every teammate who has cloned the repo has *another* full copy, each with the entire history,
|
|
because **clone copies everything**, not a snapshot.
|
|
|
|
A four-person team that pushes to one remote is sitting on five-plus complete, independent copies of
|
|
the entire project history across multiple locations and machines. They didn't run a backup tool.
|
|
They just worked. That's the quiet superpower of a *distributed* version control system: distribution
|
|
*is* the redundancy. The 3-2-1 rule, which most ops shops fight to satisfy deliberately, falls out of
|
|
a forge and a working team almost for free.
|
|
|
|
Be precise about the division of labor, because the course is honest about where analogies stop:
|
|
|
|
- **Recovery power comes from commits (Module 2, and Module 12 for the harder cases).** That's your
|
|
point-in-time restore — go back to any checkpoint.
|
|
- **Backup power comes from remotes and distribution (this module).** That's your offsite,
|
|
redundant, survives-the-disk copy.
|
|
|
|
You need both. Commits without a remote survive a mistake but not a dead drive. A remote without good
|
|
commits survives a dead drive but gives you a junk drawer to restore from. Module 12 picks up the
|
|
*recovery* half in full and is just as honest about what Git is **not** a backup for — your database,
|
|
your secrets, your uncommitted work, your large binaries. We'll hold that thought there.
|
|
|
|
---
|
|
|
|
## The AI angle
|
|
|
|
A remote isn't only about durability — it's the substrate the AI parts of this course run on.
|
|
|
|
- **Most AI tooling integrates with the forge first, not your laptop.** AI reviewers, issue-to-PR
|
|
agents, and the CI that catches code which merely *looks* right (Modules 10, 14, and Unit 5) all
|
|
operate on the *remote* repo through its API and web UI. Until your history is pushed, none of that
|
|
machinery has anything to act on. A remote is the precondition for every agent-in-the-loop module
|
|
that follows.
|
|
- **GitHub's "integrates first" status is a real, current bias — name it, then decide.** Because the
|
|
largest forge is where AI tooling lands first, picking a less-common host or self-hosting can mean
|
|
thinner first-class agent support and more wiring-it-yourself over the API. That's a legitimate cost
|
|
to weigh against control and data-residency — *not* a reason to abandon the choice. The git
|
|
mechanics are identical everywhere; it's the AI ecosystem maturity that varies, and that gap is the
|
|
thing to check (it narrows constantly).
|
|
- **The committed AI config from Module 5 only pays off once it's pushed.** Locally, your agent's
|
|
instructions file just configures *your* agent. Pushed to the remote, it configures *everyone's* —
|
|
every teammate who clones, and every automated agent that later operates on the repo, inherits the
|
|
same conventions instead of each drifting into a private setup. The remote is what turns "my AI
|
|
config" into "the project's AI config."
|
|
- **A remote is an agent's recovery insurance.** When you hand an agent a branch and let it run
|
|
(Module 6, and Unit 5 at full autonomy), a pushed branch means its work survives a crashed session,
|
|
a wiped worktree, or a machine that dies mid-run. Push early; an agent's output that only exists in
|
|
one uncommitted, unpushed working directory is the most fragile state in this whole course.
|
|
|
|
---
|
|
|
|
## Hands-on lab
|
|
|
|
**Lab language:** shell (Git commands), plus one short provided shell script. Runs on macOS, Linux,
|
|
WSL, or Git Bash on Windows. Continues the `tasks-app` repo from Module 2.
|
|
|
|
**You'll need:**
|
|
|
|
- Your `tasks-app` Git repo from Module 2 (with several commits and a `.gitignore`).
|
|
- An account on a Git host. **Hosted track:** GitHub is the worked default, but GitLab, Bitbucket,
|
|
Codeberg, or any forge works with the identical commands. **Self-hosted track:** a Forgejo/Gitea
|
|
(or other) instance you can reach, and an account on it.
|
|
- The ability to authenticate to that host — a personal access token (for HTTPS) or an SSH key added
|
|
to your account. Set this up first; failure mode #1 above is the most common first-push wall.
|
|
- Your AI assistant (still the way you've used it — this lab is about the remote, not the editor).
|
|
|
|
### Part A — Create the empty remote and push
|
|
|
|
1. On your host's web UI, create a **new, empty** repository named `tasks-app`. Do **not** add a
|
|
README, license, or `.gitignore` — leave it empty so your local history goes in clean. Copy the URL
|
|
it shows you (HTTPS or SSH).
|
|
|
|
> **Self-hosted track:** identical step, on your own forge's UI. The only thing that differs from
|
|
> the hosted track is the URL (your forge's hostname) and how you authenticate to your box.
|
|
> Everything from here on is the same commands.
|
|
|
|
2. Point your repo at the remote and push:
|
|
|
|
```bash
|
|
cd ~/ai-workflow-course/tasks-app
|
|
git remote -v # probably empty — no remote yet
|
|
git remote add origin <URL> # paste the URL you copied
|
|
git remote -v # now origin shows, for fetch and push
|
|
git push -u origin main # send main up and link it
|
|
```
|
|
|
|
If `push` errors, match it to the three failure modes above: `Authentication failed` / `Permission
|
|
denied` → token or SSH key (#1); `non-fast-forward` / `fetch first` → the remote wasn't empty (#2);
|
|
`src refspec main does not match` → branch-name mismatch, check `git branch` (#3). Fix and re-push.
|
|
|
|
3. Confirm the offsite copy exists: refresh the host's web page for the repo. Your files and your full
|
|
commit history from Module 2 are now sitting on hardware that is not your laptop. **That is the
|
|
backup half the course promised.**
|
|
|
|
### Part B — Prove distribution is redundancy
|
|
|
|
You're going to demonstrate the 3-2-1 claim with your own eyes: that a clone is a *complete,
|
|
independent* copy, history and all — not a snapshot.
|
|
|
|
4. Make a change locally, commit it, and push it (with the AI if you like — e.g. ask for a `version`
|
|
command that prints the app version):
|
|
|
|
```bash
|
|
# apply the change, then:
|
|
git add .
|
|
git commit -m "Add version command"
|
|
git push # no args needed now, thanks to -u earlier
|
|
```
|
|
|
|
5. Now clone the remote into a *separate* directory, as if you were a teammate on a fresh machine:
|
|
|
|
```bash
|
|
cd ~/ai-workflow-course
|
|
git clone <URL> tasks-app-teammate
|
|
cd tasks-app-teammate
|
|
git log --oneline # the ENTIRE history is here — every commit, not just the latest
|
|
```
|
|
|
|
Compare the commit count to your original repo (`git log --oneline | wc -l` in each). They match.
|
|
The clone didn't get "the current files" — it got the whole project's memory. That's the property
|
|
that makes a working team into an accidental backup system.
|
|
|
|
6. Run the provided check from this module's `lab/` to make the point mechanically:
|
|
|
|
```bash
|
|
# from your original repo:
|
|
bash ~/ai-workflow-course/tasks-app/verify-backup.sh # (copied from lab/verify-backup.sh)
|
|
```
|
|
|
|
The script confirms (a) you have a remote configured, (b) your local branch is fully pushed
|
|
(nothing stranded only on your disk), and (c) a fresh clone of the remote carries the exact same
|
|
commit count as your local repo — i.e. the offsite copy is complete, not partial. Read its output;
|
|
the green line is your evidence that the backup is real.
|
|
|
|
> On the **HTTPS + token** path with a *private* repo, the clone check (c) needs your credential
|
|
> helper to have cached the token from your earlier push — otherwise it can't authenticate to clone.
|
|
> The script won't hang waiting for a prompt (it disables interactive credential prompts); it just
|
|
> reports a `NOTE` that it couldn't clone, and the push checks above still stand. SSH and public
|
|
> repos clone with no credential at all.
|
|
|
|
### Part C — The everyday loop
|
|
|
|
7. Edit the README in your *teammate* clone, commit, and push from there:
|
|
|
|
```bash
|
|
cd ~/ai-workflow-course/tasks-app-teammate
|
|
# edit README.md, then:
|
|
git add . && git commit -m "Note the remote in the README"
|
|
git push
|
|
```
|
|
|
|
8. Back in your *original* repo, pull it down:
|
|
|
|
```bash
|
|
cd ~/ai-workflow-course/tasks-app
|
|
git fetch # download the new commit, but don't merge yet
|
|
git log main..origin/main # SEE exactly what's incoming before you take it
|
|
git pull # now merge it into your local main
|
|
git log --oneline # the teammate's commit is now here too
|
|
```
|
|
|
|
That fetch-then-look-then-pull rhythm is the habit to keep: you saw what was coming before you let
|
|
it touch your files. You've now pushed *and* pulled across two independent copies through one
|
|
remote — the complete remotes mechanic.
|
|
|
|
### Part D (optional) — A second remote
|
|
|
|
9. Add a *second* remote (a personal fork on another host, or even a bare repo on a USB drive or a
|
|
box on your LAN) and push to it too:
|
|
|
|
```bash
|
|
git remote add backup <SECOND-URL>
|
|
git push backup main
|
|
git remote -v # two remotes now: origin and backup
|
|
```
|
|
|
|
You now literally have the 3-2-1 rule satisfied by hand: your laptop, `origin`, and `backup` — three
|
|
copies, more than one location. Nothing about Git stopped you from pointing at as many copies as you
|
|
want.
|
|
|
|
---
|
|
|
|
## Where it breaks
|
|
|
|
The honest limits — the backup analogy especially needs them.
|
|
|
|
- **A remote backs up what you *pushed*, nothing else.** Uncommitted edits, untracked files, and
|
|
anything `.gitignore` excludes (like `tasks.json` runtime state) never leave your laptop. "I pushed"
|
|
is not "everything is safe" — it's "every *committed and pushed* change is safe." The defense is the
|
|
Module 2 habit: commit often, and now, push often too.
|
|
- **Git is not a backup for non-Git things.** Your database, your secrets (which shouldn't be in the
|
|
repo anyway — Module 17), large binaries, and build artifacts are not covered by pushing code. The
|
|
3-2-1-by-accident win applies to your *versioned source*, full stop. Module 12 is blunt about this.
|
|
- **One remote is one vendor.** Distribution across a team is great redundancy against *disk* failure;
|
|
it's weaker against *account* failure. If your whole team only ever pushes to one host and that
|
|
account is suspended, locked, or the provider has an outage, your offsite copy is temporarily out of
|
|
reach (your local clones are fine). Part D's second remote, or a periodic clone to storage you
|
|
control, is the answer for anyone who needs it — and it's the on-ramp to the self-hosting argument.
|
|
- **"GitHub integrates first" is true today and a moving target.** Don't treat the AI-ecosystem gap
|
|
between hosts as permanent; it's exactly the kind of claim that ages. Re-check it for your tooling
|
|
before you let it decide your host.
|
|
- **The comparison tables are a snapshot, not a fact of nature.** Every price and tier above was true
|
|
on 2026-06-22 and will drift. Use them to learn the *dimensions* that matter (per-user cost vs. ops
|
|
cost, built-in CI or not, footprint, AI-ecosystem maturity), then check current numbers yourself.
|
|
|
|
---
|
|
|
|
## Check for understanding
|
|
|
|
**You're done when:**
|
|
|
|
- Your `tasks-app` exists on a remote, and `git remote -v` plus the host's web UI both confirm it.
|
|
- You have pushed at least one commit and pulled at least one commit back, across two copies of the
|
|
repo through one remote.
|
|
- `verify-backup.sh` reports a clean, fully-pushed state and a clone whose commit count matches your
|
|
local repo's — you've *seen* that the offsite copy is complete.
|
|
- You can explain, in your own words, why a four-person team pushing to one remote roughly satisfies
|
|
3-2-1 without running a backup tool — and name two things that win does *not* cover.
|
|
- You can state why the choice of host is a logistics decision, not a Git one, and name at least one
|
|
hosted alternative to GitHub and one self-hostable forge.
|
|
|
|
When pushing feels like the natural end of "commit" and you trust that your history is no longer
|
|
trapped on one disk, you have the *backup* half of the backup-and-recovery thread. Module 9 starts
|
|
using the remote for more than storage — issues, the task layer where humans and agents pick up
|
|
work — and Module 12 returns to finish the *recovery* half.
|
|
|
|
---
|
|
|
|
## Verify-before-publish
|
|
|
|
This module makes dated pricing and feature claims that drift. Re-check each before relying on the
|
|
tables, and update the "as of" date when you do.
|
|
|
|
- [ ] **GitHub** tiers and prices — Free / Team / Enterprise per-user/month, and the Free-tier CI
|
|
minutes allowance for private repos.
|
|
- [ ] **GitLab** tiers — Free (user/namespace caps, CI allowance), Premium, Ultimate per-user/month,
|
|
and the SaaS-vs-self-managed price split.
|
|
- [ ] **Bitbucket** tiers — Free user cap, Standard (~$3.65), Premium (~$7.25) per-user/month, and
|
|
free build-minute allowance. (Reconciled against Atlassian's own pricing page on 2026-06-22;
|
|
stale third-party listings still quote ~$2/$5 — trust Atlassian's page, and re-confirm.)
|
|
- [ ] **Azure DevOps** — free-user count, Basic per-user/month, and the per-parallel-job pipeline
|
|
price plus free job/minutes.
|
|
- [ ] **Codeberg** — that it remains FOSS-only and free, and its current soft repo/storage caps.
|
|
- [ ] **SourceHut** — paid-to-host tiers ($5/$10/$15): the 2026 prices are now *in effect* for new
|
|
accounts (confirmed 2026-06-22), so they're no longer "proposed." Note all tiers buy the same
|
|
service ("pay what's fair"), with a reduced rate (~the earlier minimum) and financial aid for
|
|
hardship — re-confirm before relying on it.
|
|
- [ ] **Self-hosted forges** — that Forgejo/Gitea still ship GitHub-Actions-compatible CI, GitLab CE's
|
|
current minimum resource footprint, and whether OneDev/Gogs CI status has changed.
|
|
- [ ] **"GitHub integrates first" / AI-ecosystem maturity** — re-assess which forges are first-tier
|
|
agent and MCP targets; this gap narrows fast.
|
|
- [ ] **Self-host/hosted spans** — confirm GitLab still offers CE self-host, and Bitbucket/Azure DevOps
|
|
still offer their self-hostable editions, before describing either as spanning both camps.
|
|
- [ ] **Credential/token UI** — the "Getting a credential" callout names menu paths and the
|
|
write-scope label (`repo` / "read and write") generically; confirm the current wording and
|
|
scope name on the default-example host before publishing.
|
|
- [ ] Update the comparison's **"as of" date** to the build date.
|