Git常用命令和相关概念的理解

Published: by Creative Commons Licence (Last updated: )

GitHub 使用git协议总是无法操作远程仓库;但是使用https协议可以。

学习资源

学习Git

入门:

进阶:

深入理解Git:

GitHub学习

在线演练

  • Learn Git Branching非常好的在线练习网站,可选择中文版。以此网站为基础进行学习。

常用命令

更多命令可见后面的:

查看git的某个命令的帮助

git add

学习:

文件的状态变化周期

图: 文件的状态变化周期
# 添加当前目录的所有文件到暂存区
git add .

git rm

要从 Git 中移除某个文件,就必须要从已跟踪文件清单中移除(确切地说,是从暂存区域移除),然后提交。

# 删除工作区文件,并且将这次删除放入暂存区
git rm [file1] [file2] ...

# 停止追踪指定文件,但该文件会保留在工作区
git rm --cached [file]

git mv

运行 git mv 就相当于运行了下面三条命令:

mv README.md README
git rm README.md
git add README

git log

Git - 查看提交历史

# 以图表形式查看 分支 log
git log --graph

# 精简显示...
git log –pretty=oneline

# 查看当前 仓库 的操作日志
git reflog

git log 只能查看以当前状态为终点的历史日志。

git reflog 可查看当前仓库的操作日志。

查看项目分支历史:

git log --oneline --decorate --graph --all 

git diff

# 显示暂存区和工作区的差异
git diff

# 显示暂存区和上一个commit的差异
git diff --cached [file]

# 显示工作区与当前分支最新commit之间的差异
git diff HEAD

# 显示今天你写了多少行代码
git diff --shortstat "@{0 day ago}"

git commit

理解提交见: Git - 分支简介
首次提交对象及其树结构

图:首次 提交对象 及其树结构

对于小的提交,可以一步完成git add和git commit,利用-a选项即可,比如:

git commit -am "一次add加commit"

# 提交时显示所有diff信息
git commit -v

# 签署提交(使用gpg key) 使用 -S 选项
git commit -S -m 'signed commit签署提交'

在Android Studio中使用GPG对提交进行签名,见本博客的相关笔记 ,或 Signed commits · Wiki · akwizgran / briar · GitLab

该方法是对某工程设置 Signed commits,在该工程中的所有提交都使用GPG签名的方法。也可尝试在git的全局配置文件中进行配置,以用于所有提交。

git commit –amend

Git - 撤消操作: 主要涉及三个方面的撤销:

  • 更改了工作区文件后,撤消对文件的修改。
  • 运行了命令git add后,取消暂存的文件。
  • 运行了命令git commit后,修改此次提交。

进阶:Git - 重置揭密

git commit --amend的两个用法:

  • 提交完了才发现漏掉了几个文件没有add
  • 修改上一次提交信息

注意事项: 请勿修改已经发布(push)的提交记录

# 重做上一次commit,包含包指定文件的新变化
git commit --amend [file1] [file2]

# 工作流程
git commit -m 'initial commit'
git add forgotten_file
git commit --amend
# 使用一次新的commit,替代上一次提交
# 如果代码没有任何新变化,则用来改写上一次commit的提交信息
git commit --amend

使用--amend参数后,会打开配置文件中设置的文本编辑器,让你修改提交信息。请勿修改已经push到远程仓库的的提交记录(在这里犯了两次错误)。

git reset回溯

只要提供目标时间点的哈希值,即可进行回溯至该时间点的状态(包括:HEAD、暂存区、当前工作树)

git reset有多个模式,其中--hard是模式之一,另还有默认的--mixed(不改变当前工作树);详见:git help reset

# 重置暂存区的指定文件,与上一次commit保持一致,但工作区不变
git reset [file]

# 回溯到 哈希值 指定的时间点
git reset --hard 某哈希值如82b9b79

# 重置暂存区与工作区,与上一次commit保持一致
git reset --hard

# 回溯到上一个版本 (和上一条一样)
git reset --hard HEAD^

# 回溯到上上一个版本,可以一直加 ^
git reset --hard HEAD^^

# 回溯5个版本, 用 ~5 
git reset --hard HEAD~5

git branch

分支学习:

# 列出所有本地分支
git branch
git branch -vv

# 列出所有远程分支
git branch -r

# 列出所有本地分支和远程分支
git branch -a
git branch -av


# 新建一个分支,但依然停留在当前分支
git branch [branch-name]

# 新建一个分支,与指定的远程分支建立追踪关系
git branch --track [branch] [remote-branch]
# 或使用
git branch -u [remote-branch] [branch]

# 删除分支
git branch -d [branch-name]

