Git 工作流


初始化一个 Git 仓库

Git 已经成为当今版本控制工具的主流,而分布式的结构和日志型的存储让 Git 不那么容易理解. 本文以实际的案例,总结了仓库初始化的操作步骤以及涉及到的 Git 命令.

从既有远程仓库建立

  • 场景:加入一个项目,或创建一个项目副本.
  • 步骤:远程仓库已经存在的情况下,直接克隆即可得到一个仓库副本.
git clone git@foo.com:bar.git
cd bar/

从空的远程仓库建立

  • 场景:初始化一个远程仓库,例如建立一个 Github 仓库后.
  • 步骤:新建目录并将其初始化为 Git 仓库,然后添加远程仓库到 remote.
mkdir bar && cd bar
git init --bare
git remote add origin git@foo.com:bar.git
touch README.md
git add README.md && git commit -m 'init'
# 初次 Push 需指定远程分支
git push -u origin master

代码提交

Git 已经成为当今版本控制工具的主流,而分布式的结构和日志型的存储让 Git 不那么容易理解. 本文以实际的案例,总结了 Git 代码提交相关的操作步骤以及涉及到的 Git 命令.主要包括:

git add 命令将工作区内容添加到暂存区,git commit 命令将暂存区内容提交到本地仓库. 添加 -m 参数可直接用指定的 message 提交本次 commit. 否则 Vim 会打开默认的文本编辑器提示你输入 commit message. git commit 时你的 Git 没有为你打开 Vim? Git 打开哪个编辑器取决于 GIT_EDITOREDITOR 等环境变量, 一般在 ~/.bashrc 加入下面的设置即可:

# 当然你可以设为 nano
export GIT_EDITOR="vim"

添加到暂存区并提交

  • 场景 :将新增或改动的文件添加到暂存区,并提交到 Git 仓库.
  • 步骤 :使用 git add 命令即可将某个文件(的修改)添加到暂存区,再 git commit 来提交.
  • 文档 :https://git-scm.com/docs/git-add
# 添加 README.md 到暂存区
git add README.md
# 添加当前目录所有文件到暂存区
git add .
# 强制添加,忽略. gitignore 配置
git add node_modules/ --force
git commit -m 'first commit'
  • git add 会忽略列在 .gitignore 中的文件 / 目录,通过 --force 参数可以强制添加.
  • 添加并提交是 Git 中最常见的代码提交方式. Git 提交总会完整地记录一次变更,即提交总会使 Git 仓库变大,不论是新增还是删除.真正从仓库中移除记录需要 更改 Git 历史记录.

提交对仓库中文件的改动

  • 场景 :希望只提交仓库中既有文件的改动,而不想 add 其他的文件(仓库外).
  • 步骤 :省略 git add 命令,然后以 -a 参数运行 commit.
git commit -a

可通过 git status 来查看当前的改动情况,以及本地与远程的同步情况.

撤销 Add

  • 场景 :不小心添加了文件到暂存区,现在需要撤销所有的 git add.
  • 步骤 :使用 get reset,重置暂存区到 HEAD.
# 取消 Add 某一个文件
git reset path/to/file
# 取消所有 Add 的文件(将会使得所有改动变成 not staged 或 untracked)
git reset

撤销上次 Commit

  • 场景 :发现上次 commit 信息有误,或不小心 commit 了不合适的文件, 希望能撤销 commit 而文件不受改动.
  • 步骤 :使用 Git 的『软』(不改动文件)重置.
git reset --soft HEAD^

HEAD^ 回到表示重置到当前状态的前一个 commit.

修改上次 Commit

# 下面三条命令只产生一个 Commit
git commit -m 'initial commit'
git add forgotten_file # 添加漏掉的文件
git commit --amend # 补充 Commit 信息

撤销工作区所有改动

# 重置工作区的所有改动
git reset --hard
# 该命令可以指定当前目录,还是某个文件
git checkout .

空提交

  • 场景 :只想产生一个 commit 而不想改动文件.比如需要 push 一个 commit 以触发重新部署的 Git Hook 时. 步骤 :使用 --allow-empty 参数来提交.
git commit --allow-empty -m 'empty commit'

将文件从仓库中移除

  • 场景 :不小心把不应提交到仓库的文件(比如临时文件,大文件,配置文件等)提交了进去,现在希望将其删除.
  • 步骤 :使用 git rm 命令.
# 从仓库和工作区都删除它(例如临时文件)
git rm .*.swp
# 只从仓库中删除,工作区中保留(例如配置文件)
git rm --cached config.yml

