Complete Guide to Git: Everything You Need to Know

Git cover image

Introduction

Git is now an essential tool in the modern software development world. Created by the brilliant mind of Linus Torvalds in 2005, Git is a distributed version control system that has revolutionized how developers collaborate on software projects.

What is Git and why is it so important in the software development world

Git is a version control system that allows you to track and manage changes made to project files over time. Unlike a simple backup, Git preserves the entire history of how files have changed, who made the changes, and when. This allows developers to:

  • Keep track of every change made to the code
  • Return to previous versions when necessary
  • Collaborate effectively without overwriting each other’s work
  • Work simultaneously on different features
  • Experiment with new ideas without compromising the main code

The importance of Git in the modern development world is such that knowing it is now considered a fundamental requirement for any developer. Its adoption has enormously simplified collaborative workflows, especially in an era where teams distributed around the world work on the same projects.

Brief history of Git: from Linus Torvalds to today

Git was born from the practical need of Linus Torvalds, the creator of the Linux kernel. From 2002, the Linux community had been using a proprietary system called BitKeeper, but in 2005 a controversy occurred when the owning company revoked the free license.

Torvalds, dissatisfied with the available alternatives, decided to create a new version control system that would meet the specific needs of the Linux kernel:

  • Speed
  • Simple design
  • Support for non-linear development (thousands of parallel branches)
  • Complete distribution
  • Ability to handle enormous projects like Linux

In just 10 days, Torvalds wrote the initial version of Git. The name “Git” is a humorous reference by Torvalds to himself – in British slang it refers to a stubborn and unpleasant person. Since its birth, Git has grown exponentially in popularity, becoming the de facto standard in both open source and commercial projects.

Differences between Git and other version control systems (e.g., SVN, Mercurial)

Git distinguishes itself from previous systems like SVN (Subversion) and rivals like Mercurial through several fundamental characteristics:

Git vs SVN:

  • Distribution vs Centralization: SVN is centralized, requiring a constant connection to the central server. Git is completely distributed, allowing offline work.
  • Speed: Git is generally faster than SVN, especially for operations like branching and merging.
  • Data size: Git efficiently compresses data, while SVN may require more space.
  • Branching model: In Git, branches are lightweight and easy to create, while in SVN they represent a significantly greater effort.

Git vs Mercurial:

  • Flexibility: Git offers more flexibility and control, while Mercurial aims for simplicity.
  • Learning curve: Mercurial is generally considered easier to learn, while Git has a steeper learning curve but offers more power.
  • Repository management: Git allows greater manipulation of history, Mercurial is more conservative.
  • Adoption: Git has conquered a significantly larger market share.

1. Fundamental Git Concepts

1.1 Distributed Version Control System (DVCS)

Advantages over centralized systems

Distributed version control systems like Git offer numerous advantages over traditional centralized systems:

  1. Network independence: Each developer has a complete copy of the repository with the entire history, allowing offline work.
  2. Resilience: There is no single point of failure. If a server fails, any local copy can be used to restore it.
  3. Superior speed: Most operations happen locally, eliminating network latency.
  4. Workflow flexibility: Easily supports different development models (feature branch, Gitflow, fork-and-pull).
  5. Efficient branching: Creating and merging branches is quick and resource-efficient.

How repository distribution works

In Git, each developer has a complete local repository with the entire project history. The distributed operation is based on these principles:

  1. Clone: When a user clones a remote repository, they get a complete copy with the entire history.
  2. Local commits: Developers make commits in their local repository without needing a connection to the central repository.
  3. Synchronization: Through operations like push, pull, and fetch, local repositories synchronize with remote ones.
  4. Multiple origins: A local repository can communicate with several remote repositories, allowing flexible collaborations.

This distributed approach is particularly effective for geographically dispersed teams and open source projects with numerous occasional contributors.

1.2 Git Architecture

The three main states: Working Directory, Staging Area, Repository

Git’s architecture is based on three main areas that files traverse during their lifecycle:

  1. Working Directory:
    • Contains the current files you’re working on
    • Reflects a single version extracted from the repository
    • Where you modify files before adding them to the staging area
  2. Staging Area:
    • Also called the “index”
    • Functions as an intermediate zone where you prepare the next commits
    • Allows you to select which changes to include in the next commit
    • Enables organizing changes into logical and coherent commits
  3. Repository (.git directory):
    • The database where Git stores the complete history of the project
    • Contains all commits, branches, tags, and metadata
    • What gets copied when you clone a repository

