[!NOTE|label:references:]

conventional commits

[!TIP|label:references:]

TYPE FULL NAME SIMPLE DESCRIPTION DESCRIPTION
build 构建系统或外部依赖项的变化 changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)
ci CI 配置文件和脚本的更改 changes to our ci configuration files and scripts (example scopes: travis, circle, browserstack, saucelabs)
docs documentation 仅文档修改 documentation only changes
feat feature 新功能 a new feature
fix bugfix 修复问题 a bug fix
perf 性能提高 a code change that improves performance
refactor 重构 (即不是新增功能, 也不是修改bug) a code change that neither fixes a bug nor adds a feature
style formatting 格式 (不影响代码运行的变动) changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)
test 增加测试 adding missing tests or correcting existing tests
chore maintain 构建过程或辅助工具的变动 changes to the build process or auxiliary tools and libraries such as documentation generation
docs(changelog): update changelog to beta.5
fix(release): need to depend on latest rxjs and zone.js

The version in our package.json gets copied to the one we publish, and users need the latest of these.
$ npm i -g @commitlint/config-conventional

# or
$ npm install -g @commitlint/cli @commitlint/config-conventional

commitlint

[!TIP|label:references:]

// ~/.commitlintrc.js
// CommonJS Syntax (most common)
module.exports = {
  extends: ['@commitlint/config-conventional'],
  rules: {
    'header-max-length': [0],                   // disable the header-max-length rule
    'body-max-line-length': [0],                // disable the body-max-line-length rule
  }
};

for jira tickets

// .commitlintrc.disable-subject-case.js
module.exports = {
  extends: ['@commitlint/config-conventional'],
  rules: {
    'header-max-length': [0],                   // disable the header-max-length rule
    'body-max-line-length': [0],                // disable the body-max-line-length rule
    'subject-case': [0],                        // disable the subject-case rule
  },
};

emoji

tricky

help and path

$ git --info-path
/opt/homebrew/opt/git/share/info

$ git --html-path
/opt/homebrew/opt/git/share/doc/git-doc

$ git --man-path
/opt/homebrew/opt/git/share/man

git --list-cmds

[!NOTE|label:references:]

CATEGORY DESCRIPTION DESCRIPTION EXAMPLE
mainporcelain Primary User Commands 核心用户命令 add, commit
ancillaryinterrogators Secondary Query Commands 辅助查询命令 blame, status
ancillarymanipulators Secondary Action Commands 辅助操作命令 clean, gc
plumbinginterrogators Low-Level Data Extraction 底层管道查询 cat-file, ls-tree, rev-parse
plumbingmanipulators Low-Level Object Manipulation 底层管道操作 hash-object, update-index
synchingrepositories Network & Sync Commands 仓库同步命令 fetch, pull, push
foreignscminterface External SCM Interfaces 异构 SCM 接口 svn, hg, p4
purehelpers Internal Utilities 纯内部辅助程序 credential, mergetool--lib
synchelpers Transport Protocols 同步辅助协议 remote-http, upload-pack
guide Conceptual Manuals 官方指南与教程 tutorial, revisions, glossary
developerinterfaces Git Core Dev Tools 开发者接口 -
userinterfaces Native GUIs 图形化界面 (GUI) gitk, gui
$ git --list-cmds=builtins
$ git --list-cmds=parseopt
$ git --list-cmds=deprecated
$ git --list-cmds=main
$ git --list-cmds=others
$ git --list-cmds=nohelpers
$ git --list-cmds=alias
$ git --list-cmds=config
# list-<category>
$ git --list-cmds=list-ancillaryinterrogators
$ git --list-cmds=list-ancillarymanipulators
$ git --list-cmds=list-developerinterfaces
$ git --list-cmds=list-foreignscminterface
$ git --list-cmds=list-guide
$ git --list-cmds=list-mainporcelain
$ git --list-cmds=list-plumbinginterrogators
$ git --list-cmds=list-plumbingmanipulators
$ git --list-cmds=list-purehelpers
$ git --list-cmds=list-synchelpers
$ git --list-cmds=list-synchingrepositories
$ git --list-cmds=list-userinterfaces

