Git

1,575 阅读17分钟

一、概念

提交和分支

  • Git版本管理是通过提交(commit)组织起来的,每一个新的提交只记录产生变化的文件,所以存储开销不大。

  • 下文中的<ref>可以是分支(branch)、标签(tag)、提交号(CommitID)、HEAD或FETCH_HEAD等。其中branch、tag和FETCH_HEAD是指向某个提交的指针,它们的创建、删除和移动的开销很小。随着分支和提交的增多,提交历史形成了树状结构。

  • HEAD是当前所在分支的别名。如果分支名很长,可以用HEAD方便的表示当前分支。在一些自动化脚本里,HEAD也许会很有用。

  • 在某些修改发生后(例如git commit --amend),这个树状结构的某一条分支的最新的提交,没有被任何branch或tag指向,则这条分支将不会显示在提交历史中,但它在短时间内仍存在于仓库中,可以使用git reflog命令找到相关的提交,然后用git branch/checkout创建分支。

工作区和暂存区

  • 将每一个提交、工作区和暂存区分别视为一个版本,使用git status可查看暂存区版本和HEAD版本之间有差异的文件(Changes to be committed)、工作区版本和暂存区版本之间有差异的文件(Changes not staged for commit)。

  • 在分支没做任何修改的情况下,暂存区版本和HEAD版本保持一致,工作区版本和暂存区版本保持一致,所以git status结果以及VSCode的Git插件下不会显示内容。

  • 当对某文件做了修改,则工作区版本发生变化,工作区版本和暂存区版本就该文件不一致。

  • 然后使用git add将工作区文件版本刷新到暂存区,此时工作区版本和暂存区版本就该文件是一致的,而暂存区版本和HEAD版本就不再一致了。

  • git commit将暂存区版本加入到Git历史中,于是暂存区和HEAD再次一致。

远程仓库和远程分支

  • Git是分布式版本管理系统,每个仓库在Git系统中是平等的,一个仓库可以连接有多个远程仓库,它自己也可以作为其他仓库的远程仓库。

  • 位于本地仓库和远程仓库的分支在Git系统中也是平等的,只不过在开发中,人为地将远程仓库的分支作为稳定版本,而本地仓库的分支作为开发分支。之所以提这一条,是因为初学者往往把master分支和origin/master分支混淆起来,造成麻烦。

使用分支的策略

  • 如果无需连接远程仓库,设主干分支为master,一般新建dev分支开发,然后合入到master分支中。

  • 在多人协作开发中,主干分支一般为origin/master,在本地dev分支开发完后push到远程仓库的dev分支,然后创建合并请求合入到origin/master。


二、初始化仓库

git init

git init <path>
    初始化文件夹为Git仓库。

git init
git init .
这两行等价

--bare
    create a bare repository
    仓库将是bare仓库,不含工作区和暂存区。这种仓库可用作发布仓。

git clone

git clone <repo> [<dir>]
    可用<dir>指定不同于仓库名的文件夹名。

-b, --branch=<branch>
    checkout <branch> instead of the remote's HEAD
    切换到指定分支,而不是远程仓库HEAD指向的分支。

--single-branch
    clone only one branch, HEAD or --branch
    仅拉取一个分支,HEAD指向的分支或者--branch指定的分支。

--depth=<depth>
    create a shallow clone of that depth
    仅拉取给定深度的提交历史,即拉取的历史提交和分支所在提交的距离不超过depth。

--bare
    create a bare repository

三、工作区和暂存区

git add

  • 功能:用工作区的文件刷新暂存区的文件。
  • 可以使用通配符,如git add *.c
  • .gitignore文件存放不用跟踪的文件;每个文件夹都可有此文件。更多规则参考忽略文件

git commit

  • 功能:提交暂存区版本。
  • 常用选项
-m, --message <message>
    commit message

-C, --reuse-message <commit>
    reuse message from specified commit

-c, --reedit-message <commit>
    reuse and edit message from specified commit

 --[no-]reset-author
    the commit is authored by me now (used with -C/-c/--amend)
    如果使用了-C等选项,极有可能提交的作者不是自己,这样代码量就算给别人了,因此每次创建合并请求前,看看提交的作者是不是自己,如果不是则加上这个选项。

