About git - day-to-day work with git
Learn Git

About git - day-to-day work with git

Introduction

Git is a version control system that lets you track changes in your code over time.

GitHub is a cloud-based platform built around Git that allows teams to collaborate, review, and deploy code together.

In this article, you'll learn how to move from the basics of Git to practical workflows and habits used in professional environments. Whether you’re working solo or collaborating across time zones, Git and GitHub are foundational tools that boost efficiency, traceability, and teamwork. 

What Is Git?

Git is a distributed version control system. Unlike centralized systems, Git allows every contributor to keep a complete local history of the project. This ensures performance, reliability, and flexibility. Git can track changes across files, let you revert to previous versions, and allow parallel development through branching.

  • Commit: A snapshot of your code at a point in time, including metadata like author, date, and message.
  • Branch: A parallel line of development that allows experimentation without impacting the main codebase.
  • Local repository and remote repository: Usually, when we say 'remote repository', we have only one place in our minds - it's a server where we store the main code, and everyone syncs to it. (Why did I say "usually"? Because Git doesn't have a central repository and this terminology can refer to every repository that is not on our PC, More There)


Basic Commands

https://github.com/git-guides#learning-git-basics

git clone [url]: Clone (download) a repository that already exists on GitHub, including all of the files, branches, and commits.
git status: Always a good idea, this command shows you what branch you're on, what files are in the working or staging directory, and any other important information.
git branch: This shows the existing branches in your local repository. You can also use git branch [branch-name] to create a branch from your current location, or git branch --all to see all branches, both the local ones on your machine, and the remote tracking branches stored from the last git pull or git fetch from the remote.
git checkout [branch-name]: Switches to the specified branch and updates the working directory.
git add [file]: Snapshots the file in preparation for versioning, adding it to the staging area.
git commit -m "descriptive message": Records file snapshots permanently in the version history.
git pull: Updates your current local working branch with all new commits from the corresponding remote branch on GitHub. git pull is a combination of git fetch and git merge.
git push: Uploads all local branch commits to the remote.
git log: Browse and inspect the evolution of project files.
git remote -v: Show the associated remote repositories and their stored name, like origin.

General Workflow

Reference to a good explanation of the general workflow: https://www.atlassian.com/git/tutorials/comparing-workflows/feature-branch-workflow


Branches and naming conventions

Git offers a lot of flexibility and opportunity for collaboration with branches. By using branches, developers can make changes in a safe sandbox.

It is really important to choose one convention and stick to it for the rest of the project. The repository will be much more readable if the user knows what to expect. Of course, it will be even more readable if we add a few more rules.

Some rules are mandatory because those are restrictions coming from the filesystem or git itself: https://git-scm.com/docs/git-check-ref-format

But we can add more useful rules to group our branches. All names and groups should reflect our development processes, so the final form can vary for every company.

Especially useful is to use prefixes for your branch names. Grouping tokens allows the user to easily recognize what type of change is going to be done on the branch (about the grouping and what we can do with that fact?). Below is an example of prefixes:

  • Feature branches: Prefixed with feature/, these branches are used to develop new features.
  • Bugfix branches: Prefixed with bugfix/, these branches are used to make fixes.
  • Release branches: Prefixed with release/, these branches prepare a codebase for new releases.
  • Hotfix branches: Prefixed with hotfix/, these branches address urgent issues in production.


Merge, Squash and merge, Rebase

Merge

Combines the histories of two branches, preserving all commits from both sides. Git creates a merge commit that ties together the histories of the branches.

Impact on History:

  • The full history of the feature branch is preserved.
  • The dev branch will contain all the original commits from feature/cool-feature.
  • A merge commit clearly shows when the branches were brought together.

Popular problems:

  • Can create a noisy history with many small commits
  • Can make git log harder to read if not well-managed

Squash and Merge

Takes all commits from a branch and compresses them into one single commit when merging into the target branch. Used often in pull requests.

Impact on History:

  • All feature branch commits are lost (they exist only locally, not in dev).
  • dev gets one clean commit representing the whole feature.
  • History looks linear and tidy.

