This article focuses on common GIT actions that affect branches, such as merging, rebasing branches and squashing local commits.
MERGE
Merging brings changes from one branch into another. A feature branch is often created to work on a particular update. When complete, the branch needs to be merged back into “main”. The process would be as follows:
First switch to the main branch using either git switch main
or git checkout main
.
Next merge the featureA branch into main using git merge featureA
.
GIT will create a new commit with the merged changes. This is a special commit as it has two parents - the previous commit on main and the previous commit on the featureA branch. The main branch is updated to point to this new commit and HEAD continues to point to main. FeatureA still points to the last commit on that branch.
CONFLICTS
If a conflict is detected during a merge, GIT will interupt the process and prompt for user action
GIT will be in a special state where it expects the conflict to be resolved before continuing.git status
at this point shows the message “you have unmerged paths…fix conflicts and run git commit
…use git merge --abort
to abort the merge”.
❯ git status
On branch master
You have unmerged paths.
(fix conflicts and run "git commit")
Unmerged paths:
(use "git add <file>..." to mark resolution)
both modified: consolidate.py
no changes added to commit (use "git add" and/or "git commit -a")
GIT will display the files that have conflicts. The problem can be resolved at the command line, but a graphical diff tool such as p4merge may be better.
If you open the files in a basic text editor you will see GIT has marked the conflicts.
In the example below, the file “consolidate.py” is in conflict. GIT has updated the file with markers showing the lines that need attendtion. The HEAD section shows the lines as they appear in the current branch (main). Then there is a section break and directly below are the same lines as they appear in the featureA branch.
import sys
<<<<<<< HEAD
name=read "Enter your name"
age=read "Enter your age"
=======
read "Please enter your name"
read "Enter your age"
>>>>>>> featureA
To resolve the conflict, manually edit the file so it reflects the desired state and remove the markers and section break.
Save the file, add it to the GIT index, then commit the change to complete the merge, as shown below:
❯ git add consolidate.py
❯
❯ git commit
[main 1aca0e1] Introduce featureA that enables user input
In summary:
- Switch to the branch you are merging into (e.g.
git switch main
) - Merge the required branch into the current branch (i.e.
git merge feature1
) - Fix any conflict by editing the file
- Add the updated file to the index (
git add <file>
) - Commit to complete the merge (
git commit
)
FAST FORWARD MERGE
A fast forward merge occurs automatically when git moves a branch without having to create a new commit. It just re-uses an existing commit.
The most common case is when you want to merge a feature branch into main, but then continue working on the Feature branch with the latest updates from main.
When you first merge the branch into main, GIT creates a new commit on main that has two parents - the previous commit on main and the previous commit on the branch.
❯ git switch main
❯ git merge featureA
The status is now:
- main branch: contains the latest changes and any conflict resolutions
- featureA branch: contains the working feature before the merge into main
To continue developing on featureA, you need to merge main back into the featureA branch so it has the latest updates
❯ git switch featureA
❯ git merge main
At this point, GIT realises that there is already a commit that has the merged contents of featureA and main, so it just re-uses this commit. The merge message shows it performed a “fast-forward merge”
❯ git merge main
Updating 68a874e..1aca0e1
Fast-forward
consolidate.py | 6 +++---
orders.py | 4 ++++
2 files changed, 7 insertions(+), 3 deletions(-)
DETACHED HEAD
HEAD is a reference to a branch or a commit. Normally HEAD points to the current branch and thereby indirectly to the latest commit on that branch. Detached head is a state where the HEAD is not referencing a branch, it is pointing to an older commit.
Why would this happen? Perhaps you want to do some experimentation without creating a branch. You would checkout a commit rather than creating a branch. At this point HEAD is no longer tracking a branch and so it is detached. It acts like a temporary branch. After making some commits, you could do one of the following:
a) Switch back to a branch
b) Put a branch on the current commit
If you switch back to a branch, any previous commits outside a branch are isolated in the object database and are not referenced by any branch. Eventually they will be removed by the GIT garbage collector.
Alternatively, git branch <branchname>
can be used to put a branch on the current commit. At this point it is like any other branch and HEAD is no longer detached.
❯ git checkout 460ce0e
Note: switching to '460ce0e'.
You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by switching back to a branch.
REBASE
Rebase is an alternative to a merge. It changes the base of a branch, effectively adding it to the top of another branch as if the changes were sequential rather than created in parallel.
The rebase process looks at the first commit that is shared by two branches and uses the next commit as the base of the branch being rebased. It detaches this branch and re-attaches it to the head of the other branch. Under the hood, the commits do not actually move, new commits are created that are copies of the original commits on the branch. GIT moves the branch and the original commits become orphaned and eventually garbage collected.
> git switch featureA # make featureA the current branch
>
> git rebase main # rebase the featureA branch onto main
Why use Rebase?
Rebase can help to simplify the history of a project. If there is a lot of merging it can complicate the history.
Howver, use with caution. Rebased history is not the true history, so merging is safer.
Squashing commits
Squashing re-writes the GIT history, making two or more commits appear as if they were a single commit. Why would you want to do this? Developers often commit very frequently when working locally on a feature, but don’t want to complicate the shared history with all these individual commits. Squashing commits before merging or pushing to an origin repo simplifies the history in a large project.
The interactive mode of git rebase
is used to squash commits. This is totally different to the basic use of rebase. A starting point commit must be specified as we don’t normally want to edit the entire history. The starting point is excluded from the list and the interactive mode starts from the next commit.
> git rebase --interactive 80f137
pick fd4d8d9 Updates Adsense css
pick 8d41123 Adds adsense css
pick 9b2aafa Adds article
pick 32735fb Fixes error in post
pick 7f0063a Fixes typo
pick c85a17f Adds article
# Rebase 55a0831..c85a17f onto 55a0831 (6 commands)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup <commit> = like "squash", but discard this commit's log message
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
# . create a merge commit using the original merge commit's
# . message (or the oneline, if no original merge commit was
# . specified). Use -c <commit> to reword the commit message.
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
An interactive editor opens as shown above. The commits are listed (in reverse order compared to most git commands). The editor gives the commands - such as pick, reword etc.
By default all commits are on a line that starts with “pick”. Editing these lines will modify the history.
For example, you can move entire lines to change the order of commits.
Changing “pick” to “squash” will cause the commit to be merged with the one above and GIT will prompt to select or edit one of the two commit messages.
The golden rule of Rebase
Rebasing can lead to problems when sharing a repo across a team. The purpose of the rebase is to simplify history, but it can lead to duplication in the GIT multi-master sharing model. The golden rule is therefore:
Never rebase shared commits. Only use rebase for commits that have not yet been shared.
Ammending a commit
If you want to update the latest commit to add additional files, add them to the index then use the --ammend
option on git commit
. This will create a new commit with the additional files and leave the previous commit as an orphan that will eventually get garbage collected. You can only use this option to update the latest commit.
> git commit --ammend
TAGS
Tags are labels for a commit. Tags are normally used to mark releases.
❯ git tag version1_0 -a -m "First version. Basic features" # Create a new annotated tag
❯
❯ git tag version2_0 # Create a lightweight tag (not annotated)
❯
❯ git tag # get a list of tags
Tags are rederences to a commit, but unlike branches, tags never move.
CLONE
git clone
is used to copy an existing repository into an empty local folder. The copy contains the working files and the full GIT history.
The existing repo can be local or remote. It can be referenced by an SSH or HTTP address. By default, it will clone the branch HEAD is pointing to, but this can be modified using the -branch
option.
The command is mostly used to clone a repo from a hosting service such as Github - so it can be edited locally and then pushed back up to Github.
❯ git clone https://github.com/myaccount/myrepo # clone a repo from Github into the current local folder
The source repo is registered as a “remote” called origin by default. git status
will show if the local branch is ahead or behind the remote branch.
The information about the remote repo is stored in the /git/config file. The remote branches are tracked by objects in the .git/refs/remotes folder.
❯ git show-ref main # show all branches that have main in the name and the commit they are pointing to
8195805D refs/heads/main
B05DB506 refs/remotes/origin/main
If the local repo is in-sync with the remote, they will be pointing to the same commit, i.e. the hashes above would be the same.
The git clone -bare
option will clone the history but not the working area. It will also not setup the original source as a remote. This can be used to create a central repo that is just a source for cloning and not worked on directly.
PUSH / FETCH / PULL
git push
is used to send local changes to the remote origin repo. But what happens if there have been other changes on the remote before we push and we now have a conflict?
The answer is to fetch the remote changes and resolve the conflict locally before pushing. There is a comand to get the latest changes from the remote - git fetch
, but rather than fetching and then merging in two steps, the git pull
command peforms a fetch and merge in one command.
When working with a remote, you should always pull before pushing.
❯ git pull origin
This article was originally posted on Write-Verbose.com