--amend
    amend previous commit
    修订前一个提交,修改文件或提交信息。
    从另一个角度理解,实际上是把暂存区版本提交了,并将当前分支位置移动到新的提交上。

-a, --all
    commit all changed files
    提交前,先把工作区的修改文件都转到暂存区。
  • 在未使用-m-C等选项来指定提交信息时,将弹出默认编辑器来编辑提交信息。在纯终端环境,编辑器大概率是Vim类的终端文本编辑器。在Windows上可设置默认编辑器为VSCode。
git config --global core.editor "code --wait"

git restore

git restore [--source=<ref>] [--staged] [--worktree] [--] <path>
    将文件恢复为指定版本。

--staged
    在未使用--staged选项时,操作工作区,指定后,操作暂存区。
    同时使用--staged和--worktree选项,则同时操作暂存区和工作区。

--source
    在未指定source选项时,对暂存区默认source是HEAD,对工作区默认source为是暂存区。
  • 因此下列命令可以理解为
git restore <path>
    舍弃工作区的修改,即工作区版本和暂存区版本保证一致。

git restore <path> --staged
    舍弃暂存区的修改,即暂存区版本和HEAD版本保持一致,不影响工作区。

git restore <path> --staged --worktree
    舍弃暂存区和工作区的所有修改,即工作区版本和暂存区版本都和HEAD保持一致。
  • 下列命令也具有类似功能,但不建议使用。

checkout主要功能是切换分支,使用下列命令容易混淆。

git checkout <ref> [--] <path>
    将工作区和暂存区都恢复为给定版本。
    等同于
    git restore --source=<ref> --staged --worktree [--] <path>

git checkout [--] <path>
    不指定<ref>时,仅操作工作区,使其和暂存区保持一致。
    等同于
    git restore <path>

git clean

git restore不会删除新增文件和忽略文件,这些文件可通过git clean删除。

git clean <paths> --dry-run
    删除新增文件

-n, --dry-run
    dry run
    显示将要删除的文件列表,不会实际删除这些文件。

-f, --force
    force
    必须指定此选项才会删除

-d
    remove whole directories
    使用此选项才会递归文件夹

-e, --exclude <pattern>
    add <pattern> to ignore rules

-x
    remove ignored files, too
-X
    remove only ignored files
    有这两个选项,才会删除忽略文件,但是一般也不用,例如,'node_modules/'文件夹就是
    项目运行调试需要但不需要加入Git的文件夹。

git stash

当工作区或暂存区存在修改时切换分支,若有冲突,则切换失败,可将这些修改放入stash,然后切换分支。 (没怎么用这些命令行,一般使用GitLen插件)

git stash push …
    将更改放入stash

git stash list/show …
    前者列出stash列表,后者列出一个stash的信息。

git stash branch …	?

git stash pop/apply …
    应用stash,前者将这个stash从stash列表中移除,后者不会。

git stash clear/drop …
    前者删除全部stash,后者删一个。

四、分支

git branch

展示、创建、删除、重命名分支。

git branch -vva	
    显示所有分支(包括远程仓库的分支)和分支的跟踪关系,*标记当前分支。

git branch <branch-name> [<ref>]
    在当前提交或给定提交<ref>处创建新分支,不切换到新分支。

git branch -f <branch-name> [<refs>]
    强制创建分支,即移动分支位置。

git branch -d <branch-name>
    删除分支。

git branch -m <old> <new>
    重命名分支。

git checkout

切换分支。

git checkout <ref>
    <ref>如果是CommitID/tag/带仓库名的远程分支,则将在ref创建临时无名分支,切出分支后不保留修改。
    如果希望保留修改,在临时分支上创建新分支即可。

git checkout -b <new-branch> [<ref>]
    在给定提交上创建新分支,并切换到新分支。如果<ref>是远程分支,还会设置跟踪关系。

git checkout -
    切回到上一个分支。

git checkout [<ref>] -d/--detach
    在<refs>处创建临时分支,并切换到该临时分支。

--orphan
    创建一个新的分支,分支上没有任何提交,暂存区版本和HEAD版本内容相同。

git reset

重设分支位置,并且根据选项决定对工作区和暂存区的操作。HEAD是当前分支的别名,所以HEAD也会被重设。

git reset [options] <refs>

