目录

Git 的一些使用技巧

Git 是一个非常强大的版本控制工具,是程序员必须要掌握的技能。

这里记录常用的命令技巧和碰到的一些问题。

首先应该了解 Git 里面的几个概念。

/images/git-use/git-use.png

  • Workspace:工作区
  • Index/Stage:暂存区(使用 git add 命令将工作区的改动添加到暂存区)
  • Repository:本地仓库(使用 git commit 命令将暂存区的改动提交到本地仓库)
  • HEAD:指向本地仓库的当前版本,上一个版本就是 HEAD^,上上一个版本就是 HEAD^^,往上 10 个版本,可以 写成 HEAD~10
  • Remote:远程仓库

克隆仓库

git clone git@github.com:shipengqi/shipengqi.github.io.git <target dir> -b <branch>

shipengqi.github.io.git 克隆到 target dir 指定的文件夹(默认是远程仓库的名字),并切换到指定 分支 branch(默认是 master 分支)。

未暂存的内容

把未暂存的内容添加到暂存区

# 提交所有改动
git add -A

# 提交被修改 (modified) 和被删除 (deleted) 文件,不包括新文件 (new)
git add -u

# 提交新文件 (new) 和被修改 (modified) 文件,不包括被删除 (deleted) 文件
git add .

# 添加指定文件到暂存区
git add [file1] [file2] ...

# 添加指定目录到暂存区,包括子目录
git add [dir]

# 添加每个变化前,都会要求确认
# 对于同一个文件的多处变化,可以实现分次提交
git add -p

把未暂存的内容移动到一个新分支

git checkout -b new-branch

把未暂存的内容移动到另一个已存在的分支

git stash
git checkout my-branch
git stash pop

放弃未暂存的修改

git checkout <file-name>

# 放弃所有修改
git checkout .

暂存的内容

把暂存的内容添加到上一次的提交

git commit --amend

取消暂存的内容

添加到暂存区的文件,但是还没有提交,如果想要撤销暂存的文件,可以使用 git reset HEAD <file1> <file2>... 的方式取消暂存。

git reset HEAD file2
# 或者
git restore --staged <file>...

# 同时删除工作区和暂存区中的文件
git rm [file1] [file2] ...

# 从暂存区删除文件, 但工作区不删除
git rm --cached [file]

这样 file2 文件又回到了之前已修改未暂存的状态。

编辑提交

修改提交信息

# 打开默认编辑器
git commit --amend --only

# 或者
git commit --amend --only -m 'xxxxxxx'

如果已经 push 了这次提交, 那么可以修改这次提交(commit)然后强推(force push), 但是不推荐。

编辑指定 commit 的提交信息

编辑指定 commit 的提交信息,可以先使用 rebase 来修改某一次的提交信息

如果当前在 main 分支:

git rebase -i main^^

# 使用 commit 生成的哈希值来定位
git rebase -i aa588cd
git rebase -i aa588cd72b95ab35ec6e15637fb1e110281b2200

main^^ 表示当前 main 指向的 commit 之前倒数第 2 个 commit。main~2 也是一样的意思。^~{count} 都是表示把 commit 往回偏移。

执行上面的命令之后,会进入如下的编辑界面:

pick aa588cd display error
pick e10dddf regex draft

# Rebase aa588cd..e10dddf onto aa588cd (2 commands)
#
# Commands:
# p, pick = use commit
# r, reword = use commit, but edit the commit message
# e, edit = use commit, but stop for amending
# s, squash = use commit, but meld into previous commit
# f, fixup = like "squash", but discard this commit's log message
# x, exec = run command (the rest of the line) using shell
# d, drop = remove commit
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
# Note that empty commits are commented out

找到想要修改的 commit,将 pick 改为 edit,然后 wq 保存退出,接着再运行:

git commit --amend -m 'changed commit mesasge'

修改提交里的用户名和邮箱

git commit --amend --author "New Authorname <work@example.com>"

改完信息后,还需要 git rebase --continue,将基准从当前倒数第二位置移到最新一次提交。git log 去检查下状态。

commit 添加签名

git commit -m "xx" -s

如果你配置了 username 和 email,参数 -s 会自动在 commit 的信息中添加签名,如下:

commit d52618492b788425b7c86d25c5e37eb67fd8fba6
Author: shipengqi <xxx@gmail.com>
Date:   Mon May 2 18:36:11 2022 +0800

    regex draft
    
    Signed-off-by: shipengqi <xxx@gmail.com>

从一个提交(commit)里移除一个文件

从一个提交(commit)里移除一个文件:

git checkout HEAD^ myfile
git add -A
git commit --amend

当你有一个开放的补丁(open patch),你往上面提交了一个不必要的文件,需要强推(force push)去更新这个远程补丁。

撤销某一次提交

修改已经提交到了当前分支,但是还没有 push 到远程仓库

git reset --hard HEAD^

# 回退到指定的版本
git reset --hard <commit id>

# 只撤销提交,但不改变代码
git reset HEAD^
git reset commit-id

已经 push 到远程仓库的提交

已经 push 到远程仓库的提交,使用 git revert,回滚到指定的历史版本,再 git push 更新远程仓库。

# 撤销某个 commit 版本
git revert commit-id

对于已经 push 到远程仓库的提交,也可以使用 reset,然后执行 git push -f 强制推到远程仓库中去,但是可能导致冲突。 如果只是回退到上一个 commit,建议使用 revert,如果要回退到多个版本之前,还是要使用 reset。

revert 与 reset 的区别

  • reset 是在正常的 commit 历史中,删除了指定的 commit,这时 HEAD 是向后移动了,而 revert 是在正常的 commit 历史中再 commit 一 次,HEAD 是一直向前的。
  • 对于已经把代码已经 push 到远程仓库,reset 删除指定 commit 以后,push 可能导致一大堆冲突.但是 revert 不会。
  • revert 是撤销指定的某个 commit 版本,但是指定 commit 之后的版本,还会保留下来。reset 是将 HEAD 移动到了指定的 commit, 指定 commit 之后的版本都会被丢弃。

如果想恢复到之前某个提交的版本,且那个版本之后提交的版本都不要了,就用 reset。 如果想撤销之前的某一版本,但是又想保留该目标版本后面的版本,就用 revert。

revert Merge Commit

执行 git revert commitId 可能会报错:

error: commit xxxxxxxxxxx is a merge but no -m option was given.

这是因为指定的 commit 是一次 merge,需要 -m 参数指定要 revert 的这个 merge commit 中的哪一个。 比如:git revert HEAD~1 -m 1 会 revert 第一个 commit。

你也可以在 git log 找到你要 revert 的那个 commit。

意外的做了一次硬重置(hard reset),如何找回内容

当你用 git reset --hard HEAD^ 回退到上个版本时,再想恢复,就必须找到要恢复版本的 commit id。可以通过 git reflog 找到 那次 commit。

选择你想要回到的提交(commit)的 commit id,再重置一次:

git reset --hard SHA1234

提交错分支怎么办

还没有提交代码

比如忘了创建分支,并且 master 的代码可能还不是最新的,但是已经直接在 master 分支上进行了修改。这种情况下可以先把代码暂存起来,然后把 master 分 支更新到最新,再创建并切换到新的分支,然后把暂存的代码恢复回来。

# 暂存代码
git stash
# 更新 master
git fetch
git merge origin/master
# 创建新的分支并切换过去
git checkout -b <name>
# 把暂存的代码恢复回来
git stash pop

然后就可以直接 commit 了。

已经提交

代码提交了,还没有 push,这个时候可以先把它撤回来

git reset HEAD^

这样就把上一次的提交恢复为未提交的状态了,如果确定当前所在的 master 分支代码已经是最新的,就可以直接 checkout 到新的分支,来进行提交。否则,就就参考第一种情况。

查看 commit 历史

# 显示当前分支的版本历史
git log

# 显示 commit 历史,以及每次 commit 发生变更的文件
git log --stat

# 搜索提交历史,根据关键词
git log -S [keyword]

# 显示某个 commit 之后的所有变动,每个 commit 占据一行
git log [tag] HEAD --pretty=format:%s

# 显示某个 commit 之后的所有变动,其"提交说明"必须符合搜索条件
git log [tag] HEAD --grep feature

# 显示某个文件的版本历史,包括文件改名
git log --follow [file]
git whatchanged [file]

# 显示指定文件相关的每一次 diff
git log -p [file]

# 显示过去 5 次提交
git log -5 --pretty --oneline

# 显示所有提交过的用户,按提交次数排序
git shortlog -sn

# 显示指定文件是什么人在什么时间修改过
git blame [file]

# 查看命令历史
git reflog

回退前,用 git log 可以查看提交历史,以便确定要回退到哪个版本。 回退后,用 git reflog 查看命令历史,以便确定要回到未来的哪个版本。

查看某次提交

# 显示某次提交的元数据和内容变化
git show [commit]

# 显示某次提交发生变化的文件
git show --name-only [commit]

# 显示某次提交时,某个文件的内容
git show [commit]:[filename]

Stash

