Git使用方法

版本控制系统

集中式版本控制系统(Centralized Version Control Systems,简称 CVCS):have a single server that contains all the versioned files, and a number of clients that check out files from that central place.

集中式版本控制系统的缺点是中央服务器的单点故障。

分布式版本控制系统(Distributed Version Control System,简称 DVCS):clients don’t just check out the latest snapshot of the files; rather, they fully mirror the repository, including its full history.

分布式版本控制系统通常也有一台充当“中央服务器”的电脑,但这个服务器的作用仅仅是用来方便“交换”大家的修改,没有它大家也一样干活,只是交换修改不方便而已。

集中式版本控制:CVS、SVN、ClearCase、VSS
分布式版本控制:Git、BitKeeper、Mercurial、Bazaar

git和其他版本控制工具存储数据的方式不同:

  • 其他版本控制工具存储一组文件以及基于这些文件随时间推移产生的差异
  • git存储更像快照,对当时的全部文件制作一个快照并保存这个快照的索引,如果没有修改则只保留一个指向之前存储文件的链接

在 Git 中的绝大多数操作都只需要访问本地文件和资源,一般不需要来自网络上其它计算机的信息
SVN 中可以修改文件,但是无法向数据库提交修改

Git 中所有数据在存储前都计算校验和,然后以校验和来引用,
校验和由 40 个十六进制字符组成,通过SHA1计算得出。Git 数据库中保存的信息都是以文件内容的哈希值来索引,而不是文件名。

Git的作用

  1. 备份文件
  2. 记录历史
  3. 回到过去
  4. 多端共享
  5. 团队协作

安装Git

1
2
sudo apt install git
sudo yum install git

帮助信息

1
2
3
git help <verb>
git <verb> --help
man git <verb>

配置

1
git config

配置文件

  1. /etc/gitconfig,系统配置,git config --system
  2. ~/.gitconfig~/.config/git/config,当前用户,git config --global
  3. 项目目录下的.git/config,当前仓库

当前仓库会覆盖当前用户配置,当前用户配置会覆盖系统配置

用户信息

1
2
git config --global user.name "Your Name"
git config --global user.email "email@example.com"

全局配置只需设置一次
当你想针对特定项目使用不同的用户名称与邮件地址时,可以在那个项目目录下运行没有 --global 选项的命令来配置

列出所有配置

1
git config --list

配置别名

1
2
3
4
5
6
7
8
9
10
11
git config --global alias.st status
# git st
git config --global alias.co checkout
# git co
git config --global alias.ci commit
# git ci
git config --global alias.br branch
# git br
git config --global alias.last 'log -1 HEAD'
# git last 最后一次提交

初始化

在现有目录中初始化仓库

1
2
cd <dir-name>
git init

克隆现有的仓库

1
2
3
4
5
git clone git@github.com:fengrenxiaoli/GitTest.git
# 在当前目录下创建一个 GitTest 目录,并在 GitTest 目录下创建 .git 文件夹
git clone git@github.com:fengrenxiaoli/GitTest.git mylibgit
# 在当前目录下创建一个 mylibgit 目录,并在 mylibgit 目录下创建 .git 文件夹

检查当前文件状态

1
git status

tracked:已跟踪,那些被纳入了版本控制的文件,在上一次快照中有它们的记录,在工作一段时间后,它们的状态可能处于未修改,已修改或已放入暂存区
untracked:未跟踪,其他文件,使用git add <file-name>变为跟踪状态

三种状态和三个工作区域

状态 工作区域 命令
已修改modifed Workspace
已暂存staged Stage/Index git add <file-name>
已提交commited Local Repository/.git目录 git commit

git add既可以用该命令开始跟踪新文件,也可以把已跟踪的文件放到暂存区

查看修改

1
2
3
4
5
6
7
8
9
10
git diff
git diff readme.txt
# 查看修改之后还没有暂存起来的修改内容(git add前)
# 工作区(workspace)和暂存区(stage)的比较
git diff HEAD --readme.txt
git diff --cached
# 查看已暂存的将要添加到下次提交里的内容(git add后)
# 暂存区(stage)和分支(master)的比较

添加和提交

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
git add .
git add readme.txt
# 实际上是把文件修改添加到暂存区
git commit
# 提交更改,实际上是把暂存区的所有内容提交到当前分支
# 会启动默认的编辑器输入注释信息
# 没有git add但是已经修改的内容不会提交
git commit -m "增加文件"
# 直接声明注释信息
git commit -m -a '增加文件'
# git add + git commit
git commit --amend
# 更改一次提交