This division into three states is fundamental to understanding Git’s workflow and represents one of its most distinctive features.

Snapshot vs Delta-based version control

Git uses a snapshot-based approach rather than the traditional delta-based approach:

Delta-based system (e.g., SVN):

  • Stores the differences (deltas) between file versions
  • Reconstructs a file by sequentially applying all deltas from the initial version
  • Space-efficient, but potentially slower for access

Snapshot-based system (Git):

  • Saves a complete “photograph” of the project with each commit
  • For efficiency, unchanged files are references to the previous commit
  • Offers faster access to complete versions
  • Makes branching and merging operations much more efficient

This seemingly wasteful approach is made efficient through compression and references to unchanged files, allowing Git to be both fast and space-efficient.

1.3 Typical Git Workflow

Modify → Stage → Commit

The fundamental workflow in Git follows a well-defined pattern:

  1. Modify:
    • You modify files in the working directory
    • Add, delete, or alter contents as needed
  2. Stage (Preparation):
    • You select the changes to include in the next commit using git add
    • This step allows logically grouping changes
    • You can add entire files or even single portions of files (partial staging)
  3. Commit:
    • You create a new permanent snapshot with git commit
    • Add a descriptive message explaining the changes
    • The commit is stored in the local repository

This cycle repeats continuously during development. Periodically, developers synchronize their work with remote repositories through operations like push (sending) and pull (receiving).

How Git tracks changes

Git monitors changes through a sophisticated system of hashing and references:

  1. Object Database:
    • Git stores all data as objects identified by SHA-1 hashes
    • There are four main types of objects: blob (file contents), tree (directory), commit, and tag
  2. Change detection:
    • Git calculates the SHA-1 hash of each file
    • If the content changes, the hash changes
    • By comparing hashes, Git determines which files have been modified
  3. Tracking status:
    • Untracked: files not present in the previous snapshot and not in staging
    • Tracked: files already present in the repository that can be:
      • Unmodified: not changed from the last commit
      • Modified: changed but not yet added to the staging area
      • Staged: changed and added to the staging area
  4. Efficiency:
    • Git only stores complete files that have changed
    • Identical files are stored only once (deduplication)
    • Compression further reduces space usage

This system allows Git to maintain a complete efficient history and rapidly determine what has changed between any pair of commits.

2. Installation and Configuration of Git

2.1 Installation on Different Operating Systems

Git is available for practically all modern operating systems. Here’s how to install it on the main ones:

Windows

There are several options for installing Git on Windows:

  1. Git for Windows (Git SCM):
    • Visit git-scm.com
    • Download and run the installer
    • During installation, you can choose whether to:
      • Use Git from command line and/or from GUI
      • Use Git from Windows Command Prompt
      • Configure line ending conversion
  2. GitHub Desktop:
  3. Windows Subsystem for Linux (WSL):
    • For more advanced users
    • Install a Linux distribution through WSL
    • Use the distribution’s package manager to install Git

macOS

On macOS there are several options:

  1. Homebrew (recommended method):
    brew install git
    
  2. Official installer:
    • Visit git-scm.com
    • Download and run the installation package
  3. Xcode Command Line Tools:
    xcode-select --install
    

    This command installs a version of Git along with other development tools.

Linux

On Linux systems, installation happens through the distribution’s package manager:

Ubuntu/Debian:

sudo apt update
sudo apt install git

Fedora:

sudo dnf install git

CentOS/RHEL:

sudo yum install git

Arch Linux:

sudo pacman -S git

Verifying the installation

After installation, verify that Git is correctly installed by running:

git --version

This command should return the installed Git version, for example git version 2.35.1.

2.2 Initial Configuration

Setting name and email (git config --global)

The first thing to do after installation is to configure your identity, which Git will use for every commit:

git config --global user.name "Your Name"
git config --global user.email "your.email@example.com"

This information is incorporated into every commit you make. Using the --global flag, this configuration applies to all repositories on the system. You can override these settings for specific projects by omitting --global.

Preferred text editor

Git opens a text editor when it requires input, such as when writing commit messages. You can configure your preferred editor:

For VS Code:

git config --global core.editor "code --wait"

For Vim:

git config --global core.editor vim

For Notepad++ (Windows):

git config --global core.editor "'C:/Program Files/Notepad++/notepad++.exe' -multiInst -notabbar -nosession -noPlugin"

Useful aliases to speed up workflow

Aliases allow you to create shorter custom commands for common operations:

# Abbreviation for status
git config --global alias.st status

# Abbreviation for checkout
git config --global alias.co checkout

# Abbreviation for branch
git config --global alias.br branch

# Abbreviation for commit
git config --global alias.ci commit

# One-line graphical log per commit
git config --global alias.lg "log --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit"

With these aliases configured, you can use commands like git st instead of git status.

Other useful configurations

Here are some other useful configurations:

Output coloring:

git config --global color.ui auto

Default branch name (for Git 2.28+):

git config --global init.defaultBranch main

Automatic line ending conversion:

# For Windows
git config --global core.autocrlf true

# For macOS/Linux
git config --global core.autocrlf input

Store credentials (to avoid re-entering them):

# Temporary cache (15 minutes)
git config --global credential.helper cache

# Permanent storage
git config --global credential.helper store

To view all active configurations:

git config --list

 

3. Basic Git Commands

Familiarity with Git’s fundamental commands is like having a good set of tools: they allow you to work effectively without having to stop and Google “how to do X with Git” every time. Let’s explore these essential tools that I use daily.

3.1 Repository Initialization and Cloning

git init – Creating a new repository

When I have an idea for a new project, the first thing I do is initialize a Git repository:

git init

This command creates a hidden .git folder that contains all the magic: the repository structure, commits, configurations. It’s like planting a seed that will grow along with the project. If I want to be more specific, I can also indicate the directory name:

git init my-new-project

git clone – Cloning an existing repository

When I want to work on something that already exists (whether it’s my project on another computer or someone else’s code), I use:

git clone https://github.com/user/repository.git

This not only downloads the code but brings with it the entire project history. It’s like receiving not just a book, but all the author’s notes on how they wrote it. If I want to clone into a specific directory:

git clone https://github.com/user/repository.git directory-name

3.2 Managing Changes

git status – Checking the repository status

This is probably the command I use most often. It shows me exactly where I am:

git status

It tells me which files I’ve modified, which are already ready to be saved (staged), and which are completely new (untracked). It’s like having a compass that tells me where I am in the development process.

git add – Adding files to the staging area

When I’m satisfied with the changes to a file, I add it to the “waiting room” (staging area):

git add file.txt

If I want to add all modified files at once:

git add .

But beware: I use this command with caution. Adding everything indiscriminately can lead to messy commits that include unrelated changes or files I didn’t want to include.

git commit – Creating a commit

After selecting the modified files with git add, I “take a snapshot” by creating a commit:

git commit -m "Added login system"

The commit message is crucial: I always try to write something meaningful that explains why I made the change, not just what I did. If I need to be more detailed:

git commit

This opens the configured editor where I can write a multi-line message, usually with a summary first line and then the details.

git diff – Viewing differences between changes

Before making a commit, I often want to review exactly what I’ve changed:

git diff

This shows me the differences between the modified files and the last saved version. If I want to see the differences only for files already added to the staging area:

git diff --staged

3.3 Commit History and Logs

git log – Viewing commit history

To see the project history:

git log

This shows all commits with author, date, and message. If I prefer a more compact version:

git log --oneline

Or if I want to see the changes introduced in each commit:

git log -p

git show – Details of a specific commit

When I want to examine a particular commit:

git show abc123

where abc123 is the commit identifier (hash). This shows me all the details: message, author, date, and the changes made.

Filtering history (by author, date, message)

A project’s history can become very long, so it’s useful to know how to filter it:

# Commits by a specific author
git log --author="John Smith"

# Commits with a specific word in the message
git log --grep="bugfix"

# Commits from the last 7 days
git log --since="7 days ago"

# Commits that modified a specific file
git log -- file.txt

# Graphical view of branches
git log --graph --oneline --all

These basic commands are the building blocks with which I construct my daily workflow. At first, they may seem like a lot to remember, but with practice, they become automatic, like shifting gears while driving.

4. Branching and Merging in Git

4.1 Introduction to Branches

What are branches and why use them

Branches in Git are simply movable pointers to commits. Imagine a development timeline that forks into multiple directions, each independent from the other. This is exactly what happens when we create a branch.

I’ve learned to see branches as safe spaces where I can experiment without fear. I used to work directly on the main branch (formerly called “master”, now more commonly “main”) and how many times did I break everything! Now I create a dedicated branch even for the smallest change. It’s like having a room where I can make all the mess I want, and then decide if and when to “bring” the changes back to the main room.