# i.e.:
$ git --list-cmds=list-guide
core-tutorial
credentials
cvs-migration
diffcore
everyday
faq
glossary
namespaces
remote-helpers
submodules
tutorial
tutorial-2
workflows

# and
$ git help tutorial-2
$ git help help

hidden feature

git var

$ git var GIT_COMMITTER_IDENT
marslo <marslo.jiao@gmail.com> 1719963678 -0700

$ git var -l

git verify-commit

quick edit gitconfig

$ git config --edit --global

# quick replace config
$ git config --global --replace-all core.pager cat

create git patch

$ git diff --no-color HEAD^..HEAD > <name>.patch

# or
$ git format-patch HEAD^^           # create 3 patch files automatically
$ git format-patch -1 <revision>    # create 1 patch file only

get current branch

$ git branch
  sandbox/marslo
* master
  • branch

    $ git branch --show-current
    
    # or
    $ git branch --show
    
    # or
    $ git branch | sed -ne 's:^\*\s*\(.*\)$:\1:p'
    master
    
  • symbolic-ref

    $ git symbolic-ref --short HEAD
    master
    
    $ git symbolic-ref HEAD
    refs/heads/master
    
  • name-rev

    $ git name-rev --name-only HEAD
    remotes/origin/master
    
  • describe

    $ git describe --contains --all HEAD
    master
    

get previous branch

get previous branch name

$ git rev-parse --symbolic-full-name @{-1}
refs/heads/sandbox/marslo/291

or

$ git describe --all $(git rev-parse @{-1})
heads/sandbox/marslo/291

checkout to previous branch

$ git checkout -
  • or
    $ git checkout @{-1}
    

quick diff with previous branch

$ git diff ..@{-1}

# or
$ git diff @..@{-1}

# or
$ git diff HEAD..@{-1}

quick push to current branch

$ git push origin @

# or
$ git push origin HEAD

remove warning: CRLF will be replaced by LF in xxx file for .gitattributes

$ git add --all -u --renormalize .
  • or ignore the warning
    $ git config --global core.safecrlf false
    

create multiple commits

$ for c in {0..10}; do
    echo "$c" >> squash.txt
    git add squash.txt
    git commit -m "add '${c}' to squash.txt"
done

revision

the first revision

$ git rev-list --max-parents=0 HEAD

git path

get absolute root path

$ git rev-parse --show-toplevel

get relative root path

$ git rev-parse --show-cdup

get absolute root path inside submodules

$ git rev-parse --show-superproject-working-tree

get .git path

$ git rev-parse --git-dir

inside the work tree or not

$ git rev-parse --is-inside-work-tree

.gitattributes

Refreshing the repository after committing .gitattributes

reference:

$ rm -rf .git/index
# or
$ git rm --cached -r .
# or
$ git ls-files -z | xargs -0 rm

$ git reset --hard

or

$ echo "* text=auto" >.gitattributes
$ git add --renormalize .
$ git status        # Show files that will be normalized
$ git commit -m "Introduce end-of-line normalization"

format

reference Be a Git ninja: the .gitattributes file

$ cat .gitattributes
*             text=auto
*.sh          eol=lf
path/to/file  eol=lf

who-am-i

[!NOTE|label:references:]

# show default credential
$ echo -e 'protocol=https\nhost=github.com' | git credential fill
protocol=https
host=github.com
username=marslo
password=gho_jzuA**************************1VRqXz

# show another credential with specific subpath
$ echo -e 'protocol=https\nhost=github.com/<account>' | git credential fill
protocol=https
host=github.com/<account>
username=marslojiao-mvl
password=ghp_ppHq*************************g1PXSvr
  • reject the cached
    $ echo -e 'protocol=https\nhost=github.com' | git credential reject
    

find bug with bisect

