章節 ▾ 第二版

5.3 分散式 Git - 維護專案

維護專案

除了知道如何有效地為一個專案做貢獻,你可能還需要知道如何維護一個專案。這可能包括接受和應用透過 format-patch 生成並透過電子郵件傳送給你的補丁,或者將遠端分支中的更改整合到你已新增到專案中的遠端倉庫。無論你是維護一個規範倉庫,還是想透過驗證或批准補丁來提供幫助,你都需要知道如何以對其他貢獻者最清晰、對你長期可持續的方式接受工作。

在主題分支中工作

當你考慮整合新工作時,通常最好在主題分支中嘗試一下——這是一個專門用於嘗試新工作的臨時分支。這樣,你可以輕鬆地單獨調整一個補丁,如果不起作用可以暫時擱置,直到你有時間再回來處理。如果你根據你將要嘗試的工作主題建立一個簡單的分支名稱,例如 ruby_client 或其他類似的描述性名稱,那麼如果你需要暫時放棄它並在以後回來,可以輕鬆記住它。Git 專案的維護者也傾向於對這些分支進行名稱空間劃分——例如 sc/ruby_client,其中 sc 是貢獻者的簡稱。你會記得,你可以根據你的 master 分支建立分支,如下所示:

$ git branch sc/ruby_client master

或者,如果你想立即切換到該分支,可以使用 checkout -b 選項:

$ git checkout -b sc/ruby_client master

現在,你已準備好將收到的貢獻工作新增到這個主題分支中,並決定是否要將其合併到你的長期分支中。

應用來自電子郵件的補丁

如果你透過電子郵件收到需要整合到專案中的補丁,你需要將該補丁應用到你的主題分支中進行評估。有兩種方法可以應用電子郵件補丁:使用 git apply 或使用 git am

使用 apply 應用補丁

如果你收到的補丁是使用 git diff 或 Unix diff 命令的某種變體(不推薦這樣做;詳見下一節)生成的,你可以使用 git apply 命令來應用它。假設你將補丁儲存在 /tmp/patch-ruby-client.patch,你可以這樣應用補丁:

$ git apply /tmp/patch-ruby-client.patch

這會修改你的工作目錄中的檔案。它幾乎與執行 patch -p1 命令來應用補丁相同,儘管它更謹慎,並且接受的模糊匹配比 patch 少。如果檔案新增、刪除和重新命名是以 git diff 格式描述的,它也能處理,而 patch 則不能。最後,git apply 是一種“全部應用或全部中止”的模型,要麼全部應用,要麼全部不應用,而 patch 可以部分應用補丁檔案,使你的工作目錄處於奇怪的狀態。git apply 總體上比 patch 更保守。它不會為你建立提交——執行它之後,你必須手動暫存並提交所引入的更改。

你還可以使用 git apply 來檢視補丁是否能幹淨地應用,然後再實際嘗試應用它——你可以使用 git apply --check 選項執行補丁:

$ git apply --check 0001-see-if-this-helps-the-gem.patch
error: patch failed: ticgit.gemspec:1
error: ticgit.gemspec: patch does not apply

如果沒有輸出,那麼補丁應該能夠乾淨地應用。如果檢查失敗,此命令還會以非零狀態退出,因此如果你願意,可以在指令碼中使用它。

使用 am 應用補丁

如果貢獻者是 Git 使用者,並且很好地使用了 format-patch 命令來生成補丁,那麼你的工作就會更容易,因為該補丁包含了作者資訊和提交資訊。如果可以,鼓勵你的貢獻者使用 format-patch 而不是 diff 來為你生成補丁。你應該只對舊式補丁等使用 git apply

要應用由 format-patch 生成的補丁,你使用 git am(該命令名為 am 是因為它用於“從郵箱中應用一系列補丁”)。從技術上講,git am 被設計用於讀取 mbox 檔案,這是一種簡單的純文字格式,用於將一個或多個電子郵件訊息儲存在一個文字檔案中。它看起來像這樣:

From 330090432754092d704da8e76ca5c05c198e71a8 Mon Sep 17 00:00:00 2001
From: Jessica Smith <jessica@example.com>
Date: Sun, 6 Apr 2008 10:17:23 -0700
Subject: [PATCH 1/2] Add limit to log function

Limit log functionality to the first 20

這是你在上一節中看到的 git format-patch 命令輸出的開頭;它也代表了一種有效的 mbox 電子郵件格式。如果有人使用 git send-email 正確地透過電子郵件將補丁傳送給你,並且你將補丁下載為 mbox 格式,那麼你可以將 git am 指向該 mbox 檔案,它將開始應用它看到的所有補丁。如果你執行的郵件客戶端可以將多封郵件儲存為 mbox 格式,你可以將整個補丁系列儲存到一個檔案中,然後使用 git am 一次應用一個。

然而,如果有人透過 git format-patch 生成的補丁檔案上傳到票務系統或類似的地方,你可以將該檔案儲存在本地,然後將磁碟上儲存的檔案傳遞給 git am 來應用它:

$ git am 0001-limit-log-function.patch
Applying: Add limit to log function

你可以看到它已乾淨地應用並自動為你建立了新的提交。作者資訊取自電子郵件的 FromDate 標頭,提交的訊息取自電子郵件的 Subject 和正文(補丁之前的部分)。例如,如果此補丁是根據上面 mbox 示例應用的,生成的提交將如下所示:

$ git log --pretty=fuller -1
commit 6c5e70b984a60b3cecd395edd5b48a7575bf58e0
Author:     Jessica Smith <jessica@example.com>
AuthorDate: Sun Apr 6 10:17:23 2008 -0700
Commit:     Scott Chacon <schacon@gmail.com>
CommitDate: Thu Apr 9 09:19:06 2009 -0700

   Add limit to log function

   Limit log functionality to the first 20

Commit 資訊表示應用此補丁的人員和應用時間。Author 資訊是最初建立此補丁的人員和最初建立時間。

但補丁可能無法乾淨地應用。也許你的主分支與補丁所基於的分支偏離太遠,或者該補丁依賴於你尚未應用的另一個補丁。在這種情況下,git am 程序將失敗並詢問你想要做什麼:

$ git am 0001-see-if-this-helps-the-gem.patch
Applying: See if this helps the gem
error: patch failed: ticgit.gemspec:1
error: ticgit.gemspec: patch does not apply
Patch failed at 0001.
When you have resolved this problem run "git am --resolved".
If you would prefer to skip this patch, instead run "git am --skip".
To restore the original branch and stop patching run "git am --abort".

此命令會在任何有問題的檔案中放置衝突標記,非常類似於衝突的合併或 rebase 操作。你解決這個問題的方式大致相同——編輯檔案以解決衝突,暫存新檔案,然後執行 git am --resolved 以繼續下一個補丁:

$ (fix the file)
$ git add ticgit.gemspec
$ git am --resolved
Applying: See if this helps the gem

如果你想讓 Git 更智慧地嘗試解決衝突,可以向其傳遞 -3 選項,這會使 Git 嘗試進行三方合併。此選項預設不啟用,因為如果補丁宣告其所基於的提交不在你的倉庫中,它將不起作用。如果你確實擁有該提交——如果補丁是基於一個公共提交——那麼 -3 選項在應用衝突補丁時通常會更智慧:

$ git am -3 0001-see-if-this-helps-the-gem.patch
Applying: See if this helps the gem
error: patch failed: ticgit.gemspec:1
error: ticgit.gemspec: patch does not apply
Using index info to reconstruct a base tree...
Falling back to patching base and 3-way merge...
No changes -- Patch already applied.

在這種情況下,如果沒有 -3 選項,該補丁將被視為衝突。由於使用了 -3 選項,補丁已乾淨地應用。

如果你正在從 mbox 應用多個補丁,你也可以在互動模式下執行 am 命令,它會在找到每個補丁時停止並詢問你是否要應用它:

$ git am -3 -i mbox
Commit Body is:
--------------------------
See if this helps the gem
--------------------------
Apply? [y]es/[n]o/[e]dit/[v]iew patch/[a]ccept all

如果你儲存了多個補丁,這會很方便,因為如果你不記得補丁是什麼,可以先檢視它,或者如果你已經應用過該補丁,則不應用它。

當你的主題的所有補丁都已應用並提交到你的分支中時,你可以選擇是否以及如何將它們整合到長期執行的分支中。

檢出遠端分支

如果你的貢獻來自一個 Git 使用者,他設定了自己的倉庫,將一些更改推送到其中,然後將倉庫的 URL 和包含更改的遠端分支的名稱傳送給你,你可以將其新增為遠端倉庫並在本地進行合併。

例如,如果 Jessica 傳送電子郵件告訴你她在她的倉庫的 ruby-client 分支中有一個很棒的新功能,你可以透過新增遠端倉庫並在本地檢出該分支來測試它:

$ git remote add jessica https://github.com/jessica/myproject.git
$ git fetch jessica
$ git checkout -b rubyclient jessica/ruby-client

如果她稍後再次透過電子郵件傳送給你另一個包含另一個出色功能的分支,你可以直接 fetchcheckout,因為你已經設定了遠端倉庫。

如果你與某人持續合作,這會非常有用。如果某人只是偶爾貢獻一個補丁,那麼透過電子郵件接受它可能比要求每個人都執行自己的伺服器並不得不不斷新增和刪除遠端倉庫以獲取幾個補丁更省時。你也不太可能希望有數百個遠端倉庫,每個遠端倉庫只對應貢獻一兩個補丁的人。然而,指令碼和託管服務可能會使這更容易——這主要取決於你的開發方式以及你的貢獻者的開發方式。

這種方法的另一個優點是,你也能獲得提交的歷史記錄。儘管你可能遇到合法的合併問題,但你知道他們的工作是基於你歷史中的哪個位置;三方合併是預設選項,而無需提供 -3 並希望補丁是基於你可訪問的公共提交生成的。

如果你沒有與某人持續合作,但仍希望以這種方式從他們那裡拉取,你可以將遠端倉庫的 URL 提供給 git pull 命令。這會進行一次性拉取,並且不會將 URL 儲存為遠端引用:

$ git pull https://github.com/onetimeguy/project
From https://github.com/onetimeguy/project
 * branch            HEAD       -> FETCH_HEAD
Merge made by the 'recursive' strategy.

確定引入了什麼

現在你有一個包含貢獻工作的主題分支。此時,你可以決定如何處理它。本節將重新介紹幾個命令,以便你可以瞭解如何使用它們來精確審查如果你將此分支合併到主分支中將引入哪些內容。

審查此分支中存在但不在你的 master 分支中的所有提交通常很有幫助。你可以透過在分支名稱前新增 --not 選項來排除 master 分支中的提交。這與我們之前使用的 master..contrib 格式作用相同。例如,如果你的貢獻者傳送給你兩個補丁,你建立了一個名為 contrib 的分支並將這些補丁應用到那裡,你可以執行以下命令:

$ git log contrib --not master
commit 5b6235bd297351589efc4d73316f0a68d484f118
Author: Scott Chacon <schacon@gmail.com>
Date:   Fri Oct 24 09:53:59 2008 -0700

    See if this helps the gem

commit 7482e0d16d04bea79d0dba8988cc78df655f16a0
Author: Scott Chacon <schacon@gmail.com>
Date:   Mon Oct 22 19:38:36 2008 -0700

    Update gemspec to hopefully work better

要檢視每個提交引入了哪些更改,請記住你可以將 -p 選項傳遞給 git log,它將把每個提交引入的差異附加到輸出中。

要檢視如果你將此主題分支與另一個分支合併將發生什麼的完整差異,你可能需要使用一個“奇怪的技巧”才能獲得正確的結果。你可能會想到執行:

$ git diff master

此命令會給你一個差異,但它可能具有誤導性。如果你的 master 分支在你從中建立主題分支後已向前推進,那麼你將得到看似奇怪的結果。之所以會發生這種情況,是因為 Git 直接比較你所在主題分支的最後一個提交的快照和 master 分支的最後一個提交的快照。例如,如果你在 master 分支上的檔案中添加了一行,則快照的直接比較會看起來像主題分支將刪除該行。

如果 master 是你主題分支的直接祖先,這不是問題;但如果這兩個歷史已經分歧,差異將看起來像你正在新增主題分支中的所有新內容並刪除 master 分支特有的所有內容。

你真正想看到的是新增到主題分支的更改——如果你將此分支與 master 合併,你將引入的工作。你透過讓 Git 將你的主題分支上的最後一個提交與它與 master 分支的第一個共同祖先進行比較來做到這一點。

從技術上講,你可以透過明確找出共同祖先,然後在其上執行你的差異比較來做到這一點:

$ git merge-base contrib master
36c7dba2c95e6bbb78dfa822519ecfec6e1ca649
$ git diff 36c7db

或者,更簡潔地:

$ git diff $(git merge-base contrib master)

然而,這兩種方式都不是特別方便,因此 Git 提供了另一種執行相同操作的簡寫方式:三點語法。在 git diff 命令的上下文中,你可以在另一個分支後放置三個點,以在當前分支的最後一個提交與其與另一個分支的共同祖先之間進行 diff

$ git diff master...contrib

此命令僅顯示你的當前主題分支自其與 master 的共同祖先以來引入的工作。這是一個非常實用的語法,值得記住。

整合貢獻的工作

當你的主題分支中的所有工作都準備好整合到主線分支時,問題是如何進行。此外,你希望使用哪種整體工作流來維護你的專案?你有多重選擇,因此我們將介紹其中的幾種。

合併工作流

一種基本的工作流是簡單地將所有工作直接合併到你的 master 分支中。在這種情況下,你有一個 master 分支,其中包含基本穩定的程式碼。當你的主題分支中有你認為已完成的工作,或者別人貢獻且你已驗證過的工作時,你將其合併到你的 master 分支,刪除剛剛合併的主題分支,然後重複此過程。

例如,如果我們的倉庫中有兩個名為 ruby_clientphp_client 的分支,其工作情況如下所示:包含多個主題分支的歷史,如果我們先合併 ruby_client 再合併 php_client,你的歷史記錄將如下所示:主題分支合併後

History with several topic branches
圖 72. 包含多個主題分支的歷史
After a topic branch merge
圖 73. 主題分支合併後

這可能是最簡單的工作流,但如果你正在處理更大或更穩定的專案,並且希望對引入的內容非常謹慎,那麼這可能會有問題。

如果你有一個更重要的專案,你可能希望使用兩階段合併週期。在這種情況下,你有兩個長期執行的分支,masterdevelop,你在此方案中確定 master 僅在釋出非常穩定的版本時更新,並且所有新程式碼都整合到 develop 分支中。你定期將這兩個分支推送到公共倉庫。每次有新的主題分支要合併時(主題分支合併前),你將其合併到 develop 中(主題分支合併後);然後,當你標記釋出時,你將 master 快進到當前穩定的 develop 分支所在的位置(專案釋出後)。

Before a topic branch merge
圖 74. 主題分支合併前
After a topic branch merge
圖 75. 主題分支合併後
After a project release
圖 76. 專案釋出後

這樣,當人們克隆你的專案倉庫時,他們既可以檢出 master 來構建最新的穩定版本並輕鬆保持更新,也可以檢出 develop,這是更前沿的內容。你還可以擴充套件這個概念,設定一個 integrate 分支,所有工作都在其中合併。然後,當該分支上的程式碼庫穩定並透過測試時,你將其合併到 develop 分支;當 develop 分支已經穩定一段時間後,你再快進你的 master 分支。

大型合併工作流

Git 專案有四個長期執行的分支:masternextseen(以前是 'pu'——提議的更新),用於新工作,以及 maint 用於維護回溯。當貢獻者引入新工作時,它們以類似於我們所描述的方式收集到維護者的倉庫中的主題分支中(參見管理一系列複雜的並行貢獻主題分支)。此時,對主題進行評估,以確定它們是否安全並可供使用,或者是否需要更多工作。如果它們安全,則將其合併到 next 中,並將該分支推送上去,以便所有人都可以嘗試整合在一起的主題。

Managing a complex series of parallel contributed topic branches
圖 77. 管理一系列複雜的並行貢獻主題分支

如果主題仍需要工作,則將其合併到 seen 中。當確定它們完全穩定時,這些主題會重新合併到 master 中。nextseen 分支隨後從 master 重建。這意味著 master 幾乎總是向前推進,next 偶爾會 rebase,而 seen 則更頻繁地 rebase:

Merging contributed topic branches into long-term integration branches
圖 78. 將貢獻的主題分支合併到長期整合分支中

當一個主題分支最終合併到 master 中時,它將從倉庫中移除。Git 專案還有一個 maint 分支,它是從上次釋出中分叉出來的,用於在需要維護髮布時提供回溯補丁。因此,當你克隆 Git 倉庫時,你有四個分支可以檢出,以便在不同的開發階段評估專案,這取決於你想要多麼前沿或你想要如何貢獻;維護者有一個結構化的工作流來幫助他們審查新的貢獻。Git 專案的工作流是專門化的。要清楚地理解這一點,你可以查閱 Git 維護者指南

Rebase 和 Cherry-Pick 工作流

其他維護者傾向於在其 master 分支之上 rebase 或 cherry-pick 貢獻的工作,而不是合併它,以保持大部分線性的歷史記錄。當你的主題分支中有工作並確定要整合它時,你切換到該分支並執行 rebase 命令,以在當前 master(或 develop 等)分支之上重建更改。如果這順利進行,你可以快進你的 master 分支,最終你將獲得一個線性的專案歷史記錄。

將引入的工作從一個分支移動到另一個分支的另一種方法是 cherry-pick 它。Git 中的 cherry-pick 就像單個提交的 rebase。它獲取提交中引入的補丁,並嘗試將其重新應用到你當前所在的分支上。如果你在主題分支上有多個提交併且你只想整合其中一個,或者如果你的主題分支上只有一個提交併且你更喜歡 cherry-pick 它而不是執行 rebase,這會很有用。例如,假設你有一個專案,它看起來像這樣:

Example history before a cherry-pick
圖 79. Cherry-pick 前的示例歷史

如果你想將提交 e43a6 拉取到你的 master 分支中,你可以執行:

$ git cherry-pick e43a6
Finished one cherry-pick.
[master]: created a0a41a9: "More friendly message when locking the index fails."
 3 files changed, 17 insertions(+), 3 deletions(-)

這會拉取在 e43a6 中引入的相同更改,但你會得到一個新的提交 SHA-1 值,因為應用日期不同。現在你的歷史記錄看起來像這樣:

History after cherry-picking a commit on a topic branch
圖 80. 在主題分支上 Cherry-pick 提交後的歷史

現在你可以刪除你的主題分支,並放棄你不想拉取的提交。

Rerere

如果你經常進行合併和 rebase 操作,或者你正在維護一個長期存在的主題分支,Git 有一個名為“rerere”的功能可以提供幫助。

Rerere 代表“重用已記錄的解決方案”(“reuse recorded resolution”)——這是一種簡化手動解決衝突的方法。當 rerere 啟用時,Git 會保留一組成功合併的前後影像,如果它注意到某個衝突與你已經修復過的衝突完全相同,它就會直接使用上次的修復方案,而無需你再次干預。

這個功能包括兩個部分:一個配置設定和一個命令。配置設定是 rerere.enabled,它非常方便,可以新增到你的全域性配置中:

$ git config --global rerere.enabled true

現在,無論何時你進行合併並解決衝突,該解決方案都將記錄在快取中,以備將來使用。

如果需要,你可以使用 git rerere 命令與 rerere 快取進行互動。當它單獨呼叫時,Git 會檢查其解決方案資料庫,並嘗試與任何當前合併衝突匹配並解決它們(儘管如果 rerere.enabled 設定為 true,這會自動完成)。還有子命令可以檢視將記錄什麼,從快取中清除特定解決方案,以及清除整個快取。我們將在Rerere中更詳細地介紹 rerere。

標記你的釋出

當你決定釋出一個版本時,你可能希望分配一個標籤,以便你可以在未來的任何時候重新建立該版本。你可以按照Git 基礎中討論的方式建立新標籤。如果你決定以維護者的身份簽署標籤,標記過程可能如下所示:

$ git tag -s v1.5 -m 'my signed 1.5 tag'
You need a passphrase to unlock the secret key for
user: "Scott Chacon <schacon@gmail.com>"
1024-bit DSA key, ID F721C45A, created 2009-02-09

如果你確實簽署了你的標籤,你可能會遇到分發用於簽署標籤的公共 PGP 金鑰的問題。Git 專案的維護者透過將他們的公共金鑰作為 blob 包含在倉庫中,然後新增一個直接指向該內容的標籤來解決了這個問題。為此,你可以透過執行 gpg --list-keys 來確定你想要的金鑰:

$ gpg --list-keys
/Users/schacon/.gnupg/pubring.gpg
---------------------------------
pub   1024D/F721C45A 2009-02-09 [expires: 2010-02-09]
uid                  Scott Chacon <schacon@gmail.com>
sub   2048g/45D02282 2009-02-09 [expires: 2010-02-09]

然後,你可以透過匯出金鑰並將其透過 git hash-object 管道直接匯入到 Git 資料庫中,這將把包含這些內容的新 blob 寫入 Git 並返回給你該 blob 的 SHA-1 值:

$ gpg -a --export F721C45A | git hash-object -w --stdin
659ef797d181633c87ec71ac3f9ba29fe5775b92

現在你的金鑰內容已在 Git 中,你可以透過指定 hash-object 命令提供給你的新 SHA-1 值來建立一個直接指向它的標籤:

$ git tag -a maintainer-pgp-pub 659ef797d181633c87ec71ac3f9ba29fe5775b92

如果你執行 git push --tagsmaintainer-pgp-pub 標籤將與所有人共享。如果有人想驗證一個標籤,他們可以透過直接從資料庫中拉取 blob 並將其匯入 GPG 來直接匯入你的 PGP 金鑰:

$ git show maintainer-pgp-pub | gpg --import

他們可以使用該金鑰來驗證你所有的已簽名標籤。此外,如果你在標籤訊息中包含說明,執行 git show <tag> 將允許你向終端使用者提供關於標籤驗證的更具體說明。

生成構建號

因為 Git 沒有像 'v123' 這樣或等效的單調遞增數字與每個提交關聯,如果你想為提交提供一個人類可讀的名稱,你可以在該提交上執行 git describe。作為回應,Git 會生成一個字串,該字串由早於該提交的最新標籤的名稱組成,然後是自該標籤以來的提交數量,最後是所描述提交的部分 SHA-1 值(字首為字母“g”,表示 Git):

$ git describe master
v1.6.2-rc1-20-g8c5b85c

透過這種方式,你可以匯出快照或構建,並將其命名為人們易於理解的名稱。事實上,如果你從 Git 倉庫克隆的原始碼構建 Git,git --version 會給你一個類似這樣的結果。如果你描述的是你直接標記的提交,它只會給你標籤名稱。

預設情況下,git describe 命令需要帶註解的標籤(使用 -a-s 標誌建立的標籤);如果你想利用輕量級(非註解)標籤,請在命令中新增 --tags 選項。你還可以將此字串用作 git checkoutgit show 命令的目標,儘管它依賴於末尾的縮寫 SHA-1 值,因此它可能不會永遠有效。例如,Linux 核心最近從 8 個字元增加到 10 個字元以確保 SHA-1 物件唯一性,因此舊的 git describe 輸出名稱已失效。

準備釋出

現在你想要釋出一個構建。你想做的一件事是為那些不使用 Git 的“可憐人”建立程式碼最新快照的歸檔。執行此操作的命令是 git archive

$ git archive master --prefix='project/' | gzip > `git describe master`.tar.gz
$ ls *.tar.gz
v1.6.2-rc1-20-g8c5b85c.tar.gz

如果有人開啟該 tarball,他們將在 project 目錄下獲得你專案的最新快照。你也可以以大致相同的方式建立 zip 歸檔,但要向 git archive 傳遞 --format=zip 選項:

$ git archive master --prefix='project/' --format=zip > `git describe master`.zip

現在你有了你的專案釋出的漂亮 tarball 和 zip 歸檔,你可以將它們上傳到你的網站或透過電子郵件傳送給人們。

短日誌

是時候給那些想了解你的專案進展的人傳送郵件列表了。快速獲取自上次釋出或郵件以來新增到專案中的更改日誌的一種好方法是使用 git shortlog 命令。它會總結你給定的範圍內的所有提交;例如,如果你的上次釋出名為 v1.0.1,以下命令會給你自上次釋出以來的所有提交的摘要:

$ git shortlog --no-merges master --not v1.0.1
Chris Wanstrath (6):
      Add support for annotated tags to Grit::Tag
      Add packed-refs annotated tag support.
      Add Grit::Commit#to_patch
      Update version and History.txt
      Remove stray `puts`
      Make ls_tree ignore nils

Tom Preston-Werner (4):
      fix dates in history
      dynamic version method
      Version bump to 1.0.2
      Regenerated gemspec for version 1.0.2

你將獲得自 v1.0.1 以來所有提交的清晰摘要,按作者分組,你可以將其透過電子郵件傳送給你的列表。

scroll-to-top