Github

Github

September 8, 2026 · 11 min read · Angelos Ioakeimidis

Conventional Commits

Conventional Commit is a formatting convention that provides a set of rules to formulate a consistent commit message structure like so:

Conventional commit format
<type>
  [optional scope]:
  <description></description>
</type>

The commit type can include the following:

  • feat: A new feature.
  • fix: A bug fix.
  • docs: Documentation changes.
  • style: Styling changes (e.g. formatting)
  • refactor: Changes that neither fix a bug nor add a feature.
  • test: A new feature.
  • chore: Other changes that don’t modify src or test files.
  • perf: Performance improvements.
  • build: Changes that affect the build system or external dependencies.
  • ci: Changes to CI configuration files and scripts.

Here are few examples

Example commits
git commit -m "feat(login): add remember me checkbox"

git commit -m "fix(login): correct username validation error"

git commit -m "docs(readme): update installation instructions"

Show logs with commits

Plain git log gets unreadable fast once a branch has any real history. These flags just change how much noise you see - --oneline when you want to skim, --graph when you need to actually see how branches merged into each other.

git log variations
# full details
git log --date-order --all --graph --name-status
# simple view
git log --graph --oneline
# show the parent of each commit
git log --graph --parents

Show the content of a SHA

This is useful when you already have a commit hash from git log or a stack trace and want to see exactly what’s in it without checking it out.

git cat-file
git cat-file -p 2401023e502375286c4b085fd14b7412f231c1ec

Show all branches

Plain git branch only shows what you’ve fetched locally as tracking branches. -a pulls in everything on the remote too, which is the one you actually want when you’re trying to find a branch a teammate pushed.

git branch
# local branches only
git branch
# local + remote branches
git branch -a

Create a new branch

The -b just saves you a step - it creates the branch and switches into it in one command instead of git branch then git checkout.

git checkout -b
# create and switch into a branch
git checkout -b branchName

Rename a branch

Renaming locally is one command, but if you’ve already pushed it the remote doesn’t know about the rename - you have to push the new name and delete the old one manually, or anyone else tracking that branch will be confused.

Renaming a branch
# Start by switching the local branch which you want to rename:
git checkout oldBranchName
# Rename the local branch by typing:
git branch -m newBranchName

### If you've already pushed the oldBranchName branch to the remote repository, perform the next steps to rename the remote branch
# Push the newBranchName local branch and reset the upstream branch:
git push origin -u newBranchName
# Delete the oldBranchName remote branch:
git push origin --delete oldBranchName

Delete a branch

Deleting a local branch and deleting the copy that lives on the remote are two separate commands - forgetting the second one is how repos end up full of dead remote branches nobody cleans up.

Deleting a branch
# delete branch locally
git branch -d localBranchName

# delete branch remotely
git push --delete origin remoteBranchName

Cleanup local branches

After enough feature branches you end up with a pile of stale local ones. The --merged version is the safe one - it only removes branches Git can confirm are already merged into main, so you’re not losing anything.

Cleaning up local branches
# delete all your local branches, except the 'main' branch and the branch you're currently on
git branch | grep -v "main" | xargs git branch -D

# deletes the ones that are merged into the main
git branch --merged main | grep -v main | xargs git branch -d

# @config
# remove the local branches upon fetch if they are removed from the remote
git config fetch.prune true

Cleanup locally stored remote branches

Your local repo caches a copy of every remote branch it’s ever seen, even after they’re deleted on GitHub. remote prune clears out the ones that no longer exist upstream - always worth a --dry-run first so you’re not surprised.

Pruning remote branches
# list all of the remote branches stored on your local git repo
git branch -r

# find the non-existent on the remote repo
git remote prune origin --dry-run

# remove those branches from your local, run this:
git remote prune origin

Stash changes

Stash is for switching tasks without committing half-finished work. Naming your stashes with -m matters the moment you have more than one sitting in the list - stash pop alone always grabs the most recent.

