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 输出

写入~/.gd.sh
sh
gd() {
    emulate -L zsh
    setopt local_options pipe_fail no_aliases

    command git rev-parse --is-inside-work-tree >/dev/null 2>&1 || {
        printf '%s\n' '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=""
    local tmp_raw=""
    local tmp_view=""
    local tmp_top=""
    local rc=0

    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 ]] || {
                    printf '%s\n' 'gd: missing value for --sort'
                    return 1
                }
                case "$1" in
                    add|del|file)
                        sort_key="$1"
                        ;;
                    *)
                        printf 'gd: invalid sort key: %s (expected: add|del|file)\n' "$1"
                        return 1
                        ;;
                esac
                shift
                ;;
            -t|--top)
                shift
                [[ $# -gt 0 ]] || {
                    printf '%s\n' 'gd: missing value for --top'
                    return 1
                }
                [[ "$1" == <-> ]] || {
                    printf '%s\n' 'gd: --top expects a positive integer'
                    return 1
                }
                top_n="$1"
                shift
                ;;
            -h|--help)
                cat <<'EOH'
Usage:
  gd [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:
  gd
  gd -c
  gd -w
  gd 'HEAD~1'
  gd 'HEAD~3..HEAD'
  gd -S add
  gd -S del -t 20
  gd -n -S file
EOH
                return 0
                ;;
            --)
                shift
                break
                ;;
            -*)
                printf 'gd: unknown option: %s\n' "$1"
                return 1
                ;;
            *)
                if [[ -z "$revspec" ]]; then
                    revspec="$1"
                    mode="revspec"
                    show_untracked=1
                    shift
                else
                    printf 'gd: unexpected extra argument: %s\n' "$1"
                    return 1
                fi
                ;;
        esac
    done

    if [[ $# -gt 0 ]]; then
        printf 'gd: unexpected extra argument: %s\n' "$1"
        return 1
    fi

    tmp_raw="$(command mktemp)" || return 1
    tmp_view="$(command mktemp)" || {
        command rm -f -- "$tmp_raw"
        return 1
    }
    tmp_top="${tmp_view}.top"

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

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

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

    if (( rc == 0 )) && [[ -n "$top_n" ]]; then
        command head -n "$top_n" -- "$tmp_view" > "$tmp_top"
        rc=$?
        if (( rc == 0 )); then
            command mv -- "$tmp_top" "$tmp_view"
            rc=$?
        fi
    fi

    if (( rc == 0 )); then
        if [[ "$name_only" -eq 1 ]]; then
            command awk -F '\t' '
                BEGIN {
                    gray  = "\033[90m"
                    green = "\033[32m"
                    red   = "\033[31m"
                    blue  = "\033[34m"
                    reset = "\033[0m"
                }

                function is_positive_num(value) {
                    return value ~ /^[0-9]+$/ && value > 0
                }

                function file_color(adds, dels, add_present, del_present) {
                    add_present = is_positive_num(adds)
                    del_present = is_positive_num(dels)

                    if (add_present && del_present) {
                        return blue
                    }
                    if (add_present) {
                        return green
                    }
                    if (del_present) {
                        return red
                    }
                    return blue
                }

                NF >= 3 && $3 != "" {
                    file = $3
                    dir = ""
                    base = file

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

                    printf "%s%s%s%s%s\n", gray, dir, reset, file_color($1, $2), base reset
                }
            ' "$tmp_view"
            rc=$?
        else
            command awk -F '\t' '
                BEGIN {
                    gray    = "\033[90m"
                    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
                    add_files = 0
                    del_files = 0
                    change_files = 0
                }

                function is_positive_num(value) {
                    return value ~ /^[0-9]+$/ && value > 0
                }

                function display_count(value) {
                    return value == 0 ? "" : value
                }

                function append_summary(summary, text) {
                    return summary == "" ? text : summary ", " text
                }

                function file_color(adds, dels, add_present, del_present) {
                    add_present = is_positive_num(adds)
                    del_present = is_positive_num(dels)

                    if (add_present && del_present) {
                        return blue
                    }
                    if (add_present) {
                        return green
                    }
                    if (del_present) {
                        return red
                    }
                    return blue
                }

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

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

                    add_present = is_positive_num($1)
                    del_present = is_positive_num($2)

                    if (add_present && del_present) {
                        change_files += 1
                    } else if (add_present) {
                        add_files += 1
                    } else if (del_present) {
                        del_files += 1
                    }

                    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 {
                    add_sum_display = display_count(add_sum)
                    del_sum_display = display_count(del_sum)

                    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 "%s  %s  %s\n" reset, sep_num, sep_num, 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)
                        }

                        add_display = display_count(adds[i])
                        del_display = display_count(dels[i])
                        color = file_color(adds[i], dels[i])

                        printf green "%" maxw "s" reset "  " red "%" maxw "s" reset "  %s%s%s%s%s\n", \
                               add_display, del_display, gray, dir, reset, color, base reset
                    }

                    printf magenta "%s  %s  %s\n" reset, sep_num, sep_num, sep_file

                    summary = ""
                    if (add_files > 0) {
                        summary = append_summary(summary, green sprintf("add %d files", add_files) reset)
                    }
                    if (del_files > 0) {
                        summary = append_summary(summary, red sprintf("del %d files", del_files) reset)
                    }
                    if (change_files > 0) {
                        summary = append_summary(summary, blue sprintf("change %d files", change_files) reset)
                    }

                    total_label = summary == "" ? "TOTAL" : "TOTAL (" summary ")"
                    printf green "%" maxw "s" reset "  " red "%" maxw "s" reset "  %s\n", add_sum_display, del_sum_display, total_label
                    printf magenta "%s  %s  %s\n" reset, sep_num, sep_num, sep_file
                }
            ' "$tmp_view"
            rc=$?
        fi
    fi

    command rm -f -- "$tmp_raw" "$tmp_view" "$tmp_top" 2>/dev/null
    return $rc
}

~/.zshrc 中添加:

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

或者实时更新:

sh
source ~/.zshrc

用法:

sh
gd -h
sh
Usage:
  gd [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:
  gd
  gd -c
  gd -w
  gd 'HEAD~1'
  gd 'HEAD~3..HEAD'
  gd -S add
  gd -S del -t 20
  gd -n -S file