main.go 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. /*
  2. git-remote-helper implements a git-remote helper that uses the ipfs transport.
  3. TODO
  4. Currently assumes a IPFS Daemon at localhost:5001
  5. Not completed: new Push (issue #2), IPNS, URLs like fs:/ipfs/.. (issue #3), embedded IPFS node
  6. ...
  7. $ git clone ipfs://ipfs/$hash/repo.git
  8. $ cd repo && make $stuff
  9. $ git commit -a -m 'done!'
  10. $ git push origin
  11. => clone-able as ipfs://ipfs/$newHash/repo.git
  12. Links
  13. https://ipfs.io
  14. https://github.com/whyrusleeping/git-ipfs-rehost
  15. https://git-scm.com/docs/gitremote-helpers
  16. https://git-scm.com/book/en/v2/Git-Internals-Plumbing-and-Porcelain
  17. https://git-scm.com/docs/gitrepository-layout
  18. https://git-scm.com/book/en/v2/Git-Internals-Transfer-Protocols
  19. */
  20. package main
  21. import (
  22. "bufio"
  23. "fmt"
  24. "io"
  25. "os"
  26. "path/filepath"
  27. "strings"
  28. "github.com/cryptix/git-remote-ipfs/internal/path"
  29. "github.com/cryptix/go/logging"
  30. shell "github.com/ipfs/go-ipfs-api"
  31. "github.com/pkg/errors"
  32. )
  33. const usageMsg = `usage git-remote-ipfs <repository> [<URL>]
  34. supports:
  35. * ipfs://ipfs/$hash/path..
  36. * ipfs:///ipfs/$hash/path..
  37. `
  38. func usage() {
  39. fmt.Fprint(os.Stderr, usageMsg)
  40. os.Exit(2)
  41. }
  42. var (
  43. ref2hash = make(map[string]string)
  44. ipfsShell = shell.NewShell("localhost:5001")
  45. ipfsRepoPath string
  46. thisGitRepo string
  47. thisGitRemote string
  48. errc chan<- error
  49. log logging.Interface
  50. check = logging.CheckFatal
  51. )
  52. func logFatal(msg string) {
  53. log.Log("event", "fatal", "msg", msg)
  54. os.Exit(1)
  55. }
  56. func main() {
  57. // logging
  58. logging.SetupLogging(nil)
  59. log = logging.Logger("git-remote-ipfs")
  60. // env var and arguments
  61. thisGitRepo = os.Getenv("GIT_DIR")
  62. if thisGitRepo == "" {
  63. logFatal("could not get GIT_DIR env var")
  64. }
  65. if thisGitRepo == ".git" {
  66. cwd, err := os.Getwd()
  67. logging.CheckFatal(err)
  68. thisGitRepo = filepath.Join(cwd, ".git")
  69. }
  70. var u string // repo url
  71. v := len(os.Args[1:])
  72. switch v {
  73. case 2:
  74. thisGitRemote = os.Args[1]
  75. u = os.Args[2]
  76. default:
  77. logFatal(fmt.Sprintf("usage: unknown # of args: %d\n%v", v, os.Args[1:]))
  78. }
  79. // parse passed URL
  80. for _, pref := range []string{"ipfs://ipfs/", "ipfs:///ipfs/"} {
  81. if strings.HasPrefix(u, pref) {
  82. u = "/ipfs/" + u[len(pref):]
  83. }
  84. }
  85. p, err := path.ParsePath(u)
  86. check(err)
  87. ipfsRepoPath = p.String()
  88. // interrupt / error handling
  89. go func() {
  90. check(interrupt())
  91. }()
  92. check(speakGit(os.Stdin, os.Stdout))
  93. }
  94. // speakGit acts like a git-remote-helper
  95. // see this for more: https://www.kernel.org/pub/software/scm/git/docs/gitremote-helpers.html
  96. func speakGit(r io.Reader, w io.Writer) error {
  97. //debugLog := logging.Logger("git")
  98. //r = debug.NewReadLogrus(debugLog, r)
  99. //w = debug.NewWriteLogrus(debugLog, w)
  100. scanner := bufio.NewScanner(r)
  101. for scanner.Scan() {
  102. text := scanner.Text()
  103. switch {
  104. case text == "capabilities":
  105. fmt.Fprintln(w, "fetch")
  106. fmt.Fprintln(w, "push")
  107. fmt.Fprintln(w, "")
  108. case strings.HasPrefix(text, "list"):
  109. var (
  110. forPush = strings.Contains(text, "for-push")
  111. err error
  112. head string
  113. )
  114. if err = listInfoRefs(forPush); err == nil { // try .git/info/refs first
  115. if head, err = listHeadRef(); err != nil {
  116. return err
  117. }
  118. } else { // alternativly iterate over the refs directory like git-remote-dropbox
  119. if forPush {
  120. log.Log("msg", "for-push: should be able to push to non existant.. TODO #2")
  121. }
  122. log.Log("err", err, "msg", "didn't find info/refs in repo, falling back...")
  123. if err = listIterateRefs(forPush); err != nil {
  124. return err
  125. }
  126. }
  127. if len(ref2hash) == 0 {
  128. return errors.New("did not find _any_ refs...")
  129. }
  130. // output
  131. for ref, hash := range ref2hash {
  132. if head == "" && strings.HasSuffix(ref, "master") {
  133. // guessing head if it isnt set
  134. head = hash
  135. }
  136. fmt.Fprintf(w, "%s %s\n", hash, ref)
  137. }
  138. fmt.Fprintf(w, "%s HEAD\n", head)
  139. fmt.Fprintln(w)
  140. case strings.HasPrefix(text, "fetch "):
  141. for scanner.Scan() {
  142. fetchSplit := strings.Split(text, " ")
  143. if len(fetchSplit) < 2 {
  144. return errors.Errorf("malformed 'fetch' command. %q", text)
  145. }
  146. err := fetchObject(fetchSplit[1])
  147. if err == nil {
  148. fmt.Fprintln(w)
  149. continue
  150. }
  151. // TODO isNotExist(err) would be nice here
  152. //log.Log("sha1", fetchSplit[1], "name", fetchSplit[2], "err", err, "msg", "fetchLooseObject failed, trying packed...")
  153. err = fetchPackedObject(fetchSplit[1])
  154. if err != nil {
  155. return errors.Wrap(err, "fetchPackedObject() failed")
  156. }
  157. text = scanner.Text()
  158. if text == "" {
  159. break
  160. }
  161. }
  162. fmt.Fprintln(w, "")
  163. case strings.HasPrefix(text, "push"):
  164. for scanner.Scan() {
  165. pushSplit := strings.Split(text, " ")
  166. if len(pushSplit) < 2 {
  167. return errors.Errorf("malformed 'push' command. %q", text)
  168. }
  169. srcDstSplit := strings.Split(pushSplit[1], ":")
  170. if len(srcDstSplit) < 2 {
  171. return errors.Errorf("malformed 'push' command. %q", text)
  172. }
  173. src, dst := srcDstSplit[0], srcDstSplit[1]
  174. f := []interface{}{
  175. "src", src,
  176. "dst", dst,
  177. }
  178. log.Log(append(f, "msg", "got push"))
  179. if src == "" {
  180. fmt.Fprintf(w, "error %s %s\n", dst, "delete remote dst: not supported yet - please open an issue on github")
  181. } else {
  182. if err := push(src, dst); err != nil {
  183. fmt.Fprintf(w, "error %s %s\n", dst, err)
  184. return err
  185. }
  186. fmt.Fprintln(w, "ok", dst)
  187. }
  188. text = scanner.Text()
  189. if text == "" {
  190. break
  191. }
  192. }
  193. fmt.Fprintln(w, "")
  194. case text == "":
  195. break
  196. default:
  197. return errors.Errorf("Error: default git speak: %q", text)
  198. }
  199. }
  200. if err := scanner.Err(); err != nil {
  201. return errors.Wrap(err, "scanner.Err()")
  202. }
  203. return nil
  204. }