Wednesday

18-06-2025 Vol 19

I Thought I Knew Git—Until I Started Using It Like a Senior Engineer

I Thought I Knew Git—Until I Started Using It Like a Senior Engineer

Git is a version control system that’s ubiquitous in software development. Most developers learn the basics fairly quickly: git init, git add, git commit, git push. You might even throw in some branching and merging. But mastering Git is a journey, not a destination. It’s easy to feel like you understand Git until you encounter complex scenarios or work on large, collaborative projects. This is when the gap between basic knowledge and senior-level proficiency becomes apparent. This article explores that gap, highlighting the Git skills and workflows that separate junior developers from senior engineers. We’ll dive deep into topics like rebasing, interactive staging, advanced branching strategies, and more. Get ready to level up your Git game!

Table of Contents

  1. Introduction
  2. The Basics I Thought I Knew
    1. init, add, commit, push: The Standard Flow
    2. Branching and Merging: A Simple Understanding
    3. Handling Conflicts: The Occasional Headache
  3. Beyond the Basics: Git Skills of a Senior Engineer
    1. Rebasing: Rewriting History with Purpose
      1. Interactive Rebasing: Fine-Grained Control
      2. When to Rebase (and When Not To)
    2. Interactive Staging: Selective Commits
    3. Advanced Branching Strategies: Gitflow, GitHub Flow, etc.
      1. Gitflow: For Structured Release Cycles
      2. GitHub Flow: For Continuous Deployment
      3. GitLab Flow: A More Flexible Approach
    4. Git Hooks: Automating Workflows
      1. Client-Side Hooks: pre-commit, pre-push
      2. Server-Side Hooks: update, post-receive
    5. git bisect: Finding the Culprit Commit
    6. Git Submodules: Managing External Dependencies
    7. Git LFS: Large File Storage
  4. Collaborative Git Practices
    1. Code Reviews: Pull Requests and Their Importance
    2. Branching Naming Conventions for Clarity
    3. Commit Message Etiquette: Clear and Concise Descriptions
  5. Dealing with Disasters: Recovery Strategies
    1. Recovering from Accidental Commits: amend, reset
    2. Undoing Merges and Rebases: The Reflog to the Rescue
    3. Dealing with Stolen or Lost Commits
  6. Performance and Optimization
    1. Managing Large Repositories
    2. Shallow Clones for Faster Downloads
    3. gc: Garbage Collection for Efficiency
  7. Git GUI Clients and Tools
    1. Visual Studio Code: Integrated Git Support
    2. Sourcetree: A User-Friendly Git GUI
    3. GitKraken: A Feature-Rich Cross-Platform Client
  8. Best Practices for Senior Git Users
    1. Staying Updated with Git Releases and Features
    2. Mentoring Junior Developers: Sharing Git Knowledge
    3. Contributing to Open-Source Git Projects
  9. Conclusion

Introduction

We’ve all been there: the initial excitement of using Git wears off, and you find yourself stuck in a cycle of basic commands. You can commit, push, and pull, but when things get complicated – like resolving complex merge conflicts or needing to rewrite commit history – you’re left scratching your head. This article isn’t about shaming anyone’s current Git knowledge. It’s about bridging the gap between a functional understanding of Git and the mastery that senior engineers demonstrate. We’ll explore how seasoned developers leverage Git to not only manage code but also to streamline workflows, improve collaboration, and even recover from potential disasters. Let’s embark on this journey to unlock the full potential of Git and elevate your skills to the next level.

The Basics I Thought I Knew

Before we dive into the advanced topics, let’s recap the fundamental Git commands that most developers learn early on. This will serve as a baseline for understanding the more complex concepts we’ll cover later.

init, add, commit, push: The Standard Flow

These are the bread and butter of Git. They form the core of your daily workflow.

  • git init: This command initializes a new Git repository in a directory. It creates a .git subdirectory that contains all the necessary repository metadata.
  • git add: This command stages changes for the next commit. You can add specific files (git add file.txt) or all modified files (git add .).
  • git commit: This command creates a snapshot of the staged changes along with a descriptive message. A good commit message explains why the changes were made, not just what was changed.
  • git push: This command uploads your local commits to a remote repository (e.g., on GitHub, GitLab, or Bitbucket). It usually requires specifying the remote name and the branch to push to (e.g., git push origin main).