stashadd 的区别: git stash 的作用是把工作区(必须是工作区中已经被 git 追踪到的文件)和暂存区中的内容暂时存到一个栈上。而且这个堆是和分支不 相关的。切换分支后,依然可以看到并使用。

git add 命令将修改添加到暂存区。

暂存工作目录下的所有改动

git stash

暂存所有改动,包括 untracked 的文件(新建的文件)

git stash -u

暂存指定文件

# 暂存某一个文件
git stash push working-directory-path/filename.ext

# 暂存多个文件
git stash push working-directory-path/filename1.ext working-directory-path/filename2.ext

暂存时记录消息

git stash save <message>

# 或
git stash push -m <message>

使用某个指定暂存

# 查看 stash 记录
git stash list

# apply 某个 stash
git stash apply "stash@{n}"

n 是 stash 在栈中的位置,最上层的 stash 会是 0

使用最后一个 stash 的状态,并删除这个 stash

git stash pop

删除所有的 stash

git stash clear

仅从 stash 中拿出某个文件的修改

git checkout <stash@{n}> -- <file-path>

比较差异

# 显示暂存区和工作区的差异
git diff

# 显示本地仓库中任意两个 commit 之间的文件变动
git diff <commit-id> <commit-id>

# 显示暂存区和最近的 commit 的不同
git diff --cached

# 显示工作区与当前分支最新 commit 之间的差异
git diff HEAD

分支

# 切换分支
git checkout dev

# 切换并新建一个分支
git checkout -b newBranch

# 删除一个分支
git branch -d branch

# 列出所有本地分支
git branch

# 列出所有远程分支
git branch -r

# 列出所有本地分支和远程分支
git branch -a

# 删除远程分支
git push origin --delete [branch-name]
git branch -dr [remote/branch]

# 合并指定分支到当前分支
git merge [branch]

# 选择一个 commit,合并进当前分支
git cherry-pick [commit]

# 关联远程分支
git branch -u origin/mybranch
# 或者在 push 时加上 -u 参数
git push origin/mybranch -u

# 重命名本地分支
git branch -m <new-branch-name>

需要提交到一个新分支,但错误的提交到了 master

在 master 下创建一个新分支,不切换到新分支,仍在 master 下:

(master)$ git branch my-branch

把 master 分支重置到前一个提交:

(master)$ git reset --hard HEAD^

checkout 到刚才新建的分支继续工作:

(master)$ git checkout my-branch

从错误的分支拉取了内容,或把内容拉取到了错误的分支

使用 git reflog 找到在这次 pull 之前 HEAD 的指向。

(master)$ git reflog
ab7555f HEAD@{0}: pull origin wrong-branch: Fast-forward
c5bc55a HEAD@{1}: checkout: checkout message goes here

重置分支到你所需的提交:

git reset --hard c5bc55a

恢复误删除的分支

有些时候可能删除了还没有推到远程的分支,如何恢复?例,创建一个分支,并做一次提交:

(master)$ git checkout -b my-branch
(my-branch)$ git branch
(my-branch)$ touch foo.txt
(my-branch)$ ls
README.md foo.txt
(my-branch)$ git add .
(my-branch)$ git commit -m 'foo.txt added'
(my-branch)$ foo.txt added
 1 files changed, 1 insertions(+)
 create mode 100644 foo.txt

现在我们切回到主(master)分支,‘不小心的’删除 my-branch 分支

(my-branch)$ git checkout master
Switched to branch 'master'
Your branch is up-to-date with 'origin/master'.
(master)$ git branch -D my-branch
Deleted branch my-branch (was 4e3cd85).
(master)$ echo oh noes, deleted my branch!
oh noes, deleted my branch!

开始恢复删除的分支,先使用 reflog 命令, 它存储了仓库(repo)里面所有动作的历史。

(master)$ git reflog
69204cd HEAD@{0}: checkout: moving from my-branch to master
4e3cd85 HEAD@{1}: commit: foo.txt added
69204cd HEAD@{2}: checkout: moving from master to my-branch

可以看到一个删除分支的提交 hash(commit hash),开始恢复:

(master)$ git checkout -b my-branch-help
Switched to a new branch 'my-branch-help'
(my-branch-help)$ git reset --hard 4e3cd85
HEAD is now at 4e3cd85 foo.txt added
(my-branch-help)$ ls
README.md foo.txt

标签

# 查看标签
git tag

# 展示当前分支的最近的 tag
git describe --tags --abbrev=0

# 查看标签详细信息
git tag -ln

# 本地创建标签
git tag [version-number]

# 默认 tag 是打在最近的一次 commit 上,指定 commit 打 tag
git tag -a [version-number] -m "v1.0 发布(描述)" [commit-id]