$ git bisect start
$ git bisect bad                 # current commit is bad
$ git bisect good <commit-hash>  # a known good commit

view file at specific commit

$ git show <commit-hash>:path/to/file

edit .gitignore after committing

$ echo "node_modules/" >> .gitignore
$ git rm -r --cached node_modules/
$ git commit -m "Update .gitignore"

check file in which branch

$ path='path/to/file.txt'

# with detail info
$ while read -r branch; do
    foundit=$(git ls-tree -r "${branch}" "${path}");
    [[ -n "${foundit:-}" ]] && echo -e ">> ${branch} <<\n${foundit}\n";
  done< <(git branch --no-color -r)

# with rev-parse
$ while read -r branch; do
    git rev-parse --verify -q "${branch}":"${path}" && echo -e "\n>> ${branch} <<"
  done< <(git branch --no-color -r)

# with cat-file
$ while read -r branch; do
    git cat-file -e "${branch}":"${path}" 2>/dev/null && echo -e ">> ${branch} <<"
  done< <(git branch -r --no-color)

# to show lfs files
$ while read -r path; do
    echo -e "\n\n\033[7;37m>> ${path}\033[0m"
    git check-attr -a "${path}"; echo ''

    while read -r branch; do
      foundit=$(git ls-tree -r "${branch}" "${path}");
      if [[ -n "${foundit:-}" ]]; then echo -e "\033[0;32m>> ${branch} <<\n${foundit}\033[0m\n"; fi;
    done< <( git for-each-ref refs/remotes/origin --sort=-committerdate --format="%(refname:short)" | grep --color=never -v -E '^origin$' )
  done< <( sed 's:^/::; /^#/d' "$(git rev-parse --show-toplevel)/.gitattributes" | awk '{print $1}' | sort -u )

check blob hash in which branch

[!NOTE|label:references:] hash getting older: Branch (Ref) -> Commit -> Tree -> Blob (file content) scripts:

# find the largest files in the repo
$ git rev-list --objects --all |
  git cat-file --batch-check='%(objectname) %(objecttype) %(objectsize) %(rest)' |
  awk '{printf "%s\t%.2f MB\t%s\n", $1, $3/1024/1024, $4}' |
  sort -rn -k 2 |
  head -5
6e73f2d4dcfb223fe300c324f0f8ce90277a7397  405.65 MB   path/to/largefile.bin

# check which branch contains the blob
$ while read commit_hash; do
    git for-each-ref --contains "${commit_hash}";
  done< <(git log --all --find-object=6e73f2d4dcfb223fe300c324f0f8ce90277a7397 --pretty=format:"%H")
5c70ecd90f3c9329a9f2ac035ed2ff8d979414f1 commit refs/heads/sandbox/temp
08581b446ca928790c7c0f5b0829fe8253071c6c commit refs/heads/sandbox/emulator-20200713
5c70ecd90f3c9329a9f2ac035ed2ff8d979414f1 commit refs/heads/sandbox/temp
37cdb8df0df82db41056d8808bf4b1c0c01ebfa6 tag  refs/tags/sandbox/octeontx2-20200422
4c36260dd076c5e45f29c211f674891daa7a36d7 tag  refs/tags/sandbox/temp-next-20200409-98xx-networking
c8d82b97f17abe97d5054cb61ff6d82c43d56e10 commit refs/heads/sandbox/temp-next-ww09
2c077958364bae76d84f11ea6288d0c7b203d399 commit refs/tags/sandbox/ww09-firmware-booting-20200319
1e5a883c252c239812b47172306c96d9eda57d02 commit refs/heads/sandbox/temp-SDK-10.3.0.0-ED1007
26b6604cfd80df7809509695300dba3042d323e2 commit refs/heads/sandbox/temp-next-new

trailers

[!NOTE|label:references:]

git config

[!TIP|label:tips:]

  • if trailer.sign.command is not set, the default value is git var GIT_COMMITTER_IDENT
  • if trailer.sign.key set as "Signed-off-by: ", it will impacted the git log --format=%(trailers:key=Signed-off-by:,valueonly,separator=%x2C)