# git 1.7.0 之后: 删除远程分支. 
git push origin --delete [branch-name]
git branch -dr [remote/branch]

# git 1.7.0 之前: 删除远程分支
# 通过推送一个空分支到远程,来达到删除远程分支的目的
# 冒号左边为空,表是空分支
git push origin  :<branchName>

Git 的分支,其实本质上仅仅是指向提交对象的可变指针。创建分支仅仅是创建一个可移动的指针。
由于 Git 的分支实质上仅是包含所指对象校验和(长度为 40 的 SHA-1 值字符串)的文件,所以它的创建和销毁都异常高效。(该指针是使用一个文件表示?)

创建分支:

git branch testing

运行git branch testing后

图: 两个指向相同提交历史的分支,其中HEAD 指向当前所在的分支

分支切换:
做了两个事情,1.是使HEAD指向testing;2.将工作目录变为testing指向的快照内容。

git checkout testing 

git checkout切换分支

# 切换到指定分支,并更新工作区
git checkout [branch-name]

# 创建并切换分支
git checkout -b 分支名称

# 在develop分子的基础上创建一个feature-x分支,并切换
git checkout -b feature-x develop


# 获取远程仓库的feature-D分支
# 并在本地仓库中创建对应的名称同样为feature-D的分支,并切换
git checkout -b feature-D origin/feature-D


# 切换回上一个分支
git checkout -

git checkout其它用法

利用git checkout撤销某文件的修改:


# 此时会有两种情况: 
# 1. 该文件还没有放入暂存区;此时撤销后内容与版本库中一样
# 2. 该文件已经放入暂存区;此时撤销后内容与添加到暂存区中时的一样
git checkout -- filename

# 恢复某个commit的指定文件到暂存区和工作区
git checkout [commit] [file]

# 恢复暂存区的所有文件到工作区
git checkout .

利用git checkout 分离HEAD

git checkout 231333head

git merge合并

# 合并指定分支到当前分支
git merge [branch]

# 创建合并提交「常用」。在合并的时候进行提交  --no-ff
git merge --no-ff [要合并进来的分支名称]

合并相关的工作流程:

  • 使用git checkout切换到要进行合并的分支上
  • 使用git merge将某分支合并到当前分支
  • 使用git branch删除被合并的分支(肯定会删除该指针「文件」;那些只属于该分支的对象是如何处理,也是直接删除?)

解决合并冲突:

步骤:

  1. 使用git status查看哪些文件中有冲突
  2. 修改这些文件中出现特殊区段(特殊标记)的地方
  3. 对每个文件使用 git add 命令来将其标记为冲突已解决
  4. 使用 git commit 来完成合并提交

出现合并冲突后,此时 Git 做了合并,但是没有自动地创建一个新的合并提交。 Git 会暂停下来,等待你去解决合并产生的冲突。 你可以在合并冲突后的任意时刻使用 git status 命令来查看那些因包含合并冲突而处于未合并(unmerged)状态的文件。

git remote

与远程相关的操作参考:

git remote add 添加远程库:

git remote add [远程仓库的本地别名] [地址]
# 示例
git remote add origin http://127.0.0.1:8081/TestGroup/Test.git

使用git remote add将路径中的仓库设置为本地仓库的远程仓库;并将远程仓库的名称(别名)设置为 origin (默认就是这个名称)

查看远程仓库的地址:

git remote -v
git remote rm [别名]
git remote rename [别名]
git remote show [别名]

# 该命令的输出示例:
$ git remote show origin-gitlab
* 远程 origin-gitlab
  fetch获取地址:git@gitlab.com:faner/faner.gitlab.io.git
  push推送地址:git@gitlab.com:faner/faner.gitlab.io.git
  HEAD 分支:master
  远程分支:
    master 已跟踪
  为 'git pull' 配置的本地分支:
    master 与远程 master 合并
  为 'git push' 配置的本地引用:
    master 推送至 master (最新)

git clone 获取远程仓库

git clone [仓库提供的url]

# 示例
git clone https://github.com/github-book/git-tutorial.git

执行该命令后,我们默认是处于master分支(获取了master分支),同时系统会自动将origin设置成该远程仓库的标识符。

git branch -a 同时查看本地和远程仓库的分支信息。

之后再获取其他分支的命令:

# 获取远程仓库的feature-D分支
# 在本地仓库中创建对应名称也为 feature-D 的分支,并切换
git checkout -b feature-D origin/feature-D

本地与远程的操作: 从远程仓库获取数据 和 向远程仓库传输数据。

查看一个git clone克隆下来的仓库,默认做了哪些动作 :

[fan 15:07:01]/tmp/github/jekyll-demo$ git branch -a
* master
  remotes/origin/HEAD -> origin/master
  remotes/origin/gh-pages
  remotes/origin/master
  remotes/origin/pr-patch-1

