Git Course 0%

Maintenance and migration

Expert ≈ 10 min

Before this lesson

What you will learn

  • How to measure a repository's size and find what is in it
  • What gc, prune and maintenance actually do
  • How to move a project between GitLab and GitHub without losing anything

After this lesson you can

  • I can diagnose a slow repository and migrate one safely

Why this matters

Repositories get slow for a small number of reasons, and all of them are measurable. Migration comes up whenever a company changes platform, and doing it by cloning and pushing loses more than people expect.

Measuring

Terminal
$ git count-objects -vH
count: 27
size: 108.00 KiB
in-pack: 0
packs: 0
size-pack: 0 bytes
prune-packable: 0
Line Means
count and size Loose objects, not yet packed
in-pack and size-pack Objects inside packfiles (lesson 18.1)
prune-packable Loose objects already in a pack, removable

A large size-pack is history; a large size with many loose objects usually means gc has not run recently.

To find what is actually big, list the largest objects and map them back to paths:

Terminal
$ git rev-list --objects --all \
  | git cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' \
  | awk '$1=="blob"' | sort -k3 -n -r | head -10

That is worth keeping somewhere: it is the standard answer to "why is this clone two gigabytes?", and the answer is almost always a handful of binaries committed years ago.

Cleaning up

Command Does
git gc Packs loose objects, removes unreachable ones past the expiry
git gc --aggressive Repacks more thoroughly; slow, and rarely worth it
git prune Removes unreachable objects; gc calls it
git maintenance start Registers background maintenance on a schedule
git repack -ad Repacks everything into one pack

Git runs gc automatically when loose objects accumulate, so most repositories need nothing. git maintenance start is the modern replacement for a cron job on a large repository, and it does incremental work rather than one long pause.

Large files that are already in history

Deleting a large file in a new commit does not shrink anything: every old commit still contains it (lesson 8.7). Removing it means rewriting history, with git filter-repo, and that has the same consequences as any rewrite, applied to every commit.

The realistic procedure:

  1. Agree it with the whole team, and pick a time when nobody has unpushed work.
  2. Back up: a full mirror clone, kept until everyone is satisfied.
  3. Run git filter-repo with the path to remove.
  4. Force-push all branches and tags.
  5. Everyone re-clones. Not pulls: re-clones, because every hash has changed.
  6. Move the file into LFS or out of the repository entirely, or it will come back (lesson 13.10).

Step 5 is why this is a planned operation rather than a Tuesday afternoon.

Migrating between platforms

Cloning normally and pushing copies your current branch and nothing else. A mirror copies everything:

Terminal
$ git clone --mirror https://gitlab.com/northwind-trails/trailguide.git
$ cd trailguide.git
$ git remote set-url --push origin git@github.com:northwind-trails/trailguide.git
$ git push --mirror

That moves every branch, every tag and every note. What it does not move is everything that is not Git:

Moves with --mirror Does not
Commits, branches, tags Issues and their comments
Notes Merge or pull requests, and their reviews
— Wiki (it is a separate repository: mirror it too)
— CI configuration and variables, and secrets
— Members, permissions, protected branches
— Releases, packages, and LFS objects

Both platforms have importers that carry issues and pull requests as well, and both are the right first choice: GitHub's importer for a GitLab project, and GitLab's Import project for a GitHub repository. Use --mirror when you only want the Git part, or when the importer cannot reach the source.

Two things to do before announcing the move: redirect or archive the old project, so nobody keeps pushing to it, and rewrite the remotes in everyone's clones:

Terminal
$ git remote set-url origin git@github.com:northwind-trails/trailguide.git

LFS on migration

LFS objects live outside the Git repository, so a mirror push carries the pointers and not the content (lesson 13.10). They need moving explicitly:

Terminal
$ git lfs fetch --all
$ git lfs push --all <new remote>

Forgetting this produces a repository that looks complete and whose images are three lines of text.

How to do it

Terminal
$ git count-objects -vH
$ git gc
$ git maintenance start
$ git clone --mirror <old>
$ git push --mirror <new>
$ git remote set-url origin <new>

Common mistakes

  • Deleting a large file and expecting the repository to shrink. Every old commit still contains it.
  • Rewriting history without warning everyone, which leaves colleagues with incompatible clones.
  • git gc --prune=now during a recovery.
  • Migrating with a normal clone and push, which moves one branch.
  • Forgetting the wiki, which is a separate repository.
  • Forgetting LFS content, which produces a repository full of pointer files.
  • Leaving the old project writable, so work continues in two places.

Try it yourself

Goal: measure a repository and mirror it safely.

  1. Run git count-objects -vH on a repository you use and note the pack size.
  2. Run the largest-objects command and read the top ten. Are any of them surprising?
  3. Run git gc and compare the numbers before and after.
  4. Make a mirror clone of your practice project into a new folder, and confirm git branch -a inside it lists every branch.
  5. Create an empty project on the other platform, push the mirror to it, and check that tags and branches all arrived.

Expected result: a measured repository, and a complete copy on another platform with every ref intact.

Show solution

Step 4 is the part people skip. A mirror clone is a bare repository containing every ref, which is why git push --mirror from it moves everything; a normal clone has one branch checked out and remote-tracking references for the rest, and pushing from it moves far less than you would expect.

Check yourself

1. You deleted a 200 MB file last year. Why is the repository still large?
2. What does git clone --mirror followed by git push --mirror move?
3. When is git gc --prune=now a bad idea?

Key terms

Repository (repo) Clone Git LFS (Large File Storage) Remote