为什么 Git 本身不支持 UTF-16?

Why doesn't Git natively support UTF-16?

Git 支持多种不同的编码方案,UTF-7, UTF-8, and UTF-32,以及非 UTF 方案。

鉴于此,为什么它不支持 UTF-16?

有很多问题询问如何 Git 支持 UTF-16,但我认为还没有明确询问或回答这个问题。

Git 代码库中第一次提到 UTF-8 可以追溯到 d4a9ce7 (Aug. 2005, v0.99.6),这是关于邮箱补丁:

Optionally, with the '-u' flag, the output to .info and .msg is transliterated from its original chaset [sic] to utf-8. This is to encourage people to use utf8 in their commit messages for interoperability.

Junio C Hamano /滨野纯<junkio@cox.net>签名。

字符编码已在 commit 3a59e59 (July 2017, Git v2.6.0-rc0) 中阐明:

That "git is encoding agnostic" is only really true for blob objects. E.g. the 'non-NUL bytes' requirement of tree and commit objects excludes UTF-16/32, and the special meaning of '/' in the index file as well as space and linefeed in commit objects eliminates EBCDIC and other non-ASCII encoding.

Git expects bytes < 0x80 to be pure ASCII, thus CJK encoding that partly overlap with the ASCII range are problematic as well. E.g. fmt_ident() removes trailing 0x5C from usernames on the assumption that it is ASCII '\'. However, there are over 200 GBK double byte codes that end in 0x5C.

UTF-8 as default encoding on Linux and respective path translations in the Mac and Windows versions have established UTF-8 NFC as de facto standard for path names.

有关最后一个补丁的更多信息,请参阅“git, msysgit, accents, utf-8, the definitive answers”。

Documentation/i18n.txt 的最新版本包括:

Git is to some extent character encoding agnostic.

  • The contents of the blob objects are uninterpreted sequences of bytes.
    There is no encoding translation at the core level.

  • Path names are encoded in UTF-8 normalization form C.
    This applies to:

    • tree objects,
    • the index file,
    • ref names, as well as path names in
    • command line arguments,
    • environment variables and
    • configuration files (.git/config, gitignore, gitattributes and gitmodules)

您可以在commit 0217569 (Jan. 2012, Git v2.1.0-rc0)中查看 UTF-8 路径转换的示例,它添加了 Win32 Unicode 文件名支持。

Changes opendir/readdir to use Windows Unicode APIs and convert between UTF-8/UTF-16.

关于命令行参数,请参见。 commit 3f04614 (Jan. 2011, Git v2.1.0-rc0),在启动时将命令行参数从 UTF-16 转换为 UTF-8。


注意:在 Git 2.21(2019 年 2 月)之前,代码和测试假定系统提供的 iconv() 在被要求编码为UTF-16(或 UTF-32),但显然有些实现输出没有 BOM 的 big-endian。
添加了编译时旋钮以帮助此类系统(例如 NonStop)将 BOM 添加到输出以提高可移植性。

参见 commit 79444c9 (12 Feb 2019) by brian m. carlson (bk2204)
(由 Junio C Hamano -- gitster -- in commit 18f9fb6 合并,2019 年 2 月 13 日)

utf8: handle systems that don't write BOM for UTF-16

When serializing UTF-16 (and UTF-32), there are three possible ways to write the stream. One can write the data with a BOM in either big-endian or little-endian format, or one can write the data without a BOM in big-endian format.

Most systems' iconv implementations choose to write it with a BOM in some endianness, since this is the most foolproof, and it is resistant to misinterpretation on Windows, where UTF-16 and the little-endian serialization are very common. For compatibility with Windows and to avoid accidental misuse there, Git always wants to write UTF-16 with a BOM, and will refuse to read UTF-16 without it.

However, musl's iconv implementation writes UTF-16 without a BOM, relying on the user to interpret it as big-endian. This causes t0028 and the related functionality to fail, since Git won't read the file without a BOM.

所以这里添加的“编译时旋钮”在Makefile:

# Define ICONV_OMITS_BOM if your iconv implementation does not write a
# byte-order mark (BOM) when writing UTF-16 or UTF-32 and always writes in
# big-endian format.
#
ifdef ICONV_OMITS_BOM
    BASIC_CFLAGS += -DICONV_OMITS_BOM
endif

