0%

Git 安装使用

指令

Git Bash 使用 ssh 登陆

配置 Git Bash:

1
2
3
4
5
git config --global user.name <name>
git config --global user.email <email>
# 如果开了 Github 的 `Keep my email addresses private` 功能,
# 设置 email 时 要填 Github 生成的 email, 而不是注册 email.
# 还有一些其他可选设置

以下是我的一些配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
$ git config --list
core.symlinks=false
core.autocrlf=true
core.fscache=true
color.diff=auto
color.status=auto
color.branch=auto
color.interactive=true
help.format=html
rebase.autosquash=true
http.sslbackend=openssl
http.sslcainfo=C:/Program Files/Git/mingw64/ssl/certs/ca-bundle.crt
credential.helper=manager
core.editor="C:\\Program Files\\Microsoft VS Code\\Code.exe" --wait
diff.astextplain.textconv=astextplain
filter.lfs.clean=git-lfs clean -- %f
filter.lfs.smudge=git-lfs smudge -- %f
filter.lfs.process=git-lfs filter-process
filter.lfs.required=true
filter.lfs.clean=git-lfs clean -- %f
filter.lfs.smudge=git-lfs smudge -- %f
filter.lfs.process=git-lfs filter-process
filter.lfs.required=true
user.name=username
user.email=fakenameg@users.noreply.github.com
core.quotepath=true

配置 ssh:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 查看版本 (我的版本比较新, 老版本的指令略有不同)
$ ssh -V
OpenSSH_8.0p1, OpenSSL 1.1.1c 28 May 2019

# 新开一个 ssh 代理
$ ssh-agent -s
SSH_AUTH_SOCK=/tmp/ssh-zBSkGOjqZpYl/agent.1678; export SSH_AUTH_SOCK;
SSH_AGENT_PID=1679; export SSH_AGENT_PID;
echo Agent pid 1679;

# 确定一下这个 ssh 还没有密钥
$ ssh-add -l -E md5
The agent has no identities.

# 整一个新的密钥, 一路回车确定 (默认配置)
$ ssh-keygen -t rsa -b 4096 -C "随便一串字符"

# 新密钥的私钥和公钥储存位置 (id_rsa 和 id_rsa.pub)
$ ls ~/.ssh/
id_rsa id_rsa.pub known_hosts

# 添加到 ssh-agent
$ ssh-add ~/.ssh/id_rsa
Identity added: /c/Users/xie/.ssh/id_rsa (随便一串字符)

# 再次查看 ssh-agent 的密钥, 已经成功添加
$ ssh-add -l -E md5
4096 MD5:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx 随便一串字符 (RSA)

然后登陆 Github, 复制公钥:

1
clip < ~/.ssh/id_rsa.pub

登陆后 https://github.com/settings/keys -> New SSH Key, 'Title' 随意, 在 'Key' 粘贴.

然后测试一下连接 (初次使用以下指令会多一段对话(如下), 输入 "yes"):

1
2
3
4
5
6
$ ssh -T git@github.com
The authenticity of host 'github.com (13.250.177.223)' can't be established.
RSA key fingerprint is SHA256:aF9zXis7VUy4CsdfSltbl6ZCksa34ChdVfA54S3xcA2.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added 'github.com,13.250.177.223' (RSA) to the list of known hosts.
Hi XieNaoban! You've successfully authenticated, but GitHub does not provide shell access.

提交三部曲

直接偷懒把当前时间作为本次提交版本的 "提交说明", 每次都要思考 "提交说明" 怎么写真是太累了 (真实项目别这么干).

Bash:

1
2
3
4
5
6
# 获取当前时间作为 "提交说明"
readonly date=$(date +%Y-%m-%d\ %H:%M:%S)

git add . # 添加当前目录所有文件到暂存区
git commit -m "${date}" # 提交到本地仓库, 以时间作为 "提交说明"
git push origin master # origin 表示远程主机, master 为分支名称

PowerShell:

1
2
3
4
5
$date = Get-Date -Format 'yyyyMMdd-HH:mm:ss'

git add .
git commit -m "${date}"
git push origin master

分支操作

1
2
3
4
5
6
7
8
9
10
11
12
git branch                  # 查看分支

