Skip to content

Git 常用命令

Update local clone after remote branch name changes

sh
git branch -m <OLD-BRANCH> <NEW-BRANCH>
git fetch origin
git branch -u origin/<NEW-BRANCH> <NEW-BRANCH>
git remote set-head origin -a

One-line example (change master to main):

sh
git branch -m master main && git fetch origin && git branch -u origin/main main && git remote set-head origin -a

Separate subfolder in current Git repo to a new repo

  1. Create a new folder where would place the new repo
  2. git clone the original repo under this folder
  3. Download git-filter-repo from here: (Do not chagne its extension)
  4. cd the cloned repo root folder, and run following: python git-filter-repo --path <directory_to_separate_to_new_repo> --force
  5. Copy follwing files to root path of the new folder created in step 1:
    • Remaining files under <directory_to_separate_to_new_repo>
    • .git directory in original repo root folder
    • Run git status in the new folder root, to check the git status
    • Run git rev-list --count main to check the commits count of separated directory
  6. git add ., and commit the change as a re-organizing of file structure
  7. Create new repo in Github, and get its url, then run:
    sh
    git remote add origin https://github.com/<owner>/<new_repo>.git
    git remote -v
    git push -u origin main

See: Quickly rewrite git repository history (filter-branch replacement)

清除 GitHub 上的僵尸提醒

Stuck with a notification for a deleted repository #174843

Bug: ghost notifications #6874

这种一般是因为提醒的仓库被删除了。

清除未读提醒:

sh
gh api notifications\?all=true | jq -r 'map(select(.unread) | .id)[]' | xargs -L1 sh -c 'gh api -X PATCH notifications/threads/$0'

如果还想删除侧边栏里这些提醒的仓库列表:

sh
gh api 'notifications?all=true' | jq -r '.[].id' | xargs -I {} gh api -X DELETE 'notifications/threads/{}'

make changes in fork repo and make the commits clean

清理 commit 并 rebase

配置 upstream 并拉取最新代码:

sh
git remote add upstream https://github.com/<OLD_OWNER>/<OLD_REPO>.git
git fetch upstream

切换到 main 分支并基于 upstream/main 进行 rebase:

sh
git checkout main
git rebase -i upstream/main

编辑 commit 记录

在编辑器中,删除不想要的 commit:

sh
pick aaaaaaa change message 1
pick bbbbbbb change message 2
pick ccccccc Merge branch '<OLD_OWNER>:main' into main
pick ddddddd change message 2
  • 比如这里的 ccccccc 是合并 upstream/main 的 commit,可以删除这一行。

同时如果想合成一个 commit,可以把多行 pick 改成 squashs

sh
pick aaaaaaa change message 1
s bbbbbbb change message 2
s ddddddd change message 2

:wq 保存退出。

如果用了 squash,会进入下一个编辑界面,编辑合并后的 commit message:

sh
# This is a combination of 3 commits.
# The first commit's message is:
change message 1
# The 2nd commit's message is:
change message 2
# The 3rd commit's message is:
change message 2

编辑成想要的 commit message,保存退出即可。

rebase 出现问题

如果 git rebase -i 的过程中出现了问题,可以回到 rebase 的当前状态:

sh
git rebase --edit-todo

或者干脆放弃 rebase:

sh
git rebase --abort

强制推送到 fork repo

强制推送到自己 fork 的 repo:

sh
git push --force-with-lease origin main

找回误删的 commit

如果多删了 commit,可以通过 git reflog 找回:

sh
git reflog

找到对应的 commit id,然后执行:

sh
git checkout main
git cherry-pick <COMMIT_ID>

回到 rebase 前的状态

sh
git reflog

找到 rebase 前的 commit id,然后执行:

sh
git checkout main
git reset --hard <COMMIT_ID>

将 main 分支的改动放到 dev 分支

从 main 分支创建 dev 分支:

sh
git checkout main
git checkout -b dev

将 dev 推送到远程:

sh
git push -u origin dev

将 main 复位到 upstream/main:

sh
git checkout main
git fetch upstream
git reset --hard upstream/main
git push --force-with-lease origin main

使用 main 同步上游,切换 dev 开发

同步上游 main 分支的改动到本地 main 分支,并推送到自己的 fork repo:

sh
git checkout main
git fetch upstream
git merge --ff-only upstream/main
git push origin main

在 dev 分支上进行开发:

sh
git checkout -b dev   # 或 feature/xxx

完成开发和改动后,推送到远程 dev 分支:

sh
git push -u origin dev

sync with upstream, in both local and remote of forked repo

sh
# git remote -v
# git remote add upstream https://github.com/<OLD_OWNER>/<OLD_REPO>.git
git fetch upstream
git pull upstream main
git push origin main

或者一行:

sh
git fetch upstream && git pull upstream main && git push origin main

美化 git diff 输出

写入~/.gdc.sh
sh
gdc() {
  emulate -L zsh

  git rev-parse --is-inside-work-tree >/dev/null 2>&1 || {
    echo "Not a git repository"
    return 1
  }

  local mode="head"
  local revspec=""
  local show_untracked=1
  local name_only=0
  local sort_key=""
  local top_n=""

  while [[ $# -gt 0 ]]; do
    case "$1" in
      -c|--cached|-s|--staged)
        mode="cached"
        show_untracked=0
        shift
        ;;
      -w|--worktree)
        mode="worktree"
        show_untracked=1
        shift
        ;;
      -n|--name-only)
        name_only=1
        shift
        ;;
      -S|--sort)
        shift
        [[ $# -gt 0 ]] || {
          echo "gdc: missing value for --sort"
          return 1
        }
        case "$1" in
          add|del|file)
            sort_key="$1"
            ;;
          *)
            echo "gdc: invalid sort key: $1 (expected: add|del|file)"
            return 1
            ;;
        esac
        shift
        ;;
      -t|--top)
        shift
        [[ $# -gt 0 ]] || {
          echo "gdc: missing value for --top"
          return 1
        }
        [[ "$1" == <-> ]] || {
          echo "gdc: --top expects a positive integer"
          return 1
        }
        top_n="$1"
        shift
        ;;
      -h|--help)
        cat <<'EOH'
Usage:
  gdc [options] [<rev-or-range>]

Modes:
  -c, --cached        Show staged changes only
  -s, --staged        Same as --cached
  -w, --worktree      Show unstaged changes + untracked files
      (default)       Show diff vs HEAD (staged + unstaged) + untracked files

Display options:
  -n, --name-only     Show only file names
  -S, --sort KEY      Sort by: add | del | file
  -t, --top N         Show only the first N rows after sorting/current order
  -h, --help          Show this help

Examples:
  gdc
  gdc -c
  gdc -w
  gdc HEAD~1
  gdc HEAD~3..HEAD
  gdc -S add
  gdc -S del -t 20
  gdc -n -S file
EOH
        return 0
        ;;
      --)
        shift
        break
        ;;
      -*)
        echo "gdc: unknown option: $1"
        return 1
        ;;
      *)
        if [[ -z "$revspec" ]]; then
          revspec="$1"
          mode="revspec"
          show_untracked=1
          shift
        else
          echo "gdc: unexpected extra argument: $1"
          return 1
        fi
        ;;
    esac
  done

  local tmp_raw tmp_view
  tmp_raw="$(mktemp)" || return 1
  tmp_view="$(mktemp)" || {
    rm -f "$tmp_raw"
    return 1
  }

  {
    case "$mode" in
      head)
        git diff --numstat HEAD
        ;;
      cached)
        git diff --cached --numstat
        ;;
      worktree)
        git diff --numstat
        ;;
      revspec)
        git diff --numstat "$revspec"
        ;;
    esac

    if [[ "$show_untracked" -eq 1 ]]; then
      git ls-files --others --exclude-standard -z |
      while IFS= read -r -d '' file; do
        [[ -f "$file" ]] || continue
        printf '%s\t0\t%s\n' "$(wc -l < "$file" 2>/dev/null | tr -d '[:space:]')" "$file"
      done
    fi
  } > "$tmp_raw"

  if [[ -n "$sort_key" ]]; then
    case "$sort_key" in
      add)
        LC_ALL=C sort -t $'\t' -k1,1nr -k2,2nr -k3,3 "$tmp_raw" > "$tmp_view"
        ;;
      del)
        LC_ALL=C sort -t $'\t' -k2,2nr -k1,1nr -k3,3 "$tmp_raw" > "$tmp_view"
        ;;
      file)
        LC_ALL=C sort -t $'\t' -k3,3 "$tmp_raw" > "$tmp_view"
        ;;
    esac
  else
    cp "$tmp_raw" "$tmp_view"
  fi

  if [[ -n "$top_n" ]]; then
    head -n "$top_n" "$tmp_view" > "${tmp_view}.top"
    mv "${tmp_view}.top" "$tmp_view"
  fi

  if [[ "$name_only" -eq 1 ]]; then
    awk -F '\t' '
      BEGIN {
        blue  = "\033[34m"
        reset = "\033[0m"
      }
      NF >= 3 && $3 != "" {
        file = $3
        dir = ""
        base = file

        if (match(file, /.*\//)) {
          dir = substr(file, 1, RLENGTH)
          base = substr(file, RLENGTH + 1)
        }

        printf "%s" blue "%s" reset "\n", dir, base
      }
    ' "$tmp_view"
  else
    awk -F '\t' '
      BEGIN {
        green   = "\033[32m"
        red     = "\033[31m"
        blue    = "\033[34m"
        magenta = "\033[35m"
        bold    = "\033[1m"
        reset   = "\033[0m"

        add_sum = 0
        del_sum = 0
        n = 0
        maxw = 3
        filew = 4
      }

      NF < 3 || $3 == "" { next }

      {
        adds[++n] = $1
        dels[n]   = $2
        files[n]  = $3

        if ($1 ~ /^[0-9]+$/) {
          add_sum += $1
          if (length($1) > maxw) maxw = length($1)
        }
        if ($2 ~ /^[0-9]+$/) {
          del_sum += $2
          if (length($2) > maxw) maxw = length($2)
        }
        if (length($3) > filew) filew = length($3)
      }

      END {
        if (length(add_sum "") > maxw) maxw = length(add_sum "")
        if (length(del_sum "") > maxw) maxw = length(del_sum "")

        sep_num  = sprintf("%" maxw "s", "")
        sep_file = sprintf("%" filew "s", "")
        gsub(/ /, "-", sep_num)
        gsub(/ /, "-", sep_file)

        printf magenta bold "%" maxw "s  %" maxw "s  %-" filew "s\n" reset, "ADD", "DEL", "FILE"
        printf magenta "%s  %s  %s\n" reset, sep_num, sep_num, sep_file

        for (i = 1; i <= n; i++) {
          file = files[i]
          dir = ""
          base = file

          if (match(file, /.*\//)) {
            dir = substr(file, 1, RLENGTH)
            base = substr(file, RLENGTH + 1)
          }

          printf green "%" maxw "s" reset "  " red "%" maxw "s" reset "  %s" blue "%s" reset "\n",
                 adds[i], dels[i], dir, base
        }

        if (n > 1) {
          printf magenta "%s  %s  %s\n" reset, sep_num, sep_num, sep_file
          printf green "%" maxw "d" reset "  " red "%" maxw "d" reset "  %s\n", add_sum, del_sum, "TOTAL"
        }
      }
    ' "$tmp_view"
  fi

  local rc=$?
  rm -f "$tmp_raw" "$tmp_view" "${tmp_view}.top" 2>/dev/null
  return $rc
}

~/.zshrc 中添加:

sh
[[ -f ~/.gdc.sh ]] && source ~/.gdc.sh

或者实时更新:

sh
source ~/.zshrc

用法:

sh
gdc -h
sh
Usage:
  gdc [options] [<rev-or-range>]

Modes:
  -c, --cached        Show staged changes only
  -s, --staged        Same as --cached
  -w, --worktree      Show unstaged changes + untracked files
      (default)       Show diff vs HEAD (staged + unstaged) + untracked files

Display options:
  -n, --name-only     Show only file names
  -S, --sort KEY      Sort by: add | del | file
  -t, --top N         Show only the first N rows after sorting/current order
  -h, --help          Show this help

Examples:
  gdc
  gdc -c
  gdc -w
  gdc HEAD~1
  gdc HEAD~3..HEAD
  gdc -S add
  gdc -S del -t 20
  gdc -n -S file