Table of content

Generic

Best Practice

In this section we want to give you some insites in best practices for working with git. Of course not all of them are valide for everyone and keep in mind, that this are suggestsions and everyone needs to decide on its own if this is helpful or not.

Commit Messages

A very nice documentaiton about best practices on git commit mesage is the follogwing https://cbea.ms/git-commit/ which is worth giving it a view.

Config

CommandsDescription
git config core.fileMode falseignores permission changes on files/dirs
git -c core.fileMode=false [git-command]ignores permission changes on files/dirs for one command e.g. git -c core.fileMode=false diff
git config --global core.fileMode falsesets the ignore permission global, NOT A GOOD IEAD ;)
git config --global --add safe.directory '*'since git version 2.53.3 you can use this command to “disable” the unsave repository error (save.direcotry), as it assumes every repository on the server is save

Commands

CommandsDescription
GIT_TRACE=1 git <command>enables tracing of git command + alias and external application
git add --interactiveshows menu for interacting with index and head
git add -psame as add --interactive but lighter
git archive [branchname] --format=[compression format] --output=[outputfile]archivles branch into compressed file
git branch [branchname]creates local branch with [branchname]
git branch -[d/D] [branchname]deletes local branch with [branchname]
git branch -m [new branchname]renames current branch to [new branchname]
git branch -m [old branchname] [new branchname]renames [old branchname] branch to [new branchname]
git branch --no-mergedlists only branches which are not merged
git branch --show-currentdisplays current branch name (possible since version 2.22)
git bundle create [outputfile-bundlefile] [branchname]export a branch with history to a file
git check-ignore *list ignored files
git checkout -b [branchname]checkout and create branch
git checkout -b --orphan [branchname]creates branches with no parents
git checkout [branchname] [file]pulls file from branch to active branch
git cherry-pick [commitid]cherry-picks commit into current branch
git clean -ndry run of remove (tracked files)
git clean -fremove (tracked) files
git clean -[n/f] -d(dry run) remove (tracked) files/directories
git clean -[n/f] -X(dry run) remove (ignored) files
git clean -[n/f] -X -d(dry run) remove (ignored) files/directories
git clean -[n/f] -x(dry run) remove (ignored and untracked) files
git clean -[n/f] -x -d(dry run) remove (ignored and untracked) files/directories
git clean -X -fclean the files from .gitignore
git clone /file/to/git/repo.gitclone
git clone -b [branchname] --single-branch [repodestination]conles only single branch from repo
git clone -b [targetbranch] [output-bundlefile] [repositoryname]creates new repo [repositoryname] with data from [output-bundlefile] into branch [targetbranch]
git clone [repodestination] --depth [x]clones repo with a depth of [x]
git clone ssh//user@host/path/to/git/repo.gitclone
git commit --amand --no-editreapplies last commit without chaning commitmsg
git commit --fixup [sha-1]marks your commit as a fix of a previous commit
git commit --no-verifybypass pre-commit and commit-msg githoos
git describeshows how old the last merge/tag/branch is
git diff branch1name branch2name /path/to/filediffes file between branches
git diff branch1name:./file branch2name:./filediffes file between branches
git diff [remotename]/[remotebranch] -- [file]diffes the file against a fetched branch
git diff-index --quiet HEAD --returns 1 if there are changes to commit/add, returns 0 if nothing to do
git fetch -pupdates local db of remote branches
git gc --prune=now --aggressivepruine all unreachable objects from object database
git init --bare /path/to/git/folder.gitinit bare repo
git log [branch1] ^[branch2]shows commits which are in [branch1] but not in [branch2]
git log --all --full-history -- "[path]/[file]"displays log of file, specially nice for deleted files, e.g. for unknown path/extention use "**/[filename].*"
git log --date=relativedisplays date relative to your current date
git log --diff-filter=D --summarydisplays/shows all deleted files/folders
git log --show-signaturedisplays the signature if it got signed
git log --pretty=email --patch-with-stat --reverse --full-index --binary -m --first-parent -- [file(s)_to_export] [/path/to/new/patchfile]exports file(s)/dir(s) with git history as a patch
git log -S [string]searches for changes on [string] in the changesset (pickaxe functionalitys)
git log -G [string]searches for any mention of [string] in the changessets (pickaxe functionality)
git merge --no-ffforces merge commit over branch
git name-rev --nameonly [sha-1]check if the change was part of a release
git push [remote_name] :[branchname]deletes remote branch with [branchname]
git push [remote zb origin] :refs/tags/[tag_string_name]removes tag from remote
git push -d [remote_name] [branchname]deletes remote branch with [branchname]
git push --force-with-lease [remote] [branchname]force psuh but still ensure you don’t overwirte other’s work
git rebase -i --rootrebases the history including the very first commit
git rebase --autostashstashes changes before rebasing
git stashstashes uncommited changes
git stash -- <file1> <file2> <fileN>stashes uncommited changes of specific file(s)
git stash branch [branchname]branch off at the commit at which the stash was originally created
git stash cleardrops all stashes
git stash dropremove a single stashed state from the stash list
git stash listlists all stashes
git stash popremove and apply a single stashed state from the stash list
git stash showshow the changes recorded in the stash as a diff
git stash push -m "<stage message>"creates stash with message “
git stash push -m "<stage message>" <file1> <file2> <fileN>creates stash with message “” for specific file(s)
git stash push -m "<stage message>" --stagedcreates stash with message “” which includes only changed that are staged (since git 2.35)
git stash --keep-index ; git stash push -m "<stage_name>"same as above, but long way
git rm --cached [file]removes file from index
git rm --cached -r [destination e.g. file . ./\*]removes the added stuff as well
`git remote show [remotename]sed -n ‘/HEAD branch/s/.*: //p’`
git reset --hard [ID]rest to commit
git reset HEAD^ --harddeletes last commit
git reset HEAD^ -- [filename]removes file from current commit
git restore --source=HEAD^ -- [filename]restores [filename] from source HEAD^ (last commit), but does not remov file from current commit
git restore --source=HEAD^ --staged -- [filename]restores [filename] from source HEAD^ (last commit) and stages the file, but does not remov file from current commit
git rev-parse --abbrev-ref HEADreturns current branch name
git rev-parse --is-inside-work-treereturns true or false if targeted directory is part of a git-worktree
git rev-parse --show-toplevelshows the absolut path of the top-level (root) directory
git revert [ID/HEAD]will revert the commit with the ID or the HEAD commit
git shortlogsummarize the log of a repo
gtt shortlog --all --summarysame as above but counts all branches in as well and returns the counter for commits per author
git show-branchshows branches and there commits
git status --ignoredstatus of ignored files
git tag -d [tag_string_name]removes tag
git tag -v [tag]verifies gpg key for tag
git update-index --assume-unchanged [filename]don’t consider changes for tracked file
git update-index --no-assume-unchanged [filename]undo assume-unchanged
git verify-commit [gpg-key]verifies gpg key for commit
git whatchanged --since=[time frame]shows log over time frame e.g. ‘2 weeks ago’