git stash
# list all stashed changes across ALL branches
git stash list
# stash changes with a message
git stash push -m "Change the ID into a string"
# Recover and delete the latest stashed change
git stash pop
# recover and delete a stashed change with specific entryIndex
git stash pop stash@{0}

Rename files (case sensitive)

Git won’t register a rename that only changes casing - casesensitive to CaseSensitive looks like no change at all to it. Renaming through a temporary name in between forces Git to actually see it as two separate operations.

Case-sensitive rename
# from casesensitive to CaseSensitive
git mv casesensitive tmp
git mv tmp CaseSensitive

# non case-sensitive
git mv oldFileName newFileName

Amend last commit

Rewrites the last commit instead of creating a new one - good for fixing a typo or forgotten file right after committing, bad for anything you’ve already pushed and shared.

git commit --amend
# will add changes to the last commit,
# and allows you to change the commit message as well.
git commit --amend

# skips the edit dialog
git commit --amend --no-edit

Undo changes

--soft keeps your changes staged so you can redo the commit differently - --hard throws them away completely, and combined with clean -d -f it also wipes untracked files. That last combo is the only genuinely destructive one here.

git reset
# the last commit
git reset HEAD~

# the last commits
git reset --soft HEAD~2

# undo all commits up to that SHA
git reset --soft commitSha

# delete uncommitted changes the untracked files
git reset --hard; git clean -d -f

Cherry pick a commit

Grabs a single commit from another branch without merging everything else that came with it. Good for pulling a hotfix into a release branch without dragging in unrelated work.

git cherry-pick
# pick all change's from the commit
git cherry-pick ec485b624e85b2cad930cf8b7c383a134748b057
# when things go wrong and want to cancel
git cherry-pick --abort
# when face conflicts but fixed and you want to continue
git cherry-pick --continue

Cherry pick only changes to certain files

Sometimes you only want part of what a commit touched. -n stages the changes without committing, so you can unstage everything, re-add just the files you care about, and commit that instead.

Partial cherry-pick
# Get the commit but it won't commit the changes. It will only stage all the changes
git cherry-pick -n commitHash

# Unstage everything
git reset HEAD

# Stage the modifications you want to keep
git add filePath

# Make the work tree match the index
# (do this from the top level of the repo)
git checkout .

Rebase instead of merge

git pull --rebase
# do a rebase on the pull,
git pull --rebase

# update your feature branch with the master's latest changes
git pull --rebase origin master

# @config
# change the default behaviour, by making every 'git pull' into 'git pull --rebase'
git config pull.rebase true

Note: After rebasing a local branch, you have to git push –force-with-lease.

Now with git pull rebasing instead of mergingm you can enjoy other advantages like auto-stashing:

Auto-stash on rebase
git config rebase.autoStash true

This means if you are working on something, you don’t need to stash them before pulling; git will do it for you. It will stash the changes, do a pull (rebase), and then apply the stashed changes to the result.

Don’t force push, unless you want to break so

--force: overwrites a remote branch with your local branch

--force-with-lease: it is designed to protect against overwriting changes in the remote repository that you have not see. When you use this option, Git, checks that the remote branch has not changed since your last fetch. If the remote branch has been updated (e.g., by another collaborator’s push), the --force-with-lease push will fail.

--force-with-lease alias
# @config
# create an alias
git config --global alias.enforce "push --force-with-lease"
# run the command
git enforce
# or
git enforce origin the-feature-branch

Resolve obvious conflicts automatically

For conflicts where you already know which side should win entirely, --ours/--theirs skips the manual resolve. The grep one-liner is for when that’s true across a bunch of files at once, not just one.

Resolving conflicts by side
# If the solution is to accept local/our version,
git checkout --ours PATH/FILE

# If the solution is to accept remote/other-branch version
git checkout --theirs PATH/FILE

# If you have multiple files that you want to accept
grep -lr '<<<<<<<' . | xargs git checkout --ours
grep -lr '<<<<<<<' . | xargs git checkout --theirs

Dry run

Shows you exactly what a push would do - which refs, which commits - without actually sending anything. Worth running before any push you’re not sure about.

