Jeg har slutta med git

Glenn Bitar

Jeg har slutta med git og begynt å bruke jj.

Jeg brukte git for første gang i 2013. Jeg tror iallefall det, fordi mitt første repo på GitHub ble oppretta 19. mars det året. Repoet består av C#-kode for et gruppeprosjekt gjennomført mitt andre semester ved Høgskolen i Telemark. Repoet har to commits, så jeg tror ikke jeg kunne så mye mer enn git commit og git push på den tida. Jeg ville bare lagre koden et sted, og GitHub var stedet for kode.

Skjermdump av en commit-historikk med to commits.

For å finne den datoen så bladde jeg gjennom GitHub-repoene mine sortert etter opprettelsesdato, og det vekka en del gamle minner. Stort sett om skoleoppgaver, tutorials og spede ideer som aldri ble noe av. Git har vært med omtrent hele veien, men det tok lang tid før jeg ble god. For git er vanskelig. Det finnes veldig mye læremateriale om git, verktøy for opplæring, generelle guider, utallige Stack Overflow-spørsmål om hvordan man kommer seg ut av vriene floker som oppsto når man skulle bare. Please help me unfuck my git situation er jo en klasse av spørsmål som stadig går (gikk?) igjen. Og vennligst uten å miste data. Jeg har vært der mange ganger. Den enkle løsninga er ofte å kopiere kildekoden i en annen mappe, slette repoet hardt og klone på nytt, deretter lime tilbake filene. Eller om de endringene som du mister ikke er så viktige: git reset --hard origin/master :(. Skulle ønske Elisabeths mange guider på denne bloggen om å forbedre git-livet fantes for 13 år siden. Men det er ikke til å stikke under en stol at git er vanskelig.

Git er vanskelig

For å forstå git, må man forstå abstraksjonene den bygger på. Git er jo et distribuert versjonskontrollsystem, så abstraksjonene finnes flere plasser. Lokalt, samt på den sentrale serveren[1] man typisk bruker for å samarbeide. Lokalt har man jo selvfølgelig commits, som på en måte er grunnenheten som beskriver endring av filene i repoet. I tillegg har man branches. Disse peker til commits, og da typisk til løvnoden i grafen av commits repoet består av (men ikke alltid). Når man bruker git er det også slik at man typisk jobber i en branch, og når man committer, følger branchen med. Dvs. at branchen automatisk peker til den committen man nettopp laget. I tillegg til disse konseptene, har man index, også kjent som staging, samt working directory. Working directory er rett og slett filene i filsystemet du jobber mot. Indexen er endringene som ligger an til å bli med i neste commit, via git add. Man har også en spesiell peker som heter HEAD, som peker til hvor man befinner seg i git-grafen akkurat nå. Serveren du jobber mot har kopier av dine (og andres) commits og branches, men ikke index eller working directory. Serveren er registrert som en remote.

Diagram som viser konsepter og operasjoner i git

Ofte ser man slike illustrasjoner som skal hjelpe til med å forstå alle konseptene. Og man tenker jo at det er en grunn til at git er såpass komplisert. Sånn må det være. Men må det egentlig det?

jj

Da jeg først hørte om jujutsu (eller jj; jeg sier jayjay) synes jeg det var interessant med et nytt forsøk på versjonskontroll, som samtidig var helt kompatibel med git. Derimot trodde jeg aldri jeg kom til å ta det i bruk, fordi jeg var så produktiv med git. Jeg har jo lært meg såpass at jeg kan skrive om historikken, unfucke de fleste situasjoner jeg og kollegaene mine kan havne i, og stort sett gjøre hva jeg vil. Å lære seg et nytt system ville vært vanskelig og ikke gitt så mye gevinst. Men det var så mange som skrev om at jj på samme tid var enklere og mer kapabelt enn git.[2] Noen mente også det er mye lettere for nybegynnere å lære seg jj enn git. Det er verdt å sjekke ut.

Etter flere forsøk, og nå etter kun å ha brukt jj i flere måneder, kommer jeg aldri til å gå tilbake. Ting jeg kommer på som er annerledes (og i hovedsak bedre) med jj:

Alt er en commit

  • Working directory? Det er en commit (det heter working copy i jj).
  • Index? Finnes ikke, alt du har i repoet ditt blir automatisk snapshottet til working copy (som er en commit).
  • HEAD? Finnes ikke. Man bruker bare working copy.

Dette har overraskende store konsekvenser. Og det gjør ting enklere. Si at du jobber med en endring i en branch, men så må du fikse noe pga. en PR review i en annen branch. Med git så var det viktig at du enten stashet endringene dine, ev. staget og committet dem til en work in progress commit, så du ikke mistet dem, før du git switch-et deg til den andre branchen. Med jj? jj edit other-branch (ev. jj new other-branch) -- og vipps så jobber du med den andre branchen. Hva skjedde med det du jobba med? jj snapshottet endringene dine til working copy. Og working copy er jo en commit som blir lagret. Ikke noe stashing eller wip. Alt er jo commits, så det tas vare på.

Konflikter tas også vare på

Selv om man er i stand til å fikse konflikter i git, så er det fremdeles plagsomt at man må gjøre det nå med en gang før man foretar seg noe annet. I jj er konflikter first class citizens, og lagres bare som alle andre endringer. Man kan fikse dem senere.

Commits har stabile referanser

Siden gits commit-referanser er basert på innholdet (kalt content addressable), så endres referansen idet man endrer på filer i committen. Det vil si at dersom du ønsker å skrive om på commit d1b2f3, så får den en ny adresse idet du endrer. Man kan ikke lenger referere til endringa med d1b2f3. I jj har alle commits en stabil referanse. Det vil si at om du ønsker å endre på commit kzyl (f.eks. jj describe kzyl -m 'Fixer upper' for å endre commit-meldinga), så heter committen fremdeles kzyl. Dette har også overraskende store konsekvenser. F.eks. at brancher blir nesten helt unødvendige. Du kan bare jj new kzyl for å jobbe med en endring basert på den committen. Nå for tiden lager jeg branch-navnet idet jeg pusher til GitHub, ellers bruker jeg dem aldri.

Brancher er bare pekere

I git så er en branch en peker til en commit, men det er også en del automatikk rundt dem. F.eks. flyttes en branch til committen man lager automatisk. Det er også slik at dersom man ikke er en branch (les: HEAD), så er man i en såkalt detached tilstand. I jj er branches (det heter forresten bookmarks i jj) bare pekere. Man trenger ikke være på en branch. Man setter working copyen sin på en commit (man kan fremdeles bruke et bookmark-navn som en referanse til en commit) ved hjelp av jj edit eller jj new, også jobber man. Ingen detached HEAD state. Dette er mindre komplisert, men har en negativ konsekvens: I git flyttes branchen automatisk idet man git commit-er. I jj må man flytte branchen selv før man pusher til GitHub.

Historien skriver seg om av seg selv

Lyst til å pynte på historikken til PR-en du jobber med? Snike inn en test før en refaktorering, men etter en annen? jj new --before qwrt --after prtq. Da har man en ny commit mellom qwrt og prtq. Alle commits som kommer etter blir automatisk skrevet om når man gjør endringer i working copy. Det samme gjelder ved rebases, squashes, og alt slik. Og forresten, så har man to ortogonale operasjoner som hjelper til med dette:

  • jj squash: flytter endringer (altså endringer i filer, ikke commits) fra én commit til en annen
  • jj rebase: flytter commits. Her kan man flytte én commit (-r pqr), en hel branch av commits (-b pqr, tar med commits før og etter som er i samme branch), alle commits i en branch fra og med en commit (-s pqr, tar med pqr og alt etter, men ikke før) hvor som helst i commit-grafen din.

Innebygget ooops CTRL+Z

jj undo fikser så å si alt du skulle ønske du ikke gjorde. Sletta en commit? jj undo. Rebasa helt feil? jj undo. Og ønsker du deg langt tilbake, finner du med overveiende sannsynlighet akkurat den hjelpen du trenger i jj op log. Denne er gull verdt.

Et rikt språk for valg av commits

jj log -r <revs> m.fl. aksepterer et uttrykk basert på jjs revset language. Istedenfor å forklare, kommer det her noen eksempler.

Se alle commits som har conventional commit-prefixen fix:

jj log -r 'subject(regex:"^fix!?:")'

Finn commits fra forfatter ved navn Anders, men ikke fra Anders And:

jj log -r 'author(substring:"Anders") ~ author(substring:"Anders And")'

Dette er et kraftig verktøy som også kan ha kirurgisk presisjon.

Hva så?

La oss se på en tilsvarende figur som den for git, bare for jj:

Diagram som viser konsepter og operasjoner i jj

Dette viser en enklere modell. Man må forstå abstraksjonene, men i jj er det færre av dem. Likevel oppleves verktøyet som mer kapabelt. Jeg anbefaler deg som ny eller gammel utvikler å prøve.

Verktøy

I tillegg til selve jj-CLI-verktøyet, som i seg selv har mange quality-of-life-forbedringer ift. git (innebygget pager, noen enkle TUIs for interaktive endringer, osv), finnes det en haug av verktøy for å gjøre jj-hverdagen enklere.

  • jjui: Interagér med jj via TUI framfor CLI-et
  • jj-spr: Verktøy for jj + GitHub arbeidsflyter
  • stakk: Interaktiv TUI som gjør det lett å lage stacked pull-requests mot GitHub (disclaimer: jeg har laget dette, og bruker det hver dag)

Allikevel anbefaler jeg å bli iallefall litt kjent med jj-CLI-et i seg selv. APT har #fag-jj-kanalen på Slack (internt), fint med mer aktivitet der!

PS: Jeg skrev en oppfølging med konkrete eksempler: Ta kontroll over historikken med jj


  1. Jeg synes også dette er ironisk. ↩︎

  2. Les jj-vcs/jj: A Git-compatible VCS that is both simple and powerful [hn], Steve's Jujutsu Tutorial [hn], Jujutsu (jj), a git compatible VCS [hn] ↩︎