DEV Community

Asem Zletni
Asem Zletni

Posted on

Essential Git - a pragmatic guide

Introduction

Git is a dreaded ubiquitous version control system that helps you keep track of changes to your local files and allows you to collaborate with others on software projects. This serves as a practical guide that covers a number of the essential, most common commands in Git that you'll find yourself frequently using in your daily developer life.

There is nothing earth-shattering here: it's merely a compilation of simple commands and useful variations on them that I find very helpful to use. All of this and more is obviously covered by the official Git documentation. Of course, there's only one problem with the documentation: it's impenetrable; hence, this guide.

If you absolutely hate Git and feel intimidated by it, this is your lifeline. Bookmark it for when the going gets tough.

Basic Terminology

Before we get started, let's go over some important definitions to avoid ambiguity down the line:

  • Commit: a snapshot that captures the state of your project at a particular point in time.
  • HEAD: a pointer that usually points to the tip of the currently checked out branch. The tip of a branch is the most recent commit in that branch.
  • Branch: a series of commits with a pointer that points to the most recent one.
  • Working directory: the local workspace hosting your entire project files and directories and where you experiment with new changes.
  • Staging area: a place where changed files are added before they can be part of the next commit.
  • Repository: the hidden .git directory that is created and used by Git to keep track of changes to your files; it contains a history of all of your commits.
  • Remote: the name of a pointer, typically named "origin," that points to the URL of a cloud-hosted repository.

Getting Started

When you're setting up Git for the first time on your local machine, you need to set up your name and email address:

git config --global user.name "John Doe"
git config --global user.email "john@email.com"
Enter fullscreen mode Exit fullscreen mode

Optionally, you may want to set up your default text editor. As an example, here's how to set up Visual Studio Code as the default editor:

git config --global editor.core "code --wait"
Enter fullscreen mode Exit fullscreen mode

To initialize a new Git project (i.e., create a new Git repository and start tracking changes to your project files), simply run:

git init
Enter fullscreen mode Exit fullscreen mode

To clone an existing Git repo instead:

git clone <url>
Enter fullscreen mode Exit fullscreen mode

Snapshotting

git status

Show the current state of your repository. This command shows you the following:

  • A list of files that are not tracked by Git,
  • a list of files that are added but have been modified since the last commit,
  • and a list of files that are in the staging area (also known as the index) and ready to be committed.

Run git status -s to see the output in a shorter format.

git add <file1> <file2> ...

Add changed files or previously untracked files from the working directory to the staging area. Use git add . to add all files at once.

git commit -m "commit message"

Create a new commit with files that are currently in the staging area. The commit message can be anything you want to describe the changes made in the current commit.

To add a separate commit message that explains in more detail the changes in your commit, run: git commit -m "commit message" -m "another commit message". Multiple message flags are concatenated as separate paragraphs. The first message is usually a short summary of the changes (50 characters or less), and the second message is a more detailed explanation.

You can add all tracked files and commit them in one go by running git commit -am "commit message". However, keep in mind that this only works if you're committing files that are already tracked by Git.

To add new changes to the most recent commit instead of creating a new one, or to modify the commit message run git commit --amend or git commit --amend -m "new commit message" after staging your new changes if there are any.

Note that git commit --amend will launch your default text editor with the commit message of the most recent commit. You can edit the message and save the file to change the commit message or simply close the editor without making any changes to keep the same message.

If you want to create an empty commit with no changes, run git commit --allow-empty -m “commit message”. I personally find this useful in one situation: to test or trigger a CI/CD pipeline.

git log

Show a log of commits for the current repository along with the current position of HEAD (which commit it points to). Each commit has a hash, an author, a date, and a commit message.

A useful variant is git log --oneline which shows a shortened commit hash alongside the commit message.

You use the --grep option to search for commits that contain a specific string in their commit message. For example: git log --grep="commit message". You can limit your search to a specific number of commits by using the -n flag: git log --grep="commit message" -n 5. And you can also use -i flag to make the search case-insensitive.

Branching

git branch <name>

Create a new branch called <name> based on the currently checked out branch.

git branch

Get a list of all branches in the current repository. Depending on your shell configuration, the currently checked out branch, i.e., the branch that HEAD currently points to, will be shown in green with an asterisk.

You get the same output by running git branch --list.

git switch <branch> or git checkout <branch>

Switch to an existing branch named <branch>. What happens when you run this command is that HEAD starts pointing to the <branch> pointer and <branch> becomes the currently checked out branch.

git switch -c <branch> or git checkout -b <branch>

Create a new branch <branch> and then switch to it. This effectively combines git branch <branch> and git switch <branch>.

git branch -d <branch>