Branches are mainly used for:

  • Developing new features in isolation
  • Fixing bugs without interfering with other activities
  • Experimenting with risky ideas
  • Maintaining different versions of the software

Branch vs Fork

I’m often asked what the difference is between a branch and a fork. It’s a subtle but important distinction:

A branch is a divergence within the same repository. It’s quick to create, easy to manage, and primarily designed for working in teams on the same project.

A fork is a complete copy of a repository in a new space. You create a fork when you want to develop independently from the original project, often because you don’t have write permissions to the original repository or because you want to take the project in a completely different direction. Forks are typical in the open source development model, where anyone can create their own version of a project.

My personal rule? I use branches when working within my team and forks when I want to contribute to external projects.

4.2 Branch Management

git branch – Creating, listing and deleting branches

The git branch command is our main tool for branch management:

# List all local branches
git branch

# List all branches (local and remote)
git branch -a

# Create a new branch (without switching to it)
git branch new-feature

# Delete a branch (if already merged)
git branch -d completed-feature

# Force delete a branch (even if not merged)
git branch -D abandoned-feature

When I show the result of git branch in my presentations, I always highlight the asterisk that indicates the active branch. It’s a small detail that makes all the difference in orientation.

git checkout / git switch – Changing branches

To move between branches, historically git checkout has been used:

# Switch to an existing branch
git checkout feature-xyz

# Create a new branch and switch to it in a single command
git checkout -b new-feature

In more recent versions of Git (2.23+), git switch was introduced to make commands more intuitive:

# Switch to an existing branch
git switch feature-xyz

# Create a new branch and switch to it
git switch -c new-feature

I personally prefer switch because it’s clearer: “I’m switching to another branch”. The checkout command had too many responsibilities and created confusion, especially for beginners.

git merge – Uniting branches

When development in a branch is complete, you want to bring the changes back to the main branch. This is done with git merge:

# First switch to the destination branch
git checkout main

# Then merge the source branch
git merge feature-xyz

Git handles merging in two main ways:

  1. Fast-forward: If there have been no new commits in the destination branch since the source branch was created, Git simply moves the pointer forward. It’s as if you had worked directly on the main branch.
  2. Merge commit: If there have been parallel commits in both branches, Git creates a new commit that unites the two lines of development. This commit has two parents.

I like to use git merge --no-ff feature-xyz to always create a merge commit, even when a fast-forward would be possible. This makes the project history clearer, especially in large teams.

git rebase – Rearranging commit history

Rebase is perhaps the most powerful (and dangerous) operation in Git. Instead of creating a merge commit, git rebase rewrites history by applying your commits one by one on top of the destination branch:

# First switch to the branch you want to rearrange
git checkout feature-xyz

# Then rebase onto the destination branch
git rebase main

The result is a linear history, as if you had written all the code in sequence without ever creating a branch. It’s great for maintaining a clean history, but remember the golden rule: don’t rebase public branches (i.e., branches that others have already downloaded).

I’ve learned the hard way how frustrating it can be to resolve conflicts after a failed rebase. But over time I’ve developed a personal workflow: I use rebase for my personal work branches and merge for branches shared with the team.

4.3 Conflict Resolution

How to identify a conflict

A conflict in Git occurs when the same piece of code has been modified in different ways in two branches you’re trying to merge. Git is smart, but it can’t decide which version is the “right” one.

When a conflict occurs, Git stops the merge or rebase operation and shows a message like:

CONFLICT (content): Merge conflict in file.txt
Automatic merge failed; fix conflicts and then commit the result.

In the conflicting file, Git inserts special markers:

<<<<<<< HEAD
Version in the current branch
=======
Version in the other branch
>>>>>>> feature-xyz

Tools for resolution (merge tools)

Resolving conflicts directly in the files can be complicated. Fortunately, there are tools that make the process more visual:

  • Integrated Git: git mergetool launches the configured tool
  • Code editors: VSCode, Atom, Sublime Text have great Git integrations
  • Dedicated tools: GitKraken, Sourcetree, Beyond Compare

Personally, I use VSCode for simple conflicts and Beyond Compare for more complex ones. The three-panel view (your version, their version, the result) has saved me countless times.

Best practices to avoid conflicts

Prevention is better than cure. Here are some practices I’ve adopted to minimize conflicts:

  1. Small and frequent commits: The smaller the commits, the easier it will be to resolve any conflicts.
  2. Regular pulls: Frequently update your branch with changes from the main branch.
  3. Team communication: Coordinate on who is working on which files.
  4. Modularization: Organize code so that different features touch different files.
  5. Short-lived branches: The longer a branch lives, the more likely conflicts are.

