分类 技术 下的文章

Bash 并不是唯一可供选择的 Shell。还存在数量众多的 Shell,它们都有一些独特的特性,例如 Zsh、Fish、Ksh 和 Xonsh

在你的系统中,你可以同时安装多个 Shell。

要想将另一个 Shell 设为默认值,你可以按照以下方式使用 chsh 命令:

chsh -s path_to_binary_of_shell

如需找到 Shell 的二进制路径,你可以查看 /etc/shells 文件的内容。另外,你也可使用以下自动获取所需 Shell 二进制路径的命令:

chsh -s $(which new_shell)

接下来,让我们详细了解一下如何确定并更改 Shell。

我现在用的是哪个 Shell?

有很多方法可以帮你找出当前使用的是哪个 Shell,虽然专家可能会辩论这些方法的准确度。

最常用的,也是最简单的方式是:

echo $0

$0 是一个特别的 Shell 变量,这可以获取你正在使用的 Shell 或 Shell 脚本的名称(如果你在脚本中使用了它)。

你还可以使用下面的命令检查进程:

ps -p $

其中,$ 代表的是当前进程 / Shell 的进程 ID。

如何安装另一个 Shell?

和其他软件包一样,大部分知名 Shell,例如 Fish 和 Zsh,都可以直接从你的发行版软件仓库中下载安装。新的,相对小众的 Shell,例如 Xonsh,可能就需要不同的安装步骤了。你可以在它们的项目网页上找到具体的安装指南。

比如,你想 在 Ubuntu 上安装 Zsh,那么可以使用以下命令:

sudo apt install zsh

如何更改当前的 Shell?

假设你已经安装了另一个 Shell,那么我们来看一看如何切换过去。

实际上,你只需要输入新 Shell 的名称即可。比方说,你想切换到 Zsh,那就输入:

zsh

若要退出当前的 Shell,只需输入 exit 即可,你会回到你的默认 Shell。

如何查看默认的 Shell 是哪个?

有一个 SHELL 的环境变量,它可以告诉你当前账户的默认 Shell 是哪个:

echo $SHELL

举个例子,我切换到了 Zsh。此时,当前 Shell 显示的信息是 zsh,而默认 Shell 依然显示为 bash

这说明更改 Shell 并不会改变默认的 Shell。也就是说,下次你再次登录到该终端或系统,你还会返回到旧的默认 Shell,而不是新的 Shell。

如何知道系统中可用的 Shell 有哪些?

你可以通过查看 /etc/shells 文件,来了解系统中具有哪些可用的 Shell:

cat /etc/shells

下面就是我当前系统中所有可用的 Shell:

如何更改默认的 Shell?

/etc/shells 文件的内容显示了所有可用 Shell 的二进制文件位置。你需要将它与 chsh 命令一起使用。

假设我想让 Zsh 成为默认的 Shell,我可以输入:

chsh -s /usr/bin/zsh

更改后,你需要重新登入才能看到变化。

请注意,以上操作只会更改当前用户的默认 Shell。如果你是管理员,并且想更改其他用户的默认 Shell,那么你可以使用以下命令:

sudo chsh -s /usr/bin/zsh other_username

结论

Linux 的一大特色就是,用户可以自主选择。你完全可以根据自己的需要进行更改。这就是另一个例子,你不必局限于发行版提供的默认 Shell 的选择。你很欢迎自选一款 Shell,让你的工作变得更顺手。最后,祝你使用愉快 ?

(题图:DA/cf9b865d-2b98-4ada-88df-de1d1839aba1)


via: https://itsfoss.com/linux-change-default-shell/

作者:Abhishek Prakash 选题:lujun9972 译者:ChatGPT 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

Vely 可让你在网络应用程序中利用 C 语言的强大功能。

Vely 将 C 语言的高性能和低内存占用与 PHP 等语言的易用性和安全性相结合。作为自由开源软件,它以 GPLv3 和 LGPL 3 授权,所以你甚至可以用它来构建商业软件。

利用 Vely 构建 SaaS

你可以使用 Vely 创建一个多租户网络应用程序,它可以作为软件即服务模式(SaaS)在互联网上运行。每个用户都有一个完全独立的数据空间。

在这个网络应用程序示例中,用户可以注册一个笔记本服务来创建笔记,然后查看和删除它们。它仅用了 7 个源文件,310 行代码,就展示了如何集成多项技术:

  • MariaDB
  • 网络浏览器
  • Apache
  • Unix 套接字

运作原理

以下是从用户的角度来看应用程序是如何工作的。下图是代码演示。

该应用允许用户通过指定电子邮件地址和密码创建新的登录名。你可以用任何你喜欢的方式设置它们,例如运用 CSS:

创建一个用户账户

验证用户的电子邮件:

验证用户的电子邮件地址

每个用户使用自己独有的用户名和密码登录:

用户登录

一旦登录,用户就可以添加笔记:

用户可以添加笔记

用户可以获取笔记列表:

用户列举笔记

删除笔记之前,应用会申请确认信息:

删除笔记之前,应用会申请确认信息

用户确认后,笔记被删除:

用户确认后,笔记被删除

设置先决条件

遵照 Vely.dev 上的安装指示。这是一个使用 DNF、APT、Pacman 或者 Zypper 等标准工具包的快速流程。

由于它们都是这个范例的一部分,你必须安装 Apache 作为网络服务器,安装 MariaDB 作为数据库。

安装 Vely 后,如使用 Vim,打开里面的“语法高亮显示”:

vv -m

获取源代码

这个演示 SaaS 应用程序的源代码是 Vely 安装的一部分。为每个应用程序创建一个单独的源代码目录不失为一个好主意(而且你可以按自己喜好命名)。在这种情况下,解包源代码会帮你完成这些工作:

$ tar xvf $(vv -o)/examples/multitenant_SaaS.tar.gz
$ cd multitenant_SaaS

默认情况下,该应用程序以 multitenant_SaaS 命名,但你可以将其命名为任何内容(如果这么做,其他每个地方你都需要改一下)。

创建应用程序

第一步是创建一个应用程序。使用 Vely 的 vf 工具就可以轻松完成:

$ sudo vf -i-u $(whoami) multitenant_SaaS

这个命令创建了一个新的应用程序主目录(/var/lib/vv/multitenant_SaaS),并帮你执行应用程序设置。通常,这意味着在该主目录中创建各种子目录并分配权限。在这种情况下,只有当前用户(whoami 的结果)拥有目录,具有 0700 权限,这确保了其他人没有访问文件的权限。

创建数据库

在你键入任何代码之前,你需要一个能够存储该应用程序所用信息的空间。首先,创建一个名为 db_multitenant_SaaS 的 MariaDB 数据库,由用户名为 vely 的用户所有,密码为 your_password 。你可以修改刚才提到的任何值,但得记住,在这个示例里,你需要将包含这些内容的每个地方都得修改一遍。

在 MySQL 中以 root 身份登录:

create database if not exists db_multitenant_SaaS;
create user if not exists vely identified by 'your_password';
grant create,alter,drop,select,insert,delete,update on db_multitenant_SaaS.* to vely;

然后在数据库内创建数据库对象(表,记录等等):

use db_multitenant_SaaS;
source setup.sql;
exit

将 Vely 连接至数据库

为了让 Vely 知晓你数据库的位置以及如何登录进去,创建一个名为 db_multitenant_SaaS 的数据库配置文件。(该名称用于在源代码中的数据库声明,所以如果你改了它,确保在它存在的每个地方都改一遍。)

Vely 使用原生的 MariaDB 数据库连接,因此你可以指定给定的数据库所能允许的任何选项:

$ echo '[client]
user=vely
password=your_password
database=db_multitenant_SaaS
protocol=TCP
host=127.0.0.1
port=3306' > db_multitenant_SaaS

构建应用程序

使用 vv 工具构建应用程序,利用 --db 选项指定 MariaDB 数据库和数据库配置文件:

$ vv -q--db=mariadb:db_multitenant_SaaS

启用应用服务器

启动你的网络应用程序的服务器,需要使用 vf FastCGI 进程管理器。应用程序服务器使用 Unix 套接字与网络服务器(创建反向代理)通信:

$ vf -w3 multitenant_SaaS

这么做会启用三个守护进程,为接收到的请求提供服务。你也可以启动一个自适应服务器,它会增加进程的数量从而服务更多的请求,并在不需要他们时减少进程的数量:

$ vf multitenant_SaaS

请参阅 vf 了解更多选项,以帮助你实现最佳性能。

当你需要停止你的应用程序服务器,使用 -m quit 选项:

$ vf -m quit multitenant_SaaS

创建网络服务器

这是一个网络应用程序,那么应用程序就得需要一个网络服务器。该示例通过一个 Unix 套接字监听器使用 Apache。

1、设置 Apache

将 Apache 配置为一个反向代理,并将你的应用程序与之连接,你需要启用 FastCGI 代理支持,这通常使用 proxyproxy_fcgi 模块。

对于 Fedora 系统(或者其它的,比如 Arch)来说,通过在 Apache 配置文件 /etc/httpd/conf/httpd.conf 中添加(或取消注释)适当的 LoadModule 指令,就可启用 proxyproxy_fcgi 模块。

以下指令适用于 Debian,Ubuntu 以及类似的系统,启用 proxyproxy_fcgi 模块:

$ sudo a2enmod proxy
$ sudo a2enmod proxy_fcgi

以下指令适用于 OpenSUSE,将这几行添加在 /etc/apache2/httpd.conf 结尾处:

LoadModule proxy_module modules/mod_proxy.so
LoadModule proxy_fcgi_module modules/mod_proxy_fcgi.so

2、配置 Apache

现在你必须将代理信息添加在 Apache 的配置文件中:

ProxyPass "/multitenant_SaaS" unix:///var/lib/vv/multitenant_SaaS/sock/sock|fcgi://localhost/multitenant_SaaS

你的配置文件的位置可能会有所不同,这取决于不同的 Linux 发行版:

  • Fedora、CentOS、Mageia 和 Arch: /etc/httpd/conf/httpd.conf
  • Debian、Ubuntu、Mint: /etc/apache2/apache2.conf
  • OpenSUSE:/etc/apache2/httpd.conf

3、重新启动

最后,重启 Apache。在 Fedora 和类似系统,还有 Arch Linux 是如下指令:

$ sudo systemctl restart httpd

在 Debian 和基于 Debian 的系统,还有 OpenSUSE 是如下指令:

$ sudo systemctl restart apache2

设置本地邮箱

这个示例中,电子邮件是其功能的一部分。如果你的服务器已经可以发送电子邮件了,你可以跳过这一条。此外,你可以使用本地邮箱(myuser@localhost)来测试它。要做到这一点,需安装 Sendmail。

