git版本控制工具(二)

远程仓库:

  • 目前我们的开发都是在本地进行的,代码也是保存在本地仓库的,我们只能对本地代码进行操作。
  • 但在真实开发中,一个项目通常是由多人同时进行的,所以我们需要将代码共享到远程仓库中。
  • 而远程仓库也很好实现,只需要在Git服务器上搭建一个仓库即可,在Git服务器上的仓库我们称之为Git远程仓库。

我们可以通过如下方式使用Git服务器:

  1. 使用第三方的Git服务器:GitHubGiteeGitlab

  2. 在自己的服务器上搭建一个Git服务(通过Gitlab开源软件进行自行部署)。

远程仓库的验证

如果我们想要操作Git服务器中的私有仓库时,远程仓库会对我们进行身份验证。

目前Git服务器验证手段有两种:

  1. 基于HTTP的凭证存储(Credential Storage)。

  2. 基于SSH的密钥。

基于HTTP的凭证存储

由于HTTP协议是无状态的连接,这意味着每次连接都需要携带用户名和密码。

基于这种操作非常麻烦,Git提供了一个凭证系统来处理这个事情。

Git凭证系统有一些模式可以供我们选择:

(1) 临时输入模式

  • 不缓存凭证,每次操作(git clonegit push)时都需要输入用户名和密码。

  • 安全性高,但频繁操作时过于麻烦。

  • 触发条件:未配置任何凭证助手(credential hepler)。

凭证助手:当我们安装git时,

(2) 明文存储模式

  • 将凭证铭文保存在磁盘文件中,通过git config --global credential.helper store进行配置。

  • 凭证长期有效,无需重复输入。

  • 安全性低(明文存储,需严格保护文件权限)。

(3) 内存缓存模式

  • 将凭证临时保存在内存中(默认缓存15分钟)。

  • 配置方式:

  • 短期缓存,适合临时使用。

  • 重启后凭证失效。

1
2
3
git config --global credential.helper cache
# 自定义缓存时间(单位:秒)
git config --global credential.helper 'cache --timeout=3600'

(4) 系统密钥管理器集成

  • 将凭证存储在操作系统的密钥管理器中(如 Windows 凭据管理器、macOS Keychain)。

  • 我们在安装git时默认会为我们安装一个Git Credential Manager凭证管理工具。

  • 我们可以在控制面板->用户账户->凭证管理器->Windows凭证中看到我们的git凭证信息。

基于SSH的远程仓库验证

Secure Shell(安全外壳协议,简称SSH)是一种加密的网络传输协议,可在不安全的网络中为网络服务提供安全的传输环境。

我们需要先生成一个SSH密钥

(1) 打开Git黑窗口

(2) 生成ssh key

1
2
ssh-keygen -t rsa -C "你的邮箱"
# 执行后一直回车即可

(3) 获取ssh key公钥内容

1
2
cd ~/.ssh
cat id_rsa.pub

(4) 复制获取的公钥,然后存储到git服务器上,以Gitee为例:

管理远程仓库

我们可以通过git remote进行有关远程操作的操作。

  • git remote -v/--verbose:查看当前项目关联的远程仓库地址。

  • git remote add <shortname> <url>:添加远程仓库。

    • git remote add origin http://xxx//xxx.git
  • git reomte rename origin main:重命名远程地址。

  • git remote remove origin:移除远程地址。

本地分支的上游分支(跟踪分支)

当我们与成功添加远程仓库后,如果远程仓库的部分内容与本地不同的话,我们需要先通过git pull命令将远程仓库的内容拉取到本地才能进行提交,但当我们进行git pull操作时,可能会出现以下情况:

出现这个问题的原因是因为当前分支没有和远程的分支进行跟踪。

git pull其实是git fetchgit merge的合并操作,在没有进行跟踪的情况下,是不允许git fetch直接拉取到本地的,但我们可以指定远程仓库的分支进行获取:

1
2
# git pull <本地设置的远程仓库的shortname> <远程仓库的分支>
git pull origin main

但又会出现以下问题:

我们可以看出,已经拉取到本地了,但无法进行合并,这个问题后面再说。

我们不能每次都进行指定远程仓库的分支进行操作,为了解决这个问题,我们需要给当前分支设置一个跟踪分支。

1
2
# git branch --set-upstream-to=<本地设置的远程仓库的shortname>/<远程仓库的分支>
git branch --set-upstream-to=origin/main