While these commands are essential, relying solely on them can lead to a messy and unorganized commit history, especially in collaborative projects.

Branching and Merging: A Simple Understanding

Branching allows you to create parallel lines of development. This is crucial for working on new features or bug fixes without disrupting the main codebase.

  • git branch: This command creates, lists, or deletes branches. git branch feature/new-feature creates a new branch named “feature/new-feature”.
  • git checkout: This command switches between branches. git checkout feature/new-feature switches to the “feature/new-feature” branch.
  • git merge: This command combines changes from one branch into another. From the target branch (e.g., main), you would run git merge feature/new-feature to merge the changes from the feature branch into main.

Simple branching and merging are fundamental, but they often lead to messy merge histories, especially when dealing with long-lived feature branches. This is where more advanced techniques like rebasing come into play.

Handling Conflicts: The Occasional Headache

Merge conflicts arise when Git cannot automatically resolve differences between branches. This usually happens when the same lines of code have been modified in different branches.

  • Identifying conflicts: Git will mark conflicting areas in your files with special markers (<<<<<<<, =======, >>>>>>>).
  • Resolving conflicts: You need to manually edit the conflicting files to choose the correct changes and remove the conflict markers.
  • Staging and committing: After resolving the conflicts, you need to stage the resolved files (git add) and create a commit to complete the merge.

While resolving conflicts is a necessary skill, senior engineers aim to minimize conflicts by adopting effective branching strategies and communicating effectively with their team.

Beyond the Basics: Git Skills of a Senior Engineer

Now, let's explore the Git techniques that distinguish senior engineers from those with just a basic understanding. These skills are crucial for managing complex projects, collaborating effectively, and maintaining a clean and understandable commit history.

Rebasing: Rewriting History with Purpose

Rebasing is a powerful technique that allows you to rewrite the commit history of a branch. It involves moving a branch's starting point to a different commit. This can be useful for cleaning up messy commit histories, integrating changes from other branches, and avoiding complex merge commits.

The basic command is: git rebase <target-branch>. For example, if you're on a feature branch and want to rebase it onto the main branch, you would run git rebase main.

Interactive Rebasing: Fine-Grained Control

Interactive rebasing (git rebase -i <commit-hash>) provides even more control over the rebasing process. It allows you to:

  1. Reorder commits: Change the order of commits in your branch.
  2. Combine (squash) commits: Merge multiple commits into a single commit. This is useful for cleaning up small, incremental commits.
  3. Edit commit messages: Modify the messages of existing commits. This is helpful for clarifying commit descriptions or correcting mistakes.
  4. Drop commits: Remove unnecessary or redundant commits.
  5. Split commits: Divide a single commit into multiple smaller commits. This can be useful if a commit contains unrelated changes.

When you run git rebase -i <commit-hash>, Git will open a text editor with a list of commits in the branch. You can then modify the list by changing the commands (e.g., pick, squash, edit, drop) next to each commit. Save and close the editor to execute the rebase.

When to Rebase (and When Not To)

Rebasing is a powerful tool, but it should be used with caution. Never rebase commits that have been pushed to a public branch. Rebasing changes the commit history, which can cause serious problems for other developers who have based their work on the original commits.

Here are some situations where rebasing is appropriate:

  • Cleaning up local feature branches: Rebasing a feature branch onto main before merging can create a linear and easy-to-follow history.
  • Squashing commits before a pull request: Combining small, incremental commits into a single, well-described commit makes the code review process easier.
  • Integrating changes from main into a feature branch: Rebasing your feature branch onto the latest main branch can help avoid merge conflicts and keep your branch up-to-date. An alternative is merging main into your feature branch, however that leads to a non-linear history.

Here are situations where rebasing should be avoided:

  • Public branches: As mentioned earlier, rebasing commits that have been pushed to a public branch is a big no-no.
  • Shared branches: If multiple developers are working on the same branch, rebasing can cause confusion and conflicts.

In general, if you're unsure whether to rebase, err on the side of caution and use merging instead.

Interactive Staging: Selective Commits

Interactive staging (git add -p or git add --patch) allows you to selectively stage changes from a file. Instead of staging the entire file, you can review each change and choose whether or not to include it in the next commit.

