為了獲得更好的閱讀體驗(yàn),建議訪問原地址:傳送門
<https://www.wmyskxz.com/2019/08/16/git-yuan-li-ru-men-jian-xi/>
前言: 之前聽過公司大佬分享過 Git 原理之后就想來自己總結(jié)一下,最近一忙起來就拖得久了,本來想塞更多的干貨,但是不喜歡拖太久,所以先出一版足夠入門的;
一、Git 簡介
Git 是當(dāng)前流行的分布式版本控制管理工具,最初由 Linux Torvalds (Linux 之父) 創(chuàng)造,于 2005 年發(fā)布。
Git,這個詞其實(shí)源自英國俚語,意思大約是 “混賬”。Linux 為什么會以這樣自嘲的名字來命名呢?這其中還有一段兒有趣的歷史可以說一說:
* 以下摘自:https://www.liaoxuefeng.com/wiki/896043488029600/896202815778784
<https://www.liaoxuefeng.com/wiki/896043488029600/896202815778784>
Git 的誕生:
很多人都知道,Linus 在 1991 年創(chuàng)建了開源的 Linux,從此,Linux 系統(tǒng)不斷發(fā)展,已經(jīng)成為最大的服務(wù)器系統(tǒng)軟件了。
Linus 雖然創(chuàng)建了 Linux,但 Linux 的壯大是靠全世界熱心的志愿者參與的,這么多人在世界各地為 Linux 編寫代碼,那 Linux
的代碼是如何管理的呢?
事實(shí)是,在 2002 年以前,世界各地的志愿者把源代碼文件通過 diff 的方式發(fā)給 Linus,然后由 Linus 本人通過手工方式合并代碼!
你也許會想,為什么 Linus 不把 Linux 代碼放到版本控制系統(tǒng)里呢?不是有 CVS、SVN 這些免費(fèi)的版本控制系統(tǒng)嗎?因?yàn)?Linus 堅(jiān)定地反對
CVS 和 SVN,這些集中式的版本控制系統(tǒng)不但速度慢,而且必須聯(lián)網(wǎng)才能使用。有一些商用的版本控制系統(tǒng),雖然比 CVS、SVN 好用,但那是付費(fèi)的,和
Linux 的開源精神不符。
不過,到了 2002 年,Linux 系統(tǒng)已經(jīng)發(fā)展了十年了,代碼庫之大讓 Linus
很難繼續(xù)通過手工方式管理了,社區(qū)的弟兄們也對這種方式表達(dá)了強(qiáng)烈不滿,于是 Linus 選擇了一個商業(yè)的版本控制系統(tǒng) BitKeeper,BitKeeper
的東家 BitMover 公司出于人道主義精神,授權(quán) Linux 社區(qū)免費(fèi)使用這個版本控制系統(tǒng)。
安定團(tuán)結(jié)的大好局面在 2005 年就被打破了,原因是 Linux 社區(qū)牛人聚集,不免沾染了一些梁山好漢的江湖習(xí)氣。開發(fā) Samba 的 Andrew
試圖破解 BitKeeper 的協(xié)議(這么干的其實(shí)也不只他一個),被 BitMover 公司發(fā)現(xiàn)了(監(jiān)控工作做得不錯?。谑?BitMover
公司怒了,要收回 Linux 社區(qū)的免費(fèi)使用權(quán)。
Linus 可以向 BitMover 公司道個歉,保證以后嚴(yán)格管教弟兄們,嗯,這是不可能的。實(shí)際情況是:Linus 花了兩周時間自己用 C
寫了一個分布式版本控制系統(tǒng),這就是 Git!一個月之內(nèi),Linux 系統(tǒng)的源碼已經(jīng)由 Git 管理了!牛是怎么定義的呢?大家可以體會一下。
Git 迅速成為最流行的分布式版本控制系統(tǒng),尤其是 2008 年,GitHub 網(wǎng)站上線了,它為開源項(xiàng)目免費(fèi)提供 Git 存儲,無數(shù)開源項(xiàng)目開始遷移至
GitHub,包括 jQuery,PHP,Ruby 等等。
歷史就是這么偶然,如果不是當(dāng)年 BitMover 公司威脅 Linux 社區(qū),可能現(xiàn)在我們就沒有免費(fèi)而超級好用的 Git 了。
版本控制系統(tǒng)
不管是集中式的 CVS、SVN 還是分布式的 Git
工具,實(shí)際上都是一種版本控制系統(tǒng),我們可以通過他們很方便的管理我們的文件、代碼等,我們可以先來暢想一下如果自己來設(shè)計(jì)這么一個系統(tǒng),你會怎么設(shè)計(jì)?
摁,這不禁讓我想起了之前寫畢業(yè)論文的日子,我先在一個開闊的空間創(chuàng)建了一個文件夾用于保存我的各種版本,然后開始了我的 “畢業(yè)論文版本管理”,參考下圖:
這好像暴露了我寫畢業(yè)論文愉快的經(jīng)歷..但不管怎么樣,我在用一個粗粒度版本的制度,在對我的畢業(yè)論文進(jìn)行著管理,摁,我通過不停在原基礎(chǔ)上迭代出新的版本的方式,不僅保存了我各個版本的畢業(yè)論文,還有這清晰的一個路徑,完美?NO!
問題是:
* 每一次的迭代都更改了什么東西,我現(xiàn)在完全看不出來了!
* 當(dāng)我在迭代我的超級無敵怎么樣都不改的版本的時候,突然回想起好像之前版本 1.0 的第一節(jié)內(nèi)容和 2.0
版本第三節(jié)的內(nèi)容加起來才是最棒的,我需要打開多個文檔并創(chuàng)建一個新的文檔,仔細(xì)對比文檔中的不同并為我的新文檔添加新的東西,好麻煩啊...
* 到最后文件多起來的時候,我甚至都不知道是我的 “超級無敵版” 是最終版,還是 “打死都不改版” 是最終版了;
* 更為要命的是,我保存在我的桌面上,沒有備份,意味著我本地文件手滑刪除了,那我就...我就...就...
并且可能問題還遠(yuǎn)不止于此,所以每每想起,就不自覺對 Linux 膜拜了起來。
集中式與分布式的不同
Git 采用與 CSV/SVN 完全不同的處理方式,前者采用分布式,而后面兩個都是集中式的版本管理。
先說集中式版本控制系統(tǒng),版本庫是集中存放在中央服務(wù)器的,而干活的時候,用的都是自己的電腦,所以要先從中央服務(wù)器取得最新的版本,然后開始干活,干完活了,再把自己的活推送給中央服務(wù)器。中央服務(wù)器就好比是一個圖書館,你要改一本書,必須先從圖書館借出來,然后回到家自己改,改完了,再放回圖書館。
集中式版本控制系統(tǒng)最大的毛病就是必須聯(lián)網(wǎng)才能工作,如果在局域網(wǎng)內(nèi)還好,帶寬夠大,速度夠快,可如果在互聯(lián)網(wǎng)上,遇到網(wǎng)速慢的話,可能提交一個10M的文件就需要5分鐘,這還不得把人給憋死啊。
那分布式版本控制系統(tǒng)與集中式版本控制系統(tǒng)有何不同呢?首先,分布式版本控制系統(tǒng)根本沒有 “中央服務(wù)器”,每個人的電腦上都是一個完整的版本庫,
這樣,你工作的時候,就不需要聯(lián)網(wǎng)了,因?yàn)榘姹編炀驮谀阕约旱碾娔X上。既然每個人電腦上都有一個完整的版本庫,那多個人如何協(xié)作呢?比方說你在自己電腦上改了文件
A,你的同事也在他的電腦上改了文件 A,這時,你們倆之間只需把各自的修改推送給對方,就可以互相看到對方的修改了。
和集中式版本控制系統(tǒng)相比,分布式版本控制系統(tǒng)的安全性要高很多,因?yàn)槊總€人電腦里都有完整的版本庫,某一個人的電腦壞掉了不要緊,隨便從其他人那里復(fù)制一個就可以了。而集中式版本控制系統(tǒng)的中央服務(wù)器要是出了問題,所有人都沒法干活了。
在實(shí)際使用分布式版本控制系統(tǒng)的時候,其實(shí)很少在兩人之間的電腦上推送版本庫的修改,因?yàn)榭赡苣銈儌z不在一個局域網(wǎng)內(nèi),兩臺電腦互相訪問不了,也可能今天你的同事病了,他的電腦壓根沒有開機(jī)。因此,分布式版本控制系統(tǒng)通常也有一臺充當(dāng)
“中央服務(wù)器” 的電腦,但這個服務(wù)器的作用僅僅是用來方便 “交換” 大家的修改,沒有它大家也一樣干活,只是交換修改不方便而已。
當(dāng)然,Git 的強(qiáng)大還遠(yuǎn)不止此。
二、Git 原理入門
Git 初始化
首先,讓我們來創(chuàng)建一個空的項(xiàng)目目錄,并進(jìn)入該目錄。
$ mkdir git-demo-project $ cd git-demo-project
如果我們打算對該項(xiàng)目進(jìn)行版本管理,第一件事就是使用 git init 命令,進(jìn)行初始化。
$ git init
git init 命令只會做一件事,就是在項(xiàng)目的根目錄下創(chuàng)建一個 .git 的子目錄,用來保存當(dāng)前項(xiàng)目的一些版本信息,我們可以繼續(xù)使用 tree -a
命令查看該目錄的完整結(jié)構(gòu),如下:
$ tree -a . └── .git ├── HEAD ├── branches ├── config ├── description ├──
hooks │?? ├── applypatch-msg.sample │?? ├── commit-msg.sample │?? ├──
fsmonitor-watchman.sample │?? ├── post-update.sample │?? ├──
pre-applypatch.sample │?? ├── pre-commit.sample │?? ├── pre-push.sample │?? ├──
pre-rebase.sample │?? ├── pre-receive.sample │?? ├── prepare-commit-msg.sample
│?? └── update.sample ├── index ├── info │?? └── exclude ├── objects │?? ├──
.DS_Store │?? ├── info │?? └── pack └── refs ├── heads └── tags
Git 目錄簡單解析
config 目錄
config 是倉庫的配置文件,一個典型的配置文件如下,我們創(chuàng)建的遠(yuǎn)端,分支都在等信息都在配置文件里有表現(xiàn);fetch 操作的行為也是在這里配置的:
[core] repositoryformatversion = 0 filemode = false bare = false
logallrefupdates = true symlinks = false ignorecase = true [remote "origin"]
url = [email protected]:yanhaijing/zepto.fullpage.git fetch =
+refs/heads/*:refs/remotes/origin/* [branch "master"] remote = origin merge =
refs/heads/master [branch "dev"] remote = origin merge = refs/heads/dev
objects 目錄
Git 可以通過一種算法可以得到任意文件的 “指紋”(40 位 16 進(jìn)制數(shù)字),然后通過文件指紋存取數(shù)據(jù),存取的數(shù)據(jù)都位于 objects 目錄。
例如我們可以手動創(chuàng)建一個測試文本文件并使用 git add . 命令來觀察 .git 文件夾出現(xiàn)的變化:
$ touch test.txt $ git add .
git add . 命令就是用于把當(dāng)前新增的變化添加進(jìn) Git 本地倉庫的,在我們使用后,我們驚奇的發(fā)現(xiàn) .git 目錄下的 objects/
目錄下多了一個目錄:
$ tree -a . ├── .git │?? ├── HEAD │?? ├── branches │?? ├── config │?? ├──
description │?? ├── hooks │?? │?? ├── 節(jié)省篇幅..省略.. │?? ├── index │?? ├── info │??
│?? └── exclude │?? ├── objects │?? │?? ├── .DS_Store │?? │?? ├── e6 │?? │??
│?? └── 9de29bb2d1d6434b8b29ae775ad8c2e48c5391 │?? │?? ├── info │?? │?? └──
pack │?? └── refs │?? ├── heads │?? └── tags └── test.txt
我們可以使用 git hash-object test.txt 命令來看看剛才我們創(chuàng)建的 test.txt 的 “文件指紋”:
$ git hash-object test.txt e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
這時候我們可以發(fā)現(xiàn),新創(chuàng)建的目錄 e6 其實(shí)是該文件哈希值的前兩位,這其實(shí)是 Git 做的一層類似于索引一樣的東西,并且默認(rèn)采用 16
進(jìn)制的兩位數(shù)來當(dāng)索引,是非常合適的。
objects 目錄下有 3 種類型的數(shù)據(jù):
* Blob;
* Tree;
* Commit;
文件都被存儲為 blob 類型的文件,文件夾被存儲為 tree 類型的文件,創(chuàng)建的提交節(jié)點(diǎn)被存儲為 Commit 類型的數(shù)據(jù);
一般我們系統(tǒng)中的目錄(tree),在 Git 會像下面這樣存儲:
而 Commit 類型的數(shù)據(jù)則整合了 tree 和 blob 類型,保存了當(dāng)前的所有變化,例如我們可以再在剛才的目錄下新建一個目錄,并添加一些文件試試:
$ mkdir test $ touch test/test.file $ tree -a . ├── .git │?? ├── HEAD │?? ├──
branches │?? ├── config │?? ├── description │?? ├── hooks │?? │?? ├──
節(jié)省篇幅..省略.. │?? ├── index │?? ├── info │?? │?? └── exclude │?? ├── objects │??
│?? ├── .DS_Store │?? │?? ├── e6 │?? │?? │?? └──
9de29bb2d1d6434b8b29ae775ad8c2e48c5391 │?? │?? ├── info │?? │?? └── pack │??
└── refs │?? ├── heads │?? └── tags ├── test │?? └── test.file └── test.txt
提交一個 Commit 再觀察變化:
$ git commit -a -m "test: 新增測試文件夾和測試文件觀察.git文件的變化" [master (root-commit)
30d51b1] test: 新增測試文件夾和測試文件觀察.git文件的變化 1 file changed, 0 insertions(+), 0
deletions(-) create mode 100644 test.txt $ tree -a . ├── .git │?? ├──
COMMIT_EDITMSG │?? ├── HEAD │?? ├── branches │?? ├── config │?? ├── description
│?? ├── hooks │?? │?? ├── 節(jié)省篇幅..省略.. │?? ├── index │?? ├── info │?? │?? └──
exclude │?? ├── logs │?? │?? ├── HEAD │?? │?? └── refs │?? │?? └── heads │??
│?? └── master │?? ├── objects │?? │?? ├── .DS_Store │?? │?? ├── 30 │?? │?? │??
└── d51b1edd2efd551dd6bd52d4520487b5708c0e │?? │?? ├── 5e │?? │?? │?? └──
fb9bc29c482e023e40e0a2b3b7e49cec842034 │?? │?? ├── e6 │?? │?? │?? └──
9de29bb2d1d6434b8b29ae775ad8c2e48c5391 │?? │?? ├── info │?? │?? └── pack │??
└── refs │?? ├── heads │?? │?? └── master │?? └── tags ├── test │?? └──
test.file └── test.txt
首先我們可以觀察到我們提交了一個 Commit 的時候在第一句話里面返回了一個短的像是哈希值一樣的東西: [master (root-commit)
30d51b1] 中 的 30d51b1,對應(yīng)的我們也可以在 objects 找到剛才 commit 的對象,我們可以使用 git cat-file -p
命令輸出一下當(dāng)前文件的內(nèi)容:
$ git cat-file -p 30d5 tree 5efb9bc29c482e023e40e0a2b3b7e49cec842034 author
我沒有三顆心臟 <[email protected]> 1565742122 +0800 committer 我沒有三顆心臟
<[email protected]> 1565742122 +0800 test:
新增測試文件夾和測試文件觀察.git文件的變化
我們發(fā)現(xiàn)這里面有提交的內(nèi)容信息、作者信息、提交者信息以及 commit message,當(dāng)然我們可以進(jìn)一步看到提交的內(nèi)容具體有哪些:
$ git cat-file -p 5efb 100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
test.txt
我們再試著提交一個 commit 來觀察變化:
$ touch test/test2.file $ git commit -a -m "test: 新增加一個 commit 以觀察變化." [master
9dfabac] test: 新增加一個 commit 以觀察變化. 2 files changed, 0 insertions(+), 0
deletions(-) create mode 100644 test/test.file create mode 100644
test/test2.file $ git cat-file -p 9dfabac tree
c562bfb9441352f4c218b0028148289f1ea7d7cd parent
30d51b1edd2efd551dd6bd52d4520487b5708c0e author 龍?zhí)?
<[email protected]> 1565878699 +0800 committer 龍?zhí)?
<[email protected]> 1565878699 +0800 test: 新增加一個 commit 以觀察變化.
可以觀察到這一次的 commit 多了一個 parent 的行,其中的 “指紋” 和上一次的 commit 一模一樣,當(dāng)我們提交兩個 commit
之后我們的 Git 倉庫可以簡化為下圖:
* 說明:其中因?yàn)槲覀?test 文件夾新增了文件,也就是出現(xiàn)了變化,所以就被標(biāo)識成了新的 tree 類型的對象;
refs 目錄
refs 目錄存儲都是引用文件,如本地分支,遠(yuǎn)端分支,標(biāo)簽等
* refs/heads/xxx 本地分支
* refs/remotes/origin/xxx 遠(yuǎn)端分支
* refs/tags/xxx 本地tag
引用文件的內(nèi)容都是 40 位長度的 commit
$ cat .git/refs/heads/master 9dfabac68470a588a4b4a78742249df46438874a
這就像是一個指針一樣,它指向了你的最后一次提交(例如這里就指向了第二次提交的 commit),我們補(bǔ)充上分支信息,現(xiàn)在的 Git 倉庫就會像下圖所示:
HEAD 目錄
HEAD 目錄下存儲的是當(dāng)前所在的位置,其內(nèi)容是分支的名稱:
$ cat HEAD ref: refs/heads/master
我們再補(bǔ)充上 HEAD 的信息,現(xiàn)在的 Git 倉庫如下圖所示:
Git 中的沖突
您也在上面了解到了,在 Git 中分支是一種十分輕便的存在,僅僅是一個指針罷了,我們在廣泛的使用分支中,不可避免的會遇到新創(chuàng)建分支的合并,這時候不論是選擇
merge 還是 rebase,都有可能發(fā)生沖突,我們先來看一下沖突是如何產(chǎn)生的:
圖上的情況,并不是移動分支指針就能夠解決問題的,它需要一種合并策略。首先我們需要明確的是誰與誰的合并,是 2,3 與 4, 5, 6
兩條線的合并嗎?其實(shí)并不是的,真實(shí)合并的其實(shí)只有 3 和 6,因?yàn)槊恳淮蔚奶峤欢及隧?xiàng)目完整的快照,即合并只是 tree 與 tree 的合并。
這可能說起來有點(diǎn)繞,我們可以先來想一個簡單的算法,用來比較 3 和 6 的不同。如果我們只是單純的比較 3 和 6
的信息,其實(shí)并沒有意義,因?yàn)樗鼈冎g并不能確切的表達(dá)出當(dāng)前的沖突狀態(tài)。因此我們需要選取它們兩個分支的分歧點(diǎn)(merge base)作為參考點(diǎn),進(jìn)行比較。
首先我們把 1 作為基礎(chǔ),然后把 1、3、6 中所有的文件做一個列表,然后依次遍歷這個列表中的文件。我們現(xiàn)在拿列表中的一個文件進(jìn)行舉例,把在提交在
1、3、6 中的該文件分別稱為版本1、版本3、版本6,可能出現(xiàn)如下幾種情況:
1. 版本 1、版本 3、版本 6 的 “指紋” 值都相同:這種情況則說明沒有沖突;
2. 版本 3 or 版本 6 至少有一個與版本 1 狀態(tài)相同(指的是指紋值相同或都不存在):這種情況可以自動合并,比如版本 1 中存在一個文件,在版本 3
中沒有對該文件進(jìn)行修改,而版本 6 中刪除了這個文件,則以版本 6 為準(zhǔn)就可以了;
3. 版本 3 or 版本 6 都與版本 1 的狀態(tài)不同:這種情況復(fù)雜一些,自動合并策略很難生效了,所以需要手動解決;
merge 操作
在解決完沖突后,我們可以將修改的內(nèi)容提交為一個新的提交,這就是 merge。
可以看到 merge
是一種不修改分支歷史提交記錄的方式,這也是我們常用的方式。但是這種方式在某些情況下使用起來不太方便,比如我們創(chuàng)建了一些提交發(fā)送給管理者,管理者在合并操作中產(chǎn)生了沖突,還需要去解決沖突,這無疑增加了他人的負(fù)擔(dān)。
而我們使用 rebase 可以解決這種問題。
rebase 操作
假設(shè)我們的分支結(jié)構(gòu)如下:
rebase 會把從 Merge Base 以來的所有提交,以補(bǔ)丁的形式一個一個重新打到目標(biāo)分支上。這使得目標(biāo)分支合并該分支的時候會直接 Fast
Forward(可以簡單理解為直接后移指針),即不會產(chǎn)生任何沖突。提交歷史是一條線,這對強(qiáng)迫癥患者可謂是一大福音。
其實(shí) rebase 主要是在 .git/rebase-merge 下生成了兩個文件,分別為 git-rebase-todo 和 done
文件,這兩個文件的作用光看名字就大概能夠看得出來。git-rebase-todo 中存放了 rebase 將要操作的 commit,而 done
存放正操作或已操作完畢的 commit,比如我們這里,git-rebase-todo 存放了 4、5、6 三個提交。
首先 Git 會把 4 這個 commit 放入 done,表示正在操作 4,然后將 4 以補(bǔ)丁的方式打到 3 上,形成了新的
4`,這一步是可能產(chǎn)生沖突的,如果有沖突,需要解決沖突之后才能繼續(xù)操作。
接著按同樣的方式把 5、6 都放入 done,最后把指針移動到最新的提交 6` 上,就完成了 rebase 的操作。
從剛才的圖中,我們就可以看到 rebase 的一個缺點(diǎn),那就是修改了分支的歷史提交。
如果已經(jīng)將分支推送到了遠(yuǎn)程倉庫,會導(dǎo)致無法將修改后的分支推送上去,必須使用 -f 參數(shù)(force)強(qiáng)行推送。
所以使用 rebase 最好不要在公共分支上進(jìn)行操作。
Squash and Merge 操作
簡單說就是壓縮提交,把多次的提交融合到一個 commit
中,這樣的好處不言而喻,我們著重來討論一下實(shí)現(xiàn)的技術(shù)細(xì)節(jié),還是以我們上面最開始的分支情況為例,首先,Git 會創(chuàng)建一個臨時分支,指向當(dāng)前 feature
的最新 commit。
然后按照上面 rebase 的方式,變基到 master 的最新 commit 處。
接著用 rebase 來 squash 之,壓縮這些提交為一個提交。
最后以 fast forward 的方式合并到 master 中。
可見此時 master 分支多且只多了一個描述了這次改動的提交,這對于大型工程,保持主分支的簡潔易懂有很大的幫助。
說明:想要了解更多的諸如 checkout、cherry-pick 等操作的話可以看看參考文章的第三篇,這里就不做細(xì)致描述了。
三、總結(jié)
通過上面的了解,其實(shí)我們已經(jīng)大致的掌握了 Git 中的基本原理,我們的 Commit
就像是一個鏈表節(jié)點(diǎn)一樣,不僅有自身的節(jié)點(diǎn)信息,還保存著上一個節(jié)點(diǎn)的指針,然后我們以 Branch 這樣輕量的指針保存著一條又一條的 commit
鏈條,不過值得注意的是,objects 目錄下的文件是不會自動刪除的,除非你手動 GC,不然本地的 objects
目錄下就保留著你當(dāng)前項(xiàng)目完整的變化信息,所以我們通常都會看到 Git 上面的項(xiàng)目通常是沒有 .git 目錄的,不然僅僅通過 .git
目錄理論上就可以還原出你的完整項(xiàng)目!
參考文章
* https://www.liaoxuefeng.com/wiki/896043488029600/896202780297248
<https://www.liaoxuefeng.com/wiki/896043488029600/896202780297248> -
集中式vs分布式(廖雪峰的官方網(wǎng)站)
* https://yanhaijing.com/git/2017/02/08/deep-git-3/
<https://yanhaijing.com/git/2017/02/08/deep-git-3/> - 起底Git-Git內(nèi)部原理
* https://coding.net/help/doc/practice/git-principle.html
<https://coding.net/help/doc/practice/git-principle.html> - 使用原理視角看 Git
按照慣例黏一個尾巴:
歡迎轉(zhuǎn)載,轉(zhuǎn)載請注明出處!
獨(dú)立域名博客:wmyskxz.com
簡書ID:@我沒有三顆心臟 <https://www.jianshu.com/u/a40d61a49221>
github:wmyskxz <https://github.com/wmyskxz/>
歡迎關(guān)注公眾微信號:wmyskxz
分享自己的學(xué)習(xí) & 學(xué)習(xí)資料 & 生活
想要交流的朋友也可以加qq群:3382693
熱門工具 換一換