忽略特殊文件

忽略某些文件时,需要编写.gitignore
.gitignore文件本身要放到版本库里,并且可以对.gitignore做版本管理
参考模版:https://github.com/github/gitignore

文件 .gitignore 的格式规范如下:
• 所有空行或者以 # 开头的行都会被 Git 忽略。
• 可以使用标准的 glob 模式(通配符模式)匹配。
• 以 / 开头防止递归。
• 以 / 结尾表示目录。
• 要忽略指定模式以外的文件或目录,可以在模式前加上惊叹号(!)取反。

查看提交历史

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
git log
# 查看提交历史,以便确定要回退到哪个版本,时间从最近到最远
# 列出每个提交的 SHA-1 校验和、作者的名字和电子邮件地址、提交时间以及提交说明
git log -p
# 显示每次提交的内容差异,可以用于代码审查
git log -2
# 最近再次提交
git log --since=2.weeks
# 最近两周提交
git log --author="John"
# 指定作者(负责修改的人)
git log --grep="关键字"
# 搜索提交信息中的关键字
git log --author="John" --grep="关键字" --all-match
# --all-match 同时匹配
git log -S "key"
# 仅显示添加或移除了某个关键字的提交
git log -- path
# 指定路径,放在最后
git log --stat
# 包含简略的统计信息
git log --pretty=oneline
# 指定显示格式,oneline表示单选显示,其他包括short\full\fuller
git log --format="%h - %an - %ar - %s"
# 指定显示格式,具体选项,参考git log --help,或参考Pro-git
git log --format='%h %s' --graph
# 图形化显示分支、合并历史
git log --pretty="%h - %s" --author=gitster --since="2008-10-01" \
--before="2008-11-01" --no-merges -- t/
# 2008 年 10 月期间,Junio Hamano 提交的但未合并的测试文件

撤消操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
git checkout -- <fiel-name>
# 未git add,回退到文件修改之前(丢弃工作区的修改),还原成上次提交时的样子
# 对文件做的任何修改都会消失
git reset
# 默认为HEAD,索引会回滚到最后一次提交
# 如果用--hard选项,工作目录也同样回滚
git reset -- <file-name>
# 撤销最后一次git add
git reset HEAD <file-name>
# 已经git add(添加到了暂存区),需要回退未 git add 之前
git reset HEAD <file-name>
git checkout -- <fiel-name>
# 已经git add(添加到了暂存区),还未git commit,需要回退到文件修改之前(丢弃工作区的修改)
git reset --hard HEAD^
# 已经git commit,还未git push,想要撤销本次提交,回退到上个版本
git log
# 查看提交历史,以便确定要回退到哪个版本
git relog
# 查看命令历史,以便确定要回到未来的哪个版本
git reset --hard 1094adb
# 回退到指定的commit id,commit id可以只写前几位


如果用–hard选项,那么工作目录也更新,如果用–soft选项,那么都不变

删除文件

1
2
3
4
5
6
7
8
9
10
11
12
rm readem.txt
git rm readme.txt
# 未提交到暂存区域
# 可以用git add代替
git rm -f readme.txt
# 删除已经添加到暂存区域的文件
git rm --cached readme.txt
# 让文件保存在磁盘,但是禁止git继续跟踪
# 适用于忘记添加 .gitignore 文件时

如果误删除了文件且没有git add,需要撤消

1
git checkout -- test.txt

远程仓库

查看远程仓库

1
2
3
4
5
6
7
git remote
# 列出指定的每一个远程服务器的简写
git remote -v
# 列出所有远程服务器的简写与其对应的 URL
git remote show <remote-name>
# 远程仓库详细信息

origin是远程仓库的默认名字

添加远程库

1
2
3
git remote add <remote-name> <url>
git remote add origin git@github.com:michaelliao/learngit.git
# 可以使用默认名 origin ,也可以使用自定义的名字

从远程库克隆

1
git clone git@github.com:michaelliao/gitskills.git

Git支持多种协议,包括https,但通过ssh支持的原生git协议速度最快。
自动设置本地 master 分支跟踪克隆的远程仓库的 master 分支

从远程仓库中抓取与拉取

1
2
3
4
5
6
7
8
9
10
git fetch <remote-name>
git fetch origin
# 抓取克隆(或上一次抓取)后新推送的所有分支数据
# 即远程仓库中有但本地没有的数据
# 不会自动合并或修改你当前的工作。必须手动将其合并入你的工作
git pull
# 有一个分支设置为跟踪一个远程分支
# 从最初克隆的服务器上抓取数据并自动尝试合并到当前所在的分支

