一、Git 工作流程
① 工作区域
- 四个区:
-
- Workspace:工作区
-
- Index / Stage:暂存区(和 git stash 命令暂存的地方不一样)
-
- Repository:仓库区(或本地仓库)
-
- Remote:远程仓库
- 五种状态:
-
- 未修改 Origin
-
- 已修改 Modified
-
- 已暂存 Staged
-
- 已提交 Committed
-
- 已推送 Pushed
- 工作区:
-
- 程序员进行开发改动的地方,是我们当前看到的,也是最新的。
-
- 平常我们开发就是拷贝远程仓库中的一个分支,基于该分支进行开发。在开发过程中就是对工作区的操作。
- 暂存区:
-
- .git 目录下的 index 文件,暂存区会记录 git add 添加文件的相关信息(文件名、大小、timestamp,…),不保存文件实体,通过 id 指向每个文件实体。可以使用 git status 查看暂存区的状态,暂存区标记了当前工作区中,哪些内容是被 git 管理的。
-
- 当完成某个需求或功能后需要提交到远程仓库,那么第一步就是通过 git add 先提交到暂存区,被 git 管理。
- 本地仓库:
-
- 保存了对象被提交过的各个版本,比起工作区和暂存区的内容,它要更旧一些。
-
- git commit 后同步 index 的目录树到本地仓库,方便从下一步通过 git push 同步本地仓库与远程仓库的同步。
- 远程仓库:
-
- 远程仓库的内容可能被分布在多个地点的处于协作关系的本地仓库修改,因此它可能与本地仓库同步,也可能不同步,但是它的内容是最旧的。
-
- 初始化远程仓库:
$ git init
$ git add README.md
$ git commit -m "first commit"
-
- 关联远程仓库:当使用上述命令来关联远程服务器仓库的时候,本地 .git 目录也是会发生改变的,通过命令查看 .git/config 文件的话,可以看到配置文件中出现了 [remote] 字段:
# 关联远程仓库
$ git remote add origin git@github.com:escapelife/git-demo.git
➜ cat .git/config
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
ignorecase = true
precomposeunicode = true
[remote "origin"]
url = git@github.com:escapelife/git-demo.git
fetch = +refs/heads/*:refs/remotes/origin/*
-
- 推送本地分支:
-
-
- 当执行如下命令,将本地 master 分支推送到远程 origin 仓库的 master 分支之后,登录 GitHub 就可以看到推送的文件及目录内容。
-
-
-
- 推送分支内容的时候,会列举推送的 objects 数量,并将其内容进行压缩,之后推送到远程的 GitHub 仓库,并且创建一个远程的 master 分支(origin 仓库)。
-
# 推送本地分支
$ git push -u origin master
-
- 推送之后,可以发现,本地的 .git 生成一些文件和目录,它们都是什么呢?如下所示,会新增四个目录和两个文件,皆为远程仓库的信息,当通过命令查看 master 这个文件的内容时,会发现其也是一个 commit 对象。此时与本地 master 分支所指向的一致,而其用于表示远程仓库的当前版本,用于和本地进行区别和校对的:
➜ tree .git
├── logs
│ ├── HEAD
│ └── refs
│ ├── heads
│ │ ├── dev
│ │ ├── master
│ │ └── tmp
│ └── remotes # 新增目录
│ └── origin # 新增目录
│ └── master # 新增文件
└── refs
├── heads
│ ├── dev
│ ├── master
│ └── tmp
├── remotes # 新增目录
│ └── origin # 新增目录
│ └── master # 新增文件
└── tags
- 小结
-
- 任何对象都是在工作区中诞生和被修改;
-
- 任何修改都是从进入 index 区才开始被版本控制;
-
- 只有把修改提交到本地仓库,该修改才能在仓库中留下痕迹;
-
- 与协作者分享本地的修改,可以把它们 push 到远程仓库来共享。
-
- 如下所示,直接阐述了四个区域之间的关系:
② 分支管理
- 团队开发中,遵循一个合理、清晰的 Git 使用流程,是非常重要的,否则每个人都提交一堆杂乱无章的 commit,项目很快就会变得难以协调和维护。
- develop 分支 - 开发分支:
-
- develop 分支作为常驻开发分支,用来汇总下个版本的功能提交,在下个版本的 release 分支创建之前不允许提交非下个版本的功能到 develop 分支;
-
- develop 分支内容会自动发布到内网开发环境,确保 develop 分支随时可编译、可运行,上面的功能模块是相对稳定、干净的,随时可以在 develop 上拉 feature 分支进行开发。
- feature 分支 - 功能分支:
-
- 对于新功能开发,应从 develop 上切出一个 feature 分支进行开发,命名格式为 feature/project,其中功能名使用小写单词结合中划线连缀方式,如 feature/update-web-style;
-
- feature 分支进行编译通过并自测通过后,再合并到主干 develop 分支上。
- 补丁分支 - hotfix branch:
-
- 所谓补丁分支,就是当代码已经部署到了正式环境之后发现没有预测到底 Bug 时,通过补丁分支进行修改之后,生成补丁进行修复;
-
- 当然也可以说不使用布丁分支,重新发布新版本也是可以的,但是这样还是不太合理。
- master 分支 - 主分支:
-
- 当 develop 分支开发完成一个版本的时候,测试没有问题之后就可以将其提交之后合并到 master 分支,master 分支内容会自动发布到内网正式环境;
-
- 需要注意的是,一般情况从开发分支合入到主干分支不会有代码冲突的,如果有的话那就是没有按照上述流程严格执行的结果。
- release 分支 - 预发布分支:
-
- 最后就到了发包的最后阶段,将已经在 master 内网正式环境上测试没有问题的版本合入 release 分支,打包给客户部署或者更新线上环境,完成最后的更新操作。
③ 代码规范
- 提交代码其实是有很多讲究的,如果都按照自己的想法随意的提交代码,到最后自己都不知道当时这次提交到底是为了解决什么问题,良好的代码提交习惯即有利于自己之后的审查,也有助于其他人观看,同时利用脚本来提取有价值的信息,如查看人个人的工作量,每日的工作任务等。
- 提交模板 - commit model,我们更多使用的是如下简化版 commit 模板:
# 模板格式
<提交类型>-<项目名称>: <本次代码提交的动机>
# 示例说明
feat-Pandas: update runtime to V1.3.0
- 类型分类 - brand list:
提交类型 | 类型说明 | 使用频率 |
---|---|---|
feat | 增加新功能 | 高 |
fix | 修复 Bug | 高 |
perf | 提高性能的代码 | 高 |
style | 编码规范或风格上的修改,不影响功能 | 高 |
docs | 仅改变项目文档 | 高 |
build | 改变项目构建流程或包依赖 | 低 |
ci | 改变 CI 配置或执行脚本 | 低 |
test | 添加缺失测试或更正现有测试 | 低 |
refactor | 代码更改既不修复错误也不添加功能 | 低 |
④ 使用技巧
- 代码合并 - pull request:
-
- 功能分支合并进 master 分支,必须通过 Pull Request 操作,在 Gitlab 里面叫做 Merge Request。
-
- Pull Request 本质其实就是一种对话机制,你可以在提交的时候附加上有用的信息,并 @ 相关的核心开发者或团队,引起他们的注意,让他们为了的代码把好最后一道关卡,保证你的代码质量。
- 分支保护 - protected branch:
-
- master 分支应该受到保护,不是每个人都可以修改这个分支,以及拥有审批 Pull Request 的权力;
-
- Github 和 Gitlab 都提供“保护分支”的功能。
- 新建问题 - issue:
-
- Issue 用于Bug 追踪和需求管理,建议先新建 Issue,再新建对应的功能分支;
-
- 功能分支总是为了解决一个或多个 Issue,功能分支的名称,可以与 issue 的名字保持一致,并且以 issue 的编号起首,比如 15-change-password;
-
- 开发完成后,在提交说明里面,可以写上"fixes #15" 或者 “closes #67”;
-
- Github 和 gitlab 规定,只要 commit message 里面有下面这些动词+编号,就会关闭对应的 issue。
# 以下词语都可以关闭对应issue
close
closes
closed
fix
fixes
fixed
resolve
resolves
resolved
-
- 这种方式还可以一次关闭多个 issue,或者关闭其他代码库的 issue,格式是 username/repository#issue_number。Pull Request 被接受以后,issue 关闭,原始分支就应该删除,如果以后该 issue 重新打开,新分支可以复用原来的名字。
- 冲突解决 - merge:
-
- Git 有两种合并:一种是 “直进式合并”(fast forward)不生成单独的合并节点;另一种是 “非直进式合并” (none fast-forword)会生成单独节点;
-
- 前者不利于保持 commit 信息的清晰,也不利于以后的回滚,建议总是采用后者(即使用 --no-ff 参数)只要发生合并,就要有一个单独的合并节点。
- 合并提交 - squash:
-
- 为了便于他人阅读你的提交,也便于 cherry-pick 或撤销代码变化,在发起 Pull Request 之前,应该把多个 commit 合并成一个。
-
- 前提是,该分支只有你一个人开发,且没有跟 master 合并过,这可以采用 rebase 命令附带的 squash 操作:
# 第一步:新建分支
$ git checkout master
$ git pull
$ git checkout -b myfeature
# 第二步:提交分支
$ git add .
$ git status
$ git commit -m "this is a test."
# 第三步:与主干同步
$ git fetch origin
$ git rebase origin/master
# 第四步:合并多个commit为一个
# i参数表示互动并打开一个互动界面进行下一步操作
# 会列出当前分支最新的几个commit,越下面越新
# 默认是pick类型,squash和fixup可以用来合并commit
$ git rebase -i origin/master
pick 07c5abd Introduce OpenPGP and teach basic usage
s de9b1eb Fix PostChecker::Post#urls
s 3e7ee36 Hey kids, stop all the highlighting
pick fa20af3 git interactive rebase, squash, amend
# 第六步:推送到远程仓库并发出合并请求
# 要加上force参数是因为rebase以后分支历史改变
$ git push --force origin myfeature
编号 | 参数类型 | 使用说明 |
---|---|---|
1 | p/pick | 正常选中 |
2 | r/reword | 正常选中,并且修改提交信息 |
3 | e/edit | 正常选中,rebase 时会暂停,允许你修改这个 commit |
4 | s/squash | 正常选中,会将当前 commit 与上一个 commit 合并 |
5 | f/fixup | 与 squash 相同,但不会保存当前 commit 的提交信息 |
6 | x/exec | 执行其他 shell 命令 |
二、常用的 Git 命令
① HEAD
- 在掌握具体命令前,先理解下 HEAD:
- HEAD 始终指向当前所处分支的最新的提交点,所处的分支变化了,或者产生了新的提交点,HEAD 就会跟着改变。
② init
- 执行 git init,完成如下命令之后,可以得到下图所示的内容,右侧的就是 git 创建的代码仓库,其中包含了用于版本管理所需要的内容:
$ mkdir git-demo
$ cd git-demo && git init
$ rm -rf .git/hooks/*.sample
# 右边执行
$ watch -n 1 -d find .
- 生成的 .git 目录的结构如下:
➜ tree .git
.git
├── HEAD
├── config
├── description
├── hooks
├── info
│ └── exclude
├── objects
│ ├── info
│ └── pack
└── refs
├── heads
└── tags
- .git/config - 当前代码仓库本地的配置文件:
-
- 本地配置文件(.git/config)和全局配置文件(~/.gitconfig);
-
- 通过执行如下命令,可以将用户配置记录到本地代码仓库的配置文件中去;
-
- git config user.name “demo”;
-
- git config user.email “demo@demo.com”。
➜ cat .git/config
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
ignorecase = true
precomposeunicode = true
[user]
name = demo
email = demo@demo.com
- .git/objects - 当前代码仓库代码的存储位置:
-
- blob 类型;
-
- commit 类型;
-
- tree 类型。
# 均无内容
➜ ll .git/objects
total 0
drwxr-xr-x 2 escape staff 64B Nov 23 20:39 info
drwxr-xr-x 2 escape staff 64B Nov 23 20:39 pack
➜ ll .git/objects/info
➜ ll .git/objects/pack
- .git/info - 当前仓库的排除等信息:
➜ cat ./.git/info/exclude
# git ls-files --others --exclude-from=.git/info/exclude
# Lines that start with '#' are comments.
# For a project mostly in C, the following would be a good set of
# exclude patterns (uncomment them if you want to use them):
# *.[oa]
# *~
- .git/hooks - 当前代码仓库默认钩子脚本:
./.git/hooks/commit-msg.sample
./.git/hooks/pre-rebase.sample
./.git/hooks/pre-commit.sample
./.git/hooks/applypatch-msg.sample
./.git/hooks/fsmonitor-watchman.sample
./.git/hooks/pre-receive.sample
./.git/hooks/prepare-commit-msg.sample
./.git/hooks/post-update.sample
./.git/hooks/pre-merge-commit.sample
./.git/hooks/pre-applypatch.sample
./.git/hooks/pre-push.sample
./.git/hooks/update.sample
- .git/HEAD - 当前代码仓库的分支指针:
➜ cat .git/HEAD
ref: refs/heads/master
- .git/refs - 当前代码仓库的头指针:
# 均无内容
➜ ll .git/refs
total 0
drwxr-xr-x 2 escape staff 64B Nov 23 20:39 heads
drwxr-xr-x 2 escape staff 64B Nov 23 20:39 tags
➜ ll .git/refs/heads
➜ ll .git/refs/tags
- .git/description - 当前代码仓库的描述信息:
➜ cat .git/description
Unnamed repository; edit this file 'description' to name the repository.
③ add
- add 主要实现将工作区修改的内容提交到暂存区,交由 git 管理:
命令 | 描述 |
---|---|
git add | 添加当前目录的所有文件到暂存区 |
git add < dir> | 添加指定目录到暂存区,包括子目录 |
git add < file1> | 添加指定文件到暂存区 |
- 执行完成如下命令之后,可以得到下图所示的内容,可以发现右侧新增了一个文件,但是 git 目录里面的内容丝毫没有变化,这是因为,现在执行的修改默认是放在工作区的,而工作区里面的修改不归 git 目录去管理:
# 左边执行
$ echo "hello git" > helle.txt
$ git status
$ git add hello.txt
# 右边执行
$ watch -n 1 -d find .
- 生成的 8d 目录以及下面的文件,而其名称的由来是因为 git 对其进行了一个叫做 SHA1 的 Hash 算法,用于将文件内容或者字符串变成这么一串加密的字符:
# 查看objects的文件类型
$ git cat-file -t 8d0e41
blob
# 查看objects的文件内容
$ git cat-file -p 8d0e41
hello git
# 查看objects的文件大小
$ git cat-file -s 8d0e41
10
# 拼装起来
blob 10\0hello git
- 执行 git add 命令将文件从工作区添加到暂存区里面,git 会把帮助我们生成一些 git 的对象,它存储的是文件的内容和文件类型并不存储文件名称。
- 为了验证上述的说法,可以添加同样的内容到另一个文件,然后进行提交,来观察 .git 目录的变化。可以看到,右侧的 objects 目录并没有新增目录和文件,这就可以证明,blob 类型的 object 只存储的是文件的内容,如果两个文件的内容一致的话,则只需要存储一个 object 即可。
- 那么 object 为什么没有存储文件名称呢?这里因为 SHA1 的 Hash 算法计算哈希的时候,本身就不包括文件名称,所以取什么名称都是无所谓的。
# 左边执行
$ echo "hello git" > tmp.txt
$ git add tmp.txt
# 右边执行
$ watch -n 1 -d find .
④ commit
- commit 主要实现将暂存区的内容提交到本地仓库,并使得当前分支的 HEAD 向后移动一个提交点:
命令 | 描述 |
---|---|
git commit -m | 提交暂存区到本地仓库,message 代表说明信息 |
git commit -m | 提交暂存区的指定文件到本地仓库 |
git commit --amend -m | 使用一次新的 commit,替代上一次提交 |
- Git 仓库中的提交记录保存的是我们的目录下所有文件的快照,就像是把整个目录复制,然后再粘贴一样,但比复制粘贴优雅许多。Git 希望提交记录尽可能地轻量,因此在每次进行提交时,它并不会盲目地复制整个目录,条件允许的情况下,它会将当前版本与仓库中的上一个版本进行对比,并把所有的差异打包到一起作为一个提交记录,Git 还保存提交的历史记录,这也是为什么大多数提交记录的上面都有父节点的原因。
- 当使用 add 命令将工作区提交到暂存区,而暂存区其实保存的是当前文件的一个状态,其中包括有哪些目录和文件,以及其对应的大小和内容等信息,但是最终是需要将其提交到代码仓库(本地)的,而其命令就是 git commit:
- 而当执行 git commit 命令的时候,究竟都发生了什么呢?可以看到当提交之后,.git 目录中生成了两个信息的 object 对象,其中 logs 和 refs 目录都有新的文件生成。通过如下操作,可以查看到其提交的类型和对应内容:
# 左边执行
$ git commit -m "1st commit"
$ git cat-file -t 6e4a700 # 查看commit对象的类型
$ git cat-file -p 6e4a700 # 查看commit对象的内容
$ git cat-file -t 64d6ef5 # 查看tree对象的类型
$ git cat-file -p 64d6ef5 # 查看tree对象的内容
# 右边执行
$ watch -n 1 -d tree .git
- 当执行 git commit 命令之后,会生成一个 commit 对象和一个 tree 对象,commit 对象内容里面包含一个 tree 对象和相关提交信息,而 tree 对象里面则包含这次提交版本里面的文件状态(文件名称和 blob 对象),这样就知道了这次提交的变动:
- 这次提交之后,处理 objects 目录发生变动之外,还有一些其他的变化,比如 logs 和 refs 的目录有所变化。再查看 refs 目录里面的内容,发现其指向 6e4a70 这个 commit 对象,即当前 master 分支上面最新的提交就是这个 6e4a70。
- 而这个 6e4a70 这个 commit 对象,有一个 HEAD 的指向,就是 .git 目录下的 HEAD 文件,其实质就是一个指针,其永远指向我们当前工作的分支,即这里我们工作在 master 分支上。当切换分支的时候,这个文件的指向也会随机改变的。
# 左边执行
$ cat .git/refs/heads/master
$ cat .git/HEAD
# 右边执行
$ watch -n 1 -d tree .git
- 当再次对 file2.txt 文件的内容进行变更、添加以及提交之后,发现在提交的时候,查看的 commit 对象的内容时,其包含有父节点的 commit 信息:
$ echo "file2.txt" > file2.txt
$ git status
$ git add file2.txt
$ git ls-files -s
$ git cat-file -p 0ac9638
$ git commit -m "2nd commit"
$ git cat-file -p bab53ff
$ git cat-file -p 2f07720
# 右边执行
$ watch -n 1 -d tree .git
- Git 版本的提交流向如下:
- 在 Git 中空文件夹是不算在追踪范围内的,而且添加文件夹并不会增加 object 对象,当查看 index 内容的时候,会发现文件名称是包含相对路径的。
- 而当通过 commit 命令提交之后,会发现生成了三个 object 对象,因为 commit 操作不会生成 blob 对象,所以分别是一个 commit 对象和两个 tree 对象。可以发现,tree 对象里面有包含一个目录的 tree,其里面包含对象文件内容。
- 下图所示的文件状态,可以体会到 git 中版本的概念,即 commit 对象指向一个该版本中的文件目录树的根(tree),然后 tree 在指向 blob 对象(文件)和 tree 对象(目录),这样就可以无限的往复下去形成一个完整的版本:
$ mkdir floder1
$ echo "file3" > floder1/file3.txt
$ git add floder1
$ git ls-files -s
$ git commit -m "3rd commit"
$ git cat-file -p 1711e01
$ git cat-file -p 9ab67f8
$ watch -n 1 -d tree .git
⑤ checkout
- 当执行 checkout 命令的时候,其不光可以切换分支,而且可以切换到指定的 commit 上面,即 HEAD 文件会指向某个 commit 对象。在 Git 里面,将 HEAD 文件没有指向 master 的这个现象称之为 detached HEAD。这里不管 HEAD 文件指向的是分支名称也好,是 commit 对象也罢,其实本质都是一样的,因为分支名称也是指向某个 commit 对象的。
- 执行如下命令,结果如下:
# 左边执行
$ git checkout 6e4a700
$ git log
# 右边执行
$ glo = git log
- 当切换到指定的 commit 的时候,如果需要在对应的 commit 上继续修改代码提交的话,可以使用上文提及的 swtich 命令创建新分支,再进行提交。但是,通常我们都不会这么玩,都会使用 checkout 命令来创建新分支的:
$ git checkout -b tmp
$ git log
- 即使可以这样操作,我们也很少使用,创建的 dev 分支的时候,创建该分支并有了一个新的提交,但是没有合并到 master 分支就直接删除,就无法再“看到”。其实,在 Git 里面任何的操作,比如分支的删除,它只是删除了指向某个特定 commit 的指针引用而已,而那个 commit 本身并不会被删除,即 dev 分支的那个 commit 提交还是在的。
- 那怎么找到这个 commit 呢?
-
- 第一种方法:在 objects 目录下面,自己一个一个看,然后切换过去;
-
- 第二种方法:使用 Git 提供的 git reflog 专用命令来查找,该命令的作用就是用于将之前的所有操作都记录下来。
# 左边执行
$ git reflog
$ git checkout 9fb7a14
$ git checkout -b dev
# 右边执行
$ glo = git log
⑥ branch
- 涉及到协作,自然会涉及到分支。关于分支,大概有四种操作:
-
- 展示分支
-
- 切换分支
-
- 创建分支
-
- 删除分支
- 分支是用来标记特定代码的提交,每一个分支通过 SHA1sum 值来标识,所以对分支的操作是轻量级的,改变的仅仅是 SHA1sum 值:
命令 | 描述 |
---|---|
git branch | 列出所有本地分支 |
git branch -r | 列出所有远程分支 |
git branch -a | 列出所有本地分支和远程分支 |
git branch < branch-name> | 新建一个分支,但依然停留在当前分支 |
git checkout -b < branch-name> | 新建一个分支,并切换到该分支 |
git branch --track < branch> | 新建一个分支,与指定的远程分支建立追踪关系 |
git checkout < branch-name> | 切换到指定分支,并更新工作区 |
git branch -d < branch-name> | 删除分支 |
git push origin --delete < branch-name> | 删除远程分支 |
- 分支就是一个有名字的(master/dev)指向 commit 对象的一个指针,在初始化仓库的时候,会默认分配一个叫做 master 的分支(在最新的版本默认仓库已经变更为 main),而 master 分支就是指向最新的一次提交。为什么需要给分支起名字呢?就是为了方便使用和记忆,可以简单理解为 alias 命令的意义一致:
- 要实现一个分支,最基本需要解决两个问题,第一个就是需要存储每一个分支指向的 commit,第二个问题就是在切换分支的时候帮助我们标识当前分支。在 git 中,它有一个非常特殊的 HEAD 文件,而 HEAD 文件是一个指针,其有一个特性就是总会指向当前分支的最新的一个 commit 对象。而这个 HEAD 文件正好,解决了这两个问题。
- 当从 master 切换分支到 dev 的时候,HEAD 文件也会随即切换,即指向 dev 这个指针:
# 左边执行
$ cat .git/HEAD
$ cat .git/refs/heads/master
$ git cat-file -t 1711e01
# 右边执行
$ glo = git log
- 如下所示,分支切换之后,HEAD 指向发生变动:
# 左边执行
$ git branch
$ git branch dev
$ ll .git/refs/heads
$ cat .git/refs/heads/master
$ cat .git/refs/heads/dev
$ cat .git/HEAD
$ git checkout dev
$ cat .git/HEAD
# 右边执行
$ glo = git log
- 即使删除分支,但是该分支上一些特有的对象并不会被删除的,这些对象其实就是俗称的垃圾对象,还有多次使用 add 命令所产生的也有垃圾对象:
# 左边执行
$ echo "dev" > dev.txt
$ git add dev.txt
$ git commit -m "1st commit from dev branch"
$ git checkout master
$ git branch -d dev
$ git branch -D dev
$ git cat-file -t 861832c
$ git cat-file -p 861832c
$ git cat-file -p 680f6e9
$ git cat-file -p 38f8e88
# 右边执行
$ glo = git log
⑦ fetch
命令 | 描述 |
---|---|
git fetch <远程主机名> | 将某个远程主机的更新全部取回本地 |
git fetch <远程主机名> <分支名> | 取回特定分支的更新 |
git fetch origin master | 取回 origin 主机的 master 分支 |
- 一旦远程主机的版本库有了更新(Git 术语叫做 commit),需要将这些更新取回本地,这时就要用到 git fetch 命令:
$ git fetch <远程主机名>
- 上面命令将某个远程主机的更新,全部取回本地。git fetch 命令通常用来查看其他人的进程,因为它取回的代码对你本地的开发代码没有影响。默认情况下,git fetch 取回所有分支(branch)的更新。如果只想取回特定分支的更新,可以指定分支名。
- 取回更新后,会返回一个 FETCH_HEAD,指的是某个 branch 在服务器上的最新状态,可以在本地通过它查看刚取回的更新信息:
$ git log -p FETCH_HEAD
- 取回 origin 主机的 master 分支:
$ git fetch origin master
- 所取回的更新,在本地主机上要用“远程主机名/分支名”的形式读取,比如 origin 主机的 master,就要用 origin/master 读取。git branch 命令的 -r 选项,可以用来查看远程分支,-a 选项查看所有分支:
$ git branch -r
origin/master
$ git branch -a
* master
remotes/origin/master
- 上面命令表示,本地主机的当前分支是 master,远程分支是 origin/master。取回远程主机的更新以后,可以在它的基础上,使用 git checkout 命令创建一个新的分支。如下所示,在 origin/master 的基础上,创建一个新分支:
$ git checkout -b newBrach origin/master
- 此外,也可以使用 git merge 命令或者 git rebase 命令,在本地分支上合并远程分支:
$ git merge origin/master
# 或者
$ git rebase origin/master
- 上面命令表示在当前分支上,合并 origin/master。
⑧ merge
- merge 命令把不同的分支合并起来,如上图,在实际开放中,我们可能从 master 分支中切出一个分支,然后进行开发完成需求,中间经过 R3、R4、R5 的 commit 记录,最后开发完成需要合入 master 中,这便用到了 merge。
命令 | 描述 |
---|---|
git fetch < remote> | merge 之前先拉一下远程仓库最新代码 |
git merge < branch> | 合并指定分支到当前分支 |
- 一般在 merge 之后,会出现 conflict,需要针对冲突情况,手动解除冲突,主要是因为两个用户修改了同一文件的同一块区域。
-
- 中断合并:
git merge --abort
-
- 撤销合并:撤销合并时采用 git reset/revert 操作。
⑨ rebase
- rebase 又称为衍合,是合并的另外一种选择。在开始阶段,我们处于 new 分支上,执行 git rebase dev,那么 new 分支上新的 commit 都在 master 分支上重演一遍,最后 checkout 切换回到 new 分支。这一点与 merge 是一样的,合并前后所处的分支并没有改变。
- git rebase dev,通俗的解释就是 new 分支想站在 dev 的肩膀上继续下去,rebase 也需要手动解决冲突。
- rebase 与 merge 的区别:
-
- 现在有这样的两个分支:test 和 master,提交如下:
D---E test
/
A---B---C---F master
-
- 在 master 执行 git merge test,然后会得到如下结果:
D--------E
/ \
A---B---C---F----G test, master
-
- 在 master 执行 git rebase test,然后得到如下结果:
A---B---D---E---C'---F' test, master
-
- 可以看到,merge 操作会生成一个新的节点,之前的提交分开显示,而 rebase 操作不会生成新的节点,是将两个分支融合成一个线性的提交。
- 如果想要一个干净的,没有 merge commit 的线性历史树,那么应该选择 git rebase;如果想保留完整的历史记录,并且想要避免重写 commit history 的风险,应该选择使用 git merge。
⑩ reset
- reset 命令把当前分支指向另一个位置,并且相应的变动工作区和暂存区:
命令 | 描述 |
---|---|
git reset —soft < commit> | 只改变提交点,暂存区和工作目录的内容都不改变 |
git reset —mixed < commit> | 改变提交点,同时改变暂存区的内容 |
git reset —hard < commit> | 暂存区、工作区的内容都会被修改到与提交点完全一致的状态 |
git reset --hard HEAD | 让工作区回到上次提交时的状态 |
⑪ revert
- git revert 用一个新提交来消除一个历史提交所做的任何修改:
- revert 与 reset 的区别:
- 说明:
-
- git revert 是用一次新的 commit 来回滚之前的 commit,git reset 是直接删除指定的commit;
-
- 在回滚这一操作上看,效果差不多,但是在日后继续 merge 以前的老版本时有区别。因为 git revert 是用一次逆向的 commit “中和”之前的提交,因此日后合并老的 branch 时,导致这部分改变不会再次出现,减少冲突。但是 git reset 是之间把某些 commit 在某个 branch 上删除,因而和老的branch 再次 merge 时,这些被回滚的 commit 应该还会被引入,产生很多冲突。关于这一点,请参考:git reset revert 回退回滚取消提交返回上一版本;
-
- git reset 是把 HEAD 向后移动了一下,而 git revert 是 HEAD 继续前进,只是新的 commit 的内容和要 revert 的内容正好相反,能够抵消要被 revert 的内容。
⑫ push
- 上传本地仓库分支到远程仓库分支,实现同步:
命令 | 描述 |
---|---|
git push < remote> | 上传本地指定分支到远程仓库 |
git push < remote> --force | 强行推送当前分支到远程仓库,即使有冲突 |
git push < remote> --all | 推送所有分支到远程仓库 |
- git push 命令用于将本地分支的更新,推送到远程主机,它的格式与 git pull 命令相仿:
$ git push <远程主机名> <本地分支名>:<远程分支名>
- 注意,分支推送顺序的写法是<来源地>:<目的地>,所以 git pull 是<远程分支>:<本地分支>,而 git push 是<本地分支>:<远程分支>。如果省略远程分支名,则表示将本地分支推送与之存在“追踪关系”的远程分支(通常两者同名),如果该远程分支不存在,则会被新建。
- 将本地的 master 分支推送到 origin 主机的 master 分支,如果后者不存在,则会被新建:
$ git push origin master
- 如果省略本地分支名,则表示删除指定的远程分支,因为这等同于推送一个空的本地分支到远程分支。删除 origin 主机的 master 分支,如下所示:
$ git push origin :master
# 等同于
$ git push origin --delete master
- 如果当前分支与远程分支之间存在追踪关系,则本地分支和远程分支都可以省略。将当前分支推送到 origin 主机的对应分支:
$ git push origin
- 如果当前分支只有一个追踪分支,那么主机名都可以省略:
$ git push
- 如果当前分支与多个主机存在追踪关系,则可以使用 -u 选项指定一个默认主机,这样后面就可以不加任何参数使用 git push。将本地的 master 分支推送到 origin 主机,同时指定 origin 为默认主机,后面就可以不加任何参数使用 git push:
$ git push -u origin master
- 不带任何参数的 git push,默认只推送当前分支,这叫做 simple 方式。此外,还有一种 matching 方式,会推送所有有对应的远程分支的本地分支。Git 2.0 版本之前,默认采用 matching 方法,现在改为默认采用 simple 方式,如果要修改这个设置,可以采用 git config 命令:
$ git config --global push.default matching
# 或者
$ git config --global push.default simple
- 还有一种情况,就是不管是否存在对应的远程分支,将本地的所有分支都推送到远程主机,这时需要使用 --all 选项,将所有本地分支都推送到 origin 主机:
$ git push --all origin
- 如果远程主机的版本比本地版本更新,推送时 Git 会报错,要求先在本地做 git pull 合并差异,然后再推送到远程主机。这时,如果一定要推送,可以使用 --force 选项。如下所示,使用 --force 选项,结果导致远程主机上更新的版本被覆盖,除非很确定要这样做,否则应该尽量避免使用 --force 选项:
$ git push --force origin
- 最后,git push 不会推送标签(tag),除非使用 --tags 选项:
$ git push origin --tags
⑬ pull
- git pull 命令的作用是,取回远程主机某个分支的更新,再与本地的指定分支合并。它的完整格式稍稍有点复杂:
$ git pull <远程主机名> <远程分支名>:<本地分支名>
- 比如,取回 origin 主机的 next 分支,与本地的 master 分支合并,需要写成下面这样:
$ git pull origin next:master
- 如果远程分支是与当前分支合并,则冒号后面的部分可以省略:
$ git pull origin next
- 上面命令表示,取回 origin/next 分支,再与当前分支合并。实质上,这等同于先做 git fetch,再做 git merge:
$ git fetch origin
$ git merge origin/next
- 在某些场合,Git 会自动在本地分支与远程分支之间,建立一种追踪关系(tracking)。比如,在 git clone 的时候,所有本地分支默认与远程主机的同名分支,建立追踪关系,也就是说,本地的 master 分支自动“追踪” origin/master 分支。
- Git 也允许手动建立追踪关系,如下,指定 master 分支追踪 origin/next 分支:
git branch --set-upstream master origin/next
- 如果当前分支与远程分支存在追踪关系,git pull 就可以省略远程分支名。如下,本地的当前分支自动与对应的 origin 主机"追踪分支"(remote-tracking branch)进行合并:
$ git pull origin
- 如果当前分支只有一个追踪分支,连远程主机名都可以省略。如下,当前分支自动与唯一一个追踪分支进行合并:
$ git pull
- 如果合并需要采用 rebase 模式,可以使用 --rebase 选项。
$ git pull --rebase <远程主机名> <远程分支名>:<本地分支名>
- 如果远程主机删除了某个分支,默认情况下,git pull 不会在拉取远程分支的时候,删除对应的本地分支。这是为了防止,由于其他人操作了远程主机,导致 git pull 不知不觉删除了本地分支。
但是,我们可以改变这个行为,加上参数 -p 就会在本地删除远程已经删除的分支:
$ git pull -p
# 等同于下面的命令
$ git fetch --prune origin
$ git fetch -p
⑭ stash
命令 | 描述 |
---|---|
git stash save “xx” | 执行存储,并添加备注,只执行 git stash 也是可以的,但查找时不方便识别 |
git stash list | 查看 stash 了哪些存储 |
git stash show | 显示做了哪些修改,默认 show 第一个存储,如果要显示其他存储,后面加stash@{$num},比如第二个 git stash show stash@{1} |
git stash show -p | 显示第一个存储的改动,如果想显示其他存储,加上 stash@{$num},比如第二个:git stash show stash@{1} -p |
git stash apply | 应用某个存储,但不会把存储从存储列表中删除,默认使用第一个存储,即 stash@{0},如果要使用其他的,git stash apply stash@{$num} , 比如第二个:git stash apply stash@{1} |
git stash pop | 命令恢复之前缓存的工作目录,将缓存堆栈中的对应 stash 删除,并将对应修改应用到当前的工作目录下,默认为第一个 stash,即 stash@{0},如果要应用并删除其他 stash,命令:git stash pop stash@{$num},比如应用并删除第二个:git stash pop stash@{1} |
git stash drop stash@{$num} | 丢弃 stash@{$num} 存储,从列表中删除这个存储 |
git stash clear | 删除所有缓存的stash |
- 注意:没有在 git 版本控制中的文件,是不能被 git stash 存起来的。如果新增了一个文件,直接执行 git stash 是不会存起来的,可以先执行 git add,再执行 git stash。这个时候,想切分支就再也不会报错有改动未提交了。
- 如果要应用这些 stash,直接使用 git stash apply 或者 git stash pop 就可以再次导出来了。
- 总结:
-
- git add 只是把文件加到 git 版本控制里,并不等于就被 stash 起来了,git add 和 git stash 没有必然的关系,但是执行 git stash 能正确存储的前提是文件必须在 git 版本控制中才行。
-
- 常规 git stash 的一个限制是它会一下暂存所有的文件。有时,只备份某些文件更为方便,让另外一些与代码库保持一致。一个非常有用的技巧,用来备份部分文件:
-
-
- add 不想备份的文件;
-
-
-
- 调用 git stash –keep-index,只会备份那些没有被 add 的文件;
-
-
-
- 调用 git reset 取消已经 add 的文件的备份,继续自己的工作。
-
⑮ clone
- 远程操作的第一步,通常是从远程主机克隆一个版本库,这时就要用到 git clone 命令:
$ git clone <版本库的网址>
- 例如,克隆 jQuery 的版本库:
$ git clone https://github.com/jquery/jquery.git
- 命令会在本地主机生成一个目录,与远程主机的版本库同名。如果要指定不同的目录名,可以将目录名作为 git clone 命令的第二个参数:
$ git clone <版本库的网址> <本地目录名>
- git clone 支持多种协议,除了 HTTP(s) 以外,还支持 SSH、Git、本地文件协议等,如下:
$ git clone http[s]://example.com/path/to/repo.git/
$ git clone ssh://example.com/path/to/repo.git/
$ git clone git://example.com/path/to/repo.git/
$ git clone /opt/git/project.git
$ git clone file:///opt/git/project.git
$ git clone ftp[s]://example.com/path/to/repo.git/
$ git clone rsync://example.com/path/to/repo.git/
- SSH 协议还有另一种写法:
$ git clone [user@]example.com:path/to/repo.git/
- 通常来说,Git 协议下载速度最快,SSH 协议用于需要用户认证的场合。
⑯ remote
- 为了便于管理,Git 要求每个远程主机都必须指定一个主机名,git remote 命令就用于管理主机名。不带选项的时候,git remote 命令列出所有远程主机:
$ git remote
origin
- 使用 -v 选项,可以参看远程主机的网址:
$ git remote -v
origin git@github.com:jquery/jquery.git (fetch)
origin git@github.com:jquery/jquery.git (push)
- 上面命令表示,当前只有一台远程主机,叫做 origin,以及它的网址。克隆版本库的时候,所使用的远程主机自动被 Git 命名为 origin,如果想用其他的主机名,需要用 git clone 命令的 -o 选项指定:
$ git clone -o jQuery https://github.com/jquery/jquery.git
$ git remote
jQuery
- 上面命令表示,克隆的时候,指定远程主机叫做 jQuery。git remote show 命令加上主机名,可以查看该主机的详细信息:
$ git remote show <主机名>
- git remote add 命令用于添加远程主机:
$ git remote add <主机名> <网址>
- git remote rm 命令用于删除远程主机:
$ git remote rm <主机名>
- git remote rename 命令用于远程主机的改名:
$ git remote rename <原主机名> <新主机名>
⑰ 其他命令
命令 | 描述 |
---|---|
git status | 显示有变更的文件 |
git log | 显示当前分支的版本历史 |
git diff | 显示暂存区和工作区的差异 |
git diff HEAD | 显示工作区与当前分支最新 commit 之间的差异 |
git diff --cached | 查看到暂存区和本地仓库之间的差异 |
git diff master | 查看到master和本地仓库之间的差异 |
git cherry-pick < commit> | 选择一个 commit,合并进当前分支 |
三、Git Reset 三种模式
- 使用 Git 时有可能 commit 提交代码后,发现这一次 commit 的内容是有错误的,那么有两种处理方法:
-
- 修改错误内容,再次 commit 一次;
-
- 使用 git reset 命令撤销这一次错误的 commit;
-
- 第一种方法多一条 commit 记录;第二种方法,错误的 commit 不会被保留下来。
- 一句话概括 git reset:
git reset:Reset current HEAD to the specified state // 让 HEAD 指针指向其他的地方
- 例如,有一次 commit 不是很满意,需要回到上一次的 Commit 里面,那么这个时候就需要通过 reset,把 HEAD 指针指向上一次的 commit 的点,它有三种模式:soft、mixed、hard,如下所示:
- 理解了这三个模式,对于使用这个命令很有帮助。在理解这三个模式之前,需要略微知道一点 Git 的基本流程,如下所示:
-
- Working Tree 当前的工作区域;
-
- Index/Stage 暂存区域,和 git stash 命令暂存的地方不一样,使用 git add xx,就可以将 xx 添加近 Stage 里面;
-
- Repository 提交的历史,即使用 git commit 提交后的结果。
- 简单叙述一下把文件存入 Repository 流程:
-
- 刚开始 working tree、index 与 repository(HEAD) 里面的内容都是一致的:
-
- 当 git 管理的文件夹里面的内容出现改动后,此时 working tree 的内容就会跟 index 及 repository(HEAD) 的不一致,而 Git 知道是哪些文件(Tracked File)被改动过,直接将文件状态设置为 modified (Unstaged files):
- 当 git 管理的文件夹里面的内容出现改动后,此时 working tree 的内容就会跟 index 及 repository(HEAD) 的不一致,而 Git 知道是哪些文件(Tracked File)被改动过,直接将文件状态设置为 modified (Unstaged files):
-
- 当执行 git add 后,会将这些改变的文件内容加入 index 中 (Staged files),所以此时 working tree 跟 index 的内容是一致的,但与 repository(HEAD) 内容不一致:
-
- 接着执行 git commit 后,将 Git 索引中所有改变的文件内容提交至 Repository 中,建立出新的 commit 节点(HEAD)后,working tree、index 与 repository(HEAD) 区域的内容又会保持一致:
① reset --hard:重置 stage 区和工作目录
- reset --hard 会在重置 HEAD 和 branch 的同时,重置 stage 区和工作目录里的内容。当在 reset 后面加了 --hard 参数时,stage 区和工作目录里的内容会被完全重置为和 HEAD 的新位置相同的内容,换句话说,就是没有 commit 的修改会被全部擦掉:
- 然后,执行 reset 并附上了 --hard 参数:
git reset --hard
- HEAD 和当前 branch 切到上一条 commit 的同时,工作目录里的新改动和已经 add 到 stage 区的新改动也一起全都消失:
- 可以看到,在 reset --hard 后,所有的改动都被擦掉。
② reset --soft:保留工作目录,并把重置 HEAD 所带来的新的差异放进暂存区
- reset --soft 会在重置 HEAD 和 branch 时,保留工作目录和暂存区中的内容,并把重置 HEAD 所带来的新的差异放进暂存区。
- 那么,什么是“重置 HEAD 所带来的新的差异”?如下所示:
- 由于 HEAD 从 4 移动到了 3,而且在 reset 的过程中工作目录和暂存区的内容没有被清理掉,所以 4 中的改动在 reset 后就也成了工作目录新增的“工作目录和 HEAD 的差异”,这就是上面一段中所说的“重置 HEAD 所带来的差异”。
- 此模式下会保留 working tree 工作目录的内容,不会改变到目前所有的 git 管理的文件夹的内容;也会保留 index 暂存区的内容,让 index 暂存区与 working tree 工作目录的内容是一致的。就只有 repository 中的内容的更变需要与 reset 目标节点一致,因此原始节点与 reset 节点之间的差异变更集合会存在与 index 暂存区中(Staged files),所以可以直接执行 git commit 将 index 暂存区中的内容提交至 repository 中。
- 当我们想合并“当前节点”与“reset 目标节点”之间不具有太大意义的 commit 记录(可能是阶段性地频繁提交)时,可以考虑使用 Soft Reset 来让 commit 演进线图较为清晰点:
-
- 同样的情况下,把修改后的 AppDelegate.h 文件 add 到 stage 区,修改后的 AppDelegate.m 保留在工作目录:
-
- 查看当前最新的 commit 记录:
git show --stat
-
- 这时,执行 git reset --soft HEAD^:
-
- 那么除了 HEAD 和它所指向的 branch1 被移动到 HEAD^ 之外,原先 HEAD 处 commit 的改动(README.md 文件)也会被放进暂存区。
- 这就是 --soft 和 --hard 的区别:–hard 会清空工作目录和暂存区的改动,而 --soft 则会保留工作目录的内容,并把因为保留工作目录内容所带来的新的文件差异放进暂存区。
③ reset --mixed:保留工作目录,并清空暂存区
- reset 如果不加参数,那么默认使用 --mixed 参数,它的行为是:保留工作目录,并且清空暂存区。也就是说,工作目录的修改、暂存区的内容以及由 reset 所导致的新的文件差异,都会被放进工作目录。简而言之,就是把所有差异都混合(mixed)放在工作目录中。
- 以上面的情况为例:
- 工作目录的内容和 --soft 一样会被保留,但和 --soft 的区别在于,它会把暂存区清空,并把原节点和 reset 节点的差异的文件放在工作目录。总而言之,工作目录的修改、暂存区的内容以及由 reset 所导致的新的文件差异,都会被放进工作目录。
④ Reset 三种模式总结
- reset 的本质:移动 HEAD 以及它所指向的 branch。实质上,reset 这个指令虽然可以用来撤销 commit,但它的实质行为并不是撤销,而是移动 HEAD ,并且捎带上 HEAD 所指向的 branch(如果有的话)。也就是说,reset 这个指令的行为其实和它的字面意思“重置”十分相符:它是用来重置 HEAD 以及它所指向的 branch 的位置的。
- 而 reset --hard HEAD^ 之所以起到了撤销 commit 的效果,是因为它把 HEAD 和它所指向的 branch 一起移动到了当前 commit 的父 commit 上,从而起到了“撤销”的效果:
- Git 的历史只能往回看,不能向未来看,所以把 HEAD 和 branch 往回移动,就能起到撤回 commit 的效果。所以同理,reset --hard 不仅可以撤销提交,还可以用来把 HEAD 和 branch 移动到其他的任何地方:
git reset --hard branch2
⑤ reset 三种模式区别和使用场景
- 区别:
-
- –hard:重置位置的同时,直接将 working Tree 工作目录、index 暂存区及 repository 都重置成目标 Reset 节点的内容,所以效果看起来等同于清空暂存区和工作区;
-
- –soft:重置位置的同时,保留 working Tree 工作目录和 index 暂存区的内容,只让 repository 中的内容和 reset 目标节点保持一致,因此原节点和 reset 节点之间的【差异变更集】会放入 index 暂存区中(Staged files)。所以效果看起来就是工作目录的内容不变,暂存区原有的内容也不变,只是原节点和 Reset 节点之间的所有差异都会放到暂存区中。
-
- –mixed(默认):重置位置的同时,只保留 Working Tree 工作目录的内容,但会将 Index 暂存区和 Repository 中的内容更改和 reset 目标节点一致,因此原节点和 Reset 节点之间的【差异变更集】会放入 Working Tree 工作目录中。所以效果看起来就是原节点和 Reset 节点之间的所有差异都会放到工作目录中。
- 使用场景:
-
- –hard:
-
-
- 要放弃目前本地的所有改变时,即去掉所有 add 到暂存区的文件和工作区的文件,可以执行 git reset -hard HEAD 来强制恢复 git 管理的文件夹的内容及状态;
-
-
-
- 真的想抛弃目标节点后的所有 commit(可能觉得目标节点到原节点之间的 commit 提交都是错了,之前所有的 commit 有问题)。
-
-
- –soft:
-
-
- 原节点和 reset 节点之间的【差异变更集】会放入 index 暂存区中(Staged files),所以假如之前工作目录没有改过任何文件,也没 add 到暂存区,那么使用 reset --soft 后,可以直接执行 git commit 将 index 暂存区中的内容提交至 repository 中。为什么要这样呢?
-
-
-
- 这样做的使用场景是:假如想合并「当前节点」与「reset 目标节点」之间不具太大意义的 commit 记录(可能是阶段性地频繁提交,就是开发一个功能的时候,改或者增加一个文件的时候就commit,这样做导致一个完整的功能可能会好多个 commit 点,这时假如需要把这些 commit 整合成一个 commit 的时候)时,可以考虑使用 reset --soft 来让 commit 演进线图较为清晰。总而言之,可以使用 --soft 合并 commit 节点。
-
-
- –mixed(默认):
-
-
- 使用完 reset --mixed 后,可以直接执行 git add 将這些改变果的文件内容加入 index 暂存区中,再执行 git commit,将 Index 暂存区 中的内容提交至 Repository 中,这样一样可以达到合并 commit 节点的效果(与上面 --soft 合并 commit 节点差不多,只是多了 git add 添加到暂存区的操作);
-
-
-
- 移除所有 Index 暂存区中准备要提交的文件(Staged files),可以执行 git reset HEAD 来 Unstage 所有已列入 Index 暂存区的待提交的文件(有时候发现 add 错文件到暂存区,就可以使用命令);
-
-
-
- commit 提交某些错误代码,或者没有必要的文件也被 commit 上去,不想再修改错误再 commit (因为会留下一个错误 commit 点),可以回退到正确的 commit 点上,然后所有原节点和 reset 节点之间差异会返回工作目录,假如有个没必要的文件的话就可以直接删除了,再 commit 上去就 OK 了。
-
四、撤销修改
① 已修改,未暂存
- 如果只是在编辑器里修改了文件,但还没有执行 git add .,这时候文件还在工作区,并没有进入暂存区,可以用:
git checkout .
- 或者
git reset --hard
- 来进行撤销操作。
- 可以看到,在执行完 git checkout . 之后,修改已被撤销,git diff 没有任何内容了。git add . 的反义词是 git checkout .,做完修改之后,如果想向前走一步,让修改进入暂存区,就执行git add .,如果想向后退一步,撤销刚才的修改,就执行 git checkout .。
② 已暂存,未提交
- 已经执行 git add .,但还没有执行 git commit -m “comment”:
git reset // 退回到 git add . 之前,即本地文件处于已修改未暂存状态
git checkout . // 撤销修改
- 或者
git reset --hard
- 可以发现两种情况都可以用同一个命令 git reset --hard 来完成,这个强大的命令,可以一步到位地把你的修改完全恢复到未修改的状态。
③ 已提交,未推送
- 执行 git commit 后,代码已经进入了本地仓库:
git reset --hard origin/master
- 还是这个 git reset --hard 命令,只不过这次多了一个参数 origin/master,正如上文中的,origin/master 代表远程仓库,既然已经污染了本地仓库,那么就从远程仓库把代码取回来。
④ 已推送
- 如果执行了 git add -> git commit -> git push 了,这时代码已经进入远程仓库。如果想恢复的话,只需要先撤销本地修改,再强制 push 到远程仓库:
git reset --hard HEAD^
git push -f
五、同时 push 到多个远程仓库
- 进入项目目录,打开 .git/config 文件(.git 是隐藏目录,需要打开显示隐藏文件):
- 只需要在 [remote “origin”] 下增加一条 url 地址,就可以在 push 时推送到多个远程仓库:
- 如果使用 sourceTree 软件进行 git 操作,那么需要注意在 settings -> 高级下面,多个远程地址对应的验证信息是否正确: