Conquer Version Control: A Deep Dive into Git & GitHub

Ashutosh PandeyAshutosh Pandey
18 min read

File System:

  • A file system is where you store and organize files on your computer or server (like folders and files you see in Windows Explorer or Mac Finder).

  • It keeps track of your files but doesn’t save old versions or changes.

  • If you overwrite or delete a file, it’s lost unless you have a backup.

Example: When you save a document on your desktop, it gets stored in your computer’s file system. But if you make changes to that document, the older version is gone unless you manually save different versions.


Version Control System (VCS):

  • A version control system tracks changes to your files over time.

  • It allows you to go back to previous versions, compare changes, and collaborate with others without losing work.

  • Even if you make changes or delete something, the VCS keeps a history, so you can recover older versions.

Example: If you're writing code in Git and you make changes, Git tracks those changes. You can go back to any previous version, see what was changed, and even work with multiple people without overwriting each other’s work.


Git is a free and open-source version control system designed to handle projects of any size. It helps you track changes in files, collaborate with others, and maintain a history of your work.

Key Features of Git:

  1. Version Control: It tracks every change made to files, allowing you to view the history, go back to previous versions, and undo mistakes.

  2. Collaboration: Multiple people can work on the same project without overwriting each other’s changes.

  3. Branches: Git allows you to create separate branches of a project, so you can work on different features without affecting the main project. Later, you can merge the changes.

  4. Distributed: Each user has a full copy of the project’s history on their local machine, so you don’t rely on a central server.


HASH

Git uses hashing to track every change you make to your project. Each change (or commit) is given a unique hash, which acts like a snapshot of your project at that moment. This makes it easy to identify and retrieve specific versions of your work.

Hash is also called commit id. It makes a unique id for commit and we can go back to the any version we want.

Simple Example:

  1. You make a change to a file (like adding a new line of code).

  2. When you commit this change, Git generates a unique hash (using a SHA-1 algorithm) based on the content of the change, the author, and other details.

  3. The hash looks something like this:
    6a7b34c...

This hash is like an ID for that specific change. Even if you make another small change, Git will generate a completely different hash for the new commit.


Common Git Terms:

  • Repository (Repo): A storage space where your project files and their history are stored.

  • Commit: A snapshot of your project at a specific time. It records the changes you’ve made.

  • Branch: A separate version of your project where you can make changes without affecting the main project.

  • Merge: Combining changes from one branch into another.

  • Clone: Making a copy of an existing repository to your local machine.

  • Pull: Fetching the latest changes from a remote repository.

  • Push: Sending your changes to a remote repository (e.g., GitHub).


Commands

Head always points to the latest change/commit you are working in the repository.

  1. git init: Initializes a new Git repository.

  2. git clone: Copies a remote repository to your local machine.

    1. Example -> git clone https://github.com/username/repo.git
  3. git status: Shows the status of your working directory and staged changes.

  4. git add: Stages changes to prepare them for commit.

    1. Example -> git add filename or git add . (this adds all the files to be added)
  5. git commit -m "": Saves your changes with a descriptive message.

    1. Example -> git commit -m "Added new feature"
  6. git push: Uploads local commits to a remote repository.

    1. Example -> git push origin main => this command sends your local changes to the main branch of the origin remote.

    2. origin: The default name for the remote repository you cloned from or have set up. It's a shorthand reference to the remote URL.

    3. Using git push origin main, you are pushing your local mainbranch's code to themainbranch of the remote repository namedorigin`.

  7. git pull: Fetches and merges changes from a remote repository.

    1. Example -> git pull origin main => this command gets the latest changes from the main branch of the origin remote and merges them into your local branch.
  8. git branch: Lists or creates branches.

    1. Example -> git branch -b branch-name
  9. git checkout: Switches between branches or commits.

    1. Example -> git checkout branch-name
  10. git merge: Combines changes from one branch into another.

    1. Example -> git merge branch-name
  11. git log: Shows the commit history of the repository.

    1. git log ---oneline => to display log in short one line.
  12. git remote: Manages remote repository connections.

  13. git diff: Displays the differences between your files.

  14. git reset: Unstages changes from the staging area.

  15. Set global username and email for Git (Locally).

git config --global user.name "<your username>"  #sets username
git config --global user.email "<your email>" #sets email
  1. Clone an existing Git Repository
git clone <repository URL>
  1. Add remote origin URL
git remote add origin <your remote git URL>
  1. Fetch all the remote branches
git fetch

I] What is the meaning of On branch master nothing to commit, working tree clean ?

  • You're on the master branch.

  • There are no changes to be committed (no modifications or new files).

  • Your working directory is clean (everything is up to date).

II] The master branch was traditionally the default or main branch in Git, often considered the core branch from which other branches are created. However, in many modern practices, the main branch is now named main instead of master, to better reflect inclusivity and modern naming conventions.


Branch

Branching in Git allows you to create separate "lines" of development in your project. Each branch is an independent version of the codebase where you can experiment, add features, or fix bugs without affecting the main project.

  • Branch: A branch is a pointer to a specific commit in the Git history. By default, Git starts with a main (or master) branch.

  • HEAD: HEAD is a reference to the current branch or commit you're working on

Key Git Branching Commands:

  • Create branch: git branch -b <branch-name>

  • Switch branch: git checkout <branch-name> or git switch <branch-name> both does the same work.

  • Create & switch: git checkout -b <branch-name>

  • View branches: git branch

  • Merge branch: git merge <branch-name>

  • Delete branch: git branch -d <branch-name>


Git vs. GitHub

  • Git:

    • Developed by Linus Torvalds

    • Local version control system.

    • Tracks changes, commits, and branches on your machine.

    • Works offline.

    • Manages repository history.

  • GitHub:

    • Acquired and managed by Microsoft.

    • Cloud-based platform for Git repositories.

    • Hosts repositories online for collaboration.

    • Enables remote access and sharing.

    • Provides additional features like pull requests, issues, and project management.



We can push the file from local to remote repo via two methods

  1. SSH

  2. PAT -> Personal access token


I] Pushing via SSH

  1. Create a new key pair with the command ssh-keygen -t , both private and public keys are generated.

  2. Copy the public key and add it in GitHub->settings->SSH and GPG keys.

  3. We clone any project/repo on which we want to push via SSH.

  4. Now create a new branch(demo) => create file (f1.txt) which is to be added.

  5. Now add the file (f1.txt) and commit it.

  6. These files are on new branch (demo) , but not in master/main branch.

  7. To push it to the demo branch we use git push origin demo , this will push the code to the demo branch, !! Note => Master branch is still unaffected to get the changes in master we have to merge the branches.


II] Pushing Via PAT