Popular problems:

  • Original detailed history is gone
  • Makes troubleshooting specific changes harder
  • Requires starting a new branch for further fixes (can't reuse the old branch cleanly)

Rebase

Moves your commits from one base (e.g., old dev) onto a new base (e.g., latest dev), rewriting commit history in the process.

Impact on History:

  • Rewrites the feature branch's history to look as if it was built on top of the latest dev.
  • Makes history linear: no merge commits, no "branching off."
  • Original commits are rewritten as new ones with new hashes.

Popular problems:

  • Rewrites history — dangerous on shared branches
  • Can cause conflicts during rebase
  • Team members working on the same branch will face problems if history diverges


revert, reset - how to change commits and not make your team angry

Each commit id is created based on:

  • parent commit id
  • author info
  • commit message
  • ... others

Changing any of those parameters will result in creating a new commit ID. Later, I will show some cases when it happens and the consequences of the action.

ref: http://gist.github.com/masak/2415865

Git reset

This command resets the current branch head to <commit>. There are 3 useful flags that determine what will happen with the changes in the code. This command can easily make a mess when used outside the local repository.

git reset --soft <commit id>        

--soft keeps the changes staged - we are ready to create a new commit using git commit -m "". It is useful when we work on a local branch, and in a fast way we can change the commit message, or unstage a file we did not want to commit, or we want to override the local branch history and create one commit out of many.

--mixed goes one step earlier, so we need to add those files manually e.g. git add . Useful when we want to change multiple files or split the latest commit into smaller ones.

--hard with this flag, we will come back to one of the previous commits and we will lose all changes. It is useful when we want to make a fast, clean start. This will erase our commits and changes related to them.

NOTE: If the branch is already on the remote repository, you need to use git push --force to override the branch history on the remote repository. If some user has already pulled the changes before you overrode them, it can lead to conflicts in the future because of incompatible branch history.

Git revert

This command creates a new commit that contains the changes required to revert the commit. It is a good choice when we don't want to mess with the branch history, e.g., the remote main branch, because the changes are already on the remote repository.

git revert <commit id>        

Using Git in examples

Case 1: Squash Merges and Bugs

You're working on a feature branch (feature/new-feature) and after finishing your work, you merge it into the dev branch using a squash merge. Squash merging takes all your feature branch commits and condenses them into a single commit on the dev branch. This makes the history of dev cleaner, with one commit per feature instead of a potentially messy commit history from the feature branch.

After merging, you discover a bug in the feature you just merged. You fix it on feature/new-feature and want to merge this fix back into dev. Even though the code is already in the target branch, Git treats it as if the changes have not been merged. This is because the changes are already present in both branches, but the branch histories differ.

Why This Happens:

  • Squash merge rewrites your commits into a single new commit on the target branch (dev).
  • Your feature branch still contains all the original commits, which no longer match dev's history.
  • Git merge relies on common ancestry; squash merges create a disconnect in ancestry tracking.

Once you've squash-merged a feature branch:

  1. Don't keep developing on it. Treat it as complete and closed.
  2. For any further fixes, create a new branch from dev, e.g., feature/new-feature-fix.
  3. Commit the bug fix on this new branch.
  4. Merge this fix back into dev

Why this works: Starting from dev ensures your new branch begins from the state that already contains the squashed feature work. Git can cleanly track this ancestry.

Case 2: Why Making Atomic Commits and Pushing Regularly is Good Practice

Don't be afraid of creating tons of commits on your feature branches - You can squash commits while merging to another branch.

Imagine a developer is working on a complex feature in a dedicated branch (feature/complex-task). They make steady progress, carefully crafting atomic commits along the way. Each commit:

  • Solves a small part of the problem.
  • Is self-contained and describes what it changes.
  • Leaves the codebase in a buildable or logically consistent state.

They regularly push this work to the remote repository, even though the feature isn’t finished yet.

Before completing the task, the developer has to leave for a longer PTO, perhaps suddenly due to personal reasons. Now, their teammates need to pick up where they left off.

Thanks to that approach:

  • Teammates can review the history of atomic commits to clearly understand what was done and what remains.
  • Since the in-progress work was regularly pushed to the remote repository, nothing is trapped on the developer’s local machine. The team can access the latest state immediately.

What Happens If Commits Weren’t Atomic or Pushed?

  • The team might have no access to their work at all.
  • Even if they pushed a giant, unfinished commit last-minute, it would be harder to understand what’s complete and what isn’t.
  • Teammates would waste time unraveling tangled changes, making assumptions, or redoing work unnecessarily.

Case 3: Problem with Non-Atomic, Poorly Named Commits

I often see that people add unclear commit messages. It is hard to find out what is going on in the repository when the git history looks like this:

commit 1a2b3c4d - Fix
commit 2b3c4d5e - Fix
commit 3c4d5e6f - Another fix
commit 4d5e6f7g - Small change
commit 5e6f7g8h - Fix CSS
commit 6f7g8h9i - Add feature
commit 7g8h9i0j - Fix
commit 8h9i0j1k - Revert Fix
commit 9i0j1k2l - Fix bug
commit 0j1k2l3m - Fix 
...
(commit 20) - Fix        

There is no way to understand the purpose of each commit. It’s super hard to review, debug, and collaborate. In this case, history is useless.

The situation changes quickly if we add a few easy-to-apply and use rules.

  1. Add the ticket number to keep track of the business case and conversation.
  2. Add clear messages that start with a basic verb form

Those two rules will make the commit history readable and super useful.

commit a1b2c3d4 - [Ticket-412] Add layout for user profile page
commit b2c3d4e5 - [Ticket-750] Implement API call to fetch user data        

Case 4: Ooops! I pushed something to the remote repository. I need to delete those changes and don't let others know I did it.

A developer pushes a commit to the remote dev branch but soon realizes it was a mistake (e.g., wrong code, confidential data, broken feature). To remove it, they use:

git reset --hard HEAD~1 # Move local branch 1 commit back 
git push --force # Overwrite the remote branch        

Now, the unwanted commit is gone from the remote history. However, this force push introduces several potential problems, especially when working in a team.

For example, another team member has already pulled the changes. When they try to push new changes later, Git will reject their push because their history diverges from the remote. They may see errors like:

! [rejected]        dev -> dev (fetch first)
error: failed to push some refs to 'origin'        


On the other hand, it can break the CI/CD pipeline, but usually the master branch is protected - in this case, you will break only the staging environment.



Links

https://github.com/git-guides#what-is-git

https://git-scm.com/


Andy Ramgobin

CodeZero25K followers

8mo

Great article Kornel! I hope you’re well mate :)

Like
Reply
Michał Kaczmarek

EMBED Systems - Michał…644 followers

8mo

I see your expertise here. I am glad that I was working with such talented colleague.

Michał Dąbrowski, PhD

Samsung Electronics913 followers

8mo

Thanks for sharing, Kornel! Especially section on reset vs revert, with useful options.

Dmytro Bielyi

Ghostery255 followers

8mo

Exactly what I need right now. Perfect timing! Thanks for crafting this one. :)

To view or add a comment, sign in

More articles by Kornel Pietrzyk

  • [Python] Classes and OOP

    "Everything in Python is an object" Python classes Classes provide a means of bundling data and functionality together.…

    2 Comments
  • Curriculum Vitae - first steps

    The document was created as my notes and as a starting point for people looking for a job for the first time or after a…

    1 Comment

Others also viewed

Explore content categories