在 Fedora 和类似系统中是如下指令:

$ sudo dnf installsendmail
$ sudo systemctl start sendmail

而在 Debian 和类似系统(如 Ubuntu):

$ sudo apt installsendmail
$ sudo systemctl start sendmail

当应用程序向本地用户发送电子邮件,比如说 OS_user@localhost,你就可以通过查看 /var/mail/ 处(即所谓“邮件池”)来确认电子邮件是否被发送。

从浏览器访问应用服务器

假设你在本地运行该应用,可以通过使用 http://127.0.0.1/multitenant_SaaS?req=notes&action=begin 域名从你的网络服务器访问你的应用服务器。如果你在互联网上的在线服务器运行该程序,你可能就需要调整防火墙设置以允许 HTTP 通信。

源代码

该应用程序示例包含 7 个源文件。你可以自行回顾代码(记住,这些文件只有 310 行代码),下面是每个文件的概述。

SQL 设置(setup.sql)

创建的两个表:

  • users:每个用户的信息。在 users 表中,每个用户都有自己唯一的 ID (userId 列),以及其他信息,如电子邮件地址和该地址是否通过了验证。还有一个哈希密码。实际的密码永远不会存储在纯文本(或其他形式)中,单向哈希用于检查密码。
  • notes:用户输入的笔记。notes 表包含了所有的笔记,每个笔记都有一个 userId 列,表示哪个用户拥有它们。userId 列的值与 users 表中的同名列匹配。这样,每个笔记显然都属于单个用户。

该文件内容如下:

create table if not exists notes (dateOf datetime, noteId bigint auto_increment primary key, userId bigint, note varchar(1000));
create table if not exists users (userId bigint auto_increment primary key, email varchar(100), hashed_pwd varchar(100), verified smallint, verify_token varchar(30), session varchar(100));
create unique index if not exists users1 on users (email);

运行时数据(login.h)

为了正确地显示登录、注册和注销链接,你需要一些在应用程序中任何地方都可以使用的标志。此外,应用程序使用 cookie 来维护会话,因此它需要在任何地方都可用,例如,验证会话是否有效。发送到应用程序的每个请求都以这种方式进行确认。只有带有可验证 cookie 的请求是允许的。

所以要做到这种效果,你需要有一个 global_request_data 类型的 reqdata(请求数据),其中包含 sess_userId(用户的 ID)以及 sess_id(用户目前的会话 ID)。此外,还有一些不言自明的标志,可以帮助渲染页面:

#ifndef _VV_LOGIN
#define _VV_LOGIN

typedef struct s_reqdata {
    bool displayed_logout; // true 则显示登出连接
    bool is_logged_in; // true 则会话已验证登录
    char *sess_userId; // 目前会话的用户 ID
    char *sess_id; // 会话 ID
} reqdata;

void login_or_signup ();

#endif

会话检查和会话数据(\_before.vely)

Vely 里有一个 请求前处理程序 before_request handler 的概念。你写的代码会在其它处理请求的代码之前执行的。要达到这个目的,你只需要将这样的代码写在名为 _before.vely 的文件中,然后剩余的部分将会自动处理。

SaaS 应用程序所作的任何事情,例如处理发送至应用程序的请求,必须验证其安全性。这样,应用程序就能知晓调用方是否有执行操作所需要的权限。

在这里,通过请求前处理程序进行权限检查。这样,无论其他代码如何处理请求,都已经掌握了会话信息。

为保持会话数据(比如会话 ID 和用户 ID)在你代码中的任何地方都可用,你可以使用 global_request_data。它只是一个指向内存的通用指针(void*),任何处理请求的代码都可以访问它。这非常适合处理会话,如下所示:

#include "vely.h"
#include "login.h"

// _before() 是一个请求前处理程序。
// 它总是在处理请求的其他代码之前执行。
// 对于任何类型的请求范围设置或数据初始化,它都是一个很好的位置。
void _before() {
    // 输出 HTTP 请求头
    out-header default
    reqdata *rd; // 这是全局请求数据,见 login.h
    // 为全局请求数据分配内存,
    // 将在请求结束时自动释放
    new-mem rd size sizeof(reqdata)
    // 初始化标志
    rd->displayed_logout = false;
    rd->is_logged_in = false;
    // 将我们创建的数据设置为全局请求数据,
    // 可以从任何处理请求的代码中访问
    set-req data rd
    // 检查会话是否存在(基于来自客户端的 cookie)
    // 这在任何其他请求处理代码之前执行,
    // 使其更容易准备好会话信息
    _check_session ();
}

检查会话是否有效(\_check\_session.vely)

多租户 SaaS 应用程序中最重要的任务之一就是通过检查用户是否登录来(尽快)检查会话是否有效。这是通过从客户端(例如网络浏览器)获取会话 ID 和用户 ID 的 cookie,并将它们与存储会话的数据库进行比较来实现的:

#include "vely.h"
#include "login.h"


// 检查会话是否有效
void _check_session () {
    // 获取全局请求数据
    reqdata *rd;
    get-req data to rd
    // 自用户浏览器获取 cookies
    get-cookie rd->sess_userId="sess_userId"
    get-cookie rd->sess_id="sess_id"
    if (rd->sess_id[0] != 0) {
        // 检查给定用户 ID 下的会话 ID 是否正确
        char *email;
        run-query @db_multitenant_SaaS = "select email from users where userId='%s' and session='%s'" output email : rd->sess_userId, rd->sess_id row-count define rcount
            query-result email to email
        end-query
        if (rcount == 1) {
            // 如果正确,设置登录标志
            rd->is_logged_in = true;
            // 如果登出链接不显示,则显示它
            if (rd->displayed_logout == false) {
                @Hi <<p-out email>>! <a href="https://opensource.com/?req=login&action=logout">Logout</a><br/>
                rd->displayed_logout = true;
            }
        } else rd->is_logged_in = false;
    }
}

注册、登录、登出(login.vely)

任何多租户系统的基础便是具有用户注册\登录和登出的功能。通常情况下,注册包括验证电子邮件地址;不止于此,同一电子邮件地址会作为一个用户名。这里就是这种情况。

这里实现了几个执行该功能所必须的子请求:

  • 注册新用户时,显示 HTML 表单以收集信息。它的 URL 请求签名是 req=login&action=newuser
  • 作为对注册表单的响应,创建一个新用户。URL 请求的签名是 req=login&action=createuser。输入参数(input-param)信号获取 emailpwd 的 POST 表单字段。密码值是单向散列,电子邮件验证令牌是一个随机的 5 位数字。这些被插入到 users 表中,创建一个新用户。系统会发送一封验证邮件,并提示用户阅读邮件并输入代码。
  • 通过输入发送到该电子邮件的验证码来验证电子邮件。URL 请求的签名是 req=login&action=verify
  • 显示一个登录表单,让用户登录。URL 请求的签名是 req=login(例如,action 为空)。
  • 通过验证电子邮件地址(用户名)和密码登录。URL 请求的签名是 req=login&action=login
  • 应用户要求登出。URL 请求的签名是 req=login&action=logout
  • 应用程序的登录页。URL 请求的签名是 req=login&action=begin
  • 如果用户当前已登录,转到应用程序的登录页面。

可以看看下面这些例子:

#include "vely.h"
#include "login.h"

// 处理云端多租户应用程序的会话维护、登录、注销、会话验证
void login () {
    // 获取 URL 的输入参数 `action`
    input-param action

    // 获取全局请求数据,我们在其中记录会话信息,所以它很方便
    reqdata *rd;
    get-req data to rd

    // 如果会话已经建立,我们不会
    // 继续到应用程序主页的唯一原因是我们正在登出
    if (rd->is_logged_in) {
        if (strcmp(action, "logout")) {
            _show_home();
            exit-request
        }
    }

    // 应用程序页面启动。显示登录或注册的链接,
    // 并显示适当的主屏幕
    if (!strcmp (action, "begin")) {
        _show_home();
        exit-request

    // 开始创建新用户。询问电子邮件和密码,
    // 然后提交此表单时创建用户。
    } else if (!strcmp (action, "newuser")) {
        @Create New User<hr/>
        @<form action="https://opensource.com/?req=login" method="POST">
        @<input name="action" type="hidden" value="createuser">
        @<input name="email" type="text" value="" size="50" maxlength="50" required autofocus placeholder="Email">
        @<input name="pwd" type="password" value="" size="50" maxlength="50" required placeholder="Password">
        @<input type="submit" value="Sign Up">
        @</form>

    // 验证用户发送到电子邮件的代码。代码必须匹配,从而验证电子邮件地址   
    } else if (!strcmp (action, "verify")) {
        input-param code
        input-param email
        // 获取基于电子邮件的验证令牌
        run-query @db_multitenant_SaaS = "select verify_token from users where email='%s'" output db_verify : email
            query-result db_verify to define db_verify
            // 将数据库中记录的令牌与用户提供的令牌进行比较
            if (!strcmp (code, db_verify)) {
                @Your email has been verifed. Please <a href="https://opensource.com/?req=login">Login</a>.
                // 如果匹配,更新用户信息以表明已验证。
                run-query @db_multitenant_SaaS no-loop = "update users set verified=1 where email='%s'" : email
                exit-request
            }
        end-query
        @Could not verify the code. Please try <a href="https://opensource.com/?req=login">again</a>.
        exit-request

    // 创建用户 —— 当用户使用电子邮件和密码提交表单以创建用户时运行    
    } else if (!strcmp (action, "createuser")) {
        input-param email
        input-param pwd
        // 创建散列(单向)密码
        hash-string pwd to define hashed_pwd
        // 生成随机的 5 位数字字符串验证代码
        random-string to define verify length 5 number
        // 创建用户:插入电子邮件、哈希密码、验证令牌。当前验证状态为 0,或未验证
        begin-transaction @db_multitenant_SaaS
        run-query @db_multitenant_SaaS no-loop = "insert into users (email, hashed_pwd, verified, verify_token, session) values ('%s', '%s', '0', '%s', '')" : email, hashed_pwd, verify affected-rows define arows error define err on-error-continue
        if (strcmp (err, "0") || arows != 1) {
            // 如果不能添加用户,则可能该用户不存在。不管怎样,我们都无法继续。
            login_or_signup();
            @User with this email already exists.
            rollback-transaction @db_multitenant_SaaS
        } else {
            // 创建带有验证码的电子邮件并将其发送给用户
            write-string define msg
                @From: [email protected]
                @To: <<p-out email>>
                @Subject: verify your account
                @
                @Your verification code is: <<p-out verify>>
            end-write-string
            exec-program "/usr/sbin/sendmail" args "-i", "-t" input msg status define st
            if (st != 0) {
                @Could not send email to <<p-out email>>, code is <<p-out verify>>
                rollback-transaction @db_multitenant_SaaS
                exit-request
            }
            commit-transaction @db_multitenant_SaaS
            // 通知用户查看邮件并输入验证码
            @Please check your email and enter verification code here:
            @<form action="https://opensource.com/?req=login" method="POST">
            @<input name="action" type="hidden" value="verify" size="50" maxlength="50">
            @<input name="email" type="hidden" value="<<p-out email>>">
            @<input name="code" type="text" value="" size="50" maxlength="50" required autofocus placeholder="Verification code">
            @<button type="submit">Verify</button>
            @</form>
        }

    // 这里在登录用户登出时运行    
    } else if (!strcmp (action, "logout")) {
        // 更新用户表以清除会话,即没有该用户登录
        if (rd->is_logged_in) {
            run-query @db_multitenant_SaaS = "update users set session='' where userId='%s'" : rd->sess_userId no-loop affected-rows define arows
            if (arows == 1) {
                rd->is_logged_in = false; // 提示用户未登录
                @You have been logged out.<hr/>
            }
        }
        _show_home();

    // 登录:当用户输入用户名和密码时运行
    } else if (!strcmp (action, "login")) {
        input-param pwd
        input-param email
        // 创建单向散列,目的是与用户表进行比较 —— 密码**永远不会**被记录
        hash-string pwd to define hashed_pwd
        // 为会话 ID 创建一个随机的 30 位长的字符串
        random-string to rd->sess_id length 30
        // 检查用户名和哈希密码是否匹配
        run-query @db_multitenant_SaaS = "select userId from users where email='%s' and hashed_pwd='%s'" output sess_userId : email, hashed_pwd
            query-result sess_userId to rd->sess_userId
            // 如果匹配,使用会话 ID 更新用户表
            run-query @db_multitenant_SaaS no-loop = "update users set session='%s' where userId='%s'" : rd->sess_id, rd->sess_userId affected-rows define arows
            if (arows != 1) {
                @Could not create a session. Please try again. <<.login_or_signup();>> <hr/>
                exit-request
            }
            // 设置“用户 ID”和“会话 ID”为 cookie。用户的浏览器将在每个请求中返回这些信息
            set-cookie "sess_userId" = rd->sess_userId
            set-cookie "sess_id" = rd->sess_id
            // 显示主页,确保会话是正确的,并设置标志
            _check_session();
            _show_home();
            exit-request
        end-query
        @Email or password are not correct. <<.login_or_signup();>><hr/>

    // 登录界面,要求用户输入用户名和密码  
    } else if (!strcmp (action, "")) {
        login_or_signup();
        @Please Login:<hr/>
        @<form action="https://opensource.com/?req=login" method="POST">
        @<input name="action" type="hidden" value="login" size="50" maxlength="50">
        @<input name="email" type="text" value="" size="50" maxlength="50" required autofocus placeholder="Email">
        @<input name="pwd" type="password" value="" size="50" maxlength="50" required placeholder="Password">
        @<button type="submit">Go</button>
        @</form>
    }
}