[fan 15:05:25]/tmp/github/jekyll-demo$ git remote show origin 
* 远程 origin
  获取地址:https://github.com/FanDean/jekyll-demo.git
  推送地址:https://github.com/FanDean/jekyll-demo.git
  HEAD 分支:master
  远程分支:
    gh-pages   已跟踪
    master     已跟踪
    pr-patch-1 已跟踪
  为 'git pull' 配置的本地分支:
    master 与远程 master 合并
  为 'git push' 配置的本地引用:
    master 推送至 master (最新)

[fan 15:13:41]/tmp/github/jekyll-demo$ git log --oneline --decorate --graph --all
* 523f0b2 (origin/pr-patch-1) Create pr-test1.md
* f567581 (HEAD -> master, origin/master, origin/HEAD) 修改README.md
* 3ced95a 删除不要的文件,创建README.md
| * e843202 (origin/gh-pages) Update 2017-06-29-本博客搭建过程.md
| * 4603c83 添加第一篇文章
| * 3596f40 update 配置文件,将github图标指向自己
| * 2d3eb8b 构建静态网站
|/  
* 09136d3 初始化仓库

[fan 15:23:44]/tmp/github/jekyll-demo$ git log --oneline --decorate --graph
* f567581 (HEAD -> master, origin/master, origin/HEAD) 修改README.md
* 3ced95a 删除不要的文件,创建README.md
* 09136d3 初始化仓库

[fan 15:24:48]/tmp/github/jekyll-demo$ git log --oneline --graph
* f567581 修改README.md
* 3ced95a 删除不要的文件,创建README.md
* 09136d3 初始化仓库

git fetch

从远程仓库获取分支或分支的更新。

使用此命令的时候,可多加利用 tab 补全。

# 取回远程仓库所有分支的更新
git fetch [remote]

# 取回特定分支的更新
git fetch [remote] [分支名称] 
# 示例:取回origin主机的master分支
git fetch origin master

git fetch 命令会访问远程仓库,从中拉取所有你还没有的数据。 执行完成后,你将会拥有那个远程仓库中所有分支的引用,可以随时合并或查看。 必须注意 git fetch 命令会将数据拉取到你的本地仓库 - 它并不会自动合并或修改你当前的工作。

所取回的更新,在本地主机上要用"远程主机名/分支名"的形式访问。比如origin主机的master,就要用origin/master访问。

使用git fetch获取到整个远程分支该分支的更新后的下一步:进行 查看 或 合并 。

查看: 取回远程主机的更新以后,可以在它的基础上,使用git checkout命令创建一个新的分支:

git checkout -b newBrach origin/master

合并: 也可以使用git merge命令或者git rebase命令,在本地分支上合并远程分支:

# 在当前分支上,合并origin/master
git merge origin/master
# 或者
git rebase origin/master

git pull

git pull命令的作用是,取回远程主机某个分支的更新,再与本地的指定分支合并。

完整语法:

# 基本语法:远程 -> 本地
git pull <远程主机名> <远程分支名>:<本地分支名>

示例:

# 取回origin主机的next分支,并与本地master分支合并
git pull origin next:master

# 取回远程仓库的变化,并与本地当前分支合并
git pull origin feature-D
# 等同于
git fetch origin
git merge origin/feature-D

在某些场合,Git会自动在本地分支与远程分支之间,建立一种追踪关系(tracking)。比如,在git clone的时候,所有本地分支默认与远程主机的同名分支,建立追踪关系,也就是说,本地的master分支自动"追踪"origin/master分支。

Git也允许手动建立追踪关系:

# 指定master分支追踪origin/next分支;--set-upstream也可工作,但是会提示其已经废弃;另参考后文的跟踪分支。
git branch --set-upstream master origin/next
# 推荐方法 (仓库的位置相反,如不确定可运行 git help branch 查看)
git branch -u origin/next master

如果当前分支与远程分支存在追踪关系,git pull就可以省略远程分支名。

# 本地的当前分支自动与对应的origin主机"追踪分支"(remote-tracking branch)进行合并
git pull origin

What is the difference between 'git pull' and 'git fetch'? - Stack Overflow

当前理解:

git fetch 相关步骤:

  • git fetch 获取远程分支到本地
  • git checkout 检出该远程分支并创建对应的本地分支,并且默认会自动跟踪。
  • 在工作区进行相关工作

git pull相关步骤:

  • git pull 获取远程分支并与本地分支合并(假设是当前分支)
  • 在工作区进行相关工作

git push

git push 推送到远程仓库

# 基本语法:本地 -> 远程
git push <远程主机名> <本地分支名>:<远程分支名>

