一、基本概念:
注:对于git的分布式概念及其优点,不重复说明,自己百度或谷歌。本文中涉及到指令前面有$的,在cmd中运行时,不要加上,$代表当前用户是root。
三种文件状态
- 已暂存:对某个文件使用了git add指令,就会进入暂存区
- 已提交:对某个文件使用了git commit指令,就会变为已提交,进入Git仓库。
- 已修改:对某个已提交的文件做了修改,但是未使用git add指令,进入工作目录。
三个工作区域:
- Git 仓库
是 Git 用来保存项目的元数据和对象数据库的地方。 这是 Git 中最重要的部分,从其它计算机克隆仓库时,拷贝的就是这里的数据。
- 工作目录
从 Git 仓库的压缩数据库中,将项目的某个版本独立提取出来,放在磁盘上供你使用或修改。
- 暂存区域
是一个文件,保存了下次将提交的文件列表信息,一般在 Git 仓库目录中。 有时候也被称作“索引”,不过一般说法还是叫暂存区域。
二、常用指令
git clone :克隆仓库
- 克隆远程仓库代码,会克隆项目的所有版本的代码,全部保存在.git文件夹中,并且master代码将默认存在于工作目录中。
示例:git clone https://github.com/libgit2/libgit2
git add :添加文件到暂存区
(1)可以用它开始跟踪新文件,
(2)或者把已跟踪的文件放到暂存区,
(3)还能用于合并时把有冲突的文件标记为已解决状态等。
(4)使用文件或目录的路径作为参数;如果参数是目录的路径,该命令将递归地跟踪该目录下的所有文件。
示例:git add README CONTRIBUTING.md;这里提交了两个文件,注意空格。
git status:列出文件的状态
- 假设有个README文件已被跟踪,CONTRIBUTING.md已被修改
运行git status,显示如下
On branch master
Initial commit
Changes to be committed:
(use "git rm --cached <file>..." to unstage)
new file: CONTRIBUTING.md
new file: README
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: CONTRIBUTING.md
- On branch master:指的是现在分支名是 “master”,这是默认的分支名。
- Changes to be committed:表明是文件是
已暂存状态
- Untracked files:未跟踪文件,即未git add
- Changes not staged for commit:说明已跟踪文件的内容发生了变化,但还没有放到暂存区,要使用git add放入暂存区。
- 可以看到CONTRIBUTING.md 现在既存在于暂存区,又存在于git仓库,这是什么情况?好吧,其实你只要明白一件事,那就是你运行git commint命令的时候,提交的是最后一次git add过的文件,你git add之后可能还会再次修改文件且不立即提交,但是这个再次修改的文件假如不再次运行git add命令,将不会把再次修改过的东西提交到git仓库,只会提交你最后一次git add时的文件状态,这下明白了吧。
git status -s:列出文件的简要状态信息
M README
MM Rakefile
A lib/git.rb
M lib/simplegit.rb
?? LICENSE.txt
- 右边M:修改了的文件,未git add,处于工作区;注意倒数第二行还有个左边M
- 左边M:修改了的文件,已git add,处于暂存区;
- MM:git add了之后,又去修改,就是之前提到的处于两种状态的文件,处于暂存区和工作区
- A:第一次git add,但是还没有git commit的文件
- ??:从未git add过的文件
.gitignore:文件忽略列表
一般我们总会有些文件无需纳入 Git 的管理,也不希望它们总出现在未跟踪文件列表。通常都是些自动生成的文件,比如日志文件,或者编译过程中创建的临时文件等。
示例:
$ cat .gitignore
*.[oa]
*~
第一行*.[oa]告诉 Git 忽略所有以 .o 或 .a 结尾的文件。
第二行告诉 Git 忽略所有以波浪符(~)结尾的文件,许多文本编辑软件(比如 Emacs)都用这样的文件名保存副本。
- 格式规范
所有空行或者以 # 开头的行都会被 Git 忽略。
可以使用标准的 glob 模式匹配。
匹配模式可以以(/)开头防止递归。
匹配模式可以以(/)结尾指定目录。
要忽略指定模式以外的文件或目录,可以在模式前加上惊叹号(!)取反。
- glob 模式是指 shell 所使用的简化了的正则表达式
- 星号(*)匹配零个或多个任意字符;
- [oa] 匹配oa中任意一个字符
- 问号(?)只匹配一个任意字符
- [0-9] 表示匹配所有 0 到 9 的任意一个数字
- 两个星号(*) 表示匹配任意中间目录,比如a/**/z 可以匹配 a/z, a/b/z 或 a/b/c/z等。
git diff:差异比较
- 不加参数:查看尚未暂存的文件更新了哪些部分,直接输入 git diff
- file:查看尚未暂存且指定的文件,更新了哪些部分
- --cached:查看已暂存的,即将commit的文件和上次commit的文件的差异。
- ffd98b291e0caa6c33575c1ef465eae661ce40c9 b8e7b00c02b95b320f14b625663fdecf2d63e74c 查看某两个版本之间的差异
- ffd98b291e0caa6c33575c1ef465eae661ce40c9:filename b8e7b00c02b95b320f14b625663fdecf2d63e74c:filename 查看某两个版本的某个文件之间的差异
git commit:提交
- -m :表示提交到仓库,并且添加注释
- -a :跳过暂存区域,不git add,直接提交。
移除文件
rm PROJECTS.md * 删除工作目录中的文件
rm -f PROJECTS.md *删除暂存区域的文件
git rm PROJECTS.md *提交删除
git rm --cached README * 把文件从 Git 仓库中删除(亦即从暂存区域移除),但仍然希望保留在当前工作目录中。适用于不小心git add了。
git rm log/\*.log: 删除log文件夹下的所有log结尾的文件
git mv:重命名
git mv相当于以下三条指令:
$ mv README.md README
$ git rm README.md
$ git add README
git log:查看提交历史
git clone https://github.com/schacon/simplegit-progit
进入到项目中执行: git log
- git log -p -2:p表示显示每次提交的内容差异。用 -2 来仅显示最近两次提交
- git log --stat:打印每次提交的简略的统计信息
- git log --pretty=format:"%h - %an, %ar : %s":按照一定的格式打印
- git log --pretty=format:"%h %s" --graph:更加优雅的打印
- git log --since=2.weeks:列出最近两周提交的内容
- git log -Sfunction_name:列出包含名为function_name提交内容的历史
git commit --amend:追加提交
假如你提交后发现忘记了暂存某些需要的修改,可以像下面这样操作:
$ git commit -m 'initial commit'
$ git add forgotten_file
$ git commit --amend
最终你只会有一个提交 - 第二次提交将代替第一次提交的结果。
这个命令也可以修改提交信息。
git reset:从暂存区撤回到工作目录
例如,你已经修改了两个文件并且想要将它们作为两次独立的修改提交,但是却意外地输入了 git add * 暂存了它们两个。如何只取消暂存两个中的一个呢?
$ git add *
$ git status
On branch master
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
renamed: README.md -> README
modified: CONTRIBUTING.md
在 “Changes to be committed” 文字正下方,提示使用 git reset HEAD <file>... 来取消暂存。所以,我们可以这样来取消暂存 CONTRIBUTING.md 文件:
$ git reset HEAD CONTRIBUTING.md
Unstaged changes after reset:
M CONTRIBUTING.md
$ git status
On branch master
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
renamed: README.md -> README
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: CONTRIBUTING.md
CONTRIBUTING.md 文件已经是修改未暂存的状态了。
git checkout:完全撤销
如果你并不想保留对 CONTRIBUTING.md 文件的修改怎么办?
运行:git checkout -- CONTRIBUTING.md,另外这个指令比较危险,因为它拷贝了另一个文件来覆盖当前文件。
git remote:查看远程仓库
- -v:列出远程仓库使用的 Git 保存的简写与其对应的 URL。
示例:git remote -v
git remote add:添加远程仓库
示例:$ git remote add pb https://github.com/paulboone/ticgit;
- 其中pb相当于后面那个连接的一个别名或引用,现在你可以在命令行中使用字符串 pb 来代替整个 URL。例如,如果你想拉取 Paul 的仓库中有但你没有的信息,可以运行 git fetch pb;
git fetch:从远程仓库中抓取与拉取
示例:$ git fetch [remote-name]
- 这个命令会访问远程仓库,从中拉取所有你还没有的数据。执行完成后,你将会拥有那个远程仓库中所有分支的引用,可以随时合并或查看。
git pull :从远程仓库中抓取并merge
由于 git pull 的魔法经常令人困惑所以通常单独显式地使用 fetch 与 merge 命令会更好一些。
git push [remote-name] [branch-name]:推送到远程仓库
$ git push origin master
- 只有当你有所克隆服务器的写入权限,并且之前没有人推送过时,这条命令才能生效。当你和其他人在同一时间克隆,他们先推送到上游然后你再推送到上游,你的推送就会毫无疑问地被拒绝。你必须先将他们的工作拉取下来并将其合并进你的工作后才能推送。
git remote show [remote-name]:查看远程仓库
示例:$ git remote show origin
* remote origin
Fetch URL: https://github.com/schacon/ticgit
Push URL: https://github.com/schacon/ticgit
HEAD branch: master
Remote branches:
master tracked
dev-branch tracked
Local branch configured for 'git pull':
master merges with remote master
Local ref configured for 'git push':
master pushes to master (up to date)
git remote rename:重命名远程仓库的引用
示例:git remote rename pb paul
git remote rm:远程仓库的移除
示例:git remote rm paul
git rm -r --cached :撤回远程仓库中不必要的文件,但不删除工作区的文件
git rm -r --cached .idea/
git commit
git push
git tag :创建与查看标签
- 不加参数:查看所有标签
- -a [version]:创建附注标签
- [version]:创建轻量标签
示例:
$ git tag -a v1.4
$ git tag
v0.1
v1.3
v1.4
git push origin
[version]:将标签推送到远程仓库
--tags:将所有未推送到远程的标签推送到远程
示例:
git push origin v1.5
git push origin --tags
git checkout -b version2 v2.0.0:同步标签
git config --global alias:创建指令的别名
示例如下:
$ git config --global alias.co checkout
$ git config --global alias.br branch
$ git config --global alias.ci commit
$ git config --global alias.st status
- 这意味着,当要输入 git commit时,只需要输入 git ci。
添加取消暂存的别名:
$ git config --global alias.unstage 'reset HEAD --'
这会使下面的两个命令等价:
$ git unstage fileA
$ git reset HEAD -- fileA
如何轻松地看到最后一次提交:
$ git config --global alias.last 'log -1 HEAD'
$ git last
commit 66938dae3329c7aebe598c2246a8e6af90d04646
Author: Josh Goebel <dreamer3@example.com>
Date: Tue Aug 26 19:48:51 2008 +0800
test for current head
Signed-off-by: Scott Chacon <schacon@example.com>
git add -i :交互式暂存
当你修改一组文件后,希望这些改动能放到若干提交而不是混杂在一起成为一个提交时,这几个工具会非常有用。 通过这种方式,可以确保提交是逻辑上独立的变更集,同时也会使其他开发者在与你工作时很容易地审核。
可以看到这个命令以非常不同的视图显示了暂存区 - 基本上与 git status 是相同的信息,但是更简明扼要一些。 它将暂存的修改列在左侧,未暂存的修改列在右侧。
(1) 暂存与取消暂存文件
-
如果在 What now> 提示符后键入 2 或 u,脚本将会提示想要暂存哪个文件:
每个文件前面的 * 意味着选中的文件将会被暂存。 如果在 Update>> 提示符后不输入任何东西并直接按回车,Git 将会暂存之前选择的文件。
-
如果这时想要取消暂存 TODO 文件,使用 3 或 r(撤消)选项:
-
如果想要查看已暂存内容的区别,可以使用 6 或 d(区别)命令。 它会显示暂存文件的一个列表,可以从中选择想要查看的暂存区别。 这跟你在命令行指定 git diff --cached 非常相似:
(2) 暂存补丁
Git 也可以暂存文件的特定部分。 例如,如果在 simplegit.rb 文件中做了两处修改,但只想要暂存其中的一个而不是另一个,Git 会帮你轻松地完成。
-
假如现在你在文件中修改了如下图的两个部分:
现在键入5 或 p(补丁),将会一个个显示你的修改区块,如下:
注意最下面一行,指的是你当前可选的操作,再键入?,查看选项的详细说明。
通常情况下可以输入 y 或 n 来选择是否要暂存每一个区块,当然,暂存特定文件中的所有部分或为之后的选择跳过一个区块也是非常有用的。
继续输入“y”:
退出,然后输入:git commit
也可以不必在交互式添加模式中做部分文件暂存 - 可以在命令行中使用 git add -p 或 git add --patch 来启动同样的脚本。
git stash:储藏与清理修改
储藏修改:即,在某个分支上修改了部分代码,然后将未完成的修改保存到一个栈上,而你可以在任何时候重新应用这些改动到其他的分支上。
进入项目并改动几个文件,然后可能暂存其中的一个改动。 如果运行 git status,可以看到有改动的状态:
$ git status
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
modified: index.html
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: lib/simplegit.rb
现在想要切换分支,但是还不想要提交之前的工作;所以储藏修改。 将其推送到栈上,运行git stash 或 git stash save:
$ git stash
Saved working directory and index state \
"WIP on master: 049d078 added the index file"
HEAD is now at 049d078 added the index file
(To restore them type "git stash apply")
然后查看工作目录,发现是干净的了:
$ git status
# On branch master
nothing to commit, working directory clean
在这时,你能够轻易地切换分支并在其他地方工作;你的修改被存储在栈上。 要查看储藏的东西,可以使用 git stash list:
$ git stash list
stash@{0}: WIP on master: 049d078 added the index file
stash@{1}: WIP on master: c264051 Revert "added file_size"
stash@{2}: WIP on master: 21d80a5 added number to log
在本例中,有两个之前做的储藏,所以你接触到了三个不同的储藏工作。 可以通过原来 stash 命令的帮助提示中的命令将你刚刚储藏的工作重新应用:git stash apply。 如果想要应用其中一个更旧的储藏,可以通过名字指定它,像这样:git stash apply stash@{2}。 如果不指定一个储藏,Git 认为指定的是最近的储藏:
$ git stash apply
# On branch master
# Changed but not updated:
# (use "git add <file>..." to update what will be committed)
#
# modified: index.html
# modified: lib/simplegit.rb
#
但是这里有个问题,lib/simplegit.rb不是暂存状态,这和未切换分支前的状态不一样。所以可以使用以下指令,将这两个文件的状态和未切换分支前保持一致:
$ git stash apply --index
# On branch master
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
# modified: index.html
#
# Changed but not updated:
# (use "git add <file>..." to update what will be committed)
#
# modified: lib/simplegit.rb
#
- 几个创造性的储藏方式
(1)git stash --keep-index:已暂存的文件其状态储藏为已修改
(2)git stash -u:储藏时,不要跟踪任何文件,应用储藏之后由自己手动git add
(3)git stash --patch:交互式储藏
git stash branch:从储藏创建一个分支
如果储藏了一些工作,将它留在那儿了一会儿,然后继续在储藏的分支上工作,在重新应用工作时可能会有问题。 如果应用尝试修改刚刚修改的文件,你会得到一个合并冲突并不得不解决它。 如果想要一个轻松的方式来再次测试储藏的改动,可以运行 git stash branch 创建一个新分支,检出储藏工作时所在的提交,重新在那应用工作,然后在应用成功后扔掉储藏:
$ git stash branch testchanges
Switched to a new branch "testchanges"
# On branch testchanges
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
# modified: index.html
#
# Changed but not updated:
# (use "git add <file>..." to update what will be committed)
#
# modified: lib/simplegit.rb
#
Dropped refs/stash@{0} (f0dfc4d5dc332d1cee34a634182e168c4efc3359)
这是在新分支轻松恢复储藏工作并继续工作的一个很不错的途径。
git clean:从工作目录中移除未被追踪的文件(请谨慎使用)
你需要谨慎地使用这个命令,因为它被设计为从工作目录中移除未被追踪的文件。 如果你改变主意了,你也不一定能找回来那些文件的内容。 一个更安全的选项是运行 git stash --all 来移除每一样东西并存放在栈中。
如果只是想要看看它会做什么,可以使用 -n 选项来运行命令,这意味着 “做一次演习然后告诉你将要 移除什么”。
$ git clean -d -n
Would remove test.o
Would remove tmp/
git grep:搜索
Git 提供了一个 grep 命令,你可以很方便地从提交历史或者工作目录中查找一个字符串或者正则表达式。
你可以传入 -n 参数来输出 Git 所找到的匹配行行号:
如果你想看匹配的行是属于哪一个方法或者函数,你可以传入 -p 选项:
git log :查看代码是什么时候引入的
行日志搜索
行日志搜索是另一个相当高级并且有用的日志搜索功能。 这是一个最近新增的不太知名的功能,但却是十分有用。 在 git log 后加上 -L 选项即可调用,它可以展示代码中一行或者一个函数的历史。
比如想要查看listShareLink方法在ConsoleController.java文件中的提交历史,可以运行以下命令:
git log -L '/listShareLink/',/^}/:src/main/java/com/jss/jssfinance/controller/ConsoleController.java
git reset:重点理解
git reset指令用于撤销,revert到之前的版本,理解了它,才能更好的理解git;
几个基本概念(三种树)
- HEAD树:分支的引用,里面包含最后一次提交的快照对象,可以粗狂的理解为Git仓库
- Index树:暂存区的快照
- Working Directory树:可以粗狂的理解为工作目录
假设某个master分支有三次提交,看起来像下面:
三种树的状态如下:
如果这个时候运行git status,发现三者状态一样,是clean状态。
紧接着运行git reset,它主要做了三件事情:
第 1 步:移动 HEAD
三种树的状态如下:
但如果你不运行get reset “上一次序列号”,而是运行 git reset --soft “上一次序列号”,它就不继续往下。这种状态相当于撤回commit,将HEAD的引用指向上一次提交的序列号,但是不会改变索引和工作目录。
第 2 步:更新索引(--mixed)
接下来,reset 会用 HEAD 指向的当前快照的内容来更新索引。
三种树的状态如下:
如果指定 --mixed 选项,reset 将会在这时停止。 这也是默认行为,所以如果没有指定任何选项(在本例中只是 git reset HEAD~),这就是命令将会停止的地方。这样表示将取消暂存,相当于撤回了git add命令。
第 3 步:更新工作目录(--hard)
reset 要做的的第三件事情就是让工作目录看起来像索引。 如果使用 --hard 选项,它将会继续这一步。
必须注意,--hard 标记是 reset 命令唯一的危险用法,它也是 Git 会真正地销毁数据的仅有的几个操作之一。 其他任何形式的 reset 调用都可以轻松撤消,但是 --hard 选项不能,因为它强制覆盖了工作目录中的文件。
三、分支
什么是分支?看图,看完图你就明白了。
这里有个假设,假设某个目录下有三个待保存到暂存区的文件。
产生的提交对象会包含一个指向上次提交对象(父对象)的指针
创建分支指令:git branch testing ,其中master是默认存在的分支
注意:HEAD是一个标记,HEAD指向哪个分支,就代表你当前的仓库处于哪个分支上
分支切换: git checkout testing
可以看到HEAD已经指向了testing
这时候提交一次版本,发现testing分支在向前移动:
再切换回master分支:git checkout master
这条命令做了两件事。一是使 HEAD 指回 master 分支,二是将工作目录恢复成 master 分支所指向的快照内容。也就是说,你现在做修改的话,项目将始于一个较旧的版本。本质上来讲,这就是忽略 testing 分支所做的修改,以便于向另一个方向进行开发。
再提交一个版本:git commit -a -m 'made other changes'
现在,这个项目的提交历史已经产生了分叉,你可以在不同分支间不断地来回切换和工作,并在时机成熟时将它们合并起来。而所有这些工作,你需要的命令只有 branch、checkout 和 commit。
查看分叉历史
git log --oneline --decorate --graph --all
$ git log --oneline --decorate --graph --all
* c2b9e (HEAD, master) made other changes
| * 87ab2 (testing) made a change
|/
* f30ab add feature #32 - ability to add new formats to the
* 34ac2 fixed bug #1328 - stack overflow under certain conditions
* 98ca9 initial commit of my project
由于 Git 的分支实质上仅是包含所指对象校验和(长度为 40 的 SHA-1 值字符串)的文件,所以它的创建和销毁都异常高效。创建一个新分支就像是往一个文件中写入 41 个字节(40 个字符和 1 个换行符),如此的简单能不快吗?
四、分支的创建
假设你现在有一个默认分支master,并且已经有了一些提交;现在,你已经决定要解决你的公司使用的问题追踪系统中的 #53 问题,所以你新建了一个分支并同时切换到那个分支上,你可以运行一个带有 -b 参数的 git checkout 命令:
$ git checkout -b iss53
Switched to a new branch "iss53"
- 它是下面两条命令的简写:
$ git branch iss53
$ git checkout iss53
你继续在 #53 问题上工作,并且做了一些提交,结果如下图:
现在你接到个电话,有个紧急问题(hotfix)等待你来解决。有了 Git 的帮助,你不必把这个紧急问题和 iss53 的修改混在一起,你也不需要花大力气来还原关于 53# 问题的修改,然后再添加关于这个紧急问题的修改,最后将这个修改提交到线上分支。你所要做的仅仅是切换回 master 分支。
但是要留意你的工作目录和暂存区里那些还没有被提交的修改,它可能会和你即将检出的分支产生冲突从而阻止 Git 切换到该分支。最好的方法是,在你切换分支之前,保持好一个干净的状态。可以使用保存进度(stashing) 和 修补提交(commit amending)的方式达到这一目的。
$ git checkout master
Switched to branch 'master'
接下来,你要修复这个紧急问题,于是建立一个针对该紧急问题的分支(hotfix branch):
$ git checkout -b hotfix
Switched to a new branch 'hotfix'
$ vim index.html
$ git commit -a -m 'fixed the broken email address'
[hotfix 1fb7853] fixed the broken email address
1 file changed, 2 insertions(+)
于是当前分支情况如下图:
然后你可以运行你的测试,确保你的修改是正确的,然后将其合并回你的 master 分支来部署到线上。你可以使用 git merge 命令来达到上述目的:
$ git checkout master
$ git merge hotfix
Updating f42c576..3a0874c
Fast-forward
index.html | 2 ++
1 file changed, 2 insertions(+)
在合并的时候,你应该注意到了"快进(fast-forward)"这个词。当你试图合并两个分支时,如果顺着一个分支走下去能够到达另一个分支,那么 Git 在合并两者的时候,只会简单的将指针向前推进(指针右移),因为这种情况下的合并操作没有需要解决的分歧——这就叫做 “快进(fast-forward)”。
合并完成之后,如下图:
关于这个紧急问题的解决方案发布之后,你准备回到被打断之前时的工作中。然而,你应该先删除 hotfix 分支,因为你已经不再需要它了 —— master 分支已经指向了同一个位置。你可以使用带 -d 选项的 git branch 命令来删除分支:
$ git branch -d hotfix
Deleted branch hotfix (3a0874c).
现在你可以切换回你正在工作的分支继续你的工作,也就是针对 #53 问题的那个分支(iss53 分支)。
$ git checkout iss53
Switched to branch "iss53"
$ vim index.html
$ git commit -a -m 'finished the new footer [issue 53]'
[iss53 ad82d7a] finished the new footer [issue 53]
1 file changed, 1 insertion(+)
当前分支情况如下图:
git merge --abort:遇到冲突时,退出合并。
五、分支的合并
手动合并
假设你已经修正了 #53 问题,并且打算将你的工作合并入 master 分支。为此,你需要合并 iss53 分支到 master 分支,这和之前你合并 hotfix 分支所做的工作差不多。你只需要检出到你想合并入的分支,然后运行 git merge 命令:
$ git checkout master
Switched to branch 'master'
$ git merge iss53
Merge made by the 'recursive' strategy.
index.html | 1 +
1 file changed, 1 insertion(+)
这和你之前合并 hotfix 分支的时候看起来有一点不一样。在这种情况下,你的开发历史从一个更早的地方开始分叉开来(diverged)。因为,master 分支所在提交并不是 iss53 分支所在提交的直接祖先,Git 不得不做一些额外的工作。出现这种情况的时候,Git 会使用两个分支的末端所指的快照(C4 和 C5)以及这两个分支的工作祖先(C2),做一个简单的三方合并。参照下图:
和之前将分支指针向前推进所不同的是,Git 将此次三方合并的结果做了一个新的快照并且自动创建一个新的提交指向它。这个被称作一次合并提交,它的特别之处在于他有不止一个父提交。如下图:
需要指出的是,Git 会自行决定选取哪一个提交作为最优的共同祖先,并以此作为合并的基础;这和更加古老的 CVS 系统或者 Subversion (1.5 版本之前)不同,在这些古老的版本管理系统中,用户需要自己选择最佳的合并基础。Git 的这个优势使其在合并操作上比其他系统要简单很多。
既然你的修改已经合并进来了,你已经不再需要 iss53 分支了。现在你可以在任务追踪系统中关闭此项任务,并删除这个分支。
$ git branch -d iss53
其他类型的合并
如果我们回到之前我们使用的 “hello world” 例子中,我们可以看到合并入我们的分支时引发了冲突。
$ git merge mundo
Auto-merging hello.rb
CONFLICT (content): Merge conflict in hello.rb
Resolved 'hello.rb' using previous resolution.
Automatic merge failed; fix conflicts and then commit the result.
然而如果我们运行时增加 -Xours 或 -Xtheirs 参数就不会有冲突。
$ git merge -Xours mundo
Auto-merging hello.rb
Merge made by the 'recursive' strategy.
hello.rb | 2 +-
test.sh | 2 ++
2 files changed, 3 insertions(+), 1 deletion(-)
create mode 100644 test.sh
撤销合并
有时候,你不小心合并了两个分支,但是你想撤销。
首先查看合并历史:
Merge: 6b9c6bc d70c00b,表示e9c5e9e 是从6b9c6bc和d70c00b这两次提交合并生产的,然后运行以下指令:
git revert -m 1 HEAD
这会使得当前的代码还原为6b9c6bc的提交状态,如果-m后面指定的是2,则会还原为d70c00b的提交状态。
如果这个时候,另一个被合并的分支继续提交一次,然后合并,git会告诉你已经是最新状态,无需合并了,这就让人困惑了。然后继续运行以下指令:
git revert ^M
再合并:
git merge 指定分支
这样就解决无法合并的问题。
六、遇到冲突时的分支合并
有时候合并操作不会如此顺利。如果你在两个不同的分支中,对同一个文件的同一个部分进行了不同的修改,Git 就没法干净的合并它们。如果你对 #53 问题的修改和有关 hotfix 的修改都涉及到同一个文件的同一处,在合并它们的时候就会产生合并冲突:
$ git merge iss53
Auto-merging index.html
CONFLICT (content): Merge conflict in index.html
Automatic merge failed; fix conflicts and then commit the result.
此时 Git 做了合并,但是没有自动地创建一个新的合并提交。Git 会暂停下来,等待你去解决合并产生的冲突。你可以在合并冲突后的任意时刻使用 git status
命令来查看那些因包含合并冲突而处于未合并(unmerged)状态的文件:
$ git status
On branch master
You have unmerged paths.
(fix conflicts and run "git commit")
Unmerged paths:
(use "git add <file>..." to mark resolution)
both modified: index.html
no changes added to commit (use "git add" and/or "git commit -a")
任何因包含合并冲突而有待解决的文件,都会以未合并状态标识出来。Git 会在有冲突的文件中加入标准的冲突解决标记,这样你可以打开这些包含冲突的文件然后手动解决冲突。出现冲突的文件会包含一些特殊区段,看起来像下面这个样子:
<<<<<<< HEAD:index.html
<div id="footer">contact : email.support@github.com</div>
=======
<div id="footer">
please contact us at support@github.com
</div>
>>>>>>> iss53:index.html
这表示 HEAD
所指示的版本(也就是你的 master
分支所在的位置,因为你在运行 merge 命令的时候已经检出到了这个分支)在这个区段的上半部分(=======
的上半部分),而 iss53
分支所指示的版本在 =======
的下半部分。为了解决冲突,你必须选择使用由 =======
分割的两部分中的一个,或者你也可以自行合并这些内容。例如,你可以通过把这段内容换成下面的样子来解决冲突:
<div id="footer">
please contact us at email.support@github.com
</div>
上述的冲突解决方案仅保留了其中一个分支的修改,并且 <<<<<<<
, =======
, 和 >>>>>>>
这些行被完全删除了。在你解决了所有文件里的冲突之后,对每个文件使用 git add
命令来将其标记为冲突已解决。一旦暂存这些原本有冲突的文件,Git 就会将它们标记为冲突已解决。
如果你想使用图形化工具来解决冲突,你可以运行 git mergetool
,该命令会为你启动一个合适的可视化合并工具,并带领你一步一步解决这些冲突:
$ git mergetool
This message is displayed because 'merge.tool' is not configured.
See 'git mergetool --tool-help' or 'git help config' for more details.
'git mergetool' will now attempt to use one of the following tools:
opendiff kdiff3 tkdiff xxdiff meld tortoisemerge gvimdiff diffuse diffmerge ecmerge p4merge araxis bc3 codecompare vimdiff emerge
Merging:
index.html
Normal merge conflict for 'index.html':
{local}: modified file
{remote}: modified file
Hit return to start merge resolution tool (opendiff):
如果你想使用除默认工具(在这里 Git 使用 opendiff
做为默认的合并工具,因为作者在 Mac 上运行该程序)外的其他合并工具,你可以在 “下列工具中(one of the following tools)” 这句后面看到所有支持的合并工具。然后输入你喜欢的工具名字就可以了。
如果你需要更加高级的工具来解决复杂的合并冲突,我们会在 “高级合并” 介绍更多关于分支合并的内容。
等你退出合并工具之后,Git 会询问刚才的合并是否成功。如果你回答是,Git 会暂存那些文件以表明冲突已解决:你可以再次运行 git status
来确认所有的合并冲突都已被解决:
$ git status
On branch master
All conflicts fixed but you are still merging.
(use "git commit" to conclude merge)
Changes to be committed:
modified: index.html
如果你对结果感到满意,并且确定之前有冲突的的文件都已经暂存了,这时你可以输入 git commit
来完成合并提交。默认情况下提交信息看起来像下面这个样子:
Merge branch 'iss53'
Conflicts:
index.html
#
# It looks like you may be committing a merge.
# If this is not correct, please remove the file
# .git/MERGE_HEAD
# and try again.
# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
# On branch master
# All conflicts fixed but you are still merging.
#
# Changes to be committed:
# modified: index.html
#
六、遇到冲突时的分支合并(二)
上面处理冲突是比较手工的方式,有没有优雅一点的方式呢?
1. 非手动修改文件再合并
首先要理解,冲突是怎么来的,为什么会产生冲突。
在版本控制工具里面冲突是这样的,大家从同一个分支中克隆代码,然后都修改了同一个地方,先提交的不会有冲突的问题,但是紧接着后提交的就会产生一个冲突。
注意上面这段话的关键,是一个文件同一个地方被连续的提交。
假设有一个hello.rb文件,产生冲突的原因是两个分支上的hello.rb文件所使用的换行符是不同的,一个是unix的换行符,一个是dos的换行符,当merge之后,产生了冲突,你可以运行以下指令导出三个版本的文件:
$ git show :1:hello.rb > hello.common.rb #代表祖先版本
$ git show :2:hello.rb > hello.ours.rb #自己的版本
$ git show :3:hello.rb > hello.theirs.rb #他们的版本
解决的方案是这样的:先转换换行符,然后调用git merge-file指令进行合并,如下:
$ dos2unix hello.theirs.rb
dos2unix: converting file hello.theirs.rb to Unix format ...
$ git merge-file -p \
hello.ours.rb hello.common.rb hello.theirs.rb > hello.rb
$ git diff -b
diff --cc hello.rb
index 36c06c8,e85207e..0000000
--- a/hello.rb
+++ b/hello.rb
@@@ -1,8 -1,7 +1,8 @@@
#! /usr/bin/env ruby
+# prints out a greeting
def hello
- puts 'hello world'
+ puts 'hello mundo'
end
hello()
你还可以运行以下指令查看引入了什么:
- git diff --ours
- git diff --theirs
- git diff --base
2. 检出冲突
现在有只在 master 分支上的三次单独提交,还有其他三次提交在 mundo 分支上。 如果我们尝试将 mundo 分支合并入 master 分支,我们得到一个冲突。
$ git merge mundo
Auto-merging hello.rb
CONFLICT (content): Merge conflict in hello.rb
Automatic merge failed; fix conflicts and then commit the result.
我们想要看一下合并冲突是什么。 如果我们打开这个文件,我们将会看到类似下面的内容:
#! /usr/bin/env ruby
def hello
<<<<<<< HEAD
puts 'hola world'
=======
puts 'hello mundo'
>>>>>>> mundo
end
hello()
合并的两边都向这个文件增加了内容,但是导致冲突的原因是其中一些提交修改了文件的同一个地方。
继续运行以下指令:
$ git checkout --conflict=diff3 hello.rb
会得到这样一个结果:
#! /usr/bin/env ruby
def hello
<<<<<<< ours
puts 'hola world'
||||||| base
puts 'hello world'
=======
puts 'hello mundo'
>>>>>>> theirs
end
hello()
不仅仅只给你 “ours” 和 “theirs” 版本,同时也会有 “base” 版本在中间来给你更多的上下文。
七、分支管理
git branch 命令不只是可以创建与删除分支。如果不加任何参数运行它,会得到当前所有分支的一个列表:
$ git branch
iss53
* master
testing
注意 master 分支前的 * 字符:它代表现在检出的那一个分支(也就是说,当前 HEAD 指针所指向的分支)。这意味着如果在这时候提交,master 分支将会随着新的工作向前移动。如果需要查看每一个分支的最后一次提交,可以运行 git branch -v 命令:
$ git branch -v
iss53 93b412c fix javascript issue
* master 7a98805 Merge branch 'iss53'
testing 782fd34 add scott to the author list in the readmes
--merged 与 --no-merged 这两个有用的选项可以过滤这个列表中已经合并或尚未合并到当前分支的分支。如果要查看哪些分支已经合并到当前分支,可以运行 git branch --merged:
$ git branch --merged
iss53
* master
因为之前已经合并了 iss53 分支,所以现在看到它在列表中。在这个列表中分支名字前没有 * 号的分支通常可以使用 git branch -d 删除掉;你已经将它们的工作整合到了另一个分支,所以并不会失去任何东西。
查看所有包含未合并工作的分支,可以运行 git branch --no-merged:
$ git branch --no-merged
testing
这里显示了其他分支。因为它包含了还未合并的工作,尝试使用 git branch -d 命令删除它时会失败:
$ git branch -d testing
error: The branch 'testing' is not fully merged.
If you are sure you want to delete it, run 'git branch -D testing'.
如果真的想要删除分支并丢掉那些工作,如同帮助信息里所指出的,可以使用 -D 选项强制删除它。
八、建立远程仓库
1.安装远程和本地git,参考百度或谷歌
或参考:https://git-scm.com/book/zh/v1/%E8%B5%B7%E6%AD%A5-%E5%AE%89%E8%A3%85-Git
2.使用ssh的方式作为git协议
- (1)首先要在本地生成一对密钥,使用以下指令:
ssh-keygen -t rsa -b 4096 -C "your_email@example.com"
进入你的家目录(不知道什么是家目录,请谷歌或百度自补), 可以看到在家目录下面多了一个.ssh文件夹,且多了两个文件,id_rsa是私钥,id_rsa.pub是公钥,如下图:
- (2)添加公钥
然后将id_rsa.pub放到远程主机,也就是git服务器上面的git用户的家目录的.ssh文件夹下面,你最好重命名再上传。假设你现在已经将id_rsa.pub放到了.ssh文件夹下面,然后你需要运行以下指令:
cat id_rsa.pub >> authorized_keys
重启ssh服务:
service sshd restart
3.将项目上传到远程服务器,并建立分支
(1)以项目Thirddevops为例,进入Thirddevops文件夹下,运行以下指令:
git init
(2)运行以下指令,生成一个裸仓库:
git clone --bare Thirddevops Thirddevops.git
紧接着会在当前文件夹下面生成了一个Thirddevops.git文件夹
(3)将Thirddevops.git文件夹复制到远程主机
scp -r Thirddevops.git 用户名@远程主机ip或域名:目标路径/Thirddevops.git
(4)将Java项目push到远程仓库
# on my computer
$ cd Thirddevops
$ git add .
(或git add 指定的文件夹)
$ git commit -m 'initial commit'
$ git remote add origin git@gitserver:/opt/git/Thirddevops.git
$ git push origin master
(4)让别人拉取远程仓库的代码
$ git clone git@gitserver:/opt/git/Thirddevops.git
$ cd Thirddevops
$ vim README
$ git commit -am 'fix for the README file'
$ git push origin master
九、一些其他的小问题
1.有关于跨平台换行符的问题
windows用户需要做这样一个全局的配置:
$ git config --global core.autocrlf true
unix或mac用户则需要这样配置:
$ git config --global core.autocrlf input