Git-ul este o unealtă foarte utilă pentru orice programator întrucât vă oferă toatea avantajele unui tool de versionare. Mai exact, puteți reveni la o formă precedentă a proiectului vostru sau puteți să aveți o formă securizată a proiectului în cloud pe care o puteți descărca pe alt device cu foarte mare ușurință.
Pentru a învăța cum să folosiți tool-ul Git vă punem la dispoziție acest tutorial scurt despre funcționalitățile lui de baza.
În următoarele secțiuni vă vom explica anumite comenzi folosindu-ne de un repository demo.
Pentru a ne putea folosi de toate comenzile puse la dispozitie de tool-ul Git, trebuie mai întâi să creăm un repository care va reprezenta proiectul nostru.
După ce vă faceți cont pe GitHub puteți să apăsați butonul “Create a new repository” pentru a începe configurarea unui repo.
Conform pozei de mai sus, avem următoarele opțiuni:
După ce v-ați ales setările dorite, putem crea repository-ul care va arăta la început astfel:
În repo-ul creat avem următoarele secțiuni:
Pe lângă aceste secțiuni avem și câteva etichete care ne arată metrici ale repo-ului:
După ce creați un repository pe GitHub-ul vostru personal va trebui să îl clonați și local pentru a putea face modificări în acesta. Se pot modifica fișiere și din interfața grafică provizionată de GitHub, dar aceste schimbări din GUI au o funcționalitate limitată, din acest motiv vă recomandăm să vă clonați repo-ul local în cazul în care vreți să faceți schimbări mai ample.
Pentru a clona un repo, folosim comanda “git clone adresa_https_repo [cale_repo]“.
student@student:~$ git clone https://github.com/username/Test-Git.git remote: Enumerating objects: 5, done. remote: Counting objects: 100% (5/5), done. remote: Compressing objects: 100% (4/4), done. remote: Total 5 (delta 0), reused 0 (delta 0), pack-reused 0 Receiving objects: 100% (5/5), 4.73 KiB | 4.73 MiB/s, done.
Există posibilitatea să vă faceți un repo direct din folder-ul din care lucrați folosind comanda “git init”.
student@student:~$ git init hint: Using 'master' as the name for the initial branch. This default branch name hint: is subject to change. To configure the initial branch name to use in all hint: of your new repositories, which will suppress this warning, call: hint: hint: git config --global init.defaultBranch <name> hint: hint: Names commonly chosen instead of 'master' are 'main', 'trunk' and hint: 'development'. The just-created branch can be renamed via this command: hint: hint: git branch -m <name>
Dacă alegeți să faceți un repository local folosind comanda “git init”, va fi nevoie să îl legați pe acesta la un repository din cloud pentru a putea avea proiectul vostru sincronizat și disponibil pe mai multe device-uri. Înainte să începeți procesul de legare, trebuie să vă creați un repository, urmând ca apoi să rulați comenzile de mai jos cu URL-ul repository-ului creat de voi.
student@student:~$ git remote add origin https://github.com/github_id/test_repo.git
Pentru a explica mai bine ce face comanda de mai sus o vom sparge în mai multe bucăți:
După ce am creat conexiunea către repository-ul nostru din cloud, când dorim să aducem modificările locale în cloud (acțiune asociată comenzii “git push”) va trebui să specificăm care este branch-ul nostru local main (sau master) și care este branch-ul repository-ului nostru main (sau master). După ce “conectăm” branch-urile noastre, putem rula direct comanda “git push” pentru a trimite modificările noastre și în cloud. Pentru mai multe detalii legate de comanda “git push” și despre “branch-uri” vă rugăm să citiți secțiunile de mai jos.
student@student:~$ git push --set-upstream origin master Enumerating objects: 3, done. Counting objects: 100% (3/3), done. Writing objects: 100% (3/3), 205 bytes | 205.00 KiB/s, done. Total 3 (delta 0), reused 0 (delta 0), pack-reused 0 To https://github.com/luis6156/wow.git * [new branch] master -> master branch 'master' set up to track 'origin/master'.
Commit-urile reprezintă milestone-uri pe care le-au atins modificările făcute de voi. Într-un commit puteți avea mai multe schimbări adăugate cum ar fi redenumiri, ștergeri sau adăugări de fișiere.
Să presupunem că am creat o nouă clasă “Engine” pe care vrem să o avem valabilă și în regim remote.
class Engine { private int pistons; private String shape; public Engine(int pistons, String shape) { this.pistons = pistons; this.shape = shape; } public void turnEngineOn() { System.out.println("Engine started."); } public void turnEngineOff() { System.out.println("Engine shut down."); } }
Tot ce trebuie să facem este să selectăm fișierele care reprezintă milestone-ul nostru folosind comanda “git add nume_fișier” sau “git add .” dacă vrem să adăugăm recursiv toate fișierele de la calea curentă.
student@student:~$ git status On branch main Your branch is up to date with 'origin/main'. Untracked files: (use "git add <file>..." to include in what will be committed) Engine.java nothing added to commit but untracked files present (use "git add" to track) student@student:~$ git add Engine.java student@student:~$ git status On branch main Your branch is up to date with 'origin/main'. Changes to be committed: (use "git restore --staged <file>..." to unstage) new file: Engine.java
Acum avem fișierul selectat pentru commit-ul pe care vrem să îl creăm, deci vom rula comanda «git commit -m “mesaj”».
student@student:~$ git commit -m "added Engine class" [main a339de3] added Engine class 1 file changed, 17 insertions(+) create mode 100644 Engine.java student@student:~$ git status On branch main Your branch is ahead of 'origin/main' by 1 commit. (use "git push" to publish your local commits) nothing to commit, working tree clean
Toate modificările pe care le-ați făcut până acum au fost doar locale, dacă vrem ca aceste modificări să fie disponibile în Cloud trebuie să le “împingem” și pe remote folosind comanda “git push”.
student@student:~$ git push Enumerating objects: 4, done. Counting objects: 100% (4/4), done. Delta compression using up to 10 threads Compressing objects: 100% (3/3), done. Writing objects: 100% (3/3), 496 bytes | 496.00 KiB/s, done. Total 3 (delta 0), reused 0 (delta 0), pack-reused 0 To https://github.com/luis6156/Test-Git.git 3499c09..a339de3 main -> main student@student:~$ git status On branch main Your branch is up to date with 'origin/main'. nothing to commit, working tree clean
Acum modificările noastre apar și pe GitHub.
Pentru ca ceilalți membrii ai echipei să poată vedea aceste modificări și local trebuie să “tragă” informațiile de pe remote folosind comanda “git pull”.
student@student:~$ git pull Already up to date.
Dacă faceți o greșeală în momentul în care adăugați un fișier folosind comanda “git add nume_fișier” puteți folosi comanda “git restore --staged nume_fișier”.
Să presupunem că am adăugat din greșeală clasa “InlineEngine”, putem face restore astfel:
class InlineEngine extends Engine { private Boolean isInverted; public InlineEngine(int pistons, String shape, Boolean isInverted) { super(pistons, shape); this.isInverted = isInverted; } public Boolean getIsInverted() { return isInverted; } }
student@student:~$ git add InlineEngine.java student@student:~$ git status On branch main Your branch is up to date with 'origin/main'. Changes to be committed: (use "git restore --staged <file>..." to unstage) new file: InlineEngine.java student@student:~$ git restore --staged InlineEngine.java student@student:~$ git status On branch main Your branch is up to date with 'origin/main'. Untracked files: (use "git add <file>..." to include in what will be committed) InlineEngine.java nothing added to commit but untracked files present (use "git add" to track)
Mai există un caz în care putem folosi restore. Să presupunem că am făcut o schimbare în clasa “Engine”.
class Engine { private int pistons; private String shape; public Engine(int pistons, String shape) { this.pistons = pistons; this.shape = shape; } public void turnEngineOn() { System.out.println("Engine started."); } public void turnEngineOff() { System.out.println("Engine shut down."); } // NEW CODE ADDED HERE, PLS DELETE AFTER DEMO public Boolean helloBrother() { System.out.println("Hello brother, you may rest here."); } }
Dacă vrem să revenim la ultima variantă a codului prezentă pe remote putem să folosim comanda “git restore nume_fișier”.
student@student:~$ git status On branch main Your branch is up to date with 'origin/main'. Changes not staged for commit: (use "git add <file>..." to update what will be committed) (use "git restore <file>..." to discard changes in working directory) modified: Engine.java no changes added to commit (use "git add" and/or "git commit -a") student@student:~$ git restore Engine.java student@student:~$ git status On branch main Your branch is up to date with 'origin/main'. student@student:~$ cat Engine.java class Engine { private int pistons; private String shape; public Engine(int pistons, String shape) { this.pistons = pistons; this.shape = shape; } public void turnEngineOn() { System.out.println("Engine started."); } public void turnEngineOff() { System.out.println("Engine shut down."); } }
Există situații în care vreți să reveniți la o formă precedentă a fișierului vostru, dar fără să vă pierdeți modificările curente. Pentru acest caz avem comanda “git stash” care ne permite sa reținem modificările noastre într-o stivă pe care o putem accesa oricând este nevoie.
Să presupunem că avem următoarele modificări în fișierul noastru și vrem să îl convertim la forma lui precedentă:
class Engine { private int pistons; private String shape; public Engine(int pistons, String shape) { this.pistons = pistons; this.shape = shape; } public void turnEngineOn() { System.out.println("Engine started."); } public void turnEngineOff() { System.out.println("Engine shut down."); } // NEW METHOD ADDED HERE public void makeSound() { System.out.println("Vroooooom vroom"); } }
student@student:~$ git status On branch main Your branch is up to date with 'origin/main'. Changes not staged for commit: (use "git add <file>..." to update what will be committed) (use "git restore <file>..." to discard changes in working directory) modified: Engine.java no changes added to commit (use "git add" and/or "git commit -a") student@student:~$ git stash Saved working directory and index state WIP on main: a339de3 added Engine class student@student:~$ cat Engine.java class Engine { private int pistons; private String shape; public Engine(int pistons, String shape) { this.pistons = pistons; this.shape = shape; } public void turnEngineOn() { System.out.println("Engine started."); } public void turnEngineOff() { System.out.println("Engine shut down."); } }
Acum că avem fișierul la forma lui inițială, putem să readucem modificările în fișier folosind comanda “git stash pop”.
student@student:~$ git stash show Engine.java | 5 +++++ 1 file changed, 5 insertions(+) (END) student@student:~$ git stash pop On branch main Your branch is up to date with 'origin/main'. Changes not staged for commit: (use "git add <file>..." to update what will be committed) (use "git restore <file>..." to discard changes in working directory) modified: Engine.java no changes added to commit (use "git add" and/or "git commit -a") Dropped refs/stash@{0} (93e0db694a99d8293a707b9c0021fc71b7d621a1) student@student:~$ cat Engine.java class Engine { private int pistons; private String shape; public Engine(int pistons, String shape) { this.pistons = pistons; this.shape = shape; } public void turnEngineOn() { System.out.println("Engine started."); } public void turnEngineOff() { System.out.println("Engine shut down."); } // NEW METHOD ADDED HERE public void makeSound() { System.out.println("Vroooooom vroom"); } }
Branch-urile în Git încapsulează mai multe commit-uri, adică mai multe milestone-uri. Drept urmare, ele mai sunt denumite ca și snapshoot-uri. Într-un branch puteți să aveți încapsulat un singur feature sau bug fix, ceea ce vă permite să lucrați izolat la părți independente din proiect fără să afectați ramura principală, mai exact branch-ul “main”. În branch-ul default “main” e bine să aveți cod stabil si verificat de echipă, deoarece tot ce se află pe “main” este considerat “production ready”. În schimb, tot ce se află pe un branch diferit este considerat “experimental” sau “in development”.
Branch-urile pot fi create atât din GUI cât și din CLI.
Pentru a crea un branch din CLI avem comanda “git checkout -b nume_branch” care vă va și comuta pe acesta.
student@student:~$ git checkout -b "test_branch" Switched to a new branch 'test_branch' student@student:~$ git status On branch test_branch nothing to commit, working tree clean
Pentru a comuta pe un branch diferit putem folosi comanda “git switch nume_branch”.
student@student:~$ git branch main * test_branch (END) student@student:~$ git switch main Switched to branch 'main' Your branch is up to date with 'origin/main'. student@student:~$ git status On branch main Your branch is up to date with 'origin/main'. nothing to commit, working tree clean
Branch-urile pot fi de două tipuri: locale și remote. Cele locale sunt prezente doar pe mașina voastră, iar cele remote sunt prezente în cloud. Probabil ați observat că atunci când vă mutați pe branch-ul “main” aveți vă apare atât branch-ul “main” cât și branch-ul “origin/main”. Branch-urile prefixate de “origin/” reprezintă branch-uri remote. Ideal, un branch remote este sincronizat cu branch-ul local echivalent lui. Pentru comanda de mai sus noi am creat doar un branch local, dacă dorim ca branch-ul nostru să poată fi văzut și de ceilalți și să fie disponibil și în cloud, putem folosi comanda “git push –set-upstream origin nume_branch”.
student@student:~$ git status On branch test_branch nothing to commit, working tree clean student@student:~$ git push fatal: The current branch test_branch has no upstream branch. To push the current branch and set the remote as upstream, use git push --set-upstream origin test_branch To have this happen automatically for branches without a tracking upstream, see 'push.autoSetupRemote' in 'git help config'. student@student:~$ git push --set-upstream origin test_branch Total 0 (delta 0), reused 0 (delta 0), pack-reused 0 remote: remote: Create a pull request for 'test_branch' on GitHub by visiting: remote: https://github.com/luis6156/Test-Git/pull/new/test_branch remote: To https://github.com/luis6156/Test-Git.git * [new branch] test_branch -> test_branch branch 'test_branch' set up to track 'origin/test_branch'. student@student:~$ git status On branch test_branch Your branch is up to date with 'origin/test_branch'. nothing to commit, working tree clean
Acum branch-ul local “test_branch” este sincronizat cu branch-ul remote “origin/test_branch”. Putem verifica asta intrând pe GitHub.
Pentru a crea un branch din GUI, mai exact din GitHub, putem să scriem direct numele branch-ului dorit și să selectăm “create branch”.
Ca să avem branch-ul remote prezent și local va trebui să “tragem” ultimele modificări folosind comanda “git pull”.
student@student:~$ git pull From https://github.com/luis6156/Test-Git * [new branch] test_branch_gui -> origin/test_branch_gui Already up to date. student@student:~$ git switch test_branch_gui branch 'test_branch_gui' set up to track 'origin/test_branch_gui'. Switched to a new branch 'test_branch_gui' student@student:~$ git status On branch test_branch_gui Your branch is up to date with 'origin/test_branch_gui'. nothing to commit, working tree clean
Un “pull request” este în esență o verificare înainte de a se face aduce commit-urile de pe un branch pe branch-ul “main”. Rolul acestuia este de a verifica corectitudinea codului scris înainte de a fi introdus pe branch-ul default, deoarece acesta trebuie să conțină cod cât mai bun.
Să presupunem că avem un branch “test” pe care avem un fișier “test_file.txt”.
Conform pozei de mai sus, observăm un prompt de creare a unui “pull request”. În momentul în care se fac modificări pe un branch Git ne va oferi acel prompt pentru a aduce modificările și pe “main”.
După ce apăsăm pe prompt, putem să creăm un titlu sau o descriere pentru PR sau să adăugăm “reviewers” sau “assignees”.
În final avem și un panou dedicat unde putem să schimbăm setări sau să dăm review sau să vedem exact commit-uri/schimbări.
Comenzile “merge” și “rebase” sunt folosite pentru a aduce commit-urile de pe un branch sursă pe un branch destinație. Deși scopul este același, ele diferă în execuție. Pentru a înțelege diferența dintre aceste comenzi ne vom folosi de poza de mai sus, unde pe branch-ul “main” se află commit-urile “1”, “2” și “3”, iar pe branch-ul “feature” se află commit-urile “A” și “B”.
De asemenea, vom presupune că avem un branch “test_src” care conține fișierul “InlineEngine.java” pe care vrem să-l aducem pe branch-ul “test_dst”.
student@student:~$ git switch test_dst branch 'test_dst' set up to track 'origin/test_dst'. Switched to a new branch 'test_dst'
Comanda de “merge” ia commit-urile “A” și “B” și le combină într-un singur commit, mai exact commit-ul “4”, păstrând log-urile intacte.
Avantaje:
Dezavantaje:
Ca și exemplu, așa poate arăta un log plin de merge-uri:
Pentru a face “merge”, vom folosi comanda “git merge branch_src”.
student@student:~$ git merge test_src Updating a339de3..843f191 Fast-forward InlineEngine.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 InlineEngine.java student@student:~$ ls Engine.java InlineEngine.java LICENSE README.md
Comanda de “rebase” ia commit-urile “A” și “B” și le adaugă liniar la branch-ul “main”, mai exact se crează commit-urile “4” și “5”, alterând log-urile. Rebase a fost creat pentru a combate limitările comenzii “merge”, mai exact pentru a crea lizibilitate în log-uri.
Avantaje:
Dezavantaje:
Pentru a face “rebase”, vom folosi comanda “git rebase branch_dst”.
student@student:~$ git rebase test_src Successfully rebased and updated refs/heads/test_src. student@student:~$ ls Engine.java InlineEngine.java LICENSE README.md
Ca să înțelegem când să folosim merge/rebase este important să observăm că un developer nu poate vedea în urma unui rebase de unde vin commit-urile “A” și “B”. Deci putem să tragem concluzia că este mai bine să folosim “merge” când log-urile sunt importante și când alți developeri au acces la branch-ul sursă, în schimb folosim “rebase” când log-urile nu sunt importante și când alți developeri nu au acces la branch-ul sursă.
Atunci când dorim să facem o operație de “merge” sau “rebase” putem să avem conflicte între branch-ul sursă și branch-ul destinație. Acest conflict poate apărea în situația în care noi modificăm un fișier pe un branch și un coleg modifică diferit același fișier pe alt branch și apoi dorim să îmbinăm cele două branch-uri.
Pentru a ilustra și rezolva această situație o să presupunem că avem branch-urile “test_src” și “test_dst” în care modificăm fișierul “InlineEngine.java”.
În branch-ul “test_dst” avem:
class InlineEngine extends Engine { private Boolean isInverted; public InlineEngine(int pistons, String shape, Boolean isInverted) { super(pistons, shape); this.isInverted = isInverted; } public Boolean getIsInverted() { return isInverted; } // METODA ADAUGATA IN AMBELE BRANCH-URI CU MICI DIFERENTE public void methodFromDST() { System.out.println("BarFoo"); } }
În branch-ul “test_src” avem:
class InlineEngine extends Engine { private Boolean isInverted; public InlineEngine(int pistons, String shape, Boolean isInverted) { super(pistons, shape); this.isInverted = isInverted; } public Boolean getIsInverted() { return isInverted; } // METODA ADAUGATA IN AMBELE BRANCH-URI CU MICI DIFERENTE public void methodFromSRC() { System.out.println("FooBar"); } }
Acum dacă încercăm să facem merge în “test_dst”:
student@student:~$ git status On branch test_dst Your branch is up to date with 'origin/test_dst'. nothing to commit, working tree clean student@student:~$ git merge test_src Auto-merging InlineEngine.java CONFLICT (content): Merge conflict in InlineEngine.java Automatic merge failed; fix conflicts and then commit the result. student@student:~$ git status On branch test_dst Your branch is up to date with 'origin/test_dst'. You have unmerged paths. (fix conflicts and run "git commit") (use "git merge --abort" to abort the merge) Unmerged paths: (use "git add <file>..." to mark resolution) both modified: InlineEngine.java no changes added to commit (use "git add" and/or "git commit -a")
Git ne avertizează că au fost făcute modificări pe ambele branch-uri în același fișier și nu știe ce modificări să păstreze în urma operației de “merge”. Ca să rezolvăm această problemă trebuie să intrăm în fișierul problematic folosind orice editor text/IDE pentru a alege schimbările corecte.
student@student:~$ vim InlineEngine.java class InlineEngine extends Engine { private Boolean isInverted; public InlineEngine(int pistons, String shape, Boolean isInverted) { super(pistons, shape); this.isInverted = isInverted; } public Boolean getIsInverted() { return isInverted; } // METODA ADAUGATA IN AMBELE BRANCH-URI CU MICI DIFERENTE <<<<<<< HEAD public void methodFromDST() { System.out.println("BarFoo"); ======= public void methodFromSRC() { System.out.println("FooBar"); >>>>>>> test_src } }
Pentru a marca diferitele schimbări de pe ambele branch-uri “Git” a introdus niște delimitatori. De asemenea, acolo unde începe secțiunea “HEAD” avem textul care era deja prezent pe branch-ul curent (în acest caz “methodFromDST()”). Pentru a selecta segmentul corect, tot ce trebuie să facem este să ștergem delimitatorii și bucățile de cod care nu ne interesează.
class InlineEngine extends Engine { private Boolean isInverted; public InlineEngine(int pistons, String shape, Boolean isInverted) { super(pistons, shape); this.isInverted = isInverted; } public Boolean getIsInverted() { return isInverted; } // METODA ADAUGATA IN AMBELE BRANCH-URI CU MICI DIFERENTE public void methodFromSRC() { System.out.println("FooBar"); } } :wq student@student:~$ git add InlineEngine.java student@student:~$ git commit -m "merged InlineEngine file" [test_dst ab22900] merged InlineEngine file student@student:~$ git push Enumerating objects: 1, done. Counting objects: 100% (1/1), done. Writing objects: 100% (1/1), 214 bytes | 214.00 KiB/s, done. Total 1 (delta 0), reused 0 (delta 0), pack-reused 0 To https://github.com/luis6156/Test-Git.git 008128f..ab22900 test_dst -> test_dst
Acum avem modificările corecte în urma merge-ului atât pe local, cât și pe remote.