git branch <branch> # 创建分支
git checkout <branch> # 切换分支

git checkout -b <branch> # 创建并切换分支

# 推送到远程分支: 远程还没有该分支
git push origin <local_branch>:<remote_branch>

# 删除远程分支
git push origin --delete <remote_branch>

找不同

1
2
3
git status      # 找出不同的文件、文件夹
git status -s # 找不同, 显示很精简规整, 就很舒服
git diff # 找出不同的每一行

分支合并

1
2
3
git checkout <branch_1>     # 转到分支1
git merge <branch_2> # 合并分支2
git push origin <branch_1> # 推送到远程分支

从远程更新到本地

1
2
3
4
5
6
7
8
9
10
11
12
git pull <host> <remote_branch>:<local_branch>
git pull <host> <remote_branch> # 与当前本地分支合并
git pull # 简写, 合并本分支

# git pull origin tmp 相当于:
git fetch origin
git merge origin/tmp

# git pull 更快, 但 fetch 再 merge 可以方便查看 diff
git fetch origin master:tmp
git diff tmp
git merge tmp

撤销

1
2
3
4
5
6
7
# 撤销未 git add 的文件的修改
git checkout . # 全部撤销修改
git checkout <file> # 撤销文件修改
git reset --hard # 效果与 checkout 一样

# 撤销 git add 操作 (不修改文件本身)
git reset HEAD

踩坑

Git Bash 中文乱码

例如对于 git status 指令, 中文文件 "新建文本文档.txt" 会被显示为

1
\346\226\260\345\273\272\346\226\207\346\234\254\346\226\207\346\241\243.txt

解决方法如下:

然后

1
2
# 解决 Windows Git Bash、Linux 下的中文转码问题
git config --global core.quotepath false

Powershell 中文乱码

例如对于 git log 指令, 中文的 Commit Message 会被显示为:

1
<E8><BF><87><E6><BB><A4><E5><8A><9F><E8><83><BD><E5><9F><BA><E6><9C><AC><E5><AE><8C><E6><88><90>

解决方法如下, 在 Powershell 输入:

1
2
3
4
5
git config --global core.quotepath false
git config --global gui.encoding utf-8
git config --global i18n.commit.encoding utf-8
git config --global i18n.logoutputencoding utf-8
$env:LESSCHARSET='utf-8' #该行可以不输入, 但可能之后要重启 Powershell

然后设置系统环境变量, 设置 LESSCHARSETutf-8.

Git Bash 命令提示符的 $ 总是另起一行

命令前的那一串叫 "系统终端命令提示符 (Prompt Sign)" (就是那一串包含用户名, 工作目录和 $ 符号的东西). Windows 下的 Git Bash 的命令提示符的 $ 总是另起一行.

这玩意由 $PS1 控制. 输入 echo "$PS1", 可得:

1
2
3
4
5
# 我的PS1被 conda 修改过, 所以前面有个 "(base)"
(base)
xie@DESKTOP-BAD47V8 MINGW64 ~
$ echo "$PS1"
(base) \[\033]0;$TITLEPREFIX:$PWD\007\]\n\[\033[32m\]\u@\h \[\033[35m\]$MSYSTEM \[\033[33m\]\w\[\033[36m\]`__git_ps1`\[\033[0m\]\n$

修改它就能修改提示符样式.

而 Ubuntu 下的 $PS1 为:

1
2
xie@xie-vm:~$ echo "$PS1"
\[\e]0;\u@\h: \w\a\]${debian_chroot:+($debian_chroot)}\[\033[01;32m\]\u@\h\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$

那么依葫芦画瓢改一改. 用 vim 或 code (vscode) 命令打开 ~/.bash_profile, 添加一行:

1
export PS1="\[\e]0;\u@\h: \w\a\]${debian_chroot:+($debian_chroot)}\[\033[32m\]\u@\h\[\033[00m\]:\[\033[33m\]\w\[\033[00m\]\$ "

保存重启 Git Bash即可.

使用 ssh 执行 git clone 报错

1
2
3
4
5
6
7
$ git clone git@github.com:xxx/xxx.git
Cloning into 'xxx'...
git@github.com: Permission denied (publickey).
fatal: Could not read from remote repository.

Please make sure you have the correct access rights
and the repository exists.

此时如果测试连接, 却显示正常:

1
2
$ ssh -T git@github.com
Hi XieNaoban! You've successfully authenticated, but GitHub does not provide shell access.

网上的解决方案大多是说 ssh 配置出了问题. 但我这里不是这个原因. 原因是勾选了 https://github.com/settings/emails -> Keep my email addresses private, 好像是我的隐私邮箱被系统更换了, 所以就挂了. 解决方案同下面的 Push 时报错 push declined due to email privacy restrictions

Push 时报错 push declined due to email privacy restrictions

从某天开始突然不能 push 了, 错误信息:

1
! [remote rejected] master -> master (push declined due to email privacy restrictions)

原因是勾选了 https://github.com/settings/emails -> Keep my email addresses private, 可能是因为它重置了隐私邮箱吧.

复制 Keep my email addresses private 下给的邮箱 (数字+名字@users.noreply.github.com),

1
2
git config --global user.email 12345678+XieNaoban@users.noreply.github.com
git commit --amend --reset-author # 会打开文本编辑器, 直接关闭即可

就好了.

Windows 下 Clone 的文件以 LF 为换行符而非 CRLF

设置:

1
2
3
git config --global core.autocrlf input # 提交时转换为 LF, 签出时不转换
# 或
git config --global core.autocrlf false # 提交签出均不转换

以下载 LF 的版本. 折腾完了改回去 (毕竟 Win 下还是默认开着好):

1
git config --global core.autocrlf true # 提交时转换为 LF, 签出时转换为 CRLF

解决方案: 直接干脆把文件统一成 LF, 不要转来转去了, 反正在 Win 下用 LF 大多数软件也支持.

Win 下 warning: LF will be replaced by CRLF

Windows 以 CRLF 为换行符, Linux 以 LF 为换行符, Git 仓库以 LF 为换行符. 从 Win 上 git clone 时, Git 会自动把仓库里所有的文件从 LF 转为 CRLF. 当提交时, 又会把 CRLF 转回 LF. 如果提交文件是纯 LF 文件, 会显示一个警告: warning: LF will be replaced by CRLF in xxx..

直接关了这个 warning:

1
git config --global core.safecrlf false # 默认是 'warn'

git resetgit revert 区别

git reset 回滚到某 commit, 后面的 commmit 都抛弃了. 而 git revert 创建一个新 commit, 中和之前的提交.

git resetgit push 会报错, 提示你当前本地版本落后于远程版本. 需要强制推送 (git push -f).

保留空文件夹

在提交时, 空文件夹 (或者文件夹内所有文件均被gitignore忽略) 不会被提交, 所以 git clone 时将没有那个文件夹. 如果希望提交时保留空文件夹, 可以在该文件夹下新建一个空白文件, 并取名 .gitkeep... 原理也很简单, 有了这个文件, 该文件夹就不是空的了... 所以其实取什么名字都行, 只不过 .gitkeep 比较约定俗成, 比如 IntelliJ 的文件目录视图貌似会自动忽略这个文件.

删除历史中的所有大文件

年少无知的我没写 .gitignore, 把一堆巨大的 json 文件一并 add 到了历史里, 期间这些 json 还改动过好几次, 于是乎我的 .git 文件夹就理所当然地膨胀到了 100MB... 现在我很后悔, 发现这些 json 从一开始就不应该 add, 于是想把它们统统从历史中删除.

参考了不少博客, Github 官方也有文档: Removing sensitive data from a repository - GitHub Help.

先看一下Git历史数据占地面积:

1
git count-objects -v

得到:

1
2
3
4
5
6
7
8
count: 119
size: 43363
in-pack: 999
packs: 1
size-pack: 30787
prune-packable: 0
garbage: 0
size-garbage: 0

然后找出所有大文件, 用的大佬写好的 bash 脚本 (findBigFile.sh):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#!/bin/bash
#set -x

# Shows you the largest objects in your repo's pack file.
# Written for osx.
#
# @see http://stubbisms.wordpress.com/2009/07/10/git-script-to-show-largest-pack-objects-and-trim-your-waist-line/
# @author Antony Stubbs

# set the internal field spereator to line break, so that we can iterate easily over the verify-pack output
IFS=$'\n';

# list all objects including their size, sort by size, take top 10
objects=`git verify-pack -v .git/objects/pack/pack-*.idx | grep -v chain | sort -k3nr | head`

echo "All sizes are in kB. The pack column is the size of the object, compressed, inside the pack file."

output="size,pack,SHA,location"
for y in $objects
do
# extract the size in bytes
size=$((`echo $y | cut -f 5 -d ' '`/1024))
# extract the compressed size in bytes
compressedSize=$((`echo $y | cut -f 6 -d ' '`/1024))
# extract the SHA
sha=`echo $y | cut -f 1 -d ' '`
# find the objects location in the repository tree
other=`git rev-list --all --objects | grep $sha`
#lineBreak=`echo -e "\n"`
output="${output}\n${size},${compressedSize},${other}"
done

echo -e $output | column -t -s ', '

脚本运行结果大致如下:

1
2
3
4
5
6
size   pack  SHA                                       location
60832 2900 a8b382af350184d5ead001b1e91c66cea62c54b7 path/to/your/file1
33288 4034 e7bba187e68f752e88664c49b95e5ba18e022dca path/to/your/file2
13804 572 c0c7383de6e9b28e97822a896199b11174ade578 path/to/your/file3
13751 571 2609b4429eb13a2a9885a27073b608e8bdecb3d0 path/to/your/file4
...

默认展示 10 条, 如果想要展示 233 条更多只要把脚本里的 head 改为 head -233 即可. 核心就是 objects 那句, 大佬注释写的挺完整, 在此不解释了.

然后就是最核心的删除语句:

1
git filter-branch --force --index-filter "git rm --cached --ignore-unmatch path/to/your/file" --prune-empty --tag-name-filter cat -- --all

这行指令原理应该是遍历你的所有 commit, 如果该 commit 有这个目录有这个文件, 就删除. 运行结果如下 (运行速度挺慢的):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
WARNING: git-filter-branch has a glut of gotchas generating mangled history
rewrites. Hit Ctrl-C before proceeding to abort, then use an
alternative filtering tool such as 'git filter-repo'
(https://github.com/newren/git-filter-repo/) instead. See the
filter-branch manual page for more details; to squelch this warning,
set FILTER_BRANCH_SQUELCH_WARNING=1.
Proceeding with filter-branch...

Rewrite a361c1d183b59a5df22bc2b7d38eeadf45255675 (73/78) (44 seconds passed, remaining 3 predicted) rm 'path/to/your/file'
Rewrite 434e4dd7e434e2516dabf8987ab26ce0c45d2dc5 (75/78) (45 seconds passed, remaining 1 predicted) rm 'path/to/your/file'
Rewrite 50ce64408ebf9f9e96d2aa407c8f5b752460b66a (75/78) (45 seconds passed, remaining 1 predicted) rm 'path/to/your/file'
Rewrite 820ae95f38eac7e60eeeae40fe6d624c1c61a05a (77/78) (47 seconds passed, remaining 0 predicted) rm 'path/to/your/file'
Rewrite 1e2781e656495a06e325641e59eed36fd0df1f40 (77/78) (47 seconds passed, remaining 0 predicted)

Ref 'refs/heads/master' was rewritten
Ref 'refs/remotes/origin/master' was rewritten
WARNING: Ref 'refs/remotes/origin/WS' is unchanged
WARNING: Ref 'refs/remotes/origin/master' is unchanged
Ref 'refs/remotes/origin/xie' was rewritten

然后强制 push:

1
git push origin --force --all

有些教程到这就结束了, 有些还有后续 (清理、压缩 .git 等), 后续各不相同, 我综合整理了个自己的版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
rm -rf .git/refs/original/
# 虽然物理上文件被删了, 但逻辑上可能还有没更新的引用, 删了以下的文件让 git 重新生成

git reflog expire --expire=now --all
# 管理所有的 reflog 信息, expire 表示修剪旧的 reflog 条目.
# --expire=now 表示修剪早于当前时间的条目;
# --all 表示处理所有引用的 reflog.
# 刚刚删掉的内容貌似就是在此重新生成 (应该吧).

git fsck --full --unreachable
# 验证数据库中对象的连接性和有效性.
# --full 啥都检查;
# --unreachable 如果有节点存在但不可达, 则输出.
# 这条指令貌似只是检测, 不修改什么东西.

git repack -A -d
# 把没打包的对象打包并删除冗余对象.
# -A 把引用的所有内容打到一个包里, -a 和 -A 好像是一个意思;
# -d 删除冗余包.

git gc --aggressive --prune=now
# 清理不必要的文件, 优化本地存储.
# --aggressive 更好地优化存储, 但会花费更多时间;
# --prune=now 修剪比当前时间更早的松散物体

其中 git repack -A -dgit gc --aggressive --prune=now 输出的 log 几乎一样, 可能只执行其中一个就行?

整理了个自动删除大文件的脚本 (事先找好要删的文件放在 rm_list 里):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/bin/bash
rm_list=('path/file1', 'path/file2', 'path/file3')
for f in ${rm_list[@]}
do
echo '---'
echo "removing '${f}'..."
git filter-branch --force --index-filter "git rm --cached --ignore-unmatch ${f}" --prune-empty --tag-name-filter cat -- --all
done

# clean up and reclaim space
rm -rf .git/refs/original/
git reflog expire --expire=now --all
git fsck --full --unreachable
git repack -A -d
git gc --aggressive --prune=now

# force-push changes to overwrite remote repository
git push origin --force --all

删了 30 多个 json, 最后把 100MB 的 .git 清理到了 10MB (捂脸). 删除过程贼慢, 最后 push 时 "Writing objects" 也贼慢.

.gitignore 对已 track 文件不起作用

当一个文件已经被 commit 后才被发现这个文件是应该 ignore 的, 此时再把它添加入 .gitignore 的话, git 依然会追踪它. 修改该文件, 然后你会发现 git 仍然记录了这个文件的修改. 原因是 .gitignore 仅对新文件生效, 已被 track 的文件不受影响. 解决方法:

  1. 强制清除对所有文件的追踪 (只清除不想追踪的文件应该也行, 没试)
    1
    git rm -r -f --cache .
  2. 更新你的 .gitignore (或者在上述指令之前更新也没关系)
  3. 重新 addcommit.

给 Git SSH 上 Socks5 代理

如果是使用 http/https 给 git 上代理, 那方法很多, 比如给终端上代理或者直接给 git 设置代理:

1
2
3
4
5
git config --global http.proxy socks5://127.0.0.1:10808
git config --global https.proxy socks5://127.0.0.1:10808
使用完后:
git config --global --unset http.proxy
git config --global --unset https.proxy

但是我现在想用 ssh 进行 git clone (git clone git@github.com:xxx/xxx.git), 针对 http/https 的代理对 ssh 是无效的. 所以要通过配置 ssh 自身来实现代理.

方法是打开 ~/.ssh/config (Win下就是 个人文件夹\.ssh\config), 如果没有就新建这个文件.

然后写入一行代理指令, Win 和 Linux 指令不同:

1
2
3
4
5
6
# Linux (如果无 nc 请先安装. 但是 Win 下压根没有 nc)
# -X: 指定协议, 5 就是 Socks5 (不写该参数默认也是 Socks5); -x 指定 address:port
ProxyCommand nc -X 5 -x 127.0.0.1:10808 %h %p

# Win (使用 Git 在 Win 下自己实现的工具)
ProxyCommand "C:\Program Files\Git\mingw64\bin\connect.exe" -S 127.0.0.1:10808 %h %p

以上指令效果是全局的, 局部的稍微麻烦一点, 要在指令里指定主机和用户, 懒得弄了.

当然, 直接在控制台像这样设置也行:

1
2
3
4
5
# -o 即 option 的意思
# 格式有两种, 语法略有不同而已, 本质一样的
ssh -o ProxyCommand="nc -X 5 -x 127.0.0.1:10808 %h %p"
ssh -o "ProxyCommand nc -X 5 -x 127.0.0.1:10808 %h %p"
# 以上是 Linux 的指令, Win 还是改成上面对应的指令

不过我对ssh 不太熟, 也没去试, 暂不知道 -o 是永久的还是仅限当前会话.