进行上述操作后我们就可以直接进行git pull操作了:

可以看出,git fetch这一步已经没有问题了,接下来,我们需要解决的是fatal: refusing to merge unrelated histories这个问题了。

拒绝合并不相干的历史

上文提到,当我们进行合并远程分支操作时,会出现拒绝合并不相干历史的情况:

出现上述的原因如下

要解决这个问题,我们可以通过下面操作来处理:

1
git merge --allow-unrelated-histories

进行上述操作以后,我们就可以正常使用git pull操作了。

Git对远程仓库的常见操作

  • git clone <远程仓库地址>:克隆远程仓库到本地。

  • git push <本地设置的远程仓库的shortname> <当前分支>:将代码推送到远程仓库,只输入git push的情况默认是将当前分支推送到远程仓库中。

注意:对于git push的提交操作,会将当前分支的代码提交到远程仓库的相同名称的分支中,否者就会报以下错误:

上图已经给出了解决方案:git push origin HEAD[:本地分支名]

进行上面操作后会在远程创建一个与本地分支名一致的新分支,但这样每次提交都需要指定远程仓库名和本地分支。

为了解决这个问题,我们只需要将当前分支名改为与远程分支一样即可:

1
2
# git branch -m old-name new-name  如果改变当前的分支名,old-name可以省略
git branch -m master

造成上述问题的原因其实与push.default配置项有关,我们看一下push.default可配置选项:

配置项 说明
nothing 不推送任何内容
matching (Git 2.0 之前的默认值)推送所有匹配的分支
upstream 将当前分支推送到其上游分支(tracking 是 upstream 的已弃用的同义词)
current 将 Current 分支推送到同名分支
simple (Git 1.7.11 中的新功能,Git 2.0 后默认)与 upstream 类似,但如果上游分支的名称与本地分支的名称不同,则拒绝推送

根据上文内容,我们可以知道,如果我们不想改变当前分支的名称,直接想推送到上游分支中,只需要将push.default的值改为upstream即可:

1
git config push.default upstream
  • git fetch:拉取远程仓库的代码。

  • git merge:合并拉取的远程代码到本地。

  • git pullgit fetch+git merge

Git标签

当我们进行项目开发时,当完成一项重大开发后,通常会在当前版本打上一个标签,起到标识作用,是一项里程碑进度,表示当前版本的重要性。

创建Git标签

通过Git命令创建标签:

  • 创建轻量标签(lightweight):git tag v1.0

  • 创建附注标签(annotated):git tag -a v1.0 -m '附注信息'

在默认情况下,git push操作并不会传送标签到远程服务器上,我们需要显示的推送标签到服务器上,这样别人在获取时也能获得标签。

1
2
3
4
# git push <远程仓库shortname> <标签>
git push origin v1.0
# 将所有标签提交
git push origin --tags

删除和检出标签

删除标签的操作我们对本地和远程分别操作:

  • 删除本地分支:git tag -d <标签名>

    • git tag -d v1.2
  • 删除远程标签:git push <远程仓库shortname> --delete <标签名>

    • git push origin --delete v1.2

检出标签:用于查看某个标签所指向的版本:

1
2
# git checkout <标签名>
git checkout v1.1

但这个操作会创建一个游离分支,新分支处于detached HEAD(分离头指针)状态。

detached HEAD(分离头指针)

  • HEAD 在 Git 中通常指向当前分支(例如 main, develop),而分支本身是一个指向某个特定提交的指针。

  • 在分离头指针状态下,HEAD 直接指向一个具体的提交,而不是指向一个分支指针。

  • 简单说:你不是站在某个命名的分支道路上,而是直接站在历史长河中的某个具体快照(提交)点上。

好处:

  • 安全地探索历史(Look Around):我们可以安全的查看某个版本是什么样子的。

  • 进行实验性修改(Make Experimental Changes): 尝试一些想法、修复一个 bug、写个新功能,但还不确定是否要保留这些改动,或者不想干扰当前的工作分支。

  • 快速修复基于特定版本的 Bug:比如用户报告了线上版本 (v1.0) 的一个严重 bug,但你的 main 分支已经开发 v2.0 了,代码变化很大。

注意:不能直接将分离头指针(detached HEAD)状态下的提交推送到远程仓库的常规分支(如 origin/main)

但如果我们真想提交的话,可以使用这个命令:

1
git push origin HEAD:<name-of-remote-branch>
  • <name-of-remote-branch>: 指定你想要在远程仓库创建或更新的目标分支名称(例如 my-experiment, temp-fix, 甚至 main - 强烈不推荐覆盖重要分支!)。

Git提交对象(Commit Object)

Git版本控制系统以Commit Object(提交对象)的形式支持分支。

在每次提交时,都会创建一个提交对象:

  • 该提交对象会包含一个指向暂存内容快照的指针。

  • 该提交对象还包含了作者的姓名和邮箱、提交时输入的信息以及指向它的父对象的指针。

    • 首次提交产生的提交对象没有父对象,普通提交操作产生的提交对象有一个父对象
    • 而由多个分支合并产生的提交对象有多个父对象。

Git分支

Git分支,本质上是指向提交对象的可变指针。

1
2
3
4
5
          main

A ← B ← C ← D ← E

feature ← HEAD

在上面的结构中,A、B、C、D、E都是一个提交对象,main和feature是不同的分支,我们对于分支的切换其实就是将HEAD这个头指针指向不同分支的可变指针(即分支引用),从而间接指向该分支所指向的提交对象。

Git对分支的操作

  • git branch <分支名>:创建分支。

  • git checkout <分支名>:切换分支。

  • git checkout -b <分支名>:创建并切换到新分支。

  • git branch:查看当前所有的分支。

  • git branch -v:查看当前所有的分支,同时查看最后一次提交。

  • git branch --merged:查看所有合并到当前分支的分支。

  • git branch --no-merged:查看所有没有合并到当前分支的分支。

  • git branch –d <分支名>:删除当前分支。

  • git branch –D <分支名>:强制删除某一个分支。

  • git push origin <branch>:推送分支到远程。

  • git checkout --track <remote>/<branch>:跟踪远程分支,如果远程分支存在,本地不存在,会创建一个与远程分支相同名的分支。

  • git push origin --delete <branch>:删除远程分支。

Git工作流

Git工作流的出现源于软件开发的协作需求Git分布式架构的特性的深度结合。

我们在大型项目开发时,可能会出现以下问题:

  • 协作冲突:多人并行修改同一文件。

  • 版本管理:稳定版 vs 开发版 vs 紧急修复。

  • 质量控制:代码审查与自动化测试。

  • 责任追溯:故障定位与代码溯源。

通过Git工作流可以解决以上问题,其实本质上就是为特定协作场景设计的分支管理策略框架,通过规范分支的创建、合并规则及生命周期,解决团队协作中的关键问题。

简单来说,就是创建不同的分支,不同的分支有不同的职责,可以同步开发或测试,最后合并到主分支成为一个完整的项目。

目前比较常见的工作流如下图:

Git分支合并操作

场景:我们有一个hotfix分支是用于修复系统bug的,我们修复完后,但当前master分支是没有修复后的代码的,我们就可以将master分支和hotfix分支进行合并:

1
2
3
4
# 切换到master分支
git checkout master
# 合并hotfix分支内容
git merge hotfix

Git变基操作

在Git中整合来自不同分支的修改有merge和rebase两种方法。

merge操作会保留分支拓扑(查看图结构时可以看到有分叉),rebase是生成线性结构,rebase找到当前分支和目标分支的最近共同祖先,然后提取当前分支在共同祖先之后的所有提交,将这些提交重新应用到目标分支的最新提交之后,如果是相同文件的操作,可能会需要处理合并冲突,然后将当前分支的可变指针指向变基后的最新提交对象上。

我们用图的形式来更直观的看一下:

假设我们有一个提交历史,如下图:

目标:target-branch合并current-branch分支。

1
2
git checkout target-branch
git merge current-branch

目标:current-branch分支变基到目标分支。

注意:

  • 不要对已推送到远程的公共分支执行rebase 。
  • 不要对多人协作的分支执行rebase。

原因:重写历史会破坏其他开发者环境

对于mergerebase的使用场景:

  • 私有分支(仅自己使用)→ 大胆使用rebase

  • 公共分支(多人协作)→ 始终使用merge

目标:将target-branch合并到current-branch

1
2
git checkout target-branch
git merge current-branch

目标:current-branch分支变基到目标分支。

注意:

  • 不要对已推送到远程的公共分支执行rebase 。
  • 不要对多人协作的分支执行rebase。

原因:重写历史会破坏其他开发者环境

对于mergerebase的使用场景:

  • 私有分支(仅自己使用)→ 大胆使用rebase

  • 公共分支(多人协作)→ 始终使用merge