| 1 | --- |
| 2 | title: Mirroring |
| 3 | description: Keep a repository in step with copies of it elsewhere. A mirror stands by as a read-only copy until you take over; hand it back when you're done, or move it to g1t for good. |
| 4 | --- |
| 5 | |
| 6 | Mirroring keeps a repository on g1t in step with a copy of it on another |
| 7 | host: GitHub, another g1t (such as one you run yourself), or any git host |
| 8 | over HTTPS. |
| 9 | |
| 10 | Every linked repository has exactly one **leader**, where work happens. The |
| 11 | other copies follow it. That one rule answers every question about which |
| 12 | push wins: a write reaches the leader, or it doesn't land. |
| 13 | |
| 14 | | The repository on g1t is | Who leads | What works on g1t | |
| 15 | | --- | --- | --- | |
| 16 | | A **mirror**, standing by | The remote | Reading, cloning, search, what security scanning found, insights and settings. Nothing else: it's an exact, read-only copy that runs nothing and opens nothing. | |
| 17 | | A mirror in **CI failover** | The remote, for code | The same, and the remote's workflows run on g1t. Code, issues and pull requests stay on the remote. | |
| 18 | | A mirror **taken over** | g1t, for now | Everything: pushes, pull requests, issues, agents, workflows. | |
| 19 | | **Mirrored to** other hosts | g1t | Everything. Each push is sent to the followers. | |
| 20 | |
| 21 | ## Make a mirror |
| 22 | |
| 23 | - **From GitHub:** choose **New project → Import from GitHub**, pick the |
| 24 | repositories and choose **Standby mirror**. See [GitHub](/guides/github/). |
| 25 | - **From another g1t or any git host:** create an empty repository, then |
| 26 | in its **Settings → Mirroring** choose **Add a remote**, with **The remote |
| 27 | leads**. Give its https address and a token that can read it. The |
| 28 | repository is filled from the remote, and only an empty repository can |
| 29 | become a mirror. |
| 30 | |
| 31 | A mirror follows every branch and tag of its remote: |
| 32 | |
| 33 | - **GitHub** tells g1t about each push, so the mirror catches up within |
| 34 | seconds. |
| 35 | - **Other hosts** are asked every five minutes. |
| 36 | |
| 37 | Branches deleted on the remote are deleted on g1t. When the remote |
| 38 | force-pushes a branch or deletes it, the commit g1t had is kept under |
| 39 | `refs/g1t/replaced/<branch>/<time>` for at least 30 days. Nothing a mirror |
| 40 | held is lost silently. |
| 41 | |
| 42 | ## What a standby mirror offers |
| 43 | |
| 44 | A standby mirror is a backup you can switch to: an exact copy of the |
| 45 | remote, kept up to date, that you can read and clone. It doesn't take work |
| 46 | of its own, and nothing on g1t opens anything on it by itself. One rule |
| 47 | decides, by state: |
| 48 | |
| 49 | | | Standing by | CI failover | Taken over | Handing back | |
| 50 | | --- | --- | --- | --- | --- | |
| 51 | | **Code**: files, commits, branches, tags, compare, releases; clone, fetch, search | ✓ | ✓ | ✓ | ✓ | |
| 52 | | **Security**: what scanning found, read-only | ✓ | ✓ | ✓ | ✓ | |
| 53 | | **Insights**, **Settings** (mirroring settings included), rules, webhooks | ✓ | ✓ | ✓ | ✓ | |
| 54 | | **Workflows**: `.g1t/workflows` | Only with **Keep CI warm** | ✓ | ✓ | – | |
| 55 | | **Workflows**: `.github/workflows` | – | ✓ | ✓ unless turned off | – | |
| 56 | | Push, merge, edit on the web, make a release | – | – | ✓ | – | |
| 57 | | **Issues**, **Pull requests**, **Agents** | – | – | ✓ | – | |
| 58 | | **Deployments** on g1t.page | – | – | ✓ | – | |
| 59 | | Security updates, dependency updates, upkeep: anything that opens a branch or a pull request on its own | – | – | ✓ | – | |
| 60 | |
| 61 | What it looks like: |
| 62 | |
| 63 | - The repository shows **Mirror of github.com/acme/web** beside its name, and |
| 64 | so does its row on the workspace's **Projects** page, in the list and the |
| 65 | grid, where the **Kind** filter can show **Mirrors** or **On g1t** alone. |
| 66 | A mirror in CI failover, taken over or handing back says that instead. |
| 67 | - Its sidebar lists only what it offers: **Code**, **Security**, **Insights** |
| 68 | and **Settings**, plus **Workflows** when they run here. Opening an address |
| 69 | it doesn't offer, such as `/acme/web/issues` or `/acme/web/pulls/new`, shows |
| 70 | a note that says it's a mirror, links the remote, and leads to **Settings → |
| 71 | Mirroring**, instead of the list or the form. |
| 72 | - Its overview shows the copy: the remote, the default branch, the latest |
| 73 | commit, when it last synced, the latest commits and the About card. There |
| 74 | is no **Where does it run**, no **Get started** checklist and no |
| 75 | **Right now**: nothing runs here. |
| 76 | - **Security → Vulnerabilities** still lists what the daily scan found, but |
| 77 | no security update opens: each alert says the update waits until the |
| 78 | repository is taken over or moved to g1t. The same goes for |
| 79 | [dependency updates](/guides/dependency-updates/) and anything else g1t |
| 80 | does on its own. |
| 81 | - Every button that would write is greyed out, with the reason on hover, and |
| 82 | the services refuse the write as well: a form posted to a mirror answers |
| 83 | `409` with the reason, and the API answers `403`. |
| 84 | |
| 85 | A `git push` to a standby mirror answers: |
| 86 | |
| 87 | ```text |
| 88 | remote: acme/web is a mirror of github.com/acme/web, so it is read-only here. |
| 89 | remote: Push to github.com/acme/web, or take over in Settings → Mirroring to work on g1t. |
| 90 | ! [remote rejected] main -> main |
| 91 | ``` |
| 92 | |
| 93 | **Taking over** changes all of it at once: the sidebar fills in, the |
| 94 | overview shows where the project runs and what is moving, security and |
| 95 | dependency updates open on the next scan, and pushes, pull requests, issues, |
| 96 | agents, workflows and deployments work until you hand back. **Moving to g1t** |
| 97 | does the same for good. |
| 98 | |
| 99 | ## When the remote stops answering |
| 100 | |
| 101 | g1t checks each remote's host every minute. A host is **unreachable** |
| 102 | after three failed checks over at least two minutes. A refused token or a |
| 103 | deleted repository isn't the host being down; those show as an error on |
| 104 | the link instead. The host is reachable again after three good checks over |
| 105 | at least five minutes. |
| 106 | |
| 107 | By default an unreachable remote is only **shown**: the repository's dot |
| 108 | turns amber and a banner offers **Take over**. Nobody is paged and nothing |
| 109 | happens on its own. If it's midnight and nobody needs to work, GitHub comes |
| 110 | back and the mirror catches up. Nothing needs reconciling, because nobody |
| 111 | wrote to g1t. |
| 112 | |
| 113 | You choose how much more happens, per link, in **Settings → Mirroring**: |
| 114 | |
| 115 | | Setting | Default | Options | |
| 116 | | --- | --- | --- | |
| 117 | | When the remote stops answering | Show it on the repository | Also tell the workspace's owners in their [notifications](/guides/notifications/) | |
| 118 | | Take over automatically | Off | After 5 to 1440 minutes unreachable | |
| 119 | | Hand back on its own | When every branch goes back cleanly | Only when someone hands it back | |
| 120 | | Keep CI warm | Off | Run `.g1t/workflows` on each push copied in | |
| 121 | | Run the remote's `.github` workflows | On | Off | |
| 122 | | Hold workflows that deploy for approval | On | Off | |
| 123 | | Report checks to GitHub | On | Off. Only for a mirror of a GitHub repository; see [Checks on GitHub](#checks-on-github). | |
| 124 | |
| 125 | **Hand back on its own** only applies to takeovers g1t started itself. A |
| 126 | takeover a person started waits for a person to hand it back. |
| 127 | |
| 128 | ## CI failover |
| 129 | |
| 130 | Sometimes the remote's git works but its workflows don't run. **Start CI |
| 131 | failover** keeps the remote in charge of the code and runs its workflows on |
| 132 | g1t for each push copied in: |
| 133 | |
| 134 | - `.github/workflows` as well as `.g1t/workflows`. When both folders have |
| 135 | a workflow of the same `name:`, g1t's own runs. |
| 136 | - Workflows that deploy wait for approval, so nothing deploys from two |
| 137 | places. A workflow deploys when any job names an `environment:`. |
| 138 | - Secrets come from the [project on g1t](/guides/secrets-and-variables/), |
| 139 | never the remote's. |
| 140 | - A push copied in that changes `.g1t/`, `.github/workflows/` or |
| 141 | `.github/actions/` starts runs that wait for approval before they can |
| 142 | use the repository's secrets and variables. |
| 143 | |
| 144 | **End CI failover** when the remote runs its workflows again. Runs already |
| 145 | started finish. |
| 146 | |
| 147 | ## Checks on GitHub |
| 148 | |
| 149 | While g1t runs a GitHub mirror's workflows, in CI failover, during a |
| 150 | takeover, or with **Keep CI warm**, it reports their results to the commit |
| 151 | on GitHub. You can keep GitHub as the place you work and still see, on |
| 152 | each commit and pull request there, what g1t ran. |
| 153 | |
| 154 | What g1t reports is each status it sets on a commit: a workflow run, and a |
| 155 | deploy. Each becomes a commit status on GitHub: |
| 156 | |
| 157 | | On GitHub | What it is | |
| 158 | | --- | --- | |
| 159 | | Context | The status's name on g1t under `g1t / `: a workflow run's `CI / push` shows as `g1t / CI / push`. A name that already starts with `g1t / `, such as a deploy's `g1t / deploy`, stays as it is. | |
| 160 | | State | `pending` while it runs, then `success`, `failure` or `error`, as on g1t. A cancelled run is `error`. | |
| 161 | | Description | The status's description on g1t, cut to GitHub's 140 characters. | |
| 162 | | Details | A link to the run, or the deploy, on g1t. | |
| 163 | |
| 164 | A status is sent once each time it changes. Only commits that came from |
| 165 | GitHub are reported: a commit made on g1t during a takeover isn't on |
| 166 | GitHub until it's handed back, so it is skipped. A mirror standing by |
| 167 | without **Keep CI warm** runs nothing, so it reports nothing. |
| 168 | |
| 169 | g1t reports through its GitHub App, which needs the **Commit statuses** |
| 170 | permission, read and write. When the app's installation doesn't have it, |
| 171 | GitHub refuses, and the mirror's **Settings → Mirroring** page says |
| 172 | **g1t can't report checks to GitHub: the GitHub App needs the Commit |
| 173 | statuses permission**. g1t doesn't retry; it tries again with the next |
| 174 | status, and the line goes once a status gets through. To grant it, an |
| 175 | owner of the GitHub account or organization accepts the app's new |
| 176 | permissions on GitHub, under **Settings → Applications → Installed GitHub |
| 177 | Apps → g1t-sh**. |
| 178 | |
| 179 | To turn it off, clear **Report checks to GitHub** in the mirror's |
| 180 | **Settings → Mirroring** and save. Through the API, send |
| 181 | `report_checks: false` to |
| 182 | `PATCH /repos/{owner}/{name}/mirror/remotes/{id}`. |
| 183 | |
| 184 | ## Take over |
| 185 | |
| 186 | **Take over** makes g1t lead the repository for now, whether or not the |
| 187 | remote is answering. Everything works on g1t from then on. Each branch's |
| 188 | commit at that moment is recorded as where the takeover began. |
| 189 | |
| 190 | You can take over any time: during an outage g1t detected, a partial one |
| 191 | it didn't, or simply because you want to work on g1t for a while. |
| 192 | |
| 193 | If the remote is still answering, people may keep pushing there. Their |
| 194 | work isn't lost. Each branch that changed on both sides waits for a |
| 195 | decision when you hand back. |
| 196 | |
| 197 | ## Hand back |
| 198 | |
| 199 | **Review hand-back** shows what handing back would do, branch by branch, |
| 200 | by comparing each side with where the takeover began: |
| 201 | |
| 202 | | Plan | When | What happens | |
| 203 | | --- | --- | --- | |
| 204 | | **Push** | Only g1t changed it | g1t's commits are pushed to the remote. | |
| 205 | | **Take** | Only the remote changed it | The remote's commits are copied in. | |
| 206 | | **Pull request** | Only g1t changed it, and the remote protects the branch | g1t's commits go to `g1t/handback/<branch>` on the remote, with a pull request into the branch. g1t then follows the remote's branch. | |
| 207 | | **Diverged** | Both changed it | You decide: keep g1t's (pushed over the remote's), keep the remote's (g1t's is kept under `refs/g1t/replaced/`), or send g1t's as a pull request. | |
| 208 | |
| 209 | **Hand back** carries out the plan. The repository is read-only while it |
| 210 | goes back, so nothing moves under it. If the remote refuses anything, g1t |
| 211 | keeps the lead and says why; nobody is left stuck. When every branch has |
| 212 | gone back, the mirror stands by again and lists any pull requests it |
| 213 | opened. |
| 214 | |
| 215 | Hand back is refused while the remote isn't answering, and while a |
| 216 | diverged branch has no decision. |
| 217 | |
| 218 | ## Move to g1t |
| 219 | |
| 220 | **Move to g1t** makes the move permanent. The repository stops being a |
| 221 | mirror, and **g1t no longer tracks the remote**: pushes made there don't |
| 222 | come here any more. You can move from standby, CI failover or during a |
| 223 | takeover, and the takeover's work stays as it is. |
| 224 | |
| 225 | Tick **Keep the remote updated from g1t** to turn the remote into a |
| 226 | follower instead of dropping it. g1t then pushes every change to it, as |
| 227 | below. |
| 228 | |
| 229 | ## Mirror to other hosts |
| 230 | |
| 231 | A repository g1t leads can be **mirrored to** any number of followers. |
| 232 | Use another g1t to keep a copy on your own servers, or keep GitHub up to |
| 233 | date for people still working there. |
| 234 | |
| 235 | In **Settings → Mirroring**, **Add a remote** with **g1t leads**, its https |
| 236 | address, a username if the host needs one, and a token that can push. The |
| 237 | token is stored encrypted and never shown again. Repositories imported |
| 238 | from GitHub with **Move to g1t** follow g1t this way already. |
| 239 | |
| 240 | Each push to g1t is sent to every follower. When someone pushes to a |
| 241 | follower directly: |
| 242 | |
| 243 | - **Take in fast-forwards** (the default): a push that builds on g1t's |
| 244 | branch is copied in. Anything else marks the follower **stuck**. g1t |
| 245 | stops pushing to it until someone chooses **Sync now**, which pushes |
| 246 | g1t's branches over it. |
| 247 | - **Overwrite with g1t's**: g1t pushes its branches over the change at once. |
| 248 | |
| 249 | A follower that refuses g1t's push, a protected branch say, is also marked |
| 250 | stuck, with its answer on the link. |
| 251 | |
| 252 | ## Self-hosted g1t and g1t.sh |
| 253 | |
| 254 | The `g1t` remote works both ways between instances: |
| 255 | |
| 256 | - Run g1t on your own servers as the leader and mirror to |
| 257 | [g1t.sh](https://g1t.sh) for a hosted copy, or the other way round. |
| 258 | - Make a standby mirror on your own g1t of a repository on g1t.sh. Take |
| 259 | over if g1t.sh is unreachable from your network. |
| 260 | |
| 261 | Use a [token](/guides/authentication/) with `code:read` for a leader, or |
| 262 | `code:write` for a follower, on the other instance. |
| 263 | |
| 264 | ## API, MCP and events |
| 265 | |
| 266 | Everything here is also in the API at `/repos/{owner}/{name}/mirror`, and |
| 267 | as the `mirror_*` actions of the MCP `repository` tool. See the |
| 268 | [API reference](/reference/api/). Reading a repository's |
| 269 | mirroring needs read access; syncing needs push; everything else needs the |
| 270 | Admin role. Agents never move a repository to g1t, nor add, change or |
| 271 | remove a remote. |
| 272 | |
| 273 | Removing a remote makes a mirror an ordinary repository with what it has, |
| 274 | and stops pushes to a follower. It is refused during a takeover: hand it |
| 275 | back or move it to g1t first. |
| 276 | |
| 277 | [Webhooks](/guides/webhooks/) can subscribe to: |
| 278 | |
| 279 | - `mirror.unreachable` and `mirror.reachable` |
| 280 | - `mirror.state_changed`, sent when a mirror stands by, enters CI |
| 281 | failover, is taken over or is handed back |
| 282 | - `mirror.moved_in`, sent when a mirror is moved to g1t or stops because |
| 283 | its remote is gone |
| 284 | |
| 285 | ## When a remote goes away |
| 286 | |
| 287 | A standby mirror becomes an ordinary repository, keeping what it has, when: |
| 288 | |
| 289 | - the repository is deleted on GitHub, |
| 290 | - g1t's GitHub App is uninstalled from its account, or |
| 291 | - a workspace owner removes that GitHub account from the workspace. |
| 292 | |
| 293 | A takeover in progress keeps going and says why on the link: move it to |
| 294 | g1t to keep it. |
| 295 | |
| 296 | ## Not supported yet |
| 297 | |
| 298 | - Checks go back to GitHub as commit statuses, one per workflow run; a |
| 299 | run's jobs aren't shown there as separate check runs. Remotes other than |
| 300 | GitHub aren't told about checks. |
| 301 | - CI failover starts only when someone starts it. |
| 302 | - A takeover runs under the repository's own [rules](/guides/rules/) on |
| 303 | g1t, not the remote's branch protection. |
| 304 | - A mirror doesn't show the remote's pull requests. |
| 305 | - GitLab and Bitbucket repositories link as any host over HTTPS, asked |
| 306 | every five minutes. |