$ git config --global trailer.sign.key "Signed-off-by"
$ git config --global trailer.sign.ifmissing add
$ git config --global trailer.sign.ifexists doNothing
$ git config --global trailer.sign.command "echo \"$(git config user.name) <$(git config user.email)>\""

# or
$ cat ~/.gitconfig
[trailer "sign"]
  key               = Signed-off-by
  ifmissing         = add
  ifexists          = doNothing
  command           = echo \"$(git config user.name) <$(git config user.email)>\"

generate trailers

[!NOTE|label:references:]

$ git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p'

## commit-msg.sample

# Uncomment the below to add a Signed-off-by line to the message.
# Doing this in a hook is a bad idea in general, but the prepare-commit-msg
# hook is more suited to it.
#
# SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p')
# grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1"

# This example catches duplicate Signed-off-by lines.
test "" = "$(grep '^Signed-off-by: ' "$1" |
         sort | uniq -c | sed -e '/^[   ]*1[    ]/d')" || {
        echo >&2 Duplicate Signed-off-by lines.
        exit 1
}
  • by template

    $ cat ~/.git-template
    Signed-off-by: Your Name <your.email@example.com>
    
    $ git config commit.template ~/.git-template
    

commit

$ git commit --signoff
# or
$ git commit -s
  • with control

    declare signed="$(git log -n1 --format='%(trailers:key=Signed-off-by,valueonly,separator=%x2C)' | command grep -q "$(git config user.email)"; echo $?)";
    if [ 0 -eq ${signed} ]; then
      OPT='commit --amend --allow-empty';
    else
      OPT='commit --signoff --amend --allow-empty';
    fi;
    
  • i.e.:

    [alias]
    ### [c]omit [a]dd [a]all
    caa         = "!f() { \
                          git add --all; \
                          declare signed=\"$(git log -n1 --format='%(trailers:key=Signed-off-by,valueonly,separator=%x2C)' | command grep -q \"$(git config user.email)\"; echo $?)\"; \
                          if [ 0 -eq ${signed} ]; then \
                            git commit --amend --no-edit --allow-empty;\
                          else \
                            git commit --signoff --amend --no-edit --allow-empty;\
                          fi; \
                        }; f \
    

show trailers

[!NOTE|label:references:]

$ git log -n1 --format='%(trailers:key=Signed-off-by:,valueonly,separator=%x2C)'
marslo <marslo.jiao@gmail.com>

$ git log -n1 --format='%(trailers:key=Signed-off-by:,keyonly,separator=%x2C)'
Signed-off-by

# or
$ git log -n1 --format=%B | git interpret-trailers --parse
Signed-off-by: marslo <marslo.jiao@gmail.com>
Change-Id: I3cc1cb4cfaf4300d2e7972eb39a7319e81012c65

# or
$ git log -1 --pretty=format:"%b"
Signed-off-by: marslo <marslo.jiao@gmail.com>
Change-Id: I3cc1cb4cfaf4300d2e7972eb39a7319e81012c65

# or
$ git log --pretty=format:"%b" | command grep -E "^(Signed-off-by|Co-authored-by):"

configure and format

  • Signed-off-by:

    $ git config --global trailer.sign.key 'Signed-off-by: '
    $ git log -1 --format="%(trailers:key=Signed-off-by,valueonly,separator=%x2C)"
    
    $ git log -1 --format="%(trailers:key=Signed-off-by: ,valueonly,separator=%x2C)"
    marslo <marslo.jiao@gmail.com>
    
  • Signed-off-by

    $ git config --global trailer.sign.key 'Signed-off-by'
    
    $ git log -1 --format="%(trailers:key=Signed-off-by: ,valueonly,separator=%x2C)"
    
    $ git log -1 --format="%(trailers:key=Signed-off-by:,valueonly,separator=%x2C)"
    marslo <marslo.jiao@gmail.com>
    $ git log -1 --format="%(trailers:key=Signed-off-by,valueonly,separator=%x2C)"
    marslo <marslo.jiao@gmail.com>
    

