Tak się zdarzyło, że w ciągu ostatnich miesięcy pracowałem nad wieloma projektami programistycznymi. Repozytoria leżały u różnych dostawców, musiałem więc użyć osobnych kont i metod autoryzacji. W moim przypadku były to jedno konto na Codebergu oraz dwa konta na Githubie - prywatne oraz pracowe. Problemem do rozwiązania stała się kwestia zarządzania tymi tożsamościami, kluczami, itp. Oto co chciałem osiągnąć: niezależnie od tego, skąd oraz dokąd sklonuję repozytorium, wszystkie zmiany powinny być opisane odpowiednim do projektu imieniem oraz adresem email, a pobieranie i wrzucanie zmian na serwer autoryzowane odpowiednim kluczem.
Do przechowywania konfiguracji repozytoriów Git używa się... Konfiguracji Gita. Jest to seria prostych plików tekstowych + narzędzie linii poleceń. Seria, ponieważ pliki te znajdują się w kilku miejscach i mają różny zakres działania. Mnie interesują najbardziej:
~/.gitconfig- wersja globalna konfiguracji, znajduje się w folderze domowym. Tutaj znajdują się globalne ustawienia, takie jak strategie scalania czy aliasy (o których będzie kilka zdań na końcu), które są automatycznie aplikowane do każdego repozytorium.<PROJEKT>/.git/config- wersja lokalna konfiguracji, znajduje się wewnątrz każdego repozytorium. Przechowuje przede wszystkim informacje dotyczące konkretnego repozytorium, takie jak adres zdalnego repozytorium, aktywne gałęzie, czy właśnie wspomniane wcześniej imię i adres email, używane do opisywania zmian. Można tu też nadpisać wartości z konfiguracji globalnej - lokalna wartość ma zawsze wyższy priorytet.
Przykładowy plik wygląda następująco:
# <PROJEKT>/.git/config
[core]
repositoryformatversion = 0
filemode = true
bare = false
[remote "origin"]
url = git@github.com:osobiste/projekt.git
fetch = +refs/heads/*:refs/remotes/origin/*
[branch "main"]
remote = origin
merge = refs/heads/main
[user]
name = "Andrzej"
email = "andrzej@buziaczek.pl"
Drugą częścią układanki są klucze SSH. Pozwalają one na automatyczną autoryzację dostępu do różnych zasobów, również do repozytoriów Git. Pary kluczy - jeden prywatny i jeden publiczny - najlepiej stworzyć per tożsamość+dostawca; w moim przypadku są to trzy pary. To, które klucze używane są do którego repozytorium, można ustawić w kolejnym pliku konfiguracyjnym: ~/.ssh/config.
Przykładowy plik wygląda tak:
# ~/.ssh/config
Host codeberg.org
HostName codeberg.org
User git
IdentityFile ~/.ssh/id_rsa_codeberg_osobiste
Host github.com
HostName github.com
User git
IdentityFile ~/.ssh/id_rsa_github_osobiste
Host github.com
HostName github.com
User git
IdentityFile ~/.ssh/id_rsa_github_pracodawca
Co uważniejsi mogą już tutaj zauważyć pewną niedogodność: pole Host, które jest identyfikatorem danej konfiguracji, nie rozróżnia pomiędzy różnymi kontami u danego dostawcy - mamy dwa wpisy oznaczone github.com. Na szczęście da się to łatwo obejść (spoiler: to pole to jedynie alias).
Rozwiązanie, które pozwoliło mi osiągnąć efekt, polega na użyciu dwóch intrygujących dyrektyw Konfiguracji Gita:
[includeIf hasconfig:remote.*.url]- która pozwala zastosować osobną konfigurację w zależności od adresu repozytorium - ma sens
[url.<base>.insteadOf]- która pozwala na całkowitą zmianę adresu danego repozytorium - ⁉️🙋♀️🤯
Jak to wszystko działa? Bardzo prosto. Zacznijmy od globalnego ~/.gitconfig:
# ~/.gitconfig
[includeIf "hasconfig:remote.*.url:git@codeberg.org:osobiste/**"]
path = ~/.gitconfig_osobiste.codeberg.org.gitconfig
[includeIf "hasconfig:remote.*.url:git@github.com:osobiste/**"]
path = ~/.gitconfig_osobiste.github.com.gitconfig
[includeIf "hasconfig:remote.*.url:git@github.com:pracodawca/**"]
path = ~/.gitconfig_pracodawca.github.com.gitconfig
[alias]
nuke = ...
check = ...
Mamy tutaj trzy identyczne wpisy includeIf, które robią dokładnie to samo, czyli: dla dowolnego repozytorium, jeśli adres tego repozytorium pasuje do wzorca, to załącz do końcowej konfiguracji dla tego repozytorium zawartość pliku wskazanego w path. Nazwa i lokalizacja tych plików może być dowolna - w moim przypadku wyglądają one jak wyglądają, ponieważ:
- chciałem, żeby wszystkie pliki
.gitconfigw katalogu domowym były "koło siebie" np. w przeglądarce plików VSCodium - chciałem od razu widzieć, który plik odpowiada jakiej wersji tożsamość+dostawca
- VSCodium rozpoznaje plik o nazwie
.gitconfigjako Konfigurację Gita - i ładnie koloruje składnię i oznacza ikonkę - ale pliku o nazwie.gitconfig_osobiste.codeberg.orgjuż nie ogarnia, i dopiero dodanie "rozszerzenia" załatwiało sprawę
Poniżej przykład jednego z "inkludowanych" plików - w tym przypadku jest to wersja dla Githuba:
# ~/.gitconfig_osobiste.github.com.gitconfig
[user]
name = "Andrzej"
email = "andrzej@buziaczek.pl"
[url "git@osobiste.github.com:"]
insteadOf = git@github.com:
Sekcja [user] jest oczywista. Natomiast nikt może zapytać, o co chodzi z tą podmianą adresu? To jest rozwiązanie wspomnianej niedogodności z konfiguracją SSH! Żeby to miało pełny sens, trzeba wprowadzić do niej małą poprawkę:
# ~/.ssh/config
Host osobiste.github.com # poprawiony Host, a.k.a `alias`
HostName github.com
User git
IdentityFile ~/.ssh/id_rsa_github_osobiste
...
I teraz wszystko działa automagicznie w ten sposób:
- klonujemy przykładowe repozytorium:
git clone git@github.com:osobiste/projekt.git - adres repozytorium pasuje do jednej z dyrektyw
includeIfw globalnej konfiguracji Git - imię oraz email są odczytane z dedykowanej konfiguracji - każda zmiana jest odpowiednio podpisana
- adres repozytorium zostaje podmieniony na
osobiste.github.com- odpowiednia para kluczy może być zidentyfikowana w konfiguracji SSH, a finalnie i tak zostanie użyty poprawny adres pobrany z polaHostName
Od teraz każde nowe repozytorium powinno konfigurować się samodzielnie, a obsłużenie nowego dostawcy czy tożsamości jest całkiem proste.
Aliasy. Konfiguracja Gita pozwala na tworzenie własnych komend Gita. Stworzenie aliasu jest bardzo proste - polega na dodaniu do konfiguracji w sekcji [alias] pary alias = polecenie. Dla poleceń Git wystarczy podać samo polecenie (co), ale można też używać komend powłoki (ojesu) lub nawet odpalać skrypty (nuke, check):
# ~/.gitconfig
[alias]
co = checkout
ojesu = !find . -type f -name "*.js" | xargs wc -l | sort -nr | head -10
nuke = !~/.gitconfig_aliases.sh nuke
check = !~/.gitconfig_aliases.sh check
Rezultat:
% git ojesu
5506 total
3201 ./static/js/searchElasticlunr.js
1760 ./static/js/mermaid.js
270 ./static/js/count.js
103 ./static/js/codeblock.js
82 ./static/js/themetoggle.js
49 ./static/js/toc.js
25 ./static/js/main.js
14 ./static/js/note.js
1 ./static/js/searchElasticlunr.min.js
W gratisie dorzucam plik .gitconfig_aliases.sh, w którym napisałem dwie funkcje użyte w przykładowych aliasach. Pierwsza z nich przegląda wszystkie katalogi w miejscu uruchomienia, sprawdza, czy są tam repozytoria Git i wyświetla informacje z konfiguracji. Tego aliasu używałem bardzo często podczas pracy nad przedmiotem tego wpisu. Drugi alias to tak ugrzeczniony git clean --dfx, który automatycznie działa w obrębie danego folderu i zawsze pyta o pozwolenie. Ułatwia kasowanie plików z .gitignore czy pustych folderów, których nie widać w git diff. Plik skryptu trzeba potraktować chmod +x, żeby aliasy mogły go użyć.
#!/usr/bin/env zsh
check() {
for dir in */; do
[[ -d "$dir/.git" ]] || continue
{
echo "$dir"
git -C "$dir" config --get remote.origin.url
git -C "$dir" config --get user.email
git -C "$dir" config --get user.name
} | paste -sd $'\t' -
done | column -t
}
nuke() {
[[ -n $GIT_PREFIX ]] && echo GIT_PREFIX=$GIT_PREFIX
files=$(git clean -ndfx -- $GIT_PREFIX | tee /dev/tty)
[[ -z $files ]] && echo "Nothing to nuke" && exit
read -n 1 -p "Proceed? (yY) " proceed
echo
[[ $proceed == [yY] ]] && git clean -dfx -- $GIT_PREFIX
}
"$@"