git push --dry-run
# Do everything except actually send the updates.
# https://git-scm.com/docs/git-push
git push --dry-run

Squash commits

Interactive rebase (squash)
# first choose the number of commits you want to squash
git rebase -i HEAD~5
git rebase -i 0433f9d

# This command starts something called an interactive rebase, on the last 5 commits
pick f72e7cb Feat: Resurrect flatMap
pick 1a18b1c Feat: Add list transformations
pick 3420af4 3.0.0
pick f55a9ee Docs: Add marble diagrams to README
pick 0433f9d Update README.md

# Keep the first line as is and replace every pick in the following lines with an s (for squash):
pick f72e7cb Feat: Resurrect flatMap
s 1a18b1c Feat: Add list transformations
s 3420af4 3.0.0
s f55a9ee Docs: Add marble diagrams to README
s 0433f9d Update README.md

# Save and exit. Then the editor launches once more with the commit messages of all 5 commits. Feel free to delete it all and rewrite the whole message

Note: After squashing, you need to git push --force-with-lease instead of push.

Squash Before Rebase: Squashing your multiple commits into 1 makes rebasing a lot easier. Especially, if your later commits reverted the earlier ones or canceled them in a sense. Else if there if a conflict you will have to fix it on every single commit.

Feature Branch Workflow

Feature branch workflow
# 1. Switch to the 'main' branch
git checkout main

# 2. Fetch latest changes from main branch
git pull --rebase

# 3. Reset the repo's local copy of 'main' to match the latest version
git reset --hard origin/main

# 4. Checks out a branch called 'new-feature' based on `main`
git checkout -b new-feature

# 5. Commit your changes
git status;
git add .;git commit -m "Message"

# 6. Push your changes with `-u` flag (if pushed first time)
git push -u origin new-feature

# 7. When ready with your code open a PR in Github for teammates to review it. Resolve their comments locally, commit, and push the suggested changes to remote again. Your updates appear in the pull request.

# 8. Before you merge, you may have to resolve merge conflicts if others have made changes to the repo. When your pull request is approved and conflict-free, you can add your code to the `main` branch. Merge the pull request in Bitbucket.

Sometimes you may need to get the latest changes from main before merging your PR. There are two strategies:

Merge Branch Flow

The straightforward way to bring main’s changes into your branch. --no-ff keeps an explicit merge commit even when Git could have fast-forwarded, so the history actually shows that a merge happened.

Merge branch flow
# ======================== Merge branch flow =====================

# 1. Go to main branch
git checkout main

# 2. Pull all latest changes
git pull

# 3. Check if any branches ever merged with current branch which is `main`
git branch --merged

# 4. Checkout into your feature branch to get the changes
git checkout new-feature

# 5. Merge the changes
git merge --no-ff main

# 6. Solve conflicts and push into 'new-feature'
git push

Rebase Flow

Replays your commits on top of main instead of merging, keeping history linear - but it rewrites your branch’s commits, so anyone else working on that branch will have a bad time. Only rebase branches nobody else has pulled.

Rebase flow
# when there are no conflicts
git checkout feature
git rebase main
git push --force-with-lease

# rewind history to just before the current Git rebase operation
git rebase --abort
# When you're done with the modifications
git add .
git rebase --continue
Key Takeaways
  • Conventional commits (feat, fix, chore...) turn commit history into a changelog you can actually read later.

  • --force-with-lease checks the remote hasn't moved since your last fetch before overwriting it - use it instead of plain --force.

  • Squash before you rebase: if you rebase multi-commit work first and hit a conflict, you fix it once per commit instead of once total.

  • git config rebase.autoStash true stashes and reapplies your in-progress changes automatically around a pull --rebase.

Liked this article? Share it with a friend on Twitter or support me to take on more ambitious projects to write about. Have a question, feedback or simply wish to contact me privately? Shoot me a DM and I'll do my best to get back to you.

Have a wonderful day.

Angelos Ioakeimidis

Angelos Ioakeimidis

Data Science & AI Student. Writing about software engineering, design, and AI.