// 显示登录或注册链接
void login_or_signup() {
        @<a href="https://opensource.com/?req=login">Login</a> & & <a href="https://opensource.com/?req=login&action=newuser">Sign Up</a><hr/>
}

通用应用程序(\_show\_home.vely)

借助本教程,你可以创建你想要的任何多租户 SaaS 应用程序。上面的多租户处理模块(login.vely)调用 _show_home() 函数,它可以容纳你的任何代码。这个示例代码展示了笔记应用程序,但它可以是任何内容。_show_home() 函数可以调用你想要的任何代码,它是一个通用的多租户应用程序插件:

#include "vely.h"

void _show_home() {
    notes();
    exit-request
}

笔记应用程序(notes.vely)

该应用程序能够添加、列举以及删除任何给定的笔记:

#include "vely.h"
#include "login.h"

// 多租户云中的笔记应用程序
void notes () {
    // 获取全局请求数据
    reqdata *rd;
    get-req data to rd
    // 如果会话有效,显示登录或注册
    if (!rd->is_logged_in) {
        login_or_signup();
    }
    // 问候用户
    @<h1>Welcome to Notes!</h1><hr/>
    // 如果没有登出,退出 —— 这里确保对用户身份的安全验证
    if (!rd->is_logged_in) {
        exit-request
    }
    // 获取 URL 参数,告诉笔记要做什么
    input-param subreq
    // 显示笔记能够做什么操作(添加或列举笔记)
    @<a href="https://opensource.com/?req=notes&subreq=add">Add Note</a> <a href="https://opensource.com/?req=notes&subreq=list">List Notes</a><hr/>

    // 列举该用户的所有笔记
    if (!strcmp (subreq, "list")) {
        // **只**选取该用户的笔记
        run-query @db_multitenant_SaaS = "select dateOf, note, noteId from notes where userId='%s' order by dateOf desc" : rd->sess_userId output dateOf, note, noteId
            query-result dateOf to define dateOf
            query-result note to define note
            query-result noteId to define noteId
            // 使用快速缓存正则表达式将新行更改为<br/>
            match-regex "\n" in note replace-with "<br/>\n" result define with_breaks status define st cache
            if (st == 0) with_breaks = note; // 什么都没有发现/替换,只用原来的
            // 显示笔记
            @Date: <<p-out dateOf>> (<a href="https://opensource.com/?req=notes&subreq=delete_note_ask&note_id=%3C%3Cp-out%20noteId%3E%3E">delete note</a>)<br/>
            @Note: <<p-out with_breaks>><br/>
            @<hr/>
        end-query
    }

    // 要求删除笔记
    else if (!strcmp (subreq, "delete_note_ask")) {
        input-param note_id
        @Are you sure you want to delete a note? Use Back button to go back, or <a href="https://opensource.com/?req=notes&subreq=delete_note&note_id=%3C%3Cp-out%20note_id%3E%3E">delete note now</a>.
    }

    // 删除笔记
    else if (!strcmp (subreq, "delete_note")) {
        input-param note_id
        // 删除笔记
        run-query @db_multitenant_SaaS = "delete from notes where noteId='%s' and userId='%s'" : note_id, rd->sess_userId affected-rows define arows no-loop error define errnote
        // 告知用户状态
        if (arows == 1) {
            @Note deleted
        } else {
            @Could not delete note (<<p-out errnote>>)
        }
    }

    // 添加笔记
    else if (!strcmp (subreq, "add_note")) {
        // 从 note 表单中获取 URL POST 数据
        input-param note
        // 在该用户的 ID 下插入笔记
        run-query @db_multitenant_SaaS = "insert into notes (dateOf, userId, note) values (now(), '%s', '%s')" : rd->sess_userId, note affected-rows define arows no-loop error define errnote
        // 告知用户状态
        if (arows == 1) {
            @Note added
        } else {
            @Could not add note (<<p-out errnote>>)
        }
    }

    // 显示一个 HTML 表单来收集笔记,并将其发送回这里(使用 subreq="add_note" URL 参数)
    else if (!strcmp (subreq, "add")) {
        @Add New Note
        @<form action="https://opensource.com/?req=notes" method="POST">
        @<input name="subreq" type="hidden" value="add_note">
        @<textarea name="note" rows="5" cols="50" required autofocus placeholder="Enter Note"></textarea>
        @<button type="submit">Create</button>
        @</form>
    }
}

具有 C 性能的 SaaS

Vely 语言使得 C 语言在你的网络应用程序中得到充分利用这件事成为可能。多租户 SaaS 应用程序便是从中受益的一个典型用例。

看一看参考代码示例,写一写代码,然后试试 Vely。

(题图:DA/126624c8-1a47-481b-b149-92273e8e0f4f)


via: https://opensource.com/article/22/11/build-your-own-saas-vely

作者:Sergio Mijatovic 选题:lkxed 译者:Drwhooooo 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

我正在一步步解释 Git 的方方面面。在使用 Git 近 15 年后,我已经非常习惯于 Git 的特性,很容易忘记它令人困惑的地方。

因此,我在 Mastodon 上进行了调查:

你有觉得哪些 Git 术语很让人困惑吗?我计划写篇博客,来解读 Git 中一些奇怪的术语,如:“分离的 HEAD 状态”,“快速前移”,“索引/暂存区/已暂存”,“比 origin/main 提前 1 个提交”等等。

我收到了许多有洞见的答案,我在这里试图概述其中的一部分。下面是这些术语的列表:

  • HEAD 和 “heads”
  • “分离的 HEAD 状态”
  • 在合并或变基时的 “ours” 和 “theirs”
  • “你的分支已经与 'origin/main' 同步”
  • HEAD^HEAD~HEAD^^HEAD~~HEAD^2HEAD~2
  • .....
  • “可以快速前移”
  • “引用”、“符号引用”
  • refspecs
  • “tree-ish”
  • “索引”、“暂存的”、“已缓存的”
  • “重置”、“还原”、“恢复”
  • “未跟踪的文件”、“追踪远程分支”、“跟踪远程分支”
  • 检出
  • reflog
  • 合并、变基和遴选
  • rebase –onto
  • 提交
  • 更多复杂的术语

我已经尽力讲解了这些术语,但它们几乎覆盖了 Git 的每一个主要特性,这对一篇博客而言显然过于繁重,所以在某些地方可能会有一些粗糙。

HEAD 和 “heads”

有些人表示他们对 HEADrefs/heads/main 这些术语感到困惑,因为听起来像是一些复杂的技术内部实现。

以下是一个快速概述:

  • “heads” 就是 “分支”。在 Git 内部,分支存储在一个名为 .git/refs/heads 的目录中。(从技术上讲,官方 Git 术语表 中明确表示分支是所有的提交,而 head 只是最近的提交,但这只是同一事物的两种不同思考方式)
  • HEAD 是当前的分支,它被存储在 .git/HEAD 中。

我认为,“head 是一个分支,HEAD 是当前的分支” 或许是 Git 中最奇怪的术语选择,但已经设定好了,想要更清晰的命名方案已经为时已晚,我们继续。

“HEAD 是当前的分支” 有一些重要的例外情况,我们将在下面讨论。

“分离的 HEAD 状态”

你可能已经看到过这条信息:

$ git checkout v0.1
You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by switching back to a branch.

[...]

(消息译文:你处于 “分离 HEAD” 的状态。你可以四处看看,进行试验性的更改并提交,你可以通过切换回一个分支来丢弃这个状态下做出的任何提交。)