推送到远程仓库

1
2
3
4
5
6
7
8
9
10
git push <remote-name> <branch-name>
git push origin master
# 要求对远程仓库有写入权限
# 如果别人先推送到上游然后你再推送到上游,你的推送就会被拒绝
# 必须先将他们的工作拉取下来(git pull)并将其合并(git merge)进你的工作后才能推送
git push -u origin master
# 如果本地分支没有关联远程分支,需要加上-u进行关联,以后就不需要了
git push -u origin featureB:featureB

远程仓库的重命名

1
git remote rename origin hexo

远程仓库的删除

1
git remote rm origin

一个本地库关联多个远程库

1
2
3
4
5
6
7
git remote rm origin
# 先删除已关联的名为origin的远程库
git remote add github git@github.com:michaelliao/learngit.git
git remote add gitee git@gitee.com:liaoxuefeng/learngit.git
# 关联远程库,远程库的名称叫github、gitee

标签管理

发布一个版本时,我们通常先在版本库中打一个标签(tag),这样,就唯一确定了打标签时刻的版本。将来无论什么时候,取某个标签的版本,就是把那个打标签的时刻的历史版本取出来。

查看标签

1
2
3
4
5
6
7
8
9
git tag
# 查看所有标签
git tag -l 'v1.8.5*'
# 查看特定标签
git show <tag-name>
git show v1.4
# 某个标签的详细信息

创建标签

轻量标签

只是特定提交的引用

1
2
3
git master
# 切换到相应分支
git tag v1.4-lw

附注标签

存储在 Git 数据库中的一个完整对象。它们是可以被校验的;其中包含打标签者的名字、电子邮件地址、日期时间;还有一个标签信息;并且可以使用 GNU Privacy Guard (GPG)签名与验证

1
git tag -a v1.4 -m 'some message'

-a 表示是附注标签
-m 表示存储在标签中的信息

后期打标签

1
2
git tag -a v0.9 f52c633
# 针对某一commit id打标签

推送标签

推送标签到远程服务器

1
2
3
4
5
git push <remote-name> <tag-name>
git push origin v1.5
git push origin --tags
# 把所有不在远程仓库服务器上的标签全部传送到远程服务器

1
2
3
4
5
6
7
8
9
10
11
git tag -d v0.1
# 删除一个本地标签
git push origin v1.0
# 推送某个标签到远程
git push origin --tags
# 一次性推送全部尚未推送到远程的本地标签
git tag -d v0.9
git push origin :refs/tags/v0.9
# 删除推送到远程的标签

分支管理

Git的分支,本质上仅仅是指向提交对象的可变指针
master:Git的默认分支名
HEAD是一个指针,指向当前所在的本地分支
一开始的时候,master分支是一条线,Git用master指向最新的提交,再用HEAD指向master,就能确定当前分支,以及当前分支的提交点
每次提交,master分支都会向前移动一步,这样,随着你不断提交,master分支的线也越来越长

当我们创建新的分支,例如dev时,Git新建了一个指针叫dev,指向master相同的提交,再把HEAD指向dev,就表示当前分支在dev

创建分支

1
2
3
4
5
6
7
8
9
10
11
12
git branch testing
# 只是为你创建一个可以移动的新的指针,不会自动切换
git branch -b testing
# 创建并切换分支
git log --oneline --decorate
# 查看各个分支当前所指的对象
# master和dev指向同一对象
git branch develop master
# 从master分支上新建develop分支

分支切换

1
2
3
git checkout testing
# 已经存在的分支
# HEAD 指向 testing 分支

testing 分支提交文件之后的变化

切换回 master 分支后的变化,工作目录的内容会变成 master 最全后一次提交的情况

master 分支提交文件

查看分支

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
git log --oneline --decorate
# 查看各个分支当前所指的对象
git log --oneline --decorate --graph --all
# 查看分叉历史
git branch
# 列出所有分支,* 表示当前分支
git branch -v
# 每一个分支的最后一次提交
git branch --merged
# 看哪些分支已经合并到当前分支
git branch --no-merged
# 查看所有包含未合并工作的分支,无法使用 -d 选项删除,可以使用 -D 选项强制删除

合并分支

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
git commit -a -m "..."
# 先提交
# 三方合并
git checkout master
# 切换回需要合并到的分支
git merge hotfix
# 合并分支到当前分支
# 直接把 master 指向 hotfix 的当前提交,就完成了合并
# 分支合并可能会遇到冲突,需要手动解决并提交
git status
# 查看冲突
vim <conflict-file-name>
git add <conflict-file-name>
git commit -m "...."
# 不需要再次 merge
  • fast-forward:没有需要解决的冲突,直接将 HEAD 指向分支
  • 一次合并提交:三方合并,你的开发历史从一个更早的地方开始分叉开来

  • non-fast-forward:存在需要解决的冲突,需要先拉取到本地并进行合并才能推送

删除分支

删除不再需要的分支

1
2
git branch -d hotfix
# 删除 hotfix 分支就是把 hotfix 指针给删掉

分支开发工作流

长期分支

在整个项目开发周期的不同阶段,你可以同时拥有多个开放的分支;你可以定期地把某些特性分支合并入其他分支中
比如只在 master 分支上保留完全稳定的代码

特性分支

短期分支,它被用来实现单一特性或其相关工作

Bug分支

每个bug都可以通过一个新的临时分支来修复,修复后,合并分支,然后将临时分支删除。

假定需要在master分支上修复,就从master创建临时分支

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
git stash
# 保存现场工作
git checkout master
git checkout -b issue-101
git add readme.txt
git commit -m "fix bug 101"
git checkout master
git merge --no-ff -m "merged bug fix 101" issue-101
git checkout dev
# 切换到之前的工作分支
git stash list
# 列出保存的工作现场
git stash pop
# 恢复的同时把stash内容也删了

Feature分支

添加一个新功能

1
2
3
4
5
6
git checkout -b feature-vulcan
# 原来在dev分支
git add vulcan.py
git commit -m "add feature vulcan"
git checkout dev

1
2
git branch -D feature-vulcan
# 强行删除分支,未合并之前删除使用

远程分支

之前的分支操作都是在本地执行的。

远程跟踪分支是远程分支状态的引用。以 (remote)/(branch) 形式命名,比如 origin/master 表示远程的master分支



1
2
git fetch origin
# 更新本地数据

推送

1
2
3
4
5
git push origin serverfix
# 推送本地的 serverfix 分支来更新远程仓库上的 serverfix 分支
git push origin serverfix:awesomebranch
# 本地的 serverfix 分支推送到远程仓库上的 awesomebranch 分支

其他协作者通过 git fetch origin 会下载新的分支,但不能修改

1
2
3
4
5
git merge origin/serverfix
# 合并分支
git checkout -b serverfix origin/serverifx
# 跟踪远程分支,这样本地可以修改 serverfix 分支

跟踪分支

跟踪分支是与远程分支有直接关系的本地分支
当克隆一个仓库时,它通常会自动地创建一个跟踪 origin/master 的 master 分支

1
2
3
4
5
6
7
8
9
10
11
12
git checkout -b <branch> <remote-branch>
git checkout -b sf origin/serverfix
# 本地分支名和远程分支名不同
git branch -u orgin/serverfix
# 创建分支,跟踪远程分支
git fetch --all
git branch -vv
# 查看设置的所有跟踪分支,需要先更新
# 可以确定本地分支与远程分支是否是领先(ahead)、落后(ahead)或是都有(说明远程有其他提交,本地也有提交)

拉取

1
2
git pull
# 更新本地仓库至最新改动并尝试合并

也就是 git fetchgit merge,建议显示使用 git fetchgit merge

删除远程分支

1
git push origin --delete <remote-branch>

变基

变基是另一种合并方式
不要对在你的仓库外有副本的分支执行变基

变基使得提交历史更加整洁,提交历史是一条直线没有分叉



首先找到这两个分支(即当前分支 experiment、变基操作的目标基底分支 master)的最近共同祖先 C2,然后对比当前分支相对于该祖先的历次提交,提取相应的修改并存为临时文件,然后将当前分支指向目标基底 C3, 最后以此将之前另存为临时文件的修改依序应用

1
2
3
4
5
6
7
8
9
git checkout experiment
git rebase master
git checkout master
git merge experiment
git rebase <basebranch> <topicbranch>
git rebase master server
git checkout master
git merge server

解决冲突

不同的分支各自都分别有新的提交,当进行合并时就会发生冲突,必须手动解决冲突后再提交。git status也可以告诉我们冲突的文件

修改文件中的冲突后可以再次提交

git log --graph命令可以看到分支合并图产品

git有个最佳实践,master是主分支,用来做正式发布版之后的保留历史,其他分支包括dev用来做正常开发,多个feature用来做某些特性功能,release用来做发布版历史,每次发布都是用release打包,hotfix用来做发布版之后的一些及时迭代修复bug的工作。

