初始化一个 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_EDITOR
和 EDITOR
等环境变量, 一般在 ~/.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 信息,希望能够补充一下而不是撤销再重新 Commit.
- 步骤 :使用
--amend
参数即可修改上次 Commit. - 文档 :https://git-scm.com/book/zh/v1/Git-%E5%9F%BA%E7%A1%80-%E6%92%A4%E6%B6%88%E6%93%8D%E4%BD%9C
# 下面三条命令只产生一个 Commit
git commit -m 'initial commit'
git add forgotten_file # 添加漏掉的文件
git commit --amend # 补充 Commit 信息
撤销工作区所有改动
- 场景 :希望撤销所有工作区的改动,回到最后一次 commit 的状态.
- 步骤 :
git checkout
和git reset
都可达到目的. - 文档 :https://git-scm.com/docs/git-reset, https://git-scm.com/docs/git-checkout
# 重置工作区的所有改动
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
和 bashrm
的参数类似,基本可通用.
分支管理
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.然而你们修改了同一文件的同一行.
- 步骤 :
- 先解决冲突,打开 Git 提示有冲突的每个文件(其中有 Git 给出的冲突信息),更改文件内容为你想要的最终内容并删除 Git 的冲突信息.
git add
相应的冲突文件.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-x
到master
时需要解决冲突并回归测试. 为了减少维护者的工作,开发者可以将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 fetch
和git 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
删除远程分支
- 场景:不小心把一个分支名 Push 上去了,需要在远程删除一个分支.
- 步骤:直接 push,添加–delete 参数即可.
- 文档:https://git-scm.com/book/zh/v2/Git-%E5%88%86%E6%94%AF-%E8%BF%9C%E7%A8%8B%E5%88%86%E6%94%AF
# 删除远程 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 status
和git 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
请注意 merge
和 rebase
的区别