如果希望从仓库历史中也删除(例如大文件),那么需要使用 git filter-branch 系列命令. 请参考 寻找并删除 Git 记录中的大文件 一文.

git rm 和 bash rm 的参数类似,基本可通用.

分支管理

Git 已经成为当今版本控制工具的主流,而分布式的结构和日志型的存储让 Git 不那么容易理解. Git 的一个分支相当于一个 commit 节点的命名指针.分支之间可互相 merge. 本文以实际的案例,总结了 Git 分支管理的操作步骤以及涉及到的 Git 命令.

查看分支

  • 场景 :查看当前位于哪个分支,以及本地和远程各有什么分支.
  • 步骤 :使用 git branch 系列命令.
# 查看本地分支
git branch
# 查看所有分支
git branch -a

分支增删

  • 场景 :删除某个分支,分支只是 Commit 的指针,删除分支不会影响 Git 中的 Commit 树.
  • 步骤 :使用 git branch -D 命令.
# 创建一个名为 `test` 的分支并切换到该分支:
git checkout -b test
# 切换回 `master` 分支:
git checkout master
# 删除 `test` 分支:
git branch -D test
# 从远程仓库删除
git push --delete origin test

分支重命名

  • 场景 :分支名写错了,或者找到了更合适的分支名.可能也会需要更改服务器上的分支名.
  • 步骤 :使用 git branch -m 重命名,git push --set-upstream 来重新映射 track 关系(以及将新分支 Push 到远程).
# 在本地仓库重命名
git branch -m old_branch new_branch
# 在远程删除旧分支
git push origin :old_branch
# 新分支 Push 到远程
git push --set-upstream origin new_branch

如果要重命名的是当前分支,可以直接 git branch -m new_branch.

分支合并

  • 场景 :需要将某个分支合并到 master,或任意两个分支间希望合并.
  • 步骤 :使用 git merge 系列命令,merge 成功会产生一次 commit.
  • 文档 :https://git-scm.com/docs/git-merge
# 将 feature-1 分支合并到当前分支
git merge feature-1 -m 'merge feature 1'

注意 merge 前必须 commit 工作区的更改,否则 merge 后无法回到当前工作区的状态.

查看分支图

  • 场景 :需要了解当前分支的父分支,或者仓库中分支之间的关系.
  • 步骤 :使用 git log 系列命令.
# --graph 画图,--decorate 标明分支名(而不只是 ID)
git log --graph --decorate

冲突解决

  • 场景 :你的 commit 修改了某个文件,希望 merge 另一个 commit.然而你们修改了同一文件的同一行.
  • 步骤 :
  1. 先解决冲突,打开 Git 提示有冲突的每个文件(其中有 Git 给出的冲突信息),更改文件内容为你想要的最终内容并删除 Git 的冲突信息.
  2. git add 相应的冲突文件.
  3. git commit 本次合并.
# 假设合并发生了冲突,文件 a.txt
git merge feature-2
# 修改冲突的文件
vim a.txt
# 添加到暂存区
git add a.txt
# 提交本次合并
git commit -a 'merged feature 2'

新建 Tag

  • 场景 :新建某个 Tag.
  • 步骤 :使用 -a(annotated),-d(delete)参数运行 git tag,最后更新到远程仓库.
# 创建
git tag -a v1.4 -m 'my version 1.4'
# Push 到远程
git push --tags

删除 Tag

  • 场景 :有一个错误的 Tag.
  • 步骤 :先在本地仓库删除,然后 push 到远程仓库.
git tag -d old
git push origin :refs/tags/old

重命名 Tag

  • 场景 :当然是 Tag 名字起错了.
  • 步骤 :先从旧的 Tag 创建新的,删除 Tag,push 到远程.
git tag new old
git tag -d old
git push origin :refs/tags/old
git push --tags

列出所有 Tag

  • 场景 :需要列出所有 Tag.
  • 步骤 :git tag 即可.
# 列出所有
git tag
# 同时列出 message
git tag -n
# 过滤
git tag -l 'v1.2.\*'

列出 Tag 之间的 Commit 日志

  • 场景 :需要知道版本之间有哪些改动,以生成 Chnagelog 或 ReleaseNote.
  • 步骤 :查询 Git 日志,使用 .. 来选择一段区间.
git log v1.1..v1.2

衍合

  • 场景 :开发者从主分支(master)checkout 一个分支(如 feature-x)进行开发, 当要合并入主分支时发现主分支已经改变. 仓库维护者合并 feature-xmaster 时需要解决冲突并回归测试. 为了减少维护者的工作,开发者可以将 feature-x Rebase(衍合)到 master. 这样在 master 合并 feature-x 就是 Fast Forward 了.
  • 步骤 :切换到 feature-x,进行衍合,Push 代码.
  • 文档 :https://git-scm.com/book/zh/v1/Git-%E5%88%86%E6%94%AF-%E5%88%86%E6%94%AF%E7%9A%84%E8%A1%8D%E5%90%88
