Skip to content

Advanced

Git is a really powerful command line tool when you start looking into the advanced things you can do, but when maintaining or developing a project, you are probably not going to touch most of them. This is a list of ones which I use on a regular basis.

It’s important to note that the more advanced commands can have quite powerful functions which can lose code if used incorrectly. However, in most cases git caches a significant amount, so if you lose a commit or some changes, don’t panic and look up your issue (someone has done this before and found a way to get the changes back).

More on Commit IDs

Where it says to use the commit-id, there are different inputs you can use to specify multiple commits easily.

Let A and B be the ids for two different commits. To reference a range you can do:

  • A to B (including A): A^..B
  • A to B (excluding A): A..B

Or you can specify the last X commits:

  • HEAD~X e.g. git reset --soft HEAD~4

Stashing

Stashing is used when you want to record the current state of the working directory and the index, but want to go back to a clean working directory.

It allows you to set aside some changes for later or just never commit them (without altering the .gitignore file).

By stashing your changes, they will be removed so you can no longer see them, but you can always pop or apply the stash to get the back at any point.

Commands

Terminal window
git stash # Stash current unstaged (but tracked) changes
git stash apply # Restore the last stash without deleting it
git stash pop # Restore the last stash and delete it
git stash list # List the stashed changes
git stash show # Inspect the stashed changes

This is commonly used to switch branches (however the new switch subcommand is designed to replace this)

Reverting vs Resetting

To either reset or revert, you need the git commit hash id (or the start of it), this can be done via git log.

Once you have that, the interface is quite similar:

Terminal window
git reset commit-id
git revert commit-id

They both do the same thing of reverting the changes from the given commit. But the difference is, revert will create a new commit (therefore you can just run git push or merging without any issues), whereas reset will remove those commits outright.

When resetting, the tree (simply, the list of commits) itself is altered. This means that when pushing to a repo which already contains that commit, you have to use git push --force. This will cause anyone else working on the same branch to lose their changes, as they are forced to run git pull --force.

This command is dangerous so make sure you check everything before using it as it forces the upstream to be exactly like your local branch. So if you accidentally removed the wrong commit, you cannot easily get it back.

Extra options

With reset you also have extra options which you may find useful. You can either use --soft (Put all the changes of the commit in staged) or --hard (which is the default, just forget all changes).

With revert you can use --no-commit which will put the inverse of the changes in staged (and not create a new commit). This allows you to add multiple reverts or more changes in it.

Rebasing

Rebasing is used when commits have been added to main and you want to bring them to your branch (with was branched off of main).

To rebase off of main (when you are in your branch), you can run:

Terminal window
git rebase main

The steps it is takes are:

  • Temporarily reset all your commits which you added to this branch
  • Apply all the new commits in main (or the given branch)
  • Re-apply all of your commits

Dealing with Conflicts

Sometimes there may be conflicting changes from the changes added to main (e.g. you’ve changed the same line as another change).

This is when it gets quite confusing and dangerous.

If this happens, git will print out an error, saying what files are are in conflict and where to find them.

If you run git status you will see that some files are staged and some are not. The idea for these are:

  • Staged files: These are the files which are not conflicting and will be committed on git rebase --continue.
  • Unstaged files: These are the files with conflicts

If you open one of the conflicting files you will find that git has altered it where the conflicts are.

The format is:

<<<<<<< HEAD
- Auto writing README
- A cool logo - hw
- who's the above guy?
=======
- something
- A cool logo - hw
- other
>>>>>>> 8f309e1 (Test commit)

You will see the changes you made are below the ======= and the (updated) upstream code in main is above it.

What you have to do is, for each of these conflicts, to choose which one to keep (or create a mixture). To do this, you just remove everything that shouldn’t be there (which includes <<<<<<< HEAD, =======, etc). For example, in this case it should result in:

- Auto writing README
- something
- A cool logo - hw
- other
- who's the above guy?

Once you are happy an entire file is now conflict-free and correct, you can stage it.

Then once there are no unstaged files left you can run git rebase --continue, which will save the changes under the original commit (sometimes it will ask you to confirm the commit message with your editor, you can just save and exit it).

Once you have finished rebasing, make sure to test that your code still works. It’s common for code to break after rebasing due to unexpected changes made by someone else.