Long Commands

Apply commit from another repository

$ git --git-dir=<source-dir>/.git format-patch -k -1 --stdout <SHA1> | git am -3 -k

or

git -C </path/to/other/repo> log --pretty=email --patch-with-stat --reverse --full-index --binary -m --first-parent -- <file1> <file2> | git am -3 -k --committer-date-is-author-date

Get all commits from all repos

This command allows you to count all comits on all branches for all users.

Be aware, squashed commits which are not stored on another branchs seperatly and not-squashed are counted as 1 commit of course

This has to be executred on the bare repos, to ensuare that all needed information is available on the fs

This sample was created with gitea as code hosting application where repos can be stored beneath organisations and personal accounts and limits to the year 2002

$ for f in $(ls -1 /gitea_repos) ; do for i in $(ls -1 /gitea_repos/$f) ; do echo "$f/$i" ; git -C /gitea_repos/$f/$i shortlog --all --summary --since "JAN 1 2002" --until "DEC 31 2002" | awk '{print $1}' ; done ; done | awk '{s+=$1} END {print s}'

If you want to have it for the full live time of the repo, just remove the options --since and --until with there values.

Git config

The .gitconfig allows you to include more files into the configuration by using the include section:

[user]
    name = mr robot
    email = mr.robot@localhorst.at
[include]
    path = /path/to/the/file/you/want/to/include

All the configuration you are doing in the file which is getting included will be instantly applied to your config when you save it.

This makes the shearing of similar parts of you config very easy.

Git alias

git is able to use aliases meaning you can shorten longer commands like in bash into a short word. These alias are defined in your normal .gitconfig. A very useful alias, is the alias alias ;)

alias = !git config --get-regexp '^alias.' | colrm 1 6 | sed 's/[ ]/ = /'

It will print you all your configured aliases form the .gitconfig and there included files.

When you are using aliases, you can run git internal commands (e.g. push, cherry-pick,..) but you can also run external commands like grep,touch,…

ps = push
cm = commit
cma = cm --amend
fancy = !/run/my/fancy/script

To execute now such aliases, just type git

$ git cm -m "testing"
$ git cma
$ git ps
$ git fancy

You have to think about two thing while using external commands in the aliases:

  1. Don’t separate commands with a semicolon (;) combine them with two ands (&&)
  2. if you need the current path where you are located, you have to add a command before your original command in the alias

Note that shell commands will be executed from the top-level directory of a repository, which may not necessarily be the current directory.

GIT_PREFIX is set as returned by running git rev-parse –show-prefix from the original current directory

In the .gitconfig you would need add cd -- ${GIT_PREFIX:-.} && to your alias

[alias]
    fancy = !cd -- ${GIT_PREFIX:-.} && /run/my/fancy/script

Extended alias

Extended alias are known as git alias which are calling e.g. shell functions.

These can be very helpful if you have to execute complex commands and/or you want to deal with (multible) parameters inside your alias.

Lets see how a simpe extended alias could look like:

[alias]
  addandcommit = "! functionname() { for file in ${@} ; do git add \"${file}\" ; echo "File ${file} added" ; done ; sleep 5 ; git commit ; }; functionname"

By adding the above alias, you could do for example this:

$ git addandcommit ./file1 ./file2 ./file3 ./file4
File file1 added
File file2 added
File file3 added
File file4 added

#sleeps now ;) and opends your editor to place your commit message:

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# On branch master
# Your branch is up to date with 'origin/master'.
#
# Changes to be committed:
#       modified:   file1
#       modified:   file2
#       modified:   file3
#       modified:   file4
#

Git exec

Sometimes an alias is to less what you would need from git to do. For this cases you can create scripts which are executed by git.

Of course you could create an alias, something like this:

[alias]
  run_script = !/home/user/my_git_script.sh

But there is a different way as well, you could make use fo the git exec path.

To get the information where this is on your system, just run git --exec-path and you will get something like this:

$ git --exec-path
/usr/lib/git-core

Now we know in our example, that the path is /usr/lib/git-core. In there you can place all sorts of scripts, just ensure the following:

  • prefix of script file: git-
  • file is executeable
  • file has a shebang

In there you can place now every sort of script to exend your git workflow.

One more thing worth to mention. git not only looks into the exec path, it also looksup all direcotries you have specified in your $PATH variable.

Same requirements as for the scritps in the exec path (prefix,permissions,shebang)

Parameters for gerrit

$ git push ssh://user@gerritserver:port/path/to/repo HEAD:refs/for/master%private         # pushes the commited change to gerrit and sets it in private mode
$ git push ssh://user@gerritserver:port/path/to/repo HEAD:refs/for/master%remove-private  # removes the private flag from the commit in gerrit

$ git push ssh://user@gerritserver:port/path/to/repo HEAD:refs/for/master%wip             # pushes the commited change to gerrit and sets it to work inprogress
$ git push ssh://user@gerritserver:port/path/to/repo HEAD:refs/for/master%ready           # removes the wip flag fromt the commit in gerrit

How to combine multiple commits into one

$ git fetch <remote>/<branch>                                                 # fetching data from a different remote and brach e.g. upstream/master
$ git merge <renite>/<branch>                                                 # merge upstream/master into origin/<current branch> e.g. origin/master
$ git rebase -i <AFTER-THIS-COMMIT>                                           # interactive rebase to bring commits together in one
# there you now have to change all the "pick"ed stated commits to "squash" or "fixup" beginnig from the second commit from the top
# now all those commits will be combined into one
# maybe you have to resolve some issues, just modify the files and add it after wards and continue with the rebase
$ git rebase --continue                                                       # continues with rebase after resoling issues
$ git push

Tags

git is able to craete tags which is a pointer to a commit, similar to branch, without the posibility of performing changes.

There are two and a half different kinds of tags

An annotated tag allows you to add a message to the tag it self, which will be shown then in git show command.

To tag a commit in the past, yout add the commit id at the very end of the tag command (works for all kinds of tags)

Lighweight Tags

To craete a lightweight tag, you just need to run:

$ git tag <tagname>

like:

$ git tag v0.0.6

A lighweight tag will be created as so, as long as you don’t add -a, -s, -S or -m to the git-tag command.

Annotated Tags

Creating annotated tags is nearly the same as creating lightweight tags

$ git tag -a <tagname> -m "tag message"
$ git tag <tagname> -m "tag message"
$ git tag -a v0.0.7 -m "Mr. Bond"
$ git tag -a v0.0.7 "Mr. Bond"

If you do not add -m <tag message> (only if -a is given), git will open you default editor like for commit mesages, so that you can enter them in there.

By using git-show <tagname> you will be able to see the tag message.

$ git show 0.0.7
tag 0.0.7
Tagger: Name of the tager <mail.of.the@tag.er>
Date:   Fri Dof 13 13:37:42 2002

annotated-test