这条信息的实质是:

  • 在 Git 中,通常你有一个已经检出的 “当前分支”,例如 main
  • 存放当前分支的地方被称为 HEAD
  • 你做出的任何新提交都会被添加到你的当前分支,如果你运行 git merge other_branch,这也会影响你的当前分支。
  • 但是,HEAD 不一定必须是一个分支!它也可以是一个提交 ID。
  • Git 会称这种状态(HEAD 是提交 ID 而不是分支)为 “分离的 HEAD 状态”
  • 例如,你可以通过检出一个标签来进入分离的 HEAD 状态,因为标签不是分支
  • 如果你没有当前分支,一系列事情就断链了:

    • git pull 根本就无法工作(因为它的全部目的就是更新你的当前分支)
    • 除非以特殊方式使用 git push,否则它也无法工作
    • git commitgit mergegit rebasegit cherry-pick 仍然可以工作,但它们会留下“孤儿”提交,这些提交没有连接到任何分支,因此找到这些提交会很困难
  • 你可以通过创建一个新的分支或切换到一个现有的分支来退出分离的 HEAD 状态

在合并或变基中的 “ours” 和 “theirs”

遇到合并冲突时,你可以运行 git checkout --ours file.txt 来选择 “ours” 版本中的 file.txt。但问题是,什么是 “ours”,什么是 “theirs” 呢?

我总感觉此类术语混淆不清,也因此从未用过 git checkout --ours,但我还是查找相关资料试图理清。

在合并的过程中,这是如何运作的:当前分支是 “ours”,你要合并进来的分支是 “theirs”,这样看来似乎很合理。

$ git checkout merge-into-ours # 当前分支是 “ours”
$ git merge from-theirs # 我们正要合并的分支是 “theirs”

而在变基的过程中就刚好相反 —— 当前分支是 “theirs”,我们正在变基到的目标分支是 “ours”,如下:

$ git checkout theirs # 当前分支是 “theirs”
$ git rebase ours # 我们正在变基到的目标分支是 “ours”

我以为之所以会如此,因为在操作过程中,git rebase main 其实是将当前分支合并到 main (它类似于 git checkout main; git merge current_branch),尽管如此我仍然觉得此类术语会造成混淆。

这个精巧的小网站 对 “ours” 和 “theirs” 的术语进行了解释。

人们也提到,VSCode 将 “ours”/“theirs” 称作 “当前的更改”/“收到的更改”,同样会引起混淆。

“你的分支已经与 origin/main 同步”

此信息貌似很直白 —— 你的 main 分支已经与源端同步!

但它实际上有些误导。可能会让你以为这意味着你的 main 分支已经是最新的,其实不然。它真正的含义是 —— 如果你最后一次运行 git fetchgit pull 是五天前,那么你的 main 分支就是与五天前的所有更改同步。

因此,如果你没有意识到这一点,它对你的安全感其实是一种误导。

我认为 Git 理论上可以给出一个更有用的信息,像是“与五天前上一次获取的源端 main 是同步的”,因为最新一次获取的时间是在 reflog 中记录的,但它没有这么做。

HEAD^HEAD~HEAD^^HEAD~~HEAD^2HEAD~2

我早就清楚 HEAD^ 代表前一次提交,但我很长一段时间都困惑于 HEAD~HEAD^ 之间的区别。

我查询资料,得到了如下的对应关系:

  • HEAD^HEAD~ 是同一件事情(指向前 1 个提交)
  • HEAD^^^HEAD~~~HEAD~3 是同一件事情(指向前 3 个提交)
  • HEAD^3 指向提交的第三个父提交,它与 HEAD~3 是不同的

这看起来有些奇怪,为什么 HEAD~HEAD^ 是同一个概念?以及,“第三个父提交”是什么?难道就是父提交的父提交的父提交?(剧透:并非如此)让我们一起深入探讨一下!

大部分提交只有一个父提交。但是合并提交有多个父提交 - 因为它们合并了两个或更多的提交。在 Git 中,HEAD^ 意味着 “HEAD 提交的父提交”。但是如果 HEAD 是一个合并提交,那 HEAD^ 又代表怎么回事呢?

答案是,HEAD^ 指向的是合并提交的第一个父提交,HEAD^2 是第二个父提交,HEAD^3 是第三个父提交,等等。

但我猜他们也需要一个方式来表示“前三个提交”,所以 HEAD^3 是当前提交的第三个父提交(如果当前提交是一个合并提交,可能会有很多父提交),而 HEAD~3 是父提交的父提交的父提交。

我想,从我们之前对合并提交 “ours”/“theirs” 的讨论来看,HEAD^ 是 “ours”,HEAD^2 是 “theirs”。

.....

这是两个命令:

  • git log main..test
  • git log main...test

我从没用过 ..... 这两个命令,所以我得查一下 man git-range-diff。我的理解是比如这样一个情况:

A - B main
  \
    C - D test
  • main..test 对应的是提交 C 和 D
  • test..main 对应的是提交 B
  • main...test 对应的是提交 B,C,和 D

更有挑战的是,git diff 显然也支持 .....,但它们在 git log 中的意思完全不同?我的理解如下:

  • git log test..main 显示在 main 而不在 test 的更改,但是 git log test...main 则会显示 两边 的改动。
  • git diff test..main 显示 test 变动 main 变动(它比较 BD),而 git diff test...main 会比较 AD(它只会给你显示一边的差异)。

有关这个的更多讨论可以参考 这篇博客文章

“可以快速前移”

git status 中,我们会经常遇到如下的信息:

$ git status
On branch main
Your branch is behind 'origin/main' by 2 commits, and can be fast-forwarded.
  (use "git pull" to update your local branch)

(消息译文:你现在处于 main 分支上。你的分支比 origin/main 分支落后了 2 个提交,可以进行快速前进。 (使用 git pull 命令可以更新你的本地分支))

但“快速前移” 到底是何意?本质上,它在告诉我们这两个分支基本如下图所示(最新的提交在右侧):

main:        A - B - C
origin/main: A - B - C - D - E

或者,从另一个角度理解就是:

A - B - C - D - E (origin/main)
        |
        main

这里,origin/main 仅仅多出了 2 个 main 不存在的提交,因此我们可以轻松地让 main 更新至最新 —— 我们所需要做的就是添加上那 2 个提交。事实上,这几乎不可能出错 —— 不存在合并冲突。快速前进式合并是个非常棒的事情!这是合并两个分支最简单的方式。

运行完 git pull 之后,你会得到如下状态:

main:        A - B - C - D - E
origin/main: A - B - C - D - E

下面这个例子展示了一种不能快速前进的状态。

A - B - C - X  (main)
        |
        - - D - E  (origin/main)

此时,main 分支上有一个 origin/main 分支上无的提交(X),所以无法执行快速前移。在此种情况,git status 就会如此显示:

$ git status
Your branch and 'origin/main' have diverged,
and have 1 and 2 different commits each, respectively.

(你的分支和 origin/main 分支已经产生了分歧,其中各有 1 个和 2 个不同的提交。)

“引用”、“符号引用”

在使用 Git 时,“引用” 一词可能会使人混淆。实际上,Git 中被称为 “引用” 的实例至少有三种:

  • 分支和标签,例如 mainv0.2
  • HEAD,代表当前活跃的分支
  • 诸如 HEAD^^^ 这样的表达式,Git 会将其解析成一个提交 ID。确切说,这可能并非 “引用”,我想 Git 将其称作 “版本参数”,但我个人并未使用过这个术语。

个人而言,“符号引用” 这个术语颇为奇特,因为我觉得我只使用过 HEAD(即当前分支)作为符号引用。而 HEAD 在 Git 中占据核心位置,多数 Git 核心命令的行为都基于 HEAD 的值,因此我不太确定将其泛化成一个概念的实际意义。

refspecs

.git/config 配置 Git 远程仓库时,你可能会看到这样的代码 +refs/heads/main:refs/remotes/origin/main

[remote "origin"]
    url = [email protected]:jvns/pandas-cookbook
    fetch = +refs/heads/main:refs/remotes/origin/main

我对这段代码的含义并不十分清楚,我通常只是在使用 git clonegit remote add 配置远程仓库时采用默认配置,并没有动机去深究或改变。

“tree-ish”

git checkout 的手册页中,我们可以看到:

git checkout [-f|--ours|--theirs|-m|--conflict=<style>] [<tree-ish>] [--] <pathspec>...

