├── LICENSE ├── ref.go ├── runner.go ├── capabilities.go ├── example ├── peernet │ └── peer.go └── cmd │ ├── git-ws-server │ └── main.go │ └── git-remote-wstest │ └── main.go ├── commands.go └── command_parser.go /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Simon Menke 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /ref.go: -------------------------------------------------------------------------------- 1 | package gitremote 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | ) 7 | 8 | type ListRef struct { 9 | Name string 10 | 11 | Hash string // sha1 hash value 12 | Sym string // @ for symref 13 | // When Hash and Sym are blank the is '?' 14 | 15 | Unchanged bool // unchanged attribute 16 | } 17 | 18 | type PushRef struct { 19 | Src string 20 | Dst string 21 | Force bool 22 | 23 | Ok bool 24 | Err error 25 | } 26 | 27 | type listRefSlice []ListRef 28 | 29 | func (r listRefSlice) writeTo(w io.Writer) error { 30 | var buf bytes.Buffer 31 | var err error 32 | 33 | writeRune := func(r rune) { 34 | if err != nil { 35 | return 36 | } 37 | 38 | _, err = buf.WriteRune(r) 39 | } 40 | 41 | writeString := func(s string) { 42 | if err != nil { 43 | return 44 | } 45 | 46 | _, err = buf.WriteString(s) 47 | } 48 | 49 | writeRef := func(ref ListRef) { 50 | if err != nil { 51 | return 52 | } 53 | 54 | if ref.Hash != "" { 55 | writeString(ref.Hash) 56 | } else if ref.Sym != "" { 57 | writeRune('@') 58 | writeString(ref.Sym) 59 | } else { 60 | writeRune('?') 61 | } 62 | 63 | writeRune(' ') 64 | writeString(ref.Name) 65 | 66 | if ref.Unchanged { 67 | writeRune(' ') 68 | writeString("unchanged") 69 | } 70 | 71 | writeRune('\n') 72 | } 73 | 74 | for _, ref := range r { 75 | writeRef(ref) 76 | } 77 | 78 | writeRune('\n') 79 | 80 | if err == nil { 81 | _, err = buf.WriteTo(w) 82 | } 83 | 84 | return err 85 | } 86 | 87 | type pushRefSlice []*PushRef 88 | 89 | func (r pushRefSlice) writeTo(w io.Writer) error { 90 | var buf bytes.Buffer 91 | var err error 92 | 93 | writeRune := func(r rune) { 94 | if err != nil { 95 | return 96 | } 97 | 98 | _, err = buf.WriteRune(r) 99 | } 100 | 101 | writeString := func(s string) { 102 | if err != nil { 103 | return 104 | } 105 | 106 | _, err = buf.WriteString(s) 107 | } 108 | 109 | writeRef := func(ref *PushRef) { 110 | if err != nil { 111 | return 112 | } 113 | 114 | if ref.Ok { 115 | writeString("ok ") 116 | writeString(ref.Dst) 117 | } else { 118 | writeString("error ") 119 | writeString(ref.Dst) 120 | if ref.Err != nil { 121 | writeRune(' ') 122 | writeString(ref.Err.Error()) 123 | } 124 | } 125 | 126 | writeRune('\n') 127 | } 128 | 129 | for _, ref := range r { 130 | writeRef(ref) 131 | } 132 | 133 | writeRune('\n') 134 | 135 | if err == nil { 136 | _, err = buf.WriteTo(w) 137 | } 138 | 139 | return err 140 | } 141 | -------------------------------------------------------------------------------- /runner.go: -------------------------------------------------------------------------------- 1 | package gitremote 2 | 3 | import ( 4 | "bufio" 5 | "errors" 6 | "io" 7 | "os" 8 | "sync" 9 | 10 | "golang.org/x/net/context" 11 | ) 12 | 13 | var ErrInvalidArguments = errors.New("invalid arguments.") 14 | var ErrUnsupportedOption = errors.New("unsupported option") 15 | 16 | type Helper interface { 17 | Capabilities() Capabilities 18 | SetOption(key, value string) error 19 | 20 | List(ctx context.Context, cmd *CmdList) ([]ListRef, error) 21 | Fetch(ctx context.Context, cmd *CmdFetch) error 22 | Push(ctx context.Context, cmd *CmdPush) error 23 | Export(ctx context.Context, cmd *CmdExport) error 24 | Import(ctx context.Context, cmd *CmdImport) error 25 | Connect(ctx context.Context, cmd *CmdConnect) error 26 | Unknown(ctx context.Context, cmd *CmdUnknown) error 27 | } 28 | 29 | type Config struct { 30 | Helper Helper 31 | Dir string 32 | Remote string 33 | URL string 34 | Stdin io.Reader 35 | Stdout io.Writer 36 | Err error 37 | } 38 | 39 | type runner struct { 40 | Config 41 | mtx sync.Mutex 42 | br *bufio.Reader 43 | bw *bufio.Writer 44 | err error 45 | } 46 | 47 | func DefaultConfig() Config { 48 | c := Config{} 49 | 50 | args := os.Args[1:] 51 | if len(args) == 0 || len(args) > 2 { 52 | c.Err = ErrInvalidArguments 53 | return c 54 | } 55 | 56 | c.Dir = os.Getenv("GIT_DIR") 57 | c.Stdin = os.Stdin 58 | c.Stdout = os.Stdout 59 | c.Remote = args[0] 60 | 61 | c.URL = args[0] 62 | if len(args) > 1 { 63 | c.URL = args[1] 64 | } 65 | 66 | return c 67 | } 68 | 69 | func Run(ctx context.Context, config Config) error { 70 | if config.Err != nil { 71 | return config.Err 72 | } 73 | 74 | var r runner 75 | r.Config = config 76 | 77 | return r.run(ctx) 78 | } 79 | 80 | func (r *runner) run(ctx context.Context) error { 81 | r.mtx.Lock() 82 | r.br = bufio.NewReader(r.Stdin) 83 | r.bw = bufio.NewWriter(r.Stdout) 84 | r.mtx.Unlock() 85 | 86 | ctx, cancel := context.WithCancel(ctx) 87 | defer cancel() 88 | 89 | commands := r.readCommands(ctx) 90 | 91 | LOOP: 92 | for { 93 | select { 94 | 95 | case <-ctx.Done(): 96 | if r.setError(ctx.Err()) { 97 | break LOOP 98 | } 99 | 100 | case cmd, ok := <-commands: 101 | if !ok { 102 | break LOOP 103 | } 104 | if r.setError(r.runCommand(ctx, cmd)) { 105 | break LOOP 106 | } 107 | 108 | } 109 | } 110 | 111 | return r.err 112 | } 113 | 114 | func (r *runner) runCommand(ctx context.Context, cmd Command) error { 115 | cmd.setConfig(r.Config) 116 | 117 | err := cmd.runCommand(r, ctx) 118 | 119 | flushErr := r.bw.Flush() 120 | if err == nil { 121 | err = flushErr 122 | } 123 | 124 | return err 125 | } 126 | 127 | func (r *runner) setError(err error) bool { 128 | r.mtx.Lock() 129 | defer r.mtx.Unlock() 130 | 131 | if r.err == nil { 132 | r.err = err 133 | } 134 | 135 | return r.err != nil 136 | } 137 | -------------------------------------------------------------------------------- /capabilities.go: -------------------------------------------------------------------------------- 1 | package gitremote 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | ) 7 | 8 | type Capabilities struct { 9 | Optional Capability 10 | Mandatory Capability 11 | Refspecs []string 12 | ExportMarks string 13 | ImportMarks string 14 | } 15 | 16 | type Capability uint 17 | 18 | const ( 19 | CapConnect Capability = 1 << iota 20 | CapPush 21 | CapFetch 22 | CapExport 23 | CapImport 24 | CapOption 25 | CapRefspec 26 | CapBidiImport 27 | CapExportMarks 28 | CapImportMarks 29 | CapNoPrivateUpdate 30 | CapCheckConnectivity 31 | CapSignedTags 32 | ) 33 | 34 | func (c Capability) String() string { 35 | switch c { 36 | case CapConnect: 37 | return "connect" 38 | case CapPush: 39 | return "push" 40 | case CapFetch: 41 | return "fetch" 42 | case CapExport: 43 | return "export" 44 | case CapImport: 45 | return "import" 46 | case CapOption: 47 | return "option" 48 | case CapRefspec: 49 | return "refspec" 50 | case CapBidiImport: 51 | return "bidi-import" 52 | case CapExportMarks: 53 | return "export-marks" 54 | case CapImportMarks: 55 | return "import-marks" 56 | case CapNoPrivateUpdate: 57 | return "no-private-update" 58 | case CapCheckConnectivity: 59 | return "check-connectivity" 60 | case CapSignedTags: 61 | return "signed-tags" 62 | default: 63 | panic("unknown Capability") 64 | } 65 | } 66 | 67 | func (c Capabilities) writeTo(w io.Writer) error { 68 | var buf bytes.Buffer 69 | var err error 70 | 71 | writeRune := func(r rune) { 72 | if err != nil { 73 | return 74 | } 75 | 76 | _, err = buf.WriteRune(r) 77 | } 78 | 79 | writeString := func(s string) { 80 | if err != nil { 81 | return 82 | } 83 | 84 | _, err = buf.WriteString(s) 85 | } 86 | 87 | callExtra := func(extra func()) { 88 | if err != nil { 89 | return 90 | } 91 | if extra == nil { 92 | return 93 | } 94 | 95 | extra() 96 | } 97 | 98 | writeCap := func(cap Capability, extra func()) { 99 | if err != nil { 100 | return 101 | } 102 | 103 | if c.Mandatory&cap == cap { 104 | writeRune('*') 105 | writeString(cap.String()) 106 | callExtra(extra) 107 | writeRune('\n') 108 | return 109 | } 110 | 111 | if c.Optional&cap == cap { 112 | writeString(cap.String()) 113 | callExtra(extra) 114 | writeRune('\n') 115 | return 116 | } 117 | } 118 | 119 | writeCap(CapConnect, nil) 120 | writeCap(CapPush, nil) 121 | writeCap(CapFetch, nil) 122 | writeCap(CapExport, nil) 123 | writeCap(CapImport, nil) 124 | writeCap(CapOption, nil) 125 | writeCap(CapBidiImport, nil) 126 | 127 | writeCap(CapExportMarks, func() { 128 | writeRune(' ') 129 | writeString(c.ExportMarks) 130 | }) 131 | 132 | writeCap(CapImportMarks, func() { 133 | writeRune(' ') 134 | writeString(c.ImportMarks) 135 | }) 136 | 137 | for _, refspec := range c.Refspecs { 138 | writeCap(CapRefspec, func() { 139 | writeRune(' ') 140 | writeString(refspec) 141 | }) 142 | } 143 | 144 | writeRune('\n') 145 | 146 | if err == nil { 147 | _, err = buf.WriteTo(w) 148 | } 149 | 150 | return err 151 | } 152 | -------------------------------------------------------------------------------- /example/peernet/peer.go: -------------------------------------------------------------------------------- 1 | package peernet 2 | 3 | import ( 4 | "io" 5 | "net/http" 6 | "net/url" 7 | "sync" 8 | 9 | "github.com/inconshreveable/muxado" 10 | "golang.org/x/net/websocket" 11 | ) 12 | 13 | type Peer struct { 14 | session muxado.Session 15 | handler http.Handler 16 | client *http.Client 17 | } 18 | 19 | func Dial(url string, handler http.Handler) (*Peer, error) { 20 | ws, err := websocket.Dial(url, "", "http://localhost/") 21 | if err != nil { 22 | return nil, err 23 | } 24 | 25 | sess := muxado.Client(ws) 26 | 27 | client := &http.Client{ 28 | Transport: &http.Transport{ 29 | Dial: sess.NetDial, 30 | DialTLS: sess.NetDial, 31 | DisableKeepAlives: true, 32 | }, 33 | } 34 | 35 | if handler == nil { 36 | handler = http.HandlerFunc(http.NotFound) 37 | } 38 | 39 | peer := &Peer{sess, handler, client} 40 | 41 | go http.Serve(sess.NetListener(), peerHandler(peer)) 42 | 43 | return peer, nil 44 | } 45 | 46 | func Listen(addr string, handler http.Handler) error { 47 | if handler == nil { 48 | handler = http.HandlerFunc(http.NotFound) 49 | } 50 | 51 | var h = func(ws *websocket.Conn) { 52 | sess := muxado.Server(ws) 53 | 54 | client := &http.Client{ 55 | Transport: &http.Transport{ 56 | Dial: sess.NetDial, 57 | DialTLS: sess.NetDial, 58 | DisableKeepAlives: true, 59 | }, 60 | } 61 | 62 | peer := &Peer{sess, handler, client} 63 | 64 | defer peer.session.Close() 65 | 66 | http.Serve(peer.session.NetListener(), peerHandler(peer)) 67 | } 68 | 69 | return http.ListenAndServe(addr, websocket.Handler(h)) 70 | } 71 | 72 | func (p *Peer) Do(req *http.Request) (resp *http.Response, err error) { 73 | if req != nil { 74 | req.URL.Scheme = "http" 75 | req.URL.Host = "_" 76 | } 77 | return p.client.Do(req) 78 | } 79 | 80 | func (p *Peer) Get(url string) (resp *http.Response, err error) { 81 | return p.client.Get("http://_" + url) 82 | } 83 | 84 | func (p *Peer) Head(url string) (resp *http.Response, err error) { 85 | return p.client.Head("http://_" + url) 86 | } 87 | 88 | func (p *Peer) Post(url string, bodyType string, body io.Reader) (resp *http.Response, err error) { 89 | return p.client.Post("http://_"+url, bodyType, body) 90 | } 91 | 92 | func (p *Peer) PostForm(url string, data url.Values) (resp *http.Response, err error) { 93 | return p.client.PostForm("http://_"+url, data) 94 | } 95 | 96 | func (p *Peer) Close() error { 97 | return p.session.Close() 98 | } 99 | 100 | var ( 101 | peersMtx sync.RWMutex 102 | peers = map[*http.Request]*Peer{} 103 | ) 104 | 105 | func peerHandler(p *Peer) http.HandlerFunc { 106 | return func(rw http.ResponseWriter, req *http.Request) { 107 | peersMtx.Lock() 108 | peers[req] = p 109 | peersMtx.Unlock() 110 | 111 | defer func() { 112 | peersMtx.Lock() 113 | delete(peers, req) 114 | peersMtx.Unlock() 115 | }() 116 | 117 | p.handler.ServeHTTP(rw, req) 118 | } 119 | } 120 | 121 | func LookupPeer(req *http.Request) *Peer { 122 | peersMtx.RLock() 123 | defer peersMtx.RUnlock() 124 | 125 | return peers[req] 126 | } 127 | -------------------------------------------------------------------------------- /commands.go: -------------------------------------------------------------------------------- 1 | package gitremote 2 | 3 | import ( 4 | "io" 5 | 6 | "golang.org/x/net/context" 7 | ) 8 | 9 | type Command interface { 10 | setConfig(config Config) 11 | runCommand(r *runner, ctx context.Context) error 12 | } 13 | 14 | type CmdUnknown struct { 15 | Config Config 16 | Line string 17 | } 18 | 19 | type CmdCapabilities struct { 20 | Config Config 21 | } 22 | 23 | type CmdList struct { 24 | Config Config 25 | ForPush bool 26 | } 27 | 28 | type CmdOption struct { 29 | Config Config 30 | Key string 31 | Value string 32 | } 33 | 34 | type CmdFetch struct { 35 | Config Config 36 | Objects map[string]string 37 | } 38 | 39 | type CmdPush struct { 40 | Config Config 41 | Refs []*PushRef 42 | Options []string 43 | } 44 | 45 | type CmdImport struct { 46 | Config Config 47 | Names []string 48 | io.Reader 49 | io.Writer 50 | } 51 | 52 | type CmdExport struct { 53 | Config Config 54 | io.Reader 55 | io.Writer 56 | } 57 | 58 | type CmdConnect struct { 59 | Config Config 60 | Service string 61 | io.Reader 62 | io.Writer 63 | } 64 | 65 | func (c *CmdUnknown) setConfig(config Config) { c.Config = config } 66 | func (c *CmdCapabilities) setConfig(config Config) { c.Config = config } 67 | func (c *CmdList) setConfig(config Config) { c.Config = config } 68 | func (c *CmdOption) setConfig(config Config) { c.Config = config } 69 | func (c *CmdFetch) setConfig(config Config) { c.Config = config } 70 | func (c *CmdPush) setConfig(config Config) { c.Config = config } 71 | func (c *CmdImport) setConfig(config Config) { c.Config = config } 72 | func (c *CmdExport) setConfig(config Config) { c.Config = config } 73 | func (c *CmdConnect) setConfig(config Config) { c.Config = config } 74 | 75 | func (c *CmdUnknown) runCommand(r *runner, ctx context.Context) error { 76 | return r.Helper.Unknown(ctx, c) 77 | } 78 | 79 | func (c *CmdCapabilities) runCommand(r *runner, ctx context.Context) error { 80 | caps := r.Helper.Capabilities() 81 | return caps.writeTo(r.bw) 82 | } 83 | 84 | func (c *CmdList) runCommand(r *runner, ctx context.Context) error { 85 | refs, err := r.Helper.List(ctx, c) 86 | if err != nil { 87 | return err 88 | } 89 | 90 | l := listRefSlice(refs) 91 | return l.writeTo(r.bw) 92 | } 93 | 94 | func (c *CmdOption) runCommand(r *runner, ctx context.Context) error { 95 | err := r.Helper.SetOption(c.Key, c.Value) 96 | if err == ErrUnsupportedOption { 97 | _, err = r.bw.WriteString("unsupported\n") 98 | return err 99 | } 100 | if err != nil { 101 | _, err = r.bw.WriteString("error " + err.Error() + "\n") 102 | return err 103 | } 104 | _, err = r.bw.WriteString("ok\n") 105 | return err 106 | } 107 | 108 | func (c *CmdFetch) runCommand(r *runner, ctx context.Context) error { 109 | err := r.Helper.Fetch(ctx, c) 110 | if err != nil { 111 | return err 112 | } 113 | 114 | _, err = r.bw.WriteRune('\n') 115 | return err 116 | } 117 | 118 | func (c *CmdPush) runCommand(r *runner, ctx context.Context) error { 119 | err := r.Helper.Push(ctx, c) 120 | if err != nil { 121 | return err 122 | } 123 | 124 | s := pushRefSlice(c.Refs) 125 | return s.writeTo(r.bw) 126 | } 127 | 128 | func (c *CmdImport) runCommand(r *runner, ctx context.Context) error { 129 | return r.Helper.Import(ctx, c) 130 | } 131 | 132 | func (c *CmdExport) runCommand(r *runner, ctx context.Context) error { 133 | return r.Helper.Export(ctx, c) 134 | } 135 | 136 | func (c *CmdConnect) runCommand(r *runner, ctx context.Context) error { 137 | return r.Helper.Connect(ctx, c) 138 | } 139 | -------------------------------------------------------------------------------- /example/cmd/git-ws-server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io/ioutil" 8 | "log" 9 | "net/http" 10 | "sync" 11 | 12 | "github.com/gorilla/mux" 13 | 14 | "bitbucket.org/simonmenke/featherhead/pkg/git/objects" 15 | "github.com/fd/go-git-remote-helper/example/peernet" 16 | ) 17 | 18 | func main() { 19 | r := mux.NewRouter() 20 | 21 | r.HandleFunc("/{repo}/refs", handleRefs).Methods("GET") 22 | r.HandleFunc("/{repo}/refs", handlePush).Methods("POST") 23 | r.HandleFunc("/objects/{hash}", handleObject).Methods("GET") 24 | 25 | peernet.Listen(":3000", r) 26 | } 27 | 28 | func handleRefs(rw http.ResponseWriter, req *http.Request) { 29 | var ( 30 | vars = mux.Vars(req) 31 | repo = vars["repo"] 32 | refs map[string]string 33 | ) 34 | 35 | mtx.RLock() 36 | defer mtx.RUnlock() 37 | 38 | refs, ok := allRefs[repo] 39 | if !ok { 40 | http.NotFound(rw, req) 41 | return 42 | } 43 | if refs == nil { 44 | refs = map[string]string{} 45 | } 46 | 47 | rw.Header().Set("Content-Type", "application/json; charset=utf-8") 48 | rw.WriteHeader(200) 49 | json.NewEncoder(rw).Encode(refs) 50 | } 51 | 52 | func handlePush(rw http.ResponseWriter, req *http.Request) { 53 | var ( 54 | vars = mux.Vars(req) 55 | peer = peernet.LookupPeer(req) 56 | repo = vars["repo"] 57 | refs map[string]string 58 | ) 59 | 60 | mtx.Lock() 61 | defer mtx.Unlock() 62 | 63 | refs, ok := allRefs[repo] 64 | if !ok { 65 | http.NotFound(rw, req) 66 | return 67 | } 68 | 69 | var refOps []*struct { 70 | Name string 71 | Hash string 72 | Force bool 73 | 74 | Ok bool 75 | Err string 76 | } 77 | 78 | err := json.NewDecoder(req.Body).Decode(&refOps) 79 | if err != nil { 80 | panic(err) 81 | } 82 | 83 | rw.Header().Set("Content-Type", "application/json; charset=utf-8") 84 | rw.WriteHeader(200) 85 | 86 | for _, op := range refOps { 87 | 88 | // delete ref 89 | if op.Hash == "" { 90 | if _, f := refs[op.Name]; f { 91 | delete(refs, op.Name) 92 | op.Ok = true 93 | } else { 94 | op.Ok = false 95 | op.Err = "not found" 96 | } 97 | continue 98 | } 99 | 100 | // update ref 101 | prevHash := refs[op.Name] 102 | foundPrevHash := false 103 | err := <-loadObject(peer, op.Hash, func(hash string) error { 104 | if prevHash == hash { 105 | foundPrevHash = true 106 | } 107 | return nil 108 | }) 109 | if err != nil { 110 | log.Printf("error: %s", err) 111 | op.Ok = false 112 | op.Err = fmt.Sprintf("failed to load all objects") 113 | continue 114 | } 115 | 116 | if prevHash == "" || foundPrevHash { 117 | refs[op.Name] = op.Hash 118 | op.Ok = true 119 | } else { 120 | op.Ok = false 121 | op.Err = "not fast-forward" 122 | } 123 | } 124 | 125 | json.NewEncoder(rw).Encode(refOps) 126 | } 127 | 128 | func handleObject(rw http.ResponseWriter, req *http.Request) { 129 | var ( 130 | vars = mux.Vars(req) 131 | hash = vars["hash"] 132 | ) 133 | 134 | mtx.RLock() 135 | o, found := objectMap[hash] 136 | mtx.RUnlock() 137 | 138 | if !found { 139 | http.NotFound(rw, req) 140 | return 141 | } 142 | 143 | rw.WriteHeader(200) 144 | rw.Write(o) 145 | } 146 | 147 | var ( 148 | mtx sync.RWMutex 149 | allRefs = map[string]map[string]string{ 150 | "bootloader": {}, 151 | } 152 | objectMap = map[string][]byte{} 153 | ) 154 | 155 | type beforeLoadFunc func(hash string) error 156 | 157 | func loadObject(peer *peernet.Peer, hash string, f beforeLoadFunc) <-chan error { 158 | out := make(chan error, 1) 159 | go func() { 160 | defer close(out) 161 | 162 | err := f(hash) 163 | if err != nil { 164 | out <- err 165 | return 166 | } 167 | 168 | if _, found := objectMap[hash]; found { 169 | return 170 | } 171 | 172 | resp, err := peer.Get("/objects/" + hash) 173 | if err != nil { 174 | out <- err 175 | return 176 | } 177 | 178 | defer resp.Body.Close() 179 | 180 | if resp.StatusCode != 200 { 181 | out <- fmt.Errorf("unexpected status %d", resp.StatusCode) 182 | } 183 | 184 | data, err := ioutil.ReadAll(resp.Body) 185 | if err != nil { 186 | out <- err 187 | return 188 | } 189 | 190 | objectMap[hash] = data 191 | var q []<-chan error 192 | 193 | r, err := objects.NewReader(bytes.NewReader(data)) 194 | if err != nil { 195 | out <- err 196 | return 197 | } 198 | 199 | switch r.Type() { 200 | 201 | case objects.CommitType: 202 | o, err := objects.Parse(r) 203 | if err != nil { 204 | out <- err 205 | return 206 | } 207 | c := o.(*objects.Commit) 208 | q = append(q, loadObject(peer, c.Tree, f)) 209 | for _, parent := range c.Parents { 210 | q = append(q, loadObject(peer, parent, f)) 211 | } 212 | 213 | case objects.TreeType: 214 | o, err := objects.Parse(r) 215 | if err != nil { 216 | out <- err 217 | return 218 | } 219 | t := o.(*objects.Tree) 220 | for _, e := range t.Entries { 221 | q = append(q, loadObject(peer, e.Sha, f)) 222 | } 223 | 224 | } 225 | 226 | // wait for others 227 | for _, p := range q { 228 | err := <-p 229 | if err != nil { 230 | out <- err 231 | return 232 | } 233 | } 234 | 235 | log.Printf("Loaded: %q", hash) 236 | }() 237 | return out 238 | } 239 | -------------------------------------------------------------------------------- /command_parser.go: -------------------------------------------------------------------------------- 1 | package gitremote 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "strings" 7 | 8 | "golang.org/x/net/context" 9 | ) 10 | 11 | type ErrInvalidCommand string 12 | 13 | func (e ErrInvalidCommand) Error() string { 14 | return fmt.Sprintf("invalid command: %q", string(e)) 15 | } 16 | 17 | func (r *runner) readCommands(ctx context.Context) <-chan Command { 18 | var out = make(chan Command) 19 | 20 | go func() { 21 | ctx, cancel := context.WithCancel(ctx) 22 | defer cancel() 23 | defer close(out) 24 | 25 | for { 26 | cmd, err := r.readCommand() 27 | if err == io.EOF { 28 | return 29 | } 30 | if r.setError(err) { 31 | return 32 | } 33 | 34 | select { 35 | case <-ctx.Done(): 36 | return 37 | case out <- cmd: 38 | } 39 | } 40 | }() 41 | 42 | return out 43 | } 44 | 45 | func (r *runner) readCommand() (Command, error) { 46 | var ( 47 | cmd Command 48 | fetchCmd *CmdFetch 49 | pushCmd *CmdPush 50 | importCmd *CmdImport 51 | state int 52 | ) 53 | 54 | MORE: 55 | str, err := r.br.ReadString('\n') 56 | if err == io.EOF { 57 | return nil, io.ErrUnexpectedEOF 58 | } 59 | if err != nil { 60 | return nil, err 61 | } 62 | 63 | str = strings.TrimSuffix(str, "\n") 64 | 65 | switch state { 66 | 67 | case 0: // root 68 | switch { 69 | case str == "": 70 | // done 71 | 72 | case str == "capabilities": 73 | cmd = &CmdCapabilities{} 74 | 75 | case str == "list": 76 | cmd = &CmdList{} 77 | 78 | case str == "list for-push": 79 | cmd = &CmdList{ForPush: true} 80 | 81 | case strings.HasPrefix(str, "option "): 82 | parts := strings.SplitN(str, " ", 3) 83 | if len(parts) != 3 { 84 | cmd = &CmdUnknown{Line: str} 85 | } else { 86 | cmd = &CmdOption{Key: parts[1], Value: parts[2]} 87 | } 88 | 89 | case strings.HasPrefix(str, "fetch "): 90 | parts := strings.SplitN(str, " ", 3) 91 | if len(parts) != 3 { 92 | cmd = &CmdUnknown{Line: str} 93 | } else { 94 | fetchCmd = &CmdFetch{Objects: map[string]string{parts[1]: parts[2]}} 95 | cmd = fetchCmd 96 | state = 1 97 | goto MORE 98 | } 99 | 100 | case strings.HasPrefix(str, "push "): 101 | parts := strings.SplitN(str, " ", 2) 102 | if len(parts) != 2 { 103 | cmd = &CmdUnknown{Line: str} 104 | } else { 105 | var ref = &PushRef{} 106 | 107 | if strings.HasPrefix(parts[1], "+") { 108 | ref.Force = true 109 | parts[1] = parts[1][1:] 110 | } 111 | 112 | parts = strings.Split(parts[1], ":") 113 | if len(parts) != 2 { 114 | cmd = &CmdUnknown{Line: str} 115 | } else { 116 | ref.Src = parts[0] 117 | ref.Dst = parts[1] 118 | 119 | pushCmd = &CmdPush{Refs: []*PushRef{ref}} 120 | cmd = pushCmd 121 | state = 2 122 | goto MORE 123 | } 124 | } 125 | 126 | case strings.HasPrefix(str, "import "): 127 | parts := strings.SplitN(str, " ", 2) 128 | if len(parts) != 2 { 129 | cmd = &CmdUnknown{Line: str} 130 | } else { 131 | importCmd = &CmdImport{Names: []string{parts[1]}, Reader: r.br, Writer: r.bw} 132 | cmd = importCmd 133 | state = 3 134 | goto MORE 135 | } 136 | 137 | case str == "export": 138 | cmd = &CmdExport{Reader: r.br, Writer: r.bw} 139 | 140 | case strings.HasPrefix(str, "connect "): 141 | parts := strings.SplitN(str, " ", 2) 142 | if len(parts) != 2 { 143 | cmd = &CmdUnknown{Line: str} 144 | } else { 145 | cmd = &CmdConnect{Service: parts[1], Reader: r.br, Writer: r.bw} 146 | } 147 | 148 | default: 149 | cmd = &CmdUnknown{Line: str} 150 | 151 | } 152 | 153 | case 1: // fetch 154 | switch { 155 | case str == "": 156 | // done 157 | 158 | case strings.HasPrefix(str, "fetch "): 159 | parts := strings.SplitN(str, " ", 3) 160 | if len(parts) != 3 { 161 | return nil, ErrInvalidCommand(str) 162 | } else { 163 | fetchCmd.Objects[parts[1]] = parts[2] 164 | goto MORE 165 | } 166 | 167 | default: 168 | return nil, ErrInvalidCommand(str) 169 | 170 | } 171 | 172 | case 2: // fetch 173 | switch { 174 | case str == "": 175 | // done 176 | 177 | case strings.HasPrefix(str, "push "): 178 | parts := strings.SplitN(str, " ", 2) 179 | if len(parts) != 2 { 180 | return nil, ErrInvalidCommand(str) 181 | } else { 182 | var ref = &PushRef{} 183 | 184 | if strings.HasPrefix(parts[1], "+") { 185 | ref.Force = true 186 | parts[1] = parts[1][1:] 187 | } 188 | 189 | parts = strings.Split(parts[1], ":") 190 | if len(parts) != 2 { 191 | return nil, ErrInvalidCommand(str) 192 | } else { 193 | ref.Src = parts[0] 194 | ref.Dst = parts[1] 195 | 196 | pushCmd.Refs = append(pushCmd.Refs, ref) 197 | goto MORE 198 | } 199 | } 200 | 201 | default: 202 | pushCmd.Options = append(pushCmd.Options, str) 203 | goto MORE 204 | 205 | } 206 | 207 | case 3: // import 208 | switch { 209 | case str == "": 210 | // done 211 | 212 | case strings.HasPrefix(str, "import "): 213 | parts := strings.SplitN(str, " ", 2) 214 | if len(parts) != 2 { 215 | return nil, ErrInvalidCommand(str) 216 | } else { 217 | importCmd.Names = append(importCmd.Names, parts[1]) 218 | goto MORE 219 | } 220 | 221 | default: 222 | return nil, ErrInvalidCommand(str) 223 | 224 | } 225 | 226 | } 227 | 228 | if cmd == nil { 229 | return nil, io.EOF 230 | } 231 | 232 | return cmd, nil 233 | } 234 | -------------------------------------------------------------------------------- /example/cmd/git-remote-wstest/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "compress/zlib" 6 | "encoding/json" 7 | "fmt" 8 | "io" 9 | "net/http" 10 | "net/url" 11 | "os" 12 | "path" 13 | "strings" 14 | "sync" 15 | 16 | "github.com/fd/git" 17 | "github.com/gorilla/mux" 18 | "golang.org/x/net/context" 19 | 20 | "github.com/fd/go-git-remote-helper" 21 | "github.com/fd/go-git-remote-helper/example/peernet" 22 | ) 23 | 24 | func main() { 25 | conf := gitremote.DefaultConfig() 26 | 27 | fmt.Fprintf(os.Stderr, "config: %v\n", conf) 28 | 29 | u, err := url.Parse(conf.URL) 30 | assert(err) 31 | 32 | repo, err := git.OpenRepository(conf.Dir) 33 | assert(err) 34 | 35 | repoName := strings.TrimPrefix(strings.TrimSuffix(u.Path, ".git"), "/") 36 | u.Path = "/" 37 | u.Scheme = "ws" 38 | 39 | r := mux.NewRouter() 40 | r.HandleFunc("/objects/{hash}", objectHandler(repo)).Methods("GET") 41 | 42 | peer, err := peernet.Dial(u.String(), r) 43 | assert(err) 44 | 45 | conf.Helper = &Helper{peer: peer, repoName: repoName, repo: repo} 46 | 47 | ctx, cancel := context.WithCancel(context.Background()) 48 | defer cancel() 49 | 50 | err = gitremote.Run(ctx, conf) 51 | assert(err) 52 | } 53 | 54 | func assert(err error) { 55 | if err != nil { 56 | fmt.Fprintf(os.Stderr, "error: %s\n", err) 57 | os.Exit(1) 58 | } 59 | } 60 | 61 | type Helper struct { 62 | peer *peernet.Peer 63 | repoName string 64 | repo *git.Repository 65 | 66 | mtx sync.Mutex 67 | loaderCache map[string]bool 68 | } 69 | 70 | func (h *Helper) Capabilities() gitremote.Capabilities { 71 | cap := gitremote.Capabilities{} 72 | cap.Mandatory = gitremote.CapPush | gitremote.CapFetch 73 | cap.Optional = gitremote.CapOption 74 | return cap 75 | } 76 | 77 | func (h *Helper) SetOption(key, value string) error { 78 | fmt.Fprintf(os.Stderr, "option %q %q\n", key, value) 79 | return nil 80 | } 81 | 82 | func (h *Helper) List(ctx context.Context, cmd *gitremote.CmdList) ([]gitremote.ListRef, error) { 83 | resp, err := h.peer.Get("/" + h.repoName + "/refs") 84 | if err != nil { 85 | return nil, err 86 | } 87 | 88 | defer resp.Body.Close() 89 | 90 | if resp.StatusCode != 200 { 91 | return nil, fmt.Errorf("unexpected status code %d for %q", resp.StatusCode, resp.Request.URL) 92 | } 93 | 94 | var body map[string]string 95 | err = json.NewDecoder(resp.Body).Decode(&body) 96 | if err != nil { 97 | return nil, err 98 | } 99 | 100 | var ( 101 | refs []gitremote.ListRef 102 | hasMaster bool 103 | hasHead bool 104 | ) 105 | 106 | for name, hash := range body { 107 | if name == "HEAD" { 108 | hasHead = true 109 | } 110 | if name == "refs/heads/master" { 111 | hasMaster = true 112 | } 113 | 114 | refs = append(refs, gitremote.ListRef{ 115 | Name: name, 116 | Hash: hash, 117 | }) 118 | } 119 | 120 | if hasMaster && !hasHead { 121 | refs = append(refs, gitremote.ListRef{ 122 | Name: "HEAD", 123 | Sym: "refs/heads/master", 124 | }) 125 | } 126 | 127 | return refs, nil 128 | } 129 | 130 | func (h *Helper) Fetch(ctx context.Context, cmd *gitremote.CmdFetch) error { 131 | for hash, _ := range cmd.Objects { 132 | err := <-h.loadObject(hash) 133 | if err != nil { 134 | return err 135 | } 136 | } 137 | 138 | return nil 139 | } 140 | 141 | func (h *Helper) Push(ctx context.Context, cmd *gitremote.CmdPush) error { 142 | type RefOp struct { 143 | Name string 144 | Hash string 145 | Force bool 146 | 147 | Ok bool 148 | Err string 149 | } 150 | 151 | var ( 152 | ops []RefOp 153 | buf bytes.Buffer 154 | ) 155 | 156 | for _, ref := range cmd.Refs { 157 | var ( 158 | hash string 159 | err error 160 | ) 161 | 162 | if ref.Src != "" { 163 | hash, err = h.repo.GetCommitIdOfRef(ref.Src) 164 | if err != nil { 165 | return err 166 | } 167 | } 168 | 169 | ops = append(ops, RefOp{Name: ref.Dst, Hash: hash, Force: ref.Force}) 170 | } 171 | 172 | err := json.NewEncoder(&buf).Encode(ops) 173 | if err != nil { 174 | return err 175 | } 176 | 177 | resp, err := h.peer.Post("/"+h.repoName+"/refs", "application/json; charset=utf-8", &buf) 178 | if err != nil { 179 | return err 180 | } 181 | 182 | defer resp.Body.Close() 183 | 184 | if resp.StatusCode != 200 { 185 | return fmt.Errorf("unexpected status code %d for %q", resp.StatusCode, resp.Request.URL) 186 | } 187 | 188 | ops = nil 189 | err = json.NewDecoder(resp.Body).Decode(&ops) 190 | if err != nil { 191 | return err 192 | } 193 | 194 | for _, op := range ops { 195 | for _, ref := range cmd.Refs { 196 | if ref.Dst == op.Name { 197 | ref.Ok = op.Ok 198 | 199 | if op.Err != "" { 200 | ref.Err = fmt.Errorf(op.Err) 201 | } 202 | 203 | break 204 | } 205 | } 206 | } 207 | 208 | return nil 209 | } 210 | 211 | func (h *Helper) Export(ctx context.Context, cmd *gitremote.CmdExport) error { 212 | panic("unsupported command: Export") 213 | } 214 | 215 | func (h *Helper) Import(ctx context.Context, cmd *gitremote.CmdImport) error { 216 | panic("unsupported command: Import") 217 | } 218 | 219 | func (h *Helper) Connect(ctx context.Context, cmd *gitremote.CmdConnect) error { 220 | panic("unsupported command: Connect") 221 | } 222 | 223 | func (h *Helper) Unknown(ctx context.Context, cmd *gitremote.CmdUnknown) error { 224 | panic(fmt.Sprintf("unsupported command: %q", cmd.Line)) 225 | } 226 | 227 | func objectHandler(repo *git.Repository) http.HandlerFunc { 228 | return func(rw http.ResponseWriter, req *http.Request) { 229 | var ( 230 | vars = mux.Vars(req) 231 | hash = vars["hash"] 232 | err error 233 | ) 234 | 235 | typ, length, rc, err := repo.GetRawObject(hash, false) 236 | if git.IsObjectNotFound(err) { 237 | http.NotFound(rw, req) 238 | return 239 | } 240 | if err != nil { 241 | panic(err) 242 | } 243 | 244 | defer rc.Close() 245 | 246 | fmt.Fprintf(os.Stderr, "sending %s %q %d\n", typ, hash, length) 247 | 248 | header := fmt.Sprintf("%s %d\x00", strings.ToLower(typ.String()), length) 249 | 250 | rw.Header().Set("Content-Length", fmt.Sprintf("%d", int64(len(header))+length)) 251 | rw.WriteHeader(200) 252 | 253 | _, err = io.WriteString(rw, header) 254 | if err != nil { 255 | panic(err) 256 | } 257 | 258 | _, err = io.Copy(rw, rc) 259 | if err != nil { 260 | panic(err) 261 | } 262 | } 263 | } 264 | 265 | func (h *Helper) loadObject(hash string) <-chan error { 266 | out := make(chan error, 1) 267 | go func() { 268 | defer close(out) 269 | 270 | if !h.needsObject(hash) { 271 | return 272 | } 273 | 274 | _, _, _, err := h.repo.GetRawObject(hash, true) 275 | if err == nil { 276 | return 277 | } 278 | if git.IsObjectNotFound(err) { 279 | err = nil 280 | } 281 | if err != nil { 282 | out <- err 283 | return 284 | } 285 | 286 | resp, err := h.peer.Get("/objects/" + hash) 287 | if err != nil { 288 | out <- err 289 | return 290 | } 291 | 292 | defer resp.Body.Close() 293 | 294 | if resp.StatusCode != 200 { 295 | out <- fmt.Errorf("unexpected status %d", resp.StatusCode) 296 | } 297 | 298 | dir := path.Join(os.Getenv("GIT_DIR"), "objects", hash[:2]) 299 | fname := path.Join(dir, hash[2:]) 300 | 301 | err = os.MkdirAll(dir, 0755) 302 | if err != nil { 303 | out <- err 304 | return 305 | } 306 | 307 | f, err := os.Create(fname) 308 | if err != nil { 309 | out <- err 310 | return 311 | } 312 | 313 | defer f.Close() 314 | 315 | w := zlib.NewWriter(f) 316 | _, err = io.Copy(w, resp.Body) 317 | if err != nil { 318 | fmt.Fprintf(os.Stderr, "HERE H: %s\n", err) 319 | out <- err 320 | return 321 | } 322 | 323 | err = w.Close() 324 | if err != nil { 325 | fmt.Fprintf(os.Stderr, "HERE G: %s\n", err) 326 | out <- err 327 | return 328 | } 329 | 330 | err = f.Close() 331 | if err != nil { 332 | fmt.Fprintf(os.Stderr, "HERE F: %s\n", err) 333 | out <- err 334 | return 335 | } 336 | 337 | var q []<-chan error 338 | 339 | typ, _, _, err := h.repo.GetRawObject(hash, true) 340 | if err != nil { 341 | fmt.Fprintf(os.Stderr, "HERE E: %s (%s)\n", err, typ) 342 | out <- err 343 | return 344 | } 345 | 346 | switch typ { 347 | 348 | case git.ObjectCommit: 349 | c, err := h.repo.GetCommit(hash) 350 | if err != nil { 351 | fmt.Fprintf(os.Stderr, "HERE D: %s\n", err) 352 | out <- err 353 | return 354 | } 355 | 356 | q = append(q, h.loadObject(c.TreeId().String())) 357 | for i, l := 0, c.ParentCount(); i < l; i++ { 358 | id, err := c.ParentId(i) 359 | if err != nil { 360 | fmt.Fprintf(os.Stderr, "HERE C: %s\n", err) 361 | out <- err 362 | return 363 | } 364 | 365 | q = append(q, h.loadObject(id.String())) 366 | } 367 | 368 | case git.ObjectTree: 369 | t, err := h.repo.GetTree(hash) 370 | if err != nil { 371 | fmt.Fprintf(os.Stderr, "HERE B: %s\n", err) 372 | out <- err 373 | return 374 | } 375 | 376 | for _, e := range t.ListEntries() { 377 | q = append(q, h.loadObject(e.Id.String())) 378 | } 379 | 380 | case git.ObjectTag: 381 | t, err := h.repo.GetTagWithId(hash) 382 | if err != nil { 383 | fmt.Fprintf(os.Stderr, "HERE A: %s\n", err) 384 | out <- err 385 | return 386 | } 387 | 388 | q = append(q, h.loadObject(t.Object.String())) 389 | 390 | } 391 | 392 | // wait for others 393 | for _, p := range q { 394 | err := <-p 395 | if err != nil { 396 | out <- err 397 | return 398 | } 399 | } 400 | 401 | fmt.Fprintf(os.Stderr, "Loaded: %q\n", hash) 402 | }() 403 | return out 404 | } 405 | 406 | func (h *Helper) needsObject(hash string) bool { 407 | h.mtx.Lock() 408 | defer h.mtx.Unlock() 409 | 410 | if h.loaderCache == nil { 411 | h.loaderCache = make(map[string]bool) 412 | } 413 | 414 | if h.loaderCache[hash] { 415 | return false 416 | } 417 | 418 | h.loaderCache[hash] = true 419 | return true 420 | } 421 | --------------------------------------------------------------------------------