--soft
    不重置工作区版本和暂存区版本,即不变。

--mixed
    不重置工作区版本,重置暂存区版本,

--hard
    同时重置工作区和暂存区版本,使其与重置后HEAD保持一致。即丢失所有修改。

使用--mix--hard可能导致丢失修改。

当<refs>为HEAD,将不会重设分支位置,命令效果和git restore相似。(前两条从没用过,第三条用得较多。)

git reset --soft HEAD
    无任何作用。

git reset --mixed HEAD
    等同于
    git restore * --staged

git reset --hard HEAD
    等同于
    git restore * --staged --worktree

五、分支和提交

git merge

git merge <ref> -m <message>
  • 如果当前分支是被合并分支的祖先,则会简单移动地当前分支。
  • 如果被合并分支是当前分支的祖先,则不变。
  • 如果位于两条支链上,将在当前分支创建新的提交把另一个分支(节点)内容合并到当前分支,可能需要解决冲突。
git merge <ref1> <ref2> -m <message>
    一次可以合并多个分支。

--squash
    create a single commit instead of doing a merge
    不使用这个选项时,提交历史将保留合并关系,即是由哪些分支合并来的。
    使用这个选项,将仅创建新提交,提交历史成线性。这个选项适合被合并分支的
    提交历史过多或过于杂乱的情况。

--no-ff
    一个线性的提交历史上的两个分支,较老的分支合并较新的分支,在本地进行这
    样的操作时,将会fast forward,如果希望采用创建新提交的方式合并,则使用
    这个选项。

git rebase

git rebase <base-ref> <ref>
git rebase <base-ref> 等价于 git rebase <base-ref> "current branch"
git rebase 等价于 git rebase "upsteam" "current-branch"
  • 如果base-ref和ref是祖先关系,则同merge。
  • 如果位于两条支链上,则将共同祖先到ref的所有commit应用到base-ref。操作后,会checkout到rebase后的ref上。
-f, --force
    强制rebase。不加此选项时,如果<base-ref><ref>的祖先,rebase不会生效。

-i, --interactive
   交互式rebase。调整pick子命令顺序,则可改变commit的顺序。使用squash,可将commit合并到上一个pick的提交中,第一个提交必须使用pick。
git rebase --onto=<base-ref> <ref1> <ref2>
    将<ref1>到<ref2>的提交应用到<base-ref>上,不包含<ref1>,操作后会checkout到rebase后的<ref2>上。

git rebase --onto=<base-ref> <ref1>^ <ref2>
将<ref1>到<ref2>的提交应用到<base-ref>上,包含<ref1>。^符号表示前一个提交,有n个^符号就表示第前n个提交。

也可以用~n来表示,如下:
git rebase --onto=<base-ref> <ref>~n <ref>
将<ref>在内的最新的n个提交rebase到<base-ref>。

git cherry-pick

git cherry-pick <ref1> <ref2>…
    将一些提交应用到当前分支。

git cherry-pick <ref1>^..<ref2>
    应用ref1到ref2的提交到当前分支。

git revert

git revert <commitid> <commitid> …
    在当前分支新建提交,这个提交会将CommitID所做的变更取消。

六、远程仓库和远程分支

git remote

git remote [-v | --verbose]
    查看远程仓库列表。

git remote add <name> <url>
    添加远程仓库。

git remote show <name>
    查看远程仓库信息。

git remote rename <old> <new>
    重命名远程仓库。

git remote remove <name>
    删除远程仓库。

git fetch / git pull

git fetch [<options>] <repository> <refspec>...
    拉取一个远程仓库的一个分支。

git fetch [<options>] <repository>
    拉取远程库的所有分支。

git fetch [<options>]
    拉取当前分支跟踪的分支。

git fetch --all
    拉取所有远程仓库。

--depth=<num>
    deepen history of shallow clone

--dry-run
    dry run

-p, --prune
    prune remote-tracking branches no longer on remote
    如果本地分跟踪的远程分支不存在,则删除本地分支。结合--dry-run使用。
    同git remote prune <repo> --dry-run

不推荐使用git pull (git fetch + git merge),推荐使用git fetch + git rebase

git push

git push <repo> <local-branch>:<remote-branch>
    将本地分支推送到远程仓库的分支,两分支可不同名。

