Lesson 8: Version Control with Git

Learning Objectives

By the end of this lesson, you will be able to:

  1. Clone and manage Git repositories to work with existing codebases and contribute to team projects effectively
  2. Create and manage branches using appropriate branching strategies for features, bug fixes, and releases
  3. Merge branches and resolve conflicts systematically to integrate changes while maintaining code integrity
  4. Execute push and pull operations to synchronize work with remote repositories and collaborate with team members
  5. Apply Git best practices including meaningful commit messages, atomic commits, and workflow conventions that support professional development

Introduction

Version control is fundamental to modern software development, and Git has become the de facto standard for tracking changes, collaborating with teams, and managing code history. Created by Linus Torvalds in 2005 for Linux kernel development, Git is now used by millions of developers and powers platforms like GitHub, GitLab, and Bitbucket [1].

Git solves critical problems that plague software development without version control. It provides a complete history of every change to your code, enabling you to understand what changed, when, why, and by whom. When bugs appear, you can identify exactly which commit introduced the problem and revert it. When multiple developers work on the same codebase, Git manages their concurrent changes and helps resolve conflicts [2].

Unlike older centralized version control systems, Git is distributed—each developer has a complete copy of the repository including its full history. This enables working offline, creates natural backups, and allows flexible workflows. Git’s branching and merging capabilities are particularly powerful, making it practical to work on multiple features simultaneously, experiment with ideas, and integrate changes confidently [1].

This lesson focuses on the core Git operations you’ll use daily: cloning repositories, creating branches, merging changes, resolving conflicts, and synchronizing with remote repositories. While Git has dozens of commands and hundreds of options, mastering these fundamentals enables effective collaboration and forms the foundation for more advanced techniques. You’ll learn not just the commands, but the concepts behind them and the workflows that make teams productive [3].


Core Content

Understanding Git Fundamentals

Before diving into specific commands, understanding Git’s conceptual model is essential. Git thinks about data differently from other version control systems, and this mental model shapes how you use it effectively [1].

The Three States and Three Trees:

Git has three main states that your files can be in:

  1. Modified: You’ve changed the file but haven’t committed it yet
  2. Staged: You’ve marked a modified file to go into your next commit
  3. Committed: The data is safely stored in your local repository

This corresponds to three “trees” (really data structures) Git manages:

Working Directory  →  Staging Area  →  Git Repository
     (modified)         (staged)        (committed)

Understanding this flow is crucial. You don’t commit changes directly from your working directory. Instead, you selectively stage changes (using git add), then commit the staged changes (using git commit). This allows you to craft precise commits that capture logical units of change [2].

Commits and the Commit Graph:

A commit is a snapshot of your project at a specific point in time. Each commit contains:

  • A snapshot of all files in your project
  • Metadata (author, date, commit message)
  • A reference to its parent commit(s)

Commits form a directed acyclic graph (DAG), where each commit points to its parent(s):

A ← B ← C ← D  (main branch)
     ↖
       E ← F  (feature branch)

This graph structure enables powerful operations like branching, merging, and traversing history [1].

Repositories: Local and Remote:

A Git repository is the .git directory that stores all project history and metadata. You typically work with two types:

  • Local repository: On your computer, where you make commits
  • Remote repository: On a server (GitHub, GitLab), where you push commits and pull others’ work

Remote repositories aren’t special—they’re just Git repositories stored elsewhere. The convention is to name the primary remote origin [3].

Cloning Repositories

Cloning creates a complete local copy of a remote repository, including all history, branches, and tags. This is typically your first step when joining a project [1].

Basic Cloning:

# Clone a repository
git clone https://github.com/username/repository.git

# Clone into a specific directory name
git clone https://github.com/username/repository.git my-project

# Clone a specific branch
git clone -b develop https://github.com/username/repository.git

When you clone a repository:

  1. Git downloads the entire repository history
  2. Creates a local repository in a new directory
  3. Creates a remote reference called origin pointing to the source
  4. Checks out the default branch (usually main or master)

Understanding Remote Tracking:

After cloning, your local repository “tracks” the remote:

# View remote repositories
git remote -v
# origin  https://github.com/username/repo.git (fetch)
# origin  https://github.com/username/repo.git (push)

# View all branches (local and remote)
git branch -a
# * main
#   remotes/origin/main
#   remotes/origin/develop
#   remotes/origin/feature-login

The remotes/origin/* branches are remote-tracking branches—local references to the state of branches on the remote repository. They update when you fetch or pull from the remote [2].

HTTPS vs SSH Authentication:

You can clone using HTTPS or SSH:

# HTTPS - requires username/password or token
git clone https://github.com/username/repository.git

# SSH - requires SSH key setup
git clone [email protected]:username/repository.git

SSH is preferred for frequent operations—it uses key-based authentication rather than entering credentials repeatedly [3].

Branching: Parallel Development

Branches enable working on multiple features, bug fixes, or experiments simultaneously without interfering with each other. Git’s branching is lightweight and fast—creating a branch just creates a new pointer to a commit [1].

Creating and Switching Branches:

# Create a new branch
git branch feature-user-authentication

# Switch to the branch
git checkout feature-user-authentication

# Create and switch in one command
git checkout -b feature-user-authentication

# Modern syntax (Git 2.23+)
git switch -c feature-user-authentication

# View all branches (* indicates current branch)
git branch
#   main
# * feature-user-authentication

When you create a branch, Git creates a new pointer to the current commit. When you make commits on that branch, only that branch pointer moves forward—other branches remain unchanged [1].

Branching Strategies:

Different teams use different branching strategies. Common approaches include:

Git Flow: Uses multiple long-lived branches for different purposes [4].

main (production-ready code)
├── develop (integration branch)
    ├── feature/user-auth (feature branches)
    ├── feature/payment-gateway
    └── hotfix/critical-bug (urgent fixes)

GitHub Flow: Simpler workflow with one main branch and short-lived feature branches [4].

main (always deployable)
├── feature/add-search
├── fix/login-bug
└── docs/update-readme

Trunk-Based Development: Developers work on short-lived branches or directly on main with feature flags [4].

main (trunk)
├── user-auth (1-2 day branch)
└── payment-fix (1-2 day branch)

Branch Naming Conventions:

Use descriptive, consistent names:

# Good branch names
feature/user-authentication
feature/payment-integration
bugfix/login-validation
hotfix/security-patch
docs/api-documentation

# Avoid vague names
fix-bug
new-feature
temp
test

Working with Remote Branches:

# Push local branch to remote
git push -u origin feature-user-authentication

# The -u flag sets up tracking
# Future pushes just need: git push

# Fetch remote branches
git fetch origin

# Checkout remote branch (creates local tracking branch)
git checkout feature-payment
# or explicitly:
git checkout -b feature-payment origin/feature-payment

# Delete local branch
git branch -d feature-user-authentication

# Delete remote branch
git push origin --delete feature-user-authentication

Committing Changes: Capturing Work

Commits are the fundamental units of change in Git. Each commit should represent a logical, self-contained change that can be understood and potentially reverted independently [2].

The Staging Area Workflow:

# Check what's changed
git status
# On branch feature-login
# Changes not staged for commit:
#   modified:   src/auth.py
#   modified:   src/utils.py
# Untracked files:
#   tests/test_auth.py

# Stage specific files
git add src/auth.py
git add tests/test_auth.py

# Stage all changes
git add .

# Check what's staged
git status
# Changes to be committed:
#   modified:   src/auth.py
#   new file:   tests/test_auth.py
# Changes not staged for commit:
#   modified:   src/utils.py

# Unstage if needed
git reset HEAD src/auth.py

# Commit staged changes
git commit -m "Add user authentication with JWT tokens"

# Stage and commit in one step (tracked files only)
git commit -a -m "Update logging configuration"

Writing Good Commit Messages:

Commit messages are crucial for understanding project history. Follow these conventions [5]:

# Structure:
# <type>: <subject> (50 chars or less)
#
# <body - explain what and why, not how> (wrap at 72 chars)
#
# <footer - reference issues, breaking changes>

# Good commit message:
git commit -m "feat: Add JWT authentication to login endpoint

Implement JSON Web Token authentication for the /api/login endpoint
to replace the previous session-based authentication. This provides
better scalability for our API and enables mobile app integration.

- Add JWT token generation with 1-hour expiration
- Update login endpoint to return JWT token
- Add middleware to verify JWT tokens on protected routes
- Include refresh token mechanism for extended sessions

Closes #234"

# Commit types (Conventional Commits):
# feat:     New feature
# fix:      Bug fix
# docs:     Documentation changes
# style:    Formatting, missing semicolons, etc.
# refactor: Code restructuring without behavior change
# test:     Adding or updating tests
# chore:    Maintenance tasks, dependency updates

Atomic Commits:

Make each commit atomic—a single logical change. This makes history clearer and enables selective reverting:

# Bad - mixing unrelated changes
git add src/auth.py src/payment.py src/email.py
git commit -m "Various updates"

# Good - separate logical commits
git add src/auth.py tests/test_auth.py
git commit -m "feat: Add two-factor authentication"

git add src/payment.py tests/test_payment.py
git commit -m "fix: Handle edge case in payment processing"

git add src/email.py
git commit -m "refactor: Extract email sending to separate service"

Viewing History:

# View commit history
git log

# Compact one-line format
git log --oneline

# Show file changes
git log --stat

# Show actual differences
git log -p

# Graphical branch visualization
git log --oneline --graph --all

# Filter by author
git log --author="John Doe"

# Filter by date
git log --since="2 weeks ago"

# Search commit messages
git log --grep="authentication"

Merging: Integrating Changes

Merging combines changes from different branches. Understanding merge strategies and handling conflicts is essential for collaborative development [1].

Fast-Forward Merges:

When the target branch hasn’t changed since you created your feature branch, Git can “fast-forward”—just move the branch pointer:

# Starting state:
# main:    A ← B ← C
# feature:         ↖ D ← E

# Merge feature into main
git checkout main
git merge feature

# Result (fast-forward):
# main:    A ← B ← C ← D ← E
# feature:                 ↑
git checkout main
git merge feature-simple-change
# Updating a1b2c3d..e4f5g6h
# Fast-forward
#  src/config.py | 2 +-
#  1 file changed, 1 insertion(+), 1 deletion(-)

Three-Way Merges:

When both branches have new commits, Git creates a merge commit with two parents:

# Starting state:
# main:    A ← B ← C ← F
# feature:     ↖ D ← E

# Merge feature into main
git checkout main
git merge feature

# Result (three-way merge):
# main:    A ← B ← C ← F ← G (merge commit)
# feature:     ↖ D ← E ↗
git checkout main
git merge feature-user-profile
# Merge made by the 'recursive' strategy.
#  src/models/user.py | 45 +++++++++++++++++++++
#  tests/test_user.py | 30 ++++++++++++++
#  2 files changed, 75 insertions(+)

Merge Strategies:

# Standard merge (creates merge commit)
git merge feature-branch

# Fast-forward only (fails if not possible)
git merge --ff-only feature-branch

# No fast-forward (always create merge commit)
git merge --no-ff feature-branch

# Squash merge (combines all commits into one)
git merge --squash feature-branch
git commit -m "Add user profile feature"

When to Use Each Strategy:

  • Fast-forward: Clean history for simple changes, but loses branch information
  • No fast-forward: Preserves that a feature was developed on a branch, clearer for code review
  • Squash: Cleans up messy commit history before merging to main

Conflict Resolution: Handling Divergent Changes

Conflicts occur when Git can’t automatically merge changes because the same lines were modified differently in each branch [2].

Understanding Conflicts:

git merge feature-auth
# Auto-merging src/config.py
# CONFLICT (content): Merge conflict in src/config.py
# Automatic merge failed; fix conflicts and then commit the result.

git status
# On branch main
# 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:   src/config.py

Conflict Markers:

Git adds markers to show conflicting sections:

# src/config.py
DATABASE_HOST = 'localhost'

<<<<<<< HEAD
DATABASE_PORT = 5432
DATABASE_NAME = 'production_db'
=======
DATABASE_PORT = 3306
DATABASE_NAME = 'main_database'
>>>>>>> feature-auth

DEBUG = False

The section between <<<<<<< HEAD and ======= is your current branch’s version. The section between ======= and >>>>>>> feature-auth is the incoming branch’s version.

Resolving Conflicts:

# 1. Open the conflicted file and manually resolve
# Edit src/config.py to:
DATABASE_HOST = 'localhost'
DATABASE_PORT = 5432  # Keeping current setting
DATABASE_NAME = 'production_db'  # Keeping current setting
DEBUG = False

# 2. Stage the resolved file
git add src/config.py

# 3. Check status
git status
# All conflicts fixed: run "git commit"

# 4. Complete the merge
git commit -m "Merge feature-auth, keeping production DB config"

# Alternative: abort the merge if needed
git merge --abort

Using Merge Tools:

# Configure a visual merge tool
git config --global merge.tool vimdiff
# or meld, kdiff3, beyond compare, etc.

# Launch merge tool
git mergetool

# The tool shows three panels:
# LOCAL (your changes) | BASE (common ancestor) | REMOTE (their changes)
#                      MERGED (result)

Strategies for Minimizing Conflicts:

  1. Pull frequently: Keep your branch up-to-date with main
  2. Small, focused branches: Merge features quickly rather than long-lived branches
  3. Communicate: Coordinate with team members on overlapping work
  4. Rebase before merging: Keep branch history clean (advanced technique)

Push and Pull: Synchronizing with Remotes

Push and pull operations synchronize your local repository with remote repositories, enabling collaboration [3].

Pulling Changes:

# Fetch and merge changes from remote
git pull origin main

# This is equivalent to:
git fetch origin
git merge origin/main

# Pull with rebase instead of merge (cleaner history)
git pull --rebase origin main

Understanding Fetch vs Pull:

  • Fetch: Downloads new data but doesn’t integrate it (safe)
  • Pull: Fetches and merges automatically (convenient but can cause unexpected merges)
# Fetch to see what's new without merging
git fetch origin

# Compare your branch with remote
git log main..origin/main

# If you like what you see, merge
git merge origin/main

Pushing Changes:

# Push to remote repository
git push origin main

# Push new branch and set up tracking
git push -u origin feature-new-feature

# Future pushes on this branch
git push

# Force push (dangerous - overwrites remote)
git push --force origin main

# Safer force push (rejects if remote has new commits)
git push --force-with-lease origin main

Common Push/Pull Scenarios:

Scenario 1: Behind Remote

git push
# To https://github.com/user/repo.git
#  ! [rejected]        main -> main (fetch first)
# error: failed to push some refs

# Someone else pushed since your last pull
git pull origin main
# Resolve any conflicts
git push origin main

Scenario 2: Diverged Histories

git pull
# Merge made by the 'recursive' strategy.
# (creates a merge commit)

# Alternative: rebase to avoid merge commit
git pull --rebase
# (replays your commits on top of remote changes)

Scenario 3: Push Rejected (Fast-Forward Issue)

git push
# error: failed to push some refs

# Either merge:
git pull origin main
git push origin main

# Or rebase (cleaner):
git pull --rebase origin main
git push origin main

Collaborative Workflows

Different teams adopt different Git workflows. Understanding common patterns helps you adapt to any team [4].

Centralized Workflow:

Everyone pushes to main branch (simple, suitable for small teams):

# Daily workflow
git pull origin main
# Make changes
git commit -m "Add feature"
git pull origin main  # In case others pushed
git push origin main

Feature Branch Workflow:

Features developed on branches, merged via pull requests:

# Developer 1: Start feature
git checkout -b feature-search
# Work and commit
git push -u origin feature-search
# Create pull request on GitHub/GitLab

# Developer 2: Review and merge via web interface

# Developer 1: Update local after merge
git checkout main
git pull origin main
git branch -d feature-search

Forking Workflow:

Contributors fork repository, work on their fork, submit pull requests:

# Fork repository on GitHub (creates your-username/repo)

# Clone your fork
git clone https://github.com/your-username/repo.git

# Add upstream remote
git remote add upstream https://github.com/original/repo.git

# Make changes on feature branch
git checkout -b fix-bug
git commit -m "Fix authentication bug"
git push origin fix-bug

# Create pull request from your fork to original repo

# Keep your fork synchronized
git fetch upstream
git checkout main
git merge upstream/main
git push origin main

Common Pitfalls

Pitfall 1: Committing Directly to Main

Working directly on the main branch makes it difficult to review work, creates messy history, and risks breaking production code.

Best Practice: Always create feature branches for new work. Keep main stable and deployable. Use pull requests for code review before merging [4].

Pitfall 2: Large, Infrequent Commits

Making huge commits after days of work makes history useless and conflicts inevitable.

Best Practice: Commit frequently with small, logical changes. Each commit should pass tests and represent one coherent change. Aim for multiple commits per day [2].

Pitfall 3: Meaningless Commit Messages

Messages like “fix,” “update,” or “changes” provide no useful information for understanding project history.

Best Practice: Write descriptive commit messages following Conventional Commits format. Explain what changed and why, not how. Future you will be grateful [5].

Pitfall 4: Not Pulling Before Pushing

Pushing without pulling first often leads to rejected pushes and messy merge commits.

Best Practice: Pull (or fetch and merge) before pushing. Make it part of your workflow: pull, work, commit, pull again, then push [3].

Pitfall 5: Force Pushing to Shared Branches

Force pushing rewrites history, destroying others’ work if they’ve pulled that branch.

Best Practice: Never force push to shared branches (main, develop). If you must force push to a feature branch, coordinate with anyone else working on it. Use --force-with-lease for safety [3].


Summary

Git is an essential tool for modern software development, enabling version control, collaboration, and code management. Mastering Git’s core operations empowers you to work effectively on any development team.

Cloning repositories creates a complete local copy of a project including its full history. Use git clone to get started with existing projects, understanding that cloning sets up remote tracking automatically with the remote named origin.

Branching enables parallel development without interfering with others’ work. Create branches with git branch or git checkout -b, name them descriptively (feature/, bugfix/, hotfix/*), and choose a branching strategy that matches your team’s needs—Git Flow for complex release management, GitHub Flow for continuous deployment, or trunk-based development for rapid iteration.

Committing changes captures your work in logical units. Use the staging area to craft precise commits with git add and git commit. Write meaningful commit messages following Conventional Commits format. Make commits atomic—each should represent one logical change that can stand alone.

Merging branches integrates changes from different lines of development. Git uses fast-forward merges when possible and three-way merges when both branches have diverged. Choose merge strategies based on your needs: standard merges preserve branch history, squash merges clean up commits, and no-fast-forward merges always show that work happened on a branch.

Resolving conflicts is inevitable when multiple developers work on the same code. Understand conflict markers, manually resolve differences, stage resolved files, and complete the merge. Use merge tools for complex conflicts, and minimize conflicts by pulling frequently and keeping branches short-lived.

Push and pull operations synchronize with remote repositories. Pull before starting work and before pushing. Understand that pull combines fetch and merge. Push your commits to share work with the team. Use --force-with-lease instead of --force when rewriting history is unavoidable.

Collaborative workflows vary by team. Centralized workflows have everyone push to main, feature branch workflows use branches with pull requests for review, and forking workflows enable open source contributions. Adapt to your team’s workflow while applying Git fundamentals consistently.

Git’s power lies not in memorizing commands but in understanding the underlying model: commits form a graph, branches are pointers, merges combine histories, and remotes synchronize repositories. This conceptual foundation enables you to use Git confidently in any scenario.


Practice Quiz

Question 1: You’ve been working on a feature branch for three days without pushing. When you try git push origin feature-login, you get an error that the branch doesn’t exist on the remote. What happened, and how do you fix it?

Answer: You created the branch locally but never pushed it to the remote repository. Local branches don’t automatically appear on the remote—you must explicitly push them.

Fix:

# First push with upstream tracking
git push -u origin feature-login

# The -u flag sets up tracking between your local branch
# and the remote branch, so future pushes just need:
git push

After this, origin/feature-login will exist as a remote tracking branch, and your local feature-login branch will track it. The -u (or --set-upstream) flag is only needed the first time you push a new branch [3].

Question 2: You’re about to merge your feature branch into main when a colleague mentions they just pushed important changes to main. What’s the correct sequence of Git commands to safely merge your feature while incorporating their changes?

Answer: Update your local main branch first, then merge it into your feature branch to handle any conflicts before merging the feature into main:

# 1. Save your current work
git checkout feature-user-profile

# 2. Update main with remote changes
git checkout main
git pull origin main

# 3. Go back to your feature branch
git checkout feature-user-profile

# 4. Merge updated main into your feature
git merge main
# (Resolve any conflicts here on your feature branch)

# 5. If conflicts occurred, resolve them and commit
git add .
git commit -m "Merge main into feature-user-profile, resolve conflicts"

# 6. Now merge your feature into main
git checkout main
git merge feature-user-profile

# 7. Push to remote
git push origin main

This approach resolves conflicts on your feature branch rather than on main, keeping main stable [2][4].

Question 3: After making three commits on your feature branch, you realize the first commit message was vague (“update files”). How can you rewrite this message without changing the actual code changes?

Answer: Use interactive rebase to edit commit history:

# Start interactive rebase for the last 3 commits
git rebase -i HEAD~3

# This opens an editor showing:
# pick a1b2c3d update files
# pick e4f5g6h Add user validation
# pick h7i8j9k Update tests

# Change 'pick' to 'reword' (or just 'r') for the commit to edit:
# reword a1b2c3d update files
# pick e4f5g6h Add user validation
# pick h7i8j9k Update tests

# Save and close. Git will open another editor for the commit message.
# Change it to something meaningful:
# feat: Add user profile model and database schema

# Save and close. Rebase completes.

Important: Only do this for commits that haven’t been pushed to a shared branch. Rewriting pushed history requires force pushing and can disrupt collaborators [3].

Question 4: You pull from main and get this conflict in config.py:

<<<<<<< HEAD
API_TIMEOUT = 30
=======
API_TIMEOUT = 60
>>>>>>> main

Your timeout works fine for your feature. Your colleague increased it because some API calls were timing out in production. How should you resolve this?

Answer: This requires understanding the context, not just mechanically choosing one version.

Best resolution:

# Keep the higher value and document why
API_TIMEOUT = 60  # Increased from 30 to handle slower API endpoints (see issue #156)

Steps:

# 1. Edit config.py to resolve conflict
# Remove conflict markers and choose appropriate value

# 2. Stage the resolved file
git add config.py

# 3. Complete the merge
git commit -m "Merge main into feature, keep increased API timeout

Production monitoring showed timeouts at 30 seconds.
Keeping 60-second timeout from main to prevent issues."

Key principle: When resolving conflicts, don’t just pick “yours” or “theirs” automatically. Understand why each change was made and create a resolution that makes sense [2]. When in doubt, talk to the other developer.

Question 5: You’re working on feature-payment branch. You want to see what commits exist on main that aren’t in your feature branch, and vice versa. What commands show this?

Answer: Use log range syntax to compare branches:

# Commits in main but not in feature-payment
git log feature-payment..main
# Shows what you'll get when you merge main into your branch

# Commits in feature-payment but not in main
git log main..feature-payment
# Shows what you'll contribute when you merge your branch into main

# Visual representation of both
git log --oneline --graph --left-right main...feature-payment
# The three dots show commits unique to each branch

# See just the commit count
git rev-list --count main..feature-payment
# Outputs a number, like: 5

# Detailed view with file changes
git log main..feature-payment --stat

This is particularly useful before merging to understand what changes you’re about to integrate [1][2].


References

[1] Chacon, S., & Straub, B. (2014). Pro Git (2nd Edition). Apress. URL: https://git-scm.com/book/en/v2, Quote: “Git is a distributed version control system where every developer has a complete copy of the repository including its full history. Git thinks about data as a series of snapshots, where each commit represents the entire state of the project at a point in time. This distributed nature enables powerful workflows and makes operations fast because most operations are local.”

[2] Loeliger, J., & McCullough, M. (2012). Version Control with Git (2nd Edition). O’Reilly Media. URL: https://www.oreilly.com/library/view/version-control-with/9781449345037/, Quote: “The three-tree architecture—working directory, staging area, and repository—is fundamental to Git’s power and flexibility. The staging area allows you to craft precise commits, grouping related changes together while leaving other modifications for separate commits. This level of control over commit granularity is essential for maintaining clean, understandable project history.”

[3] Silverman, R., & Wright, P. (2019). Git Pocket Guide (2nd Edition). O’Reilly Media. URL: https://www.oreilly.com/library/view/git-pocket-guide/9781492042483/, Quote: “Understanding remotes and remote-tracking branches is crucial for collaboration. When you clone a repository, Git automatically creates a remote called ‘origin’ and sets up remote-tracking branches. Push and pull operations synchronize your local repository with remotes, enabling team collaboration while preserving the distributed nature of Git.”

[4] Driessen, V. (2010). A successful Git branching model. URL: https://nvie.com/posts/a-successful-git-branching-model/, Quote: “Git Flow defines a strict branching model designed around project releases. It uses multiple long-lived branches—main for production, develop for integration, and short-lived branches for features, releases, and hotfixes. While this model adds complexity, it provides clear conventions for managing releases and hotfixes in projects with scheduled release cycles.”

[5] Conventional Commits Specification. (2022). Conventional Commits. URL: https://www.conventionalcommits.org/, Quote: “The Conventional Commits specification provides a lightweight convention for commit messages that makes history easy to navigate and enables automated tooling. Commits are prefixed with a type (feat, fix, docs, etc.) followed by a colon and description. This structure enables automatic changelog generation, semantic versioning, and clear communication of changes across the team.”