5. Working with Remote Repositories

5.1 Key Concepts

Remote, origin, upstream

Working with Git isn’t just about managing code locally. The real magic happens when multiple people collaborate, and this is where remote repositories come into play.

  • Remote: A remote repository is a version of your project hosted on the internet or on a network.
  • Origin: By convention, the repository you cloned from is called “origin”.
  • Upstream: When working with a fork, “upstream” refers to the original repository.

I like to think of remotes as intelligent backups, which not only save my work but also allow me to collaborate with others. How many times has my laptop decided to die the day before a deadline! But with all the code on a remote, I could simply clone the repository to another computer and continue working as if nothing had happened.

Push and Pull

The workflow with remotes is based on two fundamental operations:

  • Push: Send local commits to the remote repository.
  • Pull: Download and integrate remote commits into the local repository.

With experience, I’ve learned that “pull early, pull often” is a golden rule to avoid surprises when working in a team.

5.2 Commands for Remote Management

git remote – Managing remote repositories

# List remote repositories
git remote -v

# Add a new remote
git remote add origin https://github.com/user/repository.git

# Add an original repository as upstream (for forks)
git remote add upstream https://github.com/original-project/repository.git

# Change a remote's URL
git remote set-url origin https://new-url.git

# Remove a remote
git remote remove remote-name

git fetch – Download changes without merging

git fetch is like saying “show me what’s new, but don’t touch anything”:

# Download all updates from all remotes
git fetch --all

# Download updates from a specific remote
git fetch origin

I often use git fetch followed by git log origin/main..HEAD to see which commits I’ve made locally that aren’t yet on the remote.

git pull – Download and merge changes

git pull is essentially a git fetch followed by a git merge:

# Download and merge changes to the current branch
git pull

# Equivalent to:
git fetch origin
git merge origin/current-branch-name

# Pull with rebase instead of merge
git pull --rebase

The --rebase flag is my favorite when working alone on a project. It keeps the history clean by avoiding unnecessary merge commits.

git push – Upload changes to the remote

# Upload local commits to the remote
git push

# Specify the remote and branch
git push origin main

# Set tracking for a new branch
git push -u origin new-branch

# Forced push (use with GREAT caution)
git push --force

A personal tip: never use git push --force on shared branches. If you must, use git push --force-with-lease which will fail if there are remote changes you haven’t yet downloaded. It has saved me multiple times from erasing colleagues’ work.

5.3 Hosting Platforms (GitHub, GitLab, Bitbucket)

Main differences

There are several platforms for hosting Git repositories, each with its strengths:

GitHub:

  • The most popular with the largest community
  • Clean and intuitive interface
  • Great integration with third-party tools
  • GitHub Actions for CI/CD
  • Free packages for students and open source projects

GitLab:

  • Available both as a cloud service and self-hosted
  • Natively integrated CI/CD
  • Complete suite of DevOps tools
  • Greater control over permissions
  • Generous free plan even for private repositories

Bitbucket:

  • Great integration with other Atlassian products (Jira, Confluence)
  • Ideal for teams already using Atlassian tools
  • Integrated CI/CD pipelines
  • Free plan with limits on the number of users

I’ve worked with all three, and my personal preference is GitHub for open source projects and GitLab for company projects. The latter, with its self-hosting capability, has allowed me to implement Git even in companies with very strict security policies.

Typical workflow (e.g. Fork + Pull Request)

The Fork + Pull Request model has become the de facto standard for contributing to open source projects:

  1. Fork: Create a personal copy of the original repository on the platform.
  2. Clone: Download your fork to your computer.
  3. Branch: Create a branch for your modification.
  4. Commit: Develop and commit your changes.
  5. Push: Upload your branch to your fork.
  6. Pull Request (PR): Request that your changes be incorporated into the original repository.
  7. Review: Other developers examine your changes and suggest improvements.
  8. Merge: If approved, your code is merged into the original project.

I’ve learned that a well-made PR is almost an art: clear description, screenshots if necessary, references to related issues, and most importantly, clean and well-tested code. There’s no greater satisfaction than seeing your PR approved and merged into a project you love!

In my team, we’ve also adopted the PR practice for internal projects, even when technically we could commit directly. This ensures that at least one other person has seen the code before it enters the main branch, drastically reducing bugs.

6. Advanced Git Techniques