因为 NonStop OS and its associated NonStop SQL product always use UTF-16BE (16-bit) encoding for the Unicode (UCS2) character set,您可以在该环境中使用 ICONV_OMITS_BOM

我用了整整一章的重要部分(目前相当垂死)book (see Chapter 3, which is in better shape than later chapters) to the issue of character encoding, because it is a historical mess. It's worth mentioning here, though, that part of the premise of this question—that Git supports UTF-7 and UTF-32 in some way—is wrong: UTF-7 is a standard that never even came about,可能根本不应该使用(所以自然地,旧的 Internet Explorer 版本会这样做,这会导致安全性链接的维基百科页面上提到的问题)。

也就是说,让我们先将字符编码代码页分开。 (另请参阅下面的 footnote-ish 部分。)这里的根本问题是计算机——嗯,无论如何,现代 计算机——使用一系列 8 位 bytes,每个字节表示 [0..255] 范围内的一个整数。旧系统有 6、7、8 甚至 9 位字节,但我认为将任何少于 8 位的东西称为“字节”是一种误导。 (BBN's "C machines" had 10-bit bytes!) In any case, if one byte represents one character-symbol, this gives us an upper limit of 256 kinds of symbols. In those bad old days of ASCII,这就足够了,因为 ASCII 只有 128 个符号,其中 33 个是 non-printing 符号(控制代码 0x000x1f,加上 0x7f 表示DEL 或纸带上删除的打孔器,此处以十六进制表示)。

当我们需要超过 94 个可打印符号加上 space (0x20) 时,我们——我们 我的意思是 人在世界各地使用计算机,不具体—说:好吧,看看这个,我们有 128 个未使用的编码,0x80通过 0xff,让我们使用其中的一些! 所以法语使用了一些 ç 和 é 等等,以及像 « 和 » 这样的标点符号。 Z-with-caron,捷克人需要一个。对于西里尔字母,俄罗斯人需要很多。希腊人需要很多,等等。结果是8位space的上半部分炸成了很多不兼容的集合,人们称之为code pages.

本质上,计算机存储一些 eight-bit 字节值,例如十进制 235(0xEB 十六进制),这取决于其他东西——另一个计算机程序,或者最终是一个人盯着屏幕,将 235 解释为西里尔字母 л 或希腊字母 λ 或其他任何字符。代码页,如果我们使用的话,告诉我们“235”是什么意思:我们应该强加什么样的语义。

这里的问题是我们可以支持多少个字符代码是有限制的。如果我们想让西里尔字母 L (л) 与希腊字母 L (lambda, λ) 共存,我们不能同时使用 CP-1251 and CP-1253 at the same time, so we need a better way to encode the symbol. One obvious way is to stop using one-byte values to encode symbols: if we use two-byte values, we can encode 65536 values, 0x0000 through 0xffff inclusive; subtract a few for control codes and there is still room for many alphabets. However, we rapidly blew through even this limit, so we went to Unicode, which has room for 1,114,112 of what it calls code points,它们分别代表某种具有某种语义意义的符号。现在有超过 100,000 个正在使用,包括 Emoji 和 .

将 Unicode 编码为字节或字

这里是UTF-8,UTF-16,UTF-32,UCS-2, and UCS-4 all come in. These are all schemes for encoding Unicode code points—one of those ~1 million values—into byte-streams. I'm going to skip over the UCS ones entirely and look only at the UTF-8 and UTF-16 encodings, since those are the two that are currently the most interesting. (See also What are Unicode, UTF-8, and UTF-16?)

UTF-8 编码很简单:十进制值小于 128 的任何代码点都被编码为包含该值的字节。这意味着普通的 ASCII 文本字符仍然是普通的 ASCII 文本字符。 0x0080(十进制 128)到 0x07ff(十进制 2047)中的代码点编码为两个字节,其值都在 128-255 范围内,因此可与 one-byte 编码值区分开来. 0x08000xffff 范围内的代码点编码为同一 128-255 范围内的三个字节,其余有效值编码为四个这样的字节。 就 Git 本身而言,这里的关键是没有任何编码值类似于 ASCII NUL (0x00) 或斜杠 (0x2f)。

这种 UTF-8 编码的作用是允许 Git 假装 文本字符串——尤其是文件名——是 slash-separated 名称组成部分末端是,或者可以是,用 ASCII NUL 字节标记。这是 Git 在 tree object 中使用的编码,因此 UTF-8 编码树 object 正好适合,不需要摆弄。

UTF-16 编码每个字符使用两个成对的字节。这对 Git 和路径名有两个问题。首先,一对中的一个字节可能意外地类似于 /,并且所有 ASCII-valued 字符都必须编码为一对字节,其中一个字节是 0x00,类似于 ASCII NUL。因此 Git 需要知道:此路径名已用 UTF-16 编码并在 byte-pairs 上工作。树 object 中没有空间容纳此信息,因此 Git 需要一个新的 object 类型。其次,每当我们将一个 16 位值分成两个单独的 8 位字节时,我们都会按某种顺序进行:我要么先给你更重要的字节,然后是更不重要的字节;或者我先给你不太重要的字节,然后是更重要的字节。这第二个问题导致UTF-16有byte order marks的原因。 UTF-8 不需要字节顺序标记,就足够了,那么为什么不在树中使用它呢?所以 Git 确实如此。

这对树来说很好,但我们也有提交、标签和 blob

Git 对这四种 object 中的三种有自己的解释:

  1. 提交包含哈希 ID。
  2. 树包含路径名、文件模式和哈希 ID。
  3. 标签包含哈希 ID。

这里没有列出的是blob,而且在大多数情况下,Git不做任何解释斑点。

为了便于理解提交、树和标签,Git 将这三者大部分限制在 UTF-8 中。但是,Git 允许 日志消息 在提交中,或 标记文本 在标签中,有点(大部分)未被解释。这些在 Git 解释的 header 之后,所以即使此时有一些特别棘手或丑陋的东西,那也是非常安全的。 (这里有一些小风险,因为 PGP 签名出现在 header 下方,do 得到解释。)特别是对于提交,现代 Git 将包括解释部分中的 encoding header 行,然后 Git 可以尝试 decode 提交消息 body,并且 re-encode 将其转换为解释 Git 吐出的字节的任何程序使用的任何编码。1

同样的规则也适用于带注释的标签 objects。我不确定 Git 是否有为标签执行此操作的代码(提交代码可能主要是 re-used,但标签更常见的是具有 PGP 签名,强制使用 UTF-8 可能更明智这里)。由于树是 internal objects,它们的编码基本上是不可见的——你不需要意识到这一点(除了我在我的书中指出的问题) .

这留下了斑点,那是大猩猩。


1这是计算世界中反复出现的主题:一切都被反复编码和解码。考虑一些东西是如何通过 Wi-Fi 或有线网络连接到达的:它被编码成某种无线电波或类似的东西,然后一些硬件将其解码成 bit-stream,而其他一些硬件 re-encodes 转换为字节流。硬件 and/or 软件剥离 headers,以某种方式解释剩余的编码,适当更改数据,以及 re-encode 位和字节,以供另一层硬件和软件处理.完成任何事情都是奇迹。


Blob 编码

Git 喜欢声称它完全不知道存储在文件中的实际 数据,如 Git blob。这甚至大部分都是正确的。或者,好吧,一半是真的。或者其他的东西。只要 Git 所做的只是 存储 您的数据,那就完全正确! Git 只存储字节。这些字节 的意思 由你决定。

当您 运行 git diffgit merge 时,这个故事就会分崩离析,因为 diff 算法以及合并代码是 line导向。行以换行符终止。 (如果您使用的系统使用 CRLF 而不是换行符,那么 CRLF 对的第二个字符 换行符,所以这里没有问题 - Git没有终止的最后一行是可以的,虽然这会导致一些轻微的胃灼热。)如果文件是用 UTF-16 编码的,很多字节往往看起来是 ASCII NUL,所以 Git 只是将其视为二进制文件。

这个可修复的:Git可以将UTF-16数据解码为UTF-8,通过其现有的所有line-oriented算法提供该数据(现在会看到 newline-terminated 行),然后 re-encode 数据返回到 UTF-16。这里有很多小的技术问题;最大的问题是确定某个文件 UTF-16,如果是,是哪种字节顺序(UTF-16-LE 或 UTF-16-BE?)。如果文件有一个字节顺序标记,它会处理字节序问题,并且可以将 UTF-16-ness 编码为 .gitattributes,就像您当前可以声明文件 binarytext ,所以一切都可以解决。乱七八糟的,还没有人做过这个工作

Footnote-ish:代码页可以被认为是一种(蹩脚的)编码形式

我在上面提到过,我们使用 Unicode 所做的事情是将 21 位代码点值编码为一定数量的 eight-bit 字节(UTF-8 中为 1 到 4 个字节,UTF-中为 2 个字节- 16—UTF-16 调用 surrogates 有一个丑陋的小技巧,将 21 位值压缩到 16 位容器中,偶尔使用成对的 16 位值,在这里)。这种编码技巧意味着我们可以表示所有合法的 21 位代码点值,尽管我们可能需要多个 8 位字节才能这样做。

当我们使用代码页 (CP-number) 时,我们正在做的是,或者至少可以看作是,mapping 256 个值——那些适合一个 8 位字节的值——进入 那个 21 位代码点 space。我们从不超过 256 个这样的代码点中挑选出一些子集,然后说:这些是我们允许的代码点。 我们将第一个代码点编码为 0xa0 ,第二个为 0xa1,依此类推。我们总是至少为一些控制代码留出空间——通常是 0x000x1f 范围内的所有 32 个——并且通常我们保留整个 7 位 ASCII 子集,就像 Unicode 本身所做的那样(参见 List of Unicode characters),这就是为什么我们通常从 0xa0.

开始

当一个人编写正确的 Unicode 支持时库,代码页 只需使用这种形式的索引就可以变成翻译表。困难的部分是为所有代码页制作准确的表格,其中有很多。

代码页的好处在于,字符再次 one-byte-each。糟糕的是,当您说:我使用此代码页时,您选择了一次符号集。 从那时起,您就被锁定在 Unicode 的这个小子集中。如果切换到另一个代码页,部分或全部 eight-bit 字节值代表 不同的 符号。

短格式增加了对宽字符的支持,这让一切变得更加困难。处理任何 8 位 ISO 代码页或 UTF-8 或任何其他 MBCS 的所有内容都可以毫不费力地 scan/span/copy 字符串。尝试添加对传输编码包含嵌入空值的字符串的支持,即使是微不足道的操作也会让您的所有代码变得臃肿。

我什至不知道 声称 UTF-16 的任何优点都不会被实际开始使用时出现的缺点所抵消。您可以使用相同的简单代码识别任何 ASCII、UTF-8、所有 16 个 ISO/IEC-8859 集、所有 EBCDIC 以及可能还有十几个中的字符串边界。只有轻微的限制(基于 ascii,为多行终止符约定添加几行),您可以获得基本的标记化,并且音译到通用内部代码页基本上是免费的。

添加 UTF-16 支持,您只是为自己付出了大量额外的努力和复杂性,但所有这些工作都无济于事 -- 在说“哦,但现在它可以处理 UTF-16!”之后,什么 else 现在可以通过所有增加的膨胀和努力实现吗?没有什么。 UTF-16 可以做的所有事情,UTF-8 也可以做,而且通常要好得多。

Git 即将支持 UTF-16...对于环境变量,Git 2.20(2018 年第 4 季度)
(以及 Git 2.21 中的错误修复:请参阅答案的第二部分)

参见 commit fe21c6b, commit 665177e (30 Oct 2018) by Johannes Schindelin (dscho)
帮助:Jeff Hostetler (jeffhostetler).
(由 Junio C Hamano -- gitster -- in commit 0474cd1 合并,2018 年 11 月 13 日)

mingw: reencode environment variables on the fly (UTF-16 <-> UTF-8)

On Windows, the authoritative environment is encoded in UTF-16.
In Git for Windows, we convert that to UTF-8 (because UTF-16 is such a foreign idea to Git that its source code is unprepared for it).

Previously, out of performance concerns, we converted the entire environment to UTF-8 in one fell swoop at the beginning, and upon putenv() and run_command() converted it back.

Having a private copy of the environment comes with its own perils: when a library used by Git's source code tries to modify the environment, it does not really work (in Git for Windows' case, libcurl, see git-for-windows/git/compare/bcad1e6d58^...bcad1e6d58^2 for a glimpse of the issues).

