Files
oam/knowledge base/git.md
2022-04-21 17:51:01 +02:00

14 KiB

Git-related useful commands

TL;DR

# create a new empty repository or reinitialize an existing one
git init
git init --bare path/to/repo.git
git init --initial-branch main

# get the current status of changes
git status
git status --verbose

# check differences
git diff
git diff --staged

# add the current changes
git add .
git add path/to/file

# interactive review of chunks of changes
git add --patch

# clone with submodules in a specific folder
git clone --recurse-submodules git@github.com:user/repo.git repos/repo

# checkout a remote branch
git checkout -b local_branch origin/remote_branch

# go back to the previous branch
git checkout -

# create a commit with no changes nor message
git commit --allow-empty --allow-empty-message

# add a new remote and push to it
git remote add gitlab git@gitlab.com:user/my-awesome-repo.git
git push gitlab

# create a patch
# FIXME: there has to be a better way to do this
git add . && git commit -m 'message' && git format-patch HEAD~1 && git reset HEAD~1
git diff > file.patch

# apply a patch
git apply file.patch

# change last commit's author
git config user.name "user name"
git config user.email user.email@mail.com
git commit --amend --reset-author

# sign all commits from now on
git config --global user.signingkey 'KEY_ID_IN_SHORT_FORMAT'
git config --local commit.gpgsign true

# working with windows fellas
git config core.autocrlf "input"  # unix
git config core.autocrlf "true"   # windows

# show git's configuration
git config --list
git config --list --show-scope
git config --list --show-origin

# render the current configuration
git config --list \
 | awk -F '=' '{print $1}' | uniq \
 | xargs -I {} sh -c 'printf "{}=" && git config --get {}'

# get the top-level directory of the current repository
git rev-parse --show-toplevel

# get the current branch
git branch --show-current         # git > v2.22
git rev-parse --abbrev-ref HEAD

# list tags
git tag

# create annotated tags
# stored as full objects in git's database
git tag --annotate v0.1.0
git tag -as v1.2.0-r0 -m "signed annotated tag for v1.2.0 release 0"
git tag -a 1.1.9 9fceb02  # specific to a commit

# create lightweight tags
# stored as a pointer to a specific commit
git tag v0.1.1-rc0

# push tags
git push origin v1.5
git push --follow-tags  # all annotated tags only
git push --tags         # all tags

# delete tags
git tag -d v1.4-lw                # local only
git push origin --delete v1.4-lw  # remote only

# create an alias
git config --local alias.co checkout
git config --global alias.unstage 'reset HEAD --'

# remove merged branches
git fetch -p && awk '/origin/&&/gone/{print $1}' <(git branch -vv) \
 | xargs git branch -d

# get a more specific diff
git diff --word-diff
git diff --word-diff=color
git diff --word-diff=porcelain

Debug

When everything else fails, use this:

export GIT_TRACE=1

Common configuration

# required
git config --local user.email 'me@me.info'
git config --local user.name 'Me'

# working with windows fellas
# 'input' on unix, 'true' on windows, 'false' only if you know what you are doing
git config --local core.autocrlf 'input'

# sign commits
git config --local user.signingkey 'KEY_ID_IN_SHORT_FORMAT'  # gpg --list-keys --keyid-format short
git config --local commit.gpgsign true                       # sign all commits
git commit --message "whatever" --gpg-sign                   # or -S

# pull submodules by default
git config --global submodule.recurse true

Checkout a remote branch

AKA create a local branch tracking a remote branch

git checkout -b "${LOCAL_BRANCH}" "${REMOTE}/${REMOTE_BRANCH}"
$ git checkout -b local_branch origin/remote_branch
Branch 'local_branch' set up to track remote branch 'remote_branch' from 'origin'.
Switched to a new branch 'local_branch'

Delete all branches already merged on master

Already present in oh-my-zsh as the gbda alias

Command source [here][prune local branches that do not exist on remote anymore]

git fetch -p && awk '/origin/&&/gone/{print $1}' <(git branch -vv) | xargs git branch -d
git branch --no-color --merged | command grep -vE "^(\*|\s*(master|develop|dev)\s*$)" | command xargs -n 1 git branch -d
for repo in $(find . -type d -name .git | awk -F '/.git' '{print $1}'); do cd ${repo}; echo "--- ${PWD##*/} ---"; gbda; cd - > /dev/null; done

Sync up all repos in the current directory

for repo in $(find . -type d -name .git | awk -F '/.git' '{print $1}'); do cd ${repo}; echo "--- ${PWD##*/} ---"; git pull; cd - > /dev/null; done

Merge master into a feature branch

git stash pull
git checkout master
git pull
git checkout feature
git pull
git merge --no-ff master
git stash pop
git checkout feature
git pull origin master