# 推送标签到远程仓库,保证本地创建好了标签才可以推送标签到远程仓库:
git push origin [local-version-number]

# 一次性推送所有标签
git push origin --tags

# 删除本地标签
git tag -d [tag-name]

# 删除远程标签
git push origin --delete tag [tagname]

# 切回到某个标签
git checkout -b branch_name tag_name

恢复已删除标签

首先, 需要找到无法访问的标签(unreachable tag):

git fsck --unreachable | grep tag

得到这个标签(tag)的 hash,然后:

git update-ref refs/tags/<tag_name> <hash>

这时标签(tag)应该已经恢复了。

远程仓库

# 下载远程仓库的所有变动
git fetch

# 显示所有远程仓库
git remote -v

# 显示某个远程仓库的信息
git remote show [remote]

# 增加一个新的远程仓库,并命名
git remote add [shortname] [url]

# 修改远程仓库的 url
git remote set-url [remote] [url]

# 取回远程仓库的变化,并与本地分支合并
git pull [remote] [branch]

# 上传本地指定分支到远程仓库
git push [remote] [branch]

# 强行推送当前分支到远程仓库,即使有冲突
git push [remote] --force

# 推送所有分支到远程仓库
git push [remote] --all

同步远程仓库到自己 fork 的仓库

如果一个项目多人维护,每个人都 fork 了主仓库,并修改,提交 PR,那么如果在你提交自己的修改之前,主仓库 merge 了别人的 PR, 你 fork 的仓库的 commit 就会落后于主仓库,例如会有类似 This branch is 12 commit behind ITOM-Shared-Services:master. 的提示。这个时候 直接提交你的代码,创建 PR,如果 merge 你的 PR,可能就会有冲突,怎么解决?

  1. 切换在你本地的仓库
  2. 添加 remote
git remote add itom git@github.houston.softwaregrp.net:ITOM-Shared-Services/keel-service.git

上面的命令中 itom 是给这个 remote 命名,git@github.houston.softwaregrp.net:ITOM-Shared-Services/keel-service.git 是 remote 的地址。

  1. 验证 git remote -v
  2. 在每次提交前执行 git pull itom master,可以把主仓库的最新 commits 拉去到本地。
  3. 提交代码

Rebase 和 Merge

rebase 和 merge 有什么区别

rebase

rebase 会把你当前分支的 commit 放到公共分支的最后面,所以叫变基。 例如,你从 master 拉了个 feature 分支出来,然后你提交了几个 commit,这个时候刚好有人把他开发的东西合并到 master 了,这个时 候 master 就比你拉分支的时候多了几个 commit,如果这个时候你 rebase master 的话,就会把你当前的几个 commit,放到那个 人 commit 的后面。

merge

merge 会把公共分支和你当前的 commit 合并在一起,形成一个新的 commit 提交。

撤销 rebase/merge

如果 merge 或 rebase 了一个错误的分支, 或者完成不了一个进行中的 rebase/merge。 Git 在进行危险操作的时候会把原始的 HEAD 保 存在一个叫 ORIG_HEAD 的变量里, 所以要把分支恢复到 rebase/merge 前的状态是很容易的。

git reset --hard ORIG_HEAD

合并冲突时如何撤销 merge

如果已经执行了 git merge 命令,但是发生了冲突,可以使用下面的命令撤销:

git merge --abort

配置

配置 http 和 socks 代理

git config --global https.proxy 'http://127.0.0.1:8001'
git config --global http.proxy 'http://127.0.0.1:8001'
git config --global socks.proxy "127.0.0.1:1080"

配置常用的命令别名

git config --global alias.st status
git config --global alias.br branch
git config --global alias.co checkout
git config --global alias.ci commit

如:

git config --global alias.st status

可以使用 git st 代替 git status

或者修改配置文件,Linux 下, Git 的配置文件储存在 ~/.gitconfig。在 [alias] 部分添加快捷别名,如下:

[alias]
    a = add
    amend = commit --amend
    c = commit
    ca = commit --amend
    ci = commit -a
    co = checkout
    d = diff
    dc = diff --changed
    ds = diff --staged
    f = fetch
    loll = log --graph --decorate --pretty=oneline --abbrev-commit
    m = merge
    one = log --pretty=oneline
    outstanding = rebase -i @{u}
    s = status
    unpushed = log @{u}
    wc = whatchanged
    wip = rebase -i @{u}
    zap = fetch -p

配置 git log 格式

原生的 git log 不太好用,一样可以配置:

git config --global alias.lg 'log --color --graph --pretty=format:"%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset" --abbrev-commit'

然后git lg就成了下面的样子: /images/git-use/log.JPG

生成压缩包

git archive