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
git stash # Stash current unstaged (but tracked) changesgit stash apply # Restore the last stash without deleting itgit stash pop # Restore the last stash and delete itgit stash list # List the stashed changesgit 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:
git reset commit-idgit 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:
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:
git rebase -i HEAD~X# Or interactive rebase off of maingit 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 commitpick ceb4454 Add quote from Varniepick 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:
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:
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:
git checkout maingit merge branch-namegit branch -d branch-name # Deletes the branch If you don't need the branch any # moregit 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:
git cherry-pick commit-idgit 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:
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:
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:
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:
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)