So far we’ve explored the basic functionality of Git, but it’s when you start mastering advanced techniques that Git becomes truly powerful. In this section, I’ll share some tools that have saved countless hours of my development time.

6.1 Git Stash

How many times have you been in the middle of making changes when suddenly you need to switch to another urgent task? This is where git stash comes in, one of my personal lifesavers.

git stash

This command temporarily sets aside all uncommitted changes, allowing you to switch to something else without losing your work.

When you’re ready to resume your interrupted work, you have several options:

git stash apply    # Reapplies the changes, but keeps the stash
git stash pop      # Reapplies the changes and removes the stash
git stash drop     # Deletes the stash without reapplying

A trick I’ve learned over time: you can also have multiple stashes simultaneously and manage them with git stash list. To apply a specific one, use git stash apply stash@{n}.

6.2 Git Tags

Tags are like bookmarks in your project’s history, and they’re particularly useful for marking releases. There are two types of tags:

  1. Lightweight tags: simple pointers to a commit
    git tag v1.0-beta
    
  2. Annotated tags: contain additional metadata such as author, date, and message
    git tag -a v1.0 -m "Version 1.0 Release"
    

Personally, I always prefer to use annotated tags for official releases; the ability to include a detailed message has proven valuable countless times when I needed to remember exactly what a particular version included.

Remember that you need to explicitly push tags to the remote repository:

git push origin v1.0    # Push a specific tag
git push --tags         # Push all tags

6.3 Git Hooks

Git hooks are like small automated assistants that run scripts at specific moments. The first time I discovered them was a revelation!

Hooks are located in the .git/hooks/ directory and can be triggered before or after events such as commits, pushes, merges, and more.

Here’s a practical example I use daily: a pre-commit hook that runs a linter on all modified code:

#!/bin/sh
# Pre-commit hook to run ESLint

FILES=$(git diff --cached --name-only --diff-filter=ACMR | grep '\.js$')
if [ "$FILES" = "" ]; then
    exit 0
fi

echo "Running ESLint..."
npx eslint $FILES

if [ $? -ne 0 ]; then
    echo "ESLint detected errors. Commit aborted."
    exit 1
fi

This script checks if there are errors in the modified JavaScript files and blocks the commit if it finds any. It has saved me from countless subsequent fixes!

6.4 Git Bisect

Have you ever had to search for the exact commit that introduced a bug? git bisect is like a private detective using binary search to find the culprit.

git bisect start
git bisect bad      # Mark the current commit as "bad"
git bisect good v1.0    # Mark a previous commit as "good"

Git will take you to an intermediate commit, and after testing it, you declare it good or bad:

git bisect good    # If the bug is not present
git bisect bad     # If the bug is present

Git will continue to guide you through commits until it finds the one that introduced the problem. When you’re done:

git bisect reset

The first time I used this feature to find an elusive bug, it literally saved me hours of debugging!

6.5 Submodules and Subtrees

When working with projects that depend on other repositories, you have two main options:

Submodules

Submodules allow you to include one repository inside another:

git submodule add https://github.com/example/library.git libs/library

The main repository only stores a pointer to the specific commit of the submodule. Updating all submodules is easy:

git submodule update --remote

Subtrees

Subtrees, on the other hand, effectively import external code into your repository:

git subtree add --prefix=libs/library https://github.com/example/library.git master --squash

When to use which? In my experience, submodules are ideal when the external project is in active development and you want to keep it separate. Subtrees are simpler to use for collaborators but make managing updates more complex.

7. Best Practices and Efficient Workflows

Over time, I’ve learned that Git’s true strength isn’t just in its features, but in how you use them. Here are some best practices that make collaboration much smoother.

7.1 Conventional Commits

Adopting a standard for commit messages has radically changed how I interact with the project history. Conventional Commits follow this format:

type(scope): description

body [optional]

footer [optional]

Where “type” can be:

  • feat: new feature
  • fix: bug fix
  • docs: documentation changes
  • style: formatting changes
  • refactor: code rewriting without functional changes
  • test: adding or modifying tests
  • chore: changes to the build process or auxiliary tools

A concrete example:

fix(auth): fixed token validation error

The token was not being properly validated when it contained special characters.

Resolves: #123

This approach makes it immediately clear what each commit does, facilitating automatic changelog generation and semantic versioning.

7.2 Git Flow and GitHub Flow

There are different workflow models for Git, but the two most popular are:

Git Flow

