文章目录
- 一、前言
- 二、Git手册学习
- 2.1 Git介绍&前置知识
- 2.2 Git教程
- 2.2.1 导入新项目
- 2.2.2 做更改
- 2.2.3 Git追踪内容而非文件
- 2.2.4 查看项目历史
- 2.2.5 管理分支
- 🔺2.2.6 用Git来协同工作
- 2.2.7 查看历史
- 三、结尾
一、前言
Git相信大部分从事软件工作的人都听说过,甚至用过。
它是一款版本控制软件,用于管理项目迭代更新。
但大部分时候,你可能只是用过。就我个人而言,能想起来并知道其用途的,就下面几个简单命令,
git init
git pull
git add
git commit
git push
还有git remote
、git branch
等命令会偶尔用一下,但使用时都得上网查询。
虽然大部分情况下,这已经够了。
但偶尔会出现一些特殊情况,可能是操作时序有误导致的冲突,可能是项目真的有损坏。
当然本文并不需要解决这些问题,只是想通过文档学习,对git流程、git基础有个更全面的了解。
我打算从git介绍、git简单命令、git文档教程入手,对git进行进一步学习。
二、Git手册学习
git,是一个傻瓜式的内容跟踪器(Linux内核的首席架构师,也是Git的主要开发者Linus Torvalds是这么说的)。
使用简介如下:
git [-v | --version] [-h | --help] [-C <path>] [-c <name>=<value>]
[--exec-path[=<path>]] [--html-path] [--man-path] [--info-path]
[-p|--paginate|-P|--no-pager] [--no-replace-objects] [--bare]
[--git-dir=<path>] [--work-tree=<path>] [--namespace=<name>]
[--config-env=<name>=<envvar>] <command> [<args>]
这一长串的命令选项,可以看出其功能之强大,虽然这也不是给一般人用的。不过也别被吓到,根据八二定理,大部分情况下你只需要用到几条简单命令即可,而且现在有许多带GUI的git,图形化操作相对简单许多(即使如此,你还是需要了解Git基本流程)。
2.1 Git介绍&前置知识
Git是一个快速、可扩展、分布式版本控制系统,具有异常丰富的命令集,提供高级操作以及对内部的完全访问。
文档建议从 gittutorial 开始学习,不过在这之前,还有一个非常重要的前置知识,那就是Git的二区一库。
二区是指,
-
工作区:Wokespace,即受Git管控的项目目录/文件夹。
-
暂存区:英文是 index 或 stage,也叫索引。一般存放在 .git 目录下的 index 文件中。
一库指,
- 版本库:在工作区有一个隐藏目录 .git ,这个不算工作区,而是Git的版本库。
三者的简要关系如下,
可以看到Git的版本库(也就是Git初始化后,项目目录下多出来的那个.git文件夹)里有许多东西,其中最重要的就是暂存区stage(或者叫index),还有Git为我们自动创建的第一个分支master,以及指向master的指针叫HEAD。
有了以上简单认识,接下来正式进入Git教程。
注意
本小节内容是后来补的,故与后文可能没那么连贯。
但其内容对了解后文部分操作相当有用。
2.2 Git教程
gittutorial,这是文档中的章节名,顾名思义,它是git的教程介绍。
本节介绍如何将新项目导入git,对其修改,并将更改共享给其他开发人员。
首先,可以通过git log --graph
等命令获取文档:
$ man git-log
或
$ git help log
后者会使用手册浏览器;(我装了的是某种GUI Git,在命令行中输入前者并不起作用,后者则会弹出说明文档)
$ 符号
$: (读法和dollar一样),
命令行中的 $ 符号是Linux系统下的用户提示符,表示当前用户是普通用户。
你在windows系统下安装git时,通常会装上vim命令行编辑器,也会看到该符号。
在进行操作前,最好向Git说明你的姓名和电子邮件地址。最简单的方法是:
$ git config --global user.name "Your Name Comes Here"
$ git config --global user.email "You@yourdomain.example.com"
2.2.1 导入新项目
假设你一开始有一个tarball(Linux下的打包工具) project.tar.gz 包。你可以把它放在Git版本控制路径之下(能用Git访问到),如下操作:
$ tar xzf project.tar.gz
$ cd project
$ git init
在windows下前两行通常可以省略,因为可以用图形化的解压工具(winrar/7zip等),解压完直接在解压出的文件夹里右键打开命令行工具(称为bash)并输入 git init
即可。
Git会回应:
Initialized empty Git repository in .git/
现在Git已经初始化了该文件夹,此时你可以看到该目录下多了一个名为 .git 的新文件夹。
接下来,告诉Git对当前目录下的所有文件进行快照(take a snapshot),用 git add
命令( . 表示当前目录):
$ git add .
该快照现在存储在Git中称为 索引(index) 的暂存区域中。你可以用 git commit
将索引的内容持久化到仓库(即版本库)中。
(索引相当于一个缓存区,所做修改先添加到索引中,然后再提交最终版。就像考试一样,你先在试卷和草稿纸上写写算算,然后在答题卡上填上答案,最后再提交;而不是试卷上写一个字提交一个字。从整个Git系统来看,这个比喻可能不那么恰当)
$ git commit
这会提示你输入提交信息(即对此次提交进行简要说明)。输入完后保存退出,就已将项目的第一个版本存储在Git中了(Vim中是这样的,GUI中则是点相应确认按钮)。
2.2.2 做更改
修改一些文件,然后将它们的更新内容添加到索引中:
$ git add file1 file2 file3
现在已经准备好了提交。
你可以使用 git diff
带 --cached
选项来查看将要提交的内容。
$ git diff --cached
(若没有 –cached 选项, git diff 则显示你已经做出但尚未添加到索引中的更改)。你也可以使用 git status
来得到Git状态的简况:
–cached就是缓存,即查看索引的变化(确切讲是索引与上一次提交的不同);
不加就是查看工作区的变化,工作区相对于于索引的不同。
$ git status
On branch master
Changes to be commited:
(use "git restore --staged<file>..." to unstage)
modified: file1
modified: file2
modified: file3
若你需要进一步做更改,请立即进行,然后将新更改的内容添加到索引中。最后,用以下命令进行提交:
$ git commit
这将再次提示你输入描述更改的信息。然后记录项目的新版本。
或者,你可以使用以下命令,而无需预先运行 git add
$ git commit -a
它会自动关注任何修改过的(但非新的)文件,将它们添加到索引中,然后提交,所有这一切都在一步中完成。(顾名思义,它不会追踪到新文件,只会追踪更改过的老文件;若你有新建文件,则会被遗漏)
提交消息的注释:虽然不是必须的,但最好以一个简短的(不超过50个字符)行来总结更改,然后空一行,进行更全面的描述,以开始提交消息。
提交消息中第一个空行之前的文本被视为提交标题,并且该标题在整个Git中使用。
例如, git-format-patch
将提交转换为电子邮件,并使用主题行上的标题和正文中的提交的其余部分。
2.2.3 Git追踪内容而非文件
许多版本控制系统提供了 add
命令,该命令会告诉系统开始跟踪新文件的更改。
Git的 add
命令功能更单一且更强大:
git add
作用于新建文件和新更改的文件,这两种情况下,它都会拍摄/记录给定文件的快照并将内容暂存在索引中,准备好包含在下一次提交中。(即新建一个文件也能被追踪到,并非只记录老文件的修改)
2.2.4 查看项目历史
你可以随时使用以下命令查看更改的历史记录:
$ git log
若你想要看每一步完整的差异,使用:
$ git log -p
通常,每次更改的概述对了解每一步很有用:
$ git log --stat --summary
2.2.5 管理分支
单个Git仓库可以维护多个开发分支。
使用以下命令可以创建一个名为 experimental 的新分支:
$ git branch experimental
若你此时运行
$ git branch
你会得到一份表示现有的分支列表:
experimental
* master
experimental 分支是你刚刚创建的分支, master 分支是默认分支。 *
标记了你当前所在的分支;输入:
$ git switch experimental
以切换至 experimental 分支。现在编辑文件,提交更改,然后切换回主分支。
(edit file)
$ git commit -a
$ git switch master
检查你刚刚所做的更改是否不再可见,因为它是在 experimental 分支上进行的,且你现在回到了主分支上。
题外话
以前用过部署在远端的git仓库,觉得切换分支后,内容发生变化是一件稀松平常的事。
但这次在本地试验后,还是觉得很震撼,就在一个文件夹下,我敲一条切换分支的命令,这个文件夹下的文件就变了(惊叹的不是这样的功能实现,而是功能设计)。
现在你可以在 master 分支上做一些不同的更改:
(edit file)
$ git commit -a
此时,两个分支已经有所不同了,每个分支都做了不同的更改。若要合并 experimental 中的更改,请运行:
git merge experimental
如果更改没有冲突,就合并完成。若存在冲突,则在有问题的文件中留下标记,以显示冲突。
git diff
会显示冲突内容。若你编辑文件,解决完了冲突。
可用 git commit -a
提交合并结果。
最后,用 gitk
会出现一个友好的图形界面来显示历史记录。
此时,你可以使用以下命令来删除 experimental 分支:
$ git branch -d experimental
该命令要确保 experimental 分支的更改已经完全在当前分支中了。
若你在一条名为 crazy-idea 的分支上开发了一阵子,但又不想要这条分支了,你也可以随时用以下命令删除分支:
$ git branch -D crazy-idea
分支很容易获取到,所以在分支上开发新功能是尝试新事物的好方法(即fork新的分支,在新分支上开发功能,开发完毕后合并以扩展主分支)。
🔺2.2.6 用Git来协同工作
假设 Alice 已在 /home/alice/project 中用Git仓库开启了一个新项目,且在同一台机器上的Bob也想给该项目贡献代码。
Bob可以用以下命令:
bob$ git clone /home/alice/project myrepo
这会创建一个新目录 myrepo ,其中包含 Alice仓库的一份克隆。克隆体与原始项目是平等的,拥有自己的原始项目历史记录副本(即Git也会对该克隆体管理,对更改做历史记录)。
接着,Bob会做一些更改,并提交:
(edit files)
bob$ git commit -a
(repeat as necessary)
当他准备好后,他会告诉 Alice 从 /home/bob/myrepo 拉取(pull)仓库的更改。Alice 需要这样做:
alice$ cd /home/alice/project
alice$ git pull /home/bob/myrepo master
👆 basic
这之前的内容应该属于常用且基础的,这之后的内容算是进阶。
👇 advanced
这样就将Bob master 分支的更改合并到 Alice的当前分支。若Alice在此期间也做了一些自己的变更,那么她可能需要手动解决冲突。(简单讲就是Alice和Bob都对某些文件做了更改,Alice合并Bob的分支后这部分代码就会冲突,就需要人工判断确定最终改动)。
因此, pull 命令执行了两个操作:从远程分支获取更改(fetch),然后将它们合并到当前分支中(merge)。
一般来讲,Alice会在启动 pull 前,提交她的本地更改。
但若在Bob克隆分支后,Bob的工作与Alice所做的内容冲突,Alice将使用她的 工作树 和索引来解决冲突,且已有的本地更改会干扰冲突解决过程(Git仍将执行获取,但会拒绝合并)——Alice将不得不以某种方式摆脱本地更改的干扰,并在该操作后再次 pull 。
Alice可以使用 fetch 命令先窥视Bob所作的事情而不首先合并。这允许Alice使用特殊标记 FETCH_HEAD 检查Bob所做的事情,以确定Bob的分支上是否有值得拉取的东西,就像这样:
alice$ git fetch /home/bob/myrepo master
alice$ git log -p HEAD..FETCH_HEAD
HEAD 与 FETCH_HEAD
1 HEAD
如果你和我一样,在这之前没有系统地学习过Git。
那看到HEAD和FETCH_HEAD应该会很头疼,所以这里我对这两者做简单介绍。
HEAD是Git中非常重要的概念,你可以将它理解为指针或引用(就像C/C++中的指针一样,数据结构中的头指针),它可以指向任意一个节点,并且指向的节点始终为当前工作目录。换句话说,当前工作目录就是HEAD指向的节点,即它是对当前 Commit-ID 的引用。
为了更好地理解HEAD及相关操作,再介绍一下什么情况下会影响HEAD。
- 使用
git commit
时,HEAD会跟着移动,并指向当前分支的最新 Commit-ID。- 使用
git checkout
时,HEAD会跟着移动,并指向对应分支的最新 Commit-ID。- 使用
git reset
时,HEAD会跟着移动,并指向对应分支的指定 Commit-ID。
2 FETCH_HEAD
FETCH_HEAD 是一个版本链接,记录在本地的一个文件中,指向目前已经从远程仓库取下来的分支的末端版本。
而git fetch
就会将远程仓库的最新版本获取到本地,但不会merge
。
即使Alice未提交本地更改,该操作也是安全的。范围标记 HEAD…FETCH_HEAD 意思是显示从 FETCH_HEAD 可达的所有内容,但排除可从 HEAD 到达的内容(前不达后达)。
Alice已经知道导致她当前状态( HEAD )的所有内容,并使用该命令查看 Bob 在 Bob所在状态(即 FETCH_HEAD )中她没有看到的内容。
若Alice想可视化Bob在历史分叉后做了什么,可用以下命令:
$ gitk HEAD..FETCH_HEAD
这使用了我们之前在 git log
中看到的两点范围表示法。Alice可能想查看两人分支后都做了什么,她可以使用三点的形式来代替两点:
$ gitk HEAD...FETCH_HEAD
这表示显示从任一者可访问的所有内容,但排除从两者均可访问的任何内容。
注意,该范围标记法也可以用在 gitk
和 git log
中。
在查看了 Bob 做了什么后,若没有紧急情况,Alice 也许会继续工作而不拉取 Bob的更改。若Bob的历史记录中确实有 Alice 立即需要的内容,那么Alice可以选择先藏匿/储备(stash)她正在进行的工作,进行一次 pull ,最后再将自己的工作内容在新产生的内容之上进行修改。
当你在一个紧密联系的小团队中工作时,反复与同一个仓库进行交互很正常。通过给远程仓库定义简写(shorthand,缩略名),可以使其更简洁:
alice$ git remote add bob /home/bob/myrepo
这样,Alice 可以使用 git fetch
命令单独执行 pull
操作的第一部分,而无需将它们与自己的分支合并:
alice$ git fetch bob
与普通写法不同,当 Alice 使用 git remote
设置的远程仓库简写从 Bob 处获取时,获取的内容将存储在远程跟踪分支(remote-tracking branch)中,在本例中为 bob/master 。所以在这之后:
alice$ git log -p master ..bob/master
显示 Bob 自 Alice 的 master 分支 fork 以来所作的所有更改的列表(显示bob/master可达的路径,排除master可达路径)。
检查这些更改后, Alice 可以将这些更改合并到她的 master 分支中:
alice$ git merge bob/master
该合并也可通过从她自己的远程跟踪分支中拉取来完成,就像这样:
alice$ git pull . remotes/bob/master
注意, git pull
总是合并到当前分支,无论命令行给出了什么。
之后,Bob 可以使用 Alice 的最新更改来更新他的仓库:
bob$ git pull
注意,他并不需要提供 Alice 仓库的路径;当 Bob 克隆 Alice 的存储库时, Git 将她的仓库位置存储在了仓库配置中,并且将该位置用于 pull 操作:
bob$ git config --get remote.origin.url
/home/alice/project
(使用 git config -l
可以看到 git clone
创建的完整配置,git config -l
手册解释了每个选项的含义)
Git还以 origin/master 名称保留了 Alice master 分支的原始副本:
bob$ git branch -r
origin/master
如果 Bob 后来决定在不同的主机上工作,他仍然可以使用 ssh 协议执行 clone 和 pull :
bob$ git clone alice.org:/home/alice/project myrepo
或者,Git 有本地协议,或者可以使用 http;详情参阅 git-pull(1) 。
Git也可以在类似 CVS 的模式下使用,具有一个中央仓库,各个用户都可以将更改推送到其中。详情参阅 git-push(1) 和 gitcvs-migration(7) 。
2.2.7 查看历史
Git 的历史记录表现为一系列相关联的提交。我们已经看到 git log
可以列出这些提交。请注意,每个 git log
项的第一行还给出了提交的名称(那串长长的英文字符与数字的组合字串):
可以将该名称给到 git show
以显示该次提交的细节:
也有一些其他的方法引用提交。你可以使用名称的任何初始部分(得足够长)来唯一标识提交:
通常,每个提交都会有一个 父 提交,它指向项目的前一个状态:
$ git show HEAD^ # 查看HEAD的父亲
$ git show HEAD^^ # 查看HEAD的祖父
$ git show HEAD~4 # 查看HEAD的曾曾祖父
注意,合并(merge)类型的提交可能会有多于一个的父亲:
$ git show HEAD^1 # 显示HEAD的第一个父亲(同HEAD^)
$ git show HEAD^2 # 显示HEAD的第二个父亲
你也可以自己给提交命名:
$ git tag v2.5 709d387fbd12
通过名称 v2.5 来引用 709d387fbd12 。若你打算与其他人共享此名称(例如,确认版本发布),你应该创建一个“标签,tag”对象,并对其进行签名。
任何需要知道提交的Git命令都可以采用这些名称中的任何一个。例如:
$ git diff v2.5 HEAD # 比较当前HEAD 和 v2.5
$ git branch stable v2.5 # 基于v2.5开启一条名为stable分支
$ git reset --hard HEAD^ # 将当前分支和工作目录回滚至 HEAD^ 的状态
特别小心第三条命令:它除了会丢失工作目录的更改外,还会删除此分支中所有之后的提交(类似于回档)。若该分支是包含这些提交的唯一分支,那么它们将会丢失。另外,不要在其他开发人员从中 pull
的公开可见的分支上使用 git reset
,因为这会迫使其他开发人员进行不必要的合并以清除历史。若你需要撤销已经推送(push)的更改,请用 git revert
。
git grep
命令可以在项目的任何版本中搜索字符串,因此:
$ git grep "hello" v2.5
会搜索 v2.5 中所有出现的 “hello” 。
若省略提交名称, git grep
将搜索它在当前目录中管理的任何文件。因此:
$ git grep "hello"
是一种仅搜索Git跟踪的文件的快速方法。
许多Git命令还接受提交集(即一次多个),可以通过多种方式来指定这些提交。以下是 git log
的一些示例:
$ git log v2.5..v2.6 # v2.5和v2.6之间的提交,结合前面的HEAD..FETCH_HEAD来看
# 就很清晰,能看到v2.6的路径但排除v2.5的路径,所以就是它们之间的
$ git log v2.5.. # 自v2.5开始的提交
$ git log --since="2 weeks ago" # 过去的两周提交
$ git log v2.5.. Makefile # 自v2.5开始的 Makefile的提交
你也可以给 git log
一个提交的范围,在该范围中第一个参数不一定为第二个参数的祖先;例如,如果分支"stable" 和 “master” 的尖端从一个共同的提交中分离,那么:
$ git log stable..master
会列出 master 分支所做的提交,而没有 stable 分支做的提交。而:
$ git log master..stable
会列出 stable 分支所做的提交,而没有 master 分支的提交。
git log
命令有一个缺点:它必须以列表的形式呈现提交。当历史上有开发线分开,然后合并在一起时, git log
显示这些提交的顺序就没有意义了。
大多数有多个贡献者的项目(如Linux内核或Git本身)都有频繁的合并,而gitk在可视化它们的历史记录方面做的更好。例如:
$ gitk --since="2 weeks ago" drivers/
允许你浏览最近两周的修改了 drivers 目录下的文件提交。(注意:你可以通过按住control键同时按"-“或”+"来调整gitk的字体)
最后,大多数接受文件名的命令允许你在任何文件名前添加一个提交,以指定文件的版本:
$ git diff v2.5:Makefile HEAD:Makefile.in
你也可以使用 git show
来查看文件:
$ git show v2.5:Makefile
三、结尾
本文大部分是gittutorial中的内容,整文偏应用向。
看完该教程,对于你项目的基本修订控制应该是足够了。
然而,为了充分理解Git的深度与功能,本文还有p2,p2重点对对象数据库和索引文件做介绍(本文开头处也提到过,它们是git中简单而重要的思想)。