To push to a Git repository using a Personal Access Token (PAT), follow these steps:

  1. Generate a PAT:

    • Go to your GitHub account settings.

    • Navigate to "Developer settings" > "Personal access tokens".

    • Generate a new token with the required permissions (e.g., repo for full control over private repositories).

  2. Use PAT for Git operations: When prompted for a username and password while performing Git operations (e.g., git push), use the following:

    • Username: Your GitHub username.

    • Password: The generated Personal Access Token. Alternatively, you can include the PAT directly in the repository URL (though it's less secure): git remote set-url origin https://<username>:<PAT>@github.com/<username>/<repository>.git

  3. Push Changes: After setting the remote URL or when prompted for credentials, you can push your changes: git push origin branch-name Replace branch-name with the branch you're pushing to.

Using PATs is recommended over using passwords, especially since GitHub has discontinued password-based authentication for Git operations.


Branching Strategies

  • Strategy 1 (BASIC NOT OPTIMAL )

    • This strategy consists of majorly 3 branches namely -> master , staging , and dev

    • Master -> The master branch (or main in some projects) is the production-ready branch. It contains code that is stable and ready to be deployed to production.

    • Staging -> The staging branch is a pre-production branch used for testing and quality assurance (QA). It acts as a middle ground between dev and master.

    • Dev -> The dev branch is the development branch where active development happens. It serves as the integration point for feature branches.

WORKFLOW => Developers work on a feature by creating their own branches from the dev branch. Each developer may name their branch without following a proper naming convention (e.g., branches named Ashutosh, Shubham, Ankit). These branches are used to develop individual features. Once the features are completed, they are merged into the dev branch. Since multiple developers are working on different features, the dev branch contains all the new features. After development, the dev branch is merged into the staging branch, where the features are thoroughly tested to ensure stability. Once testing is complete and the code is verified to be stable, the changes from the staging branch are merged into the production master branch, making the new features live in production.

  • Strategy 2 (Better than 1st)

    • This Strategy consists of three main branches namely -> Master , QA , Dev and Feature.

    • In this strategy whole process is automated with the help of Jenkins.

    • In this strategy developers make a branch to develop features from the dev branch itself with proper naming conventions. (ex -> "feature/login" , "feature/button" ...)

    • Feature branch consists of the features which are being developed by developers.

    • Workflow => In this strategy, the repository has four main branches: master, QA, dev, and feature. Developers begin by creating a feature branch from the dev branch, using a proper naming convention (e.g., feature/login, feature/button) to clearly describe the feature they are working on. Each developer works in their respective feature branch to implement new functionality or fix bugs.

Once the feature is developed and tested locally, it is merged back into the dev branch. Multiple developers can create feature branches, ensuring that their work remains organized and easy to track. After merging all feature branches into dev, the branch is pushed to the QA branch for further testing.

The QA branch is used to test the combined features and ensure there are no issues. Once all tests pass and the code is stable, the QA branch is merged into the master branch. This final merge deploys the code to production, making the new features live. This strategy promotes a clear separation of development, testing, and production stages while maintaining structured naming conventions for better organization and tracking.

What if the production encounters a Bug ?

In this strategy, if a bug is found in the master branch (production), a new branch called hotfix is created directly from master. The naming convention typically follows something like hotfix/issue-name (e.g., hotfix/login-issue). This branch allows the bug to be fixed immediately without affecting ongoing development in the dev branch. Once the bug is fixed in the hotfix branch, it is merged back into the master branch to apply the fix in production. Hotfixes are typically used for small but critical issues that cause an immediate impact on the production environment. These could be bugs, security vulnerabilities, or other problems that need to be resolved quickly.

PROBLEM FACED IN HOTFIX

When a hotfix is applied directly to the master branch, the dev and QA branches remain unaware of the bug fix, which can lead to merge conflicts later. This happens because the master branch now contains changes that aren't reflected in the dev or QA branches. When developers finish working on new features and merge them from dev to QA and eventually into master, conflicts may arise as the master branch has already moved forward with the hotfix. To prevent this, after applying the hotfix to master, it is crucial to immediately merge the fix into both the dev and QA branches. This ensures that the bug fix is present across all active branches, keeping the codebases synchronized.

  • Strategy 3 (Most Optimal) In this workflow, we use a structured branching strategy along with Jira for task management and automation. The process is organized as follows:

Workflow =>

  1. Branching and Ticket Creation:

    • Jira Kanban Board: Includes columns like To Do, In Progress, Ready to Test, and Merge to Prod.

    • Each task or feature starts with a Jira ticket. The ticket is assigned to the "To Do" column on the Kanban board.

    • Developers create a feature branch from the dev branch, named with the ticket number and work name (e.g., feature/TICKET-123-work-name).

  2. Development and Automation:

    • As developers work on the ticket, they make commits to their branch.

    • Jira is integrated to automatically update the ticket status to "In Progress" when commits are made to the branch.

  3. Testing and Merging:

    • Once development is complete, the ticket moves to the "Ready to Test" column.

    • QA engineers test the feature. If the testing is successful, the ticket moves to the "Merge to Prod" column.

    • The feature is then merged into the staging branch for final testing and review.

  4. Deployment:

    • After successful testing in staging, the feature is merged into the production branch.

    • The feature is then deployed to the production environment.

By automating the workflow through Jira and using this structured process, the development cycle is streamlined, ensuring that each feature is tracked from initial creation through to deployment, with clear visibility and updates at each stage.

In the case of a hotfix, the workflow is as follows

  1. Ticket Creation:

    • A Jira ticket is created for the hotfix. This ticket is immediately moved to the "In Progress" column on the Kanban board.
  2. Branching and Development:

    • A hotfix branch is created directly from the production branch, named with the ticket number and a description of the issue (e.g., hotfix/TICKET-456-critical-bug).
  3. Bug Fixing:

    • The bug is fixed on the hotfix branch. Once the fix is implemented, the ticket status is updated to "Ready to Test" on Jira.
  4. Testing and Merging:

    • The QA team tests the hotfix. After successful testing, the ticket moves to the "Merge to Prod" column.

    • The hotfix branch is then merged into the production branch to apply the fix.

  5. Syncing Branches:

    • To avoid conflicts with ongoing development, the hotfix branch is also merged into the dev branch. This ensures that the fix is included in future development work and keeps the codebases synchronized.

  • One of the Best Strategies


REVERT AND RESET

In Git, revert and reset are used to undo changes, but they do so in different ways and serve different purposes. Here's a comparison:

git revert

  • Purpose: To create a new commit that undoes the changes made by a previous commit. This is useful when you need to undo changes while preserving the commit history.

  • Usage: git revert <commit>

  • Effect: It creates a new commit that reverses the changes of the specified commit. The original commit remains in the history.

  • Best For: Undoing changes in a public branch where you want to preserve the history and avoid disrupting other collaborators.

Example:

git revert a1b2c3d4 This command will create a new commit that undoes the changes introduced by commit a1b2c3d4.


git reset

  • Purpose: To move the current branch to a different commit, potentially altering the commit history. This is useful for undoing changes locally or discarding commits.

  • Usage: git reset [--soft|--mixed|--hard] <commit>

    • soft: Moves the branch pointer but leaves working directory and index unchanged.

    • mixed (default): Moves the branch pointer and updates the index, but leaves the working directory unchanged.

    • hard: Moves the branch pointer and updates both the index and working directory, discarding all changes.

  • Effect: Can alter the commit history, especially with --hard and --mixed. Useful for undoing local changes but should be used with caution on shared branches.

  • Best For: Local development where you need to discard changes or reset to a previous state. Avoid using --hard on shared branches to prevent data loss.

Example:

  • Soft Reset: git reset --soft HEAD~1 This moves the branch back by one commit but keeps the changes in the working directory.

  • Mixed Reset: git reset HEAD~1 This removes the last commit and unstages the changes, but keeps them in the working directory.

  • Hard Reset: git reset --hard HEAD~1 This discards the last commit and any changes in the working directory.


MERGE AND REBASE

Git Merge

  • What it does: Combines changes from one branch into another.

  • How it works:

    • You take changes from one branch (e.g., feature-branch) and add them to another branch (e.g., main). Tree like structure formed here .

    • Git creates a special "merge commit" to show that the two branches have been combined.

  • When to use:

    • When you want to keep the history of both branches and show that they were merged together.

    • Good for combining work from different people or teams.

    • Merging can also be done with pull request on GitHub.

    • Example -> suppose you have 2 branches master and dev (2 commits behind of master) therefore we merge from master to dev, this creates a new merge commit in dev and hence both branches are merged.

Git Rebase

  • What it does: Moves your changes to the top of another branch.

  • How it works:

    • You take your changes from your branch (e.g., feature-branch) and replay them on top of another branch (e.g., main). Linear Structure formed.

    • This makes it look like your changes were made directly on top of the latest version of main.

  • When to use:

    • When you want a clean, straight line of commits.

    • Useful for keeping your branch up-to-date before merging it into the main branch.

Summary

  • Merge: Combines branches and keeps all the history. Adds a merge commit to show the branches joined together.

  • Rebase: Moves your changes to the top of another branch, making the history look cleaner and more linear.

Use merge for preserving history and rebase for a cleaner, simpler history.


Cherry-Pick

  • What it does: Allows you to apply a specific commit from one branch to another branch.

  • How it works:

    • You pick a particular commit (a snapshot of your changes) from one branch and apply it to your current branch.

    • This is useful if you want to bring just one or a few specific changes from one branch to another without merging everything.

  • When to use:

    • When you need to take a specific fix or feature from one branch and apply it to another branch.

Example:

git cherry-pick <commit-hash>

This command takes the changes from the commit with the specified hash and applies them to your current branch.

Stash

  • What it does: Temporarily saves your uncommitted changes so you can work on something else and come back to them later.

  • How it works:

    • When you’re working on changes and need to switch tasks or branches, you can stash your work.

    • Git saves your changes and reverts your working directory to the state of the last commit.

    • You can later apply the saved changes back to your working directory when you’re ready.

  • When to use:

    • When you need to switch branches or tasks but don’t want to commit your current work yet.

Example:

git stash

This command saves your changes and reverts your working directory to the last commit. You can later retrieve your changes with:

git stash pop
or
git stash apply

Squash

  • What it does: Combines multiple commits into a single commit.

  • How it works:

    • If you have several commits in a branch and want to combine them into one commit for a cleaner history, you use squash.

    • This is usually done during a rebase or merge to make the commit history more organized.

  • When to use:

    • When you want to tidy up your commit history before merging a branch into the main branch.

Example:

git rebase -i HEAD~3

This command opens an editor where you can choose to squash the last three commits into one.

Summary:

  • Cherry-Pick: Apply a specific commit from one branch to another.

  • Stash: Temporarily save your work to switch tasks or branches.

  • Squash: Combine multiple commits into one for a cleaner history.


Merge Conflict Resolution

A merge conflict happens when you're working on a project with others (or on different branches yourself), and two changes to the same part of the code (or file) don't agree with each other. Git doesn't know which change to keep, so it asks you to resolve the conflict manually.

Example Scenario

  1. You are working on a project.

    • You create a branch called feature-a and start working on some code.
  2. Someone else is also working on the same project.

    • They are on the main branch and change the same part of the code you're working on.
  3. Both of you make different changes to the same file.

    • On the feature-a branch, you update a function to print "Hello from Feature A!".

    • On the main branch, someone updates the same function to print "Hello from Main!".

  4. You try to merge main into your feature-a.

    • Git will notice that the function was modified differently in both branches and doesn't know which version to keep, so it raises a merge conflict.

What Happens When a Conflict Occurs?

When you attempt the merge, Git marks the conflict in the file like this:

function greet() {
<<<<<<< HEAD
  console.log("Hello from Feature A!");
=======
  console.log("Hello from Main!");
>>>>>>> main
}
  • The code between <<<<<<< HEAD and ======= is from your feature-a branch.

  • The code between ======= and >>>>>>> main is from the main branch.

How to Resolve the Conflict

You now have to decide which version to keep:

  1. Keep the code from feature-a (your version).

  2. Keep the code from main (the other version).

  3. Combine both changes into something that makes sense.

Final Steps

After you've edited the file, you mark the conflict as resolved:

git add <filename>
git commit

Connect with Me


1
Subscribe to my newsletter

Read articles from Ashutosh Pandey directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Ashutosh Pandey
Ashutosh Pandey

"As a curious and driven student, I'm passionate about exploring the intersection of technology and innovation. Currently, I'm delving into the worlds of DevOps, Cloud Computing, and Artificial Intelligence/Machine Learning (AI/ML). With a keen interest in streamlining processes, optimizing systems, and harnessing the power of data, I'm dedicated to developing expertise in these cutting-edge fields. Through continuous learning and hands-on experimentation, I aim to unlock new possibilities and drive meaningful impact in the tech landscape."