This guide covers the most important Git concepts and commands for beginners.
A repository (repo) is a storage space for your project and its version history.
- On GitHub/GitLab/Bitbucket: Create a repo from the web interface.
- Locally:
This creates a hidden
git init
.gitfolder which stores Git history.
Cloning makes a copy of a remote repository on your machine.
git clone https://github.com/user/repo.git- Creates a folder with all project files and history.
- Sets the remote connection automatically (usually called
origin).
-
Personal Access Token (PAT) (GitHub example):
- Go to Settings → Developer settings → Personal access tokens.
- Use the token instead of your password when pushing code:
git push https://<TOKEN>@github.com/user/repo.git
-
Set your username and email:
git config --global user.name "Your Name" git config --global user.email "[email protected]"
Git has four main areas:
- Working Directory → where you edit files.
- Staging Area → preparation area before committing.
- Local Repository → where commits are stored on your machine.
- Remote Repository → shared repo on GitHub/GitLab.
Flow:
Working Dir → git add → Staging → git commit → Local Repo → git push → Remote Repo
- Add files to staging:
git add <file>
- Commit to local repo:
git commit -m "Message" - Push to remote repo:
git push
- Pull changes from remote:
git pull
Every commit has a unique SHA-1 hash (Commit ID).
- Example:
a1b2c3d4 - Can be used to checkout or reset to a specific commit:
git checkout a1b2c3d4
View commit history:
git logUseful options:
git log --oneline→ compact view.git log --graph --all→ visual branch graph.
Always run:
git pullbefore git push.
This ensures your local repo is up-to-date and prevents conflicts if other developers have pushed changes.
Branches allow parallel development.
- List branches:
git branch
- Create new branch:
git branch <BRANCH_NAME>
- Switch to branch:
git checkout <BRANCH_NAME>
- Create + switch (shortcut):
git checkout -b <BRANCH_NAME>
Merging combines work from one branch into another.
Steps:
- Go to the target branch (the branch that will receive the changes):
git checkout main
- Merge the source branch:
git merge <BRANCH_NAME>
- If no conflicts → merge completes directly.
- If conflicts → Git will ask you to resolve them.
After merging, push the updated branch:
git push origin mainClean up old branches:
git branch -d <BRANCH_NAME>- Write meaningful commit messages.
- Use
.gitignoreto exclude logs, secrets, or build artifacts. - Commit small changes often instead of big ones.
- Use Pull Requests (PRs) for teamwork to enable code reviews.
- Visualize repo history:
git log --oneline --graph --all
What: A merge conflict happens when Git can’t automatically combine changes (usually the same lines edited on two branches, or edit vs delete/rename).
How to solve (pattern):
- Run the merge → conflict markers (
<<<<<<<,=======,>>>>>>>) appear. - Open files, choose the final content.
git add <files>to mark resolved.git commit(finishes the merge).
# setup
mkdir lab-merge-conflict-a && cd lab-merge-conflict-a
git init -b main
printf "Owner: Team
" > info.txt
git add . && git commit -m "init"
# diverge
git switch -c feature
printf "Owner: Feature
" > info.txt
git commit -am "feature: change owner"
git switch main
printf "Owner: Main
" > info.txt
git commit -am "main: change owner"
# merge → conflict
git merge feature
# open info.txt, decide the final line (e.g. "Owner: Main & Feature")
# then:
git add info.txt
git commit -m "merge: resolve owner line"cd .. && mkdir lab-merge-conflict-b && cd lab-merge-conflict-b
git init -b main
echo "data" > data.txt
git add . && git commit -m "init data"
git switch -c feat-edit
echo "feature edit" >> data.txt
git commit -am "feat: edit data"
git switch main
git rm data.txt
git commit -m "main: delete data"
git merge feat-edit # conflict: modified in one branch, deleted in the other
# option 1: keep deletion
# git rm data.txt
# git commit -m "resolve: keep deletion"
# option 2: keep modified file
# git add data.txt
# git commit -m "resolve: keep modified"Key takeaways
- Conflicts are normal; fix them deliberately.
- After editing, always
git addto mark resolved before committing.
When: The target branch hasn’t moved since you branched; your branch is a straight line ahead.
Result: main pointer just “fast-forwards” to your feature tip. No merge commit.
cd .. && mkdir lab-merge-ff && cd lab-merge-ff
git init -b main
echo base > app.txt
git add . && git commit -m "base"
git switch -c feature
echo a >> app.txt; git commit -am "feat: a"
echo b >> app.txt; git commit -am "feat: b"
git switch main
git merge --ff-only feature # fast-forward
git log --oneline --graph --decorate --allDiagram
main: A - B - C
\ x - y (feature)
fast-forward ⇒ A - B - C - x - y (main moves forward, no merge commit)
When: Both branches advanced; you need a real merge.
Result: A merge commit with two parents becomes the new tip of the target branch.
cd .. && mkdir lab-merge-3w && cd lab-merge-3w
git init -b main
echo base > f.txt
git add . && git commit -m "base"
git switch -c feature
echo feat1 >> f.txt; git commit -am "F1"
echo feat2 >> f.txt; git commit -am "F2"
git switch main
echo main1 >> f.txt; git commit -am "M1"
echo main2 >> f.txt; git commit -am "M2"
git merge feature # creates a merge commit (may or may not conflict)
git log --oneline --graph --decorate --allKey takeaways
- Fast-forward: clean, linear, no extra commit.
- Three-way: preserves true branching history; creates a merge commit.
What: “Replay” your feature commits on top of another branch, creating new commit IDs. Keeps history linear.
Rule of thumb: Don’t rebase branches others already pulled (history rewrite). If you must, push with --force-with-lease.
cd .. && mkdir lab-rebase && cd lab-rebase
git init -b main
echo base > f.txt
git add . && git commit -m "base"
git switch -c feature
echo f1 >> f.txt; git commit -am "F1"
echo f2 >> f.txt; git commit -am "F2"
git switch main
echo m1 >> f.txt; git commit -am "M1"
echo m2 >> f.txt; git commit -am "M2"
git switch feature
git rebase main # resolve if needed, then: git rebase --continue
# after rebase, feature is linear on top of main
git switch main
git merge --ff-only feature # fast-forward merge
git log --oneline --graph --decorate --all-
Merge
- ✅ Preserves real branching history
- ✅ Safe for shared branches (no rewrite)
- ❌ Log can be “busy”
- Use when: integrating long-lived branches, shared work
-
Rebase
- ✅ Linear, clean history
- ❌ Rewrites commits (new SHAs)
- Use when: cleaning up feature history before merging, personal/short-lived branches
- If already pushed:
git push --force-with-lease
main= production- Create
release/x.yfrommain→ stabilize (only bug fixes) - Merge
release/x.yback intomain(tag release) and intodevelop/active line if you have one - Create
hotfix/x.y.zfrommainfor urgent fixes → merge back
cd .. && mkdir lab-release-flow && cd lab-release-flow
git init -b main
echo v1 > app.txt; git add .; git commit -m "v1"
git switch -c release/1.1
echo "fix-rc" >> app.txt; git commit -am "release: fix"
git switch main
git merge --no-ff release/1.1 -m "release 1.1"
git tag v1.1main: productiondevelop: integrationfeature/*branches offdevelop, merges back todeveloprelease/*fromdevelopto stabilize, then merge tomainand back todevelophotfix/*frommain, merges back to both
cd .. && mkdir lab-gitflow && cd lab-gitflow
git init -b main
git switch -c develop
echo dev > app.txt; git add .; git commit -m "seed develop"
git switch -c feature/login
echo login >> app.txt; git commit -am "feature: login"
git switch develop
git merge --no-ff feature/login -m "merge: login"
git switch -c release/1.0
echo "release notes" >> notes.md; git add .; git commit -m "prep release"
git switch main
git merge --no-ff release/1.0 -m "release 1.0"
git tag v1.0
git switch develop
git merge --no-ff release/1.0 -m "back-merge release"PR: Propose changes; discuss, review, run CI, then merge.
Prevent direct merges: Protect the branch (e.g., main) to require PRs, approvals, and passing checks.
Reviewers/checks: Add reviewers; connect CI (e.g., GitHub Actions); require green status before merge.
On personal accounts: collaborators on private repos have write access; still enforce PR + checks via branch protection to block direct pushes/merges.
What: Your own copy of someone else’s repo under your account. You can experiment freely, then open a PR back.
Post‑fork best practice
# after forking on GitHub and cloning your fork:
git remote -v # origin points to YOUR fork
git remote add upstream https://github.com/ORIGINAL/REPO.git
git fetch upstream
git switch main
git merge upstream/main # or: git rebase upstream/main- Personal repo: add “collaborators.” Private personal repos give collaborators write access (no read‑only granularity).
- Organization repos: finer roles: read, triage, write, maintain, admin; best practice is to grant via teams.
- Protect important branches to require PRs and checks.
What: List patterns of files to ignore (untracked) so they don’t get committed (logs, build outputs, env files).
cd .. && mkdir lab-ignore && cd lab-ignore
git init -b main
mkdir logs build
echo "secret=123" > .env
echo "debug" > logs/app.log
echo "bin" > build/out.bin
cat > .gitignore <<'EOF'
# env & secrets
.env
# builds & logs
build/
logs/
*.log
EOF
git add .gitignore && git commit -m "add .gitignore"
git status # ignored files won't appear
# if something is already tracked and you want to untrack it:
git rm -r --cached build/
git commit -m "stop tracking build/"cd .. && mkdir lab-squash-a && cd lab-squash-a
git init -b main
echo base > f.txt; git add .; git commit -m "base"
git switch -c feature
echo 1 >> f.txt; git commit -am "step1"
echo 2 >> f.txt; git commit -am "step2"
git rebase -i HEAD~2
# in editor: keep first "pick", change second to "squash"
# write one final message
git switch main
git merge --ff-only featurecd .. && mkdir lab-squash-b && cd lab-squash-b
git init -b main
echo base > f.txt; git add .; git commit -m "base"
git switch -c feature
echo a >> f.txt; git commit -am "a"
echo b >> f.txt; git commit -am "b"
git switch main
git merge --squash feature
git commit -m "feature squashed into one"If you rebased/squashed after pushing, update remote with:
git push --force-with-lease.
Default: Stashes tracked changes (staged + unstaged).
Include untracked: -u, include ignored: -a.
Restore: apply (keep stash) or pop (apply & remove).
cd .. && mkdir lab-stash && cd lab-stash
git init -b main
echo start > notes.txt
git add . && git commit -m "init"
echo "edit" >> notes.txt
touch scratch.md # untracked
git stash push -u -m "WIP tracked+untracked"
git switch -c quick-fix
git stash pop # bring WIP here