Interactive Rebase

Interactive rebasing is one of the most powerful and fun commands ever. However it comes with the downside of it being quite dangerous.

It is used for reorganising a merge request and managing commits to clean up the git log.

To use it:

Terminal window
git rebase -i HEAD~X
# Or interactive rebase off of main
git rebase -i main

This should bring up your default text editor with a list of commits with a list of commands at the end, which looks like:

pick af44004 Annoying rebasing commit
pick ceb4454 Add quote from Varnie
pick 1f156e8 Test commit
# Rebase 315c076..1f156e8 onto 315c076 (3 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 [-C | -c] <commit> = like "squash" but keep only the previous
# commit's log message, unless -C is used, in which case
# keep only this commit's message; -c is same as -C but
# opens the editor
# 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
# u, update-ref <ref> = track a placeholder for the <ref> to be updated
# to this position in the new commits. The <ref> is
# updated at the end of the rebase
#
# 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.
#

You can see all the commits which will be re-applied after the branch has been rolled back and commits from main been added.

These all have the word pick infront of them, meaning they will be committed as is, with the same message, and no editing happening.

You can look over the other commands to see what you can do, but how it works is by replacing pick with something else, e.g. fixup or reword.

The Fixup Commit

This is a weird option when committing, that can be useful when you have an MR and you are wanting to keep a neat commit log, but are responding to review feedback.

How this works, is lets say you’ve fixed something with a previous commit in your MR and were wanting to (when merging) squash this fix into that commit. But you want to have it as a separate commit to help the reviewer to see that you have fixed it.

To do this you simply run:

Terminal window
git commit --fixup="amend:<git_commit_id>"

Which will create a new commit with the message:

amend! Add information for publicising events from BCSS and where to go
> Add information for publicising events from BCSS and where to go

Which you can then push as its own commit.

Then when you come to merge you can run:

Terminal window
git rebase -i main --autosquash

Which will automatically move your amend commit to be fixup above the commit you were amending.

Merging

Normally merging is done via the interface on the remote repository system you are using. However it can be done in the command line as well (it can also be used in place of rebase so you don’t have to use the dangerous git push --force).

It copies all the commits which have been added on a branch to the branch you are currently on (e.g. main).

So the process to merge a branch into main is:

Terminal window
git checkout main
git merge branch-name
git branch -d branch-name # Deletes the branch If you don't need the branch any
# more
git push

Conflicts can still happen, see above for more information about how to manage them.

Cherry-picking

Cherry picking allows you to bring a single commit (or multiple, see here) from another branch to your current one.

To do this, it is as simple as:

Terminal window
git cherry-pick commit-id
git push

Submodules

Submodules are normally used in what is called a “monorepo”, a repo which stores multiple difference projects or git repositories.

It is also useful for refactoring some files. E.g. if you need the same files in multiple different projects (e.g. standardised tests or config files), it is common to add a “meta” repo which stores these projects, then add this as a submodule to each project which uses it.

To add a submodule you can run:

Terminal window
git submodule add repo-url folder/to/store

This will clone the module inside the folder folder/to/store and will add a .gitmodule file in the base of the repo.

When cloning the repo on other devices, you must remember to recursively clone all the submodules as well via:

Terminal window
git clone --recurse-submodules -j8 project-url

Or if you have already cloned the repo and were wanting to update all the submodules, you can run:

Terminal window
git submodule update --init --recursive

Managing submodules

Once you have cloned a submodule, you will note that any time you pull the latest changes to it, you need to make another commit in the base repo with the update.

This is so that the submodules are locked on specific commits until you specifically say “yes this next commit is fine”.

Extra Configuration

Sometimes there are configuration options that git will recommend when they become a problem, for example:

Terminal window
git config --global pull.rebase true

This means that when pulling from a branch which has new changes, it will rebase instead of merging the new commits.

Commit Signing

Commit signing is used to verify if you are who you say you are when committing (e.g. with your email address).

I won’t go into much depth on this, instead just know it exists and is quite good practice to have but not necessary.

You can read more here.

Be aware that there are some consequences which come along with this:

  • If you have setup a strict mode with signing, you cannot commit if you loose access to your signing key
  • When rebasing your changes, you have to resign all the commits (meaning you are the only one who can do it)