Hence, it makes our environment handling substantially more robust if we switch to on-the-fly-conversion in getenv()/putenv() calls.
Based on an initial version in the MSVC context by Jeff Hostetler, this patch makes it so.

Surprisingly, this has a positive effect on speed: at the time when the current code was written, we tested the performance, and there were so many getenv() calls that it seemed better to convert everything in one go.
In the meantime, though, Git has obviously been cleaned up a bit with regards to getenv() calls so that the Git processes spawned by the test suite use an average of only 40 getenv()/putenv() calls over the process lifetime.

Speaking of the entire test suite: the total time spent in the re-encoding in the current code takes about 32.4 seconds (out of 113 minutes runtime), whereas the code introduced in this patch takes only about 8.2 seconds in total.
Not much, but it proves that we need not be concerned about the performance impact introduced by this patch.


使用 Git 2.21(2019 年第一季度),之前的路径引入了一个影响 GIT_EXTERNAL_DIFF 命令的错误:字符串 从 getenv() 返回的是非易失性的,这是不正确的,那 已更正。

参见 commit 6776a84 (11 Jan 2019) by Kim Gybels (Jeff-G)
(由 Junio C Hamano -- gitster -- in commit 6a015ce 合并,2019 年 1 月 29 日)

错误已在 git-for-windows/git issue 2007 中报告:
"Unable to Use difftool on More than 8 File"

$ yes n | git -c difftool.prompt=yes difftool fe21c6b285df fe21c6b285df~100

Viewing (1/404): '.gitignore'
Launch 'bc3' [Y/n]?
Viewing (2/404): 'Documentation/.gitignore'
[...]
Viewing (8/404): 'Documentation/RelNotes/2.18.1.txt'
Launch 'bc3' [Y/n]?
Viewing (9/404): 'Documentation/RelNotes/2.19.0.txt'
Launch 'bc3' [Y/n]? error: cannot spawn ¦?: No such file or directory
fatal: external diff died, stopping at Documentation/RelNotes/2.19.1.txt

因此:

diff: ensure correct lifetime of external_diff_cmd

According to getenv(3)'s notes:

The implementation of getenv() is not required to be reentrant.
The string pointed to by the return value of getenv() may be statically allocated, and can be modified by a subsequent call to getenv(), putenv(3), setenv(3), or unsetenv(3).

由于 getenv() 返回的字符串在后续调用 getenv() 时允许更改,因此请确保在从环境中缓存 external_diff_cmd 时进行复制。

fe21c6b 以来,此问题在 Windows 的 Git 上变得明显 (mingw:动态重新编码环境变量(UTF-16 <-> UTF-8)), 当 compat/mingw.c 中提供的 getenv() 实现被更改时 保留一定数量的分配字符串并释放它们 后续调用。