Rebase a branch on top of another

rebase takes the commits of a branch and appends them to the commits of a different branch. The commits to rebase are previously saved into a temporary area and then reapplied to the new branch, one by one, in order.

git rebase origin/${upstream} ${branch}

Rebase the current branch onto upstream branch master

git pull --rebase=interactive origin master

LFS

  1. install the LFS extension for git

    # Ubuntu
    apt install git-lfs
    
  2. enable the extension in the repository

    $ cd "${REPOSITORY}"
    [repository-root]$ git install lfs
    
  3. configure file tracking

    [repository-root]$ git lfs track "*.exe"
    [repository-root]$ git lfs track "enormous_file.*"
    
  • add the .gitattributes file to the traced files

    [repository-root]$ git add .gitattributes
    [repository-root]$ git commit -m "lfs configured"
    

Submodules

See Git Submodules: Adding, Using, Removing, Updating.

  • add a submodule to an existing repository:

    git submodule add https://github.com/ohmyzsh/ohmyzsh lib/ohmyzsh
    
  • clone a repository with submodules:

    git clone --recursive keybase://public/bananas/dotfiles
    git clone --recurse-submodules ohmyzsh keybase://public/bananas/dotfiles
    
  • update an existing repository with submodules:

    git pull --recurse-submodules
    

To delete a submodule the procedure is more complicated:

  1. de-init the submodule:

    git submodule deinit lib/ohmyzsh
    

    this wil also remove the entry from $REPO_ROOT/.git/config

  2. remove the submodule from the index:

    git rm -rf lib/ohmyzsh
    

    this wil also remove the entry from $REPO_ROOT/.gitmodules

  3. commit the changes

Crypt

FIXME

Visualize the repo's history

git log --graph --full-history --all --color --decorate --oneline

Remove a file from a commit

See remove files from git commit.

Remove a file from the repository

  1. unstage the file using git reset specify the HEAD as source

    git reset HEAD superSecretFile
    
  2. remove it from the index using git rm with the --cached option

    git rm --cached superSecretFile
    
  3. check the file is no longer in the index

    $ git ls-files | grep superSecretFile
    $
    
  4. add it to .gitignore or remove it from the disk

  5. amend the most recent commit from your repository

    git commit --amend
    

Remotes management

# add a remote
git remote add gitlab git@gitlab.com:user/my-awesome-repo.git

# add other push urls to an existing remote
git remote set-url --push --add origin https://exampleuser@example.com/path/to/repo1

# change a remote
git remote set-url origin git@github.com:user/new-repo-name.git

Push to multiple git remotes with the one command

To always push to repo1, repo2, and repo3, but always pull only from repo1, set up the remote 'origin' as follows:

git remote add origin https://exampleuser@example.com/path/to/repo1
git remote set-url --push --add origin https://exampleuser@example.com/path/to/repo1
git remote set-url --push --add origin https://exampleuser@example.com/path/to/repo2
git remote set-url --push --add origin https://exampleuser@example.com/path/to/repo3
[remote "origin"]
    url = https://exampleuser@example.com/path/to/repo1
    pushurl = https://exampleuser@example.com/path/to/repo1
    pushurl = https://exampleuser@example.com/path/to/repo2
    pushurl = https://exampleuser@example.com/path/to/repo3
    fetch = +refs/heads/*:refs/remotes/origin/*

To only pull from repo1 but push to repo1 and repo2 for a specific branch specialBranch:

[remote "origin"]
    url = ssh://git@aaa.xxx.com:7999/yyy/repo1.git
    fetch = +refs/heads/*:refs/remotes/origin/*
    ...
[remote "specialRemote"]
    url = ssh://git@aaa.xxx.com:7999/yyy/repo1.git
    pushurl = ssh://git@aaa.xxx.com:7999/yyy/repo1.git
    pushurl = ssh://git@aaa.xxx.com:7999/yyy/repo2.git
    fetch = +refs/heads/*:refs/remotes/origin/*
    ...
[branch "specialBranch"]
    remote = origin
    pushRemote = specialRemote
    ...

See https://git-scm.com/docs/git-config#git-config-branchltnamegtremote.

Delete a branch

# locally
git branch --delete feat-branch
git branch -D feat-branch

# remote
git push origin :feat-branch
git push origin --delete feat-branch

# both
git branch --delete --remotes feat-branch

Sync the branch list

git fetch --prune

Sync the tags list

git fetch --prune-tags

Troubleshooting

GPG cannot sign a commit

error: gpg failed to sign the data
fatal: failed to write commit object

If gnupg2 and gpg-agent 2.x are used, be sure to set the environment variable GPG_TTY, specially zsh users with Powerlevel10k with Instant Prompt enabled.

export GPG_TTY=$(tty)

Further readings