# 几种省略写法:
# 1. 省略远程分支名
# 将当前分支master的内容推送给与远程仓库origin存在追踪关系的分支。
git push origin master
# 2. 省略本地分支名
# 推送一个空的分支到远程,相当于删除远程主机上的master分支
git push origin :master
# 3.同时省略本地和远程分支
# 当前分支与远程分支之间存在追踪关系
git push origin
# 4. 全部省略
# 当前分支只有一个追踪分支,那么主机名都可以省略。
git push

# 推送所有分支到远程仓库
git push [remote] --all

注意 : git push 不带任何参数时的行为与 Git 的一个名为 push.default 的配置有关。它的默认值取决于你正使用的 Git 的版本,但是在教程中我们使用的是 upstream。 这没什么太大的影响,但是在你的项目中进行推送之前,最好检查一下这个配置。

追踪分支

# 追踪分支
git push -u origin master
# 该命令的作用:将本地的master分支推送到origin主机(如果origin远程主机上还没有对应的分支,则会在origin上创建一个master分支),同时指定 origin 为默认主机
# -u 参数是 --set-upstream 的简写 将 origin仓库的master分支设置为本地master分支的upstream(上游)。

在后面的远程分支部分还介绍了另一个设置追踪分支的命令:

git branch -u origin/serverfix

Git远程操作详解 - 阮一峰

push之前:
befor-git-push

push之后:
after-git-push

git cherry-pick

# 选择一个commit,合并进当前分支
git cherry-pick [commit]

git tag

Git - 打标签

轻量标签:它只是一个特定提交的引用。通常用作临时标签。
附注标签:是存储在 Git 数据库中的一个完整对象。通常建议创建附注标签。

# 列出现有标签
git tag

# 查找标签。查找1.8.5系列标签
git tag -l 'v1.8.5*'


# 使用 -a 创建附注标签 v1.4 ,-m 指定标签信息
git tag -a v1.4 -m "my version 1.4"

# 查看标签 v1.4 的信息与对应的提交信息
git show v1.4

# 后期打标签:为过去的某次提交打标签
git tag -a tagName 某次提交  

# 默认情况下,git push 命令并不会传送标签到远程仓库服务器上
# 显式地推送某个标签到远程仓库
git push origin别名  tagName 

# 将所有标签推送到远程仓库
git push origin --tags


# git 1.7之后,删除远程tag
git push origin --delete tag <tagname>

# git 1.7 之前:删除远程标签。
# 原理是:推送一个空tag到远程来覆盖远程的tag
# 先删除本地tag
git tag -d [tagName]
# 从远程删除
# 冒号左边为空,表是空tag
git push origin :refs/tags/[tagName]

git archive

# 生成一个可供发布的压缩包
$ git archive

多人协作

查看前面介绍过的Git进阶学习的文章。

Git相关概念

《Pro Git》第1章:Git起步,git基础 (必看),讲述了需要理解的Git概念。第3章:Git分支, 分支简介 (必看)。 第10章:Git内部原理。

另见《Git权威指南》

Git起步:Git基础

这些概念是理解Git的思想和基本工作原理的关键

直接记录快照,而非差异比较

比较下面两个图:

存储每个文件与初始版本的差异
图1:↑ 其它VCS存储每个文件与初始版本差异

Git存储项目随时间改变的快照.png
图2:↑ Git存储项目随时间改变的快照.(此图片在黑色背景显示效果更好,但是托管网站可能倒闭)

