git底层命令介绍

git原理

概述

首先,我们执行git init命令初始化一个新的仓库(repository),该命令会产生一个.git文件夹,通常目录结构如下

2021/11/20  23:57               112 config
2021/11/20  23:57                73 description
2021/11/20  23:57                23 HEAD
2021/11/20  23:57    <DIR>          hooks
2021/11/20  23:57    <DIR>          info
2021/11/20  23:57    <DIR>          objects
2021/11/20  23:57    <DIR>          refs

description 仅用于GitWeb项目,所以不用关心大概就是有生之年用不到
config 包含了项目特有的git配置
info 目录包含了一个全局排除文件,用于忽略项目中符合指定模式文件的变动。通俗一点说,就是项目中.gitignore文件配置
hooks 目录中则是客户端/服务器端的hook script过于高大上,所以我也不知道
objects 目录可以视为项目所有文件的数据库,其存储了项目的全部内容。一开始这句话其实很难理解,但是当你了解git add命令的原理时,就能明白objects目录的作用。
refs 目录简单一点说,就是存指针的地方,但是这里的指针不指向内存,而是git中特有的commit object。在了解git commit执行过程之后,自然能明白what is commit object?
HEAD 文件仅持有当前所check out的指针,同样指向commit object

index 文件则是git用于存储暂存区信息的位置
fetch

git object

git核心可以简化为一个键值映射的数据存储。在概述中,git记录的所有数据都存储于object子目录下

2021/11/20  23:57    <DIR>          info
2021/11/20  23:57    <DIR>          pack

各目录作用?

blob object

接下来通过git hash-object命令演示如何添加数据。

$ echo "test content" | git hash-object -w --stdin
d670460b4b4aece5915caf5c68d12f560a9fe3e4

我们通过git hash-object命令将一段文本test content写入objects目录下,并返回40位的特征hash,也就是用于查找的键值显示在控制台上。重新进入objects目录

$ find objects -type f
objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4

git按数据的hash进行存储,其中从最高位起前2位作为子目录名,其余38位才是存储文件真正的名字。
我们还可以用git cat-file来check out文件中存储的数据

$ git cat-file -p d670460b4b4aece5915caf5c68d12f560a9fe3e4
test content

有兴趣的话可以尝试把新增文件用git hash-object命令添加到git的管理中,再把该文件删除,以验证git对文件的管理是否依赖于该文件是否存在于本地之中。

但是实际应用中,为了检出文件而记住一个40位的hash是不现实的,所以git另外提供了tree object,用于对一组文件进行存储。

tree object

tree object的创建根据暂存区or索引的状态完成。所以想要创建tree object,不如先更新以下索引。使用git update-index命令将工作区文件添加至索引。在这里为test.txt文件创建新的索引。

$ echo "miku daisuki version 1" > miku.txt

$ git update-index --add miku.txt

接下来就可以使用git write-tree命令从当前索引中导出tree object

$ git write-tree
bf816897805a2f650f7e712493fe1ec12eecb416

$ git cat-file -p bf816897805a2f650f7e712493fe1ec12eecb416
100644 blob 507f71a67639a4f021d626d71dbde39e2aa5147e     miku.txt

$ git cat-file -t bf816897805a2f650f7e712493fe1ec12eecb416
tree

额外补充一点,不仅blob object可以更新到索引中,tree object也可以更新到索引中

$ git read-tree --prefix=bak bf816897805a2f650f7e712493fe1ec12eecb416

git read-tree指令将tree object中内容读取到暂存区作为子树(俗称 子目录?)

$ git write-tree
9a9e6151d79022b67bf64dba2683c59f1e3a5a4f
$ git cat-file -p 9a9e6151d79022b67bf
040000 tree bf816897805a2f650f7e712493fe1ec12eecb416     bak
100644 blob 12947fe55ea37feab29ac53598c8087db82fd055     miku.txt
100644 blob fa49b077972391ad58037050f2a75f74e3671e92     new.txt

commite object

在了解了tree object之后,我们惊奇的发现,文件检出仍然要记住40位hash码,tree object根本没有解决这个问题确实,这就是个暂存区快照
在仔细阅读官方文件后,其实commit object也没有解决40位超长hash的问题,只是对tree object进行补充

现在利用git commit-tree命令对9a9e6151d79022b进行一次commit

$ echo "first commit" | git commit-tree 9a9e6151d79022b67bf
6dc1bd2d61a88a6cf71c2094b2051721db07c7b0

$ git cat-file -p 6dc1bd2d61a88a6cf71c20
tree 9a9e6151d79022b67bf64dba2683c59f1e3a5a4f
author Fgl <white156> 1637586210 +0800
committer Fgl <white156> 1637586210 +0800

first commit

可以看到,commit object包含了检出暂存区的版本tree,以及关于这次write-tree的相关信息。
利用git log命令也可以查看commit object信息

$ git log 6dc1bd2
commit 6dc1bd2d61a88a6cf71c2094b2051721db07c7b0
Author: Fgl <white156>
Date:   Mon Nov 22 21:03:30 2021 +0800

    first commit

Git References

这次是真的,不用为了check out而特意去记40位hash。

.git/refs文件夹下,是git用于存储hash值的地方

$ find .git/refs
.git/refs
.git/refs/heads
.git/refs/tags

使用git update-ref创建一个新的引用。

$ git update-ref refs/heads/scond 0257ba130e43f89038858cb0a95580d6485441b6

$ git update-ref refs/heads/first 6dc1bd2d61a88a6cf71c2094b2051721db07c7b0

本质上就是新建一文件,里面啥也不存,就存个hash。(但是git提供的总比自己操作安全的多,具体二者区别我也不知道)。

执行git update-ref命令更新HEAD时,会把变更信息自动记录到reflog中,如果手动操作那么这条变更信息在reglog中就是缺失的。

$ echo 0257ba130e43f89038858cb0a95580d6485441b6 >.git/refs/heads/second

$ echo 6dc1bd2d61a88a6cf71c2094b2051721db07c7b0 >.git/refs/heads/first

现在想查看the first commit就可以通过first引用,而不是超长hash码

$ git cat-file -p first
tree 9a9e6151d79022b67bf64dba2683c59f1e3a5a4f
author Fgl <white156> 1637586210 +0800
committer Fgl <white156> 1637586210 +0800

first commit

在git中还自带了一个特殊ref:HEAD
默认情况下,.git/HEAD文件内容

$ cat .git/HEAD
ref: refs/heads/master

当我们切换到其他commit object

$ git checkout first
Switched to branch 'first'

$ cat .git/HEAD
ref: refs/heads/first

可以看到,HEAD文件始终持有当前使用的暂存区版本。
至于对HEAD持有引用的修改同样建议使用git所提供的git symbolic-ref

$ git symbolic-ref HEAD refs/heads/second

$ cat .git/HEAD
ref: refs/heads/second

Tag

tag object与commit相似但不同,tag指向任意 object,而不单单指向tree object。

$ git tag -a v1.1 first -m "create annotated tag"

$ cat .git/refs/tags/v1.1
9ffba760315b94fd9f999478ac502843c25798a4

$ git cat-file -p 9ffba760315b94
object 6dc1bd2d61a88a6cf71c2094b2051721db07c7b0
type commit
tag v1.1

create annotated tag

我们给branch first当前指向的commit建立一个tag。可以看到tag也是一个object,内部可以指向任意object,但是tag指向的对象,一经建立便不能修改。

除此之外,git还提供了一种轻量级的tag构建命令git update-ref。使用该命令同样会在refs/tags目录下新建一个tag,但是并不生成tag object,仅在文件内记录被指向对象的SHA-1值所以好像是可以修改的哦~

$ git update-ref refs/tags/v1.0 0257ba130e43f89038858cb0a95580d6485441b6

$ cat .git/refs/tags/v1.0
0257ba130e43f89038858cb0a95580d6485441b6

Remote

git记录各分支最后一次push的commit SHA1值,到refs/remotes文件夹下。但是remote引用通常被认为是只读的,git仅仅将remote引用作为书签,记录分支与服务器的最后一次交互。git允许checkout remote引用指向内容,但是不允许commit命令修改remote引用的内容

pakcfiles

首先我们先了解以下,当文件被修改后,git如何进行存储。
假设现在有个大文件(22k)

$ git cat-file -s 8200df1ca38e36720
21257

如果我们对它修改做细微修改,比如添加一个字母或删除一行,那么git是否要存储一个新的object呢,尽管其中的内容高度重复?我们可以做个实验:

$ echo "test" > "duilib license.txt"

$ git commit -am "modify file content"

$ git cat-file -p second^{tree}
100644 blob 9daeafb9864cf43055ae93beb0afd6c7d144bfa4     duilib license.txt
100644 blob 12947fe55ea37feab29ac53598c8087db82fd055     miku.txt
100644 blob fa49b077972391ad58037050f2a75f74e3671e92     new.txt

$ git cat-file -s 9daeafb9864cf4305
5

可以看到,git的做法可以说非常节约内存了,并没有全量存储,而是仅记录那里发生了变化,也就是增量存储。

当我们向push remote 或者 执行git gc命令的时候,git会将objects打包压缩

$ git gc
Enumerating objects: 15, done.
Counting objects: 100% (15/15), done.
Delta compression using up to 4 threads
Compressing objects: 100% (10/10), done.
Writing objects: 100% (15/15), done.
Total 15 (delta 1), reused 0 (delta 0)
Computing commit graph generation numbers:  25% (Computing commit graph generation numbers:  50% (Computing commit graph generation numbers:  75% (Computing commit graph generation numbers: 100% (Computing commit graph generation numbers: 100% (4/4), done.

这时大部分object都会被塞到pack文件里

$ find .git/objects -type f
.git/objects/info/commit-graph
.git/objects/info/packs
.git/objects/pack/pack-34e932fc24055d7a22dfa2afa13fecdc52736523.idx
.git/objects/pack/pack-34e932fc24055d7a22dfa2afa13fecdc52736523.pack

但是也并不是所有的object,如果没有任何commit中包含这些object,那么它们就不会被打包,而是继续保留在.git/objects。

gc命令会生成一个pack文件和一个idx文件。pack是数据压缩后的存储文件,git所提供的压缩比率也是非常可观的,对于打包后的pack竟然只有2k左右的大小。

$ du -b .git/objects/pack/pack-34e932fc24055d7a22dfa2afa13fecdc52736523.pack
2043    .git/objects/pack/pack-34e932fc24055d7a22dfa2afa13fecdc52736523.pack

ind可以理解问打包文件的明细,其记录了pack中包含了哪些object,以及每个object在pack中偏移量,通过git verify-pack命令可以查看ind的详细内容

$ git verify-pack -v .git/objects/pack/pack-34e932fc24055d7a22dfa2afa13fecdc52736523.idx
ee029300bbd4374599a95f088941c1826b686a09 commit 196 145 12
2014d26dd349eb8ea0c91cf1e207f7f62f20a62f commit 141 109 157
0257ba130e43f89038858cb0a95580d6485441b6 commit 142 109 266
6dc1bd2d61a88a6cf71c2094b2051721db07c7b0 commit 141 109 375
9ffba760315b94fd9f999478ac502843c25798a4 tag    130 121 484
8215d2e662e714ab73a52830a08ffd2f31a92512 tree   117 116 605
17a0e4aa1878eb9b55d4b165ab0caea3486b3c97 tree   147 143 721
bf816897805a2f650f7e712493fe1ec12eecb416 tree   36 47 864
863810752d351d53f911f1f3bc727e6bcf5acba0 tree   71 76 911
9a9e6151d79022b67bf64dba2683c59f1e3a5a4f tree   8 19 987 1 17a0e4aa1878eb9b55d4b165ab0caea3486b3c97
9daeafb9864cf43055ae93beb0afd6c7d144bfa4 blob   5 14 1006
12947fe55ea37feab29ac53598c8087db82fd055 blob   23 33 1020
fa49b077972391ad58037050f2a75f74e3671e92 blob   9 18 1053
507f71a67639a4f021d626d71dbde39e2aa5147e blob   23 33 1071
8200df1ca38e36720703d4bf41a0024a66422835 blob   21257 919 1104
non delta: 14 objects
chain length = 1: 1 object
.git/objects/pack/pack-34e932fc24055d7a22dfa2afa13fecdc52736523.pack: ok

refspec

从这里开始,git要准备与remote进行交互了。首先是配置远端服务器地址,这一动作可以通过git remote add <name> <url>命令添加,执行后在config文件中就会记录新增的remote信息

$ git remote add origin https://github.com/user/project.git

[remote "origin"]
	url = https://github.com/user/project.git
	fetch = +refs/heads/*:refs/remotes/origin/*

按照我们在命令中输入的参数,git新建了名为origin的remote配置信息。其中fetch的内容称为refspec(引用空间?),格式<src>:<dst>,src表示remote路径,dst表示本地路径,首位+表示git在no fast-forward情况下也会更新引用。

关于fast-forward在这里简单的介绍以下:
In Git, to “fast forward” means to update the HEAD pointer in such a way that its new value is a direct descendant of the prior value. In other words, the prior value is a parent, or grandparent, or grandgrandparent
在更新时,会先检测本地HEAD是否remote HEAD指向commit节点的祖先。所以一般在remote新增文件或本地修改产生冲突时,会导致no fast-forward

上述refspec的含义:将remote refs/heads文件夹所有文件更新到本地的refs/remotes/origin文件夹下。如果仅想更新某一特定文件,那么可以

fetch = +refs/heads/master:refs/remotes/origin/master

表示仅拉取master文件写入到本地master文件中。而且git还允许同时按多个refspec更新

[remote "origin"]
	url = https://github.com/user/project.git
	fetch = +refs/heads/master:refs/remotes/origin/master
	fetch = +refs/heads/experiment:refs/remotes/origin/experiment

如果要对remote新增分支的话,使用git push <repository> <refspec>:<path> 。如:将本地master push到origin的refs/heads下,并命名为experiment

$ git push origin master:refs/heads/experiment

删除remote分支与之类似

$ git push origin :refs/heads/experiment

我们省略了要推送的本地记录,也就是说我们把experiment更新为空,从而实现删除的功能。再git v1.7以后,可以使用git push <repository> --delete <refspec>

Transfer Protocols

在这一节就是git的文件传输原理,主要有两种方式:

dumb protocol

dumb协议不依赖于git,读取远程仓库通过一些列http get请求,但也有一定的局限性:read-only。并且由于http自身的安全问题,大多数git服务器都已经拒绝响应该请求,建议使用smart protocol

但是dumb protocol还是要讲的,例如在clone时

$ git clone http://server/simplegit-progit.git

首先获取info\refs文件。这个特殊文件在本地是不存在的,因为它是在hook post-receive中调用git update-server-info命令生成的。

=> GET info/refs
ca82a6dff817ec66f44342007202690a93763949     refs/heads/master

服务器响应给我们remote 引用和SHA-1,这里只是远程端信息,并没有获取具体object。然后请求HEAD引用,这样git才知道结束后应该checkout什么。

=> GET HEAD
ref: refs/heads/master

接下来请求master所只向的commite object,根据git存储规则直接到objects目录下查找

=> GET objects/ca/82a6dff817ec66f44342007202690a93763949
(179 bytes of binary data)

解压请求数据,并不断请求parent commite,实现对完整项目的获取。但是并不是所有commite object都一定在objects目录下,也有可能在packfile中,当然也有可能在别的repository,和fork模式有点关系,但是我懒,这时向objects目录请求就会出错

=> GET objects/cf/da3bf379e4f8dba8717dee55aab78aef7f4daf
(404 - Not Found)

所以就要去请求packfile。git update-server-info命令同样也会生成packfile的目录文件:objects/info/packs

=> GET objects/info/packs
P pack-816a9b2334da9953e530f27bcac22082a9f5b835.pack

=> GET objects/pack/pack-816a9b2334da9953e530f27bcac22082a9f5b835.idx
(4k of binary data)

先请求idx文件,查询所需文件是否在该packfile里,如果有则进一步请求packfile文件。

smart protocol

想对于dumb protocol来说,smart protocol有了高效的改进。其不仅能从服务器读取数据到本地,还能将本地数据传输到服务器。传说smart protocol还能读取本地文件,分辨哪些是服务器上有的,哪些是没有的,进行针对性请求。smart protocol依赖于两个过程(or 进程?):

uploading data

例如在执行git push origin master,git会启动send-pack过程,以ssh连接到服务器,向simplegit-progit项目上传数据为例:

$ ssh -x git@server "git-receive-pack 'simplegit-progit.git'"

服务器端git-receive-pack命令响应:

00a5ca82a6dff817ec66f4437202690a93763949 refs/heads/master□report-status \
	delete-refs side-band-64k quiet ofs-delta \
	agent=git/2:2.1.1+github-607-gfba4028 delete-refs
0000

第一行给出当前持有引用及其SHA-1,后接服务器可提供的功能(report-status, delete-refs .etc)。由于采用chunk方式传输,通常包括一行且前4位表示块长度,如00a5代表该数据块长度为165字节。0000则表示传输结束。

根据响应内容,send-pack会判断哪些commit是本地已经有但是服务器上没有的,然后把这些文件打包,作为一个packfile发送给服务器。
对于引用(分支)的更新,send-pack将通知receive-pack如下信息:old SHA-1,new SHA-1,待更新引用。如:更新master,本地新增experiment分支

0076ca82a6dff817ec66f44342007202690a93763949 15027957951b64cf874c3557a0f3547bd83b3ff6 \
	refs/heads/master report-status
006c0000000000000000000000000000000000000000 cdfdb42577e2506715f8cfeacdbabc092bf63e8d \
	refs/heads/experiment
0000

同样,chunk传输前4位表示长度,第一行后接客户端(send-pack发送给服务器消息)允许的功能。如果SHA-1值全0,表示之前不存在;如果删除的话,那么old SHA-1和new SHA-1值会正好相反,更新为全0表示分支删除

download data

在下载数据时,本地会启动fetch-pack过程(进程)连接服务器上upload-pack。仍然以ssh连接下载simplegit-progit项目数据为例:

$ ssh -x git@server "git-upload-pack 'simplegit-progit.git'"

首先本地fetch-pack执行如上命令,服务器返回响应

00dfca82a6dff817ec66f44342007202690a93763949 HEAD□multi_ack thin-pack \
	side-band side-band-64k ofs-delta shallow no-progress include-tag \
	multi_ack_detailed symref=HEAD:refs/heads/master \
	agent=git/2:2.1.1+github-607-gfba4028
003fe2409a098dc3e53539a9028a94b6224db9d6a6b6 refs/heads/master
0000

响应仍然是SHA-1,HEAD,服务器端可以提供的功能。此外,响应还特别指出了HEAD当前指向引用(symref=HEAD:refs/heads/master),用于checkout。
根据响应,fetch-pack再次向服务器发送请求。如果需要请求数据,以 want SHA-1的格式发送。chunk传输,所以前四位始终表示长度。

003cwant ca82a6dff817ec66f44342007202690a93763949 ofs-delta

如果已有数据,则 have SHA-1 的格式发送

0032have 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7

最后写入 done 告知服务器端upload-pack发送数据,以及 0000 表示chunk传输流结束

0009done
0000

Maintenance

在这一节主要讲一下git gc命令。该命令执行如下动作,顺序不分先后主要我也不知道先后

  1. 将objects目录下文件(学名: loose object)聚集到packfile
  2. 合并packfile为一个packfile
  3. 将没有被commit记录的loose object移除
  4. 将references合并到一个单独文件中

但是git gc命令即使在config中配置了auto gc也很难触发,需要你有7k loose object和超过50个packfile才会真正触发git gc命令。

现在主要说一下最后一条,对references的处理。

由于在gc前没有查看references信息,所以我也不知道执行前什么样

在执行git gc,你会惊喜的发现refs目录下空了,其中所有的数据都被转移到了.git/packed-refs文件中

# pack-refs with: peeled fully-peeled sorted 
6dc1bd2d61a88a6cf71c2094b2051721db07c7b0 refs/heads/first
0257ba130e43f89038858cb0a95580d6485441b6 refs/heads/scond
ee029300bbd4374599a95f088941c1826b686a09 refs/heads/second
0257ba130e43f89038858cb0a95580d6485441b6 refs/tags/v1.0
9ffba760315b94fd9f999478ac502843c25798a4 refs/tags/v1.1
^6dc1bd2d61a88a6cf71c2094b2051721db07c7b0

我们注意到最后一行与众不同,它表示它的上一行是一个annotated tag,而这一行就是这个tag所指向的object。

如果我们对ref进行修改,是不会对packed-refs文件中内容做改动的,而是在refs/heads目录下新建文件记录改动。同样,为了能找到指定的ref,git会首先在refs目录下查询,如果没有在查询packed-refs。

Data Recovery

最后则是喜闻乐见的数据回复!相信在使用git的时候,总有一天是你操作失误的时候嘤嘤嘤

假设我们git reset --hard错了真的是想想就刺激,如何才能恢复?

为了测试,首先加入一个新的文件,然后commit “reset hard test”

$ git log --pretty=oneline
f688b9208a79a9945b9a9c3a02a7f44ed25720c4 (HEAD -> second) reset hard test
ee029300bbd4374599a95f088941c1826b686a09 modify file content
2014d26dd349eb8ea0c91cf1e207f7f62f20a62f add big file

然后回退到2014d26dd349eb8e

$ git reset --hard 2014d26dd349eb8e
HEAD is now at 2014d26 add big file

$ git log --pretty=oneline
2014d26dd349eb8ea0c91cf1e207f7f62f20a62f (HEAD -> second) add big file

可以看到我们刚刚新增的commit记录已经没有了。但是好消息是,git reset --hard并没有删除我们新增的数据,所以还是有可能恢复的。
git reflog会显示所有的HEAD变更的记录,当每次HEAD发生变动时,git都会自动记录变更信息到 .git/logs/HEAD

$ git reflog
2014d26 (HEAD -> second) HEAD@{0}: reset: moving to 2014d26dd349eb8e
f688b92 HEAD@{1}: commit: reset hard test
ee02930 HEAD@{4}: commit: modify file content
2014d26 (HEAD -> second) HEAD@{5}: commit (initial): add big file

详细信息可以执行git log -g命令

$ git log -g
commit 2014d26dd349eb8ea0c91cf1e207f7f62f20a62f (HEAD -> second)
Reflog: HEAD@{0} (Fgl <white156>)
Reflog message: reset: moving to 2014d26dd349eb8e
Date:   Thu Nov 25 23:15:18 2021 +0800

    add big file

commit f688b9208a79a9945b9a9c3a02a7f44ed25720c4
Reflog: HEAD@{1} (Fgl <white156>)
Reflog message: commit: reset hard test
Date:   Sun Dec 5 16:24:52 2021 +0800

    reset hard test

commit ee029300bbd4374599a95f088941c1826b686a09
Reflog: HEAD@{2} (Fgl <white156>)
Reflog message: checkout: moving from first to second
Date:   Thu Nov 25 23:17:49 2021 +0800

    modify file content

这样我们reset前的commit SHA-1就找回来了

$ git branch reset-recover f688b9208a79a9

$ git log --pretty=oneline reset-recover
f688b9208a79a9945b9a9c3a02a7f44ed25720c4 (reset-recover) reset hard test
ee029300bbd4374599a95f088941c1826b686a09 modify file content
2014d26dd349eb8ea0c91cf1e207f7f62f20a62f (HEAD -> second) add big file

perfect~

但是实际情况往往更加痛苦,reflog也不是万能的,如果reflog也了,那改怎么操作呢?
首先我们来模拟一下

$ git branch -D reset-recover
Deleted branch reset-recover (was f688b92).

$ rm -Rf .git/logs/

虽然记录没有了,但是数据文件还是没有删,仍然有操作的可能。根据我们的操作步骤可知,要恢复的commit时无论如何都访问不到的,也就是不可达的,所以我们用git fsck --full命令来查询git中所有没有被指向的object,俗称不可达object

$ git fsck --full
Checking object directories: 100% (256/256), done.
Checking objects: 100% (15/15), done.
dangling commit f688b9208a79a9945b9a9c3a02a7f44ed25720c4

完工~


原文地址:
[1]: Git - Plumbing and Porcelain

参考文献:

[1]: What is the difference between git merge and git merge --no-ff?

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值