Delete the branch named <branch>. You can force delete it by using the -D flag.

You can delete multiple branches by listing them: git branch -d <branch1> <branch2> <branch3>.

You can't delete a branch if it's currently checked out. You have to switch to a different branch first.

git branch -m <new-name>

Rename the currently checked out branch to <new-name>. To rename a different branch: git branch -m <old-branch-name> <new-branch-name>.

git checkout

git checkout is a versatile command that can be very useful sometimes. These are some of the ways you can use it:

  • git checkout <commit>: this goes into detached HEAD mode by making HEAD point directly to the commit that you specify (instead of pointing to the tip of a branch). In this mode, you can view and make changes and at any point you may revert your changes by running git switch <branch> or git switch - to switch to the branch HEAD was pointing to before using the checkout command. If you'd like to keep the changes you make in detached HEAD, you can run git switch -c <branch> to create a new branch with your new changes.
  • git checkout HEAD~n: check out the nth ancestor commit of HEAD.
  • git checkout HEAD <file> or git checkout -- <file>: discard staged and unstaged changes to match HEAD. Similar to running git restore, explained below.

Merging

There are two steps required in merging:

  1. First, you switch to the branch you want to merge changes into.
  2. Then you run the merge command.

For example, to merge feature branch into main:

git switch main
git merge feature
Enter fullscreen mode Exit fullscreen mode

Some useful options:

  • To show a list of branches that have been merged into the currently checked out branch: git branch --merged
  • To show a list of branches that have not yet been merged into the currently checked out branch: git branch --no-merged
  • To merge changes in feature branch into main but using only a single commit: git merge --squash main. All commits from the branch will be squashed into a single commit. You’ll see all changes have been staged and you’ll have to manually create a new commit.

Fast-Forward Merge

In most cases, the merge will be a fast-forward merge: the base branch (main) pointer will simply be updated to point to the same commit that the merged branch (feature) points to.

Non-Fast-Forward Merge

Sometimes the merge isn't exactly straightforward. This happens when two branches diverge.

Suppose that you create a new branch based on main and call it feature. Let's say that you add a couple of new commits on feature and at the same time there's a new commit on main that you currently don't have. When you try to merge, Git will create a new merge commit.

You can manually create a merge commit by running git merge --no-ff <branch>. This helps you maintain a clear history of when a branch was merged into another branch.

Resolving Merge Conflicts

Merge conflicts happen when the same part of the file is modified differently on the two merged branches. When this happens, Git will let you decide what to keep and what to remove.

Merge conflicts are resolved manually. Adding files using git add <file> tells Git that conflicts have been resolved.

Run git merge --abort after a merge has caused conflicts to return to the pre-merge state. It's important to note that there is no guarantee that uncommitted changes will be reconstructed by running this command.

After resolving conflicts and adding files to the staging area, you may create a commit manually or run git merge --continue to be prompted for a merge commit message.

Cherry-Picking

You may use git cherry-pick to merge specific commits from one branch to another.

For example, if you want to merge <commit-hash> from the branch main to the branch feature, you run the following:

# swtich to the target branch first
git switch feature
# now run the cherry-pick command
git cherry-pick <commit-hash>
Enter fullscreen mode Exit fullscreen mode

Running this command may cause merge conflicts. You can resolve them normally. You get the familiar abort and continue options as you do with regular merges.

Comparing

git diff

Show the difference between the working directory (the current state of your files) and the staging area. In other words, this shows you changes to your working directory that you could add to the staging area by running git add .. The git diff command accepts different options and arguments:

  • git diff --staged or git diff --cached: show changes that have been staged relative to the most recent commit.
  • git diff HEAD: list all changes, staged and unstaged, since the last commit.
  • git diff <file1> <file2>: show working directory changes to specified files.
  • git diff <branch1> <branch2>: compare the tip of branch1 to the tip of branch2.
  • git diff <commit1> <commit2>: compare changes between the given commits (which are passed as commit hashes).
  • git diff --staged <commit> <file> : show changes that have been staged for file <file> since the commit <commit>. It basically answers the question: how is <file> going to be different in the next commit compared to the old commit <commit>?

Stashing

You'll notice that if you attempt to switch branches while having uncommitted changes, Git will do one of two things:

  • If your changes don't conflict with the branch you're switching to, then Git switches the branch aand automatically applies your uncommitted changes to the new branch.
  • If your changes do conflict with the branch you're switching to, then Git will prevent you from switching branches.

Stashing is helpful here. Let's say you have some incomplete work on a feature and right now you really want to work on a different feature altogether. You don't want to finish working on that now, and your changes are not yet ready to be committed. What do you do in this case? You stash, of course.

