DEV Community

Wen Ping
Wen Ping

Posted on Originally published at anguskit.com

Migrate from GitLab to Migrate from GitLab to AngusGit: checklist, dual-run, and rollback

This article explains how a team moves from GitLab to AngusGit as a whole, so that cutover day loses no code and a problem can be undone. After reading it, you can check each item through five stages — prepare, first sync, dual-run, cutover day, and rollback — and leave GitLab a two-week window where projects are archived and not deleted.

Sign in to AngusGit first. A GitLab path such as platform/backend/order-service has only two levels to land in. When you are unsure, or when different subgroups contain a project with the same name, fold the subgroup into the repository name: platform/backend-order-service. Change a dot in a repository name to a hyphen, for example docs.site to docs-site. Under Settings → Team, click New Team, and set Team Slug to the GitLab group’s English name. In the sidebar, open Repositories → New repository, and leave Add a README file, Add .gitignore, and Choose a license unchecked. If any box is checked, the push is rejected.

Open Settings → Profile, the Access Tokens tab, and click Generate New Token. Enter your current login password. The sync script uses a token with git:write and repository permission WRITE. Jenkins, which only fetches, uses a second token with git:read and repository permission READ. In both cases the username is your AngusGit login name, the password is the token, and you copy the token immediately. Push one repository by hand first and save it in the system credential prompt, then run the script.

Write the mapping as repos.txt, two columns per line: the GitLab path, then “owner/repository name”:

platform/backend/order-service platform/backend-order-service
Enter fullscreen mode Exit fullscreen mode

Then run it (replace the domains):

while read -r src dst; do
  git clone --bare "https://gitlab.example.com/${src}.git" tmp.git || continue
  git -C tmp.git branch -m master main 2>/dev/null
  git -C tmp.git push "https://git.anguskit.com/${dst}.git" --all
  git -C tmp.git push "https://git.anguskit.com/${dst}.git" --tags
  rm -rf tmp.git
done < repos.txt
Enter fullscreen mode Exit fullscreen mode

Compare each repository with the command below. No unexpected output means the two sides match. When master was renamed to main, that one name difference is expected:

diff \
  <(git ls-remote --heads --tags https://gitlab.example.com/platform/backend/order-service.git | sort) \
  <(git ls-remote --heads --tags https://git.anguskit.com/platform/backend-order-service.git | sort)
Enter fullscreen mode Exit fullscreen mode

Turn branch protection on only after the last sync on cutover day. Open the repository’s Settings → Branch Protection, click Add rule, and under Branch name choose the branch from the inventory (usually main). Turn on Require pull request reviews, and set Required approving review count to the number you wrote down from GitLab. Turn on Include administrators. Leave Allow force pushes and Allow deletions off. Do not turn this on during the sync, or the script’s push to main is rejected.

1. This round moves repositories only

GitLab holds more than code. It also holds merge requests, issues, CI pipelines, and integrations. Switching all of it at once means one broken piece stalls the whole move. Split it:

Round What moves Notes
This round Git repositories, member permissions, branch protection, and webhooks After the cutover, the team commits, reviews, and merges on AngusGit
Next round CI This round makes the existing build start from AngusGit. The next round decides whether to rewrite the pipelines
Scheduled separately Issues and Wiki These live in GitLab's database, not in the Git repository. A push does not bring them

A pull request is a request to merge one branch into another after someone reviews the change. GitLab calls it a merge request. AngusGit's label is Pull Requests.

CI waits for the next round, and it still needs a home before cutover day. After GitLab stops receiving pushes, GitLab CI stops starting. The safest bridge for this round is to point existing Jenkins jobs at AngusGit and turn on Jenkins Poll SCM (it checks the branch on a timer for new commits) against main, for example every two minutes. Polling notices the new commit whether it arrived from the command line or from the merge button on the pull request page. Jenkins fetches with the read-only token from the start of this article: git:read only, repository permission READ, the username is your login name, and the password is the token.

2. Prepare: take inventory first

Before the move, look up every row below and write the answers in one table. Every step on cutover day uses it.

Inventory item Where to look on GitLab What to write down
Repository list The project list of each group Path, visibility (who can see the repository), default branch name, rough size, and the matching "team/repository name" on AngusGit
Members and roles The Members page of the group and the project Whether each person needs to read or write each repository, and who is a maintainer
Protected branches Project Settings → Repository → Protected branches Which branches block a direct push, and how many approvals a merge needs
CI .gitlab-ci.yml and the runners in the project Which repositories have a build, and what starts it after the cutover
Webhooks and integrations Project Settings → Webhooks Each webhook's URL and purpose. A webhook is a notice sent to another system when something in the repository changes. Recreate them on AngusGit after the cutover
Places that hardcode a Git URL Search the code for the GitLab domain .gitmodules, CI and deploy scripts, Git URLs in dependency declarations, and clone commands in docs
In-progress merge requests The Merge requests list of each project Merge or close them before cutover day. Review records do not come over
Git LFS Whether .gitattributes contains filter=lfs AngusGit does not support LFS. Handle large files in those repositories first

Git LFS (Git Large File Storage) stores large files outside normal Git history.

The row "places that hardcode a Git URL" is the one people miss. Deploy scripts, submodules, and dependency declarations that still name GitLab do not fail immediately after the cutover. They fail together on the day GitLab is taken down.

3. First sync

  • Create teams and empty repositories on AngusGit

From the mapping in the inventory, create the teams and the empty repositories. Leave all three initialization boxes unchecked when you create a repository.

  • Give members read permission only

Add members to the team or the repository, and give them read permission only at this stage. During dual-run, code on AngusGit arrives only by sync from GitLab. Nobody pushes straight to AngusGit. Read-only permission is what enforces that.

  • Leave branch protection off

The sync script pushes branches straight to AngusGit, including main. If main already has branch protection, the sync push is rejected. You can write the protection rules down now. Create them after the last sync on cutover day.

  • Run the sync script once, then check

Run the sync script and push every repository in repos.txt. For each repository, compare GitLab and AngusGit with the git ls-remote --heads --tags command from the start of this article. No unexpected output means the sync succeeded. When master was renamed to main, that one name difference is expected.

4. Dual-run: write on GitLab, read on AngusGit

Dual-run means both platforms stay up for a while, and only one of them accepts writes. During dual-run, the team keeps committing, reviewing, and merging on GitLab. AngusGit syncs from GitLab once a day, so people can learn the new URLs and the new screens early.

Dual-run has one rule: only one side can accept writes at any time. If both sides accept writes, the code diverges. After that, the sync push is rejected, and someone has to merge by hand. That is the failure a migration is trying to avoid.

Things you can do in this window:

  • Run the sync script once a day from a scheduled job, and compare with git ls-remote after every run.
  • Ask a few people to clone from AngusGit and confirm that SSH and HTTPS both work, and that access tokens are ready.
  • On a test repository, have people walk a pull request, a review, and a merge all the way through.
  • Create a test Jenkins job that fetches from AngusGit with Poll SCM on, and confirm it notices a newly synced commit and starts a build.

5. Cutover day

Do these in order. Finish one step before you start the next.

  • Announce the freeze time ahead

Tell everyone at least one day ahead: the time pushes to GitLab stop, and the time AngusGit becomes the place to push. Merge or close in-progress merge requests before the freeze.

  • Freeze GitLab

At the time, archive each project on GitLab (project Settings → General → Advanced → Archive project). An archived project is read-only. Nobody can push. The code and the merge request records are still there.

  • Sync one last time and compare

Run the sync script, then run git ls-remote on every repository. Continue only when every comparison matches. If one does not, find which branch differs and why before you go on.

  • Turn on write permission and branch protection on AngusGit

From the inventory, change members from read-only to read-write. Then, under each repository's Settings → Branch Protection, add the rule from the start of this article: turn on Require pull request reviews and fill in the count from the inventory, turn on Include administrators, and leave Allow force pushes and Allow deletions off.

  • Switch CI and integrations

Point Jenkins jobs at the AngusGit source URL, turn on Poll SCM, push a test commit, and confirm a build starts. Recreate the other webhooks from the inventory under the repository's Settings → Webhooks on AngusGit.

  • Tell everyone to change the URL

Each person runs git remote set-url origin <AngusGit URL> in their local repository, then git fetch, and confirms the pull works. People who cloned from AngusGit during dual-run do not change anything.

Before cutover day ends, walk the inventory and change every hardcoded GitLab URL to the AngusGit URL, then commit those edits.

6. Rollback

Archive the projects on GitLab. Do not delete them. From cutover day, leave a rollback window, for example two weeks. Inside that window, a problem you cannot fix immediately — permissions wrong across the board, or every build stopped — can go back to GitLab.

On rollback, AngusGit already has new commits from after the cutover. Bring those commits back to GitLab first:

  • Freeze AngusGit

Tell everyone to stop pushing to AngusGit, and change member permissions back to read-only.

  • Unarchive GitLab

An archived GitLab project rejects pushes. Unarchive it first under project Settings → General → Advanced.

  • Push the new commits back to GitLab

Where the migration renamed master to main, rename it back to master first. GitLab has no new commits after the freeze, so this push is a normal fast-forward. It does not need a force push.

  • Compare, then tell everyone to switch the URL back

After git ls-remote shows the two sides match, tell everyone to point origin back at the GitLab URL, and switch CI back to GitLab.

For the rollback, git clone --bare makes a copy that stores only Git data and has no working files to edit, then you push it back to GitLab:

git clone --bare https://git.anguskit.com/platform/backend-order-service.git
cd backend-order-service.git
git branch -m main master
git push https://gitlab.example.com/platform/backend/order-service.git --all
git push https://gitlab.example.com/platform/backend/order-service.git --tags
Enter fullscreen mode Exit fullscreen mode

After the rollback window ends, and you are sure GitLab is no longer needed, handle the GitLab projects according to the company's data-retention rules.

7. When something fails

What you see Cause What to do
A sync push of main is rejected during dual-run Branch protection is already on for main on AngusGit Delete that protection rule, and create it again after the last sync on cutover day
A sync push reports ! [rejected] … (non-fast-forward) Someone pushed straight to AngusGit during dual-run, and the two sides diverged Find that commit and decide whether to keep it. To keep it, commit it again on GitLab. Then reset the AngusGit branch to the GitLab version. Also check that member permissions really are read-only
The comparison differs on cutover day A push landed before the freeze and missed the sync, or the sync dropped midway Run the sync script again until the comparison matches
Some builds no longer start after the cutover The Jenkins job still points at GitLab, or Poll SCM is off, or a build script still contains a GitLab URL Walk the inventory and change the URLs in each Jenkins job and script
Someone's push reports repository '…' not found The local origin is wrong, and the team name or repository name does not match the mapping Copy the URL from the Code button on the AngusGit repository page, and run git remote set-url again

Common questions

Can GitLab and AngusGit stay in two-way sync and switch slowly?

That is a poor fit. If both sides accept writes, they diverge at some point. After that, it is hard to say who merges and which side wins. During dual-run, keep a single writer. The cutover is one clear time.

Do dozens of repositories have to switch in one day?

No. Split them by team or by product line, and run section 5 once per batch. Repositories in the same batch that depend on each other, such as a main repository and its submodules, switch on the same day.

What about discussion on merge requests?

It does not come over. Before you archive GitLab, export important design discussions or turn them into documents and put them in the repository's docs directory. Merge or close in-progress merge requests before the freeze.

When does CI move after the cutover?

This round points Jenkins at AngusGit so the existing builds keep working. After the team is used to the new repositories and the review flow, schedule a separate look at CI. AngusGit Team and Enterprise include pipelines. When you create one, you can choose Import GitLab CI and generate a first draft from .gitlab-ci.yml. Community has no CI/CD menu.

Summary

Moving from GitLab to AngusGit, this round moves repositories. CI and issues are scheduled separately. Inventory the repositories, members, protected branches, CI, webhooks, and hardcoded Git URLs first. After the first sync, dual-run starts: GitLab is the only side that accepts writes, and AngusGit is read-only and syncs daily. Cutover day goes in order: freeze GitLab, sync one last time, confirm the comparison matches, then turn on write permission and branch protection, switch CI, and tell everyone to change the URL. Archive GitLab projects and do not delete them. Inside the rollback window, push AngusGit's new commits back to GitLab, then unarchive.

Conclusion: A batch migration stays single-writer. During dual-run, write only to GitLab and sync one way each day into a read-only AngusGit. On cutover day, archive and freeze GitLab, line up every ref with git ls-remote, and then turn on write permission and branch protection on AngusGit.

Further reading

Top comments (0)