Git 2.24(2019 年第 4 季度)修复了之前引入的黑客攻击。

参见 commit 2049b8d, commit 97fff61 (30 Sep 2019) by Johannes Schindelin (dscho)
(由 Junio C Hamano -- gitster -- in commit 772cad0 合并,2019 年 10 月 9 日)

Move git_sort(), a stable sort, into libgit.a

The qsort() function is not guaranteed to be stable, i.e. it does not promise to maintain the order of items it is told to consider equal.
In contrast, the git_sort() function we carry in compat/qsort.c is stable, by virtue of implementing a merge sort algorithm.

In preparation for using a stable sort in Git's rename detection, move the stable sort into libgit.a so that it is compiled in unconditionally, and rename it to git_stable_qsort().

Note: this also makes the hack obsolete that was introduced in fe21c6b (mingw: reencode environment variables on the fly (UTF-16 <-> UTF-8), 2018-10-30, Git v2.20.0-rc0), where we included compat/qsort.c directly in compat/mingw.c to use the stable sort.

Git最近开始了解UTF-16等编码。请参阅 gitattributes 文档——搜索 working-tree-encoding.

如果你希望 .txt 文件是 Windows 机器上没有 BOM 的 UTF-16,那么将它添加到你的 gitattributes 文件中:

*.txt text working-tree-encoding=UTF-16LE eol=CRLF

回应

毫无疑问,UTF-16 是一团糟。但是,考虑

  • 使用 UTF16

  • Microsoft

    一样

    注意行 UTF16…用于 Windows 操作系统上的本机 Unicode 编码

  • JavaScript 在 UCS-2 和 UTF-16

    之间使用 a mess