Jak sklonować duże repozytorium git na niestabilnym łączu sieciowym

Spis treści

Połączenie z internetem, z którego ostatnio przyszło mi korzystać, nie należało zbytnio do tych najbardziej wydajnych pod względem prędkości przesyłu danych. O ile szybkość transferu można by jeszcze przemilczeć, to owe łącze nie należało też do tych najstabilniejszych i czasem kontakt ze światem był zwyczajnie zrywany. Krótko rzecz ujmując, ten net nadawał się chyba jedynie do przeglądania stron WWW. Pech jednak chciał, że potrzebowałem zassać na takim połączeniu repozytorium git'a z patch'ami Debiana nakładanymi na kernel linux'a. To repozytorium nie należy do największych ale też do małych ono się nie zalicza -- waży około 2 GiB. Oczywiście można by zrobić jedynie płytki klon takiego repo za sprawą git clone --depth=1 , co skutkowałoby pobraniem plików w wersji z ostatniego commit'a, co z kolei zredukowałoby w znacznym stopniu wielkość pobieranych danych (parę-paręnaście MiB). Co jednak zrobić w przypadku, gdy musimy sklonować sporych rozmiarów repozytorium git, a dysponujemy przy tym dość wolnym połączeniem sieciowym? Czy jest to w ogóle możliwe, bo przecież git nie ma zaimplementowanej opcji wznawiania przerwanej synchronizacji i każde zakłócenie tego procesu skutkować będzie tym, że całe repozytorium trzeba będzie pobrać jeszcze raz i to bez względu na to, czy proces się zawiesi zaraz na początku, czy też pod koniec operacji.

Płytki klon repozytorium git

W większości przypadków nie ma potrzeby zasysać całego repozytorium git, np. gdy nie uczestniczymy w rozwoju jakiegoś projektu, a chcemy go sobie jedynie lokalnie zbudować i używać. Przykładem takiego większego projektu może być Android. W jego przypadku pobieranie całego repozytorium, które waży chyba już koło 100 GiB nie jest najlepszym wyjściem dla użytkowników, którzy by chcieli sobie jedynie zbudować nowszą wersję ROM'u i wgrać ją na swój smartfon. Przez stworzenie płytkiego klonu repozytorium możemy zaoszczędzić całą masę zasobów sieciowych jak i również nasz cenny czas.

Poniżej jest przykład jak tego typu płytki klon dowolnego repozytorium utworzyć:

$ git clone --depth=1 https://salsa.debian.org/kernel-team/linux.git/
Cloning into 'linux'...
remote: Enumerating objects: 1089, done.
remote: Counting objects: 100% (1089/1089), done.
remote: Compressing objects: 100% (738/738), done.
remote: Total 1089 (delta 48), reused 651 (delta 31)
Receiving objects: 100% (1089/1089), 1.49 MiB | 221.00 KiB/s, done.
Resolving deltas: 100% (48/48), done.

Jak widać wyżej, zostało pobranych niecałe 1,5 MiB z tych ponad 2,2 GiB, zatem różnica jest dość znaczna.

Jeśli zajrzymy sobie w log, to zobaczymy tam tylko i wyłącznie jeden commit:

$ git log --all --oneline

7d94791 (grafted, HEAD -> refs/heads/master, refs/remotes/origin/master, refs/remotes/origin/HEAD) Update to 4.20.7

Pogłębianie płytkiego klonu repozytorium git

Jeśli z jakichś przyczyn taki płytki klon repozytorium nas nie satysfakcjonuje, to możemy go pogłębić. Głębokość repozytorium jest mierzona w commit'ach. To wyżej utworzone lokalne repozytorium git ma jeden commit, więcej jego głębokość to 1.

Do pogłębiania klonu repozytorium git używa się polecenia git fetch , z tym, że mamy możliwość przeprowadzenia tego procesu na dwa sposoby. Pierwszy z nich wykorzystuje parametr --depth= , który określa ilość commit'ów, które trzeba będzie dociągnąć licząc od samej góry, czyli jeśli byśmy dali --depth=100 , to pobranych by zostało 99 commit'ów, bo jeden już mamy u siebie (w sumie mielibyśmy ich 100). Drugi ze sposobów wykorzystuje opcję --deepen , która umożliwia pobranie dodatkowych commit'ów licząc od miejsca granicy klonu. W tym przypadku jeśli byśmy określili --deepen=100 , to pobranych zostałoby 100 commit'ów, przez co byśmy mieli ich 101.

Jeśli chcemy zatem pobrać 10000 commit'ów na słabej jakości łączu internetowym, to możemy je pobierać w porcjach, np. po 1000.

$ git fetch --depth=1000
$ git fetch --depth=2000
...
$ git fetch --depth=6000
...
$ git fetch --depth=10000

lub też:

$ git fetch --deepen=1000
$ git fetch --deepen=1000
$ git fetch --deepen=1000
...
$ git fetch --deepen=1000

Gdy osiągniemy granicę repozytorium, czyli zejdziemy na maksymalną głębokość, to git nam zwróci poniższy komunikat:

$ git fetch --depth=10000
remote: Total 0 (delta 0), reused 0 (delta 0)

Jeśli ujrzymy taką informację po wydaniu polecenia git fetch , to możemy być pewni, że pobraliśmy wszystkie możliwe commit'y.

Opcja unshallow

Innym ze sposobów pobrania całego repozytorium git mając utworzony płytki klon jest skorzystanie z opcji --unshallow w poleceniu git fetch . Ta procedura w skutkach będzie dokładnie taka sama jak opisany wyżej proces pogłębiania klonu repozytorium. Jedyna różnica jest taka, że --unshallow pobierze nam wszystkie brakujące commit'y naraz. Jeśli dojdzie do przerwania tego procesu, to naturalnie trzeba będzie go ponowić. Z opcji --unshallow możemy skorzystać na dowolnej głębokości płytkiego klonu.

W przypadku, gdy nie jesteśmy pewni, czy w procesie pogłębiania klonu repozytorium nie zapomnieliśmy o czymś, to zawsze można wydać to poniższe polecenie:

$ git fetch --unshallow
fatal: --unshallow on a complete repository does not make sense

Mamy tutaj informację, która oznajmia nam, że to polecenie nie ma sensu, bo całe repozytorium mamy już pobrane na dysk.

Problemy z gałęziami

W zasadzie to można by ten artykuł skończy już, gdyby nie fakt, że coś się liczby nie zgadzają. W statystykach repozytorium można było wyczytać coś na wzór: 13,746 Commits , 29 Branches , 1,418 , Tags , 2.2 GB Files. Jednak przy głębokości 10000 mamy niby już całe repozytorium. Waga plików również się nie zgadzała.

Okazuje się, że wykonując płytki klon repozytorium git, klonujemy jedynie główną gałąź. A jak widać wyżej, tych gałęzi w tym repozytorium było 29. Gdzie zatem podziało się pozostałe 28 gałęzi i jak je odtworzyć?

Najprościej byłoby dodać parametr --no-single-branch do polecenia git clone --depth=1 . W takim przypadku nie zostanie stworzony minimalny klon repozytorium, choć i tak zostanie pobranych sporo miej danych niż przy pełnej synchronizacji.

Poniżej jest przykład polecenia:

$ git clone --depth=1 --no-single-branch https://salsa.debian.org/kernel-team/linux.git/
Cloning into 'linux'...
remote: Enumerating objects: 1003994, done.
remote: Counting objects: 100% (1003994/1003994), done.
remote: Compressing objects: 100% (395010/395010), done.
Rceiving objects:  10% (107017/1003994), 89.49 MiB | 2.13 MiB/s

No jak widać wyżej, z 1,5 MiB, zrobiło się prawie 90 MiB, a to tylko 10% , bo proces pobierania ciągle jest toku. Po ilości obiektów można stwierdzić, że pobieranych jest około 1/7 faktycznej objętości repozytorium, co niekoniecznie musi się przekładać na takie same proporcje w MiB. Niemniej jednak, mając słabe łącze możemy nie być w stanie dociągnąć takiego wielkiego kawałka.

Określenie gałęzi w pliku .git/config

Jeżeli powyższe rozwiązanie nie wchodzi w grę, to zawsze możemy pokusić się o ręczną edycję pliku .git/config , który jest ulokowany w głównym katalogu repozytorium (po wykonaniu płytkiego klonu). Ten plik zawiera sekcję [remote "origin"] , która prezentuje się w poniższy sposób:

[remote "origin"]
    url = https://salsa.debian.org/kernel-team/linux.git/
    fetch = +refs/heads/master:refs/remotes/origin/master

Widzimy, że mamy tylko jeden wpis z +refs/heads/... , który odnosi się do gałęzi master . Tego typu linijek możemy dodać tyle ile w danym repozytorium jest gałęzi. Dla przykładu, by dodać kolejną interesującą nas gałąź, przepisujemy powyższy kawałek do poniższej postaci:

[remote "origin"]
    url = https://salsa.debian.org/kernel-team/linux.git/
    fetch = +refs/heads/master:refs/remotes/origin/master
    fetch = +refs/heads/sid:refs/remotes/origin/sid
    ...

Listę gałęzi można uzyskać posługując się poleceniem git ls-remote :

