如何編寫一個(gè)全新的 Git 協(xié)議
曾幾何時(shí),我在持續(xù)追蹤自己的文件方面遇到一些問題。通常,我忘了自己是否將文件保存在自己的桌面電腦、筆記本電腦或者電話上,或者保存在了云上的什么地方。更有甚者,對(duì)非常重要的信息,像密碼和Bitcoin的密匙,僅以純文本郵件的形式將它發(fā)送給自己讓我芒刺在背。
我需要的是將自己的數(shù)據(jù)存放一個(gè)git倉庫里,然后將這個(gè)git倉庫保存在一個(gè)地方。我可以查看以前的版本而且不用提心數(shù)據(jù)被刪除。更最要的是,我已經(jīng)能熟練地在不同電腦上使用git來上傳和下載文件。
但是,如我所言,我并不想簡(jiǎn)單地上傳我的密匙和密碼到GitHub或者BitBucket,哪怕是其中的私有倉庫。
一個(gè)很酷的想法在我腦中生騰:寫一個(gè)工具來加密我的倉庫,然后再將它Push到Backup。遺憾的是,不能像平時(shí)那樣使用 git push命令,需要使用像這樣的命令:
$ encrypted-git push http://example.com/
至少,在我發(fā)現(xiàn)git-remote-helpers以前是這樣想的。
Git remote helpers
我在網(wǎng)上找到一篇git remote helpers的文檔。
原來,如果你運(yùn)行命令
$ git remote add origin asdf://example.com/repo
$ git push --all origin
Git會(huì)首先檢查是否內(nèi)建了asdf協(xié)議,當(dāng)發(fā)現(xiàn)沒有內(nèi)建時(shí),它會(huì)檢查git-remote-asdf是否在PATH(環(huán)境變量)里,如果在,它會(huì)運(yùn)行 git-remote-asdf origin asdf://example.com/repo 來處理本次會(huì)話。
同樣的,你可以運(yùn)行
$ git clone asdf::http://example.com/repo
來讓git調(diào)用 git-remote-asdf origin http://example.com/repo.
很遺憾的是,我發(fā)現(xiàn)文檔在真正實(shí)現(xiàn)一個(gè)helper的細(xì)節(jié)上語焉不詳,而這正是我需要的。但是隨后,我在Git源碼中找到了一個(gè)叫g(shù)it- remote-testgit.sh的腳本,它實(shí)現(xiàn)了一個(gè)用來測(cè)試git遠(yuǎn)程輔助系統(tǒng)的testgit。 它基本實(shí)現(xiàn)了從同樣文件系統(tǒng)的本地倉庫推送和抓取功能。所以
git clone testgit::/existing-repository
與
git clone /existing-repository
就一樣了。
同樣地,你可以透過testgit協(xié)議向本地倉庫中推送或者從中抓取。
在本文件中,我們將瀏覽git-remote-testgit的源碼并以Go語言實(shí)現(xiàn)一個(gè)全新的helper分支: git-remote-go。過程中,我將解釋源碼的意思,以及在實(shí)現(xiàn)我自己的remote helper(git-remote-grave)中領(lǐng)悟到的種種.
基礎(chǔ)知識(shí)
為了后面的章節(jié)理解方面,讓我們先學(xué)習(xí)一些術(shù)語和基本機(jī)制。
當(dāng)我們運(yùn)行
$ git remote add myremote go::http://example.com/repo
$ git push myremote master
Git會(huì)運(yùn)行以下命令來實(shí)例化一個(gè)新的進(jìn)程
git-remote-go myremote http://example.com/repo
注意:***個(gè)參數(shù)是remote name,第二個(gè)參數(shù)是url.
當(dāng)你運(yùn)行
$ git clone go::http://example.com/repo
下一條命令會(huì)實(shí)例化helper
git-remote-go origin http://example.com/repo
因?yàn)檫h(yuǎn)程origin會(huì)自動(dòng)在克隆的倉庫中自動(dòng)創(chuàng)建。
當(dāng)Git以一個(gè)新的進(jìn)程實(shí)例化helper時(shí),它會(huì)為 stdin,stdout及stderr通信打開管道。命令被通過stdin送達(dá)helper,helper通過stdout響應(yīng)。任何helper在 stderr上的輸出被重定向到git的stderr(它可能是一個(gè)終端)。
下圖說明了這種關(guān)系:
我需要說明的***一點(diǎn)是如何區(qū)分本地和遠(yuǎn)程倉庫。通常(但不是每一次),本地倉庫是我們運(yùn)行g(shù)it的地方,遠(yuǎn)程倉庫是我們需要連接的。
所以在push中,我們從本地倉庫發(fā)送更改(的地方)到遠(yuǎn)程倉庫。在Fetch中,我們從遠(yuǎn)程倉庫抓取更改(的地方)到本地倉庫。在Clone中,我們將遠(yuǎn)程倉庫克隆到本地。
當(dāng)git運(yùn)行helper時(shí),git將環(huán)境變量GIT_DIR設(shè)置為本地倉庫的Git目錄(比如:local/.git)。
項(xiàng)目開搞
在這篇文章中,我假設(shè)Go語言已經(jīng)被安裝,并且使用了環(huán)境變量$GOPATH指向一個(gè)為go的目錄。
讓我們以創(chuàng)建目錄go/src/git-remote-go開始。這樣的話我們就可以通過運(yùn)行g(shù)o install來安裝我們的插件(假設(shè)go/bin在PATH中)。
在意識(shí)里面有了這一點(diǎn)后,我們可以編寫go/src/git-remote-go/main.go最初的幾行代碼。
- package mainimport (
- "log"
- "os")func Main() (er error) {
- if len(os.Args) < 3 {
- return fmt.Errorf("Usage: git-remote-go remote-name url")
- }
- remoteName := os.Args[1]
- url := os.Args[2]}func main() {
- if err := Main(); err != nil {
- log.Fatal(err)
- }}
我將Main()分割了開來,因?yàn)楫?dāng)我們需要返回錯(cuò)誤時(shí)錯(cuò)誤處理將會(huì)變得更容易。這里我們也可以使用defet,因?yàn)閘og.Fatal調(diào)用了os.Exit但不調(diào)用defer里面的函數(shù)。
現(xiàn)在,讓我們看下git-remote-testgit文件的最頂部,看下接下來需要做什么。
- #!/bin/sh# Copyright (c) 2012 Felipe Contrerasalias=$1url=$2dir="$GIT_DIR/testgit
- /$alias"prefix="refs/testgit/$alias"default_refspec="refs/heads/*:${prefix}/heads
- /*"refspec="${GIT_REMOTE_TESTGIT_REFSPEC-$default_refspec}"test -z "$refspec" && prefix="refs"GIT_DIR="$url
- /.git"export GIT_DIRforce=mkdir -p "$dir"if test -z "$GIT_REMOTE_TESTGIT_NO_MARKS"then gitmarks="$dir/git.marks"
- testgitmarks="$dir/testgit.marks"
- test -e "$gitmarks" || >"$gitmarks"
- test -e "$testgitmarks" || >"$testgitmarks"fi
他們稱之為alias的變量就是我們所說的remoteName。url則是同樣的意義。
下一個(gè)聲明是:
dir="$GIT_DIR/testgit/$alias"
這里在Git目錄下創(chuàng)建了一個(gè)命名空間以標(biāo)識(shí)testgit協(xié)議和我們正在使用的遠(yuǎn)程路徑。通過這樣,testgit下面origin分支下的文件就能與backup分支下面的文件區(qū)分開來。
再下面,我們看到這樣的聲明:
mkdir -p "$dir"
此處確保了本地目錄已被創(chuàng)建,如果不存在則創(chuàng)建。
讓我們?yōu)槲覀兊腉o程序添加本地目錄的創(chuàng)建。
- // Add "path" to the import
- listlocaldir := path.Join(os.Getenv("GIT_DIR"), "go", remoteName)
- if err := os.MkdirAll(localdir, 0755);
- err != nil {
- return err
- }
緊接著上面的腳本,我們有以下幾行:
prefix="refs/testgit/$alias"default_refspec="refs/heads/*:${prefix}/heads
/*"refspec="${GIT_REMOTE_TESTGIT_REFSPEC-$default_refspec}"test -z "$refspec" && prefix="refs"
這里快速談?wù)撘幌聄efs。
在git中,refs存放在.git/refs:
.git
- └── refs
- ├── heads
- │ └── master
- ├── remotes
- │ ├── gravy
- │ └── origin
- │ └── master
- └── tags
在上面的樹中,remotes/origin/master包括了遠(yuǎn)程origin中mater分支下最近大量的提交。而heads/master則關(guān)聯(lián)你本地mater分支下最近大量的提交。一個(gè)ref就像一個(gè)指向一次提交的指針。
refspec則可以讓我把遠(yuǎn)程的refs的本地的refs映射起來。在上面的代碼中,prefix就是會(huì)被遠(yuǎn)程refs保留的目錄。如果遠(yuǎn)程的名 稱是原始的,那么遠(yuǎn)程master分支將會(huì)由.git/refs/testgit/origin/master所指定。這樣就很基本地為遠(yuǎn)程的分支創(chuàng)建了 指定協(xié)議的命名空間。
接下來的這一行則是refspec。這一行
default_refspec="refs/heads/*:${prefix}/heads/*"
可以擴(kuò)展成
default_refspec="refs/heads/*:refs/testgit/$alias/*"
這意味著遠(yuǎn)程分支的映射看起來就像把refs/heads/*(這里的*表示任意文本)對(duì)應(yīng)到refs/testgit/$alias/*(這里 的*將會(huì)被前面的*表示的文本替換)。例如,refs/heads/master將會(huì)映射到refs/testgit/origin/master。
基本上來講,refspec允許testgit添加一個(gè)新的分支到自己的樹中,例如這樣:
.git
- └── refs
- ├── heads
- │ └── master
- ├── remotes
- │ └── origin
- │ └── master
- ├── testgit
- │ └── origin
- │ └── master
- └── tags
下一行
refspec="${GIT_REMOTE_TESTGIT_REFSPEC-$default_refspec}"
把$refspec設(shè)置成$GIT_REMOTE_TESTGIT_REFSPEC,除非它不存在,否則它會(huì)成 為$default_refspec。這樣的話就能通過testgit測(cè)試其他的refspecs了。我們假設(shè)都已經(jīng)成功設(shè)置 了$default_refspec。
***,再下一行,
test -z "$refspec" && prefix="refs"
按照我們的理解,看起來像是如果$GIT_REMOTE_TESTGIT_REFSPEC存在卻為空時(shí)則把$prefix設(shè)置成refs。
我們需要自己的refspec,所以需要添加這一行
refspec := fmt.Sprintf("refs/heads/*:refs/go/%s/*", remoteName)
緊隨上面的代碼,我們看到了
GIT_DIR="$url/.git"export GIT_DIR
關(guān)于$GIT_DIR的另一個(gè)事實(shí)就是如果它有在環(huán)境變量中設(shè)置,那么底層的git將會(huì)使用環(huán)境變量中$GIT_DIR的目錄作為它的.git目錄,而不再是本地目錄的.git。這個(gè)命令使得未來全部插件的Git命令都能在遠(yuǎn)程制品庫的上下文中執(zhí)行。
我們把這點(diǎn)轉(zhuǎn)換成
- if err := os.Setenv("GIT_DIR", path.Join(url, ".git")); err != nil {
- return err}
當(dāng)然請(qǐng)記住,那個(gè)$dir和我們變量中的localdir依然指向我們正在fetch或push的子目錄。
main塊里面還有一小段代碼
- if test -z "$GIT_REMOTE_TESTGIT_NO_MARKS"then gitmarks="$dir/git.marks"
- testgitmarks="$dir/testgit.marks"
- test -e "$gitmarks" || >"$gitmarks"
- test -e "$testgitmarks" || >"$testgitmarks"fi
按我們的理解是,如果$GIT_REMOTE_TESTGIT_NO_MARKS未設(shè)置,if語句中的內(nèi)容將會(huì)被執(zhí)行。
這些標(biāo)識(shí)文件可以紀(jì)錄像git fast-export和git fast-import這些傳遞過程中ref和blob的有關(guān)信息。有一點(diǎn)是非常重要的,即這些標(biāo)識(shí)在各式各樣的插件中都是一樣的,所以他們都是保存在localdir中。
這里,$gitmarks關(guān)聯(lián)著我們本地制品庫中g(shù)it寫入的標(biāo)識(shí),$testgitmarks則保存遠(yuǎn)程處理寫入的標(biāo)識(shí)。
下面這兩行有點(diǎn)像touch的使用,如果標(biāo)識(shí)文件不存在,則創(chuàng)建一個(gè)空的。
test -e "$gitmarks" || >"$gitmarks"test -e "$testgitmarks" || >"$testgitmarks"
我們自己的程序中需要這些文件,所以讓我們以編寫一個(gè)Touch函數(shù)開始。
- // Create path as an empty file if it doesn't exist, otherwise do nothing.// This works by opening a file in exclusive mode; if it already exists,// an error will be returned rather than truncating it.func Touch(path string) error {
- file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0666)
- if os.IsExist(err) {
- return nil
- } else if err != nil {
- return err
- }
- return file.Close()}
現(xiàn)在我們可以創(chuàng)建標(biāo)識(shí)文件了。
- gitmarks := path.Join(localdir, "git.marks")gomarks := path.Join(localdir, "go.marks")if err := Touch(gitmarks); err != nil {
- return err}if err := Touch(gomarks); err != nil {
- return err}
然后,我遇到的一個(gè)問題就是,如果因?yàn)槟承┰蚨鴮?dǎo)致插件失敗的話,這些標(biāo)識(shí)文件將會(huì)處于殘留在一個(gè)無效的狀態(tài)。為了預(yù)防這一點(diǎn),我們可以先保存文件的原始內(nèi)容,并且如果Main()函數(shù)返回一個(gè)錯(cuò)誤的話我們就重寫他們。
- // add "io/ioutil" to importsoriginalGitmarks, err := ioutil.ReadFile(gitmarks)if err != nil {
- return err}originalGomarks, err := ioutil.ReadFile(gomarks)if err != nil {
- return err}defer func() {
- if er != nil {
- ioutil.WriteFile(gitmarks, originalGitmarks, 0666)
- ioutil.WriteFile(gomarks, originalGomarks, 0666)
- }}()
***我們可以從關(guān)鍵命令操作開始。
命令行通過標(biāo)準(zhǔn)輸入流stdin傳遞到插件,也就是每一條命令是以回車結(jié)尾和一個(gè)字符串。插件則通過標(biāo)準(zhǔn)輸出流stdout對(duì)命令作出響應(yīng);標(biāo)準(zhǔn)錯(cuò)誤流stderr則通過管道輸出給終端用戶。
下面來編寫我們自己的命令操作。
- // Add "bufio" to import list.stdinReader := bufio.NewReader(os.Stdin)for {
- // Note that command will include the trailing newline.
- command, err := stdinReader.ReadString('\n')
- if err != nil {
- return err
- }
- switch {
- case command == "capabilities\n":
- // ...
- case command == "\n":
- return nil
- default:
- return fmt.Errorf("Received unknown command %q", command)
- }}
capabilities 命令
***條有待實(shí)現(xiàn)的命令是capabilities。插件要求能以空行結(jié)尾并以行分割的形式輸出顯示它能提供的命令和它所支持的操作。
- echo 'import'echo 'export'test -n "$refspec" && echo "refspec $refspec"if test -n "$gitmarks"then echo "*import-marks $gitmarks"
- echo "*export-marks $gitmarks"fitest -n "$GIT_REMOTE_TESTGIT_SIGNED_TAGS" && echo "signed-tags"test -n "$GIT_REMOTE_TESTGIT_NO_PRIVATE_UPDATE" && echo "no-private-update"echo 'option'echo
上面使用列表中聲明了此插件支持import,import和option命令操作。option命令允許git改變我們的插件中冗長的部分。
signed-tags意味著當(dāng)git為export命令創(chuàng)建了一個(gè)快速導(dǎo)入的流時(shí),它將會(huì)把–signed-tags=verbatim傳遞給git-fast-export。
no-private-update則指示著git不需要更新私有的ref當(dāng)它被成功push后。我未曾看到有需要用到這個(gè)特性。
refspec $refspec用于告訴git我們需要使用哪個(gè)refspec。
*import-marks $gitmarks和*export-marks $gitmarks意思是git應(yīng)該保存它生成的標(biāo)識(shí)到gitmarks文件中。*號(hào)表示如果git不能識(shí)別這幾行,它必須失敗返回而不是忽略他們。這是 因?yàn)椴寮蕾囉谒4娴臉?biāo)識(shí)文件,并且不能和git不支持的版本一起工作。
我們先忽略signed-tags,no-private-update和option,因?yàn)樗鼈冇糜谠趃it-remote-testgit未完成的測(cè)試,并且在我們這個(gè)例子中也不需要這些。我們可以這樣簡(jiǎn)單地實(shí)現(xiàn)上面這些,如:
- case command == "capabilities\n":
- fmt.Printf("import\n")
- fmt.Printf("export\n")
- fmt.Printf("refspec %s\n", refspec)
- fmt.Printf("*import-marks %s\n", gitmarks)
- fmt.Printf("*export-marks %s\n", gitmarks)
- fmt.Printf("\n")
list命令
下一個(gè)命令是list。這個(gè)命令的使用說明并沒有包括在capabilities命令輸出的使用說明列表中,是因?yàn)樗ǔ6际遣寮仨氈С值摹?/p>
當(dāng)插件接收到一個(gè)list命令時(shí),它應(yīng)該打印輸出遠(yuǎn)程制品庫上的ref,并每行以$objectname $refname這樣的格式用一系列的行來表示,并且***跟著一行空行。$refname對(duì)應(yīng)著ref的名稱,$objectname則是ref指向的內(nèi) 容。$objectname可以是一次提交的哈希,或者用@$refname表示指向另外一個(gè)ref,或者是用?表示ref的值不可獲得。
git-remote-testgit的實(shí)現(xiàn)如下。
git for-each-ref --format='? %(refname)' 'refs/heads/'head=$(git symbolic-ref HEAD)echo "@$head HEAD"echo
記住,$GIT_DIR將觸發(fā)git for-each-ref在遠(yuǎn)程制品庫的執(zhí)行,并將會(huì)為每一個(gè)分支打印一行? $refname,同時(shí)還有@$head HEAD,這里的$head即為指向制品庫HEAD的ref的名稱。
在一個(gè)常規(guī)的制品庫里一般會(huì)有兩個(gè)分支,即master主分支和dev開發(fā)分支,這樣的話上面的輸出可能就像這樣
- ? refs/heads/master
- ? refs/heads/development
- @refs/heads/master HEAD
- <blank>
現(xiàn)在讓我們自己來寫這些。先寫一個(gè)GitListRefs()函數(shù),因?yàn)槲覀兩院驎?huì)再次用到。
- // Add "os/exec" and "bytes" to the import list.// Returns a map of refnames to objectnames.func GitListRefs() (map[string]string, error) {
- out, err := exec.Command(
- "git", "for-each-ref", "--format=%(objectname) %(refname)",
- "refs/heads/",
- ).Output()
- if err != nil {
- return nil, err
- }
- lines := bytes.Split(out, []byte{'\n'})
- refs := make(map[string]string, len(lines))
- for _, line := range lines {
- fields := bytes.Split(line, []byte{' '})
- if len(fields) < 2 {
- break
- }
- refs[string(fields[1])] = string(fields[0])
- }
- return refs, nil}
- 現(xiàn)在編寫GitSymbolicRef()。
- func GitSymbolicRef(name string) (string, error) {
- out, err := exec.Command("git", "symbolic-ref", name).Output()
- if err != nil {
- return "", fmt.Errorf(
- "GitSymbolicRef: git symbolic-ref %s: %v", name, out, err)
- }
- return string(bytes.TrimSpace(out)), nil}
然后可以像這樣來實(shí)現(xiàn)list命令。
- case command == "list\n":
- refs, err := GitListRefs()
- if err != nil {
- return fmt.Errorf("command list: %v", err)
- }
- head, err := GitSymbolicRef("HEAD")
- if err != nil {
- return fmt.Errorf("command list: %v", err)
- }
- for refname := range refs {
- fmt.Printf("? %s\n", refname)
- }
- fmt.Printf("@%s HEAD\n", head)
- fmt.Printf("\n")
import 命令
下一步是git在fetch或clone時(shí)會(huì)用到的import命令。這個(gè)命令實(shí)際來源于batch:它把import $refname作為一系列的行并用一個(gè)空行結(jié)束來發(fā)送。當(dāng)git將此命令發(fā)送到輔助插件時(shí),它將以二進(jìn)制形式執(zhí)行g(shù)it fast-import,并且通過管道將標(biāo)準(zhǔn)輸出stdout和標(biāo)準(zhǔn)輸入stdin綁定起來。換句話說,輔助插件期望能在標(biāo)準(zhǔn)輸出stdout上返回一個(gè) git fast-export流。
讓我們看下git-remote-testgit的實(shí)現(xiàn)。
- # read all import lineswhile truedo ref="${line#* }"
- refs="$refs $ref"
- read line test "${line%% *}" != "import" && breakdoneif test -n "$gitmarks"then echo "feature import-marks=$gitmarks"
- echo "feature export-marks=$gitmarks"fiif test -n "$GIT_REMOTE_TESTGIT_FAILURE"then echo "feature done"
- exit 1fiecho "feature done"git fast-export \
- ${testgitmarks:+"--import-marks=$testgitmarks"} \
- ${testgitmarks:+"--export-marks=$testgitmarks"} \
- $refs |
- sed -e "s#refs/heads/#${prefix}/heads/#g"echo "done"
最頂部的循環(huán),正如注釋所說的,將全部的import $refname命令匯總到一個(gè)單一的變量$refs中,而$refs則是以空格分隔的列表。
接下來的,如果腳本正在使用gitmarks文件(假設(shè)是這樣),將會(huì)輸出feature import-marks=$gitmarks和feature export-marks=$gitmarks。這里告訴git需要把–import-marks=$gitmarks和–export- marks=$gitmarks傳遞給git fast-import。
再下一行中,如果出于測(cè)試目的設(shè)置了$GIT_REMOTE_TESTGIT_FAILURE,插件將會(huì)失敗。
在那以后,feature done將會(huì)輸出,暗示著將緊跟輸出導(dǎo)出的流內(nèi)容。
***,git fast-export在遠(yuǎn)程制品庫被調(diào)用,在遠(yuǎn)程標(biāo)識(shí)上設(shè)置指定的標(biāo)識(shí)文件以及$testgitmarks,然后返回我們需要導(dǎo)出的ref列表。
git-fast-export命令的輸出內(nèi)容,通過管道經(jīng)過將refs/heads/匹配到refs/testgit/$alias/heads/的sed命令。因此在export導(dǎo)出時(shí),我們傳遞給git的refspec將能很好的使用這個(gè)匹配映射。
在導(dǎo)出流后面,緊跟done輸出。
我們可以用go來嘗試一下。
- case strings.HasPrefix(command, "import "):
- refs := make([]string, 0)
- for {
- // Have to make sure to trim the trailing newline.
- ref := strings.TrimSpace(strings.TrimPrefix(command, "import "))
- refs = append(refs, ref)
- command, err = stdinReader.ReadString('\n')
- if err != nil {
- return err
- }
- if !strings.HasPrefix(command, "import ") {
- break
- }
- }
- fmt.Printf("feature import-marks=%s\n", gitmarks)
- fmt.Printf("feature export-marks=%s\n", gitmarks)
- fmt.Printf("feature done\n")
- args := []string{
- "fast-export",
- "--import-marks", gomarks,
- "--export-marks", gomarks,
- "--refspec", refspec}
- args = append(args, refs...)
- cmd := exec.Command("git", args...)
- cmd.Stderr = os.Stderr
- cmd.Stdout = os.Stdout
- if err := cmd.Run(); err != nil {
- return fmt.Errorf("command import: git fast-export: %v", err)
- }
- fmt.Printf("done\n")
export命令
下一步是export命令。當(dāng)我們完成了這個(gè)命令,我們的輔助插件也就大功告成了。
當(dāng)我們對(duì)遠(yuǎn)程倉庫進(jìn)行push時(shí),Git 發(fā)布了這個(gè)export命令。通過標(biāo)準(zhǔn)輸入stdin發(fā)送這個(gè)命令后,git將通過由git fast-export提供的流來追蹤,而與git fast-export對(duì)應(yīng)的是可以向遠(yuǎn)程倉庫操縱的git fast-import命令。
- if test -n "$GIT_REMOTE_TESTGIT_FAILURE"then
- # consume input so fast-export doesn't get SIGPIPE;
- # git would also notice that case, but we want
- # to make sure we are exercising the later
- # error checks
- while read line; do test "done" = "$line" && break done exit 1fibefore=$(git for-each-ref --format=' %(refname) %(objectname) ')git fast-import \
- ${force:+--force} \
- ${testgitmarks:+"--import-marks=$testgitmarks"} \
- ${testgitmarks:+"--export-marks=$testgitmarks"} \
- --quiet# figure out which refs were updatedgit for-each-ref --format='%(refname) %(objectname)' |while read ref ado case "$before" in
- *" $ref $a "*)
- continue ;; # unchanged
- esac if test -z "$GIT_REMOTE_TESTGIT_PUSH_ERROR"
- then echo "ok $ref"
- else echo "error $ref $GIT_REMOTE_TESTGIT_PUSH_ERROR"
- fidoneecho
***行的if語句,和前面的一樣,僅僅是為了測(cè)試的目的而已。
再下一行更有意思。它創(chuàng)建了一個(gè)以空格分割的列表,且這個(gè)列表是以$refname $objectname對(duì) 來表示我們決定哪些將要在import中被更新ref。
再接下來的命令則相當(dāng)具有解釋性。git fast-import工作于我們接收到的標(biāo)準(zhǔn)輸入流,–forece參數(shù)表示是否特定,–quiet,以及遠(yuǎn)程的marks標(biāo)記文件。
在這之下再次運(yùn)行了git for-each-ref來檢測(cè)refs有什么變化。對(duì)于這個(gè)命令返回的每一個(gè)ref,都會(huì)檢測(cè)$refname $objectname對(duì)是否出現(xiàn)在$before列表里面。如果是,說明沒什么變化并且繼續(xù)進(jìn)行下一步。然而如果ref不存這個(gè)$before列表中, 將會(huì)打包輸出ok $refname以告知git對(duì)應(yīng)的ref被成功更新了。如果打印error $refname $message則是通知git對(duì)應(yīng)的ref在遠(yuǎn)程終端導(dǎo)入失敗。
***,打印的一個(gè)空行表明導(dǎo)入完畢。
現(xiàn)在我們可以自己編寫這些代碼了。我們可以使用我們之前定義的GitListRefs()方法。
- case command == "export\n":
- beforeRefs, err := GitListRefs()
- if err != nil {
- return fmt.Errorf("command export: collecting before refs: %v", err)
- }
- cmd := exec.Command("git", "fast-import", "--quiet",
- "--import-marks="+gomarks,
- "--export-marks="+gomarks)
- cmd.Stderr = os.Stderr
- cmd.Stdin = os.Stdin
- if err := cmd.Run(); err != nil {
- return fmt.Errorf("command export: git fast-import: %v", err)
- }
- afterRefs, err := GitListRefs()
- if err != nil {
- return fmt.Errorf("command export: collecting after refs: %v", err)
- }
- for refname, objectname := range afterRefs {
- if beforeRefs[refname] != objectname {
- fmt.Printf("ok %s\n", refname)
- }
- }
- fmt.Printf("\n")
小牛試刀
執(zhí)行 go install,應(yīng)該能夠構(gòu)建和安裝 git-remote-go 到 go/bin。
你可以這樣來測(cè)試驗(yàn)證:首先創(chuàng)建兩個(gè)空的git倉庫,然后在testlocal中commit一個(gè)提交,并通過我們新的輔助插件helper把它push到testremote。
- $ cd $HOME
- $ git init testremote
- Initialized empty Git repository in $HOME/testremote/.git/
- $ git init testlocal
- Initialized empty Git repository in $HOME/testlocal/.git/
- $ cd testlocal
- $ echo 'Hello, world!' >hello.txt
- $ git add hello.txt
- $ git commit -m "First commit."
- [master (root-commit) 50d3a83] First commit.
- 1 file changed, 1 insertion(+)
- create mode 100644 hello.txt
- $ git remote add origin go::$HOME/testremote
- $ git push --all origin
- To go::$HOME/testremote
- * [new branch] master -> master
- $ cd ../testremote
- $ git checkout master
- $ ls
- hello.txt
- $ cat hello.txt
- Hello, world!
git 遠(yuǎn)程輔助插件的使用
實(shí)現(xiàn)接口后,Git 遠(yuǎn)程輔助插件可以用于其他的源控制(如 felipec/git-remote-hg),或者推送代碼到 CouchDBs (peritus/git-remote-couch), 等等其他。你也可以想象更多其他可能的用處。
出于我最初的動(dòng)機(jī),我寫了一個(gè)git遠(yuǎn)程輔助插件git-remote-grave。你可以使用它來push和fetch你文件系統(tǒng)上或者經(jīng)過HTTP/HTTPS協(xié)議的加密檔案文檔。
$ git remote add usb grave::/media/usb/backup.grave
$ git push --all backup
使用兩種壓縮技巧,可以讓檔案文檔的大小通??s小為原來的22%。
如果你想要一個(gè)便利的地方去存放你加密后的git倉庫,可以訪問我創(chuàng)建的這個(gè)站點(diǎn): filegrave.com 。
此文章的討論交流部分放置在 Hacker News 和 /r/programming。