git checkout feature-x
git rebase master
git push origin feature-x

重置分支指针

  • 场景 :当前的工作(可以包括若干 commit,也可以只是工作区)不想要了,或者希望将的当前工作弄到别的分支. 需要恢复当前分支到之前的某个状态.
  • 步骤 :如果要保存当前工作,首先 commit 掉并从当前分支 checkout 出来. 然后 git reset 当前分支到某个历史状态(通过 git log 可以看到当前分支的所有历史状态).
  • 文档 :https://git-scm.com/docs/git-reset
# 保存当前工作到 current-work 分支
git commit -m 'current work'
git checkout current-work
# 重置当前分支指针
git reset --hard xxxxx
# 典型地,重置到远程分支
git reset --hard origin/master

操作远程仓库

Git 已经成为当今版本控制工具的主流,而分布式的结构和日志型的存储让 Git 不那么容易理解. Git 的一个分支相当于一个 commit 节点的命名指针.分支之间可互相 merge. 本文以实际的案例,总结了 Git 远程仓库的操作步骤以及涉及到的 Git 命令.

显示远程仓库

  • 场景:需要查看远程仓库地址(比如想把它拷贝给别人).
  • 步骤:使用 git remote 相关命令.
# 查看所有远程仓库
git remote -v
# 查看一个远程仓库(比如 origin)的详细信息(包括 Fetch、Push 地址)
git remote show origin
# -n 参数禁止联系远程仓库,可大大加快速度
git remote show origin -n

管理远程仓库

  • 场景:需要添加、更改或删除远程仓库时.例如远程仓库从 Github 迁移到 Coding.net 时需要更改远程仓库 URL(不需重新 clone).
  • 步骤:使用 git remote 系列命令操作.
# 添加远程仓库 bar.git 并命名为 bar
git remote add bar bar.git
# 更改远程仓库 URL
git remote set-url origin new.xxx.git

更多命令请查询 git remote --help

同步远程仓库

  • 场景:将远程仓库同步到本地,或将本地仓库同步到远程.
  • 步骤:使用 git fetchgit push 系列命令.
  • 文档:https://git-scm.com/docs/git-push
# 同步默认的 remote 仓库(通常叫 origin)到本地
# 工作区文件并不会发生改变,只同步仓库内容,即 `.git/` 目录
git fetch
# 同步所有 remote 仓库到本地
git fetch --all

多个远程仓库

  • 场景:一个本地仓库需要与多个远程仓库同步,或需要 merge 其他远程仓库时. 例如 Github Pages 博客同时 Push 到 Github 和 Coding.net.
  • 步骤:逐个添加远程仓库到 remote,逐一 Push.
# 将 coding 仓库添加到 remote
git remote add coding git@coding.net:bar.git
# 将 master 分支 Push 到 origin 的 master 分支
git push origin master
# 将 master 分支 Push 到 coding 的 coding-pages 分支
git push coding master:coding-pages

checkout 一个远程分支

  • 场景:现有一个本地仓库不存在的远程分支,希望让当前工作区进入这个分支.
  • 步骤:可以先同步本地仓库,再切换到该分支.也可以先切换到该分支再同步远程代码.
# 方法一:同步本地仓库
git fetch
# 切换到远程分支
git checkout feature-x
# 方法二:切换到新的分支
git checkout -b feature-x
git branch --set-upstream remote/feature-x
# 等效于
git branch -u remote/feature-x
git pull
# 方法三:先创建分支以及 track 关系,再切换分支
git branch feature-x
git branch -u remote/feature-x feature-x
git checkout feature-x
git pull

删除远程分支

# 删除远程 origin 上的 serverfix 分支
git push origin --delete serverfix

删除远程 Tag

  • 场景:Tag 命名错误,或者需要统一命名风格.
  • 步骤:在本地删除 Tag,然后 Push 到服务器.
git tag -d some-tag
git push origin :refs/tags/some-tags

Push 到不同的分支

  • 场景:同样的改动出现在本地和远程的不同分支,例如远程分支只用来部署时.
  • 步骤:Push 到远程时,指定本地分支与对应的远程分支.
git push origin branch-with-changes:another-branch

日志与回滚

Git 已经成为当今版本控制工具的主流,而分布式的结构和日志型的存储让 Git 不那么容易理解. 本文以实际的案例,总结了日志相关的操作步骤以及涉及到的 Git 命令.