合并分支时,加上--no-ff参数就可以用普通模式合并,合并后的历史有分支,能看出来曾经做过合并,而fast forward合并就看不出来曾经做过合并

1
git merge --no-ff -m "merge with no-ff" dev

多人协作

1
2
3
4
5
6
7
8
9
10
11
12
13
git push origin master
git push origin dev
# 推送分支
git clone git@github.com:michaelliao/learngit.git
git branch
# 默认是master分支
git checkout -b dev origin/dev
# 创建dev分支
git pull
git branch --set-upstream-to=origin/dev dev
# 解决冲突

工作流

私有小型团队

有一两个其他开发者的私有项目

私有管理团队

独立小组的工作只能被特定的工程师整合,主仓库的master 分支只能被那些工程师更新

假设 John 与 Jessica 在一个特性上工作,同时 Jessica 与 Josie 在第二个特性上工作

git flow

  • master分支,即主分支。任何项目都必须有个这个分支。对项目进行tag或发布版本等操作,都必须在该分支上进行。
  • develop分支,即开发分支,从master分支上检出。团队成员一般不会直接更改该分支,而是分别从该分支检出自己的feature分支,开发完成后将feature分支上的改动merge回develop分支。同时release分支由此分支检出。
  • release分支,即发布分支,从develop分支上检出。该分支用作发版前的测试,可进行简单的bug修复。如果bug修复比较复杂,可merge回develop分支后由其他分支进行bug修复。此分支测试完成后,需要同时merge到master和develop分支上。
  • feature分支,即功能分支,从develop分支上检出。团队成员中每个人都维护一个自己的feature分支,并进行开发工作,开发完成后将此分支merge回develop分支。此分支一般用来开发新功能或进行项目维护等。
  • fix分支,即补丁分支,由develop分支检出,用作bug修复,bug修复完成需merge回develop分支,并将其删除。所以该分支属于临时性分支。
  • hotfix分支,即热补丁分支。和fix分支的区别在于,该分支由master分支检出,进行线上版本的bug修复,修复完成后merge回master分支,并merge到develop分支上,merge完成后也可以将其删除,也属于临时性分支。

参考:

搭建Git服务器

以下方式为通过ssh协议搭建的Git服务器,添加了公钥的用户可以进行读写操作

1.安装git

1
sudo apt install git

2.创建用户

1
2
3
4
5
6
cat /etc/shells
which git-shell
sudo vim /etc/shells
# 添加git-shell的位置,git-shell可以禁止使用ssh登录shell
sudo adduser git -s /usr/bin/git-shell

3.管理公钥
收集所有需要登录的用户的公钥,就是他们自己的id_rsa.pub文件,把所有公钥导入到/home/git/.ssh/authorized_keys文件里,一行一个
人多可以用Gitosis来管理公钥

注意权限

1
2
3
4
5
6
7
8
9
su git
cd
mkdir .ssh
chmod 700 .ssh
touch .ssh/authorized_keys
chmod 600 .ssh/authorized_keys
cat /tmp/id_rsa.john.pub >> ~/.ssh/authorized_keys
cat /tmp/id_rsa.josie.pub >> ~/.ssh/authorized_keys

4.初始化Git仓库
选定一个目录作为Git仓库

1
2
3
4
5
6
cd /opt/git
mkdir project.git
cd project.git
git init --bare
chown -R git:git project.git

需要有一个人推送第一个版本

1
2
3
4
5
6
cd myproject
git init
git add .
git commit -m 'initial commit'
git remote add origin git@gitserver:/opt/git/project.git
git push origin master

5.克隆远程仓库
在各自的电脑上克隆远程仓库

1
git clone git@server:/srv/sample.git

Git不支持权限控制。不过,因为Git支持钩子(hook),所以,可以在服务器端编写一系列脚本来控制提交等操作,达到权限控制的目的。Gitolite就是这个工具。

还有其他方式,比如适合快速只读访问的Git守护进程,既可以进行授权访问又可以进行无授权访问的Smart HTTP,使用GitWeb搭建网页展示,参考Pro-git

利用工具提升工作效率,而不是去学习工具本身

  1. 多用客户端和工具,少用命令行,除非在linux服务器上直接开发
  2. 每次提交前,diff自己的代码,以免提交错误的代码
  3. 下班回家前,整理好自己的工作区
  4. 并行的项目,使用分支开发
  5. 遇到冲突时,搞明白冲突的原因,千万不要随意丢弃别人的代码

参考: