转载请注明本文出自 clevergump 的博客:http://blog.csdn.net/clevergump/article/details/54644801, 谢谢!
前言
由于本文写作时间跨度较长, 而我的业余时间实在有限, 我一直未能抽出一个统一的时间, 将文中所有的截图都统一使用同一个操作系统下的截图, 所以, 本文中的截图, 既有在Windows系统下 MinGW 环境中的黑色背景截图, 又有在 Ubuntu 系统下的红色背景截图, 截图未能统一, 可能会给一些读者造成一定的困惑, 还请见谅. 但不论是哪个操作系统, git 的操作都是相同的, 因为即使 Windows 系统下的 git 操作, 也是在 Cygwin或 MinGW 等类 Unix 系统下进行的, 而 Unix 和 Linux 又可以算作是同源的系统, 所以其实 git 的操作本身是和操作系统无关的. 对于有困惑的读者, 可以在阅读本文的同时, 也可阅读文末参考资料中的相关文章, 辅助理解, 另外一定要多亲自动手操作, 只有多动手操作才能真正理解文中介绍的常用指令的含义.最后提示一下, 如果读者发现文中有些地方本应该使用小写字母, 实际却使用了大写字母, 例如: java源文件的名称扩展名本应该使用小写的 java, 但是实际显示的却是首字母大写的 Java, 那是因为 CSDN 的关键字机制惹的祸, 他们将那些关键词的首字母强行转换成了大写并添加了超链接, 对此我也很无奈. 所以请读者朋友谅解.
本文的目录结构如下:
1. git 配置
(1) ssh key的创建, 并添加public key到git远程仓库
创建并配置 ssh key, 并使用 ssh的方式 clone 到本地, 详细情况可以见相关的Git网站的使用说明, 例如: GitHub, OsChina等各自网站对于 ssh key 配置的使用说明。 配置这个ssh key 并使用 ssh 的方式 (而非 https 的方式) 克隆的代码, 以后在提交代码的时候将无需输入用户名和密码。
这些 ssh key 是保存在当前用户目录下名为 .ssh 的文件夹中,
对于 ubuntu 系统来说, 当前用户目录就是 ~ (例如: /home/user/ ),
对于 windows 系统来说, 当前用户目录如果用 Cygwin或 MinGW 来操作的话, 其实也是 ~. 对于不熟悉 linux 的 windows用户来说, 该路径就是 “我的电脑/系统盘/Users/用户名” 的目录, 例如: C:\Users\abc.
注意重装系统时一定要备份 .ssh文件夹, 否则这些key都会丢失.
在ubuntu系统中, .ssh 文件夹的位置如下:
在windows系统中, .ssh 文件夹的位置如下 (注意根据每个人电脑用户名的不同, 具体路径可能会有差异):
其实, ssh key 是一对key, 包含 public 和 private 两个key, 俗称公钥和私钥. 其中public key是以 .pub 结尾的文件, 而 private key 是与public key同名但名称结尾没有 .pub 的那个文件. 例如: id_rsa 和 id_rsa.pub 就是一对key, 前者是私钥, 后者是公钥. 他们的工作原理其实就是RSA非对称加解密的原理. 也就是说, 在git 远程仓库中只存储 public key 的内容, 然后每次进行代码提交或拉取时, git会进行key的合法性校验, 校验原理就是根据用户提交时所用的账号(例如: 邮箱地址) 来定位到该用户在该git远程仓库中注册的 public key, 然后校验该 public key 与用户本地的 private key是否匹配, 只有确认匹配成功, 才能进行代码的提交或拉取. 所以即使其他人在git远程仓库中, 或者在我们提交代码的网络传输中盗取了我们的 public key, 他也是无法盗用我们的账户进行代码提交或拉取的, 因为他本地的private key 与我们的public key不匹配, 而我们的private key只保存在我们自己电脑里, 不会在网络中传输. 关于RSA非对称加解密的详细原理, 见阮一峰关于RSA算法原理的几篇文章, 如下:
- RSA算法原理(一) - 阮一峰的网络日志 http://www.ruanyifeng.com/blog/2013/06/rsa_algorithm_part_one.html
- RSA算法原理(二) - 阮一峰的网络日志 http://www.ruanyifeng.com/blog/2013/07/rsa_algorithm_part_two.html
(2) git 当前账号信息的查询
git config --list
#2. git 从远程仓库拉取最新代码
git pull origin branchname
在自己的分支拉取最新代码
git pull origin dev
这等效于 git pull origin dev:dev
2. git 代码的提交
(1) 向git版本控制系统中新增修改 (注意: 新增修改 != 新增文件)
git add filename
含义: 将名称为 filename 的文件纳入到git版本控制范围内, 此时还没提交到本地git仓库中.
git add filename1, filename2, filename3, ...
含义: 将名称为 filename1, filename2, filename3, …等的多个文件纳入到git版本控制范围内, 此时还没提交到本地git仓库中.
git add .
含义: 将当前新修改的或新增的所有文件纳入到git版本控制范围内, 此时还没提交到本地git仓库中.
(2) 从git版本控制系统中移除修改 (注意: 移除修改 != 移除文件)
git rm filename
含义: 将名称为 filename 的文件从 git版本控制系统中删除, 同时也会删除本地硬盘上的该文件. (该操作因为某些原因有时可能不成功, 如果要不计后果地强制执行该操作, 可以使用 git rm -f filename 指令)
git rm -f filename
含义: 强制将名称为 filename 的文件从 git版本控制系统中删除, 同时也会删除本地硬盘上的该文件.
git rm -r dirname
含义: 将名称为 dirname 的文件夹中的所有文件都从 git版本控制系统中删除, 同时也会删除本地硬盘上的该文件夹. (该操作因为某些原因可能不成功, 如果要不计后果地强制执行该操作, 可以使用 git rm -rf dirname 指令)
git rm -rf dirname
含义: 不计后果地将名称为 dirname 的文件夹中的所有文件都强制从 git版本控制系统中删除,同时也会删除本地硬盘上的该文件夹
注意:
git rm 不带 --cached 选项时的含义是: 同时删除 git 版本控制中的该文件/文件夹以及本地硬盘上的该文件/文件夹.
如果只希望删除 git 版本控制中的该文件/文件夹, 而不删除本地硬盘上的该文件/文件夹, 可以使用 --cached 选项.
git rm --cached filename
(3) 提交代码到本地git仓库
git commit -m "commit msg"
含义: 将本地的所有修改提交到本地git仓库
(4) 将本地 git仓库中新增的提交推送到远程 git仓库
分多种情况:
[情况1]
如果远程仓库中原先没有某个分支, 而现在需要将本地新创建的某个分支, 推送到远程仓库中, 使得远程仓库中也新增这个分支
指令格式是:
git push 远程仓库的别名(默认是origin) 本地新创建的分支名称 :远程仓库中原有的分支名称或要新增的分支名称
例如: 本地原先只有一个分支, 名为master, 然后在本地新创建了一个名称为 dev 的分支, 我们要将这个 dev 分支推送到远程仓库中, 使远程仓库中也新增一个 dev 分支, 那么我们的操作是:
git push origin dev:dev
然后看下图, 只要出现进度统计或者总delta统计, 就表示成功了, 另外, 我们也可以去远程仓库的网站去查询该远程仓库是否已经成功新增了这个分支。
另外, 查看 GitHub中的该项目, 也会发现确实新增了 dev分支:
[情况2]
如果是想将本地提交的代码, 推送到git远程仓库中已有的某个分支,
指令格式是:
git push 远程仓库的别名(默认是origin) 远程仓库中的分支名称
当然, git push 还有更多其他选项可供添加.
3. git 的分支操作
(1) 在本地创建一个分支
git branch 要在本地创建的分支名称
例如: 本地原先只有一个分支,名为master, 此后在本地创建一个名为 dev 的分支, 然后再次查看本地的分支信息, 会发现有2个分支, 不过当前所在的分支仍然为 master, 看星号标记。
(2) 查看本地的所有分支
git branch
见下图, 注意: 其中使用星号标注的是当前所在的分支
(3) 查看远程的所有分支
git branch -r
(4) 查看本地以及远程的所有分支
git branch -a
例如: 本地已经新建了一个 dev 分支, 本地此时有2个分支(master 和 dev), 但是远程仓库中还没有这个 dev 分支(查看下图的红字)。
而如果我们把本地新建的 dev 分支推送到远程仓库中的 dev 分支, 那么此时再次查看所有的分支情况, 就会发现, 不仅本地会有 dev 分支, 而且远程仓库中也会有 dev 分支了, 远程仓库中的分支数比以前增加了一个(看下图中的红字)。
(5) 在本地切换分支
-
切换到本地已存在的某个分支:
git checkout 要切换到的另一个分支的名称
-
切换到本地还不存在的某个分支(即:先创建一个分支, 然后再切换到该分支):
git checkout -b 要先创建然后再切换到的另一个分支的名称
注意: 如果此处的 “要切换到的另一个分支的名称”和当前所在的分支名称相同, 则实际上不会执行任何切换动作, 因为当前分支就已经是我们想要切换到的目标分支了。
注意: 留意 git checkout branchname 和 git checkout – filename 的区别:
git checkout branchname : 切换分支
git checkout – filename: 版本回退, 但本地仍保留这次的修改而不删除
例如:本地所在的分支原先是 master, 然后切换到本地的 dev 分支, 如下图, 看星号标注:
(6) 删除本地分支
- 非强制删除: git branch -d 要删除的那个本地分支的名称
- 强制删除: git branch -D 要删除的那个本地分支的名称
注意:
- 使用 -d 时, 如果当前所在的分支没有合并要删除的那个分支中的一些代码, 就去执行分支删除的指令, 那么git系统会提示我们的, 并且同时也会告诉我们, 如果我们依然想不合并就删除 (即: 强制删除), 那么可以使用 -D 的属性执行强制删除
- 不能删除当前所在的那个本地分支, 如果要删除当前所在的分支就必须先切换到其他分支然后才能删除
例如: 我的开源项目 SmartNewsReader 中, 下面的删除只是删除了本地的分支 dev, 如果想要让远程仓库中也删除该分支, 需要推送到远程仓库。
(7) 删除远程分支
git push 远程仓库别名 :远程仓库中要删除的那个分支的名称
其实这句指令的完整写法是:
git push 远程仓库别名 本地仓库中的某分支名称 : 远程仓库中要删除的那个分支的名称
其含义是: 将本地的某分支中的提交记录, 推送到远程仓库中的某个分支.
但如果想要删除远程仓库中的某个分支, 那么在编写该指令时, 将 本地仓库中的某分支名称 省略不写, 就可以实现该删除功能. 本地仓库中的某分支名称 省略不写, 就相当于推送本地的一个空分支的代码到远程仓库中的某个分支, 这也就相当于删除远程仓库中的那个分支.
(8) 合并其他分支的代码到当前分支
git merge 被合并的分支名称
备注: 可以使用该命令将本地的任何分支合并到本地的当前分支中, 合并过程中有时需要解决冲突, 当然这个合并只是在本地进行的, 此后只有commit 并 push 后才能让远程仓库也进行合并.
例如:SmartNewsReader 项目中, 本地的 dev 仓库删除了 apk文件夹, 然后切换到本地的 master 分支, 将 dev分支合并进来, 合并完成后只是本地仓库发生了变化, 此后只有推送到远程仓库才会让远程仓库也发生这个合并操作。
① 从 dev 分支切换到 master 分支:
② master 分支合并 dev 分支:
③ commit+push, 将该操作推送到远程仓库:
而合并 dev 之前的提交次数是 36次, 此时再去查看 master 分支的提交次数, 已经变为 37次了。
(9) 合并某次提交的代码到当前分支
git cherry-pick commitId
备注: 该指令常用的场景是, 有多个分支都有相同的新功能开发需求, 或都存在相同的bug需要修复, 我们可以先在一个分支上进行该新功能的开发或bug修复, 然后切换到其他分支, 使用该指令, 将先前提交到前一个分支中的那几次提交, 同步到当前分支中即可, 这样就不用做重复工作了.
4. git 远程仓库名称的查看
(1) git remote
含义: 列出 git 存储的远端仓库的别名。默认情况下,如果你的项目是克隆的(与本地创建一个新的相反), Git 会自动将你的项目克隆自的仓库添加到列表中,并取名“origin”.
(2) git remote -v
含义: 列出 git 存储的远端仓库别名以及每个别名的实际链接地址。默认情况下,如果你的项目是克隆的(与本地创建一个新的相反), Git 会自动将你的项目克隆自的仓库添加到列表中,并取名“origin”.
5. git 将一个分支中的提交同步到另一个分支中
最常见的用途是: 将修复bug的代码同步到主分支中. 根据具体场景, 选择使用 git cherry-pick 或 git merge.
[例1]:
我们在开发中通常至少都有2个分支, 一个是主分支, 专门用于新功能的开发, 另一个分支专门用于修复bug. 假设用于新功能开发的分支(即: 主分支)名称为 dev , 用于修复bug的分支的名称为 bug-fix. 如果我们在 bug-fix分支上进行了一次bug的修复并提交, 该 commit id为 commitA, 我们想将这次bug的修复合并到主分支中, 如果使用 git cherry-pick, 可以按照如下步骤操作:
① 执行 git checkout dev 切换到dev分支
② 执行 git cherry-pick commitA 即可将原先在 bug-fix 分支中提交的修复bug的代码合并到主分支中, 这样主分支中的该bug也得到了修复.
如果使用 git merge, 可以按照如下步骤操作:
① 执行 git checkout dev 切换到dev分支
② 执行 git merge bug-fix 将bug-fix分支上所有的提交都合并到当前所在分支(即: dev分支)中.
[例2]:
我们在开发中, 如果需要为不同的客户分别定制开发不同的功能, 例如: 为客户A开发他所需功能的分支为 devA, 为客户B开发他所需功能的分支为 devB, 由于A B两客户各自的需求不同, 所以分支 devA 和 devB 的基础架构代码是相同的 , 但在某些具体页面和实现上却有所不同. 如果发现基础架构的某处存在一个bug, 那么devA 和 devB 都存在该bug. 此时只需先为其中一个客户的代码进行bug修复, 然后再同步这些修复的代码到另一个客户的分支即可. 假如先为客户A进行该bug的修复, 从 devA 分支创建出一个专用于为客户A进行本次bug修复的新分支 bug-fixA, 修复后进行了一次提交, 提交的 commit id 为 commitBugfixA. 然后使用 git merge 或 git cherry-pick 将此次修复的代码合并到客户A的主分支 devA 中. 此后需要再将该修复的代码合并到客户B的主分支devB中, 那么此时只能使用 git cherry-pick, 而不能使用 git merge, 因为客户A B的需求不同, bug-fixA 分支同时包含了本次bug修复的代码, 以及客户A所特有的功能代码, 而我们此时仅仅需要将bug修复的代码合并到devB分支, 客户A所特有的功能代码却不要合并到 devB分支中, 所以就不能使用 git merge bug-fixA 了.
操作步骤是:
① 执行 git checkout devB 切换到devB分支
② 执行 git cherry-pick commitBugfixA 将 bug-fixA 分支中, 用于修复此次bug的代码, 合并到devB主分支中, 其他代码不合并进来.
6. git查看差异
(1) 对比本地的某文件与远程仓库中的该文件的差异
git diff filename
(2) 查看两个分支之间的不同之处, 或者查看自某个tag开始之后的不同之处
git diff [--stat] branchA...branchB
更多详情请见 http://gitref.org/zh/inspect/#diff 加与不加 --stat, 查询出来的结果是有区别的。
比如: 要查看分支 master 和 dev 之间的差异, 可以输入:git diff --stat master…dev
从master到dev, 变化的内容是: apk/newsreader_v0.1.0.apk 文件的大小从 1391922 bytes 变为 0 bytes. 也就是说, master 分支有该 apk文件, 而 dev 分支没有.
如果不加 --stat, 即:输入的是 git diff master…dev, 那么查询结果如下:
只会显示删除了该文件, 而不会显示变化的文件先后的字节大小等信息.
7. git 查看某次提交所涉及到的文件
git diff-tree -r commitId
备注: 请将上述指令中的 commitId 替换为想要查看的那次提交的 id 号.
当然, 我们只想查看文件的列表, 而不想查看其他信息, 那么可以添加一些选项, 例如:
- –no-commit-id 不显示要查看的这次提交的id号
- –name-status 仅显示发生变化的文件名称, 以及每个文件发生的具体变化的动作(例如:是新增, 删除, 或者修改等)
添加这些选项后的指令如下:
git diff-tree -r --no-commit-id --name-status commitId
例如: 以 JavaScript 的著名书籍《You-Dont-Know-JS》(《你不知道的JavaScript》) 的git 库为例, 书的地址是: https://github.com/getify/You-Dont-Know-JS, 我们先找到该书的前3次提交记录:
我们查看该书的最早两次提交分别涉及到了哪些文件 .
第2次提交相对于第1次提交, 以及第3次提交相对于第2次提交, 各自新增或删除或修改了如下文件:
如果只想显示发生变化的文件名称, 以及每个文件发生的具体变化的动作, 那么我们再来看上述两次对比:
8. git 暂存区(stash)的相关操作
先将当前正在编辑的代码保存暂存区, 然后从网上拉取最新代码, 最后再从暂存区中恢复原先正在编辑的代码, 操作步骤如下:
① 保存当前的修改到暂存区
git stash save [<msg>]
其中msg是对这次保存的一种备注, 可以用来提示自己, 本次保存的代码的功能或作用等
② 查看暂存区中保存的所有记录
git stash list
③ 从暂存区中取出与给定名称对应的那份代码, 如果此处不写名称, 则取出的是最近保存的一份代码.
git stash pop [stashId]
stashId通常的格式是 stash@{0}, stash@{1}, stash@{2}等, 分别表示最近一次保存, 倒数第二次保存, 倒数第三次保存等依此类推.
④ 删除暂存区中保存的某份代码或全部代码
-
删除暂存区中某一个stashId对应的那份代码:
git stash drop [stashId]
如果不写stashId, 则默认删除的是 stashId 为 stash@{0}的那份代码, 也就是最近一次保存的那份代码.
-
删除暂存区中所有的代码:
git stash clear
9. 为某次提交打标签(tag), 以作为一个里程碑, 便于今后代码的快速回退.
通常用于版本发布, 或者提交测试结构进行测试, 或者交付给客户某个产品等场合, 都需要为最后一次提交的代码打标签. 在这些重要的时间节点为最后一次提交的代码打上特定的标签, 以便今后万一需要对该版本进行bug修复或者需要在该版本基础上进行新功能开发时, 就可以根据该tag来快速回退代码.
(1) 在本地仓库为某次提交打标签
git tag [-m msg] <tagName>
(2) 将本地仓库中打的标签推送到远程仓库
-
push单个tag到远程仓库
git push origin [tagname]
例如:
git push origin v1.0 #将本地v1.0的tag推送到远端服务器
-
push所有tag到远程仓库
git push [origin] --tags
例如:
git push --tags 或 git push origin --tags
具体可参考下面这些文章:
http://www.liaoxuefeng.com/wiki/0013739516305929606dd18361248578c67b8067c8c017b000/001376951758572072ce1dc172b4178b910d31bc7521ee4000
http://blog.csdn.net/hustpzb/article/details/8056518
10. 查看git提交的历史记录
(1) 显示所有提交记录的详细信息(即: 每次提交的信息都会分多行显示)
git log
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4En6g8ap-1571036139069)(https://img-blog.csdn.net/20170121115629905?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvY2xldmVyR3VtcA==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)]
(2) 显示所有提交记录, 但每次提交的记录仅用一行显示
git log --pretty=oneline
或
git log --oneline
(3) 只显示最近N次提交的详细信息(N是正整数)
git log -N (N是正整数)
(4) 只显示某个日期当天及以前, 某个日期当天及以后的提交记录
只显示某个日期当天及以前的提交记录:
git log --before=日期(日期格式:yyyy-MM-dd)
例如:要显示2017年1月1日当天及以前的提交记录:
git log --before=2017-01-01
只显示某个日期当天及以后的提交记录:
git log --after=日期(日期格式:yyyy-MM-dd)
例如:要显示2017年1月1日当天及以后的提交记录:
git log --after=2017-01-01
只显示某个日期区间内(即: 某个日期当天及以前, 另一个日期当天及以后)的提交记录:
git log --before=日期(日期格式:yyyy-MM-dd) --after=另一个日期(日期格式:yyyy-MM-dd)
例如:
git log --after=2017-01-01 --before=2017-01-03
(5)倒序查看提交记录
git log --reverse
(6) 查看提交及回退 (commit & reset) 的记录
git reflog
例如:
(7) 查看提交记录, 并同时显示标签(tag)信息.
git log --decorate
例如:
(8) 其他
git log 更多的细节, 请看这篇文章: http://www.cnblogs.com/irocker/p/advanced-git-log.html
11. git代码回退
(1) 版本回退到某一次提交后的情况, 当前本地新修改的代码在本地不会删除, 只会从版本控制中检出
git reset commitId
(2) 版本回退到某一次提交后的情况, 当前本地新修改的代码会被删除
git reset --hard commitId
备注: git reset [–hard] HEAD^ 是回退到最近一次提交的上一次提交. 因为最近一次提交是用 HEAD 表示.
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZYd8Ja70-1571036139071)(https://img-blog.csdn.net/20170121120733182?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvY2xldmVyR3VtcA==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast)]
更详细的内容, 请参考:
http://www.liaoxuefeng.com/wiki/0013739516305929606dd18361248578c67b8067c8c017b000/0013744142037508cf42e51debf49668810645e02887691000
12. 查看最新一次提交的 id
git rev-parse --short HEAD
此时显示的是 7位字符, 如果想修改显示的字符数, 可以使用 --short=Number HEAD
可以在 gradle 打包时, 用于记录本次打包所对应的提交 id 到打包日志文件里, 并自动保存该文件, 实现自动化.
/**
* 读取Git日志
* @return
*/
def getGitVersion() {
return 'git rev-parse --short HEAD'.execute().text.trim()
}
参考资料:
通过Gradle自动生成通过Git提交的version http://blog.csdn.net/baidu_nod/article/details/38147371
13. 代码冲突的解决
当执行 git pull origin 远程分支名 以后, 提示冲突时, 注意看下图, 只有使用大写的单词 **“CONFLICT”**的行, 才是发生冲突的地方.因为单词大写表示要引起重视的地方, 因此也只有冲突无法自动合并的地方才会要求你要引起重视. 如下图, 这是在执行 git pull 拉取服务器代码时, 提示有冲突的情况:
从图中可以看出, 有两个文件发生了冲突: strings.xml 和 SendGiftPopwindow.java, 因为这两个文件的前边都提示了 “CONFLICT(content)” 的字样.
下面以Android Studio为例, 来讲解Android Studio中解决代码冲突的方法:
上图中, 我们对先前在终端中提示"CONFLICT" 的某个文件点右键, 选择Git, 再选择"Resolve Conflicts…", 即可打开一个代码比对窗口, 该窗口分为左中右三栏, 左边和右边分别是当前本地的代码及服务器上的代码, 中间是最终合并后的代码, 我们需要决定每一次冲突的代码到底是使用我们本地的代码还是服务器上的代码, 待该文件中的所有冲突都解决后, 即可确定并关闭那个窗口即可. 这里暂时缺少该比对窗口的相关截图, 今后再做补充完善.
14. 案例演示
(1) git 代码回退案例演示
[情景一]
如果需要恢复 “已经修改但还未add 到本地git仓库 (即: 还未执行 git add 命令, 使用 git status 查看状态时, 使用红色文字显示出当前的这些修改) 的代码”,
可以使用如下指令 (注意: 在我们使用 git status 查看代码状态时, git 会自动提示我们该指令的具体用法的, 无需刻意记忆)
git checkout -- <file>...
例如:
初始状态是这样的:
hello.java 的内容为:
现在修改 hello.java 文件, 添加一些内容(在下图中, 添加了行号为 2~3 的两行内容), 但还未执行 git add 命令, 修改后该文件的内容为:
此时用 git status 查看状态, 会发现, 系统用红色文字提示我们 modified: hello.java
同时会有上图中用黄线标注的提示: git checkout – … to discard changes in working directory 这句提示的意思就是, 当前的修改还未添加到本地git仓库, 如果你想还原到本次修改前的代码状态, 就可以执行 git checkout – … 这句指令, 执行这句指令, 就可以抛弃掉当前这些新增的修改, 从而使代码恢复到先前的状态.
所以: 根据以上提示可知, 在这种情况下的代码恢复, 可以使用 git checkout – … 指令.
执行完该指令后, 我们期望的结果是, Hello.java 文件的内容被还原为只有一句话 “Hello World”, 那么情况是不是这样呢? 我们打开该文件看一看吧:
果然, 确实就是我们所期望的结果.
[情景二]
如果是要恢复一个已经 add 但未commit 的文件, 例如: 我们先创建一个文件 Calculator.java, 里面有一个计算加法的方法 add(),代码如下 (请忽略代码的严谨性, 例如: 两个int相加后的结果可能会超出int的范围):
public class Calculator {
public static int add(int a, int b) {
return a+b;
}
}
将该文件提交到git仓库, 如下图所示:
查看此次提交的 commitId, 从下图可知, commitId 为 939d7fcc0186c22a41ee20111f413b8cdee42720
如果此时又添加了减法的方法 minus(), 代码如下:
public class Calculator {
public static int add(int a, int b) {
return a+b;
}
public static int minus(int a, int b) {
return a-b;
}
}
并且已经使用 git add 将这次修改加入到版本控制中, 但还未提交(即: 还未执行 git commit 操作), 如下图:
如果此时在该文件中删除 minus方法, 这样该文件的内容就和上次提交后的内容完全一致了, 但是使用 git status 查看却会发现, 系统依然会提示该文件有修改. 那么我们怎样才能让代码和 git 状态同时回退到先前记录的 commitId 为 939d7fcc0186c22a41ee20111f413b8cdee42720 的那次提交呢?
有两种方法:
[方法一]
这里为了能有更直观的印象, 先直接上图吧, 图中已经列出了操作步骤:
从上图我们可知, 一共需要执行两个步骤, 下面分别介绍:
① 先将 git 版本控制系统回退到上一次提交, 使用如下指令:
git reset HEAD Calculator.java
或者
git reset 939d7fcc0186c22a41ee20111f413b8cdee42720
此时, Calculator.java 文件就会由原先的未修改, 变为已修改状态, 即: 在 Windows 系统的 MinGW环境中, 该文件的git 状态使用颜色来表示, 就是从绿色字体变为红色字体. 因为先前是执行了 git add 指令, 这相当于将修改添加到了 git 系统中, 所以是用绿色字体来显示该文件名. 而刚才我们执行了上述 git reset 指令, git 最新的状态就从先前的 git add . 变为了 commitId 为 939d7fcc0186c22a41ee20111f413b8cdee42720 的那次提交. 所以此时该文件又变为了 已修改的状态, 所以 Calculator.java 文件名称又用红色来表示了.
② 再将此时 Calculator.java 的内容撤销. 使用如下指令:
git checkout -- Calculator.java
执行完该指令后, 再查看 git log, 确实回到了先前那次提交. 并且用 cat 查看该文件内容, 也确实是只有 add()这一个方法了.
[方法二]
在介绍方法二之前, 我希望大家先回顾一下前边介绍的 git reset 指令带 --hard 选项与不带该选项时的区别, 这部分知识的回顾请看 11. git代码回退. 回顾完以后, 再来思考这里的问题, 我们是否意识到, 我们在**[方法一]**中的两个步骤, 完全可以简化成一个步骤呢? 直接使用如下带有 --hard 的指令即可:
git reset --hard 939d7fcc0186c22a41ee20111f413b8cdee42720
如果你不相信的话, 我们再来还原出增加了 minus() 方法后的环境, 如下图所示:
在上图中, 我们先使用 git status 查看git状态, 发现 Calculator.java文件被修改了. 那么修改了哪些地方呢? 使用 git diff Calculator.java 查看该文件的修改内容, 发现是新增了 minus() 方法, 然后我们将这次修改执行 git add, 就可以制造出我们最初的场景, 如下图所示:
此时我们想删除minus() 方法 ,同时还能让 git 最后一次提交回退到添加了 add()方法后的那次提交, 我们尝试使用带有 --hard 的 git reset 指令吧, 看看只用这一个步骤, 能否实现**[方法一]**中通过两个步骤才能实现的效果呢? 如下图所示:
上图中, 我们执行完该指令后, 使用 cat 指令查看该文件的内容, 发现确实只有 add() 方法了, 另外再次查看 git log, 也发现当前最新的提交就是我们先前记录的 939d7fcc0186c22a41ee20111f413b8cdee42720, 所以只用这一个步骤就可以实现我们想要的效果了.
[情景三]
如果要将已经执行了提交(git commit)后的内容, 还原到先前的某一次提交 (commit), 可以直接执行如下指令:
git reset --hard 要回退到的那次commit的commitId
由于这和**[情景二]中的[方法二]**操作完全相同, 所以此处不赘述.
[情景四]
回退已经提交到服务器上的提交记录。
指令:
git revert -n commit
git revert -n commitA…commitB
其中, -n 表示 --no-edit, 即不需要立即编辑这次新的commit msg. 可以查看 man page
例如:要删除已经提交到服务器上的最后一次提交记录,可以执行 git revert -n HEAD.
详细说明和示例待补充。
15. 常见错误解决方案
-
git merge 后 push 到 Gerrit 失败,提示 no new changes
参考资料: http://www.voidcn.com/article/p-wfwurgjx-gm.html[问题]
使用 git merge 在本地执行分支合并操作,然后想 push 到 gerrit 上评审入库,可是在 push 时,提示:! [remote rejected] HEAD -> refs/for/dev (no new changes)
[分析]
no new changes 的意思,是说,这个合并,是个线性的合并。而合并的那些历史的 commit 节点,在 gerrit 上都已经评审过了,都是已有的 change 单,所以 gerrit 认为没有新的提交,就不让你提交评审。
[解决方法]
方法1: 在 git merge 的时候,加上 --no-ff 参数,是为了让它生成一个新的 commit,这样就可以提交了(不过生成的 gerrit change 是看不到改动信息的)
方法2: 不经过 gerrit,直接 push 入远程库。(不推荐)例如:
代码有两个分支master 和 dev, 分别表示主分支和开发分支, 先在 dev分支上开发一个新功能, 开发完成后将dev分支的代码合并到 master分支git merge dev // 在master分支执行
合并时可能要解决一系列冲突, 解决完成后, 编译代码运行起来, 测试 master分支代码的运行效果是否有问题, 在测试过程中发现了一个bug 并且这个bug是由刚才从dev分支合入的代码造成的, 然后就在当前的 master分支上修复该 bug (备注: 此时dev 分支还没有修复), 修复完成后, 继续测试, 直到没有再发现明显的bug了, 此时就把刚才的 merge 以及 bugfix 的代码一起作为一次新的 commit 去提交, 并将master分支的这次特殊的提交(它是一次 merge性质的提交) push 到对应的远程分支, 此过程没有遇到问题. 此后, 我们又想将刚才在 master分支上修复这个bug的代码直接迁移到 dev 分支上, 于是我们切换到 dev 分支后, 执行分支合并操作:
git checkout dev git merge master // 在dev分支执行
分支合并没遇到问题, 但是此后将 dev 分支上的代码 push 到远程分支时报错了, 报错截图如下:
原因是:
假如我们有如下约定:将从dev分支合并到 master分支的操作, 称为正向合并 将从master 分支合并到 dev分支的操作, 称为反向合并
我们先执行一次正向合并, 将开发的新功能同步到master分支, 然而在合并过程中我们又新增了其他代码(例如: bugfix的代码), 但是 git 并不知道我们新增了代码, 它只认为是一次正向合并, 然后我们为了把 master分支上的新增代码(例如: 刚才进行bugfix的代码) 也同步给dev分支, 我们又执行了一次反向合并, 此时 git 就会认为我们执行反向合并时两个分支代码是相同的, 没有新的修改(no new changes), 所以拒绝我们 push 这个反向合并, 所以要想解决这个问题, 我们就必须要让git 认为我们的这次反向合并的内容与先前的正向合并的内容不相同, 我们可以在执行反向合并时, 为 git merge 指令使用 --no-ff 选项, 强制为此次反向合并新增一次 commit, 这样正向和反向的 commit 信息就存在差异了, 所以git 就允许我们将反向合并 push 到远程分支了.
–no-ff 的含义是: no fast forward.--no-ff Create a merge commit even when the merge resolves as a fast-forward. This is the default behaviour when merging an annotated (and possibly signed) tag that is not stored in its natural place in refs/tags/ hierarchy.
参考资料:
- http://gitref.org/zh/creating/
- http://www.liaoxuefeng.com/wiki/0013739516305929606dd18361248578c67b8067c8c017b000/001375840202368c74be33fbd884e71b570f2cc3c0d1dcf000
- http://www.yiibai.com/git/
- http://zengrong.net/post/1746.htm
- http://www.cnblogs.com/springbarley/archive/2012/11/03/2752984.html
- https://git-scm.com/docs
- https://github.com/geeeeeeeeek/git-recipes/wiki