A complex but powerful model with dedicated branches:

  • master: production code
  • develop: stable development code
  • feature/*: new features
  • release/*: release preparation
  • hotfix/*: urgent fixes

I’ve used Git Flow in large projects with scheduled release cycles, and it’s excellent for maintaining order.

GitHub Flow

A simpler model:

  • The main branch is always deployable
  • New work happens in feature branches
  • Pull Requests initiate discussions
  • Merge into main after approval

This approach, which I use in smaller projects or with continuous deployment, is simpler but requires greater discipline in keeping the main branch always stable.

The choice between the two depends on team size and release frequency. For small teams with frequent deployments, I recommend GitHub Flow; for larger projects with planned releases, Git Flow offers more structure.

7.3 Pull Requests and Code Review

Pull Requests (PRs) are the heart of modern collaboration. Here are some practices I’ve found particularly effective:

  1. Keep PRs small and focused. Giant PRs are difficult to review and often remain pending for days.
  2. Write detailed descriptions. A good PR description should explain:
    • What changes
    • Why it’s necessary
    • How to test the changes
  3. Use commenting tools. GitHub and similar platforms allow you to comment on specific lines of code. This makes discussions much more precise.
  4. Automate verifications. Set up CI/CD to automatically run tests and linting on each PR.

Regarding code reviews, I’ve learned that tone is as important as content. Phrases like “Why not consider…” instead of “This is wrong” make a big difference in team atmosphere.

GitHub also offers features like “Suggested changes” that allow you to propose corrections directly in the comment. It’s a tool I use constantly and that significantly speeds up the review process.

Finally, remember that code reviews don’t just serve to find bugs, but also to share knowledge and improve as a team. Some of the most valuable lessons I’ve learned have come from careful reviews of my code by experienced colleagues.

8. Troubleshooting Common Problems

If you’ve reached this section, you’ve probably already had some headaches with Git. Don’t worry, it happens to all of us! Even after years of using it, I regularly find myself searching for solutions to unexpected situations. Let’s look at how to handle the most common problems.

8.1 Modifying Commit History

One of Git’s most powerful (and potentially dangerous) features is the ability to modify commit history. Be careful though: modifying history that’s already been shared with others can create enormous problems for your collaborators. The golden rule is: only modify what’s local and not yet pushed.

git commit --amend

How many times have you made a commit and immediately noticed a typo or a forgotten file? It happens to me all the time! The --amend command is your best friend in these cases:

# Add the forgotten changes
git add forgotten_file.txt

# Modify the last commit
git commit --amend

This will open the editor to allow you to modify the commit message as well. If you want to keep the same message, you can use:

git commit --amend --no-edit

git rebase -i (interactive rebase)

Interactive rebase is probably the most powerful tool Git offers for cleaning up history. It allows you to reorder, combine, modify, or delete commits before sharing them with the rest of the team.

# Modify the last 3 commits
git rebase -i HEAD~3

This will open an editor with a list of commits, where you can specify what to do with each one:

  • pick: keep the commit as is
  • reword: only modify the message
  • edit: stop the rebase to modify the content
  • squash: combine with the previous commit (keeps both messages)
  • fixup: like squash, but discards the message
  • drop: delete the commit

I often use squash to group related commits before pushing, making the history cleaner and more understandable.

git reset (soft, mixed, hard)

When things get really complicated, git reset is your wild card, but use it with caution:

# Soft reset: undoes the commit but keeps changes in the staging area
git reset --soft HEAD~1

# Mixed reset (default): undoes the commit and removes from staging area
git reset HEAD~1  # or git reset --mixed HEAD~1

# Hard reset: undoes the commit and all changes
git reset --hard HEAD~1

Hard reset is particularly dangerous because it permanently deletes changes. I’ve used it many times only to regret it bitterly. If in doubt, always prefer the less destructive option.

8.2 Restoring Files or Commits

git checkout -- <file> – Reverting local changes

Have you modified a file and want to go back to the version from the last commit? No panic:

git checkout -- path/to/file.txt

This command overwrites local changes with the version from the last commit. In modern Git, you can also use the more intuitive:

git restore path/to/file.txt

git revert – Creating a reverse commit

Unlike reset, revert doesn’t rewrite history but creates a new commit that undoes the changes of a previous commit:

git revert HEAD

This is the safest method to undo changes already shared with others, because it doesn’t cause conflicts in collaborators’ repositories.

git reflog – Recovering deleted commits

I’ve often executed a reset --hard and then realized I deleted something important. The reference log (reflog) is a kind of safety net that records all movements of the HEAD:

git reflog

Once you’ve identified the hash of the lost commit, you can recover it with:

git checkout <commit-hash>

And then create a new branch to save it:

git checkout -b recovered-branch

The reflog has saved my life more times than I’d like to admit!

8.3 Authentication Problems (SSH vs HTTPS)

Authentication problems are among the most frustrating, especially for beginners.

Configuring SSH keys

SSH authentication is the method I prefer because, once configured, it no longer requires entering passwords:

  1. Generate a new SSH key (if you don’t already have one):
    ssh-keygen -t ed25519 -C "your.email@example.com"
    
  2. Add the key to the ssh-agent:
    eval "$(ssh-agent -s)"
    ssh-add ~/.ssh/id_ed25519
    
  3. Copy the public key and add it to your GitHub/GitLab account:
    cat ~/.ssh/id_ed25519.pub
    
  4. Test the connection:
    ssh -T git@github.com
    

Personal Access Token (PAT) on GitHub/GitLab

If you prefer HTTPS, since 2021 GitHub and many other services no longer accept simple passwords:

  1. Generate a Personal Access Token from your account settings
  2. Use this token instead of the password when Git requests it
  3. To avoid entering it every time, you can configure the credential helper:
    git config --global credential.helper cache  # temporary (15 min)# orgit config --global credential.helper store  # permanent but less secure
    

Personally, I always recommend SSH if possible: a more complex initial configuration, but much more convenience in the long run.

9. Integrating Git with Other Tools

In my daily workflow, Git is rarely used alone, but integrates with numerous other tools that amplify its potential.

Git and IDEs (VSCode, IntelliJ, Eclipse)

Modern IDEs offer excellent integration with Git:

VSCode: My favorite IDE for many projects. The Git integration is fantastic thanks to the visual interface for managing branches, commits, and resolving conflicts. Colored badges in the gutter show changes, additions, and removals in real-time.

IntelliJ IDEA/WebStorm/PyCharm: The JetBrains suite offers even more powerful Git tools, such as advanced history visualizations and powerful merge tools. I particularly love the “Shelf” function, which is like a more advanced local stash system.

Eclipse: Less elegant than modern alternatives, but still functional with its EGit plugin for basic Git operations.

My advice is to learn the Git commands from the terminal first, and only then move to the graphical interface. It will save you in those situations where the IDE isn’t enough!

Git and CI/CD (GitHub Actions, GitLab CI)

Integration with CI/CD systems has revolutionized my way of working with Git:

GitHub Actions: By configuring YAML files in the .github/workflows directory, you can automate testing, building, and deployment with every push or pull request. The ability to create custom workflows is practically infinite.

GitLab CI: Similar but with some differences, it’s configured via the .gitlab-ci.yml file. I find its runner system particularly flexible, especially if you need to run jobs on specific hardware.

In my projects, I often use branch protection rules linked to CI/CD to prevent merging code that doesn’t pass automated tests. A safety net that’s worth every minute spent on configuration!

Git with Docker and containerized environments

Git and Docker complement each other perfectly:

  • You can version Dockerfiles and Docker Compose configuration files
  • You can use Git hooks to automate container builds after specific commits
  • The combination of Git + Docker + CI/CD allows you to implement a completely automated workflow

A pattern I often use is to have Git trigger a CI/CD pipeline, which in turn builds Docker containers, tests them, and publishes them to a registry – all without human intervention.

10. Conclusions and Useful Resources

Summary of key concepts

In this journey through Git, we’ve explored:

  • The fundamental concepts of distributed version control
  • The daily workflow with Git
  • Branching and collaboration strategies
  • How to solve the most common problems
  • How to integrate Git with other tools

Always remember that Git is a very powerful tool, but it requires practice and patience. The initial difficulties will pay off enormously in the long run. Personally, I can no longer imagine working on a project without Git.

Next steps to become a Git expert

If you want to delve deeper, here’s what I suggest:

  1. Explore Git hooks: Automate actions in response to Git events
  2. Contribute to open-source projects: There’s no better way to learn Git than by collaborating with other developers
  3. Customize your Git configuration: Aliases, colors, custom tools
  4. Deepen your understanding of internal mechanisms: Study how Git stores information, how blob, tree, and commit objects work

Finally, remember that Git is a tool, not an end. The goal is to develop better software, collaborate more effectively, and reduce stress. If an advanced Git feature complicates your life instead of simplifying it, there’s probably a better approach.

Good luck with Git, and don’t forget to commit often!