图2的另一个链接:   
![Git存储项目随时间改变的快照](https://i.imgur.com/bT1YwGe.png "不见图需FQ")    

每次你提交更新,或在 Git 中保存项目状态时, 它主要对当时的全部文件制作一个快照并保存这个快照的索引。
为了高效,如果文件没有修改,Git 不再重新存储该文件,而是只保留一个链接指向之前存储的文件 (图中的虚线部分体现了这一点;图中文件A的几个版本分别为 A、A1、A2,其它文件以此类推)。

在进行提交操作时,Git 会保存一个提交对象(commit object)。

Git更像是一个小型的文件系统。

Git保证完整性

Git 中所有数据在存储前都计算校验和,然后以校验和来引用。 这意味着不可能在 Git 不知情时更改任何文件内容或目录内容。

若你在传送过程中丢失信息或损坏文件,Git就能发现。(猜测:再次计算文件的hash值与之前保存的进行对比)

Git 数据库中保存的信息都是以文件内容的哈希值来索引,而不是文件名。

Git一般只添加数据

你执行的 Git 操作,几乎只往 Git 数据库中增加数据。 很难让 Git 执行任何不可逆操作,或者让它以任何方式清除数据。

未提交的更新是有可能丢失或弄乱修改的内容;但是一旦你提交快照到Git中,就难以再丢失数据,特别是如果你定期推送到其它远程仓库的话。

三种状态

Git有三种状态,你的(已跟踪的)文件可能处于其中之一:

  • 已提交(Committed):数据已经安全的保存在本地数据库中。
  • 已修改(modified):修改了文件,但还没有保存到数据库中。
  • 已暂存(staged):对一个已经修改的文件的当前版本做了标记,使之包含在下次提交的快照中。

又此引入的三个工作区域的概念:

  • Git仓库:用来保存项目的元数据和对象数据库。克隆仓库时,拷贝的就是这里的数据。即.git目录。
  • 工作目录:是对项目的某个版本独立提取出来的内容。
  • 暂存区域:是一个文件,保存了下次将提交的文件列表信息。有时也叫作"索引"


Git暂存区

Git暂存区是Git中最重要的一个概念。

结合.git目录结构学习。 HEAD在.git目录中也是一个文件,它默认指示当前被检出的分支。

在版本库(.git目录)中有一个index文件。该文件实际上就是一个包含文件索引的目录树,像是一个虚拟的工作区。在这个虚拟工作区的目录树中,记录了文件名和文件的状态信息(时间戳和文件长度等);文件的内容并没有存储在其中,而是保存在Git对象库 .git/objects/目录中,文件索引建立了文件和对象库中对象实体之间的对应。

工作区、版本库、暂存区原理图
图:工作区、版本库、暂存区原理

图中:

  • index的区域指暂存区,标记为master的是master分支所代表的目录树
  • 图中,此时HEAD实际是指向master分支的一个”游标“,所以图示的命令中出现HEAD的地方可以用master来替换。
  • objects为git的对象库,实际位于.git/objects/

该部分内容来自《Git权威指南》

git diff与版本库的关系:

enter image description here

Git对象

Git - Git 对象
Git - 分支简介

树对象

提交对象

对象存储

blob 对象(保存着文件快照)

分离HEAD

在网站Learn Git Branching上理解"分离HEAD"。

分离 HEAD 就是让其指向一个提交记录而不是分支名。

网站原话:
我们首先看一下"HEAD". HEAD 是当前提交记录的符号名称 – 其实就是你正在其基础进行工作的提交记录。

HEAD 总是指向最近一次提交记录,表现为当前工作树。大多数修改工作树的 Git 命令都开始于改变 HEAD 指向。

HEAD 通常指向分支名(比如 bugFix)。你提交时,改变了 bugFix 的状态,这一变化通过 HEAD 变得可见。

分离 HEAD 就是让其指向一个提交记录而不是分支名。这是命令执行之前的样子:

HEAD -> master -> C1 (一次提交)

git checkout C1

现在变成了: HEAD -> C1

Git分支

分支简介

Git - 分支简介

有人把 Git 的分支模型称为它的`‘必杀技特性’'

Git - 分支开发工作流

另见阮一峰的文章。

Git - 远程仓库的使用

远程分支

Git - 远程分支

远程引用: 远程引用是对远程仓库的引用(指针),包括分支、标签等等

# 通过 git ls-remote (remote) 来显式地获得远程引用的完整列表
[fan 16:12:47]/tmp/github/jekyll-demo/.git$ git ls-remote 
From https://github.com/FanDean/jekyll-demo.git
f567581e5c81e42710fcc406892ef4bf060efaed	HEAD
e84320250c32129c1b54616e4aaef17a3fc4e27a	refs/heads/gh-pages
f567581e5c81e42710fcc406892ef4bf060efaed	refs/heads/master
523f0b2d898f6039ce09e84d64fa84676416e268	refs/heads/pr-patch-1
523f0b2d898f6039ce09e84d64fa84676416e268	refs/pull/1/head
596fcd5b343786bc63680aebd5cd1a709e4ebd3d	refs/pull/1/merge

# 可以看到除了远程分支 gh-pages、master、pr-patch-1外还有其它东西


# 或通过 git remote show (remote) 获得远程分支的更多信息
[fan 16:37:02]/tmp/github/jekyll-demo/.git$ git remote show origin 
* 远程 origin
  获取地址:https://github.com/FanDean/jekyll-demo.git
  推送地址:https://github.com/FanDean/jekyll-demo.git
  HEAD 分支:master
  远程分支:
    gh-pages   已跟踪
    master     已跟踪
    pr-patch-1 已跟踪
  为 'git pull' 配置的本地分支:
    gh-pages 与远程 gh-pages 合并
    master   与远程 master 合并
  为 'git push' 配置的本地引用:
    gh-pages 推送至 gh-pages (最新)
    master   推送至 master   (最新)

远程跟踪分支:远程跟踪分支是远程分支状态的引用。 它们是你不能移动的本地引用,当你做任何网络通信操作时,它们会自动移动。 远程跟踪分支像是你上次连接到远程仓库时,那些分支所处状态的书签。它们以 (remote)/(branch) 形式命名。

示例:
假设你的网络里有一个在 git.ourcompany.com 的 Git 服务器。 如果你从这里克隆,Git 的 clone 命令会为你自动将其命名为 origin,拉取它的所有数据,创建一个指向它的 master 分支的指针,并且在本地将其命名为 origin/master。 Git 也会给你一个与 origin 的 master 分支在指向同一个地方的本地 master 分支,这样你就有工作的基础。

  1. 克隆之后的服务器与本地仓库:
    克隆之后的服务器与本地仓库

  2. 本地和远程都有提交更改后:
    在本地和远程都有提交更改后

  3. 使用git fetch origin同步后,更新你的远程仓库引用:
    使用git fetch origin同步后,更新你的远程仓库引用

跟踪分支: 从一个远程跟踪分支检出一个本地分支会自动创建一个 “跟踪分支”(有时候也叫做 “上游分支”)。 跟踪分支是与远程分支有直接关系的本地分支。如果在一个跟踪分支上输入 git pull,Git 能自动地识别去哪个服务器上抓取、合并到哪个分支。

这里跟踪分支说的不明不白。

git checkout -b sf origin/serverfix

根据之前的说明推断:上面的语句中 sf 就是一个跟踪分支。

设置已有的本地分支跟踪一个刚刚拉取下来的远程分支,或者想要修改正在跟踪的上游分支,你可以在任意时间使用 -u--set-upstream-to 选项运行 git branch 来显式地设置。

# 设置当前分支,追踪 origin/serverfix
$ git branch -u origin/serverfix

# 或者
$ git branch --set-upstream-to=origin/serverfix

# 设置某分支(非当前),追踪 origin/serverfix
$ git branch -u origin/serverfix [某分支]

# 在push时也可使用 -u 选项来设置跟踪,这也是一个比较常见的用法。略

# 取消跟踪
git branch --unset-upstream [<本地branchname>]

如果想要查看设置的所有跟踪分支,可以使用 git branch-vv 选项。 这会将所有的本地分支列出来并且包含更多的信息,如每一个分支正在跟踪哪个远程分支与本地分支是否是领先、落后或是都有。

拉取
git fetch 命令从服务器上抓取本地没有的数据时,它并不会修改工作目录中的内容。 它只会获取数据然后让你自己合并。 然而,有一个命令叫作 git pull大多数情况下它的含义是一个 git fetch 紧接着一个 git merge 命令。 如果有一个像之前章节中演示的设置好的跟踪分支,不管它是显式地设置还是通过 clonecheckout 命令为你创建的,git pull 都会查找当前分支所跟踪的服务器与分支,从服务器上抓取数据然后尝试合并入那个远程分支。

由于 git pull 的魔法经常令人困惑所以通常单独显式地使用 fetch 与 merge 命令会更好一些。

一般情况下 git pull 的操作效果与我之前想象的不一样: 这里左边为本地仓库,右边为远程仓库
执行git pull之前
执行git pull之前
执行git pull之后
执行git pull之后

偏离的工作

一下内容来自 Learn Git Branching Git 远程仓库高级操作 练习。

困难来自于远程库提交历史的偏离。

假设你周一克隆了一个仓库,然后开始研发某个新功能。到周五时,你新功能开发测试完毕,可以发布了。但是 —— 天啊!你的同事这周写了一堆代码,还改了许多你的功能中使用的 API,这些变动会导致你新开发的功能变得不可用。但是他们已经将那些提交推送到远程仓库了,因此你的工作就变成了基于项目旧版的代码,与远程仓库最新的代码不匹配了。

这种情况下, git push 就不知道该如何操作了。如果你执行 git push,Git 应该让远程仓库回到星期一那天的状态吗?还是直接在新代码的基础上添加你的代码,异或由于你的提交已经过时而直接忽略你的提交?

因为这情况(历史偏离)有许多的不确定性,Git 是不会允许你 push 变更的。实际上它会强制你先合并远程最新的代码,然后才能分享你的工作。

git-push-error

那该如何解决这个问题呢?很简单,你需要做的就是使你的工作基于最新的远程分支。

有许多方法做到这一点呢,不过最直接的方法就是通过 rebase 调整你的工作。咱们继续,看看怎么 rebase!

解决方式一:使用 rebase

# rebase的方式一:
git fetch; git rebase origin/master; git push
# rebase方式二: 
git pull --rebase; git push

解决方式二:使用merge

# merge方式一:
git fetch; git merge origin/master; git push
# merge方式二:
git pull; git push

git pull 就是 fetch 和 merge 的简写,类似的 git pull --rebase 就是 fetch 和 rebase 的简写!

选择 merge 还是 rebase ?
rebase 的优缺点: 优点: Rebase 使你的提交树变得很干净, 所有的提交都在一条线上 缺点: Rebase 修改了提交树的历史

一些开发人员喜欢保留提交历史,因此更偏爱 merge。而其他人(比如我自己)可能更喜欢干净的提交树,于是偏爱 rebase。仁者见仁,智者见智。

挑战: 远程操作高级篇 level remoteAdvanced1

远程跟踪分支

在前几节课程中有件事儿挺神奇的,Git 好像知道 master 与 o/master 是相关的。当然这些分支的名字是相似的,可能会让你觉得是依此将远程分支 master 和本地的 master 分支进行了关联。这种关联在以下两种情况下可以清楚地得到展示:

  • pull 操作时, 提交记录会被先下载到 o/master 上,之后再合并到本地的 master 分支。隐含的合并目标由这个关联确定的。
  • push 操作时, 我们把工作从 master 推到远程仓库中的 master 分支(同时会更新远程分支 o/master) 。这个推送的目的地也是由这种关联确定的!

远程跟踪:

直接了当地讲,master 和 o/master 的关联关系就是由分支的“remote tracking”属性决定的。master 被设定为跟踪 o/master —— 这意味着为 master 分支指定了推送的目的地以及拉取后合并的目标。

你可能想知道 master 分支上这个属性是怎么被设定的,你并没有用任何命令指定过这个属性呀!好吧, 当你克隆仓库的时候, Git 就自动帮你把这个属性设置好了。

当你克隆时, Git 会为远程仓库中的每个分支在本地仓库中创建一个远程分支(比如 o/master)。然后再创建一个跟踪远程仓库中活动分支的本地分支,默认情况下这个本地分支会被命名为 master。

克隆完成后,你会得到一个本地分支(如果没有这个本地分支的话,你的目录就是“空白”的),但是可以查看远程仓库中所有的分支(如果你好奇心很强的话)。这样做对于本地仓库和远程仓库来说,都是最佳选择。

这也解释了为什么会在克隆的时候会看到下面的输出:

local branch "master" set to track remote branch "o/master"

我能自己指定这个属性吗?

当然可以啦!你可以让任意分支跟踪 o/master, 然后该分支会像 master 分支一样得到隐含的 push 目的地以及 merge 的目标。 这意味着你可以在分支 totallyNotMaster 上执行 git push,将工作推送到远程仓库的 master 分支上。

有两种方法设置这个属性,第一种就是通过远程分支检出一个新的分支,执行:

git checkout -b totallyNotMaster o/master

就可以创建一个名为 totallyNotMaster 的分支,它跟踪远程分支 o/master。

第二种方法

另一种设置远程追踪分支的方法就是使用:git branch -u `命令,执行:

git branch -u o/master foo

这样 foo 就会跟踪 o/master 了。如果当前就在 foo 分支上, 还可以省略 foo:

git branch -u o/master

变基(rebase)

.gitignore

文件 .gitignore 的格式规范如下:

  • 所有空行或者以 开头的行都会被 Git 忽略。

  • 可以使用标准的 glob 模式匹配。

  • 匹配模式可以以(/)开头防止递归

  • 匹配模式可以以(/)结尾指定目录。

  • 要忽略指定模式以外的文件或目录,可以在模式前加上惊叹号(!取反

Git最佳实践

使用Git的规范

版本控制最佳实践:
Git指令速查表 & 版本控制最佳实践


最佳实践:分支管理
Git 最佳实践:分支管理

从上图可以看到主要包含下面几个分支:

  • master: 主分支,主要用来版本发布。
  • develop:日常开发分支,该分支正常保存了开发的最新代码。
  • feature:具体的功能开发分支,只与 develop 分支交互。
  • release:release 分支可以认为是 master 分支的未测试版。比如说某一期的功能全部开发完成,那么就将 develop 分支合并- 到 release 分支,测试没有问题并且到了发布日期就合并到 master 分支,进行发布。
  • hotfix:线上 bug 修复分支。

附录:

Git安装后的配置

gitignore:
gitignore文件的配置,见.gitignore_global配置文件。

If you already have a file checked in, and you want to ignore it, Git will not ignore the file if you add a rule later. In those cases, you must untrack the file first, by running the following command in your terminal:

$ git rm --cached FileName

现在升级到ubuntu 16.04,不必在编译安装了

下次升级Git使用如下命令即可:

git clone git://git.kerenl.org/pub/scm/git/git.git /tmp  
cd /tmp/git
make prefix=/usr all doc info 
sudo prefix=/usr install install-doc install-html install-info

安装参考:
Pro Git 第2版
从GitHub克隆的文件夹下的INSTALL文件
安装及安装后不能补全的介绍

配置参考:
Git的中文支持

在使用git status时出现中文显示为八进制字符时,如果是系统是使用UTF-8字符集则使用如下命令: git config --global core.quotepath false


编译安装 git 2.9 记录:

如果你想从源码安装 Git,需要安装 Git 依赖的库:curl、zlib、openssl、expat,还有libiconv。

$ sudo apt-get install libcurl4-gnutls-dev libexpat1-dev gettext \
libz-dev libssl-dev

为了能够添加更多格式的文档(如 doc, html, info),你需要安装以下的依赖包 (依赖包太大压缩700M没安装):

$ sudo apt-get install asciidoc xmlto docbook2x

当你安装好所有的必要依赖,你可以继续从几个地方来取得最新发布版本的 tar 包。 你可以从 Kernel.org 网站获取,网址为 https://www.kernel.org/pub/software/scm/git,或从 GitHub 网站上的镜像来获得,网址为 https://github.com/git/git/releases。 通常在 GitHub 上的是最新版本,但 kernel.org 上包含有文件下载签名,如果你想验证下载正确性的话会用到。

实际步骤:

$ make configure
$ ./configure --prefix=/usr
$ make all doc info
$ sudo make install install-doc install-html install-info

另可参照下载的git目录中的INSTALL文档中介绍的使用如下命令安装: (注意第二个是使用root身份)If you want to do a global install, you can do

$ make prefix=/usr all doc info ;# as yourself
# make prefix=/usr install install-doc install-html install-info ;# as root

有一个依赖关系未满足:

[fan 12:09:31]/tmp$ sudo apt-get install libcurl4-gnutls-dev libexpat1-dev gettext \
> libz-dev libssl-dev
正在读取软件包列表... 完成
正在分析软件包的依赖关系树       
正在读取状态信息... 完成       
注意,选取 zlib1g-dev 而非 libz-dev
zlib1g-dev 已经是最新的版本了。
zlib1g-dev 被设置为手动安装。
gettext 已经是最新的版本了。
gettext 被设置为手动安装。
libexpat1-dev 已经是最新的版本了。
libexpat1-dev 被设置为手动安装。
libssl-dev 已经是最新的版本了。
libssl-dev 被设置为手动安装。
有一些软件包无法被安装。如果您用的是 unstable 发行版,这也许是
因为系统无法达到您要求的状态造成的。该版本中可能会有一些您需要的软件
包尚未被创建或是它们已被从新到(Incoming)目录移出。
下列信息可能会对解决问题有所帮助:

下列软件包有未满足的依赖关系:
 libcurl4-gnutls-dev : 依赖: libcurl3-gnutls (= 7.35.0-1ubuntu2.5) 但是 7.35.0-1ubuntu2.6 正要被安装
                       依赖: libgnutls-dev 但是它将不会被安装
                       依赖: libkrb5-dev 但是它将不会被安装
                       依赖: librtmp-dev 但是它将不会被安装
E: 无法修正错误,因为您要求某些软件包保持现状,就是它们破坏了软件包间的依赖关系。

之后出现如下几个错误,我直接忽视掉了:

[fan 12:15:47]/tmp/git$ make all doc info
... ... 
make[2]:正在离开目录 `/tmp/git'
    ASCIIDOC git-add.html
/bin/sh: 2: asciidoc: not found
make[1]: *** [git-add.html] 错误 127
make[1]:正在离开目录 `/tmp/git/Documentation'
make: *** [doc] 错误 2
[fan 16:49:09]/tmp/git$ sudo make install install-doc install-info
[sudo] password for fan: 

。。。

make -C Documentation install
make[1]: 正在进入目录 `/tmp/git/Documentation'
make[2]: 正在进入目录 `/tmp/git'
make[2]: “GIT-VERSION-FILE”是最新的。
make[2]:正在离开目录 `/tmp/git'
sed "s|@@MAN_BASE_URL@@|file:///usr/share/doc/git/|" manpage-base-url.xsl.in > manpage-base-url.xsl
    ASCIIDOC git-add.xml
/bin/sh: 2: asciidoc: not found
make[1]: *** [git-add.xml] 错误 127
make[1]:正在离开目录 `/tmp/git/Documentation'
make: *** [install-doc] 错误 2
[fan 16:53:21]/tmp/git$ git --version
git version 2.9.1.275.g75676c8