曾几何时,我在持续追踪自己的文件方面遇到一些问题。通常,我忘了自己是否将文件保存在自己的桌面电脑、笔记本电脑或者电话上,或者保存在了云上的什么地方。更有甚者,对非常重要的信息,像密码和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 originhttp://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. return nil
  8. } else if 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. return nil
  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. return nil, 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. return string(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. while true
  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. exit 1
  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. exit 1
  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%。

本文来自云栖社区合作伙伴“Linux中国”,原文发布日期:2015-08-21

如何编写一个全新的 Git 协议相关推荐

  1. http服务器响应格式,熟悉Http协议的请求和响应格式,编写一个简单的Http服务器。 基本要求:1 正确解...

    熟悉Http协议的请求和响应格式,编写一个简单的Http服务器. 基本要求: 1 正确解 2016-08-23 0 0 0 暂无评分 其他 1 积分下载 如何获取积分? 熟悉Http协议的请求和响应格 ...

  2. 深入浅出Git教程+一个小时学会Git(转载)(堪称完美)

    转载自: https://www.cnblogs.com/best/p/7474442.html#!comments 目录 一个小时学会Git 一.版本控制概要 工作区 暂存区 本地仓库 远程仓库 1 ...

  3. git缓冲区查看_git原理学习记录:从基本指令到背后原理,实现一个简单的git

    好家伙~ 实操可以考虑点击阅读原文跳转到博客地址,博客可以点超链接可能会方便一些. 一开始我还担心 git 的原理会不会很难懂,但在阅读了官方文档后我发现其实并不难懂,似乎可以动手实现一个简单的 gi ...

  4. 实战!手把手教你如何编写一个Linux驱动并写一个支持物联网的LED演示demo

    目录 一.开发环境 二. 准备工作: 1. 创建一个项目工程目录 2. 创建输出与目标目录 3.头文件目录 4. 建立源代码src目录 5. 使用git管理你的项目 三.编写LED驱动 三.一 准备工 ...

  5. git原理学习记录:从基本指令到背后原理,实现一个简单的git

    一开始我还担心 git 的原理会不会很难懂,但在阅读了官方文档后我发现其实并不难懂,似乎可以动手实现一个简单的 git,于是就有了下面这篇学习记录. 本文的叙述思路参照了官方文档Book的原理介绍部分 ...

  6. python输入10个整数_python练习:编写一个程序,要求用户输入10个整数,然后输出其中最大的奇数,如果用户没有输入奇数,则输出一个消息进行说明。...

    python练习:编写一个程序,要求用户输入10个整数,然后输出其中最大的奇数,如果用户没有输入奇数,则输出一个消息进行说明. 重难点:通过input函数输入的行消息为字符串格式,必须转换为整型,否则 ...

  7. 编写一个GStreamer插件

    前面章节对GStreamer做了概述,不过我们最终用到主要是插件,下面我们对插件做一个简单介绍,大部分内容都是copy的,并非原创,主要用于学习记录,英文好的可以看官方文档,我和官方校对过,翻译的大体 ...

  8. python写一个文件下载器_Python3使用TCP编写一个简易的文件下载器

    原标题:Python3使用TCP编写一个简易的文件下载器 利用Python3来实现TCP协议,和UDP类似.UDP应用于及时通信,而TCP协议用来传送文件.命令等操作,因为这些数据不允许丢失,否则会造 ...

  9. 5G技术与触觉互联网,一个全新的世界

    如今互联网满足着人们的视听需求.假使互联网能够服务人类另一种感官--触觉,那将是什么样的体验?随着移动宽带的数据速率与日俱增,身处前沿的科学家们正开始构建触觉互联网. 时间刚过下午3点,印度尼西亚的大 ...

最新文章

  1. [WCF] - Odata Service 访问失败,查看具体错误信息的方法
  2. 第一次使用最新开发的在线编辑器讲课记录笔记
  3. EJB3.0高速入门项目开发步骤
  4. 一文搞懂Spring Cloud Zuul
  5. 二进制数组操作的数组维度必须匹配_Testbench编写指南(2)文件的读写操作
  6. [置顶] woff格式字体怎么打开和编辑?
  7. Python批量下载中国大学MOOC课件
  8. 冒险岛mysql破解_冒险岛079浩浩2020年3月年度一键端版
  9. Mov文件字幕添加与播放
  10. [AV1] interpolation
  11. mumu模拟器网络问题相关处理
  12. Tomcat一些常见错误(遇到错误就更新)
  13. VSCODE 使用One Dark Pro并优化
  14. 自定义控件 流式布局
  15. java实验报告:实验一 基于控制台的购书系统
  16. 【Cheatsheet】收录英文邮件的写作技巧(比较系统、为后续邮件写作提供参考)
  17. vue 渲染的list 数据交换顺序,简单就可以实现动画效果
  18. 鲁棒优化入门(一)——工具箱Xprog和RSOME的安装与使用
  19. 计算机网络三级考试分数划分,计算机等级考试三级网络技术题型分布
  20. 【转】 SCM工具对比分析

热门文章

  1. Linux服务器信息检测Shell脚本
  2. 当技术面试官的一些心得
  3. 谈谈学习AS3的过程
  4. pytest框架安装(MacOS)
  5. doe报告模板_技术漫谈|关于制剂研发过程中的实验设计(DOE)误区讨论
  6. 多线程及相关面试题与拓展
  7. Xamarin Essentials教程设备信息DeviceInfo
  8. Visual Studio 2017 版本 15.5.5
  9. iOS10 UI教程视图和子视图的可见性
  10. 如何吸收分数c语言,用C语言编程平均分数