git push <repo> [<local-branch>]
    将本地分支推送到远程仓库,如果没有设置跟踪分支,将推送到同名远程分支。

git push
    将本地分支推送到跟踪分支。

-u, --set-upstream
    set upstream for git pull/status
    上述语句,有些不会设置跟踪关系,可用这个选项设置。
    也可使用下列语句设置或取消跟踪关系:
    git branch -u|--set-upstream-to=<remote>/<branch>
    git branch --unset-upstream

-f, --force
    force updates
    如果远程分支所在提交不是本地分支的祖先,说明有其他人更新了远程分支,这次推送将被拒绝。
    使用此选项,将强制把远程分支重设到本地分支所在的提交。

git push <remote> --delete <refs>
    删除远程仓库的某分支/标签。

git push <remote> --prune [<refs> | --all] --dry-run
    --prune  prune locally removed refs
    删除远程仓库中的分支/标签,如果这些分支或标签不存在于本地仓库。

七、比较和查看

git diff

比较有三种情况:

  1. 工作区和暂存区;
  2. 暂存区和HEAD;
  3. 提交之间。

这三种比较都可以在VSCode中实现:1.和2.在VSCode中可直接查看,无需下载插件。3.可以通过GitLen插件查看。

命令行:

git diff [<ref>] [--] [<path>..]
    比较工作区和<ref>的差异,如果省略<ref>,则比较和暂存区的差异。

git diff --staged [<ref>] [--] [<path>..]
    比较暂存区和<ref>的差异,如果省略<ref>,则比较和HEAD的差异。

git diff <ref1> <ref2> [--] [<path>..]
    比较提交之间的差异。

git diff ... > x.patch
    可将差异输出到文件中,然后用git apply x.patch来应用补丁。

--name-only
    仅输出有差异的文件的相对路径。

--name-status
    输出相对路径和变化类型。

git difftool

如果使用VSCode,可跳过这一小节。(作者从未用过这个命令)

git diff的输出是在终端,如果希望使用指定的编辑器来显示差异,可使用git difftool

git config --global diff.tool vscode
git config --global difftool.vscode.cmd 'code --wait --diff $LOCAL $REMOTE'
    运行上面两行命令,设置差异比较工具为VSCode,也可设置为其他的工具。

git difftool [--] [<path>..]
git difftool --staged [--] [<path>..]
git difftool <commit> <commit> [--] [<path>..]
    用法完全同git diff

git status

git status [<options>] [--] [<pathspec>...]
    查看工作区和暂存区存在差异的文件,以及暂存区和HEAD存在差异的文件。

-s, --short
    show status concisely
    简短地输出。

git log

git log [<ref>] [-- <path>]
    若未指定<ref>,则默认为HEAD.
    若未指定<path>,则输出给定提交及其历史提交;否则,输出修改了该文件的历史提交(这些提交可能不连续,且不晚于<ref>)。

--graph
    输出提交网络图。

-n
    输出n个提交。

--name-only
    只显示修改的文件。
--name-status
    显示修改的文件和修改类型。
--stat
    显示修改的文件和修改行数。
--pretty=oneline/short/full/fuller/format:""
    显示的丰富程度或格式。
--since=时刻
--until=时刻
    筛选时间。
--author=
    不完全匹配式搜索作者名。
--grep=
    在提交信息中搜索。

git log [<refs>] -L :<function-name>:<filename>
    查看给定提交中,某文件中某函数的变化。

git show

git show [<ref>] [-- <path>]
    输出给定提交的文件变化。<ref>默认为HEAD。

--name-only
    只输出修改的文件。
--name-status
    输出修改的文件和修改类型。
--stat
    输出修改的文件和修改行数。

git show <ref>:<file>
    输出某提交中中,某文件的内容。

git reflog

git blame


八、其他

git tag

查看标签:

git tag
git tag -l
    列出标签

git show <tag>
    查看标签信息和对应的提交信息。

创建标签:

git tag <tag> [<ref>]
    创建简单的标签。
git tag -f <tag> [<ref>]
    强制创建标签,即移动标签位置。

git tag -a <tag> [<ref>] -m "message"
    创建有注释的标签。
-a, --annotate
    annotated tag, needs a message
-m, --message <message>
    tag message

上传/更新标签:

git push <remote> <tag>
git push <remote> --tags
    上传一个或所有标签。

git push <remote> <ref>:refs/tags/<tag>
    更新一次分支的<tag>到<ref>处。

删除标签:

git tag -d <tag>
    删除本地标签

git push <remote> --delete <tag>
    删除远程仓库的标签。远程仓库有同名的分支时,会出错,删除那个分支。
git push <remote> :refs/tags/<tag>
    删除远程标签。

git push <remote> --prune <tag> --dry-run
git push <remote> --prune --tags --dry-run
    删除远程仓库的标签,如果其在本地不存在。

git config

配置选项

git config --global user.name xxx
git config --global user.email xxx
git config --global http.proxy 127.0.0.1:7890
    设置变量

--global
    生效范围为全局。
--system
--local
    生效范围为仓库。

git config -l
    列出配置。

git config --global --unset http.proxy
    取消变量。

git archive

如果需要将指定提交打包,或希望打包HEAD但是不包含未跟踪的文件,使用git archive

git archive --format=tar -o v1.0.0.tar <ref> [<path>..]

--format <fmt>
    archive format
    Possible values are `tar`, `zip`, `tar.gz`, `tgz`.
    If `--format` is not given, and the output file is specified, the format is inferred from the filename if possible.
    Otherwise the output format is `tar`.
    如果未指定格式,则首先从文件名后缀获取格式,若失败则为tar。

-o, --output <file>
    write the archive to this file
    输出文件。如果未指定则输出到标准输出,可用在语句后接“> v1.0.0.tar”。

九、一些方法

1. 无法克隆GitHub上的仓库?

使用git clone克隆Github/Gitee等平台的仓库前,使用ssh尝试访问git@github.com,使得~/.ssh/known_hosts含有相应的内容。有时网站的ip变化了,也需要这样做。

2. 使用指定ssh私钥访问远程仓库

如果你比较注重安全,应该听闻过最小权限原则,即一个密钥做一件事,不能一个密钥既用来登录一些远程主机,又用来访问GitHub。

专门创建一对公钥密钥对github_accessgithub_access.pub,在克隆仓库时,使用下列命令:

git clone -c "core.sshCommand=ssh -i ~/.ssh/github_access -F /dev/null" git@github.com:username/repo.git

对于已存在的仓库使用下列命令:

git config --local core.sshCommand "ssh -i ~/.ssh/github_access -F /dev/null"

3. 下载远程仓库的部分文件

如果是下载GitHub仓库的文件,可以在网页上找到单个文件的下载链接。

若要使用Git命令行下载:

git init # 在一空文件夹中
git remote add origin <url>
git config core.sparsecheckout true # 允许稀疏检出
echo <path> >> .git/info/sparse-checkout
git pull origin <branch> [--depth=1]

4. 仅拉取单个分支

如果仅对某个远程仓库的某一分支感兴趣,前文提到可用git fetch <remote> <branch>拉取,但是这条语句有点长,有一种方法可以使得git fetch <remote>仅拉取一条分支。

使用git config -l --local,找到如下一条,其中origin应是希望操作的远程仓库名,这一条语句表明git fetch <remote>会拉取和显示所有分支。

remote.origin.fetch=+refs/heads/*:refs/remotes/origin/*

打开仓库下的.git/config文件,将其改为:

remote.<remote>.fetch=+refs/heads/<local_branch>:refs/remotes/<remote>/<remote-branch>

也可使用命令行来修改:

git config --local remote.<remote>.fetch +refs/heads/<local_branch>:refs/remotes/<remote>/<remote-branch>

此外,如果在git clone时使用了--single-branch选项,则上述修改是已做的。这时,如果我们又希望拉取其他分支,反向修改回去即可。

5. 拉取合并请求

假设有一主远程仓库,作者和同事都fork了这个仓库。现在同事提了合并请求到主仓,因为各种原因(例如,要帮同事定位一下问题,或者要和同事的代码合在一起做验证之类的),作者要拉取同事开发的代码,可使用下列命令:

git fetch <remote> refs/merge-requests/<MR-ID>/head
git checkout -b tmp FETCH_HEAD
或者
git fetch <remote> refs/merge-requests/<MR-ID>/head:<local-branch-name>
如果分支不存在将创建

(未完待续)