Git dla admina
Po tej lekcji: kursant trzyma swoje skrypty i konfigurację w Gicie: robi commity, czyta historię i różnice, używa gałęzi, wysyła repozytorium na serwer, GitLaba lub GitHuba przez klucz SSH, a /etc pilnuje etckeeperem.
Wstęp
Poniedziałek rano. Na serwerze ktoś zmienił plik. Kto? Kiedy? Co dokładnie? I jak to było wcześniej? Cisza w kursie. Wszyscy byli w piątek „tylko na chwilę”.
A teraz ta sama scena na serwerze z Gitem:
cd /etc && sudo git log --oneline -3
sudo git diff HEAD~1 -- ssh/sshd_configDwie komendy. Masz autora, godzinę, opis i dokładnie tę jedną linijkę, która się zmieniła. Jedną komendą możesz ją cofnąć. Dziś Git, czyli wehikuł czasu dla plików tekstowych. Programiści używają go do kodu, a admin do skryptów i konfiguracji.
Git, czyli album ze zdjęciami Twojego biurka
Wyobraź sobie, że po każdej ważnej zmianie robisz zdjęcie całego biurka i podpisujesz je: „dodałem skrypt do kopii”, „zmieniłem próg alarmu”. Album rośnie, a Ty w każdej chwili możesz otworzyć dowolne zdjęcie i zobaczyć, jak było. To jest Git: system kontroli wersji. Zdjęcie z podpisem to commit. Album to repozytorium, w skrócie repo.
Jest jeszcze stolik przed aparatem. Nie fotografujesz wszystkiego, tylko to, co na niego położysz. Ten stolik to poczekalnia (staging, index). git add kładzie plik na stolik, git commit robi zdjęcie.
Instalacja: sudo apt install git [Ubuntu], sudo dnf install git [RHEL]. Zanim zrobisz pierwsze zdjęcie, przedstaw się, bo każde zdjęcie jest podpisane:
git config --global user.name "Eryk Admin"
git config --global user.email "eryk@example.com"
git config --global init.defaultBranch mainTo ląduje w ~/.gitconfig. Teraz repozytorium na skrypty:
mkdir skrypty && cd skrypty
git init
# tu wkładasz kopia.sh, skrypt z modułu o skryptach
git statusInitialized empty Git repository in /home/eryk/skrypty/.git/
On branch main
No commits yet
Untracked files:
(use "git add <file>..." to include in what will be committed)
kopia.shGit widzi plik, ale go nie pilnuje. „Untracked” to gość bez przepustki. Kładziemy go na stolik i robimy zdjęcie:
git add kopia.sh
git commit -m "Dodaj skrypt kopia.sh"
git log --oneline[main (root-commit) 3c38898] Dodaj skrypt kopia.sh
1 file changed, 5 insertions(+)
create mode 100755 kopia.sh
3c38898 Dodaj skrypt kopia.shTen dziwny 3c38898 to początek identyfikatora commita. Zauważ też 100755: Git pamięta, że plik jest wykonywalny. Prawa właściciela i grupy już nie, ale o tym przy etckeeperze.
diff, czyli „znajdź różnicę” z gazety
Zmieniam skrypt: kopia ma zachować daty pliku i wypisać komunikat. Zanim zrobię zdjęcie, pytam Gita, co się zmieniło:
git diff@@ -2,4 +2,5 @@
# kopia.sh - kopia pliku z dzisiejszą datą w nazwie
set -euo pipefail
plik=${1:?Podaj nazwę pliku}
-cp -- "$plik" "$plik.$(date +%F)"
+cp -p -- "$plik" "$plik.$(date +%F)"
+echo "Kopia gotowa: $plik.$(date +%F)"Minus to linijka, która wyleciała, plus to linijka, która przyszła. Jak zabawa „znajdź pięć różnic”, tylko Git od razu zakreśla je na czerwono i zielono. git diff pokazuje zmiany jeszcze nieodłożone na stolik, git diff --staged te, które już na nim leżą, a git log -p całą historię razem ze zmianami.
.gitignore, czyli lista rzeczy, których nie fotografujemy
W folderze ze skryptami leży plik .env z hasłem i kopia.log. Nie chcesz ich w albumie. Hasło w repozytorium to jak zdjęcie karty płatniczej wrzucone na rodzinną grupę: niby swoi, ale jednak.
printf '%s\n' '.env' '*.log' '*.tmp' > .gitignore
git status --short M kopia.sh
?? .gitignore.env i kopia.log zniknęły z listy. M to plik zmieniony, ?? nowy. Sam .gitignore commitujesz, żeby każdy, kto sklonuje repo, miał tę samą listę.
git add .gitignore kopia.sh
git commit -m "kopia.sh: zachowaj daty i wypisz komunikat; dodaj .gitignore"Dobry opis commita mówi, co i po co. „poprawki” albo „asdf” za pół roku nie powie Ci nic. A za pół roku czytasz to Ty, tylko bardziej zmęczony.
Gałęzie, czyli brudnopis obok czystopisu
Chcesz dopisać nowy skrypt, ale nie chcesz psuć tego, co działa. Bierzesz brudnopis. W Gicie to gałąź (branch).
git switch -c dysk # nowa gałąź i od razu na nią
# tu powstaje dysk.sh
shellcheck dysk.sh
git add dysk.sh
git commit -m "Dodaj dysk.sh: alarm przy zajętości dysku"
git branch* dysk
mainGwiazdka to gałąź, na której stoisz. Wracasz na main i robisz ls: pliku dysk.sh nie ma. Nie panikuj, żyje w brudnopisie. Kiedy jest gotowy, przepisujesz go do czystopisu, czyli scalasz (merge):
git switch main
git merge dysk
git log --oneline --graph --allUpdating ae077a9..ded0042
Fast-forward
dysk.sh | 10 ++++++++++
* ded0042 Dodaj dysk.sh: alarm przy zajętości dysku
* ae077a9 kopia.sh: zachowaj daty i wypisz komunikat; dodaj .gitignore
* 3c38898 Dodaj skrypt kopia.sh„Fast-forward” znaczy, że na main nic się w międzyczasie nie działo, więc Git tylko przesunął zakładkę do przodu. Starsze poradniki zamiast git switch używają git checkout. Działa tak samo, switch jest po prostu czytelniejszy.
Repozytorium na serwerze: clone, push, pull
Album na jednym laptopie to dalej jeden laptop. Kopia albumu w sejfie w innym budynku to zdalne repozytorium (remote). Najprostsze: goły katalog na Twoim serwerze, dostępny po SSH, kluczem z modułu o SSH. „Goły” (bare) znaczy: sam album, bez biurka z plikami.
ssh web1 'sudo mkdir -p /srv/git && sudo chown "$USER": /srv/git'
ssh web1 'git init --bare -b main /srv/git/skrypty.git'
git remote add origin web1:/srv/git/skrypty.git
git push -u origin mainTo web1:/srv/git/skrypty.git
* [new branch] main -> main
branch 'main' set up to track 'origin/main'.origin to tylko przezwisko adresu. push wysyła Twoje commity do sejfu. Kolega albo drugi serwer bierze kopię przez clone, coś zmienia i wysyła:
git clone web1:/srv/git/skrypty.git
cd skrypty
# zmiana progu w dysk.sh
git commit -am "dysk.sh: domyślny próg 85%"
git push-a przy commicie sam kładzie na stolik wszystkie zmienione pliki, które Git już zna. Nowych nie. A Ty u siebie ściągasz to przez pull:
From web1:/srv/git/skrypty
ded0042..5f4a84f main -> origin/main
Updating ded0042..5f4a84f
Fast-forward
dysk.sh | 2 +-Klucz SSH do GitLaba i GitHuba
GitLab i GitHub to takie sejfy w chmurze z ładną stroną WWW. GitHub od 13 sierpnia 2021 roku nie przyjmuje hasła do konta przy operacjach Git z terminala, a GitLab nie przyjmie go, gdy masz włączone logowanie dwuskładnikowe. Więc idziesz po kluczu. Zasada jak w module o SSH: kłódka na serwerze, kluczyk w kieszeni. Tylko tym razem kłódkę wieszasz na swoim profilu.
ssh-keygen -t ed25519 -C "eryk@laptop-github"
cat ~/.ssh/id_ed25519.pubGenerating public/private ed25519 key pair.
...
The key fingerprint is:
SHA256:V7SwnRDjqGljyju9Sp8YoN0Gso9uhyff3mUhHbSdIGM eryk@laptop-githubDo przeglądarki kopiujesz wyłącznie linijkę z pliku .pub. W GitHubie: Settings, SSH and GPG keys, New SSH key. W GitLabie: ikonka profilu, Preferences (Edit profile), SSH Keys. Potem test:
ssh -T git@github.com
ssh -T git@gitlab.comOba serwisy odpowiadają powitaniem z Twoją nazwą użytkownika i nie dają powłoki, bo to nie jest serwer do logowania, tylko sejf. Od tej chwili adresy repozytoriów wybierasz w wersji SSH, na przykład git@github.com:eryk/skrypty.git.
Prywatny klucz, plik bez .pub, nie wychodzi z Twojego komputera. Nigdy. Ani do repozytorium, ani na czat, ani „na chwilę na pendrive”.
etckeeper, czyli Git, który sam pilnuje /etc
Skrypty commitujesz sam. Ale /etc zmienia się też za Twoimi plecami: instalujesz pakiet, a ten dorzuca swój plik. etckeeper wkłada /etc do repozytorium Gita i robi commity za Ciebie: przed i po każdym apt, plus raz dziennie.
sudo apt install etckeeper # [Ubuntu] od razu robi init i pierwszy commit
sudo dnf install etckeeper # [RHEL] z repozytorium EPEL, potem:
sudo etckeeper init && sudo etckeeper commit "Stan początkowy"[RHEL] Automatyczne commity przy dnf daje osobna wtyczka; sprawdź dnf search etckeeper, zanim uznasz, że działa. Teraz ktoś zmienia konfigurację SSH:
sudo etckeeper unclean && echo "są niezapisane zmiany"
cd /etc && sudo git diff
sudo etckeeper commit "SSH: zakaz logowania roota"
sudo git log --onelinesą niezapisane zmiany
-#PermitRootLogin prohibit-password
+PermitRootLogin no
e5d0b3b SSH: zakaz logowania roota
144f9d7 Stan początkowyPo apt install w historii pojawi się commit w stylu committing changes in /etc made by "apt install htop". Masz pełną kronikę serwera bez pisania ani jednego słowa.
Czemu nie zwykły git init w /etc? Bo Git nie pamięta właściciela, grupy ani większości uprawnień. etckeeper zapisuje je w pliku /etc/.etckeeper, a u mnie są tam linijki takie jak maybe chmod 0640 'shadow'. Przy okazji: w tym repozytorium leży /etc/shadow z hashami haseł. Dlatego /etc/.git ma prawa 700, a takie repo wysyłasz co najwyżej na własny serwer po SSH. Na publicznego GitHuba nigdy.
Jak to działa pod spodem
Cały album siedzi w ukrytym folderze .git. Zajrzyjmy:
cat .git/HEAD
cat .git/refs/heads/main
git cat-file -p HEAD~1 | head -4ref: refs/heads/main
1c64ba00cf4cae9b8d21bf332db2c713bf55e84a
tree 7d076209c8e6a419df27a5e5d6900623bd89c914
parent 05a9ea031032212e50371aaf45df7ae0f41a65d4
parent bc2272ca2b151781958c46a47b350e4ca8b5b9f2
author Eryk Admin <eryk@example.com> 1793741612 +0100Gałąź to plik z jednym numerem w środku. Tak, cały „brudnopis” to 41 bajtów. HEAD to karteczka „jesteś tutaj”. Commit to mały tekst: wskazuje drzewo plików (tree), rodzica (parent) i autora. Ten akurat ma dwóch rodziców, bo to scalenie. Numer commita to skrót SHA-1 z jego zawartości, więc zmiana choćby przecinka w historii zmienia wszystkie numery dalej. Dlatego historii nie da się po cichu podmienić.
Każda wersja pliku to osobny obiekt w .git/objects. Git trzyma całe pliki, nie same różnice, a diff liczy w locie. Na dysku oszczędza kompresją i paczkami.
Ściąga flag
| Komenda | Flaga | Co robi |
|---|---|---|
git config | --global user.name/user.email | podpis pod commitami |
git init | --bare -b main | goły album na serwerze, gałąź main |
git status | --short | krótko: M zmieniony, A dodany, ?? nowy |
git add | -p | dodawaj zmiany kawałkami, z pytaniem |
git commit | -m "opis" | commit z opisem |
git commit | -a | najpierw dodaj wszystkie znane zmienione pliki |
git log | --oneline --graph --all | historia w skrócie, z gałęziami |
git log | -p -- PLIK | historia jednego pliku ze zmianami |
git diff | --staged | różnice na stoliku |
git switch | -c NAZWA | nowa gałąź i przejście na nią |
git merge | --ff-only | scal tylko, jeśli to przewinięcie |
git push | -u origin main | wyślij i zapamiętaj, dokąd |
git pull | --ff-only / --no-rebase | ściągnij bez niespodzianek / ze scaleniem |
git restore | PLIK | wyrzuć niezapisane zmiany w pliku |
etckeeper | unclean | kod 0, gdy w /etc są niezapisane zmiany |
etckeeper | commit "opis" | commit całego /etc z uprawnieniami |
⚠ Typowy błąd
Pierwszy commit na nowym serwerze:
Author identity unknown
*** Please tell me who you are.
...
fatal: unable to auto-detect email address (got 'root@vm.(none)')Git nie zrobi zdjęcia bez podpisu. Naprawa: dwie linijki git config --global z początku lekcji.
Drugi klasyk, groźniejszy. Wrzuciłeś hasło do repozytorium, zauważyłeś, robisz git rm --cached baza.conf, dopisujesz go do .gitignore, commit. Ulga? A teraz:
git log -p -S 'DB_PASS' --format='%h %s'8307e22 usun haslo z repo
-DB_PASS=Lato2026!
53ac37a konfiguracja bazy
+DB_PASS=Lato2026!Hasło dalej jest w albumie, na starym zdjęciu. Każdy, kto sklonuje repo, je zobaczy. Usunięcie pliku nie usuwa historii. Jedyna pewna naprawa: zmień to hasło. Czyszczenie historii specjalnymi narzędziami to dopiero drugi krok, bo kopie mogą już krążyć. Przy okazji, trzeci znajomy: przy git pull, gdy Ty i kolega zmieniliście coś jednocześnie, Git odpowie fatal: Need to specify how to reconcile divergent branches. Wybierz git pull --no-rebase (scalenie) albo ustaw to raz na stałe przez git config --global pull.rebase false.
✎ Zadanie
Załóż repozytorium skrypty, wrzuć do niego dwa swoje skrypty z kursu i zrób .gitignore na *.log i .env. Zrób gałąź poprawki, zmień w niej jeden skrypt, scal do main i pokaż git log --oneline --graph. Na maszynie wirtualnej utwórz gołe repozytorium w /srv/git/skrypty.git, wypchnij do niego main przez SSH, sklonuj w innym folderze i sprawdź, że pliki są. Na koniec zainstaluj etckeeper, zmień coś w /etc/hosts i znajdź tę zmianę w git log -p.
Pokaż rozwiązanie
Repozytorium, .gitignore, gałąź i scalenie:
mkdir skrypty && cd skrypty && git init
cp ~/kurs/kopia.sh ~/kurs/dysk.sh .
printf '%s\n' '*.log' '.env' > .gitignore
git add . && git commit -m "Pierwsze skrypty i .gitignore"
git switch -c poprawki
# zmiana w dysk.sh
git commit -am "dysk.sh: próg z argumentu"
git switch main && git merge poprawki
git log --oneline --graphGołe repozytorium na maszynie wirtualnej i klon w innym miejscu:
ssh vm 'sudo mkdir -p /srv/git && sudo chown "$USER": /srv/git && git init --bare -b main /srv/git/skrypty.git'
git remote add origin vm:/srv/git/skrypty.git
git push -u origin main
git clone vm:/srv/git/skrypty.git /tmp/test-klonu && ls /tmp/test-klonuI etckeeper:
sudo apt install etckeeper # [RHEL] dnf z EPEL + etckeeper init
echo '192.168.122.20 web2' | sudo tee -a /etc/hosts
sudo etckeeper commit "hosts: web2"
sudo git -C /etc log -p -1Jeśli w git log -p zobaczyłeś zielony plusik przy web2, wszystko gra. Jeśli zamiast tego zobaczyłeś Author identity unknown, to Git po raz drugi prosi, żebyś się przedstawił. Grzecznie, ale stanowczo.
Co dalej
Masz już repozytorium ze skryptami i klucz SSH. W następnej lekcji jeden laptop, dziesięć serwerów: pętla po serwerach przez SSH, porządny ~/.ssh/config i uruchamianie komend na wszystkich naraz, równolegle.