Git is a distributed version control system created by Linus Torvalds in 2005. It tracks changes in source code during software development, allowing multiple developers to collaborate efficiently. Unlike centralized systems, every developer has a full copy of the repository, enabling offline work and robust branching/merging.
Key Features:
- Distributed: Every clone is a full backup.
- Branching and Merging: Lightweight branches encourage feature‑based development.
- Data Integrity: Everything is checksummed (SHA‑1) before storage.
- Staging Area: Changes can be selectively committed.
- Speed: Most operations are local, making Git fast.
#2. Installation and Configuration
#Installation
- Linux (Debian/Ubuntu):
sudo apt update sudo apt install git - macOS: Install via Homebrew or download from git-scm.com.
brew install git - Windows: Download the installer from git-scm.com. It includes Git Bash, a Unix‑like shell.
#Initial Configuration
Set your identity (required for commits):
git config --global user.name "Your Name"
git config --global user.email "your.email@example.com"
Other useful global settings:
git config --global core.editor "code --wait" # VS Code as editor
git config --global merge.tool vimdiff # merge tool
git config --global color.ui auto # colored output
View configuration:
git config --list
#3. Basic Git Workflow
#Creating a Repository
# Initialize a new repo
git init my-project
cd my-project
# Clone an existing repo
git clone https://github.com/user/repo.git
#Making Changes
# Check status
git status
# Add files to staging
git add file.txt
git add . # all changes
# Commit staged changes
git commit -m "Add initial version of file.txt"
# View commit history
git log --oneline
#Working with Remotes
# Add a remote
git remote add origin https://github.com/user/repo.git
# Push changes
git push origin main
# Pull latest changes
git pull origin main
# Fetch changes without merging
git fetch origin
#4. Branching and Merging
Branches are lightweight pointers to commits. The default branch is usually main (or master).
#Creating and Switching Branches
# Create a new branch
git branch feature/login
# Switch to it
git checkout feature/login
# Create and switch in one command
git checkout -b feature/login
#Merging
# Switch to target branch (e.g., main)
git checkout main
# Merge the feature branch
git merge feature/login
By default, Git performs a fast‑forward merge if possible; otherwise, it creates a merge commit.
#Deleting Branches
# Delete local branch (after merge)
git branch -d feature/login
# Delete remote branch
git push origin --delete feature/login
#5. Merge Conflicts: Causes and Resolution
A merge conflict occurs when two branches modify the same part of the same file, and Git cannot automatically decide which change to keep.
#Example Conflict Scenario
- On
main, you editREADME.mdand commit. - On
feature/docs, you also edit the same line ofREADME.mdand commit. - Merging
feature/docsintomainproduces a conflict.
git checkout main
git merge feature/docs
Output:
Auto-merging README.md
CONFLICT (content): Merge conflict in README.md
Automatic merge failed; fix conflicts and then commit the result.
#Resolving the Conflict
Open the conflicted file. Git marks the conflicting sections:
<<<<<<< HEAD
This line is from main.
=======
This line is from feature/docs.
>>>>>>> feature/docs
HEADis your current branch (main).- The section between
=======and>>>>>>>is from the incoming branch.
Edit the file to keep the desired changes, remove the conflict markers, then:
git add README.md
git commit -m "Merge feature/docs: resolved conflict in README"
#Using a Merge Tool
Launch a visual merge tool:
git mergetool
Configure your preferred tool (e.g., meld, vimdiff).
#6. Rebasing: When and How
Rebasing rewrites commit history by applying commits from one branch onto the tip of another. It creates a linear history, avoiding merge commits.
#Basic Rebase
# While on feature branch
git rebase main
This takes all commits on feature that are not in main and replays them on top of main.
#Interactive Rebase
Use -i to squash, reorder, or edit commits:
git rebase -i HEAD~3 # last 3 commits
An editor opens; you can change pick to squash, edit, etc.
#Golden Rule of Rebasing
Never rebase commits that have been pushed to a shared repository. Rebasing rewrites history, causing trouble for collaborators.
#7. Cherry-Picking Commits
cherry-pick applies a specific commit (from any branch) to your current branch. Useful for selectively bringing bug fixes or features.
#Example
Suppose a bug was fixed on develop in commit a1b2c3d, and you need that fix on main without merging the whole branch.
git checkout main
git cherry-pick a1b2c3d
If conflicts arise, resolve them and continue:
git add resolved-file.txt
git cherry-pick --continue
Or abort:
git cherry-pick --abort
#Real‑Use Case
- Hotfix on production: you fix a bug on a hotfix branch, then cherry‑pick that commit into
mainanddevelop. - Backporting a feature to an older release.
#8. Stashing Changes
git stash temporarily saves uncommitted changes, allowing you to switch branches without committing.
# Save current changes
git stash save "WIP: login form"
# List stashes
git stash list
# Apply most recent stash (keeps it)
git stash apply
# Apply and drop
git stash pop
# Drop a specific stash
git stash drop stash@{2}
#9. Working with Remotes (GitHub/GitLab)
#Cloning a Repository
git clone https://github.com/username/repo.git
#Pushing to a Remote
git push origin main
If your local branch has a different name:
git push origin local-branch:remote-branch
#Pull Requests / Merge Requests
- On GitHub/GitLab, you create a PR/MR from your feature branch to
main. - Reviewers comment, and after approval, the branch is merged (often via the web UI).
#Keeping Fork Updated
git remote add upstream https://github.com/original/repo.git
git fetch upstream
git checkout main
git merge upstream/main
git push origin main
#10. Git Hooks: Automating Your Workflow
Git hooks are scripts that run automatically on certain events. They reside in .git/hooks/ (rename .sample files to activate).
#Client-Side Hooks
| Hook | Trigger |
|---|---|
pre-commit | Before commit message is requested. Use for linting, formatting. |
prepare-commit-msg | Before commit message editor. Modify default message. |
commit-msg | After commit message is saved. Validate message format. |
pre-push | Before push. Run tests or checks. |
Example pre-commit hook (runs linter and prevents commit if it fails):
#!/bin/sh
# .git/hooks/pre-commit
npm run lint
if [ $? -ne 0 ]; then
echo "Linting failed. Commit aborted."
exit 1
fi
Make it executable: chmod +x .git/hooks/pre-commit.
Example commit-msg hook (enforces conventional commit format):
#!/bin/sh
# .git/hooks/commit-msg
msg=$(cat $1)
if ! echo "$msg" | grep -qE "^(feat|fix|docs|style|refactor|test|chore):"; then
echo "Commit message must start with type: feat|fix|docs|..."
exit 1
fi
#Server-Side Hooks
On the remote repository (GitHub/GitLab), you can use webhooks or CI scripts instead of traditional server‑side hooks. However, self‑hosted Git servers can use hooks like post-receive.
Example post-receive hook (deploy after push):
#!/bin/bash
# .git/hooks/post-receive
while read oldrev newrev refname
do
branch=$(git rev-parse --symbolic --abbrev-ref $refname)
if [ "$branch" == "main" ]; then
git --work-tree=/var/www/html checkout -f
echo "Deployed to production"
fi
done
#11. Integration with CI/CD (GitHub Actions, GitLab CI)
Modern platforms offer CI/CD directly integrated with Git.
#GitHub Actions Example
Create .github/workflows/ci.yml:
name: CI
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
- run: npm ci
- run: npm test
- run: npm run build
#GitLab CI Example
Create .gitlab-ci.yml:
stages:
- test
- build
test:
stage: test
script:
- npm ci
- npm test
build:
stage: build
script:
- npm run build
artifacts:
paths:
- dist/
#Webhooks for External Services
- GitHub/GitLab can send HTTP POST requests to a URL (e.g., a deployment service) on events like
push,pull_request. - Useful for triggering builds, notifications, or updating documentation.
#12. Conventional Commits
Conventional Commits is a lightweight convention on top of commit messages. It provides an easy set of rules for creating an explicit commit history, which makes it easier to write automated tools on top of.
#Why Conventional Commits?
- Automatic changelog generation: Tools can generate CHANGELOGs from commits.
- Semantic versioning: Determine the next version number based on the types of commits (
fix→ PATCH,feat→ MINOR, breaking changes → MAJOR). - Clear communication: Team members quickly understand the purpose of each commit.
- Triggering builds and deployments: Some CI tools can react to commit types.
#Specification
The commit message should be structured as follows:
<type>[optional scope]: <description>
[optional body]
[optional footer(s)]
Types:
feat– A new feature for the user.fix– A bug fix for the user.docs– Documentation changes.style– Code style changes (formatting, missing semi‑colons, etc.) – no production code change.refactor– Code change that neither fixes a bug nor adds a feature.perf– Code change that improves performance.test– Adding missing tests or correcting existing tests.chore– Changes to the build process or auxiliary tools.
Scope: Optional, can be anything specifying the place of the commit change (e.g., auth, api).
Breaking change: Indicated by adding a ! after the type/scope, or a footer BREAKING CHANGE:.
#Examples
feat: add user login endpoint
fix(api): handle null response from payment gateway
docs: update README with setup instructions
refactor!: drop support for Node 14
BREAKING CHANGE: The `validate` function now returns a promise instead of a boolean.
#Tools and Automation
Commitlint – enforces conventional commits via a pre‑commit hook or CI.
npm install --save-dev @commitlint/{config-conventional,cli}
echo "module.exports = {extends: ['@commitlint/config-conventional']}" > commitlint.config.js
Then add a commit-msg hook (using Husky):
npx husky add .husky/commit-msg 'npx --no -- commitlint --edit $1'
Standard Version or semantic-release – automatically bump version, generate changelog, and create a release based on commit messages.
Example with semantic-release (GitHub Actions):
- name: Release
uses: cycjimmy/semantic-release-action@v4
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
#13. Versioning and Tagging
#Semantic Versioning
SemVer uses the format MAJOR.MINOR.PATCH:
- MAJOR – incompatible API changes
- MINOR – backwards‑compatible new functionality
- PATCH – backwards‑compatible bug fixes
#Relation with Conventional Commits
Using Conventional Commits, you can automatically derive the next version:
fix→ PATCHfeat→ MINORBREAKING CHANGEor!→ MAJOR
#Tagging Releases
Tags mark specific points in history, usually for releases.
#Creating a Tag
# Lightweight tag (just a pointer)
git tag v1.0.0
# Annotated tag (recommended) – stores author, date, message
git tag -a v1.0.0 -m "Release version 1.0.0"
#Pushing Tags
git push origin v1.0.0
git push --tags # push all tags
#Deleting Tags
# Local
git tag -d v1.0.0
# Remote
git push origin --delete v1.0.0
#14. Real-World Scenarios
#Scenario 1: Hotfix on Production with Cherry-Pick
Situation: A critical bug is discovered in production (tag v1.0.0). You need to fix it quickly and deploy a new version (v1.0.1).
Steps:
-
Create a hotfix branch from the production tag:
git checkout -b hotfix v1.0.0 -
Fix the bug, commit using conventional commit:
git add . git commit -m "fix: prevent crash on empty input" -
Test, then tag the fix:
git tag v1.0.1 git push origin v1.0.1 -
Deploy
v1.0.1to production. -
Cherry‑pick the fix into
mainanddevelop:git checkout main git cherry-pick hotfix # or the commit hash git push origin main git checkout develop git cherry-pick hotfix git push origin develop -
Delete the hotfix branch:
git branch -d hotfix
#Scenario 2: Resolving a Complex Merge Conflict
Situation: A feature branch (feature/payment) has been worked on for weeks. Meanwhile, main has many changes. When trying to merge, there are multiple conflicting files.
Resolution Strategy:
- Update
feature/paymentwith latestmainto resolve conflicts locally before merging intomain:git checkout feature/payment git merge main - For each conflicted file, open and resolve manually or use a merge tool.
- After resolving all, commit the merge:
git add . git commit -m "Merge branch 'main' into feature/payment" - Now
feature/paymentis up‑to‑date. Test thoroughly. - Finally, merge into
main(should be conflict‑free):git checkout main git merge feature/payment git push origin main
#Scenario 3: Automating Code Quality with Pre-Commit Hooks
Situation: The team wants to enforce code formatting and conventional commits.
Implementation:
- Install
husky,lint-staged,commitlint. - Configure
package.json:{ "scripts": { "lint": "eslint src --fix", "format": "prettier --write src" }, "lint-staged": { "*.js": ["npm run lint", "npm run format"] } } - Set up Husky hooks:
npx husky add .husky/pre-commit "npx lint-staged" npx husky add .husky/commit-msg "npx --no -- commitlint --edit $1" - On every commit, staged files are linted and formatted; if commit message doesn’t follow conventional commits, commit aborts.
#Scenario 4: Triggering a Deployment via Webhook
Situation: Whenever a new tag is pushed, automatically deploy to a staging server.
Implementation (using GitHub webhook + a simple Node.js server):
- On GitHub, add a webhook pointing to
https://your-server.com/webhookwith content typeapplication/json, trigger on “Pushes”. - On your server, create an endpoint that listens for POST requests, verifies the secret, and if the payload contains a new tag, runs a deployment script.
Example (Node.js/Express):
app.post('/webhook', (req, res) => { const event = req.headers['x-github-event']; const payload = req.body; if (event === 'push' && payload.ref.startsWith('refs/tags/')) { exec('./deploy.sh', (err, stdout) => { console.log(stdout); }); } res.sendStatus(200); });
#15. Best Practices and Tips
- Write meaningful commit messages: Use imperative tense (“Add feature”, “Fix bug”). Follow Conventional Commits.
- Commit often, push regularly.
- Keep branches short‑lived to avoid painful merges.
- Never rebase public branches.
- Use
.gitignoreto exclude build artifacts, secrets, and IDE files. - Sign your commits with GPG for authenticity.
- Use Git aliases for common commands:
git config --global alias.co checkout git config --global alias.br branch git config --global alias.st status - Leverage
git reflogto recover lost commits. - Protect important branches on GitHub/GitLab (require PR reviews, status checks).
- Automate versioning with tools like
semantic-releaseto reduce manual work.
This guide covers the essential Git skills every developer needs, with practical examples you’ll face daily. By adopting Conventional Commits and integrating hooks, you’ll create a more maintainable and automated development workflow. Practice these scenarios, and Git will become a powerful ally in your development journey.