hook

[!NOTE|label:references:]

  • commit-msg for signed-off-by

    commit-message for change-id
    #!/bin/sh
    
    NAME=$(git config user.name)
    EMAIL=$(git config user.email)
    
    if [ -z "$NAME" ]; then
      echo "empty git config user.name"
      exit 1
    fi
    
    if [ -z "$EMAIL" ]; then
      echo "empty git config user.email"
      exit 1
    fi
    
    git interpret-trailers --if-exists doNothing --trailer \
        "Signed-off-by: $NAME <$EMAIL>" \
        --in-place "$1"
    
  • commit-msg for change-id

    commit-message for change-id
    #!/bin/sh
    # From Gerrit Code Review 2.6
    #
    # Part of Gerrit Code Review (http://code.google.com/p/gerrit/)
    #
    # Copyright (C) 2009 The Android Open Source Project
    #
    # Licensed under the Apache License, Version 2.0 (the "License");
    # you may not use this file except in compliance with the License.
    # You may obtain a copy of the License at
    #
    # http://www.apache.org/licenses/LICENSE-2.0
    #
    # Unless required by applicable law or agreed to in writing, software
    # distributed under the License is distributed on an "AS IS" BASIS,
    # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    # See the License for the specific language governing permissions and
    # limitations under the License.
    #
    
    unset GREP_OPTIONS
    
    CHANGE_ID_AFTER="Bug|Issue"
    MSG="$1"
    
    # Check for, and add if missing, a unique Change-Id
    #
    add_ChangeId() {
      clean_message=`sed -e '
      /^diff --git a\/.*/{
      s///
      q
      }
      /^Signed-off-by:/d
      /^#/d
      ' "$MSG" | git stripspace`
      if test -z "$clean_message"; then return; fi
    
      # Does Change-Id: already exist? if so, exit (no change).
      if grep -i '^Change-Id:' "$MSG" >/dev/null; then return; fi
    
      id=`_gen_ChangeId`
      T="$MSG.tmp.$$"
      AWK=awk
      if [ -x /usr/xpg4/bin/awk ]; then
        # Solaris AWK is just too broken
        AWK=/usr/xpg4/bin/awk
      fi
    
      # How this works:
      # - parse the commit message as (textLine+ blankLine*)*
      # - assume textLine+ to be a footer until proven otherwise
      # - exception: the first block is not footer (as it is the title)
      # - read textLine+ into a variable
      # - then count blankLines
      # - once the next textLine appears, print textLine+ blankLine* as these
      # aren't footer
      # - in END, the last textLine+ block is available for footer parsing
      $AWK '
      BEGIN {
      # while we start with the assumption that textLine+
      # is a footer, the first block is not.
      isFooter = 0
      footerComment = 0
      blankLines = 0
      }
    
      # Skip lines starting with "#" without any spaces before it.
      /^#/ { next }
    
      # Skip the line starting with the diff command and everything after it,
      # up to the end of the file, assuming it is only patch data.
      # If more than one line before the diff was empty, strip all but one.
      /^diff --git a/ {
        blankLines = 0
        while (getline) { }
        next
      }
    
      # Count blank lines outside footer comments
      /^$/ && (footerComment == 0) {
        blankLines++
        next
      }
    
      # Catch footer comment
      /^\[[a-zA-Z0-9-]+:/ && (isFooter == 1) {
        footerComment = 1
      }
    
      /]$/ && (footerComment == 1) {
        footerComment = 2
      }
    
      # We have a non-blank line after blank lines. Handle this.
      (blankLines > 0) {
        print lines
        for (i = 0; i < blankLines; i++) {
          print ""
        }
    
        lines = ""
        blankLines = 0
        isFooter = 1
        footerComment = 0
      }
    
      # Detect that the current block is not the footer
      (footerComment == 0) && (!/^\[?[a-zA-Z0-9-]+:/ || /^[a-zA-Z0-9-]+:\/\//) {
        isFooter = 0
      }
    
      {
        # We need this information about the current last comment line
        if (footerComment == 2) {
          footerComment = 0
        }
        if (lines != "") {
          lines = lines "\n";
        }
        lines = lines $0
      }
    
      # Footer handling:
      # If the last block is considered a footer, splice in the Change-Id at the
      # right place.
      # Look for the right place to inject Change-Id by considering
      # CHANGE_ID_AFTER. Keys listed in it (case insensitive) come first,
      # then Change-Id, then everything else (eg. Signed-off-by:).
      #
      # Otherwise just print the last block, a new line and the Change-Id as a
      # block of its own.
      END {
      unprinted = 1
      if (isFooter == 0) {
      print lines "\n"
      lines = ""
      }
      changeIdAfter = "^(" tolower("'"$CHANGE_ID_AFTER"'") "):"
      numlines = split(lines, footer, "\n")
      for (line = 1; line <= numlines; line++) {
      if (unprinted && match(tolower(footer[line]), changeIdAfter) != 1) {
      unprinted = 0
      print "Change-Id: I'"$id"'"
      }
      print footer[line]
      }
      if (unprinted) {
      print "Change-Id: I'"$id"'"
      }
      }' "$MSG" > "$T" && mv "$T" "$MSG" || rm -f "$T"
    }
    
    _gen_ChangeIdInput() {
      echo "tree `git write-tree`"
      if parent=`git rev-parse "HEAD^0" 2>/dev/null`; then
        echo "parent $parent"
      fi
      echo "author `git var GIT_AUTHOR_IDENT`"
      echo "committer `git var GIT_COMMITTER_IDENT`"
      echo
      printf '%s' "$clean_message"
    }
    _gen_ChangeId() {
      _gen_ChangeIdInput |
      git hash-object -t commit --stdin
    }
    
    add_ChangeId
    

filter-repo

[!TIP]

[!NOTE|label:check keywords in repo history:]

  • * git summaries

    # show accounts
    $ git shortlog -n -s -e
    
    # find all commits with old email
    $ git log --all --format='%h %D %ad %ae %ce %s' --date=short | grep -Fi '<KEYWORD>'
    $ git log --all --format='%ae%n%ce' | grep -i '<KEYWORD>'
    $ git log --format='%H %ad %cd %ae' --date=iso-strict | grep -i '<KEYWORD>'
    
    # check tags
    $ git for-each-ref refs/tags --format='%(refname:short) %(taggername) %(taggeremail)'
    

change commit email

$ cd /path/to/repo

# update email in history
$ GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null git filter-repo --force --commit-callback '
old = b"OLD@DOMAIN.COM"
new = b"NAME@EMAIL.COM"
if commit.author_email == old:
    commit.author_email = new
if commit.committer_email == old:
    commit.committer_email = new
'
# update email, name and tags
$ GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null git filter-repo --force \
    --email-callback '
        return b"NAME@EMAIL.COM" if email in [
            b"OLD@DOMAIN.COM",
            b"WRONG@DOMAIN.COM"
        ] else email
    ' \
    --name-callback '
        return b"NAME" if name in [b"OLD", b"WRONG NAME"] else name
    ' \
    --tag-callback '
        if tag.tagger_email in [b"OLD@DOMAIN.COM", b"WRONG@DOMAIN.COM"]:
            tag.tagger_email = b"NAME@EMAIL.COM"
        if tag.tagger_name in [b"OLD", b"WRONG NAME"]:
            tag.tagger_name  = b"NAME"
        return tag
    '

# re-add the origin
$ git remote add origin <remote-url>

$ git push origin --all --force
$ git push origin --tags --force
# or with case-insensitive match
$ GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null git filter-repo --force --commit-callback '
keyword = b"KEYWORD"
new = b"NAME@EMAIL.COM"
if keyword in commit.author_email.lower():
    commit.author_email = new
if keyword in commit.committer_email.lower():
    commit.committer_email = new
'
# or with message replace
$ GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null git filter-repo --force --commit-callback '
keyword = b"KEYWORD"
mail = b"NAME@EMAIL.COM"

if keyword in commit.author_email.lower():
    commit.author_email = mail
if keyword in commit.committer_email.lower():
    commit.committer_email = mail
' \
  --message-callback '
mail = b"<NAME@EMAIL.COM>"
msg = message.replace(b"<old@domain.com>", mail)
msg = msg.replace(b"<OLD@DOMAIN.COM>", mail)
return msg
'

separate folder into new repo

  1. create new repo

    # extract folder-1 and its history
    $ GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null \
     git filter-repo --force --subdirectory-filter folder-1/
    
    # extract multiple folders
    $ GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null \
     git filter-repo --force --path folder-1/ --path folder-2/ --path secret.key
    
    # extract multiple folders and merge into single folder
    # make sure no file name conflict between the folders
    $ GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null \
     git filter-repo \
       --path folder-1/ --path folder-2/ \
       --path-rename folder-1/: \
       --path-rename folder-2/:
    
  2. clean in old repo

    $ GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null \
     git filter-repo --path folder-1/ --path folder-2/ --path secret.key --invert-paths
    
    # -- with glob pattern --
    $ GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null \
     git filter-repo --path-glob 'temp-*/' --path-glob 'logs/*.log' --invert-paths
    
    # -- with config file --
    $ cat to-delete.txt
    folder-1/
    folder-2/
    config/passwords.json
    internal/test-data/
    $ GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null \
     git filter-repo --paths-from-file to-delete.txt --invert-paths
    

rewrite the commit history

# `ISSUE-xxx: xxxx` -> `[LEGACY] ISSUE-xxx: xxxx`
$ git filter-repo --message-callback '
  if not message.startswith(b"ISSUE-"):
      return b"[LEGACY] " + message
  return message
'

replace sensitive words in all files

# `my_secret_password` -> `***REDACTED***` in all files and commit messages
$ cat expressions.txt
regex:my_secret_password==>***REDACTED***
$ git filter-repo --replace-text expressions.txt

# "password" -> "******" in all files and commit messages
$ git filter-repo --replace-text <( echo "password==>******" )

# or with config file
$ cat replace.txt
password==>******
secret==>
$ git filter-repo --replace-text replace.txt

cleanup repo

# cleanup the repo by removing blobs bigger than 50MB
$ git filter-repo --strip-blobs-bigger-than 50M

textcov + gitattributes

[!NOTE|label:references:] reason:

  • Stylus browser-extension exports are minified: the whole document sits on one very long line, and CSS bodies are stored as string values with \n escaped inline. A raw git diff shows the entire file as a single changed line and is unreadable solution: a git textconv diff driver
  • ~/.gitattributes: routes matching paths to the driver
  • gitconfig: [diff "stylus-json"] section defines the driver and points to a script that expands Stylus JSON files for diffing
  • stylus-expand.sh: expands Stylus JSON files for diffing, using jq to pretty-print the JSON and sed to replace escaped newlines with actual newlines. If jq fails (e.g., if the file is not valid JSON), it falls back to simply outputting the original file content
#!/usr/bin/env bash
# ~/.marslo/gitconfig.d/bin/stylus-expand.sh

set -euo pipefail

jq '.' "$1" 2>/dev/null | sed 's/\\n/\n/g' || cat "$1"

# vim:tabstop=2:softtabstop=2:shiftwidth=2:expandtab:filetype=sh:
$ cat ~/.gitconfig
[diff "stylus-json"]
  textconv          = ~/.marslo/gitconfig.d/bin/stylus-expand.sh
  cachetextconv     = true
$ cat ~/.gitattributes
stylus-*.json    diff=stylus-json

injecting git config via environment variables

[!TIP|label:tips:]

  • references:
    • GIT_CONFIG_COUNT
    • git ≥ 2.31 supports injecting config through environment variables. Equivalent to the command-line -c section.key=value, but it is inherited by child processes.
  • use case scenarios: GIT_CONFIG_COUNT=x pre-commit run xxx
  • tips:
    • precedence: env config overrides all config files, but is itself overridden by git -c key=value on the command line — so it is not the highest.
      • order (low → high): system < global (~/.gitconfig) < repo (.git/config) < worktree < GIT_CONFIG_* env < git -c
    • affects only the current process and its children; it expires when the command exits and writes to no config file.
VARIABLE MEANING
GIT_CONFIG_COUNT=<n> number of entries to inject; git reads indices 0 … n-1
GIT_CONFIG_KEY_<i> key of entry i, full section.key (with the dot)
GIT_CONFIG_VALUE_<i> value of entry i

i.e.:

VARIABLE GIT CONFIG
GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=color.diff GIT_CONFIG_VALUE_0=auto git -c color.diff=auto
GIT_CONFIG_COUNT=2 GIT_CONFIG_KEY_0=color.diff GIT_CONFIG_VALUE_0=auto GIT_CONFIG_KEY_1=diff.colormoved GIT_CONFIG_VALUE_1=no git -c color.diff=auto -c diff.colormoved=no

[!NOTE|label:background] color.diff=always forces color onto piped output, activating colorMoved+allow-indentation-change, which hangs the git diff inside pre-commit on large diffs; injecting color.diff=auto via env fixes it

$ git config --show-scope --show-origin --get diff.colorMoved
global  file:/Users/marslo/.gitconfig zebra
$ git config --show-scope --show-origin --get diff.colorMovedWS
global  file:/Users/marslo/.gitconfig allow-indentation-change
# or
$ git config --show-scope --show-origin --get color.diff
global file:/Users/marslo/.marslo/gitconfig.d/gitcolors  always
# == -c color.diff=auto
$ GIT_CONFIG_COUNT=1 \
    GIT_CONFIG_KEY_0=color.diff GIT_CONFIG_VALUE_0=auto \
    pre-commit run --all-files

# == -c color.diff=auto -c diff.colormoved=no
$ GIT_CONFIG_COUNT=2 \
    GIT_CONFIG_KEY_0=color.diff      GIT_CONFIG_VALUE_0=auto \
    GIT_CONFIG_KEY_1=diff.colormoved GIT_CONFIG_VALUE_1=no \
    pre-commit run --all-files

others

case-sensitive repo in osx

$ hdiutil create -type SPARSE \
          -fs "Case-sensitive APFS" \
          -size 10g  \
          -volname casesensitive \
          /tmp/casesensitive.dmg

$ hdiutil attach ~/casesensitive.dmg.sparseimage

# clone
$ git clone https://github.com/<OWNER>/<REPO>.git /Volumes/casesensitive/<REPO>

show git alias

$ git --list-cmds=alias

# or
$ git config --get-regexp '^alias\.'

disk size

# reachable objects
$ git rev-list --disk-usage --objects --all

# plus reflogs
$ git rev-list --disk-usage --objects --all --reflog

# total disk size used
$ du -c .git/objects/pack/*.pack .git/objects/??/*
# alternative to du: add up "size" and "size-pack" fields
$ git count-objects -v

# report the disk size of each branch
$ git for-each-ref --format='%(refname)' |
  while read branch; do
    size=$(git rev-list --disk-usage --objects HEAD..$branch)
    echo "$size $branch"
  done |
  sort -n

# compare the on-disk size of branches in one group of refs, excluding another
$ git rev-list --disk-usage --objects --remotes=$suspect --not --remotes=origin

how is git commit sha1 formed

[!NOTE|label:references:]

Copyright © marslo 2020-2026 all right reserved,powered by GitbookLast Modified: 2026-08-26 04:22:12

results matching ""

    No results matching ""