Git tips

Posted on 2015-04-26 in Programmation Last modified on: 2018-09-02

This pages provides all gitips I have collected.

Rebase

Split commit

After git rebase -i, run git reset HEAD~ to split current commit.

To split 3rd most recent commit, git rebase -i HEAD~3.

Autosquash

You can auto-squash all commits you made to correct a previous commit with:

  1. Create a fixup commit: git commit --fixup=COMMIT_HASH
  2. Run rebase --interactive --autosquash REF

Cancel file deletion

touch <filename>
git checkout <filename>

Patch

  • Create a patch: git format-patch -1 <commit>
  • Apply as patch git apply <patch>
  • Apply as commit: git am <patch>

Branches

  • Delete a local branch: git branch -d <branch-name>
  • Delete a local branch with unmerge work: git branch -D <branch-name>
  • Delete a branch on the server: git push origin --delete <branch-name> or git push origin :branchname on old version of git

Ignoring versioned files

Run git update-index --assume-unchanged path/to/file.txt To cancel the operation, run: git update-index --no-assume-unchanged path/to/file.txt.

Note: git stash and git checkout . will have an effect on the file.

Source: https://stackoverflow.com/questions/18276951/how-do-i-stop-git-from-tracking-any-changes-to-a-file-from-this-commit-forward/18277622#18277622

Permanently remove files or folder from repo

This is an potentially dangerous command as it rewrites the history. It can discard commits if you have shared your code with other. Use carefully. You will need to use git push -f to update any distant repo.

It also implies that if you are working on a forked repository (on github for instance) you won't be able to make pull requests any more as all the rewritten commits will be included in it (that can amount to several hundreds) and it is most likely that upstream won't want them.

git filter-branch -f --prune-empty --index-filter 'git rm -r --cached --ignore-unmatch src/main/webapp/inc/img'  -- HEAD

With:

  • --ignore-unmatch allows you to ignore nonexistent files
  • -r to delete recursively
  • --prune-empty to discard empty commit

If you use a commit range (hash1..ref):

  • the first commit is not filtered
  • the end of the range must be a reference (a branch name for instance). You can try to use an temporary branch as describe here if needed.

If you messed up, you can use the reflog to reset your repo until you run the following commands. You must then remove the backup and force garbage collection to regain your disk size:

rm -rf .git/refs/original
git reflog expire --all --expire=now
git gc --prune=now --aggressive

Normally, after a git push -f you should see the reclaimed space. You can also manually update the remote tracked branch by using something like origin/master as reference.

You can add the --all option to rewrite all branches and tags:

git filter-branch --index-filter 'git rm -rf --cached --ignore-unmatch src/main/webapp/inc/img' -- --all

If you use --tree-filter with the command rm -rf instead of --index-filter you will get the result slower as it checkouts the tree.

You can use this command to find big files. It lists all files with their size for each commit in master. Then, it selects only the size and the file. Lastly, it sorts files by size, big size first and each file is counted only once.

git ls-tree -lr master | awk '{print $4 " " $5}' | sort -unr

The documentation page.

Sources:

Run multiple hooks in parallel

git hooks are very useful because they allow you to run a script before some actions (for instance: run a linter before committing or run the tests before pushing). If the script exists with 0, the action proceeds, if not, the action is cancelled.

The problem is that if you have many actions to run, it can be slow. One way to mitigate this is to run the commands of the script in parallel by terminating the line with & and rely on the wait builtin to wait for all commands to complete before continuing.

You cloud launch all the commands and at the end of the script simply put wait to wait for them but the script will always exit with the 0 status code making the hook useless.

One solution, to exit with a non zero status if a command had an error to do (adapt to your needs):

#!/usr/bin/bash
# See https://stackoverflow.com/a/26504775 for some background

make lint-js &
JS_LINT_PID=$!

make lint-py &
PY_LINT_PID=$!

pipenv run python manage.py compilemessages &
MO_LINT_PID=$!