commit 1234345456123434561234345612343456sfgasd (tag: 0.0.7)
Author: Name of commit author <mail.of.commit@au.thor>
Date:   Fri Jul 13 00:00:01 2002

    My fancy shmancy commit message for our new double 0 agent


New_00_Agent.txt

List Tags

Listing tags can be done in several ways.

To get a list of all tags, just run git tag:

$ git tag
1.0
1.1
1.2
2.0
2.0.1
2.1
2.2

To get a list of a range you would use `git tag -l “*”:

$ git tag -l "1.*"
1.0
1.1
1.2

$ git tag -l "2.0*"
2.0
2.0.1

To get the latest tag in the git repository, use git describe --tags:

$ git describe --tags
0.0.1-10-g434f285

Switch to Tag

In newer git version you are able to use git-switch to create branches or swtich to branches instead of using git-checkout, but for tags, git-checkout is still used.

$ git checkout <tagname>
$ git checkout 0.4.0
Note: switching to '0.4.0'.

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.

If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -c with the switch command. Example:

git switch -c <new-branch-name>

Or undo this operation with:

git switch -

Turn off this advice by setting config variable advice.detachedHead to false

HEAD is now at 5cc5bb0 git_branch_commit_review.sh test

And after that you will have a detached HEAD with the tag as its base.

Delete Tags

To delete a tag, you will have to add the parameter -d for local deletion.

$ git tag -d <tagname>

But this will only delete local

To delete on the remote you have to perform another command.

$ git push origin :<tagname>

It could be that you have a naming conflict and maybe want to specify it more clearly, then you would add the absolut path of the tag.

$ git push origin :/refs/tags/<tagname>

Setup gpg for signing commits or tags

GPG can be used to sign commits or tags. To do that you have to configure it with two small commands.

$ git config --global user.signingkey <your-gpg-key-id>
$ git config --global commit.gpgsign true

If this is set you can git will automatically sign your commits or tags

If you dont want to let it do automatically, just dont execute the second line.

For performing manuall signings, you have to use than -s or -S as shown below:

Signed Tags

$ git tag -s v1.5 -m 'my signed 1.5 tag'

You need a passphrase to unlock the secret key for
user: "This MyName (Git signing key) <mymail@address.com>"
4968-bit RSA key, ID <gpg-key-id>, created 2014-06-04

Signed Commits

$ git commit -a -S -m 'Signed commit'

You need a passphrase to unlock the secret key for
user: "This MyName (Git signing key) <mymail@address.com>"
4968-bit RSA key, ID <gpg-key-id>, created 2014-06-04

[master 5c3386c] Signed commit
 4 files changed, 4 insertions(+), 24 deletions(-)
  rewrite Rakefile (100%)
   create mode 100644 lib/git.erb

# also on merge possible

$ git merge --verify-signatures -S  signed-branch
Commit 13ad65e has a good GPG signature by This MyName (Git signing key) <mymail@address.com>

You need a passphrase to unlock the secret key for
user: "This MyName (Git signing key) <mymail@address.com>"
4968-bit RSA key, ID <gpg-key-id>, created 2014-06-04

Merge made by the 'recursive' strategy.
 README | 2 ++
  1 file changed, 2 insertions(+)

Verify Signed Commits and Tags

To verify a commit, you can use for example:

$ git show <commitid> --show-signature

And to verify a tag, you can use:

$ git tag <tagname> -v

Submodules and Subtrees

The simplest way to think of subtrees and submodules is that a subtree is a copy of a repository that is pulled into a parent repository while a submodule is a pointer to a specific commit in another repository.

This difference means that it is trivial to push updates back to a submodule, because we’re just pushing commits back to the original repository that is pointed to, but more complex to push updates back to a subtree, because the parent repository has no knowledge of the origin of the contents of the subtree.

Submodules

Generate submodule

Thirst of all you need to be in a git workign dir and the submodule-repo which you want to use need to exist already

than you are just going to execute something like that:

git submodule add <user>@<server>:<port></path/to/repo> <local/path/of/submodule/where/it/should/be/included>

e.g. git submodule add suchademon@sparda:/git/configs/newhost newhost

This will clone you the submodule into the destination path with the master branch

Next step is to initialise the submodule

git submodule init

Now you can do an update using submodule

git submodule update

And of course you can use it with all the normal functions of git if you just cd into it

Removing a submodule works like that:

git submodule deinit <local/path/of/submodule/where/it/should/be/included> -f

Than you will have to drop the .gitmodule files as well and you are done

Download submodule

Go into the git directory and update the .gitmodules

git submodule sync

With status you can see the current state

git submodule status
 12341234123412341234adfasdfasdf123412347 submodule1 (remotes/origin/HEAD)
 -45674567456745674567dghfdfgh54367dfggh4f submodule2

As you see above, one submodule is already available in your working directory (submodule1)

The second is not loaded right now to your working directory.

Now update with init parameter your modules to update all

git submodule update --init

Or specify the module name:

git submodule update --init submodulename

Run again the status and you will see that the second one is available

git submodule status
 12341234123412341234adfasdfasdf123412347 submodule1 (remotes/origin/HEAD)
 -45674567456745674567dghfdfgh54367dfggh4f submodule2 (heads/master)

Update submodule

Go into the git directory and update the .gitmodules

git submodule sync

Run the update command to do what it is used for ;) like this to update all

git submodule update
git submodule update --init --recursive
git submodule update --remote
git submodule foreach git pull

Or specify the module with its name

git submodule update submodulename

Subtrees

Adding a subtree

Same as for the submodule, you need to be in a git repo and the target repo (subtree) needs to exists.

$ git subtree add --prefix <local/path/in/repo> <user>@<server>:<port></path/to/repo> <branch> --squash

This will clone the remote repository into your <local/path/in/repo> folder and create two commits for it.

The first is the squashing down of the entier history of the remote repository that we are cloning and the second one will be a merge commit.

If you run git status, you will see nothing, as git subtree will has created the commits for you and left the working copy clean.

Also there will be nothing in the <local/path/in/repo> to indicate that the folder ever came from another git repository and as with submodules, this is both an advantage and disadvantage.

Update subtree

To update a subtree, you just need to perform:

$ git subtree pull --prefix <local/path/in/repo> <user>@<server>:<port></path/to/repo> <branch> --squash

and it will get updated.

Push to a subtree

Things get really tricky when we need to push commits back to the original repository. This is understandable because our repository has no knowledge of the original repository and has to figure out how to prepare the changes so that they can be applied to the remote before it can push.

$ git subtree push --prefix <local/path/in/repo> <user>@<server>:<port></path/to/repo> <branch>

As sad, it needs to first know how to prepare the changes which can take a while.

How to resolve error

object file is empty

cd <git-repo>
cp -a .git .git-bk                                                          # backup .git folder
git fsck --full                                                             # lists you the empty/broken files
    error: object file .git/objects/8b/61d0135d3195966b443f6c73fb68466264c68e is empty
    fatal: loose object 8b61d0135d3195966b443f6c73fb68466264c68e [..] is corrupt
rm <empty-file>                                                             # remove the empty file found by fsck --full
#<-- continue with the stepes git fsck --full and rm till you get the message -->
    Checking object directories: 100% (256/256), done.
    Checking objects: 100% (6805/6805), done.
    error: HEAD: invalid sha1 pointer <commit-id>
    error: refs/heads/master: invalid sha1 pointer <commit-id>
git reflog                                                                  # check for the HEAD if its is still broken
tail -4 .git/logs/refs/heads/master                                         # get the last 4 lines from the master log
    ea543250c07046ca51676dab4e65449a06387cda 6a6edcc19a258834e0a68914c34823666f61979c root@np-nb-0024 <oliver.schraml@payon.com> 1470993378 +0200   commit: committing changes in /etc after apt run
    6a6edcc19a258834e0a68914c34823666f61979c 473a418f519790843fcaeb2e0f6b5c406e11c1db root@np-nb-0024 <oliver.schraml@payon.com> 1470993386 +0200   commit: committing changes in /etc after apt run
    473a418f519790843fcaeb2e0f6b5c406e11c1db acafb909ef399f5eb4105e03bd0ffa1817ada8ac root@np-nb-0024 <oliver.schraml@payon.com> 1471325259 +0200   commit: committing changes in /etc after apt run
git show <commit-id from the last line the first one>                       # check the diff of the commit
    git show 473a418f519790843fcaeb2e0f6b5c406e11c1db
git update-ref HEAD <commit-id that you have checked the diff>              # set the HEAD to the commit id
    git update-ref HEAD 473a418f519790843fcaeb2e0f6b5c406e11c1db
git fsck --full                                                             # check again for empty/broken files
rm .git/index ; git reset                                                   # remove index from git and reset to unstage changes
git add . ; git cm <commit-message> ; git ps

gpg failed to sign the data

If you get something similat to this, it might be that you gpg key is expired. How could you know this, without looking directly into gpg --list-key.

First we need to know what command fails, to get this information, we use the environment variable GIT_TRACE like this:

$ GIT_TRACE=1 git cm "my fancy comit message"
20:25:13.969388 git.c:745               trace: exec: git-cm 'mmy fancy comit message'
20:25:13.969460 run-command.c:654       trace: run_command: git-cm 'mmy fancy comit message'
20:25:13.969839 git.c:396               trace: alias expansion: cm => commit -m
20:25:13.969856 git.c:806               trace: exec: git commit -m 'mmy fancy comit message'
20:25:13.969866 run-command.c:654       trace: run_command: git commit -m 'mmy fancy comit message'
20:25:13.972306 git.c:458               trace: built-in: git commit -m 'mmy fancy comit message'
20:25:13.979014 run-command.c:654       trace: run_command: /usr/bin/gpg2 --status-fd=2 -bsau DADCDADCDADCDADCDADCDADCDADCDADCDADC1337
error: gpg failed to sign the data
fatal: failed to write commit object

Now we know what command failed and can execute it manually:

$/usr/bin/gpg2 --status-fd=2 -bsau DADCDADCDADCDADCDADCDADCDADCDADCDADC1337
[GNUPG:] KEYEXPIRED 1655317652
[GNUPG:] KEY_CONSIDERED DADCDADCDADCDADCDADCDADCDADCDADCDADC1337 3
gpg: skipped "DADCDADCDADCDADCDADCDADCDADCDADCDADC1337": Unusable secret key
[GNUPG:] INV_SGNR 9 DADCDADCDADCDADCDADCDADCDADCDADCDADC1337
[GNUPG:] FAILURE sign 54
gpg: signing failed: Unusable secret key

And on the second line of the output, you can see that the key gets mentined as expiered.

How to upload new patch set to gerrit

# get patchset you whant to change (open the change in gerrit and go to Download > Checkout...):
git fetch ssh://<USER>@<GERRITSERVER>:<GERRITSERVERPORT>/path/of/repo refs/changes/XX/XXXX/<PATCHSETNR> && git checkout FETCH_HEAD

# now you can start to work on the fixes
cat,mv,rm,echo,.... blablabla

# after that command you will have the changes in your current branch, so create a new local branch:
git checkout -b <WORKINGBRANCH>     # e.g. git co -b FIXissue

# now its time to add your change and place the commit message
git add .
# while editing your commit message, keep always the GERRIT internal Change-Id, without it wont work
git commit --amand --no-edit

# now you are going to push the changes into a new patchset:
git push origin <WORKINGBRANCH>:refs/drafts/<DESTINATION_BRANCH_WHICH_HOLDS_THE_CHANGE_TO_MODIFY>

How to create an empty branch

#creates local branch with no parents
git checkout -b --orphan testbranch
#remove cached files
git rm --caced -r .
#remove the rest
rm -rf $(ls | grep -Ev "^\.git/$|^\./$|^\.\./$")

Worktrees

Worktrees are paths on your local filesystem, where (different) branches can be checked out from the same repository at the same time. You always have at least one worktree inside your git repositorie, which contain the current path of the local repo on the currently checkedout branch.

If you are wondering why you should make use of it, it can help you deal with hughe changes while still having the main branch available on your system.

This allows you to continue with your changes in one directory, while being able to use the content of the e.g. main branch in another directory, so you dont need to stash or commit changes before you switch to the other branch again, wich can save you some time.

One very nice benefit of worktrees is, that if you update the “main” repository, your updates performed on the other worktrees, will fetch the data from the original repository, means you can save network resources, if you fully update the “main” repository with e.g. git fetch --all as long as you have them tracked there (git branch --track [branchname]).

List worktrees

With the command git worktree list you can display all current worktries which are used by this repository.

The format will look like this: /path/to/repo <short_commit_id> [<branchname>]

Sample:

$ git worktree list
/home/awesomeuser/git_repos/dokus  ae8f74f [master]

If you want to make read the information about worktrees via a script, it is recommended to add the parameter --porcelain, according to the documentation it is then easier to parse.

$ git worktree list --porcelain
worktree /home/awesomeuser/git_repos/dokus
HEAD ae8f74fae8f74fae8f74fae8f74fae8f74fae8
branch refs/heads/master

If you would have more then one worktree, they would be seperated with a emptyline, which looks like this:

$ git worktree list --porcelain
worktree /home/awesomeuser/git_repos/dokus
HEAD ae8f74fae8f74fae8f74fae8f74fae8f74fae8
branch refs/heads/master

worktree /home/awesomeuser/git_repos/dokus2
HEAD ae8f74fae8f74fae8f74fae8f74fae8f74fae9
branch refs/heads/master

Add a new worktree

To add a new worktree, you just simply run the following command: git worktree add [path] [branchname]

Sample:

$ git worktree add ../dokus_wt_test wt_test
Preparing worktree (checking out 'wt_test')
HEAD is now at d387057 wt_test

And you have created a new worktree at the given path where the HEAD is set to the given branch:

$ git worktree list
/home/awesomeuser/git_repos/dokus          ae8f74f [master]
/home/awesomeuser/git_repos/dokus_wt_test  d387057 [wt_test]

In this second worktree you will find one .git file and nothing more.

It contains the path to the original git directory:

$ cat /home/awesomeuser/git_repos/dokus_wt_test/.git
gitdir: /home/awesomeuser/git_repos/dokus/.git/worktrees/wt_test
``

Failed to checkout branch on worktree

It can happen that if you create a new worktree, that git is not able to switch the branch in there. This means that if you cd into it, the master branch will be still the active HEAD, even it looks like this:

$ git branch -vv
  a_new_branch               7ee03ca [origin/a_new_branch] First test commit
* main                       0bf570d [origin/main] Second test commit msg
+ new_worktree               0bf570d (/path/to/repos/testRepo_new_worktree) Second test commit msg

And you tried to run git switch or git checkout which resulted into something like this:

fatal: 'new_worktree' is already checked out at '/path/to/repos/testRepo_new_worktree'

Then you can add to git checkout the parameter --ignore-other-worktrees and all will be good again.

Why is that so, because git only allows you to checkout a branch only for one worktree, for some reason it thinks that your current location is not the work tree so it won’t let you perform a “second” checkout and by adding the parameter --ignore-other-worktrees this mechanism gets just (as the parameter says) ignored.

Detect if current dir is an additional worktree or the main repository

There are three easy ways to get this done.

  1. Is to validate if .git is a file or a dir, if it is a gitlink, then this is not the main repository, but it still could be a submodule/worktree/… (would not recommend as git submodules use it as well)
  2. Comparing the output of the commands git rev-parse --absolute-git-dir and git rev-parse --path-format=absolute --git-common-dir (can provide inccorect data if it is below version ~2.13)

I guess no need for a sample on checking option 1, validation if something is a file or a dir should be possible if you use already git and have found this docu ;)

if [[ $(git rev-parse --absolute-git-dir) == $(git rev-parse --path-format=absolute --git-common-dir) ]] ; then
    echo "Main repository"
else
    echo "Additionnal workdir"
fi

Shrink repo size

# option one
# if you added the files, committed them, and then rolled back with git reset --hard HEAD^, they’re stuck a little deeper. git fsck will not list any dangling/garbage commits or blobs, because your branch’s reflog is holding onto them. Here’s one way to ensure that only objects which are in your history proper will remain:

git reflog expire --expire=now --all
git repack -ad  # Remove garbage objects from packfiles
git prune       # Remove garbage loose objects

# option two
git reflog expire --expire=now --all
git gc --prune=now
git gc --aggressive --prune=now
git remote rm origin
rm -rf .git/refs/remotes
git gc --prune=now

Messages from git

git pull origin
From ssh://SERVERNAME/git/REPO
 * branch            HEAD       -> FETCH_HEAD
 * fatal: refusing to merge unrelated histories

#This (unralted history/histories) can be solved by using the parameter
--allow-unrelated-histories

Bypassing gerrit review/limits

Is useful e.g. if gerrit returns some limit issues

git push -o skip-validation ...

How to checkout subdir from a git repo aka sparsecheckout/sparse checkout

official docu

Create dir and create + add repo url
mkdir <repo> && cd <repo>
git init
git remote add -f origin <url>

# Enable sparse checkout
git config core.sparseCheckout true

# configure subdir(s)
vim .git/info/sparse-checkout
# or
echo "/dir1" >> .git/info/sparse-checkout
echo "/dir2/subdir1" >> .git/info/sparse-checkout

# now you just need to pull
git pull origin master

sparse checkout as a function

function git_sparse_clone() (
  rurl="$1" localdir="$2" && shift 2

  mkdir -p "$localdir"
  cd "$localdir"

  git init
  git remote add -f origin "$rurl"

  git config core.sparseCheckout true

  # Loops over remaining args
  for i; do
    echo "$i" >> .git/info/sparse-checkout
  done

  git pull origin master
)

how to call the function

git_sparse_clone "http://github.com/tj/n" "./local/location" "/bin"

Change parent to newest commit

If you forgot to update the repo before you started to do your work you and have already commit your changes

you still can change the parent. This sample shows it with gerrit as review backend.

If you don’t have gerrit just skip the gerrit parts (git review…)

git pull # don't forget it now ;)

to get the change from gerrit (if you have already removed it)

git review -d <change-id>

rebase your changes to the master

git rebase master

it could the that you have merge conflicts, resolve them as usual rebase --continue is only needed if you have merge conflicts

vim ... ; git add ; git rebase --continue

afterwards push your changes back to gerrit

git review

or push it to some other destination

git ps

Rebase forked repository (e.g. on Github)

Add the remote, call it “upstream”:

git remote add upstream https://github.com/whoever/whatever.git

Fetch all the branches of that remote into remote-tracking branches, such as upstream/master:

git fetch upstream

Make sure that you’re on your master branch:

git checkout master

Rewrite your master branch so that any commits of yours that aren’t already in upstream/master are replayed on top of that other branch:

git rebase upstream/master

If you don’t want to rewrite the history of your master branch, (for example because other people may have cloned it) then you should replace the last command with

git merge upstream/master

However, for making further pull requests that are as clean as possible, it’s probably better to rebase.

If you’ve rebased your branch onto upstream/master you may need to force the push in order to push it to your own forked repository on GitHub. You’d do that with:

git push -f origin master

You only need to use the -f the first time after you’ve rebased.

Find content in all commits

With this command you can search for content in all dirs (git repos) beneath your current position.

$ mysearchstring="test"
$ for f in *; do echo $f; git -C $f rev-list --all | xargs git -C $f grep "${mysearchstring}"; done
repo1
repo2
repo3
5b8f934c78978fcbfa27c86ac06235023e602484:manifests/dite_certs_access.pp:#  echo "we are pringint test"
repo4

Find commits or tags in located in branches

git branch --contains=<tig|commitid> allows you to search for tags/commitid in al branches and returns the branch names

$ git branch --contains=7eb22db
branchA
* master
my1branch
my3branch

Debug or Trace mode

To debug commands like push, pull, fetch and so on, you can use the variables GIT_TRACE=1 and GIT_CURL_VERBOSE=1 to get more details.

There are also other debug variables which can be set, some of them we have listed at Huge debug section

Debug enabled via shell export

As alredy sad in the header, you can use export to ensure debug is enabled

$ export GIT_TRACE=1
$ git ...

Debug enabled via prefix parameter

alg is alreay an existing alias in my case which allows me to grep for aliases ;)

$ GIT_TRACE=1 git alg alg
16:54:11.937307 git.c:742               trace: exec: git-alg alg
16:54:11.937375 run-command.c:668       trace: run_command: git-alg alg
16:54:11.940118 run-command.c:668       trace: run_command: 'git config --get-regexp '\''^alias.'\'' | colrm 1 6 | sed '\''s/[ ]/ = /'\'' | grep --color -i' alg
16:54:11.944259 git.c:455               trace: built-in: git config --get-regexp ^alias.
alg = !git config --get-regexp '^alias.' | colrm 1 6 | sed 's/[ ]/ = /' | grep --color -i

Debug enabled via alias

You can even create an git alias which you could execute for this in your [alias] git-config section

[alias]
  debug = !GIT_TRACE=1 git

Huge debug

If you really want to see add the following vars:

GIT_TRACE=1 GIT_CURL_VERBOSE=1 GIT_TRACE_PERFORMANCE=1 GIT_TRACE_PACK_ACCESS=1 GIT_TRACE_PACKET=1 GIT_TRACE_PACKFILE=1 GIT_TRACE_SETUP=1 GIT_TRACE_SHALLOW=1 git <rest of git command> <-v if available>

Parameters and there descriptsion:

  • GIT_TRACE: for general traces
  • GIT_TRACE_PACK_ACCESS: for tracing of packfile access
  • GIT_TRACE_PACKET: for packet-level tracing for network operations
  • GIT_TRACE_PERFORMANCE: for logging the performance data
  • GIT_TRACE_SETUP: for information about discovering the repository and environment it’s interacting with
  • GIT_MERGE_VERBOSITY: for debugging recursive merge strategy (values: 0-5)
  • GIT_CURL_VERBOSE: for logging all curl messages (equivalent to curl -v)
  • GIT_TRACE_SHALLOW: for debugging fetching/cloning of shallow repositories

Possible values can include:

  • true
  • 1
  • 2: to write to stderr

Sample debug commands interacting with remotes

This is a smal sample who a git pull could look like in trace mode

$ GIT_TRACE=1 GIT_CURL_VERBOSE=1 git pull
12:25:24.474284 git.c:444               trace: built-in: git pull
12:25:24.476068 run-command.c:663       trace: run_command: git merge-base --fork-point refs/remotes/origin/master master
12:25:24.487092 run-command.c:663       trace: run_command: git fetch --update-head-ok
12:25:24.490195 git.c:444               trace: built-in: git fetch --update-head-ok
12:25:24.491780 run-command.c:663       trace: run_command: unset GIT_PREFIX; ssh -p 3022 gitea@gitea.sons-of-sparda.at 'git-upload-pack '\''/oliver.schraml/spellme.git'\'''
12:25:24.872436 run-command.c:663       trace: run_command: git rev-list --objects --stdin --not --all --quiet --alternate-refs
12:25:24.882222 run-command.c:663       trace: run_command: git rev-list --objects --stdin --not --all --quiet --alternate-refs
12:25:24.887868 git.c:444               trace: built-in: git rev-list --objects --stdin --not --all --quiet --alternate-refs
12:25:25.018760 run-command.c:1617      run_processes_parallel: preparing to run up to 1 tasks
12:25:25.018788 run-command.c:1649      run_processes_parallel: done
12:25:25.018801 run-command.c:663       trace: run_command: git gc --auto
12:25:25.021613 git.c:444               trace: built-in: git gc --auto
12:25:25.026459 run-command.c:663       trace: run_command: git merge --ff-only FETCH_HEAD
12:25:25.029230 git.c:444               trace: built-in: git merge --ff-only FETCH_HEAD
Already up to date.

Git LFS

The git large file support or git large file system was designed to store huge files into git

$ apt install git-lfs

As LFS was designed for http(s) interactions with the git repository it des not natively support ssh commands. This means, that you need to authenticate against your remote destination frist, befor you puch sour changes. There are two ways to do so

  1. either you change your remote destination to the https it will work instandly, but, than you have to enter all the time the username and pwd.
  2. if you want to do this via ssh, you hopefully have an gitea instance running, as that one is supporting it with a small lfs helper.

Gitea LFS helper authentication

To authenticate against lfs before you push your change, you can run the command like that sample below

$ ssh ssh://gitea.sons-of-sparda.at git-lfs-authenticate oschraml/doku download

LFS real live sample

$ ssh ssh://gitea.sons-of-sparda.at git-lfs-authenticate oschraml/doku download

LFS auth alias

Or you can create an git alias which does it automatically for your like this:

lfsauth = !ssh -q $(echo $(git remote get-url --push origin) | sed -E 's/.git$//g' | sed -E 's@:[0-9]+/@ git-lfs-authenticate @g') download >/dev/null && echo "Authentication: $(tput setaf 2)Success\\\\033[00m" || echo "Authentication: $(tput setaf 1)Failed\\\\033[00m"

The requirement for the alias is, that the remote url has the following structure: ssh://@://.git OR if you are using hosts from your ssh config ssh://://.git

So what is that one doing for you aveter you have added it somehow to your .gitconfig.

  1. It will run the exectly same command as above, but, it will get all the needed information on its own
    1. user with servername/domain
    2. git-lfs-authenticate is added instead of the port
    3. it will remove the .git from the url to have username and repository name
    4. and it will add the download string
    5. the ssh command will be set to quiet and stdout ridirected to /dev/null
    6. it will show success (in green) or fail (in red) to give you the status
  2. It will create the authentication and keep it open till the tls timeout was reached (app.ini gitea config)
    1. You dont need to have see it, copy it or something there like

LFS configruration

To small configuration will be automatically added by lfs into your .gitconfig file

[filter "lfs"]
    process = git-lfs filter-process
    required = true
    clean = git-lfs clean -- %f
    smudge = git-lfs smudge -- %f

LFS Setup server side

If you are run a coderevision platform like gitea, gitlab, … you need to enable the lfs supportin general first. If you dont do that, the repos will not allow you to use git lfs commands

LFS Setup client side

To enable the lfs support for an repository, you have to install it

$ cd ~/git/mynewlfsproject
$ git lfs install

LFS Add content

To add files to the lfs you can use the parameter track to create filters on the file namings.

$ git lfs track "*.png"
$ git lfs track "*.jpg"
$ git lfs track "*.pdf"

LFS local FS config

After you have added some filters to the lfs, you will see that the file .gitattributes was generated, or adopted. For example like that:

$ cat .gitattributes
 *.png filter=lfs diff=lfs merge=lfs -text
 *.jpg filter=lfs diff=lfs merge=lfs -text
 *.pdf filter=lfs diff=lfs merge=lfs -text

These file should be added, commited and pushed so that all other clients who are working with the repository are getting the same configuration

LFS Push

If you are done with the installation and small configuration, you can just perform your git push commands.

$ git lfs push origin master
Uploading LFS objects: 100% (1/1), 8.0 MB | 0 B/s, done.

LFS enable locking support

If you get during the push the message

Locking support detected on remote "origin".

You can run the command (shown in the mesage anyway) to add the lockverify into your git repo config

$ git config lfs.https://domain/repoowner/repo.git/info/lfs.locksverify true

Or you can perform this command which will apply the same for you:

$ git config lfs.$(git lfs env | grep -o -E "Endpoint=.*lfs " | cut -f1 -d\  | cut -f2 -d=).locksverify true

This one can be also used as an git alias of course

lfsconflock = !git config lfs.$(git lfs env | grep -o -E "Endpoint=.*lfs " | cut -f1 -d\\\\  | cut -f2 -d=).locksverify true

LFS show last logs

To view the last log you got from lfs, you can use the git lgs logs:

$ git lfs logs last

Remove files from commit

To remove one or mor files from a commit which you can go through the followin steps

In this sample we assume that the files got commited to the last commit

Frist perform a soft reset

$ git reset --soft HEAD~

Now that you have the files back in the a staged state, you just need to reset and checkout them

reset

$ git reset HEAD ./file/nubmer/one
$ git reset HEAD ./file2

checkout

$ git checkout -- ./file/number/one
$ git checkout -- ./file2

Last thing is to use commit with ORIG_HEAD to get back your commit message

$ git commit -c ORIG_HEAD

Now you can push your changes.

If they have been already pushed to a remote repository, you will have to use --force with the push command.

Create and apply patches

Create patches

To create a Git patch file, you have to use the git format-patch command, specify the branch and the target directory where you want your patches to be stored.

$ git format-patch <branch> <options>

The git format-patch command will check for commits that are in the branch specified but not in the current checked-out branch. As a consequence, running a git format-patch command on your current checkout branch won’t output anything at all. If you want to see commits differences between the target branch and the current checked out branch, use the git diff command and specify the target and the destination branch.

$ git diff --oneline --graph <branch>..<current_branch>

* 391172d (HEAD -> <current_branch>) Commit 2
* 87c800f Commit 1

If you create patches for the destination branch, you will be provided with two separate patch files, one for the first commit and one for the second commit. For example, let’s say that you have your master branch and a feature branch that is two commits ahead of your master branch. When running the git diff command, you will be presented with the two commits added in your feature branch.

$ git diff --oneline --graph master..feature

* 391172d (HEAD -> feature) My feature commit 2
* 87c800f My feature commit 1

Now, let’s try creating patch files from commits coming from the master branch.

$ git format-patch master

0001-My-feature-commit-1.patch
0002-My-feature-commit-2.patch

You successfully created two patch files using the git format-patch command.

Create patch files in a directory

As you probably noticed from the previous section, patch files were created directory in the directory where the command was run. This might not be the best thing because the patch files will be seen as untracked files by Git.

$ git status

Untracked files:
  (use "git add <file>..." to include in what will be committed)

      0001-My-feature-commit-1.patch
      0002-My-feature-commit-2.patch

In order to create Git patch files in a given directory, use the git format-patch command and provide the -o option and the target directory.

$ git format-patch <branch> -o <directory>

Back to our previous example, let’s create Git patch files in a directory named patches. This would give us the following command

$ git format-patch master -o patches

patches/0001-My-feature-commit-1.patch
patches/0002-My-feature-commit-2.patch

In this case, we provided the git format-patch will a local directory but you can provide any directory on the filesystem out of your Git repository.

Create patch from specific commit

In some cases, you are not interested in all the existing differences between two branches. You are interested in one or two commits maximum. You could obviously cherry-pick your Git commits, but we are going to perform the same action using Git patches. In order to create Git patch file for a specific commit, use the git format-patch command with the -1 option and the commit SHA.

$ git format-patch -1 <commit_sha>

Copy the commit SHA and run the git format-patch command again. You can optionally provide the output directory similarly to the example we provided in the previous section.

$ git format-patch -1 87c800f87c09c395237afdb45c98c20259c20152 -o patches

patches/0001-My-feature-commit-1.patch

Create patch from specific uncommited file

File is staged

If the file is staged already, you can use one of the commands:

--staged is a synonym for --cached

$ git diff --no-color --cached > 0001-My-feature-staged-change-1.patch
$ git diff --no-color --staged > 0001-My-feature-staged-change-1.patch

File is unstaged

If the file is still unsatged, use that command:

$ git diff --no-color > 0001-My-feature-unstaged-change-1.patch

Apply patches

Now that you have created a patch file from your branch, it is time for you to apply your patch file. In order to apply a Git patch file, use the git am command and specify the Git patch file to be used.

$ git am <patch_file>

Referring to our previous example, make sure to check out to the branch where you want your patch file to be applied.

$ git checkout feature

Switched to branch 'feature'
Your branch is up to date with 'origin/feature'.

Now that you are on your branch, apply your Git patch file with the git am command.

$ git am patches/0001-My-feature-commit-1.patch

Applying: My feature commit 1

Now, taking a look at your Git log history, you should see a brand new commit created for your patch operation.

$ git log --oneline --graph

* b1c4c91 (HEAD -> feature) My feature commit 1

When applying a Git patch, Git creates a new commit and starts recording changes from this new commit.

Troubleshooting patch

In some cases, you might run into errors when trying to apply Git patch files. Let’s say for example that you have checked out a new branch on your Git repository and tried to apply a Git patch file to this branch. When applying the Git patch, you are running into those errors.

file already exists in index

This case is easy to solve : you tried to apply a Git patch file that contained file creations (say you created two new files in this patch) but the files are already added into your new branch. In order to see files already stored into your index, use the git ls-files command with the –stage option.

$ git ls-files --stage <directory>

100644 eaa5fa8755fc20f08d0b3da347a5d1868404e462 0       file.txt
100644 61780798228d17af2d34fce4cfbdf35556832472 0       file2.txt

If your patch was trying to add the file and file2 files into your index, then it will result in the file already exists in index error. To solve this issue, you can simply ignore the error and skip the patch operation. To skip a Git patch apply operation and ignore conflicts, use git am with the –skip option.

$ git am --skip

error in file

In some cases, you might run into some merging errors that may happen when applying a patch. This is exactly the same error than when trying to merge one branch with another, Git will essentially failed to automatically merge the two branches. To solve Git apply merging errors, identify the files that are causing problems, edit them, and run the git am command with the –continue option.

$ git am --continue

Change Author of pushed commit

This affects all contributers who area also working or at least have cloned the repository, make sure you really need to do that

Sometimes you have to do noti things, for example chaning the author, or you just maybe pushed it with your wrong git config. To change an author, you just have to do two small things, a interactive rebase and a commit ammand like that: Use the parent commit ID from the commit you want to change.

$ git rebase -i 6cdf29a

Now you just navigate to your commit and change it from pick to edit It will show you then something like:

Stopped at 6cdf29a...  init
You can amend the commit now, with

  git commit --amend '-S'

Once you are satisfied with your changes, run

  git rebase --continue

Next step is to run the git commt amand with the correct Authorname and mailaddress

$ git commit --amend --author="Author Name <email@address.com>" --no-edit
[detached HEAD a04039e] init
 Date: Fri Aug 04 14:00:29 2021 +0200
 1 files changed,  2 insertions(+)
 create mode 100644 test/my_change_author_test_file

Now we are nearly done, continue with the rebase

$ git rebase --continue
git rebase --continue
Successfully rebased and updated refs/heads/master.

And force push your changes to the upstream repository

$ git push origin master --force
 + 6cdf29a...a04039e master -> master (forced update)

Extentions

Here you can find some practical additions which can be used in combination with git or makes your live easier with git. Some of these extentions have already there own documentation here and will be just listed(linked) and you can read the documentation there. Also extentions does not only to be plugsins/hooks and so on, also additional applications will be listed here.

Alrady documented additions/extentions:

  • tig: An small aplication to view git log/diffs/comits
  • <need to be generated ;) >

VIM

vim of course offers you a lot of fancy and shiny plugins to help you working with git. But not everything needs to be a plugin, vim on its own is able to do a lot of nice things which can help you too.

Color highlight in commit message dialog

Let’s use this as a sample, to visualize the best practice for git comit messages, you can add 3 lines into your vimrc config and get some handy addition applied.

autocmd FileType gitcommit set textwidth=72
autocmd FileType gitcommit set colorcolumn+=72
autocmd FileType gitcommit match Error /\v%^[a-z]%<2l.*|%>50v%<2l.*|%>72v%>2l.*/

What are the above lines about

  • textwidth: Maximum width of text that is being inserted. A longer line will be broken after white space to get this width. A zero value disables this.
  • colorcolumn: Is a comma separated list of screen columns that are highlighted with ColorColumn hl-ColorColumn.
  • match Error /\v%^[a-z]%<2l.*|%>50v%<2l.*|%>72v%>2l.*/:
    • Error: Uses the highlighting group Error
    • %^[a-z]%<2l.*: This will color the full first line if the first letter is not an uppercase letter
    • %>50v%<2l.*: This will color everything which comes after 50 characters at the first line
    • %>72v%>2l.*/: This will color everything which comes after 72 characters on all other lines

Dangling Commits Tags Trees and Blobs

Dangling commits are commits without having a reference and this means that they are not accassable via the HEAD history or any other history of other branches.

How can a dangling commit happen

This can happen if you have a branch which contains some or just one commit and the branch reverence gets deleted without merging the changes into the master/main branch.

$ git switch -c new_branch_for_dandling
Switched to a new branch 'new_branch_for_dandling'
$ echo "Asdf" > file_to_produce_dangling
$ git add file_to_produce_dangling ; git commit -m "will be dandling" ; git push branch
Pushing to ssh://....
Enumerating objects: 4, done.
Counting objects: 100% (4/4), done.
.
.
.
 * [new branch]      HEAD -> new_branch_for_dandling
 updating local tracking ref 'refs/remotes/origin/new_branch_for_dandlin

Now we have a new commit on a new branch:

$ git log -n 3
| 18bf526 2023-03-07 G  (HEAD -> new_branch_for_dandling, origin/new_branch_for_dandling) will be dandling
*   434f285 2022-01-26 N  (origin/master, origin/HEAD, master) Merge pull request 'TEST: do not merge' (#76) from testing into master
|\
| * 336d0c2 2022-01-26 G  test.sh: test notif
|/
|
...

$ git branch -a | grep new_branch
* new_branch_for_dandling                18bf526 will be dandling
  remotes/origin/new_branch_for_dandling 18bf526 will be dandling

So if we now remove the branch it self we will create the dalingling commit:

$ git switch master
Switched to branch 'master'
Your branch is up to date with 'origin/master'.
$ git push origin :new_branch_for_dandling
remote: . Processing 1 references
remote: Processed 1 references in total
To ssh://...
 - [deleted]         new_branch_for_dandling
$ git branch -D new_branch_for_dandling
Deleted branch new_branch_for_dandling (was 18bf526).

Lets have a check on the remote repository now and see what we got there:

$ git fsck --full
Checking object directories: 100% (256/256), done.
Checking objects: 100% (2058/2058), done.
dangling commit 18bf52608606535fc9d2d1c91d389a69e86a2241
Verifying commits in commit graph: 100% (1183/1183), done.

This is now a very simple one and easy to recover as it still has a valid parrent, but just emagine, that your parrent was a branch which got removed and nobody rebased your dangling change ontop of something different.

Detect dangling commits

A common way to do so is to perform git fsch --full which will validate the connectivity and validity of the objects in your database.

So you could get something like this:

$ git fsck --full
Checking object directories: 100% (256/256), done.
Checking objects: 100% (2058/2058), done.
dangling commit 18bf52608606535fc9d2d1c91d389a69e86a2241
Verifying commits in commit graph: 100% (1183/1183), done.

But it can happen, that you dont see them on your local (checked out) repository and you only see them on the remote one (e.g. on the bare repo). This is for example one of the reasons why version control systems like (forgejo,gitea,gitlab,…) are performing health checks over your repositories to detect such cases.

Another possible case to detect it (if it is only on remote side) that you get such a message while you pull updates from your remote repository:

$ git pull
fatal: invalid parent position 0
fatal: the remote end hung up unexpectedly

This can indicate to you that there are dangling commit(s) on the remote repository where git is not able to download them.

Dealing with dangling commits

You can get the commit ID’s from the git fsck commands as shown above.

Recovering dangling commits

Of course you have several ways in git to get things back, lets assume you can sill access the commit:

  • git rebase <dangling-commit-id>: lets rebase it on master
  • git merge <dangling-commit-id>: merge it to the master
  • git cherry-pic <dangling-commit-id>: picking it to the master
  • git checkout <dangling-commit-id>: directly checkout the commit
  • and many others

Now lets asusme you can not access the commit, as we dont get it from the remote repo, but as long as you are somewho able to access the data via the file system, you can recover it:

  • git cat-file <commit|tag|blob|directory> <dangling-commit-id>: this will give you some data of the commit, like author, message and so on
  • git diff -p ..<dangling-commit-id>: will give you the changes compared to the current status as a patch file content
  • git show <dangling-commit-id>: shows metadata about commit and content changes

Delete all dangling commits

Before you run this two commands, make sure that you really don’t need them any more!

$ git reflog expire --expire-unreachable=now --all
$ git gc --prune=now

The first command will enable you to remove all the dangling commits performed in the past.

Mark from man git-reflog:

The expire subcommand prunes older reflog entries. Entries older than expire time, or entries older than expire-unreachable time and not reachable from the current tip, are removed from the reflog. This is typically not used directly by end users — instead, see git-gc(1).

--all: Process the reflogs of all references.

--expire-unreachable=<time>: Prune entries older than <time> that are not reachable from the current tip of the branch. If this option is not specified, the expiration time is taken from the configuration setting gc.reflogExpireUnreachable, which in turn defaults to 30 days. --expire-unreachable=all prunes unreachable entries regardless of their age; --expire-unreachable=never turns off early pruning of unreachable entries (but see --expire).

The second one will removed the before pruned commits.

Mark from man git-gc:

--prune=<date>: Prune loose objects older than date (default is 2 weeks ago, overridable by the config variable gc.pruneExpire). --prune=now prunes loose objects regardless of their age and increases the risk of corruption if another process is writing to the repository concurrently; see “NOTES” below. --prune is on by default.