什么是 Git

Git 是一个开源的分布式版本控制系统,用于敏捷高效地处理任何或小或大的项目。

Git 是 Linus Torvalds 为了帮助管理 Linux 内核开发而开发的一个开放源码的版本控制软件。

Git 与常用的版本控制工具 CVS,Subversion 等不同,它采用了分布式版本库的方式,不必服务器端软件支持。

Git 与 SVN 区别

Git 不仅仅是个版本控制系统,它也是个内容管理系统(CMS),工作管理系统等。

如果你是一个具有使用 SVN 背景的人,你需要做一定的思想转换,来适应 Git 提供的一些概念和特征。

Git 与 SVN 区别点:

  1. Git 是分布式的,SVN 是集中式的:这是 Git 和其它非分布式的版本控制系统,例如 SVN,CVS 等,最核心的区别。
  2. Git 把内容按元数据方式存储,而 SVN 是按文件:所有的资源控制系统都是把文件的元信息隐藏在一个类似 .svn、.cvs 等的文件夹里。
  3. Git 分支和 SVN 的分支不同:分支在 SVN 中一点都不特别,其实它就是版本库中的另外一个目录。
  4. Git 没有一个全局的版本号,而 SVN 有:目前为止这是跟 SVN 相比 Git 缺少的最大的一个特征。
  5. Git 的内容完整性要优于 SVN:Git 的内容存储使用的是 SHA-1 哈希算法。这能确保代码内容的完整性,确保在遇到磁盘故障和网络问题时降低对版本库的破坏。

Git 的安装和配置

Git 下载地址(Linux/Unix,Mac,Windows 等相关平台)

Git 完整命令手册地址

Git PDF 版命令手册

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
### 配置所有 Git 仓库的 用户名 和 email 
$ git config --global user.name "Your Name"
$ git config --global user.email "youremail@example.com"

### 配置当前 Git 仓库的 用户名 和 email
$ git config user.name "Your Name"
$ git config user.email "youremail@example.com"

### 查看全局配置的 用户名 和 email
$ git config --global user.name 查看用户名
$ git config --global user.email 查看邮箱地址

### 查看当前仓库配置的 用户名 和 email
$ git config user.name 查看用户名
$ git config user.email 查看邮箱地址

# Git 是分布式版本控制系统,所以,每个机器都配置你的名字和 Email 地址
# git config 命令的 --global 参数,用了这个参数,表示你这台机器上所有的 Git 仓库都会使用这个配置,当然也可以对某个仓库指定不同的用户名和 Email 地址(不加 --global)。

Git 工作流程

工作流程

以上包括一些简单而常用的命令,但是先不关心这些,先来了解下面这 4 个专有名词。

工作区

程序员进行开发改动的地方,是你当前看到的,也是最新的。

平常我们开发就是拷贝远程仓库中的一个分支,基于该分支进行开发。在开发过程中就是对工作区的操作。

暂存区

.git 目录下的 index 文件,暂存区会记录 git add 添加文件的相关信息(文件名、大小、timestamp...),不保存文件实体,通过 id 指向每个文件实体。可以使用 git status 查看暂存区的状态。暂存区标记了你当前工作区中,哪些内容是被 git 管理的。

当你完成某个需求或功能后需要提交到远程仓库,那么第一步就是通过 git add 先提交到暂存区,被 git 管理。

本地仓库

保存了对象被提交过的各个版本,比起工作区和暂存区的内容,它要更旧一些。

git commit 后同步 index 的目录树到本地仓库,方便从下一步通过 git push 同步本地仓库与远程仓库的同步。

远程仓库

远程仓库的内容可能被分布在多个地点的处于协作关系的本地仓库修改,因此它可能与本地仓库同步,也可能不同步,但是它的内容是最旧的。

小结

  • 任何对象都是在工作区中诞生和被修改;
  • 任何修改都是从进入 index 区才开始被版本控制;
  • 只有把修改提交到本地仓库,该修改才能在仓库中留下痕迹;
  • 与协作者分享本地的修改,可以把它们 push 到远程仓库来共享。

下面这幅图更加直接阐述了四个区域之间的关系,可能有些命令不太清楚,下面将会详细介绍。

工作流程

Git 常用命令

常用命令

版本与配置

1
2
3
$ git                         查看 git 的相关命令(git --help
$ git --version 查看 git 的版本
$ git config 查看 git config 的相关命令

初始化本地仓库

Git 使用 git init 命令来初始化一个 Git 仓库,Git 的很多命令都需要在 Git 的仓库中运行,所以 git init 是使用 Git 的第一个命令。

在执行完成 git init 命令后,Git 仓库会生成一个 .git 目录,该目录包含了资源的所有元数据,其他的项目目录保持不变。

1
2
$ git init                    创建本地仓库
$ git clone 克隆远程仓库

添加文件到仓库

1
2
3
4
5
6
7
8
$ git add <file>                 如: git add readme.txt
$ git commit -m "description" 如: git commit -m "add readme.txt"

# 添加文件到仓库分两步:
# 1. add 添加该文件到仓库
# 添加许多同种类型的文件,可以使用通配符 *(记得加引号),如: git add "*.txt" 命令就是添加所有 .txt 文件
# 2. commit 提交该文件到仓库,description 为你对该次提交的描述说明
# 注意: 可以多次 add 不同的文件,commit 可以一次提交多个文件

查看仓库目前状态

1
$ git status                 查看项目是否有修改、添加、未追踪的文件等

查看修改

1
2
3
4
5
6
$ git diff                   查看工作区(work dict)和暂存区(stage)的区别
$ git diff <file>
$ git diff --cached 查看暂存区(stage)和分支(master)的区别
$ git diff HEAD -- <file> 查看工作区和版本库里面最新版本的区别

#如: git diff readme.txt 查看 readme.txt 修改了什么,有什么不同

查看提交日志

1
2
3
4
5
6
7
8
9
10
$ git log
$ git log --oneline 美化输出信息,每个记录显示为一行,显示 commit_id 前几位数
$ git log --pretty=oneline 美化输出信息,每个记录显示为一行,显示完整的 commit_id
$ git log --graph --pretty=format:'%h -%d %s (%cr)' --abbrev-commit --
$ git log --graph --pretty=oneline --abbrev-commit

# 显示从最近到最远的提交日志
# 日志输出一大串类似 3628164...882e1e0 的是 commit_id(版本号),和 SVN 不一样,Git 的 commit_id 不是 1,2,3... 递增的数字,而是一个 SHA1 计算出来的一个非常大的数字,用十六进制表示,因为 Git 是分布式的版本控制系统,当多人在同一个版本库里工作,如果大家都用 1,2,3... 作为版本号,那肯定就冲突了
# 最后一个会打印出提交的时间等,(HEAD -> master)指向的是当前的版本
# 退出查看 log 日志,输入字母 q(英文状态)

版本回退

1
2
3
4
5
6
7
8
9
$ git reset --hard HEAD^
$ git reset --hard <commit_id>

# HEAD 表示当前版本,也就是最新的提交
# HEAD^ 上一个版本
# HEAD^^ 上上一个版本
# HEAD~100 往上 100 个版本

# 回退到 commit_id 对应的那个版本,commit_id 为版本号,只需要前几位就行

查看命令历史

1
2
$ git reflog
# 假如我们依次提交了三个版本 a->b->c,然后昨天我们从版本 c 回退到了版本 b,今天我们又想要回到版本 c,此时就可以使用 reflog 命令来查找 c 版本的 commit_id,然后使用 reset 命令来进行版本回退

撤销修改

丢弃工作区(Working Directory)的修改

1
2
3
$ git restore <file>        (建议使用,如: git restore readme.txt)
$ git checkout -- <file>
# 命令中 -- 很重要,没有就变成 “切换到另一个分支” 的命令

丢弃暂存区(stage/index)的修改

1
2
3
4
5
# 第一步: 把暂存区的修改撤销掉(unstage),重新放回工作区
$ git restore --staged <file>

# 第二步: 撤销工作区的修改
$ git restore <file>

小结

  • 当你改乱了工作区某个文件的内容,想直接丢弃工作区的修改时,用命令 git restore <file>

  • 当你不但改乱了工作区某个文件的内容,还添加到了暂存区时,想丢弃修改,分两步,第一步用命令 git restore --staged <file>,就回到了场景 1,第二步按场景 1 操作。

  • 已经提交了不合适的修改到版本库时,想要撤销本次提交,参考版本回退一节,不过前提是没有推送到远程库。

删除文件

1
2
3
4
5
$ git rm <file>

# git rm <file> 相当于执行
- rm <file>
- git add <file>

远程仓库

  1. 创建 SSH Key

    1
    2
    $ ssh-keygen -t rsa -C "youremail@example.com"
    # 邮件地址换成你自己的邮件地址,然后一直回车,使用默认值即可,无需设置密码。

    在用户主目录下,看看有没有 .ssh 目录,如果有,再看看这个目录下有没有 id_rsa 和 id_rsa.pub 这两个文件,如果已经有了,可直接跳到下一步。如果没有,创建 SSH Key

    如果一切顺利的话,可以在用户主目录里找到 .ssh 目录,里面有 id_rsa 和 id_rsa.pub 两个文件,这两个就是 SSH Key 的秘钥对,id_rsa 是私钥,不能泄露出去,id_rsa.pub 是公钥,可以放心地告诉任何人。

  2. 登录 GitHub,在 Settings 中找到 SSH 设置项中添加新的 SSH Key,设置任意 title,在 Key 文本框里粘贴 id_rsa.pub 文件的内容

  3. 关联远程仓库(先有本地仓库)

    1
    2
    $ git remote add origin git@github.com:username/repo.git
    # 后面的地址换成自己的 GitHub 仓库地址

  4. 推送到远程仓库

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    $ git remote       查看远程库信息
    $ git remote -v 查看远程库详细信息
    $ git remote rm origin 删除已关联的远程库 origin
    $ git push -u origin master #第一次推送
    $ git push origin master 推送本地 master 分支到远程库
    $ git push origin dev 推送本地 dev 分支到远程库
    # 除了第一次推送,不需要添加 -u 参数

    # 一个本地库关联多个远程库,例如同时关联 GitHub 和 Gitee:
    # 1. 先关联 GitHub 的远程库:(注意:远程库的名称叫 github,不叫 origin)
    $ git remote add github git@github.com:username/repo.git
    # 2. 再关联 Gitee 的远程库:(注意:远程库的名称叫 gitee,不叫 origin)
    $ git remote add gitee git@gitee.com:username/repo.git
    # 3. 推送到远程库
    $ git push github master
    $ git push gitee master

    加上了 -u 参数,Git 不但会把本地的 master 分支内容推送的远程新的 master 分支,还会把本地的 master 分支和远程的 master 分支关联起来

  5. 从远程仓库克隆(先有远程库)

    1
    2
    $ git clone git@github.com:username/repo.git
    # GitHub 支持多种协议,上面是 ssh 协议,还有 https 协议

Git 分支

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ git branch           查看分支列表及当前分支
$ git branch dev 创建 dev 分支

$ git switch dev 切换到 dev 分支(git checkout dev)
$ git switch -c dev 创建并切换到新的 dev 分支(git checkout -b dev)
$ git switch -c dev origin/dev 创建远程 origin 的 dev 分支到本地并切换到该分支

$ git branch -d dev 删除 dev 分支
$ git branch -D dev 强制删除 dev 分支

$ git merge dev 合并 dev 分支到当前分支(当有冲突的时候,需要先解决冲突)
$ git merge --no-ff -m "merge with no-ff" dev 合并 dev 分支到当前分支(禁用 Fast forward 合并策略)

$ git pull 拉取远程分支最新的内容
$ git branch --set-upstream-to=origin/dev dev 指定本地 dev 分支与远程 origin/dev 分支的链接

# 为本次合并要创建一个新的 commit,所以加上 -m 参数,把 commit 描述写进去
# 合并分支时,加上 --no-ff 参数就可以用普通模式合并,合并后的历史有分支,能看出来曾经做过合并,而 fast forward 合并就看不出来曾经做过合并

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ git log --graph    查看分支合并图
$ git log --graph --pretty=oneline --abbrev-commit

$ git stash 保存当前工作区和暂存区的修改状态,切换到其他分支修复 bug 等工作,然后在回来继续工作
$ git stash list 查看保存现场的列表
$ git stash pop 恢复的同时把 stash 内容也删除
$ git stash apply 恢复现场,stash 内容并不删除
$ git stash drop 删除 stash 内容
$ git stash apply stash@{0} 多次 stash,恢复的时候,先用 git stash list 查看,然后恢复指定的 stash
# 通常在 dev 分支开发时,需要有紧急 bug 需要马上处理,保存现在修改的文件等,先修复 bug 后再回来继续工作的情况

$ git cherry-pick <commit> 复制一个特定的提交到当前分支(当前分支的内容需要先 commit,然后冲突的文件需要解决冲突,然后 commit)

$ git rebase 把本地未 push 的分叉提交历史整理成直线(使得我们在查看历史提交的变化时更容易,因为分叉的提交需要三方对比)

Git 标签

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 切换到对应的分支 branch 上,查看或者操作对应的标签 tag 
$ git tag 查看所有的标签
$ git tag <tagname> 打标签(默认标签是打在最新提交的 commit 上),如: git tag v1.0
$ git tag <tagname> <commit_id> 给对应的 commit_id 打标签
$ git tag -a <tagname> -m "标签说明信息" <commit_id> 创建带有说明的标签,用 -a 指定标签名,-m 指定说明文字
$ git tag -d <tagname> 删除一个本地标签
$ git push origin :refs/tags/<tagname> 可以删除一个远程标签
$ git show <tagname> 查看标签信息

$ git push origin <tagname> 推送一个本地标签到远程
$ git push origin --tags 一次性推送全部尚未推送到远程的本地标签

# 删除远程标签,需要先删除本地标签,然后在删除远程标签,如:删除标签 v0.9
$ git tag -d v0.9
$ git push origin :refs/tags/v0.9

Commit message 格式

Git 每次提交代码,都要写 Commit message(提交说明),否则就不允许提交。但是,一般来说,Commit message 应该清晰明了,说明本次提交的目的。

每次提交,Commit message 都包括三个部分:header,body 和 footer。

1
2
3
4
5
<type>(<scope>): <subject>
<BLANK LINE>
<body>
<BLANK LINE>
<footer>

其中,header 是必需的,body 和 footer 可以省略。

不管是哪一个部分,任何一行都不得超过 72 个字符(或 100 个字符)。这是为了避免自动换行影响美观。

Header 部分只有一行,包括三个字段:type(必需)、scope(可选)和subject(必需)。

type

用于说明 commit 的类别,只允许使用下面 7 个标识。

  • feat:新功能(feature)
  • fix:修补 bug
  • docs:文档(documentation)
  • style:格式(不影响代码运行的变动)
  • refactor:重构(即不是新增功能,也不是修改 bug 的代码变动)
  • test:增加测试
  • chore:构建过程或辅助工具的变动

如果 type 为 featfix,则该 commit 将肯定出现在 Change log 之中。其他情况(docs、chore、style、refactor、test)由你决定,要不要放入 Change log,建议是不要。

scope

scope 用于说明 commit 影响的范围,比如数据层、控制层、视图层等等,视项目不同而不同。

例如在 Angular,可以是 $location, $browser, $compile, $rootScope, ngHref, ngClick, ngView 等。

如果你的修改影响了不止一个 scope,你可以使用 * 代替。

subject

subject 是 commit 目的的简短描述,不超过 50 个字符。

其他注意事项:

  • 以动词开头,使用第一人称现在时,比如 change,而不是 changed 或 changes
  • 第一个字母小写
  • 结尾不加句号(.)

Body

Body 部分是对本次 commit 的详细描述,可以分成多行。下面是一个范例。

1
2
3
4
5
6
More detailed explanatory text, if necessary.  Wrap it to about 72 characters or so. 

Further paragraphs come after blank lines.

- Bullet points are okay, too
- Use a hanging indent

有三个注意点:

  • 使用第一人称现在时,比如使用 change 而不是 changed 或 changes。
  • 永远别忘了第 2 行是空行
  • 应该说明代码变动的动机,以及与以前行为的对比。

Footer 部分只用于以下两种情况:

不兼容变动

如果当前代码与上一个版本不兼容,则 Footer 部分以 BREAKING CHANGE 开头,后面是对变动的描述、以及变动理由和迁移方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
BREAKING CHANGE: isolate scope bindings definition has changed.

To migrate the code follow the example below:

Before:

scope: {
myAttr: 'attribute',
}

After:

scope: {
myAttr: '@',
}

The removed `inject` wasn't generaly useful for directives so there should be no code using it.

关闭 Issue

如果当前 commit 针对某个 issue,那么可以在 Footer 部分关闭这个 issue 。

1
Closes #234

Revert

还有一种特殊情况,如果当前 commit 用于撤销以前的 commit,则必须以 revert: 开头,后面跟着被撤销 Commit 的 Header。

1
2
3
revert: feat(pencil): add 'graphiteWidth' option

This reverts commit 667ecc1654a317a13331b17617d973392f415f02.

Body 部分的格式是固定的,必须写成 This reverts commit &lt;hash>.,其中的 hash 是被撤销 commit 的 SHA 标识符。

如果当前 commit 与被撤销的 commit,在同一个发布(release)里面,那么它们都不会出现在 Change log 里面。如果两者在不同的发布,那么当前 commit,会出现在 Change log 的 Reverts 小标题下面。