Ludzie dzielą się na tych co robią backupy, i na tych co dopiero zaczną je robić. Oczywiście, nie życzę nikomu problemów z utratą danych, ale jeżeli już trzeba faktycznie zaczynać od początku, to dobrze mieć możliwośc szybkiego przywrócenia swojego codzinnego workflow.
A ponieważ workflow w dużej mierze opiera się na dotfiles, to dobrze mieć możliwość ich łatwego backupowania, a przy okazji także możliwość synchronizacji między maszynami na których codziennie pracujemy.
Oczywiści istnieją dedykowane narzędzia, warto tutaj wymienić choćby GNU Stow, który działa na zasadzie tworzenia linków symbolicznych. Ale z gita korzystam tak czy inaczej. Więc po co komplikować?
Bare repo
Bare repo to klucz do całego rozwiązania.
Jeżeli poczytamy dokumentację gita, znajdziemy tam informację, że bare repo służy przede wszystkim do synchronizacji i udostępniania kodu między maszynami, a nie do bezpośredniej pracy nad tym kodem.
Czyli dokładnie to co jest potrzebne.
Dla przypomnienia - standardowe repozytorium gita, składa się z katalogu .git, zwierającego całą historię zmian, oraz katalogu roboczego w którym znajduje się aktualna rewizja kodu.
Bare repo to repozytorium zawierające tylko strukturę katalogu .git.
Czyli krótko podsumowując - bare repo to czysta historia zmian, bez żadnych dodatkowych elementów.
Jeżeli dodamy do tego katalog domowy jako katalog roboczy, to wychodzi z tego idealne rozwiązanie.
Repozytorium w katalogu domowym
Więc tak - katalog domowy to nasz katalog roboczy dla repozytorium (worktree).
Utwórzmy więc nowe repozytorium:
git init --bare $HOME/.dotfiles
Pierwsze pytanie które bym zadał po zobaczeniu ww. polecenia, to dlaczego ten katalog ma się nazywać
.dotfiles, a nie .git?
Jeżeli korzystasz ze zmodyfikowanego prompta, np. ze względu na motyw terminala, czy użycie OHMYZSH to użycie nazwy .git spwoduje, że prompt pokaże informację o aktualnym branchu. A ja tego nie chcę.
Oczywiście nazwa może być zupełnie inna, ale warto ten plik ustawić jako ukryty (dotfile) żeby przypadkiem nie go nie zmodyfikować.
Jeżeli przejrzymy teraz zawartość tego katalogu:
ls $HOME/.dotfiles
COMMIT_EDITMSG HEAD branches config description hooks index info logs objects refs
To zobaczymy standardową strukturę katalogu .git. I tak właśnie ma być.
Ale jak spróbuje skorzystać z git status w katalogu domowym:
git status
Błąd:
fatal: not a git repository (or any of the parent directories): .git
Nie działa. To dlatego, że git szuka katalogu .git w katalogu domowym, a tam go nie ma.
Dokładnie tak jak chciałem. Na szczęście, git pozwala na określenie katalogu repozytorium, jak również
samego worktree za pomocą parametrów --git-dir i --work-tree.
I tego właśnie chcemy użyć. Warto od razu zdefiniować też alias, żeby nie musieć ciągle wpisywać tych parametrów:
alias dot='git --git-dir=$HOME/.dotfiles --work-tree=$HOME'
I już. Trzeba tylko pamiętać, że od tej pory, zamiast git, trzeba używać dot przy odwoływaniu się
do repozytorum dla dotfilesów:
dot status
Ostatnia rzecz, jaką koniecznie trzeba zrobić, to utworzyć sobie zdalne repozytorium, np. na GitHubie. Pamiętaj tylko, żeby dodać repozytorium jak prywatne. I nie polecam trzymać tam sekretów.
Ustawmy też branch main jako domyślny.
dot remote add origin <url_do_githuba>
dot branch -M main
Okej. To jest wersja absolutnie minimalna, ale już pozwoli na łatwe zarządzanie dotfilesami.
Dodajmy zatem najważniejszy plik:
dot add .zshrc
dot commit -m "initial commit - .zshrc"
dot push -u origin main
I już. Pierwszy .dotfile jest bezpieczny.
Ukrycie plików i zachowanie porządku
Uważam, że zarządznie dotfilesami jest ważne, więc warto dodać jeszcze kilka ulepszeń. Przede wszystkim, chcemy w tym repo trzymać tylko to co niezbędne, żadnych śmieci.
Żeby przypadkiem nie dodać żadnego nieporządanego pliku, ustawmy konfigurację repozytorium tak, żeby domyślnie wszystkie pliki były ignorowane:
dot config --local status.showUntrackedFiles no
Dzięki temu, dot status pokaże tylko te pliki, które zostały dodane do repozytorium, a nie wszystkie
nieśledzone pliki w katalogu domowym.
Oczywiście, jeżeli chcemy dodać jakiś nowy plik, to musimy go ręcznie dodać do repozytorium (np. tutaj konfiguracja neovima):
dot add .config/nvim/init.vim
dot commit -m "add nvim config"
dot push
Jak nie zgubić zmian?
Warto też pamiętać, że jeżeli już używamy repo do trackowania zmian, to warto je od czasu do czasu commitować i wypychać na zdalne repo. Żeby o tym pamiętać, ustawiłem sobie taką przypominajkę w pliku .zshrc:
# check if dotfiles are committed
if [[ -n $(dot status --porcelain) ]]; then
echo "⚠️ Dotfiles not committed ⚠️"
fi
Dzięki temu, każde otwarcie sesji terminala spowoduje wyświetlenie komunikatu, jeżeli w dotfilesach są jakieś nie dodane zmiany. Wydaje mi się to najprostszym sposobem, żeby o tym pamiętać.
Oczywiście można byłoby wykorzystać np. crona do robienia automatycznych commitów, ale ww rozwiązanie wydaje mi się wystarczające.
Krótki timesaver
Dzięki poniższemu aliasowi, jeżeli wpiszę dot bez żadnych parametrów, od razu zobaczę status repozytorium.
A przekazując jakieś polecenie, np. dot add .zshrc, to zostanie ono wykonane bezpośrednio:
alias dot='git --git-dir=$HOME/.dotfiles --work-tree=$HOME "${@:-status}"'
Synchronizacja
Dobra, to skoro mamy już pliki w repo, to teraz jak je zsynchronizować na nową maszynę?
Wystarczy sklonować repozytorium do katalogu domowego, ale z parametrem --bare:
git clone --bare <url_do_githuba> $HOME/.dotfiles
Następnie, podobnie jak wcześniej, ustawiamy alias:
alias dot='git --git-dir=$HOME/.dotfiles --work-tree=$HOME "${@:-status}"'
I ostatni krok - checkout:
dot checkout --force
Dlaczego --force?
Jeżeli w katalogu domowym mamy jakieś pliki (a zapewne tak jest), git nie pozwoli na zrobienie checkouta
gdyż pliki zostaną nadpisane. Ale ponieważ wiemy, że chcemy nadpisać te pliki, to używamy --force
i git zrobi checkout bez żadnych pytań.
Gotowe.
Oczywiście warto na drugiej maszynie też ustawić ukrycie nieśledzonych plików.
Pozostało już tylko commitowanie zmian i wypychanie ich na zdalne repo. Oraz regularne używanie dot pull --rebase
żeby zawsze mieć aktualną wersje plików.
Podsumowanie
Korzystam z tego rozwiązania już dość długo, żeby mieć pewność jego działania. Jest proste, szybkie i wymaga tylko tego co już i tak jest używane w innych projektach - skonfigurowanego gita. Żadnych dodatkowych narzędzi, zero komplikacji.
Prostota jest szczytem wyrafinowania. - Leonardo da Vinci