那么这里的 tree-ish 是什么意思呢?其实当你执行 git checkout THING . 时,THING 可以是以下的任一种:

  • 一个提交 ID(如 182cd3f
  • 对一个提交 ID 的引用(如 mainHEAD^^v0.3.2
  • 一个位于提交内的子目录(如 main:./docs
  • 可能就这些?

对我个人来说,“提交内的目录”这个功能我从未使用过,从我的视角看,tree-ish 可以解读为“提交或对提交的引用”。

“索引”、“暂存”、“缓存”

这些术语都指向的是同一样东西(文件 .git/index,当你执行 git add 时,你的变动会在这里被暂存):

  • git diff --cached
  • git rm --cached
  • git diff --staged
  • 文件 .git/index

尽管它们都是指向同一个文件,但在实际使用中,这些术语的应用方式有所不同:

  • 很显然,--index--cached 并不总是表示同一种意思。我自己从未使用 --index,所以具体细节我就不展开讨论了,但是你可以在 Junio Hamano(Git 的主管维护者)的博客文章 中找到详细解释。
  • “索引” 会包含未跟踪的文件(我猜可能是对性能的考虑),但你通常不会把未跟踪的文件考虑在“暂存区”内。

“重置”、“还原”、“恢复”

许多人提到,“ 重置 reset ”、“ 还原 revert ” 和 “ 恢复 restore ” 这三个词非常相似,易使人混淆。

我认为这部分的困惑来自以下原因:

  • git reset --hardgit restore . 单独使用时,基本上达到的效果是一样的。然而,git reset --hard COMMITgit restore --source COMMIT . 相互之间是完全不同的。
  • 相应的手册页没有给出特别有帮助的描述:

    • git reset: “重置当前 HEAD 到指定的状态”
    • git revert: “还原某些现有的提交”
    • git restore: “恢复工作树文件”

虽然这些简短的描述为你详细说明了哪个名词受到了影响(“当前 HEAD”,“某些提交”,“工作树文件”),但它们都预设了你已经知道在这种语境中,“重置”、“还原”和“恢复”的准确含义。

以下是对它们各自功能的简要说明:

  • 重置 —— git revert COMMIT: 在你当前的分支上,创建一个新的提交,该提交是 COMMIT 的“反向”操作(如果 COMMIT 添加了 3 行,那么新的提交就会删除这 3 行)。
  • 还原 —— git reset --hard COMMIT: 强行将当前分支回退到 COMMIT 所在的状态,抹去自 COMMIT 以来的所有更改。这是一个高风险的操作。
  • 恢复 —— git restore --source=COMMIT PATH: 将 PATH 中的所有文件回退到 COMMIT 当时的状态,而不扰乱其他文件或提交历史。

“未跟踪的文件”、“远程跟踪分支”、“跟踪远程分支”

在 Git 中,“跟踪” 这个词以三种相关但不同的方式使用:

  • 未跟踪的文件 Untracked files ”:在 git status 命令的输出中可以看到。这里,“未跟踪” 意味着这些文件不受 Git 管理,不会被计入提交。
  • 远程跟踪分支 remote tracking branch ” 例如 origin/main。此处的“远程跟踪分支”是一个本地引用,旨在记住上次执行 git pullgit fetch 时,远程 originmain 分支的状态。
  • 我们经常看到类似 “分支 foo 被设置为跟踪 origin 上的远程分支 bar ”这样的提示。

即使“未跟踪的文件”和“远程跟踪分支”都用到了“跟踪”这个词,但是它们所在的上下文完全不同,所以没有太多混淆。但是,对于以下两种方式的“跟踪”使用,我觉得可能会产生些许困扰:

  • main 是一个跟踪远程的分支
  • origin/main 是一个远程跟踪分支

然而,在 Git 中,“跟踪远程的分支” 和 “远程跟踪分支” 是不同的事物,理解它们之间的区别非常关键!下面是对这两者区别的一个简单概述:

  • main 是一个分支。你可以在它上面做提交,进行合并等操作。在 .git/config 中,它通常被配置为 “追踪” 远程的 main 分支,这样你就可以用 git pullgit push 来同步和上传更改。
  • origin/main 则并不是一个分支,而是一个“远程跟踪分支”,这并不是一种真正的分支(这有些抱歉)。你不能在此基础上做提交。只有通过运行 git pullgit fetch 获取远程 main 的最新状态,才能更新它。

我以前没有深入思考过这种模糊的地方,但我认为很容易看出为什么它会让人感到困惑。

签出

签出做了两个完全无关的事情:

  • git checkout BRANCH 用于切换分支
  • git checkout file.txt 用于撤销对 file.txt 的未暂存修改

这是众所周知的混淆点,因此 Git 实际上已经将这两个功能分离到了 git switchgit restore(尽管你还是可以使用 checkout,就像我一样,在不愿丢弃 15 年对 git checkout 肌肉记忆的情况下)。

再者,即使用了 15 年,我仍然记不住 git checkout main file.txt 用于从 main 分支恢复 file.txt 版本的命令参数。

我觉得有时你可能需要在 checkout 命令后面加上--,帮助区分哪个参数是分支名,哪个是路径,但我并未这么使用过,也不确定何时需要这样做。

参考日志(reflog)

有很多人把 reflog 读作 re-flog,而不是 ref-log。由于本文已经足够长,我这里不会深入讨论参考日志,但值得注意的是:

  • 在 Git 中,“参考” 是一个泛指分支、标签和 HEAD 的术语
  • 参考日志(“reflog”)则为你提供了一个参考历次记录的历史追踪
  • 它是从一些极端困境中拯救出来的利器,比如说你不小心删除了重要的分支
  • 我觉得参考日志是 Git 用户界面中最难懂的部分,我总是试图避免使用它。

合并 vs 变基 vs 遴选

有许多人提及他们常常对于合并和变基的区别感到迷惑,并且不理解变基中的“ base ”指的是什么。

我会在这里尽量简要的进行描述,但是这些一句话的解释最终可能并不那么明了,因为每个人使用合并和变基创建工作流程时的方式差别挺大,要真正理解合并和变基,你必须理解工作流程。此外,有图示会更好理解。不过这个话题可能需要一篇独立的博客文章来完整讨论,所以我不打算深入这个问题。

  • 合并会创建一个新的提交,用来融合两个分支
  • 变基则会逐个地把当前分支上的提交复制到目标分支
  • 遴选跟变基类似,但是语法完全不同(一个显著的差异是变基是从当前分支复制提交,而遴选则会把提交复制到当前分支)

rebase --onto

git rebase 中,存在一个被称为 --onto 的选项。这一直让我感到困惑,因为 git rebase main 的核心功能就是将当前分支变基 main 运行上。那么,额外的 --onto 参数又是怎么回事呢?

我进行了一番查找,--onto 显然解决了一个我几乎没有或者说从未遇到过的问题,但我还是会记录下我对它的理解。

A - B - C (main)
      \
      D - E - F - G (mybranch)
          |
          otherbranch

设想一下,出于某种原因,我只想把提交 FG 变基到 main 上。我相信这应该是某些 Git 工作流中会经常遇到的场景。

显然,你可以运行 git rebase --onto main otherbranch mybranch 来完成这个操作。对我来说,在这个语法中记住 3 个不同的分支名顺序似乎是不可能的(三个分支名,对我来说实在太多了),但由于我从很多人那里听说过,我想它一定有它的用途。

提交

有人提到他们对 Git 中的提交作为一词双义(既作为动词也作为名词)的用法感到困惑。

例如:

  • 动词:“别忘了经常提交”
  • 名词:“main 分支上最新的提交”

我觉得大多数人应该能很快适应这个双关的用法,但是在 SQL 数据库中的“提交”用法与 Git 是有所不同,我认为在 SQL 数据库中,“提交”只是作为一个动词(你使用 COMMIT 来结束一个事务),并不作为名词。

此外,在 Git 中,你可以从以下三个不同的角度去考虑一个 Git 提交:

  1. 表示当前每个文件状态的快照
  2. 与父提交的差异
  3. 记录所有先前提交的历史

这些理解都是不错的:不同的命令在所有的这些情况下都会使用提交。例如,git show 将提交视为一个差异,git log 把提交看作是历史,git restore 则将提交理解为一个快照。

然而,Git 的术语并无太多助于你理解一个给定的命令正在如何使用提交。

更多令人困惑的术语

以下是更多让人觉得混淆的术语。我对许多这些术语的意思并不十分清楚。

我自己也不是很理解的东西:

  • git pickaxe (也许这是 git log -Sgit log -G,它们用于搜索以前提交的差异?)
  • 子模块(我知道的全部就是它们并不以我想要的方向工作)
  • Git 稀疏检出中的 “cone mode” (没有任何关于这个的概念,但有人提到过)

人们提及觉得混淆,但我在这篇已经 3000 字的文章中略过的东西:

  • blob、tree
  • “合并” 的方向
  • “origin”、“upstream”,“downstream”
  • pushpull 并不是对立面
  • fetchpull 的关系(pull = fetch + merge)
  • git porcelain
  • 子树
  • 工作树
  • 暂存
  • “master” 或者 “main” (听起来它在 Git 内部有特殊含义,但其实并没有)
  • 何时需要使用 origin main(如 git push origin main)vs origin/main

人们提及感到困惑的 Github 术语:

  • 拉取请求 pull request ” (与 Gitlab 中的 “ 合并请求 merge request ” 相比,人们似乎认为后者更清晰)
  • “压扁并合并” 和 “变基并合并” 的作用 (在昨天我从未听说过 git merge --squash,我一直以为 “压扁并合并” 是 Github 的特殊功能)

确实是 “每个 Git 术语”

我惊讶地发现,几乎 Git 的每个其他核心特性都被至少一人提及为某种方式中的困惑。我对听到更多我错过的混淆的 Git 术语的例子也有兴趣。

关于这个,有另一篇很棒的 2012 年的文章叫做《最困惑的 Git 术语》。它更多的讨论的是 Git 术语与 CVS 和 Subversion 术语的关联。

如果我要选出我觉得最令人困惑的 3 个 Git 术语,我现在会选:

  • head 是一个分支,HEAD 是当前分支
  • “远程跟踪分支” 和 “跟踪远程的分支” 是不同的事物
  • “索引”、“暂存的”、“已缓存的” 全部指的同一件事

就这样了!

在写这些的过程中,我学到了不少东西。我了解到了一些新的关于Git的事实,但更重要的是,现在我对于别人说Git的所有功能和特性都引起困惑有了更深的理解。

许多问题我之前根本没考虑过,比如我从来没有意识到,在讨论分支时,“跟踪”这个词的用法是多么地特别。

另外,尽管我已经尽力做到准确无误,但由于我涉猎到了一些我从未深入探讨过的Git的角落,所以可能还是出现了一些错误。

(题图:DALL-E/A/e1e5b964-5f32-41bb-811e-8978fb8556d4)


via: https://jvns.ca/blog/2023/11/01/confusing-git-terminology/

作者:Julia Evans 选题:lujun9972 译者:ChatGPT 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

在 Ubuntu 中拥有多个键盘布局并在它们之间切换非常简单。下面就教你怎么做。

你安装Ubuntu 时,你可以选择键盘布局。你可能已经默认选择了美国英语布局,现在你想将其更改为英国英语、印度英语或你选择的任何其他键盘布局。

好在你可以在同一个 Ubuntu 系统中拥有多种键盘布局。这是相当方便的。

在本教程中,你将学习:

  • 在 Ubuntu 桌面中添加新的键盘布局
  • 在可用键盘布局之间切换
  • 删除额外的键盘布局
  • 改变键盘布局的命令行方法

步骤 1:添加新的键盘布局

要更改键盘布局,你需要先在系统上启用另一个键盘布局。

进入系统设置。按 Ubuntu 中的 Super 键(Windows 键)并搜索“Setting”。

在系统设置中,在左侧边栏中查找 键盘 Keyboard 。选择后,你应该会在 输入源 Input Sources 下看到添加新键盘布局的选项。单击 “+” 号。

Add new keyboard layout

你将看到一些键盘选项,但如果单击三个点,你可以获得更多选项。你可以在此处滚动浏览或搜索。

There are more keyboard layouts available

单击你想要的键盘布局。请记住,所选的键盘布局可能有子布局。

例如,当我单击**英语(印度)时,它会向我显示从丹麦到加纳等的一些英语键盘布局。在这里,我选择了英语(印度,卢比)**键盘。这是将要添加的键盘布局。

你将在“输入源”下看到新添加的键盘布局。

这与我之前在 Ubuntu 中添加印地语键盘布局 时使用的方法相同。

步骤 2:切换键盘布局

这样,你就成功添加了另一个键盘布局。但它没有被使用。你必须在可用的输入源之间切换。

有两种方法可以做到这一点。

方法 1:同时使用 Super+Space 键

切换键盘布局的更快方法是 使用键盘快捷键 ,同时按 Super 键(Windows 键)和空格键。它将立即显示所有启用的键盘布局。

你可以在按住 Super 键的同时多次按空格键在可用选项之间移动。

方法 2:使用鼠标切换键盘布局

记住所有这些键盘快捷键并不容易,这是可以理解的。

当你在系统上启用多个键盘布局时,你会注意到键盘布局名称显示在面板的右上角。单击它,你将看到在布局之间切换的选项或查看所选的键盘布局。

Switch between keyboard layouts

额外提示:删除额外的键盘布局

不喜欢系统中的多个键盘布局? 不用担心。你可以轻松删除它们。

如你之前所见,再次进入键盘设置。单击你选择的键盘旁边的三个垂直点符号。你应该在这里看到删除选项。

使用命令行更改键盘布局(不推荐)

如果你是桌面用户,我强烈推荐上面讨论的图形方法。

如果你在服务器上并且必须使用其他键盘布局,则可以选择命令行。

现在,有多种方法可以更改 Linux 中的键盘布局。但对于 Ubuntu,我更喜欢 dpkg 方式。

sudo dpkg-reconfigure keyboard-configuration

输入你的密码,你将在终端中看到:

要在此 TUI(终端用户界面)中导航,请使用箭头键在可用选项之间移动。使用 Tab 键转至 “OK” 或 “Cancel” 选项。当你位于其中之一时,请按回车键确认你的选择。

选择你选择的国家/地区,然后你可以选择键盘布局。

添加附加键盘后,系统会要求你分配键盘快捷键以在它们之间进行切换。

你也可以将新键盘布局设置为系统中的默认布局和唯一布局。不过,如果你在不相似的语言之间执行此操作,这可能会存在风险。我的意思是,如果你使用英语美国键盘(物理)并将布局切换为匈牙利语,则你将无法使用所有按键。

之后你会看到几个屏幕。

如果你对新的键盘布局不满意,可以再次键入相同的命令,然后重新配置布局。

(题图:MJ/f03362cf-72d0-4003-b334-44c533e113a0)


via: https://itsfoss.com/ubuntu-change-keyboard/

作者:Abhishek Prakash 选题:lujun9972 译者:geekpi 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

在本系列的 上一篇文章 中,我们回顾了人工智能的历史,然后详细地讨论了矩阵。在本系列的第三篇文章中,我们将了解更多的矩阵操作,同时再介绍几个人工智能 Python 库。

在进入主题之前,我们先讨论几个人工智能和机器学习中常用的重要术语。 人工神经网络 artificial neural network (通常简称为 神经网络 neural network ,NN)是机器学习和深度学习的核心。顾名思义,它是受人脑的生物神经网络启发而设计的计算模型。本文中我没有插入神经网络模型的图片,因为在互联网上很容易找到它们。我相信任何对人工智能感兴趣的人应该都见过它们,左边是输入层,中间是一个或多个隐藏层,右边是输出层。各层之间的边上的 权重 weight 会随着训练不断变化。它是机器学习和深度学习应用成功的关键。

监督学习 supervised learning 无监督学习 unsupervised learning 是两个重要的机器学习模型。从长远来看,任何立志于从事人工智能或机器学习领域工作的人都需要学习它们,并了解实现它们的各种技术。这里我认为有必要简单说明两种模型之间的区别了。假设有两个人分别叫 A 和 B,他们要把苹果和橘子分成两组。他们从未见过苹果或橘子。他们都通过 100 张苹果和橘子的图片来学习这两种水果的特征(这个过程称为模型的训练)。不过 A 还有照片中哪些是苹果哪些是橘子的额外信息(这个额外信息称为标签)。这里 A 就像是一个监督学习模型,B 就像是无监督学习模型。你认为在是识别苹果和橘子的任务上,谁的效果更好呢?大多数人可能会认为 A 的效果更好。但是根据机器学习的理论,情况并非总是如此。如果这 100 张照片中只有 5 张是苹果,其它都是橘子呢?那么 A 可能根本就不熟悉苹果的特征。或者如果部分标签是错误的呢?在这些情况下,B 的表现可能比 A 更好。

在实际的机器学习应用中会发生这样的情况吗?是的!训练模型用的数据集可能是不充分的或者不完整的。这是两种模型都仍然在人工智能和机器学习领域蓬勃发展的众多原因之一。在后续文章中,我们将更正式地讨论它们。下面我们开始学习使用 JupyterLab,它是一个用于开发人工智能程序的强大工具。

JupyterLab 入门

在本系列的前几篇文章中,为了简单起见,我们一直使用 Linux 终端运行 Python 代码。现在要介绍另一个强大的人工智能工具——JupyterLab。在本系列的第一篇文章中,我们对比了几个候选项,最终决定使用 JupyterLab。它比 Jupyter Notebook 功能更强大,为我们预装了许多库和包,并且易于团队协作。还有一些其它原因,我们将在后续适时探讨它们。

在本系列的第一篇文章中,我们已经学习了如何安装 JupyterLab。假设你已经按文中的步骤安装好了 JupyterLab,使用 jupyter labjupyter-lab 命令在会默认浏览器(如 Mozilla Firefox、谷歌 Chrome 等)中打开 JupyterLab。(LCTT 译注:没有安装 JupyterLab 也不要紧,你可以先 在线试用 JupyterLab)图 1 是在浏览器中打开的 JupyterLab 启动器的局部截图。JupyterLab 使用一个名为 IPython(交互式 Python)的 Python 控制台。注意,IPython 其实可以独立使用,在 Linux 终端运行 ipython 命令就可以启动它。

图 1:JupyterLab 启动器

现阶段我们使用 JupyterLab 中的 Jupyter Notebook 功能。点击图 1 中用绿框标记的按钮,打开 Jupyter Notebook。这时可能会要求你选择内核。如果你按照本系列第一篇的步骤安装 JupyterLab,那么唯一的可选项就是 Python 3(ipykernel)。请注意,你还可以在 JupyterLab 中安装其它编程语言的内核,比如 C++、R、MATLAB、Julia 等。事实上 Jupyter 的内核相当丰富,你可以访问 Jupyter 内核清单 了解更多信息。

图 2:Jupyter Notebook 窗口

下面我们快速了解一下 Jupyter Notebook 的使用。图 2 显示的是一个在浏览器中打开的 Jupyter Notebook 窗口。从浏览器标签页的标题可以看出,Jupyter Notebook 打开的文件的扩展名是 .ipynb

在图 2 处可以看到有三个选项,它们表示 Jupyter Notebook 中可以使用的三种类型的单元。“Code”(绿色框) 表示代码单元,它是用来执行代码的。“Markdown” 单元可用于输入说明性的文本。如果你是一名计算机培训师,可以用代码单元和 Markdown 单元来创建交互式代码和解释性文本,然后分享给你的学员。“Raw”(红色框)表示原始数据单元,其中的内容不会被格式化或转换。

和在终端中不同,在 Jupyter Notebook 中你可以编辑并重新运行代码,这在处理简单的拼写错误时特别方便。图 3 是在 Jupyter Notebook 中执行 Python 代码的截图。

图 3:在 Jupyter Notebook 中执行 Python 代码

要在执行代码单元中的代码,先选中该单元格,然后点击蓝框标记的按钮。图 3 中用红框标记的是 Markdown 单元,用绿框标记的是代码单元,用黄框标记的执行代码的输出。在这个例子中,Python 代码输出的是 π 的值。

前面提到,JupyterLab 默认安装了许多库和包,我们不用自己安装了。你可以使用 import 命令将这些库导入到代码中。使用 !pip freeze 命令可以列出 JupyterLab 中目前可用的所有库和包。如果有库或包没有安装,大多数情况下都可以通过 pip install <全小写的库或者包的名称> 来安装它们。例如安装 TensorFlow 的命令是 pip install tensorflow。如果后面有库的安装命令不遵循这个格式,我会进行特别说明。随着本系列的继续,我们还会看到 Jupyter Notebook 和 JupyterLab 更多强大的功能。

复杂的矩阵运算

通过下面的代码,我们来了解一些更复杂的矩阵运算或操作。为了节省空间,我没有展示代码的输出。

import numpy as np
A = np.arr ay([[1,2,3],[4,5,6],[7,8,88]])
B = np.arr ay([[1,2,3],[4,5,6],[4,5,6]])
print(A.T)
print(A.T.T)
print(np.trace(A))
print(np.linalg.det(A))
C = np.linalg.inv(A)
print(C)
print(A@C)

下面我逐行来解释这些代码:

  1. 导入 NumPy 包。
  2. 创建矩阵 A
  3. 创建矩阵 B
  4. 打印矩阵 A 转置 transpose 。通过比较矩阵 AA 的转置,你用该可以大致理解转置操作到底做了什么。
  5. 打印 A 的转置的转置。可以看到它和矩阵 A 是相同的。这又提示了转置操作的含义。
  6. 打印矩阵 A trace 。迹是矩阵的对角线(也称为主对角线)元素的和。矩阵 A 的主对角线元素是 1、5 和 88,所以输出的值是 94。
  7. 打印 A 行列式 determinant 。当执行代码的结果是 -237.00000000000009(在你的电脑中可能略有区别)。因为行列式不为 0,所以称 A 为 非奇异矩阵 non-singular matrix
  8. 将矩阵 A inverse 保存到矩阵 C 中。
  9. 打印矩阵 C
  10. 打印矩阵 AC 的乘积。仔细观察,你会看到乘积是一个 单位矩阵 identity matrix ,也就是一个所有对角线元素都为 1,所有其它元素都为 0 的矩阵。请注意,输出中打印出的不是精确的 1 和 0。在我得到的答案中,有像 -3.81639165e-17 这样的数字。这是浮点数的科学记数法,表示 -3.81639165 × 10 -17, 即小数的 -0.0000000000000000381639165,它非常接近于零。同样输出中的其它数字也会有这种情况。我强烈建议你了解计算机是怎样表示浮点数的,这对你会有很大帮助。

根据第一篇文章中的惯例,可以将代码分成基本 Python 代码和人工智能代码。在这个例子中,除了第 1 行和第 9 行之外的所有代码行都可以被看作是人工智能代码。

现在将第 4 行到第 10 行的操作应用到矩阵 B 上。从第 4 行到第 6 行代码的输出没有什么特别之处。然而运行第 7 行时,矩阵 B 的行列式为 0,因此它被称为 奇异矩阵 singular matrix 。运行第 8 行代码会给产生一个错误,因为只有非奇异矩阵才存在逆矩阵。你可以尝试对本系列前一篇文章中的 8 个矩阵都应用相同的操作。通过观察输出,你会发现矩阵的行列式和求逆运算只适用于方阵。

方阵就是行数和列数相等的矩阵。在上面的例子中我只是展示了对矩阵执行各种操作,并没有解释它们背后的理论。如果你不知道或忘记了矩阵的转置、逆、行列式等知识的话,你最好自己学习它们。同时你也应该了解一下不同类型的矩阵,比如单位矩阵、对角矩阵、三角矩阵、对称矩阵、斜对称矩阵。维基百科上的相关文章是不错的入门。

现在让我们来学习 矩阵分解 matrix decomposition ,它是更复杂的矩阵操作。矩阵分解与整数的因子分解类似,就是把一个矩阵被写成其它矩阵的乘积。下面我通过图 4 中整数分解的例子来解释矩阵分解的必要性。代码单元开头的 %time 是 Jupyter Notebook 的 魔法命令 magic command ,它会打印代码运行所花费的时间。** 是 Python 的幂运算符。基本的代数知识告诉我们,变量 a 和 b 的值都等于 (6869 x 7873) 100。但图 4 显示计算变量 b 的速度要快得多。事实上,随着底数和指数的增大,执行时间的减少会越来越明显。

图 4:Python 代码的执行耗时

在几乎所有的矩阵分解技术技术中,原始矩阵都会被写成更稀疏的矩阵的乘积。 稀疏矩阵 sparse matrix 是指有很多元素值为零的矩阵。在分解后,我们可以处理稀疏矩阵,而不是原始的具有大量非零元素的 密集矩阵 dense matrix 。在本文中将介绍三种矩阵分解技术——LUP 分解、 特征分解 eigen decomposition 奇异值分解 singular value decomposition (SVD)。

为了执行矩阵分解,我们需要另一个强大的 Python 库,SciPy。SciPy 是基于 NumPy 库的科学计算库,它提供了线性代数、积分、微分、优化等方面的函数。首先,让我们讨论 LUP 分解。任何方阵都能进行 LUP 分解。LUP 分解有一种变体,称为 LU 分解。但并不是所有方阵都能 LU 分解。因此这里我们只讨论 LUP 分解。

在 LUP 分解中,矩阵 A 被写成三个矩阵 L、U 和 P 的乘积。其中 L 是一个 下三角矩阵 lower triangular matrix ,它是主对角线以上的所有元素都为零的方阵。U 是一个 上三角矩阵 upper triangular matrix ,它是主对角线以下所有元素为零的方阵。P 是一个 排列矩阵 permutation matrix 。这是一个方阵,它的每一行和每一列中都有一个元素为 1,其它元素的值都是 0。

现在看下面的 LUP 分解的代码。

import numpy as np
import scipy as sp
A=np.array([[11,22,33],[44,55,66],[77,88,888]])
P, L, U = sp.linalg.lu(A)
print(P)
print(L)
print(U)
print(P@L@U)

图 5 显示了代码的输出。第 1 行和第 2 行导入 NumPy 和 SciPy 包。在第 3 行创建矩阵 A。请记住,我们在本节中会一直使用矩阵 A。第 4 行将矩阵 A 分解为三个矩阵——PLU。第 5 行到第 7 行打印矩阵 PLU。从图 5 中可以清楚地看出,P 是一个置换矩阵,L 是一个下三角矩阵,U 是一个上三角矩阵。最后在第 8 行将这三个矩阵相乘并打印乘积矩阵。从图 5 可以看到乘积矩阵 P@L@U 等于原始矩阵 A,满足矩阵分解的性质。此外,图 5 也验证了矩阵 LUP 比矩阵 A 更稀疏。

图 5:用 SciPy 进行 LUP 分解

下面我们讨论特征分解,它是将一个方阵是用它的 特征值 eigenvalue 特征向量 eigenvector 来表示。用 Python 计算特征值和特征向量很容易。关于特征值和特征向量的理论解释超出了本文的讨论范围,如果你不知道它们是什么,我建议你通过维基百科等先了解它们,以便对正在执行的操作有一个清晰的概念。图 6 中是特征分解的代码。

图6:用 SciPy 进行特征分解

在图 6 中,第 1 行计算特征值和特征向量。第 2 行和第 3 行输出它们。注意,使用 NumPy 也能获得类似的效果,Lambda, Q = np.linalg.eig(A)。这也告诉我们 NumPy 和 SciPy 的功能之间有一些重叠。第 4 行重建了原始矩阵 A。第 4 行中的代码片段 np.diag(Lambda) 是将特征值转换为对角矩阵(记为 Λ)。对角矩阵是主对角线以外的所有元素都为 0 的矩阵。第 4 行的代码片段 sp.linalg.inv(Q) 是求 Q 的逆矩阵(记为 Q -1)。最后,将三个矩阵 QΛQ -1 相乘得到原始矩阵 A。也就是在特征分解中 A=QΛQ -1

图 6 还显示了执行的代码的输出。红框标记的是特征值,用绿框标记的是特征向量,重构的矩阵 A 用蓝框标记。你可能会感到奇怪,输出中像 11.+0.j 这样的数字是什么呢?其中的 j 是虚数单位。11.+0.j 其实就是 11.0+0.0j,即整数 11 的复数形式。

现在让我们来看奇异值分解(SVD),它是特征分解的推广。图 7 显示了 SVD 的代码和输出。第 1 行将矩阵 A 分解为三个矩阵 USV。第 2 行中的代码片段 np.diag(S)S 转换为对角矩阵。最后,将这三个矩阵相乘重建原始矩阵 A。奇异值分解的优点是它可以对角化非方阵。但非方阵的奇异值分解的代码稍微复杂一些,我们暂时不在这里讨论它。

图 7:用 SciPy 进行 奇异值分解

其它人工智能和机器学习的 Python 库

当谈到人工智能时,普通人最先想到的场景可能就是电影《终结者》里机器人通过视觉识别一个人。 计算机视觉 computer vision 是人工智能和机器学习技术被应用得最广泛的领域之一。下面我将介绍两个计算机视觉相关的库:OpenCV 和 Matplotlib。OpenCV 是一个主要用于实时计算机视觉的库,它由 C 和 C++ 开发。C++ 是 OpenCV 的主要接口,它通过 OpenCV-Python 向用户提供 Python 接口。Matplotlib 是基于 Python 的绘图库。我曾在 OSFY 上的一篇早期 文章 中详细介绍了 Matplotlib 的使用。

前面我一直在强调矩阵的重要性,现在我用一个实际的例子来加以说明。图 8 展示了在 Jupyter Notebook 中使用 Matplotlib 读取和显示图像的代码和输出。如果你没有安装 Matplotlib,使用 pip install matplotlib 命令安装 Matplotlib。

图 8:用 Matplotlib 读取和显示图像

在图 8 中,第 1 行和第 2 行从 Matplotlib 导入了一些函数。注意你可以从库中导入单个函数或包,而不用导入整个库。这两行是基本的 Python 代码。第 3 行从我的计算机中读取标题为 OSFY-Logo.jpg 的图像。我从 OSFY 门户网站的首页下载了这张图片。此图像高 80 像素,宽 270 像素。第 4 行和第 5 行在 Jupyter Notebook 窗口中显示图像。请注意图像下方用红框标记的两行代码,它的输出告诉我们变量 image 实际上是一个 NumPy 数组。具体来说,它是一个 80 x 270 x 3 的三维数组。

数组尺寸中的 80 x 270 就是图片的大小,这一点很容易理解。但是第三维度表示什么呢?这是因计算机像通常用 RGB 颜色模型来存储的彩色图。它有三层,分别用于表示红绿蓝三种原色。我相信你还记得学生时代的实验,把原色混合成不同的颜色。例如,红色和绿色混合在一起会得到黄色。在 RGB 模型中,每种颜色的亮度用 0 到 255 的数字表示。0 表示最暗,255 表示最亮。因此值为 (255,255,255) 的像素表示纯白色。

现在,执行代码 print(image), Jupyter Notebook 会将整个数组的一部分部分打印出来。你可以看到数组的开头有许多 255。这是什么原因呢?如果你仔细看 OSFY 的图标会发现,图标的边缘有很多白色区域,因此一开始就印了很多 255。顺便说一句,你还可以了解一下其他颜色模型,如 CMY、CMYK、HSV 等。

现在我们反过来从一个数组创建一幅图像。首先看图 9 中所示的代码。它展示了如何生成两个 3 x 3 的随机矩阵,它的元素是 0 到 255 之间的随机值。注意,虽然相同的代码执行了两次,但生成的结果是不同的。这是通过调用 NumPy 的伪随机数生成器函数 randint 实现的。实际上,我中彩票的几率都比这两个矩阵完全相等的几率大得多。

图 8:两个随机矩阵

接下来我们要生成一个形状为 512 x 512 x 3 的三维数组,然后将它转换为图像。为此我们将用到 OpenCV。注意,安装 OpenCV 命令是 pip install opencv-python。看下面的代码:

import cv2
img = np.random.randint(0, 256, size=(512, 512, 3))
cv2.imwrite('img.jpg', img)

第 1 行导入库 OpenCV。注意导入语句是 import cv2,这与大多数其他包的导入不同。第 3 行将矩阵 img 转换为名为 img.jpg 的图像。图 10 显示了由 OpenCV 生成的图像。在系统中运行这段代码,将图像将被保存在 Jupyter Notebook 的同一目录下。如果你查看这张图片的属性,你会看到它的高度是 512 像素,宽度是 512 像素。通过这些例子,很容易看出,任何处理计算机视觉任务的人工智能和机器学习程序使用了大量的数组、向量、矩阵以及线性代数中的思想。这也是本系列用大量篇幅介绍数组、向量和矩阵的原因。

图 10:OpenCV 生成的图像

最后,考虑下面显示的代码。image.jpg 输出图像会是什么样子?我给你两个提示。函数 zeros 在第 4 行和第 5 行创建了两个 512 x 512 的数组,其中绿色和蓝色填充了零。第 7 行到第 9 行用来自数组 redgreenblue 的值填充三维数组 img1

import numpy as np
import cv2
red = np.random.randint(0, 256, size=(512, 512))
green = np.zeros([512, 512], dtype=np.uint8)
blue = np.zeros([512, 512], dtype=np.uint8)
img1 = np.zeros([512,512,3], dtype=np.uint8)
img1[:,:,0] = blue
img1[:,:,1] = green
img1[:,:,2] = red
cv2.imwrite(‘image.jpg’, img1)

本期的内容就到此结束了。在下一篇文章中,我们将开始简单地学习 张量 tensor ,然后安装和使用 TensorFlow。TensorFlow 是人工智能和机器学习领域的重要参与者。之后,我们将暂时放下矩阵、向量和线性代数,开始学习概率论。概率论跟线性代数一样是人工智能的重要基石。

(题图:MJ/ec8e9a02-ae13-4924-b6cb-74ef96ab8af9)


via: https://www.opensourceforu.com/2023/07/ai-a-few-more-useful-python-libraries/

作者:Deepu Benson 选题:lujun9972 译者:toknow-gh 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

你好!我一直在投入写作一本关于 Git 的小册,因此我对 Git 分支投入了许多思考。我不断从他人那里听说他们觉得 Git 分支的操作方式违反直觉。这使我开始思考:直觉上的分支概念可能是什么样,以及它如何与 Git 的实际操作方式区别开来?

在这篇文章中,我想简洁地讨论以下几点内容:

  • 我认为许多人可能有的一个直觉性的思维模型
  • Git 如何在内部实现分支的表示(例如,“分支是对提交的指针”)
  • 这种“直觉模型”与实际操作方式之间的紧密关联
  • 直觉模型的某些局限性,以及为何它可能引发问题

本文无任何突破性内容,我会尽量保持简洁。

分支的直观模型

当然,人们对分支有许多不同的直觉。我自己认为最符合“苹果树的一个分支”这一物理比喻的可能是下面这个。

我猜想许多人可能会这样理解 Git 分支:在下图中,两个红色的提交就代表一个“分支”。

我认为在这个示意图中有两点很重要:

  1. 分支上有两个提交
  2. 分支有一个“父级”(main),它是这个“父级”的分支

虽然这个观点看似合理,但实际上它并不符合 Git 对于分支的定义 — 最重要的是,Git 并没有一个分支的“父级”的概念。那么,Git 又是如何定义分支的呢?

在 Git 里,分支是完整的历史

在 Git 中,一个分支是每个过去提交的完整历史记录,而不仅仅是那个“分支”提交。因此,在我们上述的示意图中,所有的分支(mainbranch)都包含了 4 次提交。

我创建了一个示例仓库,地址为:https://github.com/jvns/branch-example。它设置的分支方式与前图一样。现在,我们来看看这两个分支:

main 分支包含了 4 次提交:

$ git log --oneline main
70f727a d
f654888 c
3997a46 b
a74606f a

mybranch 分支也有 4 次提交。最后两次提交在这两个分支里都存在。

$ git log --oneline mybranch
13cb960 y
9554dab x
3997a46 b
a74606f a

因此,mybranch 中的提交次数为 4,而不仅仅是 2 次“分支”提交,即 13cb9609554dab

你可以用以下方式让 Git 绘制出这两个分支的所有提交:

$ git log --all --oneline --graph
* 70f727a (HEAD -> main, origin/main) d
* f654888 c
| * 13cb960 (origin/mybranch, mybranch) y
| * 9554dab x
|/
* 3997a46 b
* a74606f a

分支以提交 ID 的形式存储

在 Git 的内部,分支会以一种微小的文本文件的形式存储下来,其中包含了一个提交 ID。这就是我一开始提及到的“技术上正确”的定义。这个提交就是分支上最新的提交。

我们来看一下示例仓库中 mainmybranch 的文本文件:

$ cat .git/refs/heads/main
70f727acbe9ea3e3ed3092605721d2eda8ebb3f4
$ cat .git/refs/heads/mybranch
13cb960ad86c78bfa2a85de21cd54818105692bc

这很好理解:70f727main 上的最新提交,而 13cb96mybranch 上的最新提交。

这样做的原因是,每个提交都包含一种指向其父级的指针,所以 Git 可以通过追踪这些指针链来找到分支上所有的提交。

正如我前文所述,这里遗漏的一个重要因素是这两个分支间的任何关联关系。从这里能看出,mybranchmain 的一个分支——这一点并没有被表明出来。

既然我们已经探讨了直观理解的分支概念是如何不成立的,我接下来想讨论的是,为何它在某些重要的方面又是如何成立的。

人们的直观感觉通常并非全然错误

我发现,告诉人们他们对 Git 的直觉理解是“错误的”的说法颇为流行。我觉得这样的说法有些可笑——总的来说,即使人们关于某个题目的直觉在某些方面在技术上不精确,但他们通常会有完全合理的理由来支持他们的直觉!即使是“不正确的”模型也可能极其有用。

现在,我们来讨论三种情况,其中直觉上的“分支”概念与我们实际在操作中如何使用 Git 非常相符。

变基操作使用的是“直观”的分支概念

现在,让我们回到最初的图片。

当你在 main 上对 mybranch 执行 变基 rebase 操作时,它将取出“直观”分支上的提交(只有两个红色的提交)然后将它们应用到 main 上。

执行结果就是,只有两次提交(xy)被复制。以下是相关操作的样子:

$ git switch mybranch
$ git rebase main
$ git log --oneline mybranch
952fa64 (HEAD -> mybranch) y
7d50681 x
70f727a (origin/main, main) d
f654888 c
3997a46 b
a74606f a

在此,git rebase 创建了两个新的提交(952fa647d50681),这两个提交的信息来自之前的两个 xy 提交。

所以直觉上的模型并不完全错误!它很精确地告诉你在变基中发生了什么。

但因为 Git 不知道 mybranchmain 的一个分叉,你需要显式地告诉它在何处进行变基。

合并操作也使用了“直观”的分支概念

合并操作并不复制提交,但它们确实需要一个“ 基础 base ”提交:合并的工作原理是查看两组更改(从共享基础开始),然后将它们合并。

我们撤销刚才完成的变基操作,然后看看合并基础是什么。

$ git switch mybranch
$ git reset --hard 13cb960  # 撤销 rebase
$ git merge-base main mybranch
3997a466c50d2618f10d435d36ef12d5c6f62f57

这里我们获得了分支分离出来的“基础”提交,也就是 3997a4。这正是你可能会基于我们的直观图片想到的提交。

GitHub 的拉取请求也使用了直观的概念

如果我们在 GitHub 上创建一个拉取请求,打算将 mybranch 合并到 main,这个请求会展示出两次提交:也就是 xy。这完全符合我们的预期,也和我们对分支的直观认识相符。

我想,如果你在 GitLab 上发起一个合并请求,那显示的内容应该会与此类似。

直观理解颇为精准,但它有一定局限性

这使我们的对分支直观定义看起来相当准确!这个“直观”的概念和合并、变基操作以及 GitHub 拉取请求的工作方式完全吻合。

当你在进行合并、变基或创建拉取请求时,你需要明确指定另一个分支(如 git rebase main),因为 Git 不知道你的分支是基于哪个分支的。

然而,关于分支的直观理解有一个比较严重的问题:你直觉上认为 main 分支和某个分离的分支有很大的区别,但 Git 并不清楚这点。

所以,现在我们要来讨论一下 Git 分支的不同种类。

主干和派生分支

对于人类来说,mainmybranch 有着显著的区别,你可能针对如何使用它们,有着截然不同的意图。

通常,我们会将某些分支视为“ 主干 trunk ”分支,同时将其他一些分支看作是“派生”。你甚至可能有派生的派生分支。

当然,Git 自身并没有这样的区分(“派生”是我刚刚构造的术语!),但是分支的种类确实会影响你如何处理它。

例如:

  • 你可能会想将 mybranch 变基到 main,但你大概不会想将 main 变基到 mybranch —— 那就太奇怪了!
  • 一般来说,人们在重写“主干”分支的历史时比短期存在的派生分支更为谨慎。

Git 允许你进行“反向”的变基

我认为人们经常对 Git 感到困惑的一点是 —— 由于 Git 并没有分支是否是另一个分支的“派生”的概念,它不会给你任何关于何时合适将分支 X 变基到分支 Y 的指引。这一切需要你自己去判断。

例如,你可以执行以下命令:

$ git checkout main
$ git rebase mybranch

或者

$ git checkout mybranch
$ git rebase main

Git 将会欣然允许你进行任一操作,尽管在这个案例中 git rebase main 是极其正常的,而 git rebase mybranch 则显得格外奇怪。许多人表示他们对此感到困惑,所以我提供了一个展示两种变基类型的图片以供参考:

相似地,你可以进行“反向”的合并,尽管这相较于反向变基要正常得多——将 mybranch 合并到 main 和将 main 合并到 mybranch 都有各自的益处。

下面是一个展示你可以进行的两种合并方式的示意图:

Git 对于分支之间缺乏层次结构感觉有些奇怪

我经常听到 “main 分支没什么特别的” 的表述,而这令我感到困惑——对于我来说,我处理的大部分仓库里,main 无疑是非常特别的!那么人们为何会称其为不特别呢?

我觉得,重点在于:尽管分支确实存在彼此间的关系(main 通常是非常特别的!),但 Git 并不知情这些关系。

每当你执行如 git rebasegit merge 这样的 git 命令时,你都必须明确地告诉 Git 分支间的关系,如果你出错,结果可能会相当混乱。

我不知道 Git 在此方面的设计究竟“对”还是“错”(无疑它有利有弊,而我已对无休止的争论感到厌倦),但我认为,这对于许多人来说,原因在于它有些出人意料。

Git 关于分支的用户界面也同样怪异

假设你只想查看某个分支上的“派生”提交,正如我们之前讨论的,这是完全正常的需求。

下面是用 git log 查看我们分支上的两次派生提交的方法:

$ git switch mybranch
$ git log main..mybranch --oneline
13cb960 (HEAD -> mybranch, origin/mybranch) y
9554dab x

你可以用 git diff 这样查看同样两次提交的合并差异:

$ git diff main...mybranch

因此,如果你想使用 git log 查看 xy 这两次提交,你需要用到两个点(..),但查看同样的提交使用 git diff,你却需要用到三个点(...)。

我个人从来都记不住 ..... 的具体用意,所以我通常虽然它们在原则上可能很有用,但我选择尽量避免使用它们。

在 GitHub 上,默认分支具有特殊性

同样值得一提的是,在 GitHub 上存在一种“特殊的分支”:每一个 GitHub 仓库都有一个“默认分支”(在 Git 术语中,就是 HEAD 所指向的地方),具有以下的特别之处:

  • 初次克隆仓库时,默认会检出这个分支
  • 它作为拉取请求的默认接收分支
  • GitHub 建议应该保护这个默认分支,防止被强制推送,等等。

很可能还有许多我未曾想到的场景。

总结

这些说法在回顾时看似是显而易见的,但实际上我花费了大量时间去搞清楚一个更“直观”的分支概念,这是因为我已经习惯了技术性的定义,“分支是对某次提交的引用”。

同样,我也没有真正去思索过如何在每次执行 git rebasegit merge 命令时,让 Git 明确理解你分支之间的层次关系——对我而言,这已经成为第二天性,并没有觉得有何困扰。但当我反思这个问题时,可以明显看出,这很容易导致某些人混淆。

(题图:MJ/a5a52832-fac8-4190-b3bd-fec70166aa16)


via: https://jvns.ca/blog/2023/11/23/branches-intuition-reality/

作者:Julia Evans 选题:lujun9972 译者:ChatGPT 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出