wait ${JS_LINT_PID}
JS_LINT_EXIT_CODE=$?

wait ${PY_LINT_PID}
PY_LINT_EXIT_CODE=$?

wait ${MO_LINT_PID}
MO_LINT_EXIT_CODE=$?

EXIT_CODE=$((${JS_LINT_EXIT_CODE} + ${PY_LINT_EXIT_CODE} + ${MO_LINT_EXIT_CODE}))

exit ${EXIT_CODE}

In a nutshell:

  • we capture the PID of the command we just run in a variable with $!.
  • wait for the process associated to this PID to complete with wait $PID
  • capture the exit code of wait (which will match the exit code of the command) with $?
  • sum the code. If any of them is upper than 0, the sum will be too and we will exit with a non zero status code.

The problem with this basic script is that you don't have access to the output of the commands. Thus, you know you have a problem but don't know which one. This can be solved by sending the output of each command into a temporary file and displaying the content of this file if the command failed:

#!/usr/bin/bash
# https://stackoverflow.com/a/26504775

JS_LINT_LOG=$(mktemp)
make lint-js > ${JS_LINT_LOG} 2>&1 &
JS_LINT_PID=$!

PY_LINT_LOG=$(mktemp)
make lint-py > ${PY_LINT_LOG} 2>&1 &
PY_LINT_PID=$!

MO_LINT_LOG=$(mktemp)
pipenv run python manage.py compilemessages > ${MO_LINT_LOG} 2>&1 &
MO_LINT_PID=$!

wait ${JS_LINT_PID}
JS_LINT_EXIT_CODE=$?

wait ${PY_LINT_PID}
PY_LINT_EXIT_CODE=$?

wait ${MO_LINT_PID}
MO_LINT_EXIT_CODE=$?

EXIT_CODE=$((${JS_LINT_EXIT_CODE} + ${PY_LINT_EXIT_CODE} + ${MO_LINT_EXIT_CODE}))

if [[ "${JS_LINT_EXIT_CODE}" -ne 0 ]]; then
    echo "JS LINT OUTOUPUT" >&2
    cat ${JS_LINT_LOG} >&2
    echo -e "\n\n"
fi

if [[ "${PY_LINT_EXIT_CODE}" -ne 0 ]]; then
    echo "PY LINT OUTOUPUT" >&2
    cat ${PY_LINT_LOG} >&2
    echo -e "\n\n"
fi

if [[ "${MO_LINT_EXIT_CODE}" -ne 0 ]]; then
    echo "MO LINT OUTOUPUT" >&2
    cat ${MO_LINT_LOG} >&2
    echo -e "\n\n"
fi

rm ${JS_LINT_LOG} ${PY_LINT_LOG} ${MO_LINT_LOG}

exit ${EXIT_CODE}

Revert a merge commit

Because they have two ancestors, you can't revert merge commits with a simple git revert HASH. You need to specify the ancestor to revert to. For instance, imagine the graph below:

* c7545ec - Tue, 26 Mar 2019 16:52:39 +0100 (2 minutes ago)
|           Add titi - Julien Enselme
*   df849a2 - Tue, 26 Mar 2019 16:51:53 +0100 (3 minutes ago)
|\            Merge branch 'tmp' - Julien Enselme
| * a23acae - Tue, 26 Mar 2019 16:51:47 +0100 (3 minutes ago) (tmp)
|/            Add tata - Julien Enselme
* 8be0cb2 - Tue, 26 Mar 2019 16:51:34 +0100 (3 minutes ago)
|           Add toto - Julien Enselme
* 75182c4 - Tue, 26 Mar 2019 16:51:24 +0100 (3 minutes ago)
            “root” - Julien Enselme

The merge commit (df849a2) has two ancestors:

  • a23acae which comes from a feature branch.
  • 8be0cb2 which comes from the main branch.

To cancel it and cancel the commits from the feature branch, run git revert df849a2 -m 1.

git