$ git ls-remote --heads
From https://salsa.debian.org/kernel-team/linux.git/
af210b506d24cf7585629e346c921aed90e07133        refs/heads/carnil/import-4.14.14
a56ffb69de38383f26e77b740b9270ee0faf834c        refs/heads/carnil/jessie-security/CVE-2017-7533
7c1804aa712bae44319bd481523c0500df16733b        refs/heads/carnil/stretch-security/CVE-2017-7533
0205ec9ec76ff2d993241a55628e3bd196ecec44        refs/heads/dgit-master
0839599a02f9cbbcb1933e4a60bcbbcf5bcacc11        refs/heads/etch
46f3979bd0f6a567b563f0c99f95c592067e94bb        refs/heads/etch-security
a0fe8cfa8617c895bf6d42383e1975199f342c5b        refs/heads/etchnhalf
66235807ed13884dc8986d9238f83c54baa26ace        refs/heads/etchnhalf-security
7b995d73957608b43a4b2dfacfa9e17bdc37e711        refs/heads/jessie
1d63dcd6296b9ad495d1a7d70c63fa5db68b77b7        refs/heads/jessie-backports
5009a129295a70d8017974c9821ecdd12d7d9675        refs/heads/jessie-security
c7b190d1b45dd7a7df71eb0651b677754972f7fd        refs/heads/jessie-security-4.9
13bc212fa183d61162a1708df0ee9382c0ac66f0        refs/heads/jessie-updates
cd2e2790cfeebeec461ce28530f23b8bf0e24e7b        refs/heads/lenny
14865a3daf8e808072ebca063297815e0967b129        refs/heads/lenny-security
7d94791b95d147a13f2eae4fa9a5e852c95330c9        refs/heads/master
ce61976b4810abb7e88f0bf08dc8efc7e6a19dc5        refs/heads/rosh/armel_mini
26e9f62e399a67e454ff40deca2eef531c279139        refs/heads/sid
9e549e30eb6e12e97f3ff29497821aa41564ee93        refs/heads/squeeze
f8d465883cbbf23432db1d0139f0b43e570ebc9d        refs/heads/squeeze-backports
03cf6ed02e5128674ac954b345cdea2a3c6b9ddd        refs/heads/squeeze-security
155c325ba50c3cdd60d9fcdfdd961a4a636a6367        refs/heads/stretch
5357a00fc4892878f8e532862540a575df621e2c        refs/heads/stretch-backports
0a50b0efbc995f6c5710f4d52e321ef6102ee298        refs/heads/stretch-security
1d69dac66c315a290fb61c5f400e056e8d01fe50        refs/heads/ukleinek/jessie
beed93421656d6290a4fe3952e6a29696a6cbaf7        refs/heads/waldi/cloud-arm64
15ca73ce51f27d275a1e67e570cfe3a13ade3368        refs/heads/wheezy
b479e71dc9fd2215f08b3ea795e0fe9264cc39c3        refs/heads/wheezy-backports

Gdy teraz zaktualizujemy informacje o zdalnym repozytorium, to zostaną pobrane brakujące gałęzie. Nie musimy przy tym pobierać ich wszystkich na raz ale jeśli chcemy mieć ich lokalną kopię, to stosowne wpisy trzeba będzie ręcznie pododawać do pliku .git/config . Sprawdzamy teraz jak wygląda proces dodania nowej zdalnej gałęzi:

$ git remote update && git status
    Fetching origin
    remote: Enumerating objects: 209, done.
    remote: Counting objects: 100% (209/209), done.
    remote: Compressing objects: 100% (72/72), done.
    remote: Total 209 (delta 92), reused 199 (delta 89)
    Receiving objects: 100% (209/209), 260.34 KiB | 136.00 KiB/s, done.
    Resolving deltas: 100% (92/92), done.
    From https://salsa.debian.org/kernel-team/linux
     * [new branch]          sid        -> origin/sid
    On branch master
    Your branch is up to date with 'origin/master'.

    nothing to commit, working tree clean

$ git branch -a

* master
  remotes/origin/HEAD -> origin/master
  remotes/origin/master
  remotes/origin/sid

Jeśli chcielibyśmy pobrać wszystkie gałęzie naraz, to zamiast precyzować osobne linijki dla każdej z gałęzi w pliku .git/config , to możemy dodać tam poniższy wpis:

[remote "origin"]
    url = https://salsa.debian.org/kernel-team/linux.git/
    fetch = +refs/heads/*:refs/remotes/origin/*

Jeśli interesuje nas inna gałąź niż ta domyślna i chcemy wykonać jedynie jej płytki klon, to korzystamy z poniższego polecenia:

$ git clone --depth=1 https://salsa.debian.org/kernel-team/linux.git/ -b sid

Czyszczenie lokalnego klonu repozytorium

Gdy pobierzemy już całe zdalne repozytorium na dysk, to może się okazać, że ta nasza kopia lokalna zajmuje więcej miejsca niż niż jej zdalny odpowiednik. Można nieco zoptymalizować rozmiar tak utworzonego repozytorium przez wydanie poniższych poleceń:

$ git gc --prune=now
$ git remote prune origin

Przebudują nam one lokalne repozytorium i pousuwają zbędne rzeczy, przez co zoptymalizują ilość zajmowanego przez repo miejsca na dysku.

Mikhail Morfikov avatar
Mikhail Morfikov
Po ponad 10 latach spędzonych z różnej maści linux'ami (Debian/Ubuntu, OpenWRT, Android) mogę śmiało powiedzieć, że nie ma rzeczy niemożliwych i problemów, których nie da się rozwiązać. Jedną umiejętność, którą ludzki umysł musi posiąść, by wybrnąć nawet z tej najbardziej nieprzyjemniej sytuacji, to zdolność logicznego rozumowania.