提交记录

  • 场景:希望查看仓库的中所有提交的信息,比如提交人、提交时间、代码增删、Commit ID 等.
  • 步骤:通过 git statusgit log 可查询这些信息.
# 查看 Git 提交的元信息
git log
# 查看 Git 提交,以及对应的代码增删
git log -p
# 查看 app.js 的 Git 提交日志
git log -p app.js

Git Blame

  • 场景:查看每一行代码的最后改动时间,以及提交人.例如,追溯 app.js 文件中某一行是被谁改坏的.
  • 步骤:通过 git blame 来查询.
git blame app.js

更多参数请查询 git blame –help

检出历史版本

  • 场景:希望将当前项目回到某个历史版本.例如:希望从某个历史版本建立分支时.
  • 步骤:git checkout
# 检出到某个 commit,可通过 git log 得到 Commit ID
git checkout 5304f1bd...b4d4
# 检出到某个分支或 Tag
git checkout gh-pages

原则上讲 Git 历史是不允许更改的,这方面 Git 很像 日志结构的文件系统(Log-Structured File Systems). 但也有办法可以更改日志,例如:寻找并删除 Git 记录中的大文件

从 Git 中移除某些历史 Commit

在 Git 开发中通常会控制主干分支的质量,但有时还是会把错误的代码合入到远程主干. 虽然可以 直接回滚远程分支, 但有时新的代码也已经合入,直接回滚后最近的提交都要重新操作. 那么有没有只移除某些 Commit 的方式呢?可以一次 revert 操作来完成.

一个例子

考虑这个例子,我们提交了 6 个版本,其中 3-4 包含了错误的代码需要被回滚掉. 同时希望不影响到后续的 5-6.

* 982d4f6 (HEAD -> master) version 6
* 54cc9dc version 5
* 551c408 version 4, harttle screwed it up again
* 7e345c9 version 3, harttle screwed it up
* f7742cd version 2
* 6c4db3f version 1

这种情况在团队协作的开发中会很常见:可能是流程或认为原因不小心合入了错误的代码, 也可能是合入一段时间后才发现存在问题. 总之已经存在后续提交,使得直接回滚不太现实.

下面的部分就开始介绍具体操作了,同时我们假设远程分支是受保护的(不允许 Force Push). 思路是从产生一个新的 Commit 撤销之前的错误提交.

git revert

使用 git revert <commit> 可以撤销指定的提交, 要撤销一串提交可以用 <commit1>..<commit2> 语法. 注意这是一个前开后闭区间,即不包括 commit1,但包括 commit2.

git revert --no-commit f7742cd..551c408
git commit -a -m 'This reverts commit 7e345c9 and 551c408'

其中 f7742cd 是 version 2,551c408 是 version 4,这样被移除的是 version 3 和 version 4. 注意 revert 命令会对每个撤销的 commit 进行一次提交,--no-commit 后可以最后一起手动提交.

此时 Git 记录是这样的:

* 8fef80a (HEAD -> master) This reverts commit 7e345c9 and 551c408
* 982d4f6 version 6
* 54cc9dc version 5
* 551c408 version 4, harttle screwed it up again
* 7e345c9 version 3, harttle screwed it up
* f7742cd version 2
* 6c4db3f version 1

现在的 HEAD(8fef80a)就是我们想要的版本,把它 Push 到远程即可.

确认 diff

如果你像不确定是否符合预期,毕竟批量干掉了别人一堆 Commit,可以做一下 diff 来确认. 首先产生 version 4(551c408)与 version 6(982d4f6)的 diff,这些是我们想要保留的:

git diff 551c408..982d4f6

然后再产生 version 2(f7742cd)与当前状态(HEAD)的 diff:

git diff f7742cd..HEAD

如果 version 3, version 4 都被 version 6 撤销的话,上述两个 diff 为空. 可以人工确认一下,或者 grep 掉 description 之后做一次 diff. 下面介绍的另一种方法可以容易地确认 diff.

另外一种方式

类似 安全回滚远程分支, 我们先回到 version 2,让它合并 version 4 同时代码不变, 再合并 version 5, version 6.

# 从 version 2 切分支出来
git checkout -b fixing f7742cd
# 合并 version 4,保持代码不变
git merge -s ours 551c408
# 合并 version 5, version 6
git merge master

上述分支操作可以从 分支管理 一文了解. 至此,fixing 分支已经移除了 version 3 和 version 4 的代码,图示如下:

*   3cb9f8a (HEAD -> v2) Merge branch 'master' into v2
|\
| * 982d4f6 (master) version 6
| * 54cc9dc version 5
* |   c669557 Merge commit '551c408' into v2
|\ \
| |/
| * 551c408 version 4, harttle screwed it up again
| * 7e345c9 version 3, harttle screwed it up
|/
* f7742cd version 2
* 6c4db3f version 1

可以简单 diff 一下来确认效果:

# 第一次 merge 结果与 version 2 的 diff,应为空
git diff f7742cd..c669557
# 第二次 merge 的内容,应包含 version 5 和 version 6 的改动
git diff c669557..3cb9f8a

现在的 HEAD(即 fixing 分支)就是我们想要的版本,可以把它 Push 到远程了. 注意由于现在处于 fixing 分支, 需要 Push 时指定远程分支master.

安全地回滚远程分支

在 Git 中使用 reset 可以让当前分支回滚(reset)到任何一个历史版本, 直接移除那以后的所有提交.但这更改了 Git 的历史,Git 服务通常会禁止这样做. 这便需要一个更安全的方式将代码状态回到历史版本,同时不更改 Git 历史.

如果直接回滚会影响到最近的提交,可以参考 从 Git 历史移除某些 Commit 在回滚的同时保留最近的有效提交.

所谓 保护分支,就是指不允许改写 Git 历史的分支.在 Github 中对应的选项是 Force Pushes,该选项默认处于 Disallow 状态.

找到历史版本

首先,通过 git log 确认你要回滚到的版本的 commit hash. 例如,我们有 4 个版本其中后两个是坏的,要回滚到 version 2,它对应的 commit hash 就是 4a50c9f:

* d4ccf59 (HEAD -> master) version 4 (harttle screwed it up, again)
* 5b7d48e version 3 (harttle screwed it up)
* 4a50c9f version 2
* 491c6e0 version 1

签出历史版本

为了便于操作,我们给这个版本一个分支名,比如 v2:

git checkout -b v2 4a50c9f

现在就已经位于 v2 分支啦,当前的 Git 记录如下,比上一步只是多了一个分支名:

* d4ccf59 (master) version 4 (harttle screwed it up, again)
* 5b7d48e version 3 (harttle screwed it up)
* 4a50c9f (HEAD -> v2) version 2
* 491c6e0 version 1

假合并 master

为了不更改 Git 记录,我们只能生成一个新的 Commit 让代码状态回到 v2. 这意味着必须在 version 4 的基础上进行,思路和手动操作无异. 但我们可以通过一个神奇的合并操作自动完成:

git merge -s ours master

-s <strategy> 用来指定合并策略,ours 是递归合并策略的一种,即直接使用当前分支的代码. -s ours 合并的结果是产生了一个基于 master 的 Commit,但 HEAD 中的代码与合并前完全相同. 从 Git 记录可以看到 version 2 和 version 4 进行了合并:

*   94fa8a7 (HEAD -> v2) Merge branch 'master' into v2
|\
| * d4ccf59 (master) version 4 (harttle screwed it up, again)
| * 5b7d48e version 3 (harttle screwed it up)
|/
* 4a50c9f version 2
* 491c6e0 version 1

但合并中完全采用了 version 2 的代码,即合并前后 diff 为空:

git diff HEAD..4a50c9f

至此我们已经产生了一个 代码状态与历史版本完全一致,但基于 master 的一个 Commit.

push 到远程

在产生可用的 Commit 后,可以从当前分支 v2 直接发往 origin/master:

git push origin master
# 等价于
git push origin v2:master
# 等价于
git push origin HEAD:master

更详细的远程仓库操作可以参考 远程仓库 一文.

与上游仓库保持同步

How to sync a fork with an upstream

检查本地仓库的远程配置

git remote -v
origin https://github.com/YOUR_USERNAME/YOUR_FORK.git (fetch)
origin https://github.com/YOUR_USERNAME/YOUR_FORK.git (push)

添加上游远程仓库

git remote add upstream https://github.com/ORIGINAL_OWNER/ORIGINAL_REPOSITORY.git

git remote -v
origin https://github.com/YOUR_USERNAME/YOUR_FORK.git (fetch)
origin https://github.com/YOUR_USERNAME/YOUR_FORK.git (push)
upstream https://github.com/ORIGINAL_OWNER/ORIGINAL_REPOSITORY.git (fetch)
upstream https://github.com/ORIGINAL_OWNER/ORIGINAL_REPOSITORY.git (push)

同步 fork 仓库

git checkout master
Switched to branch 'master'

git fetch upstream
git merge upstream/master

请注意 mergerebase 的区别


文章作者: 庄引
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 庄引 !
  目录