歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux基礎 >> Linux教程 >> 如何編寫一個全新的 Git 協議

如何編寫一個全新的 Git 協議

日期:2017/2/28 13:58:44   编辑:Linux教程

曾幾何時,我在持續追蹤自己的文件方面遇到一些問題。通常,我忘了自己是否將文件保存在自己的桌面電腦、筆記本電腦或者電話上,或者保存在了雲上的什麼地方。更有甚者,對非常重要的信息,像密碼和Bitcoin的密匙,僅以純文本郵件的形式將它發送給自己讓我芒刺在背。

我需要的是將自己的數據存放一個git倉庫裡,然後將這個git倉庫保存在一個地方。我可以查看以前的版本而且不用提心數據被刪除。更最要的是,我已經能熟練地在不同電腦上使用git來上傳和下載文件。

但是,如我所言,我並不想簡單地上傳我的密匙和密碼到GitHub或者BitBucket,哪怕是其中的私有倉庫。

一個很酷的想法在我腦中升騰:寫一個工具來加密我的倉庫,然後再將它Push到Backup。遺憾的是,不能像平時那樣使用 git push命令,需要使用像這樣的命令:

  1. $ encrypted-git push http://example.com/

至少,在我發現git-remote-helpers以前是這樣想的。

Git remote helpers

我在網上找到一篇git remote helpers的文檔。

原來,如果你運行命令

  1. $ git remote add origin asdf://example.com/repo
  2. $ git push --all origin

Git會首先檢查是否內建了asdf協議,當發現沒有內建時,它會檢查git-remote-asdf是否在PATH(環境變量)裡,如果在,它會運行 git-remote-asdf origin asdf://example.com/repo 來處理本次會話。

同樣的,你可以運行

  1. $ git clone asdf::http://example.com/repo

很遺憾的是,我發現文檔在真正實現一個helper的細節上語焉不詳,而這正是我需要的。但是隨後,我在Git源碼中找到了一個叫git-remote-testgit.sh的腳本,它實現了一個用來測試git遠程輔助系統的testgit。 它基本實現了從同樣文件系統的本地倉庫推送和抓取功能。所以來讓git調用 git-remote-asdf origin http://example.com/repo。

  1. git clone testgit::/existing-repository

  1. git clone /existing-repository

就一樣了。

同樣地,你可以透過testgit協議向本地倉庫中推送或者從中抓取。

在本文件中,我們將浏覽git-remote-testgit的源碼並以Go語言實現一個全新的helper分支: git-remote-go。過程中,我將解釋源碼的意思,以及在實現我自己的remote helper(git-remote-grave)中領悟到的種種.

基礎知識

為了後面的章節理解方面,讓我們先學習一些術語和基本機制。

當我們運行

  1. $ git remote add myremote go::http://example.com/repo
  2. $ git push myremote master

Git會運行以下命令來實例化一個新的進程

  1. git-remote-go myremote http://example.com/repo

注意:第一個參數是remote name,第二個參數是url.

當你運行

  1. $ git clone go::http://example.com/repo

下一條命令會實例化helper

  1. git-remote-go origin http://example.com/repo

因為遠程origin會自動在克隆的倉庫中自動創建。

當Git以一個新的進程實例化helper時,它會為 stdin,stdout及stderr通信打開管道。命令被通過stdin送達helper,helper通過stdout響應。任何helper在stderr上的輸出被重定向到git的stderr(它可能是一個終端)。

下圖說明了這種關系:

我需要說明的最後一點是如何區分本地和遠程倉庫。通常(但不是每一次),本地倉庫是我們運行git的地方,遠程倉庫是我們需要連接的。

所以在push中,我們從本地倉庫發送更改(的地方)到遠程倉庫。在Fetch中,我們從遠程倉庫抓取更改(的地方)到本地倉庫。在Clone中,我們將遠程倉庫克隆到本地。

當git運行helper時,git將環境變量GIT_DIR設置為本地倉庫的Git目錄(比如:local/.git)。

項目開搞

在這篇文章中,我假設已經安裝好Go語言,並且使用了環境變量$GOPATH指向一個為go的目錄。

讓我們以創建目錄go/src/git-remote-go開始。這樣的話我們就可以通過運行go install來安裝我們的插件(假設go/bin在PATH中)。

在意識裡面有了這一點後,我們可以編寫go/src/git-remote-go/main.go最初的幾行代碼。

  1. package main
  2. import(
  3. "log"
  4. "os"
  5. )
  6. func Main()(er error){
  7. if len(os.Args)<3{
  8. return fmt.Errorf("Usage: git-remote-go remote-name url")
  9. }
  10. remoteName := os.Args[1]
  11. url := os.Args[2]
  12. }
  13. func main(){
  14. if err :=Main(); err !=nil{
  15. log.Fatal(err)
  16. }
  17. }

我將Main()分割了開來,因為當我們需要返回錯誤時錯誤處理將會變得更容易。這裡我們也可以使用defet,因為log.Fatal調用了os.Exit但不調用defer裡面的函數。

現在,讓我們看下git-remote-testgit文件的最頂部,看下接下來需要做什麼。

  1. #!/bin/sh
  2. # Copyright (c) 2012 Felipe Contreras
  3. alias=$1
  4. url=$2
  5. dir="$GIT_DIR/testgit/$alias"
  6. prefix="refs/testgit/$alias"
  7. default_refspec="refs/heads/*:${prefix}/heads/*"
  8. refspec="${GIT_REMOTE_TESTGIT_REFSPEC-$default_refspec}"
  9. test -z "$refspec"&& prefix="refs"
  10. GIT_DIR="$url/.git"
  11. export GIT_DIR
  12. force=
  13. mkdir -p "$dir"
  14. if test -z "$GIT_REMOTE_TESTGIT_NO_MARKS"
  15. then
  16. gitmarks="$dir/git.marks"
  17. testgitmarks="$dir/testgit.marks"
  18. test -e "$gitmarks"||>"$gitmarks"
  19. test -e "$testgitmarks"||>"$testgitmarks"
  20. fi

他們稱之為alias的變量就是我們所說的remoteName。url則是同樣的意義。

下一個聲明是:

  1. dir="$GIT_DIR/testgit/$alias"

這裡在Git目錄下創建了一個命名空間以標識testgit協議和我們正在使用的遠程路徑。通過這樣,testgit下面origin分支下的文件就能與backup分支下面的文件區分開來。

再下面,我們看到這樣的聲明:

  1. mkdir -p "$dir"

此處確保了本地目錄已被創建,如果不存在則創建。

讓我們為我們的Go程序添加本地目錄的創建。

  1. // Add "path" to the import list
  2. localdir := path.Join(os.Getenv("GIT_DIR"),"go", remoteName)
  3. if err := os.MkdirAll(localdir,0755); err !=nil{
  4. return err
  5. }

緊接著上面的腳本,我們有以下幾行:

  1. prefix="refs/testgit/$alias"
  2. default_refspec="refs/heads/*:${prefix}/heads/*"
  3. refspec="${GIT_REMOTE_TESTGIT_REFSPEC-$default_refspec}"
  4. test -z "$refspec"&& prefix="refs"

這裡快速談論一下refs。

在git中,refs存放在.git/refs:

  1. .git
  2. └── refs
  3. ├── heads
  4. │└── master
  5. ├── remotes
  6. │├── gravy
  7. │└── origin
  8. │└── master
  9. └── tags

在上面的樹中,remotes/origin/master包括了遠程origin中mater分支下最近大量的提交。而heads/master則關聯你本地mater分支下最近大量的提交。一個ref就像一個指向一次提交的指針。

refspec則可以讓我把遠程的refs的本地的refs映射起來。在上面的代碼中,prefix就是會被遠程refs保留的目錄。如果遠程的名稱是原始的,那麼遠程master分支將會由.git/refs/testgit/origin/master所指定。這樣就很基本地為遠程的分支創建了指定協議的命名空間。

接下來的這一行則是refspec。這一行

  1. default_refspec="refs/heads/*:${prefix}/heads/*"

可以擴展成

  1. default_refspec="refs/heads/*:refs/testgit/$alias/*"

這意味著遠程分支的映射看起來就像把refs/heads/*(這裡的*表示任意文本)對應到refs/testgit/$alias/*(這裡的*將會被前面的*表示的文本替換)。例如,refs/heads/master將會映射到refs/testgit/origin/master。

基本上來講,refspec允許testgit添加一個新的分支到自己的樹中,例如這樣:

  1. .git
  2. └── refs
  3. ├── heads
  4. │└── master
  5. ├── remotes
  6. │└── origin
  7. │└── master
  8. ├── testgit
  9. │└── origin
  10. │└── master
  11. └── tags

下一行

  1. refspec="${GIT_REMOTE_TESTGIT_REFSPEC-$default_refspec}"

把$refspec設置成$GIT_REMOTE_TESTGIT_REFSPEC,除非它不存在,否則它會成為$default_refspec。這樣的話就能通過testgit測試其他的refspecs了。我們假設都已經成功設置了$default_refspec。

最後,再下一行,

  1. test -z "$refspec"&& prefix="refs"

按照我們的理解,看起來像是如果$GIT_REMOTE_TESTGIT_REFSPEC存在卻為空時則把$prefix設置成refs。

我們需要自己的refspec,所以需要添加這一行

  1. refspec := fmt.Sprintf("refs/heads/*:refs/go/%s/*", remoteName)

緊隨上面的代碼,我們看到了

  1. GIT_DIR="$url/.git"
  2. export GIT_DIR

關於$GIT_DIR的另一個事實就是如果它有在環境變量中設置,那麼底層的git將會使用環境變量中$GIT_DIR的目錄作為它的.git目錄,而不再是本地目錄的.git。這個命令使得未來全部插件的git命令都能在遠程制品庫的上下文中執行。

我們把這點轉換成

  1. if err := os.Setenv("GIT_DIR", path.Join(url,".git")); err !=nil{
  2. return err
  3. }

當然請記住,那個$dir和我們變量中的localdir依然指向我們正在fetch或push的子目錄。

main塊裡面還有一小段代碼

  1. if test -z "$GIT_REMOTE_TESTGIT_NO_MARKS"
  2. then
  3. gitmarks="$dir/git.marks"
  4. testgitmarks="$dir/testgit.marks"
  5. test -e "$gitmarks"||>"$gitmarks"
  6. test -e "$testgitmarks"||>"$testgitmarks"
  7. fi

按我們的理解是,如果$GIT_REMOTE_TESTGIT_NO_MARKS未設置,if語句中的內容將會被執行。

這些標識文件可以紀錄像git fast-export和git fast-import這些傳遞過程中ref和blob的有關信息。有一點是非常重要的,即這些標識在各式各樣的插件中都是一樣的,所以他們都是保存在localdir中。

這裡,$gitmarks關聯著我們本地制品庫中git寫入的標識,$testgitmarks則保存遠程處理寫入的標識。

下面這兩行有點像touch的使用,如果標識文件不存在,則創建一個空的。

  1. test -e "$gitmarks"||>"$gitmarks"
  2. test -e "$testgitmarks"||>"$testgitmarks"

我們自己的程序中需要這些文件,所以讓我們以編寫一個Touch函數開始。

  1. // Create path as an empty file if it doesn't exist, otherwise do nothing.
  2. // This works by opening a file in exclusive mode; if it already exists,
  3. // an error will be returned rather than truncating it.
  4. func Touch(path string) error {
  5. file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL,0666)
  6. if os.IsExist(err){
  7. returnnil
  8. }elseif err !=nil{
  9. return err
  10. }
  11. return file.Close()
  12. }

現在我們可以創建標識文件了。

  1. gitmarks := path.Join(localdir,"git.marks")
  2. gomarks := path.Join(localdir,"go.marks")
  3. if err :=Touch(gitmarks); err !=nil{
  4. return err
  5. }
  6. if err :=Touch(gomarks); err !=nil{
  7. return err
  8. }

然後,我遇到的一個問題就是,如果因為某些原因而導致插件失敗的話,這些標識文件將會處於殘留在一個無效的狀態。為了預防這一點,我們可以先保存文件的原始內容,並且如果Main()函數返回一個錯誤的話我們就重寫他們。

  1. // add "io/ioutil" to imports
  2. originalGitmarks, err := ioutil.ReadFile(gitmarks)
  3. if err !=nil{
  4. return err
  5. }
  6. originalGomarks, err := ioutil.ReadFile(gomarks)
  7. if err !=nil{
  8. return err
  9. }
  10. defer func(){
  11. if er !=nil{
  12. ioutil.WriteFile(gitmarks, originalGitmarks,0666)
  13. ioutil.WriteFile(gomarks, originalGomarks,0666)
  14. }
  15. }()

最後我們可以從關鍵命令操作開始。

命令行通過標准輸入流stdin傳遞到插件,也就是每一條命令是以回車結尾和一個字符串。插件則通過標准輸出流stdout對命令作出響應;標准錯誤流stderr則通過管道輸出給終端用戶。

下面來編寫我們自己的命令操作。

  1. // Add "bufio" to import list.
  2. stdinReader := bufio.NewReader(os.Stdin)
  3. for{
  4. // Note that command will include the trailing newline.
  5. command, err := stdinReader.ReadString('\n')
  6. if err !=nil{
  7. return err
  8. }
  9. switch{
  10. case command =="capabilities\n":
  11. // ...
  12. case command =="\n":
  13. returnnil
  14. default:
  15. return fmt.Errorf("Received unknown command %q", command)
  16. }
  17. }

capabilities 命令

第一條有待實現的命令是capabilities。插件要求能以空行結尾並以行分割的形式輸出顯示它能提供的命令和它所支持的操作。

  1. echo 'import'
  2. echo 'export'
  3. test -n "$refspec"&& echo "refspec $refspec"
  4. if test -n "$gitmarks"
  5. then
  6. echo "*import-marks $gitmarks"
  7. echo "*export-marks $gitmarks"
  8. fi
  9. test -n "$GIT_REMOTE_TESTGIT_SIGNED_TAGS"&& echo "signed-tags"
  10. test -n "$GIT_REMOTE_TESTGIT_NO_PRIVATE_UPDATE"&& echo "no-private-update"
  11. echo 'option'
  12. echo

上面使用列表中聲明了此插件支持import,import和option命令操作。option命令允許git改變我們的插件中冗長的部分。

signed-tags意味著當git為export命令創建了一個快速導入的流時,它將會把--signed-tags=verbatim傳遞給git-fast-export。

no-private-update則指示著git不需要更新私有的ref當它被成功push後。我未曾看到有需要用到這個特性。

refspec $refspec用於告訴git我們需要使用哪個refspec。

*import-marks $gitmarks和*export-marks $gitmarks意思是git應該保存它生成的標識到gitmarks文件中。*號表示如果git不能識別這幾行,它必須失敗返回而不是忽略他們。這是因為插件依賴於所保存的標識文件,並且不能和git不支持的版本一起工作。

我們先忽略signed-tags,no-private-update和option,因為它們用於在git-remote-testgit未完成的測試,並且在我們這個例子中也不需要這些。我們可以這樣簡單地實現上面這些,如:

  1. case command =="capabilities\n":
  2. fmt.Printf("import\n")
  3. fmt.Printf("export\n")
  4. fmt.Printf("refspec %s\n", refspec)
  5. fmt.Printf("*import-marks %s\n", gitmarks)
  6. fmt.Printf("*export-marks %s\n", gitmarks)
  7. fmt.Printf("\n")

list命令

下一個命令是list。這個命令的使用說明並沒有包括在capabilities命令輸出的使用說明列表中,是因為它通常都是插件所必須支持的。

當插件接收到一個list命令時,它應該打印輸出遠程制品庫上的ref,並每行以$objectname $refname這樣的格式用一系列的行來表示,並且最後跟著一行空行。$refname對應著ref的名稱,$objectname則是ref指向的內容。$objectname可以是一次提交的哈希,或者用@$refname表示指向另外一個ref,或者是用?表示ref的值不可獲得。

git-remote-testgit的實現如下。

  1. git for-each-ref--format='? %(refname)''refs/heads/'
  2. head=$(git symbolic-ref HEAD)
  3. echo "@$head HEAD"
  4. echo

記住,$GIT_DIR將觸發git for-each-ref在遠程制品庫的執行,並將會為每一個分支打印一行? $refname,同時還有@$head HEAD,這裡的$head即為指向制品庫HEAD的ref的名稱。

在一個常規的制品庫裡一般會有兩個分支,即master主分支和dev開發分支,這樣的話上面的輸出可能就像這樣

  1. ? refs/heads/master
  2. ? refs/heads/development
  3. @refs/heads/master HEAD
  4. <blank>

現在讓我們自己來寫這些。先寫一個GitListRefs()函數,因為我們稍候會再次用到。

  1. // Add "os/exec" and "bytes" to the import list.
  2. // Returns a map of refnames to objectnames.
  3. func GitListRefs()(map[string]string, error){
  4. out, err :=exec.Command(
  5. "git","for-each-ref","--format=%(objectname) %(refname)",
  6. "refs/heads/",
  7. ).Output()
  8. if err !=nil{
  9. returnnil, err
  10. }
  11. lines := bytes.Split(out,[]byte{'\n'})
  12. refs := make(map[string]string, len(lines))
  13. for _, line := range lines {
  14. fields := bytes.Split(line,[]byte{' '})
  15. if len(fields)<2{
  16. break
  17. }
  18. refs[string(fields[1])]=string(fields[0])
  19. }
  20. return refs,nil
  21. }

現在編寫GitSymbolicRef()。

  1. func GitSymbolicRef(name string)(string, error){
  2. out, err :=exec.Command("git","symbolic-ref", name).Output()
  3. if err !=nil{
  4. return"", fmt.Errorf(
  5. "GitSymbolicRef: git symbolic-ref %s: %v", name,out, err)
  6. }
  7. returnstring(bytes.TrimSpace(out)),nil
  8. }

然後可以像這樣來實現list命令。

  1. case command =="list\n":
  2. refs, err :=GitListRefs()
  3. if err !=nil{
  4. return fmt.Errorf("command list: %v", err)
  5. }
  6. head, err :=GitSymbolicRef("HEAD")
  7. if err !=nil{
  8. return fmt.Errorf("command list: %v", err)
  9. }
  10. for refname := range refs {
  11. fmt.Printf("? %s\n", refname)
  12. }
  13. fmt.Printf("@%s HEAD\n", head)
  14. fmt.Printf("\n")

import 命令

下一步是git在fetch或clone時會用到的import命令。這個命令實際來源於batch:它把import $refname作為一系列的行並用一個空行結束來發送。當git將此命令發送到輔助插件時,它將以二進制形式執行git fast-import,並且通過管道將標准輸出stdout和標准輸入stdin綁定起來。換句話說,輔助插件期望能在標准輸出stdout上返回一個git fast-export流。

讓我們看下git-remote-testgit的實現。

  1. # read all import lines
  2. whiletrue
  3. do
  4. ref="${line#* }"
  5. refs="$refs $ref"
  6. read line
  7. test "${line%% *}"!="import"&&break
  8. done
  9. if test -n "$gitmarks"
  10. then
  11. echo "feature import-marks=$gitmarks"
  12. echo "feature export-marks=$gitmarks"
  13. fi
  14. if test -n "$GIT_REMOTE_TESTGIT_FAILURE"
  15. then
  16. echo "feature done"
  17. exit1
  18. fi
  19. echo "feature done"
  20. git fast-export \
  21. ${testgitmarks:+"--import-marks=$testgitmarks"} \
  22. ${testgitmarks:+"--export-marks=$testgitmarks"} \
  23. $refs |
  24. sed -e "s#refs/heads/#${prefix}/heads/#g"
  25. echo "done"

最頂部的循環,正如注釋所說的,將全部的import $refname命令匯總到一個單一的變量$refs中,而$refs則是以空格分隔的列表。

接下來的,如果腳本正在使用gitmarks文件(假設是這樣),將會輸出feature import-marks=$gitmarks和feature export-marks=$gitmarks。這裡告訴git需要把--import-marks=$gitmarks和--export-marks=$gitmarks傳遞給git fast-import。

再下一行中,如果出於測試目的設置了$GIT_REMOTE_TESTGIT_FAILURE,插件將會失敗。

在那以後,feature done將會輸出,暗示著將緊跟輸出導出的流內容。

最後,git fast-export在遠程制品庫被調用,在遠程標識上設置指定的標識文件以及$testgitmarks,然後返回我們需要導出的ref列表。

git-fast-export命令的輸出內容,通過管道經過將refs/heads/匹配到refs/testgit/$alias/heads/的sed命令。因此在export導出時,我們傳遞給git的refspec將能很好的使用這個匹配映射。

在導出流後面,緊跟done輸出。

我們可以用go來嘗試一下。

  1. case strings.HasPrefix(command,"import "):
  2. refs := make([]string,0)
  3. for{
  4. // Have to make sure to trim the trailing newline.
  5. ref:= strings.TrimSpace(strings.TrimPrefix(command,"import "))
  6. refs = append(refs,ref)
  7. command, err = stdinReader.ReadString('\n')
  8. if err !=nil{
  9. return err
  10. }
  11. if!strings.HasPrefix(command,"import "){
  12. break
  13. }
  14. }
  15. fmt.Printf("feature import-marks=%s\n", gitmarks)
  16. fmt.Printf("feature export-marks=%s\n", gitmarks)
  17. fmt.Printf("feature done\n")
  18. args :=[]string{
  19. "fast-export",
  20. "--import-marks", gomarks,
  21. "--export-marks", gomarks,
  22. "--refspec", refspec}
  23. args = append(args, refs...)
  24. cmd :=exec.Command("git", args...)
  25. cmd.Stderr= os.Stderr
  26. cmd.Stdout= os.Stdout
  27. if err := cmd.Run(); err !=nil{
  28. return fmt.Errorf("command import: git fast-export: %v", err)
  29. }
  30. fmt.Printf("done\n")

export命令

下一步是export命令。當我們完成了這個命令,我們的輔助插件也就大功告成了。

當我們對遠程倉庫進行push時,Git 發布了這個export命令。通過標准輸入stdin發送這個命令後,git將通過由git fast-export提供的流來追蹤,而與git fast-export對應的是可以向遠程倉庫操縱的git fast-import命令。

  1. if test -n "$GIT_REMOTE_TESTGIT_FAILURE"
  2. then
  3. # consume input so fast-export doesn't get SIGPIPE;
  4. # git would also notice that case, but we want
  5. # to make sure we are exercising the later
  6. # error checks
  7. while read line;do
  8. test "done"="$line"&&break
  9. done
  10. exit1
  11. fi
  12. before=$(git for-each-ref--format=' %(refname) %(objectname) ')
  13. git fast-import \
  14. ${force:+--force} \
  15. ${testgitmarks:+"--import-marks=$testgitmarks"} \
  16. ${testgitmarks:+"--export-marks=$testgitmarks"} \
  17. --quiet
  18. # figure out which refs were updated
  19. git for-each-ref--format='%(refname) %(objectname)'|
  20. while read ref a
  21. do
  22. case"$before"in
  23. *" $ref $a "*)
  24. continue;;# unchanged
  25. esac
  26. if test -z "$GIT_REMOTE_TESTGIT_PUSH_ERROR"
  27. then
  28. echo "ok $ref"
  29. else
  30. echo "error $ref $GIT_REMOTE_TESTGIT_PUSH_ERROR"
  31. fi
  32. done
  33. echo

第一行的if語句,和前面的一樣,僅僅是為了測試的目的而已。

再下一行更有意思。它創建了一個以空格分割的列表,且這個列表是以$refname $objectname對 來表示我們決定哪些將要在import中被更新ref。

再接下來的命令則相當具有解釋性。git fast-import工作於我們接收到的標准輸入流,--forece參數表示是否特定,--quiet,以及遠程的marks標記文件。

在這之下再次運行了git for-each-ref來檢測refs有什麼變化。對於這個命令返回的每一個ref,都會檢測$refname $objectname對是否出現在$before列表裡面。如果是,說明沒什麼變化並且繼續進行下一步。然而如果ref不存這個$before列表中,將會打包輸出ok $refname以告知git對應的ref被成功更新了。如果打印error $refname $message則是通知git對應的ref在遠程終端導入失敗。

最後,打印的一個空行表明導入完畢。

現在我們可以自己編寫這些代碼了。我們可以使用我們之前定義的GitListRefs()方法。

  1. case command =="export\n":
  2. beforeRefs, err :=GitListRefs()
  3. if err !=nil{
  4. return fmt.Errorf("command export: collecting before refs: %v", err)
  5. }
  6. cmd :=exec.Command("git","fast-import","--quiet",
  7. "--import-marks="+gomarks,
  8. "--export-marks="+gomarks)
  9. cmd.Stderr= os.Stderr
  10. cmd.Stdin= os.Stdin
  11. if err := cmd.Run(); err !=nil{
  12. return fmt.Errorf("command export: git fast-import: %v", err)
  13. }
  14. afterRefs, err :=GitListRefs()
  15. if err !=nil{
  16. return fmt.Errorf("command export: collecting after refs: %v", err)
  17. }
  18. for refname, objectname := range afterRefs {
  19. if beforeRefs[refname]!= objectname {
  20. fmt.Printf("ok %s\n", refname)
  21. }
  22. }
  23. fmt.Printf("\n")

牛刀小試

執行 go install,應該能夠構建和安裝 git-remote-go 到 go/bin。

你可以這樣來測試驗證:首先創建兩個空的git倉庫,然後在testlocal中commit一個提交,並通過我們新的輔助插件helper把它push到testremote。

  1. $ cd $HOME
  2. $ git init testremote
  3. Initialized empty Git repository in $HOME/testremote/.git/
  4. $ git init testlocal
  5. Initialized empty Git repository in $HOME/testlocal/.git/
  6. $ cd testlocal
  7. $ echo 'Hello, world!'>hello.txt
  8. $ git add hello.txt
  9. $ git commit -m "First commit."
  10. [master (root-commit)50d3a83]First commit.
  11. 1 file changed,1 insertion(+)
  12. create mode 100644 hello.txt
  13. $ git remote add origin go::$HOME/testremote
  14. $ git push --all origin
  15. To go::$HOME/testremote
  16. *[new branch] master -> master
  17. $ cd ../testremote
  18. $ git checkout master
  19. $ ls
  20. hello.txt
  21. $ cat hello.txt
  22. Hello, world!

git 遠程輔助插件的使用

實現接口後,Git 遠程輔助插件可以用於其他的源控制(如 felipec/git-remote-hg),或者推送代碼到 CouchDBs (peritus/git-remote-couch), 等等其他。你也可以想象更多其他可能的用處。

出於我最初的動機,我寫了一個git遠程輔助插件git-remote-grave。你可以使用它來push和fetch你文件系統上或者經過HTTP/HTTPS協議的加密檔案文檔。

  1. $ git remote add usb grave::/media/usb/backup.grave
  2. $ git push --all backup

使用兩種壓縮技巧,可以讓檔案文檔的大小通常縮小為原來的22%。

如果你想要一個便利的地方去存放你加密後的git倉庫,可以訪問我創建的這個站點: filegrave.com 。

此文章的討論交流部分放置在 Hacker News 和 /r/programming。

GitHub 教程系列文章

GitHub 使用教程圖文詳解 http://www.linuxidc.com/Linux/2014-09/106230.htm

Git 標簽管理詳解 http://www.linuxidc.com/Linux/2014-09/106231.htm

Git 分支管理詳解 http://www.linuxidc.com/Linux/2014-09/106232.htm

Git 遠程倉庫詳解 http://www.linuxidc.com/Linux/2014-09/106233.htm

Git 本地倉庫(Repository)詳解 http://www.linuxidc.com/Linux/2014-09/106234.htm

Git 服務器搭建與客戶端安裝 http://www.linuxidc.com/Linux/2014-05/101830.htm

Git 概述 http://www.linuxidc.com/Linux/2014-05/101829.htm

分享實用的GitHub 使用教程 http://www.linuxidc.com/Linux/2014-04/100556.htm

Ubuntu下Git服務器的搭建與使用指南 http://www.linuxidc.com/Linux/2015-07/120617.htm

Git 的詳細介紹:請點這裡
Git 的下載地址:請點這裡

Copyright © Linux教程網 All Rights Reserved