This is extremely useful when you've made multiple unrelated changes to a file and want to commit them separately. It helps to keep your commits focused and atomic.

When you run git add -p, Git will present you with each change in the file and ask you whether to stage it. You can answer with:

  • y: Stage this change.
  • n: Do not stage this change.
  • q: Quit; do not stage this change or any of the remaining ones.
  • a: Stage this change and all the remaining ones in the file.
  • d: Do not stage this change or any of the remaining ones in the file.
  • s: Split the current change into smaller hunks.
  • e: Manually edit the current change.
  • ?: Print help.

Interactive staging is a powerful tool for creating clean and well-organized commits. It forces you to think about each change you're making and helps to avoid accidentally committing unrelated changes.

Advanced Branching Strategies: Gitflow, GitHub Flow, etc.

Branching strategies define how branches are used in a Git repository. They provide a framework for managing development, releases, and hotfixes. Choosing the right branching strategy can significantly improve collaboration and streamline your workflow.

Here are some popular branching strategies:

Gitflow: For Structured Release Cycles

Gitflow is a branching model designed for projects with structured release cycles. It defines specific branches for different purposes:

  • main: Contains the production-ready code.
  • develop: Serves as the integration branch for all feature development.
  • feature/*: Used for developing new features. Branches off of develop and merges back into develop.
  • release/*: Used for preparing a new release. Branches off of develop and merges into both main and develop.
  • hotfix/*: Used for fixing bugs in production. Branches off of main and merges into both main and develop.

Gitflow is a good choice for projects that require strict versioning and have well-defined release cycles. However, it can be complex to manage, especially for smaller projects or teams.

GitHub Flow: For Continuous Deployment

GitHub Flow is a simpler branching model that's well-suited for projects that use continuous deployment. It's based on the idea that anything in the main branch is deployable.

  • main: Contains the production-ready code.
  • feature/*: Used for developing new features. Branches off of main and merges back into main after a code review.

GitHub Flow is much simpler than Gitflow and is easier to manage. It's a good choice for projects that deploy frequently and don't require strict versioning.

GitLab Flow: A More Flexible Approach

GitLab Flow is a branching model that aims to be more flexible than Gitflow and GitHub Flow. It offers different strategies for different situations, such as feature development, release management, and hotfixes.

GitLab Flow typically involves the following branches:

  • main: Contains the production-ready code.
  • feature/*: Used for developing new features. Branches off of main and merges back into main (or a dedicated integration branch).
  • pre-production or staging: Used for testing and staging releases before deploying to production. (Optional)
  • production: Mirrors the main branch and is tagged with release versions. (Optional, but recommended)

GitLab Flow allows teams to choose the branching strategy that best fits their needs. It's a good choice for projects that require a balance between structure and flexibility.

Git Hooks: Automating Workflows

Git hooks are scripts that run automatically before or after certain Git events, such as committing, pushing, or receiving changes. They allow you to automate tasks, enforce policies, and customize your Git workflow.

Git hooks are stored in the .git/hooks directory of your repository. They are simple shell scripts (or scripts written in any language) that are executed by Git.

Client-Side Hooks: pre-commit, pre-push

Client-side hooks run on the developer's local machine.

  • pre-commit: Runs before a commit is created. It can be used to check for code style violations, run unit tests, or prevent commits with invalid commit messages.
  • pre-push: Runs before a push is executed. It can be used to run integration tests, check for security vulnerabilities, or prevent pushes to the main branch.

Example pre-commit hook (using Python and flake8 for code style checking):

```python
#!/usr/bin/env python3

import subprocess
import sys

try:
subprocess.check_call(['flake8'])
sys.exit(0)
except subprocess.CalledProcessError as e:
print(f"Flake8 found errors:\n{e.output.decode()}")
sys.exit(1)
```

This script runs flake8 to check for code style errors. If flake8 finds any errors, the script will exit with a non-zero exit code, which will prevent the commit from being created.

Server-Side Hooks: update, post-receive

Server-side hooks run on the Git server.

  • update: Runs before a branch is updated. It can be used to enforce access control policies or prevent certain types of changes from being pushed.
  • post-receive: Runs after a push has been completed. It can be used to trigger deployments, send notifications, or update issue trackers.

Git hooks are a powerful tool for automating tasks and enforcing policies. They can significantly improve the efficiency and quality of your Git workflow.

git bisect: Finding the Culprit Commit

git bisect is a powerful tool for finding the commit that introduced a bug. It uses a binary search algorithm to quickly narrow down the range of commits that could contain the bug.

To use git bisect, you need to identify a "good" commit (a commit that does not contain the bug) and a "bad" commit (a commit that does contain the bug).

Here's how to use git bisect:

  1. Start the bisect session: git bisect start
  2. Mark the current commit as bad: git bisect bad (or git bisect bad <commit-hash> if you're not currently on the bad commit)
  3. Mark a known good commit: git bisect good <commit-hash>
  4. Git will then check out a commit in the middle of the range. Test whether this commit is good or bad and mark it accordingly: git bisect good or git bisect bad
  5. Repeat step 4 until Git identifies the commit that introduced the bug.
  6. End the bisect session: git bisect reset

git bisect can significantly reduce the time it takes to find the commit that introduced a bug, especially in large repositories with a long commit history.

Git Submodules: Managing External Dependencies

Git submodules allow you to include other Git repositories as subdirectories within your main repository. This is useful for managing external dependencies, such as libraries or frameworks.

To add a submodule:

  1. git submodule add <repository-url> <path> (e.g., git submodule add https://github.com/example/library.git lib)
  2. Commit the changes to your main repository. This will create a .gitmodules file that contains information about the submodules.

To clone a repository with submodules:

  1. git clone --recursive <repository-url> (This will clone the main repository and initialize and update the submodules.)
  2. Alternatively, if you've already cloned the repository without the --recursive option, you can initialize and update the submodules manually:
    • git submodule init
    • git submodule update

Git submodules can be tricky to manage, especially when dealing with updates and changes to the submodules. It's important to understand how submodules work and to follow best practices to avoid problems.

Git LFS: Large File Storage

Git LFS (Large File Storage) is an extension to Git that allows you to store large files (e.g., audio files, video files, images, datasets) outside of your Git repository. Instead of storing the actual file content in Git, Git LFS stores pointers to the files, which are stored on a separate server.

This is useful for keeping your Git repository small and efficient, especially when dealing with large binary files.

To use Git LFS:

  1. Install Git LFS: git lfs install
  2. Track the files you want to store with Git LFS: git lfs track "*.psd" (This will track all files with the .psd extension.)
  3. Commit the .gitattributes file (which contains the Git LFS tracking information) to your repository.
  4. Push your changes to the remote repository.

Git LFS requires a Git LFS server to store the large files. Many Git hosting providers (e.g., GitHub, GitLab, Bitbucket) offer Git LFS support.

Collaborative Git Practices

Git is a powerful tool for collaboration, but it's important to follow best practices to ensure a smooth and efficient workflow.

Code Reviews: Pull Requests and Their Importance

Code reviews are an essential part of the software development process. They help to improve code quality, reduce bugs, and share knowledge among team members.

Pull requests (also known as merge requests) are a mechanism for initiating code reviews. When you're ready to merge your changes into the main codebase, you create a pull request, which notifies other team members that your code is ready for review.

During the code review process, reviewers will examine your code, provide feedback, and suggest changes. It's important to be open to feedback and to address any issues raised by the reviewers.

Once the code review is complete and all issues have been resolved, the pull request can be merged.

Branching Naming Conventions for Clarity

Consistent branching naming conventions are crucial for maintaining a well-organized and understandable Git repository. They help to avoid confusion and make it easier to identify the purpose of each branch.

Here are some common branching naming conventions:

  • feature/*: For feature branches (e.g., feature/add-user-authentication)
  • bugfix/*: For bug fix branches (e.g., bugfix/fix-login-error)
  • hotfix/*: For hotfix branches (e.g., hotfix/urgent-security-patch)
  • release/*: For release branches (e.g., release/1.2.0)

Choose a naming convention that works well for your team and stick to it consistently.

Commit Message Etiquette: Clear and Concise Descriptions

Well-written commit messages are essential for understanding the history of your codebase. They should be clear, concise, and informative.

Here are some best practices for writing commit messages:

  • Use a concise subject line: The subject line should be a brief summary of the changes in the commit (ideally less than 50 characters).
  • Use the imperative mood: Write the subject line as if you're giving a command (e.g., "Add user authentication," not "Added user authentication").
  • Add a detailed body: The body should provide more details about the changes, including the reason for the changes and any important considerations.
  • Wrap the body at 72 characters: This makes the commit message easier to read in terminals and Git logs.
  • Separate the subject line from the body with a blank line: This makes the commit message easier to parse.

Example commit message:

```
Add user authentication

This commit adds user authentication functionality to the application.
It includes:
- A new User model
- Authentication routes
- Login and registration forms
```

Well-written commit messages make it easier to understand the history of your codebase and to track down bugs.

Dealing with Disasters: Recovery Strategies

Even the most experienced Git users make mistakes. It's important to know how to recover from common Git disasters.

Recovering from Accidental Commits: amend, reset

Sometimes you might accidentally commit changes that you didn't intend to commit, or you might realize that you need to make additional changes to the previous commit.

  • git commit --amend: This command allows you to modify the last commit. You can use it to add staged changes to the previous commit, modify the commit message, or both. Important: Only use git commit --amend on commits that have not been pushed to a shared repository.
  • git reset --hard <commit-hash>: This command will reset your current branch to a specific commit, discarding all changes after that commit. Use with caution, as this will permanently delete your changes if they haven't been committed or stashed. Consider using git reset --soft <commit-hash> or git reset --mixed <commit-hash> to preserve your changes in the staging area or working directory, respectively.

Example:

```
# Make some changes
git add .
git commit -m "Initial commit"

# Realize you forgot to add a file
git add forgotten_file.txt
git commit --amend --no-edit # Add the file to the previous commit without changing the message
```

Undoing Merges and Rebases: The Reflog to the Rescue

Sometimes you might make a mistake during a merge or rebase and want to undo the operation. The reflog is your friend in these situations.

The reflog is a record of all the changes to your repository's references (e.g., branches, tags, HEAD). It allows you to go back to any previous state of your repository, even if the changes have been lost or deleted.

To view the reflog:

```
git reflog
```

The reflog will show a list of recent actions, along with the corresponding commit hashes. You can use these commit hashes to reset your branch to a previous state.

Example:

```
git reflog
# Output:
# commit: HEAD@{0}: merge feature/new-feature: Fast-forward
# commit: HEAD@{1}: checkout: moving from feature/new-feature to main
# commit: HEAD@{2}: checkout: moving from main to feature/new-feature

git reset --hard HEAD@{1} # Reset to the state before the merge
```

The reflog is a powerful tool for recovering from mistakes, but it's important to understand how it works and to use it with caution.

Dealing with Stolen or Lost Commits

Sometimes you might accidentally lose commits due to various reasons, such as a hard reset or a corrupted repository.

If you haven't pushed the commits to a remote repository, you can try to recover them using the reflog, as described above.

If you have pushed the commits to a remote repository, you can try to recover them from the remote repository. However, this depends on the policies and configuration of the remote repository.

It's always a good idea to back up your Git repository regularly to prevent data loss.

Performance and Optimization

As your Git repository grows in size and complexity, it's important to optimize its performance to ensure that Git commands run quickly and efficiently.

Managing Large Repositories

Large repositories can become slow and unwieldy. Here are some tips for managing large repositories:

  • Use Git LFS for large binary files: As described earlier, Git LFS allows you to store large files outside of your Git repository.
  • Avoid storing unnecessary files in the repository: Don't commit build artifacts, temporary files, or other files that are not essential for the project. Use a .gitignore file to exclude these files from the repository.
  • Use shallow clones: Shallow clones allow you to clone only the recent history of a repository, which can significantly reduce the size and download time.
  • Run git gc regularly: git gc (garbage collection) optimizes the repository by removing unnecessary objects and packing the remaining objects more efficiently.

Shallow Clones for Faster Downloads

A shallow clone only downloads a limited amount of the repository's history, which can significantly reduce the download time and disk space required.

To create a shallow clone:

```
git clone --depth 1
```

This will clone

omcoding

Leave a Reply

Your email address will not be published. Required fields are marked *