You can stash as many times as you like. Your changes are saved to a stack-like data structure where the top entry is the most recent.

git stash

Stash away the current changes (staged and unstaged) and get back to a clean working tree matching the most recent commit. git stash push does the exact same thing.

There are several variations on the basic stash command:

  • git stash --keep-index: stash only unstaged changes, keeping staged changes intact.
  • git stash -p: interactively choose which chunks of changes to stash.
  • git stash push <file1> <file2>: stash changes made to the provided file paths in one stash entry.
  • git stash list: show a list of stashed changes.
  • git stash pop: reapply the most recently stashed changes to the working directory and remove them from the stash.
  • git stash apply: reapply the most recently stashed changes to the working directory without removing them from the stash
  • git stash apply n: reapply stashed changes saved at index n.
  • git stash drop n: drop/delete stashed changes saved at index n.
  • git stash clear: clear all stashed changes.

Undoing

git restore <file>

Discard unstaged changes made to <file> and restore its state to the most recent commit. You can specify multiple files git restore <file1> <file2> or all files git restore ..

You can also remove staged changes by using the --staged option.

git reset <commit>

Reset the current branch to a particular commit and keep the changes in the working directory. If you'd like to discard the changes, simply use the --hard option.

Warning: any changes you discard with the --hard option are permanently removed, so this should be used sparingly.

git revert <commit>

Create a new commit to undo the changes from an earlier commit.

To undo the effects of a revert, you can revert the revert commit. If there have been changes since the revert commit, you'll have to resolve conflicts.

This is the command to use—instead of git reset—when you want to undo changes that have already been shared with other collaborators on the same project.

If you use git reset to undo changes that have already been shared, you'll have to force push your changes to the remote repository. This will overwrite the history of the remote repository and could potentially cause problems for others.

Collaborating

GitHub, BitBucket and similar websites make it easy to share code and collaborate on projects. Git has several commands to make this possible.

git clone <url>

Use this to clone an existing repository hosted on GitHub or a similar website. When using this command, the remote is set automatically for you.

git remote add <name> <url>

Sometimes you create a new local repository and then you want to add a "remote" repository to push changes to.

"Origin" is a typical name for a remote repository. For example: git remote add origin https://github.com/yourUsername/yourRepoName.git.

  • To show a list of remotes: git remote
  • To show a list of remotes with URLs: git remote -v
  • To rename an existing remote: git remote rename <old-name> <new-name>
  • To remove a remote: git remote remove <name>

git push <remote> <branch>

Push committed changes from your local branch to your remote branch. If no arguments are given, <remote> defaults to "origin" and <branch> defaults to the current branch. To push all branches, use the --all option.

  • To show a list of remote branches: git branch -r.
  • To show a list of all branches, including remote branches: git branch -a.
  • To create a local branch from a remote branch: git switch <branch>.

git fetch <remote> <branch>

Get the latest changes from the remote repository without merging them locally. If no arguments are given, <remote> defaults to "origin" and <branch> defaults to the current branch.

git pull <remote> <branch>

Fetch changes then merge them locally. This is equivalent to running git fetch then running git merge. With no arguments, <remote> defaults to "origin" and <branch> defaults to currently checked out branch.

Rebasing

git rebase can be used in two ways.

As an alternative to git merge

As an example, let's suppose that you created a new branch, <feature>, based on main. While you were working on your new feature, new changes have been added to the main branch. You want to pull these changes and merge them. But you can use rebase in this case.

First, switch to your branch: git switch <feature>. Now run git rebase main to *re*base your branch commits on the latest changes from main. This command will create new commits to make a linear commit history but these new commits will keep their original metadata, such as author name and date.

Warning: do not rebase commits that you've already shared with other team members. This should only be used locally on changes you haven't shared yet. Otherwise, you'll have to force push your changes to the remote repository which will overwrite the history of the remote repository and cause problems much like what happens when you use git reset instead of git revert.

As a way to clean up your commit history

Do you want to delete, squash, rename, or reorder your commits before sharing them? Use interactive rebase for that.

git rebase -i HEAD~n
Enter fullscreen mode Exit fullscreen mode

Running this command will launch your default editor with instructions on what each command does. You must specify how far back you want to go in your commit history by specifying HEAD~n.

Rebasing is a powerful (and potentially dangerous) tool. If you're brave enough, you can read more about it here.

Note: git rebase uses git cherry-pick behind the scenes and that might mean that you’ll get more conflicts than when using a regular git merge.

TL;DR

Git is terrifying awesome.


If you're interested, there's a (slightly) less verbose version of this guide on GitHub Gist. That's what actually inspired me to write this guide in the first place.

Top comments (0)