├── git-remote-dg ├── dgit-black.png ├── constants └── constants.go ├── main.go ├── storage ├── config.go ├── readonly │ ├── readonly_test.go │ └── readonly.go ├── object_test.go ├── split │ ├── split_test.go │ └── split.go ├── chaintree │ ├── storage_test.go │ ├── storage.go │ ├── reference.go │ └── object.go ├── objiter.go ├── siaskynet │ ├── net.go │ └── object.go └── object.go ├── tupelo ├── teamtree │ ├── member.go │ ├── members.go │ └── teamtree.go ├── namedtree │ ├── namedtree_test.go │ └── namedtree.go ├── usertree │ └── usertree.go ├── tree │ └── tree.go ├── repotree │ └── repotree.go └── clientbuilder │ └── clientbuilder.go ├── .gitignore ├── .github └── workflows │ ├── dgit-push.yml │ ├── binaries.yml │ └── homebrew.yml ├── cmd ├── version.go ├── whoami.go ├── shared.go ├── root.go ├── team.go ├── init.go └── remotehelper.go ├── transport └── dgit │ ├── auth.go │ ├── loader.go │ ├── repo.go │ └── client.go ├── msg ├── parse.go └── messages.go ├── LICENSE ├── Makefile ├── go.mod ├── keyring └── keyring.go ├── README.md ├── remotehelper ├── runner_test.go └── runner.go └── initializer └── initializer.go /git-remote-dg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | git-dg remote-helper $@ 4 | -------------------------------------------------------------------------------- /dgit-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quorumcontrol/dgit/HEAD/dgit-black.png -------------------------------------------------------------------------------- /constants/constants.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | const ( 4 | Protocol = "dg" 5 | DgitConfigSection = "decentragit" 6 | DgitRemote = "dg" 7 | ) 8 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/quorumcontrol/dgit/cmd" 5 | ) 6 | 7 | var Version string 8 | 9 | func main() { 10 | cmd.Version = Version 11 | cmd.Execute() 12 | } 13 | -------------------------------------------------------------------------------- /storage/config.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "context" 5 | "crypto/ecdsa" 6 | 7 | "github.com/quorumcontrol/tupelo/sdk/consensus" 8 | tupelo "github.com/quorumcontrol/tupelo/sdk/gossip/client" 9 | ) 10 | 11 | type Config struct { 12 | Ctx context.Context 13 | Tupelo *tupelo.Client 14 | ChainTree *consensus.SignedChainTree 15 | PrivateKey *ecdsa.PrivateKey 16 | } 17 | -------------------------------------------------------------------------------- /tupelo/teamtree/member.go: -------------------------------------------------------------------------------- 1 | package teamtree 2 | 3 | type Member struct { 4 | MemberIface 5 | did string 6 | name string 7 | } 8 | 9 | func NewMember(did string, name string) *Member { 10 | return &Member{ 11 | did: did, 12 | name: name, 13 | } 14 | } 15 | 16 | func (m *Member) Did() string { 17 | return m.did 18 | } 19 | 20 | func (m *Member) Name() string { 21 | return m.name 22 | } 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | decentragit-remote 8 | /dgit 9 | /git-dg 10 | /dgit.tar.gz 11 | /decentragit.tar.gz 12 | 13 | # Test binary, built with `go test -c` 14 | *.test 15 | 16 | # Output of the go coverage tool, specifically when used with LiteIDE 17 | *.out 18 | 19 | vendor/ 20 | 21 | dist/ 22 | 23 | # Vim swp files 24 | .*.swp 25 | .*.swo 26 | -------------------------------------------------------------------------------- /.github/workflows/dgit-push.yml: -------------------------------------------------------------------------------- 1 | name: dgit push 2 | on: 3 | push: 4 | jobs: 5 | push: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - name: Check out repository 9 | uses: actions/checkout@v2 10 | with: 11 | fetch-depth: 0 12 | - name: Push to dgit 13 | uses: quorumcontrol/dgit-github-action@master 14 | env: 15 | GIT_PUSH_ARGS: "--force" 16 | DG_PRIVATE_KEY: ${{ secrets.DGIT_PRIVATE_KEY }} 17 | DG_LOG_LEVEL: debug -------------------------------------------------------------------------------- /cmd/version.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | var Version string 10 | 11 | func init() { 12 | rootCmd.AddCommand(versionCommand) 13 | } 14 | 15 | var versionCommand = &cobra.Command{ 16 | Use: "version", 17 | Short: "Print decentragit version", 18 | Long: "Output decentragit version to stdout", 19 | Run: func(cmd *cobra.Command, args []string) { 20 | fmt.Printf("decentragit version %s\n", Version) 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /storage/readonly/readonly_test.go: -------------------------------------------------------------------------------- 1 | package readonly 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/go-git/go-git/v5/storage/memory" 7 | "github.com/go-git/go-git/v5/storage/test" 8 | . "gopkg.in/check.v1" 9 | ) 10 | 11 | func Test(t *testing.T) { TestingT(t) } 12 | 13 | type StorageSuite struct { 14 | test.BaseStorageSuite 15 | } 16 | 17 | var _ = Suite(&StorageSuite{}) 18 | 19 | func (s *StorageSuite) SetUpTest(c *C) { 20 | s.BaseStorageSuite = test.NewBaseStorageSuite(NewStorage(memory.NewStorage())) 21 | } 22 | -------------------------------------------------------------------------------- /tupelo/namedtree/namedtree_test.go: -------------------------------------------------------------------------------- 1 | package namedtree 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestNamingIsCaseInsenitive(t *testing.T) { 10 | namedTreeGen := &Generator{Namespace: "Test"} 11 | lower, err := namedTreeGen.Did("quorumcontrol/dgit-test") 12 | require.Nil(t, err) 13 | mixed, err := namedTreeGen.Did("quorumcontrol/DGIT-test") 14 | require.Nil(t, err) 15 | upper, err := namedTreeGen.Did("QUORUMCONTROL/DGIT-TEST") 16 | require.Nil(t, err) 17 | 18 | require.Equal(t, lower, mixed) 19 | require.Equal(t, lower, upper) 20 | } 21 | -------------------------------------------------------------------------------- /storage/object_test.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/go-git/go-git/v5/plumbing" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestZlibBufferForObject(t *testing.T) { 11 | o := &plumbing.MemoryObject{} 12 | o.SetType(plumbing.BlobObject) 13 | o.SetSize(14) 14 | _, err := o.Write([]byte("Hello, World!\n")) 15 | require.Nil(t, err) 16 | require.Equal(t, o.Hash().String(), "8ab686eafeb1f44702738c8b0f24f2567c36da6d") 17 | 18 | buf, err := ZlibBufferForObject(o) 19 | require.Nil(t, err) 20 | 21 | require.Equal(t, buf.Bytes(), []byte{120, 156, 74, 202, 201, 79, 82, 48, 52, 97, 240, 72, 205, 201, 201, 215, 81, 8, 207, 47, 202, 73, 81, 228, 2, 4, 0, 0, 255, 255, 78, 21, 6, 152}) 22 | } 23 | -------------------------------------------------------------------------------- /transport/dgit/auth.go: -------------------------------------------------------------------------------- 1 | package dgit 2 | 3 | import ( 4 | "crypto/ecdsa" 5 | 6 | "github.com/ethereum/go-ethereum/crypto" 7 | "github.com/go-git/go-git/v5/plumbing/transport" 8 | ) 9 | 10 | const PrivateKeyAuthName = "private-key-auth" 11 | 12 | type PrivateKeyAuth struct { 13 | transport.AuthMethod 14 | key *ecdsa.PrivateKey 15 | } 16 | 17 | func NewPrivateKeyAuth(key *ecdsa.PrivateKey) *PrivateKeyAuth { 18 | return &PrivateKeyAuth{key: key} 19 | } 20 | 21 | func (a *PrivateKeyAuth) Name() string { 22 | return PrivateKeyAuthName 23 | } 24 | 25 | func (a *PrivateKeyAuth) Key() *ecdsa.PrivateKey { 26 | return a.key 27 | } 28 | 29 | func (a *PrivateKeyAuth) String() string { 30 | return crypto.PubkeyToAddress(a.key.PublicKey).String() 31 | } 32 | -------------------------------------------------------------------------------- /cmd/whoami.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func init() { 11 | rootCmd.AddCommand(whoAmICommand) 12 | } 13 | 14 | var whoAmICommand = &cobra.Command{ 15 | Use: "whoami", 16 | Short: "Print out your username in the current repo", 17 | Args: cobra.NoArgs, 18 | Run: func(cmd *cobra.Command, args []string) { 19 | callingDir, err := os.Getwd() 20 | if err != nil { 21 | fmt.Fprintln(os.Stderr, "error getting current workdir: %w", err) 22 | os.Exit(1) 23 | } 24 | 25 | repo := openRepo(cmd, callingDir) 26 | 27 | username, err := repo.Username() 28 | if err != nil { 29 | fmt.Fprintln(os.Stderr, err) 30 | os.Exit(1) 31 | } 32 | 33 | fmt.Println(username) 34 | }, 35 | } 36 | -------------------------------------------------------------------------------- /storage/split/split_test.go: -------------------------------------------------------------------------------- 1 | package split 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/go-git/go-git/v5/storage/memory" 7 | "github.com/go-git/go-git/v5/storage/test" 8 | . "gopkg.in/check.v1" 9 | ) 10 | 11 | func Test(t *testing.T) { TestingT(t) } 12 | 13 | type StorageSuite struct { 14 | test.BaseStorageSuite 15 | } 16 | 17 | var _ = Suite(&StorageSuite{}) 18 | 19 | func (s *StorageSuite) SetUpTest(c *C) { 20 | splitStore := NewStorage(&StorageMap{ 21 | ObjectStorage: memory.NewStorage(), 22 | ShallowStorage: memory.NewStorage(), 23 | ReferenceStorage: memory.NewStorage(), 24 | IndexStorage: memory.NewStorage(), 25 | ConfigStorage: memory.NewStorage(), 26 | }) 27 | s.BaseStorageSuite = test.NewBaseStorageSuite(splitStore) 28 | } 29 | -------------------------------------------------------------------------------- /tupelo/teamtree/members.go: -------------------------------------------------------------------------------- 1 | package teamtree 2 | 3 | type MemberIface interface { 4 | Did() string 5 | Name() string 6 | } 7 | 8 | type Members []MemberIface 9 | 10 | func (m Members) Dids() []string { 11 | dids := make([]string, len(m)) 12 | for i, v := range m { 13 | dids[i] = v.Did() 14 | } 15 | return dids 16 | } 17 | 18 | func (m Members) Names() []string { 19 | names := make([]string, len(m)) 20 | for i, v := range m { 21 | names[i] = v.Name() 22 | } 23 | return names 24 | } 25 | 26 | func (m Members) Map() map[string]string { 27 | ret := make(map[string]string) 28 | for _, v := range m { 29 | ret[v.Name()] = v.Did() 30 | } 31 | return ret 32 | } 33 | 34 | func (m Members) IsMember(did string) bool { 35 | for _, v := range m { 36 | if v.Did() == did { 37 | return true 38 | } 39 | } 40 | return false 41 | } 42 | -------------------------------------------------------------------------------- /storage/split/split.go: -------------------------------------------------------------------------------- 1 | package split 2 | 3 | import ( 4 | "github.com/go-git/go-git/v5/config" 5 | "github.com/go-git/go-git/v5/plumbing/storer" 6 | "github.com/go-git/go-git/v5/storage" 7 | ) 8 | 9 | type store struct { 10 | storer.EncodedObjectStorer 11 | storer.ReferenceStorer 12 | storer.ShallowStorer 13 | storer.IndexStorer 14 | config.ConfigStorer 15 | opts *StorageMap 16 | } 17 | 18 | type StorageMap struct { 19 | ObjectStorage storer.EncodedObjectStorer 20 | ReferenceStorage storer.ReferenceStorer 21 | ShallowStorage storer.ShallowStorer 22 | IndexStorage storer.IndexStorer 23 | ConfigStorage config.ConfigStorer 24 | } 25 | 26 | func NewStorage(opts *StorageMap) storage.Storer { 27 | return &store{ 28 | opts.ObjectStorage, 29 | opts.ReferenceStorage, 30 | opts.ShallowStorage, 31 | opts.IndexStorage, 32 | opts.ConfigStorage, 33 | opts, 34 | } 35 | } 36 | 37 | func (s *store) Module(name string) (storage.Storer, error) { 38 | return NewStorage(s.opts), nil 39 | } 40 | -------------------------------------------------------------------------------- /msg/parse.go: -------------------------------------------------------------------------------- 1 | package msg 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "strings" 8 | "text/template" 9 | 10 | "github.com/manifoldco/promptui" 11 | ) 12 | 13 | // TODO: probably should do a real i18n & template library, but they all looked heavy atm 14 | func Parse(str string, data map[string]interface{}) string { 15 | var buf bytes.Buffer 16 | tmpl, err := template.New("msg.Parse").Funcs(promptui.FuncMap).Parse(str) 17 | if err != nil { 18 | panic(fmt.Errorf("could not compile template:\ntemplate: %s\ndata: %+v\nerr: %w", str, data, err)) 19 | } 20 | err = tmpl.Execute(&buf, data) 21 | if err != nil { 22 | panic(fmt.Errorf("could not execute template with given args:\ntemplate: %s\nargs: %v\nerr: %w", str, data, err)) 23 | } 24 | return strings.TrimSpace(buf.String()) 25 | } 26 | 27 | func Print(str string, data map[string]interface{}) { 28 | fmt.Println(Parse(str, data)) 29 | } 30 | 31 | func Fprint(w io.Writer, str string, data map[string]interface{}) { 32 | fmt.Fprintln(w, Parse(str, data)) 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2020 Quorum Control 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /cmd/shared.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/go-git/go-git/v5" 9 | "github.com/go-git/go-git/v5/storage/filesystem" 10 | "github.com/spf13/cobra" 11 | 12 | "github.com/quorumcontrol/dgit/msg" 13 | "github.com/quorumcontrol/dgit/transport/dgit" 14 | ) 15 | 16 | func openRepo(cmd *cobra.Command, path string) *dgit.Repo { 17 | gitRepo, err := git.PlainOpenWithOptions(path, &git.PlainOpenOptions{ 18 | DetectDotGit: true, 19 | }) 20 | 21 | if err == git.ErrRepositoryNotExists { 22 | msg.Fprint(os.Stderr, msg.RepoNotFoundInPath, map[string]interface{}{ 23 | "path": path, 24 | "cmd": rootCmd.Name() + " " + cmd.Name(), 25 | }) 26 | os.Exit(1) 27 | } 28 | if err != nil { 29 | fmt.Fprintln(os.Stderr, err) 30 | os.Exit(1) 31 | } 32 | 33 | return dgit.NewRepo(gitRepo) 34 | } 35 | 36 | func newClient(ctx context.Context, repo *dgit.Repo) (*dgit.Client, error) { 37 | repoGitPath := repo.Storer.(*filesystem.Storage).Filesystem().Root() 38 | 39 | client, err := dgit.NewClient(ctx, repoGitPath) 40 | if err != nil { 41 | return nil, fmt.Errorf("error starting decentragit client: %w", err) 42 | } 43 | client.RegisterAsDefault() 44 | 45 | return client, nil 46 | } 47 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "runtime" 7 | "strings" 8 | 9 | logging "github.com/ipfs/go-log" 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | const defaultLogLevel = "PANIC" 14 | 15 | var log = logging.Logger("decentragit.cmd") 16 | 17 | var rootCmd = &cobra.Command{ 18 | Use: "git gd [command]", 19 | Short: "decentragit is git with decentralized ownership and storage", 20 | } 21 | 22 | func Execute() { 23 | setLogLevel() 24 | globalDebugLogs() 25 | 26 | if err := rootCmd.Execute(); err != nil { 27 | fmt.Fprintln(os.Stderr, err) 28 | os.Exit(1) 29 | } 30 | } 31 | 32 | func globalDebugLogs() { 33 | log.Infof("decentragit version: " + Version) 34 | log.Infof("goos: " + runtime.GOOS) 35 | log.Infof("goarch: " + runtime.GOARCH) 36 | } 37 | 38 | func setLogLevel() { 39 | // turn off all logging, mainly for silencing tupelo-go-sdk ERROR logs 40 | logging.SetAllLoggers(logging.LevelPanic) 41 | 42 | // now set decentragit.* logs if applicable 43 | logLevelStr, ok := os.LookupEnv("DG_LOG_LEVEL") 44 | if !ok { 45 | logLevelStr, ok = os.LookupEnv("DGIT_LOG_LEVEL") 46 | if ok { 47 | log.Warningf("[DEPRECATION] - DGIT_LOG_LEVEL is deprecated, please use DG_LOG_LEVEL") 48 | } 49 | } 50 | 51 | if logLevelStr == "" { 52 | logLevelStr = defaultLogLevel 53 | } 54 | 55 | err := logging.SetLogLevelRegex("decentragit.*", strings.ToUpper(logLevelStr)) 56 | if err != nil { 57 | fmt.Fprintf(os.Stderr, "invalid value %s given for DG_LOG_LEVEL: %v\n", logLevelStr, err) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /storage/readonly/readonly.go: -------------------------------------------------------------------------------- 1 | package readonly 2 | 3 | import ( 4 | "github.com/go-git/go-git/v5/config" 5 | "github.com/go-git/go-git/v5/plumbing/storer" 6 | "github.com/go-git/go-git/v5/storage" 7 | "github.com/go-git/go-git/v5/storage/memory" 8 | "github.com/go-git/go-git/v5/storage/transactional" 9 | ) 10 | 11 | type store struct { 12 | storer.EncodedObjectStorer 13 | storer.ReferenceStorer 14 | storer.ShallowStorer 15 | storer.IndexStorer 16 | config.ConfigStorer 17 | base storage.Storer 18 | } 19 | 20 | // readonly store is a shortcut to transactional store without 21 | // the commit hooks, meaning any write changes performed to this store 22 | // are never persisted and only held in memory throughout the duration 23 | // of this object 24 | func NewStorage(base storage.Storer) storage.Storer { 25 | return &store{ 26 | NewObjectStorage(base), 27 | NewReferenceStorage(base), 28 | NewShallowStorage(base), 29 | NewIndexStorage(base), 30 | NewConfigStorage(base), 31 | base, 32 | } 33 | } 34 | 35 | func (s *store) Module(name string) (storage.Storer, error) { 36 | modStore, err := s.base.Module(name) 37 | if err != nil { 38 | return nil, err 39 | } 40 | return NewStorage(modStore), nil 41 | } 42 | 43 | func NewConfigStorage(base storage.Storer) config.ConfigStorer { 44 | return transactional.NewConfigStorage(base, memory.NewStorage()) 45 | } 46 | 47 | func NewShallowStorage(base storage.Storer) storer.ShallowStorer { 48 | return transactional.NewShallowStorage(base, memory.NewStorage()) 49 | } 50 | 51 | func NewIndexStorage(base storage.Storer) storer.IndexStorer { 52 | return transactional.NewIndexStorage(base, memory.NewStorage()) 53 | } 54 | 55 | func NewReferenceStorage(base storage.Storer) storer.ReferenceStorer { 56 | return transactional.NewReferenceStorage(base, memory.NewStorage()) 57 | } 58 | 59 | func NewObjectStorage(base storage.Storer) storer.EncodedObjectStorer { 60 | return transactional.NewObjectStorage(base, memory.NewStorage()) 61 | } 62 | -------------------------------------------------------------------------------- /transport/dgit/loader.go: -------------------------------------------------------------------------------- 1 | package dgit 2 | 3 | import ( 4 | "context" 5 | "crypto/ecdsa" 6 | "fmt" 7 | 8 | "github.com/go-git/go-git/v5/plumbing/storer" 9 | "github.com/go-git/go-git/v5/plumbing/transport" 10 | "github.com/go-git/go-git/v5/plumbing/transport/server" 11 | "github.com/quorumcontrol/chaintree/nodestore" 12 | tupelo "github.com/quorumcontrol/tupelo/sdk/gossip/client" 13 | 14 | "github.com/quorumcontrol/dgit/storage" 15 | "github.com/quorumcontrol/dgit/storage/chaintree" 16 | "github.com/quorumcontrol/dgit/tupelo/namedtree" 17 | "github.com/quorumcontrol/dgit/tupelo/repotree" 18 | ) 19 | 20 | // Load loads a storer.Storer given a transport.Endpoint. 21 | // Returns transport.ErrRepositoryNotFound if the repository does not 22 | // exist. 23 | type ChainTreeLoader struct { 24 | server.Loader 25 | 26 | ctx context.Context 27 | auth transport.AuthMethod 28 | tupelo *tupelo.Client 29 | nodestore nodestore.DagStore 30 | } 31 | 32 | func NewChainTreeLoader(ctx context.Context, tupelo *tupelo.Client, nodestore nodestore.DagStore, auth transport.AuthMethod) server.Loader { 33 | return &ChainTreeLoader{ 34 | ctx: ctx, 35 | tupelo: tupelo, 36 | nodestore: nodestore, 37 | auth: auth, 38 | } 39 | } 40 | 41 | func (l *ChainTreeLoader) Load(ep *transport.Endpoint) (storer.Storer, error) { 42 | repoTree, err := repotree.Find(l.ctx, ep.Host+ep.Path, l.tupelo) 43 | 44 | var privateKey *ecdsa.PrivateKey 45 | 46 | switch auth := l.auth.(type) { 47 | case *PrivateKeyAuth: 48 | privateKey = auth.Key() 49 | case nil: 50 | // noop 51 | default: 52 | return nil, fmt.Errorf("Unsupported auth type %T", l.auth) 53 | } 54 | 55 | if err == namedtree.ErrNotFound { 56 | return nil, transport.ErrRepositoryNotFound 57 | } 58 | 59 | if err != nil { 60 | return nil, err 61 | } 62 | 63 | return chaintree.NewStorage(&storage.Config{ 64 | Ctx: l.ctx, 65 | Tupelo: l.tupelo, 66 | ChainTree: repoTree.ChainTree(), 67 | PrivateKey: privateKey, 68 | }) 69 | } 70 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | gosources = $(shell find . -type f -name '*.go' -print) 2 | 3 | FIRSTGOPATH = $(firstword $(subst :, ,$(GOPATH))) 4 | HEAD_TAG := $(shell if [ -d .git ]; then git tag --points-at HEAD; fi) 5 | GIT_REV := $(shell if [ -d .git ]; then git rev-parse --short HEAD; fi) 6 | GIT_VERSION := $(or $(HEAD_TAG),$(GIT_REV)) 7 | DEV_VERSION := $(shell if [ -d .git ]; then git diff-index --quiet HEAD || echo "${GIT_VERSION}-dev"; fi) 8 | VERSION ?= $(or $(DEV_VERSION),$(GIT_VERSION)) 9 | GOLDFLAGS += -X main.Version=$(VERSION) 10 | GOFLAGS = -ldflags "$(GOLDFLAGS)" 11 | 12 | ifeq ($(PREFIX),) 13 | PREFIX := $(or $(FIRSTGOPATH),/usr/local) 14 | endif 15 | 16 | all: build 17 | 18 | git-dg: go.mod go.sum $(gosources) 19 | go build -o $@ $(GOFLAGS) . 20 | 21 | build: git-dg 22 | 23 | dist/armv%/git-dg: go.mod go.sum $(gosources) 24 | mkdir -p $(@D) 25 | env GOOS=linux GOARCH=arm GOARM=$* go build -o $@ $(GOFLAGS) 26 | 27 | dist/arm64v%/git-dg: go.mod go.sum $(gosources) 28 | mkdir -p $(@D) 29 | env GOOS=linux GOARCH=arm64 go build -o $@ $(GOFLAGS) 30 | 31 | build-linux-arm: dist/armv6/git-dg dist/armv7/git-dg dist/arm64v8/git-dg 32 | 33 | decentragit.tar.gz: git-dg git-remote-dg 34 | tar -czvf decentragit.tar.gz $^ 35 | 36 | dist/armv%/decentragit.tar.gz: dist/armv%/git-dg git-remote-dg 37 | tar -czvf $@ $^ 38 | 39 | dist/arm64v%/decentragit.tar.gz: dist/arm64v%/git-dg git-remote-dg 40 | tar -czvf $@ $^ 41 | 42 | tarball: decentragit.tar.gz 43 | 44 | tarball-linux-arm: dist/armv6/decentragit.tar.gz dist/armv7/decentragit.tar.gz dist/arm64v8/decentragit.tar.gz 45 | 46 | install: git-dg git-remote-dg 47 | install -d $(DESTDIR)$(PREFIX)/bin/ 48 | install -m 755 git-dg $(DESTDIR)$(PREFIX)/bin/ 49 | install -m 755 git-remote-dg $(DESTDIR)$(PREFIX)/bin/ 50 | 51 | uninstall: 52 | rm -f $(DESTDIR)$(PREFIX)/bin/git-dg 53 | rm -f $(DESTDIR)$(PREFIX)/bin/git-remote-dg 54 | 55 | test: 56 | go test ./... 57 | 58 | clean: 59 | rm -f git-dg dgit.tar.gz 60 | 61 | .PHONY: all build build-linux-arm tarball tarball-linux-arm install uninstall test clean 62 | -------------------------------------------------------------------------------- /storage/chaintree/storage_test.go: -------------------------------------------------------------------------------- 1 | package chaintree 2 | 3 | import ( 4 | "context" 5 | "crypto/ecdsa" 6 | "testing" 7 | 8 | "github.com/ethereum/go-ethereum/crypto" 9 | "github.com/quorumcontrol/tupelo/sdk/consensus" 10 | "github.com/quorumcontrol/tupelo/sdk/p2p" 11 | . "gopkg.in/check.v1" 12 | 13 | gitstorage "github.com/go-git/go-git/v5/storage" 14 | "github.com/go-git/go-git/v5/storage/test" 15 | 16 | "github.com/quorumcontrol/dgit/storage" 17 | "github.com/quorumcontrol/dgit/tupelo/clientbuilder" 18 | ) 19 | 20 | func Test(t *testing.T) { TestingT(t) } 21 | 22 | type StorageSuite struct { 23 | test.BaseStorageSuite 24 | } 25 | 26 | var _ = Suite(&StorageSuite{}) 27 | 28 | func createChainTree(c *C, ctx context.Context, store *p2p.BitswapPeer) (*consensus.SignedChainTree, *ecdsa.PrivateKey) { 29 | key, err := crypto.GenerateKey() 30 | c.Assert(err, IsNil) 31 | 32 | chainTree, err := consensus.NewSignedChainTree(ctx, key.PublicKey, store) 33 | c.Assert(err, IsNil) 34 | 35 | return chainTree, key 36 | } 37 | 38 | func (s *StorageSuite) InitStorage(c *C) gitstorage.Storer { 39 | ctx := context.Background() 40 | 41 | // TODO: replace with mock client rather than local running tupelo docker 42 | tupelo, store, err := clientbuilder.BuildLocal(ctx) 43 | c.Assert(err, IsNil) 44 | 45 | chainTree, key := createChainTree(c, ctx, store) 46 | 47 | st, err := NewStorage(&storage.Config{ 48 | Ctx: ctx, 49 | Tupelo: tupelo, 50 | ChainTree: chainTree, 51 | PrivateKey: key, 52 | }) 53 | c.Assert(err, IsNil) 54 | 55 | return st 56 | } 57 | 58 | func (s *StorageSuite) SetUpSuite(c *C) { 59 | st := s.InitStorage(c) 60 | s.BaseStorageSuite = test.NewBaseStorageSuite(st) 61 | } 62 | 63 | func (s *StorageSuite) TearDownTest(c *C) { 64 | // reset this to a new storage every time or tests will share one chaintree 65 | // and interfere w/ each other 66 | s.Storer = s.InitStorage(c) 67 | 68 | s.BaseStorageSuite.TearDownTest(c) 69 | } 70 | 71 | // override a test that will fail for reasons we don't care about 72 | func (s *StorageSuite) TestModule(c *C) {} 73 | -------------------------------------------------------------------------------- /tupelo/namedtree/namedtree.go: -------------------------------------------------------------------------------- 1 | package namedtree 2 | 3 | import ( 4 | "context" 5 | "crypto/ecdsa" 6 | "strings" 7 | 8 | "github.com/quorumcontrol/dgit/tupelo/tree" 9 | "github.com/quorumcontrol/messages/v2/build/go/transactions" 10 | "github.com/quorumcontrol/tupelo/sdk/consensus" 11 | tupelo "github.com/quorumcontrol/tupelo/sdk/gossip/client" 12 | ) 13 | 14 | var ErrNotFound = tree.ErrNotFound 15 | 16 | type Generator struct { 17 | Namespace string 18 | Client *tupelo.Client 19 | } 20 | 21 | type NamedTree struct { 22 | *tree.Tree 23 | } 24 | 25 | type Options struct { 26 | Name string 27 | Tupelo *tupelo.Client 28 | Owners []string 29 | AdditionalTxns []*transactions.Transaction 30 | } 31 | 32 | func (g *Generator) GenesisKey(name string) (*ecdsa.PrivateKey, error) { 33 | return consensus.PassPhraseKey([]byte(name), []byte(g.Namespace)) 34 | } 35 | 36 | func (g *Generator) Create(ctx context.Context, opts *Options) (*NamedTree, error) { 37 | gKey, err := g.GenesisKey(opts.Name) 38 | if err != nil { 39 | return nil, err 40 | } 41 | 42 | t, err := tree.Create(ctx, &tree.Options{ 43 | Name: opts.Name, 44 | Key: gKey, 45 | Owners: opts.Owners, 46 | Tupelo: opts.Tupelo, 47 | AdditionalTxns: opts.AdditionalTxns, 48 | }) 49 | if err != nil { 50 | return nil, err 51 | } 52 | 53 | return &NamedTree{t}, nil 54 | } 55 | 56 | // Did lower-cases the name arg first to ensure that chaintree 57 | // names are case insensitive. If we ever want case sensitivity, 58 | // consider adding a bool flag to the Generator or adding a new 59 | // fun. 60 | func (g *Generator) Did(name string) (string, error) { 61 | gKey, err := g.GenesisKey(strings.ToLower(name)) 62 | if err != nil { 63 | return "", err 64 | } 65 | 66 | return consensus.EcdsaPubkeyToDid(gKey.PublicKey), nil 67 | } 68 | 69 | func (g *Generator) Find(ctx context.Context, name string) (*NamedTree, error) { 70 | did, err := g.Did(name) 71 | if err != nil { 72 | return nil, err 73 | } 74 | 75 | t, err := tree.Find(ctx, g.Client, did) 76 | if err != nil { 77 | return nil, err 78 | } 79 | 80 | return &NamedTree{t}, nil 81 | } 82 | -------------------------------------------------------------------------------- /cmd/team.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "strings" 8 | 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | func init() { 13 | rootCmd.AddCommand(teamCommand) 14 | } 15 | 16 | var teamCommand = &cobra.Command{ 17 | Use: "team (add [usernames] | list | remove [usernames])", 18 | Short: "Manage your repo's team of collaborators", 19 | Args: func(cmd *cobra.Command, args []string) error { 20 | if len(args) == 0 { 21 | return cmd.Help() 22 | } 23 | 24 | switch args[0] { 25 | case "add", "remove": 26 | if len(args) < 2 { 27 | return fmt.Errorf("%s command requires one or more usernames to %s", args[0], args[0]) 28 | } 29 | return nil 30 | case "list": 31 | if len(args) != 1 { 32 | return fmt.Errorf("unexpected arguments after list command") 33 | } 34 | return nil 35 | default: 36 | return fmt.Errorf("unknown arguments to team command: %v", args) 37 | } 38 | }, 39 | Run: func(cmd *cobra.Command, args []string) { 40 | ctx, cancel := context.WithCancel(context.Background()) 41 | defer cancel() 42 | 43 | callingDir, err := os.Getwd() 44 | if err != nil { 45 | fmt.Fprintln(os.Stderr, "error getting current workdir: %w", err) 46 | os.Exit(1) 47 | } 48 | 49 | repo := openRepo(cmd, callingDir) 50 | 51 | client, err := newClient(ctx, repo) 52 | if err != nil { 53 | fmt.Fprintln(os.Stderr, err) 54 | os.Exit(1) 55 | } 56 | 57 | subCmd := args[0] 58 | 59 | switch subCmd { 60 | case "add": 61 | err := client.AddRepoCollaborator(ctx, repo, args[1:]) 62 | if err != nil { 63 | fmt.Fprintln(os.Stderr, err) 64 | os.Exit(1) 65 | } 66 | 67 | fmt.Printf("Added:\n%s\n", strings.Join(args[1:], "\n")) 68 | case "list": 69 | collaborators, err := client.ListRepoCollaborators(ctx, repo) 70 | if err != nil { 71 | fmt.Fprintln(os.Stderr, err) 72 | os.Exit(1) 73 | } 74 | 75 | fmt.Printf("Current collaborators:\n%s\n", strings.Join(collaborators, "\n")) 76 | case "remove": 77 | err := client.RemoveRepoCollaborator(ctx, repo, args[1:]) 78 | if err != nil { 79 | fmt.Fprintln(os.Stderr, err) 80 | os.Exit(1) 81 | } 82 | 83 | fmt.Printf("Removed collaborators:\n%s\n", strings.Join(args[1:], "\n")) 84 | } 85 | }, 86 | } 87 | -------------------------------------------------------------------------------- /cmd/init.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "io/ioutil" 8 | 9 | "github.com/spf13/cobra" 10 | 11 | "github.com/quorumcontrol/dgit/initializer" 12 | ) 13 | 14 | func init() { 15 | rootCmd.AddCommand(initCommand) 16 | } 17 | 18 | var initCommand = &cobra.Command{ 19 | Use: "init", 20 | Short: "Get rolling with decentragit!", 21 | // TODO: better explanation 22 | Long: `Sets up a repo to leverage decentragit.`, 23 | Args: cobra.ArbitraryArgs, 24 | Run: func(cmd *cobra.Command, args []string) { 25 | ctx, cancel := context.WithCancel(context.Background()) 26 | defer cancel() 27 | 28 | callingDir, err := os.Getwd() 29 | if err != nil { 30 | fmt.Fprintln(os.Stderr, "error getting current workdir: %w", err) 31 | os.Exit(1) 32 | } 33 | 34 | repo := openRepo(cmd, callingDir) 35 | 36 | client, err := newClient(ctx, repo) 37 | if err != nil { 38 | fmt.Fprintln(os.Stderr, err) 39 | os.Exit(1) 40 | } 41 | 42 | // Create Git hook for Skynet Upload [dg-pages] 43 | b := []byte(`#!/bin/bash 44 | # Upload current directory to skynet if branch is dg-pages 45 | 46 | remote=$1 47 | remote_url="$2" 48 | 49 | while read local_ref remote_ref 50 | do 51 | if [ "$local_ref" == "refs/heads/dg-pages" ] 52 | then 53 | # Push to skynet 54 | mkdir _pages 55 | rsync -am --include='*.css' --include='*.js' --include='*.html' --include='*/' --exclude='*' ./ _pages 56 | touch _pages/_e2kdie_ 57 | skynet upload _pages 58 | rm -rf _pages 59 | fi 60 | done 61 | 62 | exit 0 63 | `) 64 | 65 | writeGitHookErr := ioutil.WriteFile(".git/hooks/pre-push", b, 0777) 66 | if writeGitHookErr != nil { 67 | panic(err) 68 | } 69 | 70 | // Done 71 | 72 | initOpts := &initializer.Options{ 73 | Repo: repo, 74 | Tupelo: client.Tupelo, 75 | NodeStore: client.Nodestore, 76 | } 77 | err = initializer.Init(ctx, initOpts, args) 78 | if err != nil { 79 | fmt.Println(err) 80 | os.Exit(1) 81 | } 82 | }, 83 | } 84 | -------------------------------------------------------------------------------- /cmd/remotehelper.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/go-git/go-billy/v5/osfs" 9 | "github.com/go-git/go-git/v5" 10 | "github.com/go-git/go-git/v5/plumbing/cache" 11 | "github.com/go-git/go-git/v5/storage/filesystem" 12 | "github.com/quorumcontrol/dgit/remotehelper" 13 | "github.com/quorumcontrol/dgit/storage/readonly" 14 | "github.com/quorumcontrol/dgit/storage/split" 15 | "github.com/quorumcontrol/dgit/transport/dgit" 16 | "github.com/spf13/cobra" 17 | ) 18 | 19 | func init() { 20 | rootCmd.AddCommand(remoteHelperCommand) 21 | } 22 | 23 | var remoteHelperCommand = &cobra.Command{ 24 | Use: "remote-helper", 25 | Short: "A git-remote-helper called by git directly. Not for direct use!", 26 | Long: `Implements a git-remote-helper (https://git-scm.com/docs/git-remote-helpers), registering and handling the dg:// protocol.`, 27 | Args: cobra.ArbitraryArgs, 28 | Hidden: true, 29 | Run: func(cmd *cobra.Command, args []string) { 30 | ctx, cancel := context.WithCancel(context.Background()) 31 | defer cancel() 32 | 33 | gitStore := filesystem.NewStorage(osfs.New(os.Getenv("GIT_DIR")), cache.NewObjectLRUDefault()) 34 | readonlyStore := readonly.NewStorage(gitStore) 35 | 36 | // git-remote-helper expects this script to write git objects, but nothing else 37 | // therefore initialize a go-git storage with the ability to write objects & shallow 38 | // but make reference, index, and config read only ops 39 | storer := split.NewStorage(&split.StorageMap{ 40 | ObjectStorage: gitStore, 41 | ShallowStorage: gitStore, 42 | ReferenceStorage: readonlyStore, 43 | IndexStorage: readonlyStore, 44 | ConfigStorage: readonlyStore, 45 | }) 46 | 47 | local, err := git.Open(storer, nil) 48 | if err != nil { 49 | panic(err) 50 | } 51 | 52 | r := remotehelper.New(local) 53 | 54 | log.Infof("decentragit remote helper loaded for %s", os.Getenv("GIT_DIR")) 55 | 56 | if len(args) < 2 { 57 | fmt.Fprintln(os.Stderr, "Usage: git-remote-dg ") 58 | os.Exit(1) 59 | } 60 | 61 | client, err := dgit.NewClient(ctx, os.Getenv("GIT_DIR")) 62 | if err != nil { 63 | fmt.Fprintln(os.Stderr, fmt.Sprintf("error starting decentragit client: %v", err)) 64 | os.Exit(1) 65 | } 66 | client.RegisterAsDefault() 67 | 68 | if err := r.Run(ctx, args[0], args[1]); err != nil { 69 | fmt.Fprintln(os.Stderr, err) 70 | os.Exit(1) 71 | } 72 | }, 73 | } 74 | -------------------------------------------------------------------------------- /.github/workflows/binaries.yml: -------------------------------------------------------------------------------- 1 | name: Binary Release 2 | on: 3 | release: 4 | types: [published] 5 | 6 | jobs: 7 | release: 8 | runs-on: ${{ matrix.os }} 9 | strategy: 10 | matrix: 11 | os: [ubuntu-18.04, macos-10.15] 12 | steps: 13 | - name: checkout code 14 | uses: actions/checkout@v2 15 | - name: setup Go 16 | uses: actions/setup-go@v2-beta 17 | with: 18 | go-version: '^1.14.0' 19 | - name: compile & make tarball 20 | run: make tarball 21 | - name: upload release asset 22 | uses: actions/upload-release-asset@v1 23 | env: 24 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 25 | with: 26 | upload_url: ${{ github.event.release.upload_url }} 27 | asset_path: ./dgit.tar.gz 28 | asset_name: dgit-${{ runner.os }}-x86_64.tar.gz 29 | asset_content_type: application/x-gtar 30 | release-linux-arm: 31 | runs-on: ubuntu-18.04 32 | steps: 33 | - name: checkout code 34 | uses: actions/checkout@v2 35 | - name: setup Go 36 | uses: actions/setup-go@v2-beta 37 | with: 38 | go-version: '^1.14.0' 39 | - name: compile & make tarballs 40 | run: make tarball-linux-arm 41 | - name: upload armv6 release asset 42 | uses: actions/upload-release-asset@v1 43 | env: 44 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 45 | with: 46 | upload_url: ${{ github.event.release.upload_url }} 47 | asset_path: ./dist/armv6/dgit.tar.gz 48 | asset_name: dgit-Linux-armv6.tar.gz 49 | asset_content_type: application/x-gtar 50 | - name: upload armv7 release asset 51 | uses: actions/upload-release-asset@v1 52 | env: 53 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 54 | with: 55 | upload_url: ${{ github.event.release.upload_url }} 56 | asset_path: ./dist/armv7/dgit.tar.gz 57 | asset_name: dgit-Linux-armv7.tar.gz 58 | asset_content_type: application/x-gtar 59 | - name: upload arm64v8 release asset 60 | uses: actions/upload-release-asset@v1 61 | env: 62 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 63 | with: 64 | upload_url: ${{ github.event.release.upload_url }} 65 | asset_path: ./dist/arm64v8/dgit.tar.gz 66 | asset_name: dgit-Linux-arm64v8.tar.gz 67 | asset_content_type: application/x-gtar 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/quorumcontrol/dgit 2 | 3 | go 1.13 4 | 5 | replace ( 6 | github.com/99designs/keyring => github.com/quorumcontrol/keyring v1.1.5-0.20200324201224-62c57642bc6f 7 | github.com/NebulousLabs/go-skynet => github.com/quorumcontrol/go-skynet v0.0.0-20200312164139-76fb137005c8 8 | github.com/go-critic/go-critic => github.com/go-critic/go-critic v0.4.0 9 | github.com/go-git/go-git/v5 => github.com/quorumcontrol/go-git/v5 v5.0.1-0.20200406172056-231a188e4899 10 | github.com/golangci/errcheck => github.com/golangci/errcheck v0.0.0-20181223084120-ef45e06d44b6 11 | github.com/golangci/go-tools => github.com/golangci/go-tools v0.0.0-20190318060251-af6baa5dc196 12 | github.com/golangci/gofmt => github.com/golangci/gofmt v0.0.0-20181222123516-0b8337e80d98 13 | github.com/golangci/gosec => github.com/golangci/gosec v0.0.0-20190211064107-66fb7fc33547 14 | github.com/golangci/lint-1 => github.com/golangci/lint-1 v0.0.0-20190420132249-ee948d087217 15 | github.com/keybase/go-keychain => github.com/quorumcontrol/go-keychain v0.0.0-20200324182052-9544b7eee399 16 | golang.org/x/xerrors => golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 17 | mvdan.cc/unparam => mvdan.cc/unparam v0.0.0-20190209190245-fbb59629db34 18 | ) 19 | 20 | require ( 21 | github.com/99designs/keyring v1.1.4 22 | github.com/NebulousLabs/go-skynet v0.0.0-20200306163948-8394a3e261ba 23 | github.com/allegro/bigcache v1.2.1 // indirect 24 | github.com/aristanetworks/goarista v0.0.0-20200409192631-afcb3fffc5ea // indirect 25 | github.com/btcsuite/btcd v0.20.1-beta 26 | github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d 27 | github.com/elastic/gosigar v0.10.5 // indirect 28 | github.com/ethereum/go-ethereum v1.9.3 29 | github.com/go-git/go-billy/v5 v5.0.0 30 | github.com/go-git/go-git-fixtures/v4 v4.0.1 31 | github.com/go-git/go-git/v5 v5.0.1-0.20200319142726-f6305131a06b 32 | github.com/ipfs/go-bitswap v0.1.9-0.20191015150653-291b2674f1f1 33 | github.com/ipfs/go-datastore v0.4.4 34 | github.com/ipfs/go-ds-flatfs v0.4.0 35 | github.com/ipfs/go-ipfs-blockstore v0.1.0 36 | github.com/ipfs/go-ipld-format v0.0.2 37 | github.com/ipfs/go-log v1.0.2 38 | github.com/manifoldco/promptui v0.7.0 39 | github.com/quorumcontrol/chaintree v1.0.2-0.20200417190801-195827c9d506 40 | github.com/quorumcontrol/messages/v2 v2.1.3-0.20200129115245-2bfec5177653 41 | github.com/quorumcontrol/tupelo v0.6.2-0.20200420211245-9215312e288b 42 | github.com/spf13/cobra v0.0.5 43 | github.com/steakknife/bloomfilter v0.0.0-20180922174646-6819c0d2a570 // indirect 44 | github.com/steakknife/hamming v0.0.0-20180906055917-c99c65617cd3 // indirect 45 | github.com/stretchr/testify v1.4.0 46 | github.com/tyler-smith/go-bip39 v1.0.2 47 | go.uber.org/multierr v1.5.0 // indirect 48 | go.uber.org/zap v1.14.0 49 | golang.org/x/crypto v0.0.0-20200317142112-1b76d66859c6 // indirect 50 | golang.org/x/lint v0.0.0-20200130185559-910be7a94367 // indirect 51 | golang.org/x/sys v0.0.0-20200317113312-5766fd39f98d // indirect 52 | golang.org/x/tools v0.0.0-20200226224502-204d844ad48d // indirect 53 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f 54 | honnef.co/go/tools v0.0.1-2020.1.3 // indirect 55 | ) 56 | -------------------------------------------------------------------------------- /storage/chaintree/storage.go: -------------------------------------------------------------------------------- 1 | package chaintree 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | 8 | "github.com/go-git/go-git/v5/config" 9 | "github.com/quorumcontrol/chaintree/dag" 10 | 11 | "github.com/quorumcontrol/dgit/storage" 12 | "github.com/quorumcontrol/dgit/storage/siaskynet" 13 | 14 | "github.com/go-git/go-git/v5/plumbing/storer" 15 | gitstorage "github.com/go-git/go-git/v5/storage" 16 | "github.com/go-git/go-git/v5/storage/memory" 17 | ) 18 | 19 | var RepoConfigPath = []string{"tree", "data", "config"} 20 | 21 | const defaultStorageProvider = "chaintree" 22 | 23 | type ChaintreeStorage struct { 24 | storer.EncodedObjectStorer 25 | storer.ReferenceStorer 26 | storer.ShallowStorer 27 | storer.IndexStorer 28 | config.ConfigStorer 29 | } 30 | 31 | func NewStorage(config *storage.Config) (gitstorage.Storer, error) { 32 | ctx := context.Background() 33 | 34 | objStorageProvider, err := getObjectStorageProvider(ctx, config.ChainTree.ChainTree.Dag) 35 | if err != nil { 36 | return nil, err 37 | } 38 | 39 | var objStorage storer.EncodedObjectStorer 40 | 41 | switch objStorageProvider { 42 | case "chaintree": 43 | objStorage = NewObjectStorage(config) 44 | case "siaskynet": 45 | objStorage = siaskynet.NewObjectStorage(config) 46 | default: 47 | return nil, fmt.Errorf("unknown object storage type: %s", objStorageProvider) 48 | } 49 | 50 | return &ChaintreeStorage{ 51 | objStorage, 52 | NewReferenceStorage(config), 53 | memory.NewStorage(), 54 | memory.NewStorage(), 55 | memory.NewStorage(), 56 | }, nil 57 | } 58 | 59 | func (s *ChaintreeStorage) Module(_ string) (gitstorage.Storer, error) { 60 | return nil, fmt.Errorf("ChaintreeStorage.Module not implemented") 61 | } 62 | 63 | func getObjectStorageProvider(ctx context.Context, dag *dag.Dag) (string, error) { 64 | configUncast, _, err := dag.Resolve(ctx, RepoConfigPath) 65 | if err != nil { 66 | return "", fmt.Errorf("could not resolve repo config in chaintree: %w", err) 67 | } 68 | // repo hasn't been configured yet 69 | if configUncast == nil { 70 | return defaultStorageProvider, nil 71 | } 72 | 73 | var ( 74 | ctConfig map[string]interface{} 75 | ok bool 76 | ) 77 | if ctConfig, ok = configUncast.(map[string]interface{}); !ok { 78 | return "", fmt.Errorf("could not cast config to map[string]interface{}: was %T instead", configUncast) 79 | } 80 | 81 | objectStorageConfigUncast := ctConfig["objectStorage"] 82 | var objectStorageConfig map[string]interface{} 83 | if objectStorageConfig, ok = objectStorageConfigUncast.(map[string]interface{}); !ok { 84 | return "", fmt.Errorf("could not cast objectStorage config to map[string]interface{}: was %T instead", objectStorageConfigUncast) 85 | } 86 | 87 | objStorageType, ok := objectStorageConfig["type"].(string) 88 | if !ok { 89 | return "", fmt.Errorf("could not cast objectStorage config type to string; was %T instead", objectStorageConfig["type"]) 90 | } 91 | 92 | if objStorageType == "" { 93 | return defaultStorageProvider, nil 94 | } 95 | 96 | return objStorageType, nil 97 | } 98 | 99 | func (s *ChaintreeStorage) PackfileWriter() (io.WriteCloser, error) { 100 | pw, ok := s.EncodedObjectStorer.(storer.PackfileWriter) 101 | if !ok { 102 | return nil, fmt.Errorf("could not cast object storer to packfile writer") 103 | } 104 | 105 | return pw.PackfileWriter() 106 | } 107 | -------------------------------------------------------------------------------- /tupelo/usertree/usertree.go: -------------------------------------------------------------------------------- 1 | package usertree 2 | 3 | import ( 4 | "context" 5 | "crypto/ecdsa" 6 | "fmt" 7 | "strings" 8 | 9 | logging "github.com/ipfs/go-log" 10 | "github.com/quorumcontrol/chaintree/chaintree" 11 | "github.com/quorumcontrol/messages/v2/build/go/transactions" 12 | tupelo "github.com/quorumcontrol/tupelo/sdk/gossip/client" 13 | 14 | "github.com/quorumcontrol/dgit/tupelo/namedtree" 15 | "github.com/quorumcontrol/dgit/tupelo/tree" 16 | ) 17 | 18 | type UserTree struct { 19 | *namedtree.NamedTree 20 | } 21 | 22 | var log = logging.Logger("decentragit.usertree") 23 | 24 | const userSalt = "decentragit-user-v0" 25 | 26 | var namedTreeGen *namedtree.Generator 27 | 28 | var reposMapPath = []string{"repos"} 29 | 30 | var ErrNotFound = tree.ErrNotFound 31 | 32 | func init() { 33 | namedTreeGen = &namedtree.Generator{Namespace: userSalt} 34 | } 35 | 36 | type Options namedtree.Options 37 | 38 | func Did(username string) (string, error) { 39 | return namedTreeGen.Did(username) 40 | } 41 | 42 | func Find(ctx context.Context, username string, client *tupelo.Client) (*UserTree, error) { 43 | namedTreeGen.Client = client 44 | namedTree, err := namedTreeGen.Find(ctx, username) 45 | if err == namedtree.ErrNotFound { 46 | return nil, ErrNotFound 47 | } 48 | if err != nil { 49 | return nil, err 50 | } 51 | return &UserTree{namedTree}, nil 52 | } 53 | 54 | func Create(ctx context.Context, opts *Options) (*UserTree, error) { 55 | ntOpts := namedtree.Options(*opts) 56 | 57 | namedTree, err := namedTreeGen.Create(ctx, &ntOpts) 58 | if err != nil { 59 | return nil, err 60 | } 61 | 62 | log.Debugf("created user %s (%s)", opts.Name, namedTree.Did()) 63 | 64 | return &UserTree{namedTree}, nil 65 | } 66 | 67 | func (t *UserTree) IsOwner(ctx context.Context, addr string) (bool, error) { 68 | auths, err := t.ChainTree().Authentications() 69 | if err != nil { 70 | return false, err 71 | } 72 | log.Debugf("checking %s is owner of %s, chaintree auths: %v", addr, t.Did(), auths) 73 | 74 | for _, auth := range auths { 75 | if auth == addr { 76 | return true, nil 77 | } 78 | } 79 | 80 | return false, nil 81 | } 82 | 83 | func (t *UserTree) AddRepo(ctx context.Context, ownerKey *ecdsa.PrivateKey, reponame string, did string) error { 84 | log.Debugf("adding repo %s (%s) to user %s (%s)", reponame, did, t.Name(), t.Did()) 85 | 86 | path := strings.Join(append(reposMapPath, reponame), "/") 87 | repoTxn, err := chaintree.NewSetDataTransaction(path, did) 88 | if err != nil { 89 | return err 90 | } 91 | 92 | _, err = t.Tupelo().PlayTransactions(ctx, t.ChainTree(), ownerKey, []*transactions.Transaction{repoTxn}) 93 | return err 94 | } 95 | 96 | func (t *UserTree) Repos(ctx context.Context) (map[string]string, error) { 97 | path := append([]string{"tree", "data"}, reposMapPath...) 98 | valMap := make(map[string]string) 99 | valUncast, _, err := t.Resolve(ctx, path) 100 | if err != nil { 101 | return nil, err 102 | } 103 | if valUncast == nil { 104 | return valMap, nil 105 | } 106 | 107 | valMapUncast, ok := valUncast.(map[string]interface{}) 108 | if !ok { 109 | return nil, fmt.Errorf("path %v is %T, expected map", path, valUncast) 110 | } 111 | 112 | for k, v := range valMapUncast { 113 | if v == nil { 114 | continue 115 | } 116 | vstr, ok := v.(string) 117 | if !ok { 118 | return nil, fmt.Errorf("key %s at path %v is %T, expected string", k, path, v) 119 | } 120 | valMap[k] = vstr 121 | } 122 | 123 | return valMap, nil 124 | } 125 | -------------------------------------------------------------------------------- /storage/objiter.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "sort" 8 | 9 | "github.com/go-git/go-git/v5/plumbing" 10 | "github.com/go-git/go-git/v5/plumbing/storer" 11 | ) 12 | 13 | type EncodedObjectIter struct { 14 | storer.EncodedObjectIter 15 | t plumbing.ObjectType 16 | store ChaintreeObjectStorer 17 | shards []string 18 | currentShardIndex int 19 | currentShardKeys []string 20 | currentShardKeyIndex int 21 | } 22 | 23 | func NewEncodedObjectIter(store ChaintreeObjectStorer, t plumbing.ObjectType) *EncodedObjectIter { 24 | return &EncodedObjectIter{ 25 | store: store, 26 | t: t, 27 | } 28 | } 29 | 30 | func (iter *EncodedObjectIter) getLeafKeysSorted(path []string) ([]string, error) { 31 | valUncast, _, err := iter.store.Chaintree().Dag.Resolve(context.Background(), path) 32 | if err != nil { 33 | return nil, err 34 | } 35 | if valUncast == nil { 36 | return nil, io.EOF 37 | } 38 | 39 | valMapUncast, ok := valUncast.(map[string]interface{}) 40 | if !ok { 41 | return nil, fmt.Errorf("path %v is %T, expected map", path, valUncast) 42 | } 43 | 44 | keys := make([]string, len(valMapUncast)) 45 | i := 0 46 | for key := range valMapUncast { 47 | keys[i] = key 48 | i++ 49 | } 50 | sort.Strings(keys) 51 | return keys, nil 52 | } 53 | 54 | func (iter *EncodedObjectIter) getShards() ([]string, error) { 55 | return iter.getLeafKeysSorted(ObjectsBasePath) 56 | } 57 | 58 | func (iter *EncodedObjectIter) getShardKeys(shard string) ([]string, error) { 59 | return iter.getLeafKeysSorted(append(ObjectsBasePath, shard)) 60 | } 61 | 62 | // Next returns the next object from the iterator. If the iterator has reached 63 | // the end it will return io.EOF as an error. If the object is retreieved 64 | // successfully error will be nil. 65 | func (iter *EncodedObjectIter) Next() (plumbing.EncodedObject, error) { 66 | if len(iter.shards) == 0 { 67 | shards, err := iter.getShards() 68 | if err != nil { 69 | return nil, err 70 | } 71 | if len(shards) == 0 { 72 | return nil, io.EOF 73 | } 74 | iter.shards = shards 75 | iter.currentShardIndex = 0 76 | } 77 | 78 | if iter.currentShardIndex >= len(iter.shards) { 79 | return nil, io.EOF 80 | } 81 | 82 | currentShard := iter.shards[iter.currentShardIndex] 83 | if len(iter.currentShardKeys) == 0 { 84 | shardKeys, err := iter.getShardKeys(currentShard) 85 | if err != nil { 86 | return nil, err 87 | } 88 | if len(shardKeys) == 0 { 89 | return nil, io.EOF 90 | } 91 | iter.currentShardKeys = shardKeys 92 | iter.currentShardKeyIndex = 0 93 | } 94 | 95 | currentObjectHash := plumbing.NewHash(currentShard + iter.currentShardKeys[iter.currentShardKeyIndex]) 96 | 97 | iter.currentShardKeyIndex++ 98 | 99 | // if the last key in the shard, empty out shard keys and increment shard num to trigger fetching next shard 100 | if iter.currentShardKeyIndex >= len(iter.currentShardKeys) { 101 | iter.currentShardKeys = []string{} 102 | iter.currentShardKeyIndex = 0 103 | iter.currentShardIndex++ 104 | } 105 | 106 | obj, err := iter.store.EncodedObject(plumbing.AnyObject, currentObjectHash) 107 | if err != nil { 108 | return nil, err 109 | } 110 | 111 | // if object was not the type being searched for, move to the next object 112 | if plumbing.AnyObject != iter.t && obj.Type() != iter.t { 113 | return iter.Next() 114 | } 115 | 116 | return obj, nil 117 | } 118 | 119 | func (iter *EncodedObjectIter) ForEach(cb func(plumbing.EncodedObject) error) error { 120 | return storer.ForEachIterator(iter, cb) 121 | } 122 | 123 | func (iter *EncodedObjectIter) Close() { 124 | } 125 | -------------------------------------------------------------------------------- /tupelo/tree/tree.go: -------------------------------------------------------------------------------- 1 | package tree 2 | 3 | import ( 4 | "context" 5 | "crypto/ecdsa" 6 | "fmt" 7 | "time" 8 | 9 | "github.com/ethereum/go-ethereum/crypto" 10 | "github.com/quorumcontrol/chaintree/chaintree" 11 | "github.com/quorumcontrol/messages/v2/build/go/transactions" 12 | "github.com/quorumcontrol/tupelo/sdk/consensus" 13 | "github.com/quorumcontrol/tupelo/sdk/gossip/client" 14 | ) 15 | 16 | var ErrNotFound = client.ErrNotFound 17 | 18 | type Tree struct { 19 | name string 20 | chainTree *consensus.SignedChainTree 21 | tupelo *client.Client 22 | } 23 | 24 | type Options struct { 25 | Name string 26 | Tupelo *client.Client 27 | Owners []string 28 | Key *ecdsa.PrivateKey 29 | AdditionalTxns []*transactions.Transaction 30 | } 31 | 32 | func (t *Tree) Name() string { 33 | return t.name 34 | } 35 | 36 | func (t *Tree) ChainTree() *consensus.SignedChainTree { 37 | return t.chainTree 38 | } 39 | 40 | func (t *Tree) Did() string { 41 | return t.chainTree.MustId() 42 | } 43 | 44 | func (t *Tree) Tupelo() *client.Client { 45 | return t.tupelo 46 | } 47 | 48 | func (t *Tree) Resolve(ctx context.Context, path []string) (interface{}, []string, error) { 49 | return t.ChainTree().ChainTree.Dag.Resolve(ctx, path) 50 | } 51 | 52 | func Find(ctx context.Context, tupelo *client.Client, did string) (*Tree, error) { 53 | chainTree, err := tupelo.GetLatest(ctx, did) 54 | if err == client.ErrNotFound { 55 | return nil, ErrNotFound 56 | } 57 | 58 | name, _, err := chainTree.ChainTree.Dag.Resolve(ctx, []string{"tree", "data", "name"}) 59 | if err != nil { 60 | return nil, err 61 | } 62 | if name == nil { 63 | return nil, fmt.Errorf("Invalid dgit ChainTree, must have .name set") 64 | } 65 | nameStr, ok := name.(string) 66 | if !ok { 67 | return nil, fmt.Errorf("Invalid dgit ChainTree, .name must be a string, got %T", name) 68 | } 69 | 70 | return New(nameStr, chainTree, tupelo), nil 71 | } 72 | 73 | func New(name string, chainTree *consensus.SignedChainTree, tupelo *client.Client) *Tree { 74 | return &Tree{ 75 | name: name, 76 | chainTree: chainTree, 77 | tupelo: tupelo, 78 | } 79 | } 80 | 81 | func Create(ctx context.Context, opts *Options) (*Tree, error) { 82 | var err error 83 | 84 | key := opts.Key 85 | if key == nil { 86 | key, err = crypto.GenerateKey() 87 | if err != nil { 88 | return nil, err 89 | } 90 | } 91 | 92 | owners := opts.Owners 93 | if len(owners) == 0 { 94 | owners = []string{crypto.PubkeyToAddress(key.PublicKey).String()} 95 | } 96 | 97 | chainTree, err := consensus.NewSignedChainTree(ctx, key.PublicKey, opts.Tupelo.DagStore()) 98 | if err != nil { 99 | return nil, err 100 | } 101 | 102 | setOwnershipTxn, err := chaintree.NewSetOwnershipTransaction(owners) 103 | if err != nil { 104 | return nil, err 105 | } 106 | 107 | nameTxn, err := chaintree.NewSetDataTransaction("name", opts.Name) 108 | if err != nil { 109 | return nil, err 110 | } 111 | 112 | creationTimestampTxn, err := chaintree.NewSetDataTransaction("createdAt", time.Now().Unix()) 113 | if err != nil { 114 | return nil, err 115 | } 116 | 117 | docTypeTxn, err := chaintree.NewSetDataTransaction("__doctype", "dgit") 118 | if err != nil { 119 | return nil, err 120 | } 121 | 122 | txns := []*transactions.Transaction{setOwnershipTxn, nameTxn, creationTimestampTxn, docTypeTxn} 123 | 124 | if opts.AdditionalTxns != nil { 125 | txns = append(txns, opts.AdditionalTxns...) 126 | } 127 | 128 | _, err = opts.Tupelo.PlayTransactions(ctx, chainTree, key, txns) 129 | if err != nil { 130 | return nil, err 131 | } 132 | 133 | return New(opts.Name, chainTree, opts.Tupelo), nil 134 | } 135 | -------------------------------------------------------------------------------- /tupelo/teamtree/teamtree.go: -------------------------------------------------------------------------------- 1 | package teamtree 2 | 3 | import ( 4 | "context" 5 | "crypto/ecdsa" 6 | "fmt" 7 | "strings" 8 | 9 | logging "github.com/ipfs/go-log" 10 | "github.com/quorumcontrol/chaintree/chaintree" 11 | "github.com/quorumcontrol/dgit/tupelo/tree" 12 | "github.com/quorumcontrol/messages/v2/build/go/transactions" 13 | tupelo "github.com/quorumcontrol/tupelo/sdk/gossip/client" 14 | ) 15 | 16 | var log = logging.Logger("decentragit.teamtree") 17 | 18 | var ErrNotFound = tree.ErrNotFound 19 | 20 | var membersPath = []string{"members"} 21 | 22 | type Options struct { 23 | Name string 24 | Tupelo *tupelo.Client 25 | Members Members 26 | } 27 | 28 | type TeamTree struct { 29 | *tree.Tree 30 | } 31 | 32 | func Find(ctx context.Context, client *tupelo.Client, did string) (*TeamTree, error) { 33 | log.Debugf("looking for team %s", did) 34 | t, err := tree.Find(ctx, client, did) 35 | if err != nil { 36 | return nil, err 37 | } 38 | return &TeamTree{t}, nil 39 | } 40 | 41 | func Create(ctx context.Context, opts *Options) (*TeamTree, error) { 42 | membersTxn, err := chaintree.NewSetDataTransaction(strings.Join(membersPath, "/"), opts.Members.Map()) 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | t, err := tree.Create(ctx, &tree.Options{ 48 | Name: opts.Name, 49 | Tupelo: opts.Tupelo, 50 | Owners: opts.Members.Dids(), 51 | AdditionalTxns: []*transactions.Transaction{membersTxn}, 52 | }) 53 | if err != nil { 54 | return nil, err 55 | } 56 | log.Infof("created %s team chaintree with did: %s", t.Name(), t.Did()) 57 | 58 | return &TeamTree{t}, nil 59 | } 60 | 61 | func (t *TeamTree) AddMembers(ctx context.Context, key *ecdsa.PrivateKey, members Members) error { 62 | currentMembers, err := t.ListMembers(ctx) 63 | if err != nil { 64 | return err 65 | } 66 | return t.SetMembers(ctx, key, append(currentMembers, members...)) 67 | } 68 | 69 | func (t *TeamTree) ListMembers(ctx context.Context) (Members, error) { 70 | path := append([]string{"tree", "data"}, membersPath...) 71 | valUncast, _, err := t.Resolve(ctx, path) 72 | if err != nil { 73 | return nil, err 74 | } 75 | if valUncast == nil { 76 | return Members{}, nil 77 | } 78 | 79 | valMapUncast, ok := valUncast.(map[string]interface{}) 80 | if !ok { 81 | return nil, fmt.Errorf("path %v is %T, expected map", path, valUncast) 82 | } 83 | 84 | members := make(Members, len(valMapUncast)) 85 | i := 0 86 | 87 | for name, v := range valMapUncast { 88 | did, ok := v.(string) 89 | if !ok { 90 | return nil, fmt.Errorf("key %s at path %v is %T, expected string", name, path, v) 91 | } 92 | members[i] = NewMember(did, name) 93 | i++ 94 | } 95 | return members, nil 96 | } 97 | 98 | func (t *TeamTree) RemoveMembers(ctx context.Context, key *ecdsa.PrivateKey, membersToRemove Members) error { 99 | currentMembers, err := t.ListMembers(ctx) 100 | if err != nil { 101 | return err 102 | } 103 | 104 | members := Members{} 105 | for _, member := range currentMembers { 106 | if !membersToRemove.IsMember(member.Did()) { 107 | members = append(members, member) 108 | } 109 | } 110 | 111 | return t.SetMembers(ctx, key, members) 112 | } 113 | 114 | func (t *TeamTree) SetMembers(ctx context.Context, key *ecdsa.PrivateKey, members Members) error { 115 | ownershipTxn, err := chaintree.NewSetOwnershipTransaction(members.Dids()) 116 | if err != nil { 117 | return err 118 | } 119 | 120 | membersTxn, err := chaintree.NewSetDataTransaction(strings.Join(membersPath, "/"), members.Map()) 121 | if err != nil { 122 | return err 123 | } 124 | 125 | _, err = t.Tupelo().PlayTransactions(ctx, t.ChainTree(), key, []*transactions.Transaction{ownershipTxn, membersTxn}) 126 | return err 127 | } 128 | -------------------------------------------------------------------------------- /keyring/keyring.go: -------------------------------------------------------------------------------- 1 | package keyring 2 | 3 | import ( 4 | "crypto/ecdsa" 5 | "fmt" 6 | 7 | keyringlib "github.com/99designs/keyring" 8 | "github.com/btcsuite/btcd/chaincfg" 9 | "github.com/btcsuite/btcutil/hdkeychain" 10 | "github.com/ethereum/go-ethereum/accounts" 11 | "github.com/ethereum/go-ethereum/common/hexutil" 12 | "github.com/ethereum/go-ethereum/crypto" 13 | logging "github.com/ipfs/go-log" 14 | ) 15 | 16 | var log = logging.Logger("decentragit.keyring") 17 | 18 | type Keyring struct { 19 | kr keyringlib.Keyring 20 | } 21 | 22 | var secureKeyringBackends = []keyringlib.BackendType{ 23 | keyringlib.WinCredBackend, 24 | keyringlib.KeychainBackend, 25 | keyringlib.SecretServiceBackend, 26 | keyringlib.KWalletBackend, 27 | keyringlib.PassBackend, 28 | } 29 | 30 | var KeyringPrettyNames = map[string]string{ 31 | "*keyring.keychain": "macOS Keychain Access", 32 | "*keyring.kwalletKeyring": "KWallet (KDE Wallet Manager)", 33 | "*keyring.windowsKeyring": "Windows Credential Manager", 34 | "*keyring.secretsKeyring": "libsecret", 35 | "*keyring.passKeyring": "pass", 36 | } 37 | 38 | var ErrKeyNotFound = keyringlib.ErrKeyNotFound 39 | 40 | func NewDefault() (*Keyring, error) { 41 | kr, err := keyringlib.Open(keyringlib.Config{ 42 | ServiceName: "decentragit", 43 | KeychainTrustApplication: true, 44 | KeychainAccessibleWhenUnlocked: true, 45 | AllowedBackends: secureKeyringBackends, 46 | }) 47 | if err != nil { 48 | return nil, err 49 | } 50 | k := &Keyring{kr} 51 | log.Info("keyring provider: " + k.Name()) 52 | return k, nil 53 | } 54 | 55 | func NewMemory() *Keyring { 56 | return &Keyring{keyringlib.NewArrayKeyring([]keyringlib.Item{})} 57 | } 58 | 59 | func (k *Keyring) Name() string { 60 | typeName := fmt.Sprintf("%T", k.kr) 61 | name, ok := KeyringPrettyNames[typeName] 62 | if !ok { 63 | return typeName 64 | } 65 | return name 66 | } 67 | 68 | func (k *Keyring) FindPrivateKey(keyName string) (key *ecdsa.PrivateKey, err error) { 69 | log.Debugf("finding private key %s", keyName) 70 | privateKeyItem, err := k.kr.Get(keyName) 71 | if err != nil { 72 | return nil, err 73 | } 74 | 75 | privateKeyBytes, err := hexutil.Decode(string(privateKeyItem.Data)) 76 | if err != nil { 77 | return nil, fmt.Errorf("error decoding user private key: %v", err) 78 | } 79 | 80 | key, err = crypto.ToECDSA(privateKeyBytes) 81 | if err != nil { 82 | return nil, fmt.Errorf("couldn't unmarshal ECDSA private key: %v", err) 83 | } 84 | 85 | return key, nil 86 | } 87 | 88 | func (k *Keyring) CreatePrivateKey(keyName string, seed []byte) (*ecdsa.PrivateKey, error) { 89 | derivedKeyPaths, err := accounts.ParseDerivationPath("m/44'/1392825'/0'/0") 90 | if err != nil { 91 | return nil, err 92 | } 93 | 94 | masterKey, err := hdkeychain.NewMaster(seed, &chaincfg.Params{ 95 | HDPrivateKeyID: [4]byte{0x04, 0x88, 0xad, 0xe4}, 96 | }) 97 | if err != nil { 98 | return nil, err 99 | } 100 | 101 | key := masterKey 102 | for _, n := range derivedKeyPaths { 103 | key, err = key.Child(n) 104 | if err != nil { 105 | panic(err) 106 | } 107 | 108 | } 109 | 110 | ecPrivateKey, err := key.ECPrivKey() 111 | privateKey := ecPrivateKey.ToECDSA() 112 | 113 | privateKeyItem := keyringlib.Item{ 114 | Key: keyName, 115 | Label: "decentragit." + keyName, 116 | Data: []byte(hexutil.Encode(crypto.FromECDSA(privateKey))), 117 | } 118 | 119 | err = k.kr.Set(privateKeyItem) 120 | if err != nil { 121 | return nil, fmt.Errorf("error saving private key for decentragit: %v", err) 122 | } 123 | 124 | return privateKey, nil 125 | } 126 | 127 | func (k *Keyring) DeletePrivateKey(keyName string) { 128 | err := k.kr.Remove(keyName) 129 | if err != nil { 130 | log.Warnf("error removing decentragit.%s key: %w", keyName, err) 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /storage/siaskynet/net.go: -------------------------------------------------------------------------------- 1 | package siaskynet 2 | 3 | import ( 4 | "io" 5 | "sync" 6 | 7 | "github.com/NebulousLabs/go-skynet" 8 | "github.com/go-git/go-git/v5/plumbing" 9 | "github.com/go-git/go-git/v5/plumbing/format/objfile" 10 | "github.com/quorumcontrol/dgit/storage" 11 | "go.uber.org/zap" 12 | ) 13 | 14 | type uploadJob struct { 15 | o plumbing.EncodedObject 16 | result chan string 17 | err chan error 18 | } 19 | 20 | type downloadJob struct { 21 | link string 22 | result chan plumbing.EncodedObject 23 | err chan error 24 | } 25 | 26 | type Skynet struct { 27 | sync.RWMutex 28 | 29 | uploaderCount int 30 | downloaderCount int 31 | uploadersStarted bool 32 | downloadersStarted bool 33 | uploadJobs chan *uploadJob 34 | downloadJobs chan *downloadJob 35 | 36 | log *zap.SugaredLogger 37 | } 38 | 39 | func InitSkynet(uploaderCount, downloaderCount int) *Skynet { 40 | return &Skynet{ 41 | uploaderCount: uploaderCount, 42 | downloaderCount: downloaderCount, 43 | uploadJobs: make(chan *uploadJob), 44 | downloadJobs: make(chan *downloadJob), 45 | log: log.Named("net"), 46 | } 47 | } 48 | 49 | func (s *Skynet) uploadObject(o plumbing.EncodedObject) (string, error) { 50 | buf, err := storage.ZlibBufferForObject(o) 51 | if err != nil { 52 | return "", err 53 | } 54 | 55 | uploadData := make(skynet.UploadData) 56 | uploadData[o.Hash().String()] = buf 57 | 58 | link, err := skynet.Upload(uploadData, skynet.DefaultUploadOptions) 59 | 60 | return link, nil 61 | } 62 | 63 | func (s *Skynet) startUploader() { 64 | for j := range s.uploadJobs { 65 | s.log.Debugf("uploading %s to Skynet", j.o.Hash()) 66 | link, err := s.uploadObject(j.o) 67 | if err != nil { 68 | j.err <- err 69 | continue 70 | } 71 | j.result <- link 72 | } 73 | } 74 | 75 | func (s *Skynet) startUploaders() { 76 | s.log.Debugf("starting %d uploader(s)", s.uploaderCount) 77 | 78 | for i := 0; i < s.uploaderCount; i++ { 79 | go s.startUploader() 80 | } 81 | } 82 | 83 | func (s *Skynet) UploadObject(o plumbing.EncodedObject) (chan string, chan error) { 84 | s.Lock() 85 | if !s.uploadersStarted { 86 | s.startUploaders() 87 | s.uploadersStarted = true 88 | } 89 | s.Unlock() 90 | 91 | result := make(chan string) 92 | err := make(chan error) 93 | 94 | s.uploadJobs <- &uploadJob{ 95 | o: o, 96 | result: result, 97 | err: err, 98 | } 99 | 100 | return result, err 101 | } 102 | 103 | func (s *Skynet) downloadObject(link string) (plumbing.EncodedObject, error) { 104 | objData, err := skynet.Download(link, skynet.DefaultDownloadOptions) 105 | if err != nil { 106 | return nil, err 107 | } 108 | 109 | o := &plumbing.MemoryObject{} 110 | 111 | reader, err := objfile.NewReader(objData) 112 | if err != nil { 113 | return nil, err 114 | } 115 | defer reader.Close() 116 | 117 | objType, size, err := reader.Header() 118 | if err != nil { 119 | return nil, err 120 | } 121 | 122 | o.SetType(objType) 123 | o.SetSize(size) 124 | 125 | if _, err = io.Copy(o, reader); err != nil { 126 | return nil, err 127 | } 128 | 129 | return o, nil 130 | } 131 | 132 | func (s *Skynet) startDownloader() { 133 | for j := range s.downloadJobs { 134 | s.log.Debugf("downloading %s from Skynet", j.link) 135 | o, err := s.downloadObject(j.link) 136 | if err != nil { 137 | j.err <- err 138 | continue 139 | } 140 | j.result <- o 141 | } 142 | } 143 | 144 | func (s *Skynet) startDownloaders() { 145 | s.log.Debugf("starting %d downloader(s)", s.downloaderCount) 146 | 147 | for i := 0; i < s.downloaderCount; i++ { 148 | go s.startDownloader() 149 | } 150 | } 151 | 152 | func (s *Skynet) DownloadObject(link string) (chan plumbing.EncodedObject, chan error) { 153 | s.Lock() 154 | if !s.downloadersStarted { 155 | s.startDownloaders() 156 | s.downloadersStarted = true 157 | } 158 | s.Unlock() 159 | 160 | result := make(chan plumbing.EncodedObject) 161 | err := make(chan error) 162 | 163 | s.downloadJobs <- &downloadJob{ 164 | link: link, 165 | result: result, 166 | err: err, 167 | } 168 | 169 | return result, err 170 | } 171 | -------------------------------------------------------------------------------- /transport/dgit/repo.go: -------------------------------------------------------------------------------- 1 | package dgit 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "sort" 8 | "strings" 9 | 10 | "github.com/go-git/go-git/v5" 11 | "github.com/go-git/go-git/v5/plumbing/transport" 12 | 13 | "github.com/quorumcontrol/dgit/constants" 14 | "github.com/quorumcontrol/dgit/keyring" 15 | ) 16 | 17 | var ErrEndpointNotFound = errors.New("endpoint not found") 18 | 19 | type Repo struct { 20 | *git.Repository 21 | 22 | endpoint *transport.Endpoint 23 | auth transport.AuthMethod 24 | } 25 | 26 | func NewRepo(gitRepo *git.Repository) *Repo { 27 | return &Repo{Repository: gitRepo} 28 | } 29 | 30 | func (r *Repo) Endpoint() (*transport.Endpoint, error) { 31 | if r.endpoint != nil { 32 | return r.endpoint, nil 33 | } 34 | 35 | remotes, err := r.Repository.Remotes() 36 | if err != nil { 37 | return nil, err 38 | } 39 | 40 | // get remotes sorted by dgit, then origin, then rest 41 | sort.Slice(remotes, func(i, j int) bool { 42 | iName := remotes[i].Config().Name 43 | jName := remotes[j].Config().Name 44 | if iName == "origin" && jName == constants.DgitRemote { 45 | return false 46 | } 47 | return iName == "origin" || iName == constants.DgitRemote 48 | }) 49 | 50 | dgitUrls := []string{} 51 | 52 | for _, remote := range remotes { 53 | for _, url := range remote.Config().URLs { 54 | if strings.HasPrefix(url, constants.Protocol) { 55 | dgitUrls = append(dgitUrls, url) 56 | } 57 | } 58 | } 59 | 60 | if len(dgitUrls) > 0 { 61 | ep, err := transport.NewEndpoint(dgitUrls[0]) 62 | if err != nil { 63 | return nil, err 64 | } 65 | 66 | r.endpoint = ep 67 | 68 | return ep, nil 69 | } 70 | 71 | return nil, ErrEndpointNotFound 72 | } 73 | 74 | func (r *Repo) MustEndpoint() *transport.Endpoint { 75 | ep, err := r.Endpoint() 76 | if err != nil { 77 | panic(err) 78 | } 79 | 80 | return ep 81 | } 82 | 83 | func (r *Repo) SetEndpoint(endpoint *transport.Endpoint) { 84 | r.endpoint = endpoint 85 | } 86 | 87 | func (r *Repo) Name() (string, error) { 88 | ep, err := r.Endpoint() 89 | if err != nil { 90 | return "", err 91 | } 92 | 93 | return ep.Host + ep.Path, nil 94 | } 95 | 96 | func (r *Repo) MustName() string { 97 | name, err := r.Name() 98 | if err != nil { 99 | panic(err) 100 | } 101 | 102 | return name 103 | } 104 | 105 | func (r *Repo) URL() (string, error) { 106 | ep, err := r.Endpoint() 107 | if err != nil { 108 | return "", err 109 | } 110 | 111 | return ep.String(), nil 112 | } 113 | 114 | func (r *Repo) MustURL() string { 115 | url, err := r.URL() 116 | if err != nil { 117 | panic(err) 118 | } 119 | 120 | return url 121 | } 122 | 123 | func (r *Repo) Username() (string, error) { 124 | repoConfig, err := r.Config() 125 | if err != nil { 126 | return "", err 127 | } 128 | 129 | dgitConfig := repoConfig.Merged.Section(constants.DgitConfigSection) 130 | 131 | if dgitConfig == nil { 132 | return "", fmt.Errorf("no decentragit configuration found; run `git config --global %s.username your-username`", constants.DgitConfigSection) 133 | } 134 | 135 | username := dgitConfig.Option("username") 136 | 137 | envUsername := os.Getenv("DGIT_USERNAME") 138 | if envUsername != "" { 139 | log.Warningf("[DEPRECATION] - DGIT_USERNAME is deprecated, please use DG_USERNAME") 140 | username = envUsername 141 | } 142 | envUsername = os.Getenv("DG_USERNAME") 143 | if envUsername != "" { 144 | username = envUsername 145 | } 146 | 147 | if username == "" { 148 | return "", fmt.Errorf("no decentragit username found; run `git config --global %s.username your-username`", constants.DgitConfigSection) 149 | } 150 | 151 | return username, nil 152 | } 153 | 154 | func (r *Repo) Auth() (transport.AuthMethod, error) { 155 | if r.auth != nil { 156 | return r.auth, nil 157 | } 158 | 159 | kr, err := keyring.NewDefault() 160 | if err != nil { 161 | return nil, err 162 | } 163 | 164 | username, err := r.Username() 165 | if err != nil { 166 | return nil, err 167 | } 168 | 169 | privateKey, err := kr.FindPrivateKey(username) 170 | if err != nil { 171 | return nil, err 172 | } 173 | 174 | r.auth = NewPrivateKeyAuth(privateKey) 175 | 176 | return r.auth, nil 177 | } 178 | -------------------------------------------------------------------------------- /msg/messages.go: -------------------------------------------------------------------------------- 1 | package msg 2 | 3 | var Welcome = ` 4 | Welcome to decentragit! 5 | 6 | Your decentragit username has been created as {{.username | bold | yellow}}. Others can grant you access to their repos by running: {{print "git dg team add " .username | bold | cyan}}. 7 | ` 8 | 9 | var AddDgitToRemote = ` 10 | decentragit would like to add {{.repourl | bold | yellow}} to the {{.remote | bold }} remote. This allows {{"git push" | bold | cyan}} to mirror this repository to decentragit. 11 | ` 12 | 13 | var AddDgitToRemoteConfirm = `{{"Is that ok?" | bold | green}}` 14 | 15 | var AddedDgitToRemote = ` 16 | Success, decentragit is now superpowering the {{.remote | bold }} remote. 17 | Continue using your normal git workflow and enjoy being decentralized. 18 | ` 19 | 20 | var AddDgitRemote = ` 21 | decentragit would like to add the {{.remote | bold }} remote to this repo so that you can fetch directly from decentragit. 22 | ` 23 | 24 | var AddDgitRemoteConfirm = AddDgitToRemoteConfirm 25 | 26 | var AddedDgitRemote = ` 27 | Success, decentragit is now accessible under the {{.remote | bold }} remote. 28 | {{print "git fetch " .remote | bold | cyan}} will work flawlessly from your decentralized repo. 29 | ` 30 | 31 | var FinalInstructions = ` 32 | You are setup and ready to roll with decentragit. 33 | Just use git as you usually would and enjoy a fully decentralized repo. 34 | 35 | If you would like to clone this decentragit repo on another machine, simply run {{print "git clone " .repourl | bold | cyan}}. 36 | 37 | If you use GitHub for this repo, we recommend adding a decentragit action to keep your post-PR branches in sync on decentragit. 38 | You can find the necessary action here: 39 | {{"https://github.com/quorumcontrol/dgit-github-action" | bold | blue}} 40 | 41 | Finally for more docs or if you have any issues, please visit our github page: 42 | {{"https://github.com/quorumcontrol/dgit" | bold | blue}} 43 | ` 44 | 45 | var PromptRepoNameConfirm = `It appears your repo is {{.repo | bold | yellow}}, is that correct?` 46 | 47 | var PromptRepoName = `{{"What is your full repo name?" | bold | green}}` 48 | 49 | var PromptRepoNameInvalid = `Enter a valid repo name in the form '${user_or_org}/${repo_name}'` 50 | 51 | var PromptRecoveryPhrase = `Please enter the recovery phrase for {{.username | bold | yellow}}: ` 52 | 53 | var PromptInvalidRecoveryPhrase = `Invalid recovery phrase, must be 24 words separated by spaces` 54 | 55 | var IncorrectRecoveryPhrase = ` 56 | {{"Incorrect recovery phrase:" | bold | red}} the given phrase does not provide ownership for {{.username | bold | yellow}}. Please ensure recovery phrase and username is correct. 57 | ` 58 | 59 | var PrivateKeyNotFound = ` 60 | Could not load your decentragit private key from {{.keyringProvider | bold }}. Try running {{"git dg init" | bold | cyan}} again. 61 | ` 62 | 63 | var UserSeedPhraseCreated = ` 64 | Below is your recovery phrase, you will need this to access your account from another machine or recover your account. 65 | 66 | {{"Please write this down in a secure location. This will be the only time the recovery phrase is displayed." | bold }} 67 | 68 | {{.seed | bold | magenta}} 69 | ` 70 | 71 | var UserNotFound = ` 72 | {{print "user " .user " does not exist" | bold | red}} 73 | ` 74 | 75 | var UserNotConfigured = "\nNo decentragit username configured. Run `git config --global {{.configSection}}.username your-username`.\n" 76 | 77 | var UserRestored = ` 78 | Your decentragit user {{.username | bold | yellow}} has been restored. This machine is now authorized to push to decentragit repos it owns. 79 | ` 80 | 81 | var RepoCreated = ` 82 | Your decentragit repo has been created at {{.repo | bold | yellow}}. 83 | 84 | decentragit repo identities and authorizations are secured by Tupelo - this repo's unique id is {{.did | bold | yellow}}. 85 | 86 | Storage of the repo is backed by Sia Skynet. 87 | ` 88 | 89 | var RepoNotFound = ` 90 | {{"decentragit repository does not exist." | bold | red}} 91 | 92 | You can create a decentragit repository by running {{"git dg init" | bold | cyan}}. 93 | ` 94 | 95 | var RepoNotFoundInPath = ` 96 | {{print "No .git directory found in " .path | bold | red}}. 97 | 98 | Please change directories to a git repo and run {{.cmd | bold | cyan}} again. 99 | 100 | If you would like to create a new repo, use {{"git init" | bold | cyan}} normally and run {{.cmd | bold | cyan}} again. 101 | ` 102 | 103 | var UsernamePrompt = ` 104 | {{ "What decentragit username would you like to use?" | bold | green }} 105 | ` 106 | -------------------------------------------------------------------------------- /storage/chaintree/reference.go: -------------------------------------------------------------------------------- 1 | package chaintree 2 | 3 | import ( 4 | "context" 5 | "sort" 6 | "strings" 7 | 8 | "github.com/go-git/go-git/v5/plumbing" 9 | "github.com/go-git/go-git/v5/plumbing/storer" 10 | gitstorage "github.com/go-git/go-git/v5/storage" 11 | "github.com/quorumcontrol/chaintree/chaintree" 12 | "github.com/quorumcontrol/messages/v2/build/go/transactions" 13 | "go.uber.org/zap" 14 | 15 | "github.com/quorumcontrol/dgit/storage" 16 | ) 17 | 18 | type ReferenceStorage struct { 19 | *storage.Config 20 | log *zap.SugaredLogger 21 | } 22 | 23 | var _ storer.ReferenceStorer = (*ReferenceStorage)(nil) 24 | 25 | func NewReferenceStorage(config *storage.Config) storer.ReferenceStorer { 26 | did := config.ChainTree.MustId() 27 | return &ReferenceStorage{ 28 | config, 29 | log.Named(did[len(did)-6:]), 30 | } 31 | } 32 | 33 | func (s *ReferenceStorage) SetReference(ref *plumbing.Reference) error { 34 | log.Debugf("set reference %s to %s", ref.Name().String(), ref.Hash().String()) 35 | return s.setData(ref.Name().String(), ref.Hash().String()) 36 | } 37 | 38 | func (s *ReferenceStorage) setData(key string, val interface{}) error { 39 | txn, err := chaintree.NewSetDataTransaction(key, val) 40 | if err != nil { 41 | return err 42 | } 43 | 44 | _, err = s.Tupelo.PlayTransactions(s.Ctx, s.ChainTree, s.PrivateKey, []*transactions.Transaction{txn}) 45 | if err != nil { 46 | return err 47 | } 48 | 49 | return nil 50 | } 51 | 52 | func (s *ReferenceStorage) CheckAndSetReference(ref *plumbing.Reference, old *plumbing.Reference) error { 53 | if ref == nil { 54 | return nil 55 | } 56 | 57 | if old != nil { 58 | tmp, err := s.Reference(ref.Name()) 59 | 60 | if err != nil && err != plumbing.ErrReferenceNotFound { 61 | return err 62 | } 63 | 64 | if tmp != nil && tmp.Hash() != old.Hash() { 65 | return gitstorage.ErrReferenceHasChanged 66 | } 67 | } 68 | 69 | return s.SetReference(ref) 70 | } 71 | 72 | // Reference returns the reference for a given reference name. 73 | func (s *ReferenceStorage) Reference(n plumbing.ReferenceName) (*plumbing.Reference, error) { 74 | // TODO: store default branch in chaintree as HEAD 75 | // random sha1s are showing up as HEAD from mystery code, 76 | // so for now, disable HEAD requests 77 | if n.String() == plumbing.HEAD.String() { 78 | return nil, plumbing.ErrReferenceNotFound 79 | } 80 | 81 | refPath := append([]string{"tree", "data"}, strings.Split(n.String(), "/")...) 82 | valUncast, _, err := s.ChainTree.ChainTree.Dag.Resolve(context.Background(), refPath) 83 | if err != nil { 84 | return nil, err 85 | } 86 | 87 | if valStr, ok := valUncast.(string); ok { 88 | return plumbing.NewHashReference(n, plumbing.NewHash(valStr)), nil 89 | } 90 | 91 | return nil, plumbing.ErrReferenceNotFound 92 | } 93 | 94 | func (s *ReferenceStorage) RemoveReference(n plumbing.ReferenceName) error { 95 | return s.setData(n.String(), nil) 96 | } 97 | 98 | func (s *ReferenceStorage) CountLooseRefs() (int, error) { 99 | allRefs, err := s.references() 100 | if err != nil { 101 | return 0, err 102 | } 103 | return len(allRefs), nil 104 | } 105 | 106 | func (r *ReferenceStorage) PackRefs() error { 107 | return nil 108 | } 109 | 110 | func (s *ReferenceStorage) references() ([]*plumbing.Reference, error) { 111 | refs := []*plumbing.Reference{} 112 | 113 | var recursiveFetch func(pathSlice []string) error 114 | 115 | recursiveFetch = func(pathSlice []string) error { 116 | log.Debugf("fetching references under: %s", pathSlice) 117 | 118 | valUncast, _, err := s.ChainTree.ChainTree.Dag.Resolve(context.Background(), pathSlice) 119 | 120 | if err != nil { 121 | return err 122 | } 123 | 124 | switch val := valUncast.(type) { 125 | case map[string]interface{}: 126 | sortedKeys := make([]string, len(val)) 127 | i := 0 128 | for key := range val { 129 | sortedKeys[i] = key 130 | i++ 131 | } 132 | sort.Strings(sortedKeys) 133 | 134 | for _, key := range sortedKeys { 135 | recursiveFetch(append(pathSlice, key)) 136 | } 137 | case string: 138 | refName := plumbing.ReferenceName(strings.Join(pathSlice[2:], "/")) 139 | log.Debugf("ref name is: %s", refName) 140 | log.Debugf("val is: %s", val) 141 | refs = append(refs, plumbing.NewHashReference(refName, plumbing.NewHash(val))) 142 | } 143 | return nil 144 | } 145 | 146 | err := recursiveFetch([]string{"tree", "data", "refs"}) 147 | if err != nil { 148 | return nil, err 149 | } 150 | 151 | return refs, nil 152 | } 153 | 154 | func (s *ReferenceStorage) IterReferences() (storer.ReferenceIter, error) { 155 | allRefs, err := s.references() 156 | if err != nil { 157 | return nil, err 158 | } 159 | return storer.NewReferenceSliceIter(allRefs), nil 160 | } 161 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | [![License](http://img.shields.io/:license-mit-blue.svg?style=flat-square)](http://badges.mit-license.org) 3 | [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-v2.0%20adopted-ff69b4.svg)](CODE_OF_CONDUCT.md) 4 | 5 | 6 |
7 |

8 | 9 | Logo 10 | 11 | 12 |

decentragit

13 | 14 |

15 | decentragit is an open-source project built by Quorum Control which combines 16 | the power of
git, the Tupelo DLT and Skynet from Sia.
17 | decentragit uses decentralized ownership and storage to make it trivial to 18 | create a decentralized, shareable git remote of your project.
19 | decentragit accomplishes this without changing your GitHub workflow except that you can keep collaborating when it goes down.
20 |

21 |

22 | 23 | 24 | ## Getting Started 25 | With three simple steps you can create a decentralized mirror of your existing github project. 26 | All changes will be automatically propogated to the mirror version and the git services you depend on will be there when you need them. 27 | 28 | ### Installation 29 | A quick install using [brew](https://brew.sh/) gets you started: 30 | ``` 31 | brew tap quorumcontrol/dgit && brew install dgit 32 | ``` 33 | *Or if you don't have homebrew check out our [simple manual installation instructions](https://github.com/quorumcontrol/dgit/wiki/dgit-Install-without-Homebrew).* 34 | 35 | ### Usage 36 | Next you will need to initialize each repo you want to make decentralized: 37 | ``` 38 | git dg init 39 | ``` 40 | 41 | This command does three things.
42 | 1. decentragit sets the appropriate remote urls in your repo's .git/config file.
43 | 2. decentragit creates a [ChainTree](https://docs.tupelo.org/docs/chaintree.html) which gets signed by the Tupelo DLT to specify ownership of the decentralized repo.
44 | 3. decentragit stores that repo on Skynet, the decentralized storage solution from Sia. 45 | 46 | From there you can proceed with normal git commands.
47 | 48 | If you ever want to pull from the mirror you can specify the mirror with a "dg:".
49 | As an example: 50 | `git clone dg://your_username/repo_name` 51 |
52 | 53 | If you want to keep your decentralized, shareable git remote in sync with your GitHub repo adding 54 | a simple github action as illustrated in [dgit-github-action](https://github.com/quorumcontrol/dgit-github-action) is all it takes. Once completed your decentragit decentralized shareable remote will always be up to date and ready when you need it.
55 | 56 | #### Publish to dg-pages 57 | New Feature! DGit now allows you to publish your frontend files (html, css, js, .vue, .react) files to Skynet. 58 | Just checkout your files into a new branch named dg-pages, commit and push! 59 | 60 | ** NOTE: You need to have the Skynet CLI tool installed ** [skynet-cli-repo](https://github.com/NebulousLabs/skynet-cli) 61 | 62 | #### Collaborators 63 | 64 | You can manage your repo's team of collaborators with the `git dg team` command: 65 | 66 | * `git dg team add [collaborator usernames]` 67 | * `git dg team list` 68 | * `git dg team remove [usernames]` 69 | 70 | Anyone on the team will be allowed to push to the repo in the current directory. 71 | 72 | #### Configuration 73 | 74 | - Username can be set any of the following ways: 75 | - `DG_USERNAME=[username]` env var 76 | - `git config --global decentragit.username [username]` sets it in `~/.gitconfig` 77 | - `git config decentragit.username [username]` sets it in `./.git/config` 78 | 79 | ### FAQ 80 | 81 | You can find answers to some of the most [frequently asked questions on the wiki](https://github.com/quorumcontrol/dgit/wiki/Frequently-Asked-Questions). 82 | 83 | ### Built With 84 | 85 | * [Git](https://git-scm.com/) 86 | * [Tupelo DLT](https://docs.tupelo.org/) 87 | * [Skynet](https://siasky.net/) 88 | 89 | ### Building 90 | - Clone this repo. 91 | - Run `make`. Generates `./git-dg` in top level dir. 92 | 93 | 94 | ## Contributing 95 | 96 | Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are **greatly appreciated**. 97 | 98 | 1. Fork the Project 99 | 2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`) 100 | 3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`) 101 | 4. Push to the Branch (`git push origin feature/AmazingFeature`) 102 | 5. Open a Pull Request 103 | 104 | 105 | ## License 106 | 107 | Distributed under the MIT License. See `LICENSE` for more information. 108 | 109 | 110 | ## Contact 111 | 112 | If you have any questions or concerns please [hop into our developer chat](https://gitter.im/quorumcontrol-dgit/community) 113 | on gitter and we will be glad to help. 114 | 115 | Project Link: [https://github.com/quorumcontrol/dgit](https://github.com/quorumcontrol/dgit) 116 | -------------------------------------------------------------------------------- /tupelo/repotree/repotree.go: -------------------------------------------------------------------------------- 1 | package repotree 2 | 3 | import ( 4 | "context" 5 | "crypto/ecdsa" 6 | "fmt" 7 | "os" 8 | "strings" 9 | 10 | "github.com/ethereum/go-ethereum/crypto" 11 | logging "github.com/ipfs/go-log" 12 | "github.com/quorumcontrol/chaintree/chaintree" 13 | "github.com/quorumcontrol/messages/v2/build/go/transactions" 14 | tupelo "github.com/quorumcontrol/tupelo/sdk/gossip/client" 15 | 16 | "github.com/quorumcontrol/dgit/tupelo/teamtree" 17 | "github.com/quorumcontrol/dgit/tupelo/tree" 18 | "github.com/quorumcontrol/dgit/tupelo/usertree" 19 | ) 20 | 21 | const ( 22 | DefaultObjectStorageType = "siaskynet" 23 | ) 24 | 25 | var log = logging.Logger("decentragit.repotree") 26 | 27 | var ErrNotFound = tree.ErrNotFound 28 | 29 | var teamsMapPath = []string{"teams"} 30 | 31 | type Options struct { 32 | Name string 33 | Tupelo *tupelo.Client 34 | Owners []string 35 | ObjectStorageType string 36 | } 37 | 38 | type RepoTree struct { 39 | *tree.Tree 40 | } 41 | 42 | func Find(ctx context.Context, repo string, client *tupelo.Client) (*RepoTree, error) { 43 | log.Debugf("looking for repo %s", repo) 44 | 45 | username := strings.Split(repo, "/")[0] 46 | reponame := strings.Join(strings.Split(repo, "/")[1:], "/") 47 | 48 | userTree, err := usertree.Find(ctx, username, client) 49 | if err != nil { 50 | return nil, err 51 | } 52 | log.Debugf("user chaintree found for %s - %s", username, userTree.Did()) 53 | 54 | userRepos, err := userTree.Repos(ctx) 55 | if err != nil { 56 | return nil, err 57 | } 58 | 59 | repoDid, ok := userRepos[reponame] 60 | if !ok || repoDid == "" { 61 | return nil, ErrNotFound 62 | } 63 | 64 | t, err := tree.Find(ctx, client, repoDid) 65 | if err != nil { 66 | return nil, err 67 | } 68 | log.Debugf("repo chaintree found for %s - %s", repo, t.Did()) 69 | 70 | return &RepoTree{t}, nil 71 | } 72 | 73 | func Create(ctx context.Context, opts *Options, ownerKey *ecdsa.PrivateKey) (*RepoTree, error) { 74 | log.Debugf("creating new repotree with options: %+v", opts) 75 | 76 | username := strings.Split(opts.Name, "/")[0] 77 | reponame := strings.Join(strings.Split(opts.Name, "/")[1:], "/") 78 | 79 | userTree, err := usertree.Find(ctx, username, opts.Tupelo) 80 | if err == usertree.ErrNotFound { 81 | return nil, fmt.Errorf("user %s does not exist (%w)", username, err) 82 | } 83 | if err != nil { 84 | return nil, err 85 | } 86 | log.Debugf("user chaintree found for %s - %s", username, userTree.Did()) 87 | 88 | isOwner, err := userTree.IsOwner(ctx, crypto.PubkeyToAddress(ownerKey.PublicKey).String()) 89 | if err != nil { 90 | return nil, err 91 | } 92 | if !isOwner { 93 | return nil, fmt.Errorf("can not create repo %s, current user is not an owner of %s", opts.Name, username) 94 | } 95 | 96 | userRepos, err := userTree.Repos(ctx) 97 | if err != nil { 98 | return nil, err 99 | } 100 | 101 | _, ok := userRepos[reponame] 102 | if ok { 103 | return nil, fmt.Errorf("repo %s already exists for %s", reponame, username) 104 | } 105 | 106 | if storage, found := os.LookupEnv("DGIT_OBJ_STORAGE"); found { 107 | log.Warningf("[DEPRECATION] - DGIT_OBJ_STORAGE is deprecated, please use DG_OBJ_STORAGE") 108 | opts.ObjectStorageType = storage 109 | } 110 | if storage, found := os.LookupEnv("DG_OBJ_STORAGE"); found { 111 | opts.ObjectStorageType = storage 112 | } 113 | if opts.ObjectStorageType == "" { 114 | opts.ObjectStorageType = DefaultObjectStorageType 115 | } 116 | 117 | config := map[string]map[string]string{"objectStorage": {"type": opts.ObjectStorageType}} 118 | configTxn, err := chaintree.NewSetDataTransaction("config", config) 119 | if err != nil { 120 | return nil, err 121 | } 122 | 123 | log.Debugf("using object storage type %s", opts.ObjectStorageType) 124 | 125 | defaultTeam, err := teamtree.Create(ctx, &teamtree.Options{ 126 | Name: opts.Name + " default team", 127 | Tupelo: opts.Tupelo, 128 | Members: teamtree.Members{ 129 | userTree, 130 | }, 131 | }) 132 | if err != nil { 133 | return nil, err 134 | } 135 | teamTxn, err := chaintree.NewSetDataTransaction(strings.Join(append(teamsMapPath, "default"), "/"), defaultTeam.Did()) 136 | if err != nil { 137 | return nil, err 138 | } 139 | 140 | t, err := tree.Create(ctx, &tree.Options{ 141 | Name: reponame, 142 | Tupelo: opts.Tupelo, 143 | Owners: []string{ 144 | defaultTeam.Did(), 145 | }, 146 | AdditionalTxns: []*transactions.Transaction{configTxn, teamTxn}, 147 | }) 148 | if err != nil { 149 | return nil, err 150 | } 151 | log.Infof("created %s repo chaintree with did: %s", t.Name(), t.Did()) 152 | 153 | err = userTree.AddRepo(ctx, ownerKey, reponame, t.Did()) 154 | if err != nil { 155 | return nil, err 156 | } 157 | 158 | return &RepoTree{t}, nil 159 | } 160 | 161 | func (t *RepoTree) Team(ctx context.Context, name string) (*teamtree.TeamTree, error) { 162 | path := append(append([]string{"tree", "data"}, teamsMapPath...), name) 163 | valUncast, _, err := t.Resolve(ctx, path) 164 | if err != nil { 165 | return nil, err 166 | } 167 | if valUncast == nil { 168 | return nil, teamtree.ErrNotFound 169 | } 170 | teamDid, ok := valUncast.(string) 171 | if !ok { 172 | return nil, fmt.Errorf("team %s is not a did string, got %T", name, valUncast) 173 | } 174 | return teamtree.Find(ctx, t.Tupelo(), teamDid) 175 | } 176 | -------------------------------------------------------------------------------- /storage/object.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "strings" 8 | 9 | "github.com/go-git/go-git/v5/plumbing" 10 | "github.com/go-git/go-git/v5/plumbing/format/objfile" 11 | "github.com/go-git/go-git/v5/plumbing/format/packfile" 12 | "github.com/go-git/go-git/v5/plumbing/storer" 13 | logging "github.com/ipfs/go-log" 14 | "github.com/quorumcontrol/chaintree/chaintree" 15 | "go.uber.org/zap" 16 | ) 17 | 18 | var log = logging.Logger("decentragit.storage.object") 19 | 20 | type ChaintreeObjectStorer interface { 21 | storer.EncodedObjectStorer 22 | Chaintree() *chaintree.ChainTree 23 | } 24 | 25 | type ChaintreeObjectStorage struct { 26 | *Config 27 | } 28 | 29 | var ObjectsBasePath = []string{"tree", "data", "objects"} 30 | 31 | func ObjectReadPath(h plumbing.Hash) []string { 32 | prefix := h.String()[0:2] 33 | key := h.String()[2:] 34 | return append(ObjectsBasePath, prefix, key) 35 | } 36 | 37 | func ObjectWritePath(h plumbing.Hash) string { 38 | return strings.Join(ObjectReadPath(h)[2:], "/") 39 | } 40 | 41 | func ZlibBufferForObject(o plumbing.EncodedObject) (*bytes.Buffer, error) { 42 | buf := bytes.NewBuffer(nil) 43 | 44 | writer := objfile.NewWriter(buf) 45 | defer writer.Close() 46 | 47 | reader, err := o.Reader() 48 | if err != nil { 49 | return nil, err 50 | } 51 | defer reader.Close() 52 | 53 | if err := writer.WriteHeader(o.Type(), o.Size()); err != nil { 54 | return nil, err 55 | } 56 | 57 | if _, err = io.Copy(writer, reader); err != nil { 58 | return nil, err 59 | } 60 | 61 | return buf, err 62 | } 63 | 64 | func (s *ChaintreeObjectStorage) Chaintree() *chaintree.ChainTree { 65 | return s.ChainTree.ChainTree 66 | } 67 | 68 | func (s *ChaintreeObjectStorage) NewEncodedObject() plumbing.EncodedObject { 69 | return &plumbing.MemoryObject{} 70 | } 71 | 72 | type PackWriter struct { 73 | bytes *bytes.Buffer 74 | closed bool 75 | storage ChaintreeObjectStorer 76 | log *zap.SugaredLogger 77 | } 78 | 79 | func NewPackWriter(s ChaintreeObjectStorer) *PackWriter { 80 | return &PackWriter{ 81 | bytes: bytes.NewBuffer(nil), 82 | closed: false, 83 | storage: s, 84 | log: log.Named("packwriter"), 85 | } 86 | } 87 | 88 | func (pw *PackWriter) Write(p []byte) (n int, err error) { 89 | pw.log.Debugf("writing %d bytes", len(p)) 90 | if pw.closed { 91 | return 0, fmt.Errorf("attempt to write to closed ChaintreePackWriter") 92 | } 93 | 94 | var written int64 95 | written, err = io.Copy(pw.bytes, bytes.NewReader(p)) 96 | 97 | if written != int64(len(p)) { 98 | pw.log.Warnf("got %d bytes but wrote %d", len(p), written) 99 | } 100 | 101 | n = int(written) 102 | return 103 | } 104 | 105 | func (pw *PackWriter) Close() error { 106 | pw.log.Debug("closing") 107 | pw.closed = true 108 | return pw.save() 109 | } 110 | 111 | func (pw *PackWriter) save() error { 112 | if !pw.closed { 113 | return fmt.Errorf("ChaintreePackWriter should be closed before saving") 114 | } 115 | 116 | // packfile parser needs a seekable Reader, so we can't just pass it pw.bytes 117 | scanner := packfile.NewScanner(bytes.NewReader(pw.bytes.Bytes())) 118 | 119 | po := &PackfileObserver{ 120 | storage: pw.storage, 121 | log: pw.log.Named("packfile-observer"), 122 | } 123 | 124 | parser, err := packfile.NewParser(scanner, po) 125 | if err != nil { 126 | return err 127 | } 128 | 129 | pw.log.Debug("parsing packfile") 130 | _, err = parser.Parse() 131 | return err 132 | } 133 | 134 | type PackfileObserver struct { 135 | currentObject *plumbing.MemoryObject 136 | currentTxn storer.Transaction 137 | storage ChaintreeObjectStorer 138 | log *zap.SugaredLogger 139 | } 140 | 141 | func (po *PackfileObserver) OnHeader(_ uint32) error { 142 | po.log.Debug("packfile header") 143 | return nil 144 | } 145 | 146 | func (po *PackfileObserver) OnInflatedObjectHeader(t plumbing.ObjectType, objSize, _ int64) error { 147 | po.log.Debugf("object header: %s", t) 148 | 149 | if po.currentObject != nil { 150 | return fmt.Errorf("got new object header before content was written") 151 | } 152 | 153 | po.currentObject = &plumbing.MemoryObject{} 154 | po.currentObject.SetType(t) 155 | po.currentObject.SetSize(objSize) 156 | 157 | return nil 158 | } 159 | 160 | func (po *PackfileObserver) OnInflatedObjectContent(h plumbing.Hash, _ int64, _ uint32, content []byte) error { 161 | po.log.Debugf("object content: %s", h) 162 | 163 | if po.currentObject == nil { 164 | return fmt.Errorf("got object content before header") 165 | } 166 | 167 | _, err := po.currentObject.Write(content) 168 | if err != nil { 169 | return err 170 | } 171 | 172 | txnStore, ok := po.storage.(storer.Transactioner) 173 | if !ok { 174 | return fmt.Errorf("storage does not support transactions") 175 | } 176 | 177 | if po.currentTxn == nil { 178 | po.log.Debug("beginning transaction") 179 | po.currentTxn = txnStore.Begin() 180 | } 181 | 182 | po.log.Debugf("adding current object to transaction") 183 | _, err = po.currentTxn.SetEncodedObject(po.currentObject) 184 | if err != nil { 185 | return err 186 | } 187 | 188 | po.currentObject = nil 189 | 190 | return nil 191 | } 192 | 193 | func (po *PackfileObserver) OnFooter(_ plumbing.Hash) error { 194 | po.log.Debug("packfile footer; committing current transaction") 195 | 196 | var err error 197 | if po.currentTxn != nil { 198 | err = po.currentTxn.Commit() 199 | po.currentTxn = nil 200 | } 201 | 202 | return err 203 | } 204 | -------------------------------------------------------------------------------- /transport/dgit/client.go: -------------------------------------------------------------------------------- 1 | package dgit 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "path" 7 | 8 | "github.com/go-git/go-git/v5/plumbing/transport" 9 | gitclient "github.com/go-git/go-git/v5/plumbing/transport/client" 10 | "github.com/go-git/go-git/v5/plumbing/transport/server" 11 | logging "github.com/ipfs/go-log" 12 | "github.com/quorumcontrol/chaintree/nodestore" 13 | tupelo "github.com/quorumcontrol/tupelo/sdk/gossip/client" 14 | 15 | "github.com/quorumcontrol/dgit/constants" 16 | "github.com/quorumcontrol/dgit/tupelo/clientbuilder" 17 | "github.com/quorumcontrol/dgit/tupelo/repotree" 18 | "github.com/quorumcontrol/dgit/tupelo/teamtree" 19 | "github.com/quorumcontrol/dgit/tupelo/usertree" 20 | ) 21 | 22 | var log = logging.Logger("decentragit.client") 23 | 24 | type Client struct { 25 | transport.Transport 26 | 27 | ctx context.Context 28 | Tupelo *tupelo.Client 29 | Nodestore nodestore.DagStore 30 | server transport.Transport 31 | } 32 | 33 | func Protocol() string { 34 | return constants.Protocol 35 | } 36 | 37 | func Default() (*Client, error) { 38 | client, ok := gitclient.Protocols[constants.Protocol] 39 | if !ok { 40 | return nil, fmt.Errorf("no client registered for '%s'", constants.Protocol) 41 | } 42 | 43 | asClient, ok := client.(*Client) 44 | if !ok { 45 | return nil, fmt.Errorf("%s registered %T, but is not a dgit.Tupelo", constants.Protocol, client) 46 | } 47 | 48 | return asClient, nil 49 | } 50 | 51 | func NewClient(ctx context.Context, basePath string) (*Client, error) { 52 | var err error 53 | c := &Client{ctx: ctx} 54 | dir := path.Join(basePath, constants.Protocol) 55 | c.Tupelo, c.Nodestore, err = clientbuilder.Build(ctx, dir) 56 | return c, err 57 | } 58 | 59 | // FIXME: this probably shouldn't be here 60 | func NewLocalClient(ctx context.Context) (*Client, error) { 61 | var err error 62 | c := &Client{ctx: ctx} 63 | c.Tupelo, c.Nodestore, err = clientbuilder.BuildLocal(ctx) 64 | return c, err 65 | } 66 | 67 | // FIXME: this probably shouldn't be here 68 | func (c *Client) CreateRepoTree(ctx context.Context, endpoint *transport.Endpoint, auth transport.AuthMethod) (*repotree.RepoTree, error) { 69 | var ( 70 | pkAuth *PrivateKeyAuth 71 | ok bool 72 | ) 73 | if pkAuth, ok = auth.(*PrivateKeyAuth); !ok { 74 | return nil, fmt.Errorf("unable to cast %T to PrivateKeyAuth", auth) 75 | } 76 | return repotree.Create(ctx, &repotree.Options{ 77 | Name: endpoint.Host + endpoint.Path, 78 | Tupelo: c.Tupelo, 79 | }, pkAuth.Key()) 80 | } 81 | 82 | func (c *Client) FindRepoTree(ctx context.Context, repo string) (*repotree.RepoTree, error) { 83 | return repotree.Find(ctx, repo, c.Tupelo) 84 | } 85 | 86 | func (c *Client) RegisterAsDefault() { 87 | gitclient.InstallProtocol(constants.Protocol, c) 88 | } 89 | 90 | func (c *Client) NewUploadPackSession(ep *transport.Endpoint, auth transport.AuthMethod) (transport.UploadPackSession, error) { 91 | loader := NewChainTreeLoader(c.ctx, c.Tupelo, c.Nodestore, auth) 92 | return server.NewServer(loader).NewUploadPackSession(ep, auth) 93 | } 94 | 95 | func (c *Client) NewReceivePackSession(ep *transport.Endpoint, auth transport.AuthMethod) (transport.ReceivePackSession, error) { 96 | loader := NewChainTreeLoader(c.ctx, c.Tupelo, c.Nodestore, auth) 97 | return server.NewServer(loader).NewReceivePackSession(ep, auth) 98 | } 99 | 100 | func (c *Client) AddRepoCollaborator(ctx context.Context, repo *Repo, collaborators []string) error { 101 | repoName, err := repo.Name() 102 | if err != nil { 103 | return err 104 | } 105 | 106 | repoTree, err := c.FindRepoTree(ctx, repoName) 107 | if err != nil { 108 | return err 109 | } 110 | 111 | auth, err := repo.Auth() 112 | if err != nil { 113 | return err 114 | } 115 | 116 | var ( 117 | pkAuth *PrivateKeyAuth 118 | ok bool 119 | ) 120 | if pkAuth, ok = auth.(*PrivateKeyAuth); !ok { 121 | return fmt.Errorf("auth is not castable to PrivateKeyAuth; was a %T", auth) 122 | } 123 | 124 | team, err := repoTree.Team(ctx, "default") 125 | if err != nil { 126 | return err 127 | } 128 | 129 | members := make(teamtree.Members, len(collaborators)) 130 | for i, username := range collaborators { 131 | user, err := usertree.Find(ctx, username, c.Tupelo) 132 | if err == usertree.ErrNotFound { 133 | return fmt.Errorf("User %s not found", username) 134 | } 135 | if err != nil { 136 | return err 137 | } 138 | 139 | members[i] = teamtree.NewMember(user.Did(), username) 140 | } 141 | 142 | return team.AddMembers(ctx, pkAuth.Key(), members) 143 | } 144 | 145 | func (c *Client) ListRepoCollaborators(ctx context.Context, repo *Repo) ([]string, error) { 146 | repoName, err := repo.Name() 147 | if err != nil { 148 | return []string{}, err 149 | } 150 | 151 | repoTree, err := c.FindRepoTree(ctx, repoName) 152 | if err != nil { 153 | return []string{}, err 154 | } 155 | 156 | team, err := repoTree.Team(ctx, "default") 157 | if err != nil { 158 | return []string{}, err 159 | } 160 | 161 | members, err := team.ListMembers(ctx) 162 | if err != nil { 163 | return []string{}, err 164 | } 165 | 166 | return members.Names(), nil 167 | } 168 | 169 | func (c *Client) RemoveRepoCollaborator(ctx context.Context, repo *Repo, collaborators []string) error { 170 | repoName, err := repo.Name() 171 | if err != nil { 172 | return err 173 | } 174 | 175 | repoTree, err := c.FindRepoTree(ctx, repoName) 176 | if err != nil { 177 | return err 178 | } 179 | 180 | auth, err := repo.Auth() 181 | if err != nil { 182 | return err 183 | } 184 | 185 | var ( 186 | pkAuth *PrivateKeyAuth 187 | ok bool 188 | ) 189 | if pkAuth, ok = auth.(*PrivateKeyAuth); !ok { 190 | return fmt.Errorf("auth is not castable to PrivateKeyAuth; was a %T", auth) 191 | } 192 | 193 | team, err := repoTree.Team(ctx, "default") 194 | if err != nil { 195 | return err 196 | } 197 | 198 | members := make(teamtree.Members, len(collaborators)) 199 | for i, username := range collaborators { 200 | did, err := usertree.Did(username) 201 | if err != nil { 202 | return err 203 | } 204 | 205 | members[i] = teamtree.NewMember(did, username) 206 | } 207 | 208 | return team.RemoveMembers(ctx, pkAuth.Key(), members) 209 | } 210 | -------------------------------------------------------------------------------- /storage/chaintree/object.go: -------------------------------------------------------------------------------- 1 | package chaintree 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | 9 | "github.com/quorumcontrol/dgit/storage" 10 | 11 | "github.com/go-git/go-git/v5/plumbing" 12 | "github.com/go-git/go-git/v5/plumbing/format/objfile" 13 | "github.com/go-git/go-git/v5/plumbing/storer" 14 | "github.com/go-git/go-git/v5/storage/memory" 15 | format "github.com/ipfs/go-ipld-format" 16 | logging "github.com/ipfs/go-log" 17 | "github.com/quorumcontrol/chaintree/chaintree" 18 | "github.com/quorumcontrol/messages/v2/build/go/transactions" 19 | "go.uber.org/zap" 20 | ) 21 | 22 | var log = logging.Logger("decentragit.storage.chaintree") 23 | 24 | type ObjectStorage struct { 25 | *storage.ChaintreeObjectStorage 26 | log *zap.SugaredLogger 27 | packfileWriter io.WriteCloser 28 | } 29 | 30 | var _ storage.ChaintreeObjectStorer = (*ObjectStorage)(nil) 31 | var _ storer.PackfileWriter = (*ObjectStorage)(nil) 32 | var _ storer.Transactioner = (*ObjectStorage)(nil) 33 | 34 | func NewObjectStorage(config *storage.Config) storer.EncodedObjectStorer { 35 | did := config.ChainTree.MustId() 36 | return &ObjectStorage{ 37 | &storage.ChaintreeObjectStorage{config}, 38 | log.Named(did[len(did)-6:]), 39 | nil, 40 | } 41 | } 42 | 43 | type ObjectTransaction struct { 44 | temporal storer.EncodedObjectStorer 45 | storage storer.EncodedObjectStorer 46 | } 47 | 48 | var _ storer.Transaction = (*ObjectTransaction)(nil) 49 | 50 | func (s *ObjectStorage) Begin() storer.Transaction { 51 | return &ObjectTransaction{ 52 | temporal: memory.NewStorage(), 53 | storage: s, 54 | } 55 | } 56 | 57 | func (ot *ObjectTransaction) SetEncodedObject(o plumbing.EncodedObject) (plumbing.Hash, error) { 58 | return ot.temporal.SetEncodedObject(o) 59 | } 60 | 61 | func (ot *ObjectTransaction) EncodedObject(t plumbing.ObjectType, h plumbing.Hash) (plumbing.EncodedObject, error) { 62 | return ot.temporal.EncodedObject(t, h) 63 | } 64 | 65 | func (ot *ObjectTransaction) Commit() error { 66 | iter, err := ot.temporal.IterEncodedObjects(plumbing.AnyObject) 67 | if err != nil { 68 | return err 69 | } 70 | 71 | tupeloTxns := make([]*transactions.Transaction, 0) 72 | 73 | ctStorage, ok := ot.storage.(*ObjectStorage) 74 | if !ok { 75 | return fmt.Errorf("could not cast storage to chaintree.ObjectStorage; was %T", ot.storage) 76 | } 77 | 78 | err = iter.ForEach(func(o plumbing.EncodedObject) error { 79 | tupeloTxn, err := ctStorage.SetEncodedObjectTxn(o) 80 | if err != nil { 81 | return err 82 | } 83 | 84 | tupeloTxns = append(tupeloTxns, tupeloTxn) 85 | 86 | return nil 87 | }) 88 | 89 | if len(tupeloTxns) > 0 { 90 | _, err = ctStorage.Tupelo.PlayTransactions(ctStorage.Ctx, ctStorage.ChainTree, ctStorage.PrivateKey, tupeloTxns) 91 | if err != nil { 92 | return err 93 | } 94 | } 95 | 96 | return nil 97 | } 98 | 99 | func (ot *ObjectTransaction) Rollback() error { 100 | ot.temporal = nil 101 | return nil 102 | } 103 | 104 | func (s *ObjectStorage) PackfileWriter() (io.WriteCloser, error) { 105 | return storage.NewPackWriter(s), nil 106 | } 107 | 108 | func (s *ObjectStorage) SetEncodedObjectTxn(o plumbing.EncodedObject) (*transactions.Transaction, error) { 109 | s.log.Debugf("saving %s with type %s", o.Hash().String(), o.Type().String()) 110 | 111 | if s.PrivateKey == nil { 112 | return nil, fmt.Errorf("Must specify treeKey during NewObjectStorage init") 113 | } 114 | 115 | if o.Type() == plumbing.OFSDeltaObject || o.Type() == plumbing.REFDeltaObject { 116 | return nil, plumbing.ErrInvalidType 117 | } 118 | 119 | buf, err := storage.ZlibBufferForObject(o) 120 | if err != nil { 121 | return nil, err 122 | } 123 | 124 | objectBytes, err := ioutil.ReadAll(buf) 125 | if err != nil { 126 | return nil, err 127 | } 128 | 129 | // TODO: save each git object as cid 130 | // currently objects/sha1[0:2]/ is a map with { sha1[2:] => cbor bytes } 131 | // should be objects/sha1[0:2]/ is a map with { sha1[2:] => cid } 132 | transaction, err := chaintree.NewSetDataTransaction(storage.ObjectWritePath(o.Hash()), objectBytes) 133 | if err != nil { 134 | return nil, err 135 | } 136 | 137 | return transaction, nil 138 | } 139 | 140 | func (s *ObjectStorage) SetEncodedObject(o plumbing.EncodedObject) (plumbing.Hash, error) { 141 | transaction, err := s.SetEncodedObjectTxn(o) 142 | if err != nil { 143 | return plumbing.ZeroHash, err 144 | } 145 | 146 | _, err = s.Tupelo.PlayTransactions(s.Ctx, s.ChainTree, s.PrivateKey, []*transactions.Transaction{transaction}) 147 | if err != nil { 148 | return plumbing.ZeroHash, err 149 | } 150 | 151 | return o.Hash(), nil 152 | } 153 | 154 | func (s *ObjectStorage) HasEncodedObject(h plumbing.Hash) (err error) { 155 | if _, err := s.EncodedObject(plumbing.AnyObject, h); err != nil { 156 | return err 157 | } 158 | return nil 159 | } 160 | 161 | func (s *ObjectStorage) EncodedObjectSize(h plumbing.Hash) (size int64, err error) { 162 | o, err := s.EncodedObject(plumbing.AnyObject, h) 163 | if err != nil { 164 | return 0, err 165 | } 166 | return o.Size(), nil 167 | } 168 | 169 | func (s *ObjectStorage) EncodedObject(t plumbing.ObjectType, h plumbing.Hash) (plumbing.EncodedObject, error) { 170 | s.log.Debugf("fetching %s with type %s", h.String(), t.String()) 171 | 172 | valUncast, _, err := s.ChainTree.ChainTree.Dag.Resolve(s.Ctx, storage.ObjectReadPath(h)) 173 | if err == format.ErrNotFound { 174 | s.log.Debugf("%s not found", h.String()) 175 | return nil, plumbing.ErrObjectNotFound 176 | } 177 | if err != nil { 178 | s.log.Errorf("chaintree resolve error for %s: %v", h.String(), err) 179 | return nil, err 180 | } 181 | if valUncast == nil { 182 | s.log.Debugf("%s not found", h.String()) 183 | return nil, plumbing.ErrObjectNotFound 184 | } 185 | 186 | o := &plumbing.MemoryObject{} 187 | 188 | buf := bytes.NewBuffer(valUncast.([]byte)) 189 | reader, err := objfile.NewReader(buf) 190 | if err != nil { 191 | s.log.Errorf("new reader error for %s: %v", h.String(), err) 192 | return nil, err 193 | } 194 | defer reader.Close() 195 | 196 | objType, size, err := reader.Header() 197 | if err != nil { 198 | s.log.Errorf("error decoding header for %s: %v", h.String(), err) 199 | return nil, err 200 | } 201 | 202 | o.SetType(objType) 203 | o.SetSize(size) 204 | 205 | if plumbing.AnyObject != t && o.Type() != t { 206 | s.log.Debugf("%s not found, mismatched types, expected %s, got %s", h.String(), t.String(), o.Type().String()) 207 | return nil, plumbing.ErrObjectNotFound 208 | } 209 | 210 | if _, err = io.Copy(o, reader); err != nil { 211 | s.log.Errorf("error filling object %s: %v", h.String(), err) 212 | return nil, err 213 | } 214 | 215 | return o, nil 216 | } 217 | 218 | func (s *ObjectStorage) IterEncodedObjects(t plumbing.ObjectType) (storer.EncodedObjectIter, error) { 219 | return storage.NewEncodedObjectIter(s, t), nil 220 | } 221 | -------------------------------------------------------------------------------- /tupelo/clientbuilder/clientbuilder.go: -------------------------------------------------------------------------------- 1 | package clientbuilder 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "path" 8 | "time" 9 | 10 | "github.com/ipfs/go-bitswap" 11 | ds "github.com/ipfs/go-datastore" 12 | dsync "github.com/ipfs/go-datastore/sync" 13 | flatfs "github.com/ipfs/go-ds-flatfs" 14 | blockstore "github.com/ipfs/go-ipfs-blockstore" 15 | logging "github.com/ipfs/go-log" 16 | tupelo "github.com/quorumcontrol/tupelo/sdk/gossip/client" 17 | "github.com/quorumcontrol/tupelo/sdk/gossip/client/pubsubinterfaces/pubsubwrapper" 18 | "github.com/quorumcontrol/tupelo/sdk/gossip/types" 19 | "github.com/quorumcontrol/tupelo/sdk/p2p" 20 | ) 21 | 22 | const ngToml = ` 23 | id = "gossip4" 24 | 25 | BootstrapAddresses = [ 26 | "/ip4/52.88.225.180/tcp/34001/ipfs/16Uiu2HAmQgHD5eqxDskKe21ythvG2T9o5i521kEdLrdgjc94sgCr", 27 | "/ip4/15.188.248.188/tcp/34001/ipfs/16Uiu2HAmNBupyDCfGSqo6ypNUmpHbYWy4jSaTBsbz6uRnsnY3JZN", 28 | ] 29 | 30 | [[signers]] 31 | VerKeyHex = "0x02bef6fc2cd935f83a19e07ef17e3561ed44f5fccbf3a8f74007dba7984922ae7f4d0f869a798c430d942d66c2446db3336cd8b8c5db86e08492417169b766ff2513fb7a57c3aea95b3291eb0c38ffcb84d8bafc502dd602d554edbf4e42323a3f2ef45476e6136d84e3307beaa1d505005396ec188394321895da27e7a015f5" 32 | DestKeyHex = "0x04663234f326da31b9ab32a99e41cd399b415f9a55d989f2c9e2b338d1cba0b61196d6706b17826a45f26cd1eb9be2ae131fcf98d1c72547e450a9dc5af709eb0f" 33 | 34 | [[signers]] 35 | VerKeyHex = "0x8039afab271b89dd22180602133efa884fbe090249c1832925ff69ce9f5b647147299f5585f6d7b857b23670015250ba60a456c11ae1927f8c38b6f8ac9f8d6b5b5dc37648c61f7b4a95ad14e8967ddf898cca7bb28445893885c0c1030fc57a842e2cbcd6d03689691f58fe202ada5a1bfe6e7e01de149a4514f29e9a2c349e" 36 | DestKeyHex = "0x04dc48195fc5af3c611fa8b28e8d4c0adba8deaf720ef07339abfe4022847d711cccf59d170f93357b0916b39f7045662000e004472daea5b0233c89c64020bf24" 37 | 38 | [[signers]] 39 | VerKeyHex = "0x47e9f5650766b3ce6519c45f515d18ec225ae2191ca9350d0f0993efe781bc1703c718a8ac42a4806b1df641aab90b78a0e6f97d036b445c6290724340c72e027564c6aa1b56e3735e92e460fa0508c861b89e906de5cffd537ae04c42dd89837710040ddce44df0cf9ec7bcacaa7dd1e630ffe88d75d612660419e299e36830" 40 | DestKeyHex = "0x04e97151be30893132f2670ddb1ca8c2bdc586b3252a96d8ef42623f09f7159f9f0d48041d9f11e38d35218d8d10deba95e41ba58eb68b8034c8fc05cf05ea31ca" 41 | 42 | [[signers]] 43 | VerKeyHex = "0x760bc51e4cb173a46487f447961e9ed226798f4ef52e4c2a23fecc1f74f65a1558b669d5551f308ec7ffd7654c0aa223c39055ce086c3d6a5796b360aedd7c3862c71bdccc69ad6fc103a680ce5c41519a6dbbcc6f2c27e1a39f41159de350bb6d06c2b36651f647334a06036607b8c264e02d9913764e4b0002ba5ee01a8c7c" 44 | DestKeyHex = "0x04d75251a9db8182e63a2f1483bbc240e18ca232de33207ee704275b03f7c419f5807632c8dd291589349dafce86c0e388f2f669ba9f591fdcadbe03192b72aa3c" 45 | 46 | [[signers]] 47 | VerKeyHex = "0x77b685350b31d22b50d991cf29d00e8f22a7b22039fcb63264a06503d01797e0825c9dd78828a7eea68543ff309d9195817746b805e5a241bac25599aa480e7182b0dd479934e5f5bb2b58887363ddd68e67fb8cda4811c565cd585f75aaf86171b6283a384c40c8de2798dd107a1b491fe5f5ec5051f16f1d7d4f37fc7f54fa" 48 | DestKeyHex = "0x04a3ff0167eeecbdaf3f4df25acabca8d18f6f138e60daf2e96b9db0edbc70eee8054ce211e950ecf2b19c9d8db92c3b205a62e8bfd9b68f35a26a5e4343a18fa2" 49 | ` 50 | 51 | const localNgToml = ` 52 | id = "tupelolocal" 53 | BootstrapAddresses = [ 54 | "/ip4/127.0.0.1/tcp/34001/ipfs/16Uiu2HAm3TGSEKEjagcCojSJeaT5rypaeJMKejijvYSnAjviWwV5" 55 | ] 56 | [[signers]] 57 | VerKeyHex = "0x15796b266a7d6b7c6b29c5bf97ad376fe8457e4d56bb0612ec8703c65ca7b6bb5dca004d55f5238d7764cd100c9e9cac3c5abce902bae8a5f9c29de716a145595b071b1b7038a48b4f6f88e7664b38c02062f64b3ceb499e4cbb82361457dcd731f5b48901871e7fd56a9c91ab3e06d3f7cb27288962686d9a05e02c1482f01f" 58 | DestKeyHex = "0x04f6dee3f7da1da58afd6ee58ea6b858fb67664fc6e2240bb6e3a75c0e1db9bbef5f413c8604bb864513d3cf27eca60b539b048b2a08f8799570c14dfb73f3f391" 59 | 60 | [[signers]] 61 | VerKeyHex = "0x7be8c92c8c295ef3e97be28f469f5f94d10ee7db4d202776bee5cf55c62d508a0c3550a19342d768ff073c0798ce003646df586ef588a9e9443a0ca86a234ed15150dc98ecc3f1071649fca03426f1c8c215a90752f51faa3e2e788e1dae2e9e5cf87c1ca4239a0949a0ba6ea09c061a538372cc4230dedafae929b170ad7704" 62 | DestKeyHex = "0x0438b196bddb9c3ec395b8ccb07bdab44ec768c084e7141b09ac5638d47fffbd5e7b7623f499a2e714e31464a356a0e30ad7c93045b6cd9957b45e957cc15dcb99" 63 | 64 | [[signers]] 65 | VerKeyHex = "0x88aefad94805db01cacaf190f47bc9e40f584b5085c651da168ac4034d570b4750bf7b23803d204e483e407a5ca34ee7f7a434733346451cf3f5d26c0d11e5ac45398a03fbba2d3b0dfc21cdf14615430cea394bd9423d8527eaa82a96aa6d20655724d99770ee3488b6537d6be143b84b21ad5ee12c190048757fe453313fd2" 66 | DestKeyHex = "0x0468924bd1341b5cec1fed888aaf1e3caa94e7d0f13d4f4573b01b296374b9e710a58a7b40e7161c0bcf7fd41832441ca21076f3846e854c8d8c640f2469a552b1" 67 | ` 68 | 69 | func BuildLocal(ctx context.Context) (*tupelo.Client, *p2p.BitswapPeer, error) { 70 | logging.SetAllLoggers(logging.LevelFatal) 71 | 72 | ngConfig, err := types.TomlToConfig(localNgToml) 73 | if err != nil { 74 | return nil, nil, err 75 | } 76 | return BuildWithConfig(ctx, &Config{ 77 | Storage: dsync.MutexWrap(ds.NewMapDatastore()), 78 | NotaryGroupConfig: ngConfig, 79 | }) 80 | } 81 | 82 | func Build(ctx context.Context, storagePath string) (*tupelo.Client, *p2p.BitswapPeer, error) { 83 | if err := os.MkdirAll(storagePath, 0755); err != nil { 84 | return nil, nil, err 85 | } 86 | 87 | storage, err := flatfs.CreateOrOpen(path.Join(storagePath, "storage"), flatfs.NextToLast(2), true) 88 | if err != nil { 89 | return nil, nil, err 90 | } 91 | 92 | return BuildWithConfig(ctx, &Config{Storage: storage}) 93 | } 94 | 95 | type Config struct { 96 | NotaryGroupConfig *types.Config 97 | Storage ds.Batching 98 | } 99 | 100 | func BuildWithConfig(ctx context.Context, config *Config) (*tupelo.Client, *p2p.BitswapPeer, error) { 101 | var err error 102 | 103 | ngConfig := config.NotaryGroupConfig 104 | if ngConfig == nil { 105 | ngConfig, err = types.TomlToConfig(ngToml) 106 | if err != nil { 107 | return nil, nil, err 108 | } 109 | } 110 | 111 | storage := config.Storage 112 | 113 | blockstore.BlockPrefix = ds.NewKey("") 114 | bs := blockstore.NewBlockstore(storage) 115 | bs = blockstore.NewIdStore(bs) 116 | 117 | p2pHost, peer, err := p2p.NewHostAndBitSwapPeer( 118 | ctx, 119 | p2p.WithDiscoveryNamespaces(ngConfig.ID), 120 | p2p.WithBitswapOptions(bitswap.ProvideEnabled(false)), // maybe this should be true if there is a long running decentragit node 121 | p2p.WithBlockstore(bs), 122 | ) 123 | if err != nil { 124 | return nil, nil, fmt.Errorf("error creating host: %w", err) 125 | } 126 | 127 | _, err = p2pHost.Bootstrap(ngConfig.BootstrapAddresses) 128 | if err != nil { 129 | return nil, nil, fmt.Errorf("error bootstrapping: %w", err) 130 | } 131 | 132 | group, err := ngConfig.NotaryGroup(nil) 133 | if err != nil { 134 | return nil, nil, fmt.Errorf("error getting notary group: %v", err) 135 | } 136 | 137 | if err = p2pHost.WaitForBootstrap(1+len(group.Signers)/2, 15*time.Second); err != nil { 138 | return nil, nil, err 139 | } 140 | 141 | cli := tupelo.New(group, pubsubwrapper.WrapLibp2p(p2pHost.GetPubSub()), peer) 142 | err = cli.Start(ctx) 143 | if err != nil { 144 | return nil, nil, fmt.Errorf("error starting client: %v", err) 145 | } 146 | 147 | return cli, peer, nil 148 | } 149 | -------------------------------------------------------------------------------- /.github/workflows/homebrew.yml: -------------------------------------------------------------------------------- 1 | name: Homebrew Release & Bottle 2 | on: 3 | release: 4 | types: [published] 5 | 6 | jobs: 7 | release: 8 | if: "!github.event.release.prerelease" 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: checkout formula 12 | uses: actions/checkout@v2 13 | with: 14 | repository: quorumcontrol/homebrew-dgit 15 | persist-credentials: false 16 | - name: download source tarball 17 | run: curl -L ${{ github.event.release.tarball_url }} -o dgit-source-${{ github.event.release.tag_name }}.tar.gz 18 | - name: upload source tarball artifact 19 | uses: actions/upload-artifact@v1 20 | with: 21 | name: dgit-source-${{ github.event.release.tag_name }} 22 | path: dgit-source-${{ github.event.release.tag_name }}.tar.gz 23 | - name: update release in formula 24 | run: | 25 | cd Formula 26 | 27 | # set version 28 | echo "version: \"${{ github.event.release.tag_name }}\"" > dgit.yml 29 | 30 | # set SHA256 sum of source tarball 31 | export SHASUM=$(sha256sum dgit-source-${{ github.event.release.tag_name }}.tar.gz | awk '{print $1}') 32 | echo -e "url: \"${{ github.event.release.tarball_url }}\"\nsha256: \"${SHASUM}\"" >> dgit.yml 33 | 34 | # update bottles root_url 35 | echo -e "bottles:\n root_url: \"https://github.com/quorumcontrol/dgit/releases/download/${{ github.event.release.tag_name }}\"" >> dgit.yml 36 | echo -e " sha256:" >> dgit.yml 37 | 38 | # commit 39 | git config --local user.email "action@github.com" 40 | git config --local user.name "GitHub Action" 41 | git commit -a --allow-empty -m 'Update to ${{ github.event.release.tag_name }}' 42 | - name: push changes 43 | uses: ad-m/github-push-action@master 44 | with: 45 | github_token: ${{ secrets.TAP_GITHUB_TOKEN }} 46 | repository: quorumcontrol/homebrew-dgit 47 | 48 | build-bottle: 49 | if: "!github.event.release.prerelease" 50 | runs-on: ${{ matrix.os }} 51 | needs: release 52 | strategy: 53 | matrix: 54 | os: [ubuntu-18.04, macos-10.15] 55 | steps: 56 | - name: install homebrew 57 | run: | 58 | /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)" 59 | - name: add brew to path on linux 60 | run: | 61 | echo "::add-path::/home/linuxbrew/.linuxbrew/bin" 62 | if: runner.os == 'Linux' 63 | - uses: actions/checkout@v2 64 | with: 65 | repository: quorumcontrol/homebrew-dgit 66 | persist-credentials: false 67 | - name: download source tarball artifact 68 | uses: actions/download-artifact@v1 69 | with: 70 | name: dgit-source-${{ github.event.release.tag_name }} 71 | - name: move source tarball to brew cache 72 | run: | 73 | brew tap quorumcontrol/dgit 74 | export cache_path=$(brew --cache -s quorumcontrol/dgit/dgit) 75 | export cache_dir=$(dirname $cache_path) 76 | export cache_file=$(basename $cache_path) 77 | mkdir -p $cache_dir 78 | mv dgit-source-${{ github.event.release.tag_name }}/dgit-source-${{ github.event.release.tag_name }}.tar.gz $cache_file 79 | mv $cache_file ${cache_dir}/ 80 | - name: build bottle 81 | env: 82 | TARBALL: unknown 83 | VERSION: ${{ github.event.release.tag_name }} 84 | run: | 85 | brew install --build-bottle quorumcontrol/dgit/dgit 86 | brew bottle --root-url=https://github.com/quorumcontrol/dgit/releases/download/${{ github.event.release.tag_name }} --no-rebuild quorumcontrol/dgit/dgit 87 | echo "::set-env name=TARBALL::$(ls *.tar.gz)" 88 | - name: upload bottle artifact 89 | uses: actions/upload-artifact@v1 90 | with: 91 | name: bottle-${{ runner.os }}-${{ github.event.release.tag_name }} 92 | path: ${{ env.TARBALL }} 93 | 94 | update-formula: 95 | if: "!github.event.release.prerelease" 96 | runs-on: ubuntu-latest 97 | needs: build-bottle 98 | steps: 99 | - name: download macOS bottle 100 | uses: actions/download-artifact@v1 101 | with: 102 | name: bottle-macOS-${{ github.event.release.tag_name }} 103 | - name: download linux bottle 104 | uses: actions/download-artifact@v1 105 | with: 106 | name: bottle-Linux-${{ github.event.release.tag_name }} 107 | - name: rename bottle files b/c WTF Homebrew 108 | run: | 109 | export DIR=bottle-macOS-${{ github.event.release.tag_name }} 110 | export BOTTLE=$(ls bottle-macOS-${{ github.event.release.tag_name }}) 111 | mv $DIR/$BOTTLE $DIR/$(echo $BOTTLE | sed 's/--/-/') 112 | export DIR=bottle-Linux-${{ github.event.release.tag_name }} 113 | export BOTTLE=$(ls bottle-Linux-${{ github.event.release.tag_name }}) 114 | mv $DIR/$BOTTLE $DIR/$(echo $BOTTLE | sed 's/--/-/') 115 | - name: set bottle name env vars 116 | run: | 117 | echo "::set-env name=BOTTLE_FILE_MACOS::$(ls bottle-macOS-${{ github.event.release.tag_name }}/)" 118 | echo "::set-env name=BOTTLE_FILE_LINUX::$(ls bottle-Linux-${{ github.event.release.tag_name }}/)" 119 | - name: upload macOS bottle to GitHub 120 | uses: actions/upload-release-asset@v1 121 | env: 122 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 123 | with: 124 | upload_url: ${{ github.event.release.upload_url }} 125 | asset_path: ./bottle-macOS-${{ github.event.release.tag_name }}/${{ env.BOTTLE_FILE_MACOS }} 126 | asset_name: ${{ env.BOTTLE_FILE_MACOS }} 127 | asset_content_type: application/x-gtar 128 | - name: upload Linux bottle to GitHub 129 | uses: actions/upload-release-asset@v1 130 | env: 131 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 132 | with: 133 | upload_url: ${{ github.event.release.upload_url }} 134 | asset_path: ./bottle-Linux-${{ github.event.release.tag_name }}/${{ env.BOTTLE_FILE_LINUX }} 135 | asset_name: ${{ env.BOTTLE_FILE_LINUX }} 136 | asset_content_type: application/x-gtar 137 | - name: checkout formula 138 | uses: actions/checkout@v2 139 | with: 140 | repository: quorumcontrol/homebrew-dgit 141 | persist-credentials: false 142 | path: homebrew-dgit 143 | - name: update macos bottle in homebrew formula 144 | run: | 145 | export SHASUM=$(sha256sum bottle-macOS-${{ github.event.release.tag_name }}/* | awk '{print $1}') 146 | cd homebrew-dgit/Formula 147 | echo -e " \"${SHASUM}\": \"catalina\"" >> dgit.yml 148 | - name: update linux bottle in homebrew formula 149 | run: | 150 | export SHASUM=$(sha256sum bottle-Linux-${{ github.event.release.tag_name }}/* | awk '{print $1}') 151 | cd homebrew-dgit/Formula 152 | echo -e " \"${SHASUM}\": \"x86_64_linux\"" >> dgit.yml 153 | - name: commit formula changes 154 | run: | 155 | cd homebrew-dgit 156 | git config --local user.email "action@github.com" 157 | git config --local user.name "GitHub Action" 158 | git commit -a --allow-empty -m 'Update bottles to ${{ github.event.release.tag_name }}' 159 | - name: push changes 160 | uses: ad-m/github-push-action@master 161 | with: 162 | github_token: ${{ secrets.TAP_GITHUB_TOKEN }} 163 | repository: quorumcontrol/homebrew-dgit 164 | directory: homebrew-dgit 165 | -------------------------------------------------------------------------------- /remotehelper/runner_test.go: -------------------------------------------------------------------------------- 1 | package remotehelper 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "context" 7 | "io" 8 | "os" 9 | "strings" 10 | "sync" 11 | "testing" 12 | 13 | "github.com/ethereum/go-ethereum/crypto" 14 | fixtures "github.com/go-git/go-git-fixtures/v4" 15 | "github.com/go-git/go-git/v5" 16 | "github.com/go-git/go-git/v5/config" 17 | "github.com/go-git/go-git/v5/plumbing/cache" 18 | "github.com/go-git/go-git/v5/plumbing/transport" 19 | "github.com/go-git/go-git/v5/storage/filesystem" 20 | logging "github.com/ipfs/go-log" 21 | "github.com/stretchr/testify/require" 22 | "github.com/tyler-smith/go-bip39" 23 | 24 | "github.com/quorumcontrol/dgit/keyring" 25 | "github.com/quorumcontrol/dgit/transport/dgit" 26 | "github.com/quorumcontrol/dgit/tupelo/usertree" 27 | ) 28 | 29 | func TestRunnerIntegration(t *testing.T) { 30 | defer fixtures.Clean() 31 | 32 | ctx, cancel := context.WithCancel(context.Background()) 33 | defer cancel() 34 | 35 | err := os.Setenv("DG_OBJ_STORAGE", "chaintree") 36 | require.Nil(t, err) 37 | 38 | // Just generating a random username 39 | key, err := crypto.GenerateKey() 40 | require.Nil(t, err) 41 | username := strings.ToLower(crypto.PubkeyToAddress(key.PublicKey).String()[20:]) 42 | 43 | err = os.Setenv("DG_USERNAME", username) 44 | require.Nil(t, err) 45 | 46 | client, err := dgit.NewLocalClient(ctx) 47 | require.Nil(t, err) 48 | client.RegisterAsDefault() 49 | 50 | logLevelStr, ok := os.LookupEnv("DG_LOG_LEVEL") 51 | if ok { 52 | require.Nil(t, logging.SetLogLevelRegex("decentragit.*", strings.ToUpper(logLevelStr))) 53 | } 54 | 55 | localRepoFs := fixtures.Basic().One().DotGit() 56 | store := filesystem.NewStorageWithOptions(localRepoFs, cache.NewObjectLRUDefault(), filesystem.Options{KeepDescriptors: true}) 57 | 58 | local, err := git.Open(store, nil) 59 | require.Nil(t, err) 60 | 61 | // Just a random dg url 62 | endpoint, err := transport.NewEndpoint("dg://" + username + "/test") 63 | require.Nil(t, err) 64 | 65 | remoteConfig := &config.RemoteConfig{ 66 | Name: "dgit-test", 67 | URLs: []string{endpoint.String()}, 68 | } 69 | require.Nil(t, remoteConfig.Validate()) 70 | 71 | _, err = local.CreateRemote(remoteConfig) 72 | require.Nil(t, err) 73 | 74 | gitInputWriter := newBlockingReader() 75 | gitOutpuReaderPipe, gitOutputWriter := io.Pipe() 76 | gitOutputReader := newTestOutputReader(gitOutpuReaderPipe) 77 | userMsgReaderPipe, userMsgWriter := io.Pipe() 78 | userMsgReader := newTestOutputReader(userMsgReaderPipe) 79 | require.NotNil(t, userMsgReader) 80 | 81 | kr := keyring.NewMemory() 82 | seed := bip39.NewSeed("123", username) 83 | pkey, err := kr.CreatePrivateKey(username, seed) 84 | require.Nil(t, err) 85 | auth := dgit.NewPrivateKeyAuth(pkey) 86 | 87 | _, err = usertree.Create(ctx, &usertree.Options{ 88 | Name: username, 89 | Tupelo: client.Tupelo, 90 | Owners: []string{auth.String()}, 91 | }) 92 | require.Nil(t, err) 93 | 94 | runner := &Runner{ 95 | local: local, 96 | stdin: gitInputWriter, 97 | stdout: gitOutputWriter, 98 | stderr: userMsgWriter, 99 | keyring: kr, 100 | } 101 | 102 | go func() { 103 | err = runner.Run(ctx, remoteConfig.Name, remoteConfig.URLs[0]) 104 | require.Nil(t, err) 105 | }() 106 | 107 | t.Run("it can list capabilities", func(t *testing.T) { 108 | _, err = gitInputWriter.Write([]byte("capabilities\n")) 109 | require.Nil(t, err) 110 | 111 | gitOutputReader.Expect(t, "*push\n") 112 | gitOutputReader.Expect(t, "*fetch\n") 113 | gitOutputReader.Expect(t, "\n") 114 | }) 115 | 116 | t.Run("it can push a branch with same source name", func(t *testing.T) { 117 | _, err = gitInputWriter.Write([]byte("list for-push\n")) 118 | require.Nil(t, err) 119 | gitOutputReader.Expect(t, "\n") 120 | 121 | _, err = gitInputWriter.Write([]byte("push refs/heads/master:refs/heads/master\n")) 122 | require.Nil(t, err) 123 | gitOutputReader.Expect(t, "ok refs/heads/master\n") 124 | gitOutputReader.Expect(t, "\n") 125 | }) 126 | 127 | t.Run("it can push a branch with different source name", func(t *testing.T) { 128 | _, err = gitInputWriter.Write([]byte("list for-push\n")) 129 | require.Nil(t, err) 130 | gitOutputReader.Expect(t, "@refs/heads/master HEAD\n") 131 | gitOutputReader.Expect(t, "6ecf0ef2c2dffb796033e5a02219af86ec6584e5 refs/heads/master\n") 132 | gitOutputReader.Expect(t, "\n") 133 | 134 | _, err = gitInputWriter.Write([]byte("push refs/heads/master:refs/heads/feature/test\n")) 135 | require.Nil(t, err) 136 | gitOutputReader.Expect(t, "ok refs/heads/feature/test\n") 137 | gitOutputReader.Expect(t, "\n") 138 | }) 139 | 140 | t.Run("it can delete a branch", func(t *testing.T) { 141 | _, err = gitInputWriter.Write([]byte("list for-push\n")) 142 | require.Nil(t, err) 143 | gitOutputReader.Expect(t, "@refs/heads/master HEAD\n") 144 | gitOutputReader.Expect(t, "6ecf0ef2c2dffb796033e5a02219af86ec6584e5 refs/heads/feature/test\n") 145 | gitOutputReader.Expect(t, "6ecf0ef2c2dffb796033e5a02219af86ec6584e5 refs/heads/master\n") 146 | gitOutputReader.Expect(t, "\n") 147 | 148 | _, err = gitInputWriter.Write([]byte("push :refs/heads/feature/test\n")) 149 | require.Nil(t, err) 150 | gitOutputReader.Expect(t, "ok refs/heads/feature/test\n") 151 | gitOutputReader.Expect(t, "\n") 152 | 153 | _, err = gitInputWriter.Write([]byte("list\n")) 154 | require.Nil(t, err) 155 | gitOutputReader.Expect(t, "@refs/heads/master HEAD\n") 156 | gitOutputReader.Expect(t, "6ecf0ef2c2dffb796033e5a02219af86ec6584e5 refs/heads/master\n") 157 | gitOutputReader.Expect(t, "\n") 158 | }) 159 | 160 | t.Run("it can pull a new branch", func(t *testing.T) { 161 | // create a second repo with different commits 162 | secondRepoFs := fixtures.Basic()[2].DotGit() 163 | secondStore := filesystem.NewStorageWithOptions(secondRepoFs, cache.NewObjectLRUDefault(), filesystem.Options{KeepDescriptors: true}) 164 | 165 | secondRepo, err := git.Open(secondStore, nil) 166 | require.Nil(t, err) 167 | 168 | _, err = secondRepo.CreateRemote(remoteConfig) 169 | require.Nil(t, err) 170 | 171 | auth, err := runner.auth() 172 | require.Nil(t, err) 173 | 174 | // now manually push other commits to repo tree 175 | err = secondRepo.Push(&git.PushOptions{ 176 | RemoteName: remoteConfig.Name, 177 | RefSpecs: []config.RefSpec{config.RefSpec("refs/heads/master:refs/heads/second-repo-master")}, 178 | Auth: auth, 179 | }) 180 | require.Nil(t, err) 181 | 182 | // now check that runner can pull new tree 183 | _, err = gitInputWriter.Write([]byte("list\n")) 184 | require.Nil(t, err) 185 | gitOutputReader.Expect(t, "@refs/heads/master HEAD\n") 186 | gitOutputReader.Expect(t, "6ecf0ef2c2dffb796033e5a02219af86ec6584e5 refs/heads/master\n") 187 | gitOutputReader.Expect(t, "1980fcf55330d9d94c34abee5ab734afecf96aba refs/heads/second-repo-master\n") 188 | }) 189 | } 190 | 191 | type testOutputReader struct { 192 | *bufio.Reader 193 | } 194 | 195 | func newTestOutputReader(rd io.Reader) *testOutputReader { 196 | return &testOutputReader{bufio.NewReader(rd)} 197 | } 198 | 199 | func (r *testOutputReader) Expect(t *testing.T, value string) { 200 | line, err := r.ReadString('\n') 201 | require.Nil(t, err) 202 | require.Equal(t, value, line) 203 | } 204 | 205 | type blockingReader struct { 206 | buf bytes.Buffer 207 | cond *sync.Cond 208 | closed bool 209 | } 210 | 211 | func newBlockingReader() *blockingReader { 212 | m := sync.Mutex{} 213 | return &blockingReader{ 214 | cond: sync.NewCond(&m), 215 | buf: bytes.Buffer{}, 216 | closed: false, 217 | } 218 | } 219 | 220 | func (br *blockingReader) Write(b []byte) (ln int, err error) { 221 | ln, err = br.buf.Write(b) 222 | br.cond.Broadcast() 223 | return 224 | } 225 | 226 | func (br *blockingReader) Read(b []byte) (ln int, err error) { 227 | if br.closed { 228 | return ln, io.EOF 229 | } 230 | 231 | ln, err = br.buf.Read(b) 232 | if err == io.EOF { 233 | br.cond.L.Lock() 234 | br.cond.Wait() 235 | br.cond.L.Unlock() 236 | ln, err = br.buf.Read(b) 237 | } 238 | return 239 | } 240 | 241 | func (br *blockingReader) Close() { 242 | br.closed = true 243 | } 244 | -------------------------------------------------------------------------------- /storage/siaskynet/object.go: -------------------------------------------------------------------------------- 1 | package siaskynet 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "strings" 7 | "sync" 8 | 9 | format "github.com/ipfs/go-ipld-format" 10 | "github.com/quorumcontrol/messages/v2/build/go/transactions" 11 | 12 | "github.com/quorumcontrol/dgit/storage" 13 | 14 | "github.com/go-git/go-git/v5/plumbing" 15 | "github.com/go-git/go-git/v5/plumbing/storer" 16 | logging "github.com/ipfs/go-log" 17 | "github.com/quorumcontrol/chaintree/chaintree" 18 | "go.uber.org/zap" 19 | ) 20 | 21 | const TupeloTxnBatchSize = 75 22 | 23 | var log = logging.Logger("decentragit.storage.siaskynet") 24 | 25 | type ObjectStorage struct { 26 | *storage.ChaintreeObjectStorage 27 | log *zap.SugaredLogger 28 | skynet *Skynet 29 | } 30 | 31 | var _ storer.EncodedObjectStorer = (*ObjectStorage)(nil) 32 | var _ storer.PackfileWriter = (*ObjectStorage)(nil) 33 | var _ storer.Transactioner = (*ObjectStorage)(nil) 34 | 35 | func NewObjectStorage(config *storage.Config) storer.EncodedObjectStorer { 36 | did := config.ChainTree.MustId() 37 | return &ObjectStorage{ 38 | &storage.ChaintreeObjectStorage{config}, 39 | log.Named(did[len(did)-6:]), 40 | InitSkynet(4, 1), 41 | } 42 | } 43 | 44 | type SkylinkStore map[plumbing.Hash]string 45 | 46 | type TemporalStorage struct { 47 | sync.RWMutex 48 | uploadWaitGroup sync.WaitGroup 49 | 50 | log *zap.SugaredLogger 51 | skylinks SkylinkStore 52 | skynet *Skynet 53 | } 54 | 55 | type ChaintreeLinkStorage struct { 56 | log *zap.SugaredLogger 57 | *storage.Config 58 | } 59 | 60 | func NewTemporalStorage() *TemporalStorage { 61 | return &TemporalStorage{ 62 | log: log.Named("skynet-temporal"), 63 | skylinks: make(SkylinkStore), 64 | skynet: InitSkynet(4, 1), 65 | } 66 | } 67 | 68 | func (ts *TemporalStorage) SetSkylink(h plumbing.Hash, link string) { 69 | ts.Lock() 70 | defer ts.Unlock() 71 | 72 | ts.skylinks[h] = link 73 | } 74 | 75 | func (ts *TemporalStorage) Skylinks() SkylinkStore { 76 | sls := make(SkylinkStore) 77 | 78 | ts.RLock() 79 | defer ts.RUnlock() 80 | 81 | for h, l := range ts.skylinks { 82 | sls[h] = l 83 | } 84 | 85 | return sls 86 | } 87 | 88 | func NewChaintreeLinkStorage(config *storage.Config) *ChaintreeLinkStorage { 89 | did := config.ChainTree.MustId() 90 | return &ChaintreeLinkStorage{ 91 | log.Named(did[len(did)-6:]), 92 | config, 93 | } 94 | } 95 | 96 | func uploadObjectToSkynet(s *Skynet, o plumbing.EncodedObject) (string, error) { 97 | resultC, errC := s.UploadObject(o) 98 | 99 | select { 100 | case err := <-errC: 101 | return "", err 102 | case link := <-resultC: 103 | return link, nil 104 | } 105 | } 106 | 107 | func (ts *TemporalStorage) SetEncodedObject(o plumbing.EncodedObject) (plumbing.Hash, error) { 108 | ts.log.Debugf("uploading %s to Skynet", o.Hash()) 109 | 110 | objHash := o.Hash() 111 | 112 | ts.uploadWaitGroup.Add(1) 113 | go func() { 114 | link, err := uploadObjectToSkynet(ts.skynet, o) 115 | if err != nil { 116 | ts.log.Errorf("object %s upload failed: %w", objHash, err) 117 | return 118 | } 119 | 120 | ts.SetSkylink(objHash, link) 121 | 122 | ts.uploadWaitGroup.Done() 123 | }() 124 | 125 | return objHash, nil 126 | } 127 | 128 | func downloadObjectFromSkynet(s *Skynet, link string) (plumbing.EncodedObject, error) { 129 | resultC, errC := s.DownloadObject(link) 130 | 131 | select { 132 | case err := <-errC: 133 | return nil, err 134 | case o := <-resultC: 135 | return o, nil 136 | } 137 | } 138 | 139 | type ObjectTransaction struct { 140 | temporal *TemporalStorage 141 | storage *ChaintreeLinkStorage 142 | log *zap.SugaredLogger 143 | } 144 | 145 | var _ storer.Transaction = (*ObjectTransaction)(nil) 146 | 147 | func (s *ObjectStorage) Begin() storer.Transaction { 148 | ts := NewTemporalStorage() 149 | ls := NewChaintreeLinkStorage(s.Config) 150 | return &ObjectTransaction{ 151 | // NB: Currently TemporalStorage uploads objects to 152 | // skynet as they are added to the txn. This makes sense while it's 153 | // free, but perhaps less so once it isn't. It still might make sense 154 | // perf-wise, but you'd want to clean up on Rollback / error to stop 155 | // paying for those uploads. 156 | temporal: ts, 157 | storage: ls, 158 | log: s.log.Named("object-transaction"), 159 | } 160 | } 161 | 162 | func (ot *ObjectTransaction) SetEncodedObject(o plumbing.EncodedObject) (plumbing.Hash, error) { 163 | ot.log.Debugf("added object %s to transaction", o.Hash()) 164 | return ot.temporal.SetEncodedObject(o) 165 | } 166 | 167 | func (ot *ObjectTransaction) EncodedObject(t plumbing.ObjectType, h plumbing.Hash) (plumbing.EncodedObject, error) { 168 | ot.log.Errorf("ObjectTransaction.EncodedObject is a stub to satisfy the interface; don't call it") 169 | return &plumbing.MemoryObject{}, nil 170 | } 171 | 172 | func (ot *ObjectTransaction) Commit() error { 173 | ot.log.Debugf("committing transaction") 174 | 175 | var tupeloTxns []*transactions.Transaction 176 | 177 | // make sure all pending uploads have completed and set their skylinks 178 | ot.log.Debugf("waiting for all Skynet uploads to complete") 179 | ot.temporal.uploadWaitGroup.Wait() 180 | ot.log.Debugf("Skynet uploads complete") 181 | 182 | skylinks := ot.temporal.Skylinks() 183 | 184 | for h, link := range skylinks { 185 | txn, err := setLinkTxn(h, strings.Replace(link, "sia://", "did:sia:", 1)) 186 | if err != nil { 187 | return err 188 | } 189 | 190 | tupeloTxns = append(tupeloTxns, txn) 191 | } 192 | 193 | if len(skylinks) > 0 { 194 | txnBatch := make([]*transactions.Transaction, 0) 195 | lastIdx := len(tupeloTxns) - 1 196 | for i, t := range tupeloTxns { 197 | batchIdx := (i + 1) % TupeloTxnBatchSize 198 | txnBatch = append(txnBatch, t) 199 | if batchIdx == 0 || i == lastIdx { 200 | ot.log.Debugf("saving %d Skylinks in transaction to repo chaintree", len(txnBatch)) 201 | _, err := ot.storage.Tupelo.PlayTransactions(ot.storage.Ctx, ot.storage.ChainTree, ot.storage.PrivateKey, txnBatch) 202 | if err != nil { 203 | return err 204 | } 205 | txnBatch = make([]*transactions.Transaction, 0) 206 | } 207 | } 208 | } 209 | 210 | return nil 211 | } 212 | 213 | func setLinkTxn(h plumbing.Hash, link string) (*transactions.Transaction, error) { 214 | writePath := storage.ObjectWritePath(h) 215 | 216 | txn, err := chaintree.NewSetDataTransaction(writePath, link) 217 | if err != nil { 218 | return nil, err 219 | } 220 | 221 | return txn, nil 222 | } 223 | 224 | func (ot *ObjectTransaction) Rollback() error { 225 | ot.log.Debugf("rolling back transaction") 226 | ot.temporal = nil 227 | return nil 228 | } 229 | 230 | func (s *ObjectStorage) PackfileWriter() (io.WriteCloser, error) { 231 | s.log.Debug("packfile writer requested") 232 | return storage.NewPackWriter(s), nil 233 | } 234 | 235 | func (s *ObjectStorage) SetEncodedObject(o plumbing.EncodedObject) (plumbing.Hash, error) { 236 | s.log.Debugf("saving %s with type %s", o.Hash().String(), o.Type().String()) 237 | 238 | if o.Type() == plumbing.OFSDeltaObject || o.Type() == plumbing.REFDeltaObject { 239 | return plumbing.ZeroHash, plumbing.ErrInvalidType 240 | } 241 | 242 | s.log.Debugf("uploading %s to Skynet", o.Hash().String()) 243 | link, err := uploadObjectToSkynet(s.skynet, o) 244 | if err != nil { 245 | return plumbing.ZeroHash, err 246 | } 247 | 248 | skylink := strings.TrimPrefix(link, "sia://") 249 | objDid := strings.Join([]string{"did", "sia", skylink}, ":") 250 | 251 | tx, err := setLinkTxn(o.Hash(), objDid) 252 | if err != nil { 253 | return plumbing.ZeroHash, err 254 | } 255 | 256 | _, err = s.Tupelo.PlayTransactions(s.Ctx, s.ChainTree, s.PrivateKey, []*transactions.Transaction{tx}) 257 | if err != nil { 258 | return plumbing.ZeroHash, err 259 | } 260 | 261 | return o.Hash(), nil 262 | } 263 | 264 | func (s *ObjectStorage) HasEncodedObject(h plumbing.Hash) (err error) { 265 | if _, err := s.EncodedObject(plumbing.AnyObject, h); err != nil { 266 | return err 267 | } 268 | return nil 269 | } 270 | 271 | func (s *ObjectStorage) EncodedObjectSize(h plumbing.Hash) (size int64, err error) { 272 | o, err := s.EncodedObject(plumbing.AnyObject, h) 273 | if err != nil { 274 | return 0, err 275 | } 276 | return o.Size(), nil 277 | } 278 | 279 | func (s *ObjectStorage) EncodedObject(t plumbing.ObjectType, h plumbing.Hash) (plumbing.EncodedObject, error) { 280 | s.log.Debugf("fetching %s with type %s", h.String(), t.String()) 281 | 282 | path := storage.ObjectReadPath(h) 283 | valUncast, _, err := s.ChainTree.ChainTree.Dag.Resolve(s.Ctx, path) 284 | if err == format.ErrNotFound { 285 | s.log.Debugf("%s not found in chaintree at path %s", h, path) 286 | return nil, plumbing.ErrObjectNotFound 287 | } 288 | if err != nil { 289 | s.log.Errorf("chaintree resolve error for %s: %w", h, err) 290 | return nil, err 291 | } 292 | if valUncast == nil { 293 | s.log.Debugf("%s was nil in chaintree at path %s", h, path) 294 | return nil, plumbing.ErrObjectNotFound 295 | } 296 | 297 | // TODO: Read these in higher-level code and delegate decoding to whichever 298 | // object storage system is specified in the did:storer: prefix 299 | objDid, ok := valUncast.(string) 300 | if !ok { 301 | s.log.Errorf("object DID should be a string; was a %T instead", valUncast) 302 | return nil, plumbing.ErrObjectNotFound 303 | } 304 | if !strings.HasPrefix(objDid, "did:sia:") { 305 | s.log.Errorf("object DID %s should start with did:sia:", objDid) 306 | return nil, plumbing.ErrObjectNotFound 307 | } 308 | 309 | link := strings.Replace(objDid, "did:sia:", "sia://", 1) 310 | 311 | s.log.Debugf("downloading %s from Skynet at %s", h, link) 312 | o, err := downloadObjectFromSkynet(s.skynet, link) 313 | if err != nil { 314 | err = fmt.Errorf("could not download object %s from Skynet at %s: %w", h.String(), link, err) 315 | s.log.Errorf(err.Error()) 316 | return nil, err 317 | } 318 | 319 | if plumbing.AnyObject != t && o.Type() != t { 320 | s.log.Debugf("%s not found, mismatched types, expected %s, got %s", h, t, o.Type()) 321 | return nil, plumbing.ErrObjectNotFound 322 | } 323 | 324 | return o, nil 325 | } 326 | 327 | func (s *ObjectStorage) IterEncodedObjects(t plumbing.ObjectType) (storer.EncodedObjectIter, error) { 328 | return storage.NewEncodedObjectIter(s, t), nil 329 | } 330 | -------------------------------------------------------------------------------- /remotehelper/runner.go: -------------------------------------------------------------------------------- 1 | package remotehelper 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "fmt" 7 | "io" 8 | "os" 9 | "sort" 10 | "strings" 11 | 12 | "github.com/ethereum/go-ethereum/common/hexutil" 13 | "github.com/ethereum/go-ethereum/crypto" 14 | "github.com/go-git/go-git/v5" 15 | "github.com/go-git/go-git/v5/config" 16 | "github.com/go-git/go-git/v5/plumbing" 17 | "github.com/go-git/go-git/v5/plumbing/transport" 18 | logging "github.com/ipfs/go-log" 19 | 20 | "github.com/quorumcontrol/dgit/constants" 21 | "github.com/quorumcontrol/dgit/keyring" 22 | "github.com/quorumcontrol/dgit/msg" 23 | "github.com/quorumcontrol/dgit/transport/dgit" 24 | ) 25 | 26 | var log = logging.Logger("decentragit.runner") 27 | 28 | type Runner struct { 29 | local *git.Repository 30 | stdin io.Reader 31 | stdout io.Writer 32 | stderr io.Writer 33 | keyring *keyring.Keyring 34 | } 35 | 36 | func New(local *git.Repository) *Runner { 37 | return &Runner{ 38 | local: local, 39 | stdin: os.Stdin, 40 | stdout: os.Stdout, 41 | stderr: os.Stderr, 42 | } 43 | } 44 | 45 | // > Also, what are the advantages and disadvantages of a remote helper 46 | // > with push/fetch capabilities vs a remote helper with import/export 47 | // > capabilities? 48 | 49 | // It mainly has to do with what it is convenient for your helper to 50 | // produce. If the helper would find it more convenient to write native 51 | // git objects (for example because the remote server speaks a 52 | // git-specific protocol, as in the case of remote-curl.c) then the 53 | // "fetch" capability will be more convenient. If the helper wants to 54 | // make a batch of new objects then a fast-import stream can be a 55 | // convenient way to do this and the "import" capability takes care of 56 | // running fast-import to take care of that. 57 | // 58 | // http://git.661346.n2.nabble.com/remote-helper-example-with-push-fetch-capabilities-td7623009.html 59 | // 60 | 61 | func (r *Runner) Run(ctx context.Context, remoteName string, remoteUrl string) error { 62 | log.Infof("running git-remote-dg on remote %s with url %s", remoteName, remoteUrl) 63 | 64 | // get the named remote as reported by git, but then 65 | // create a new remote with only the url specified 66 | // this is for cases when a remote has multiple urls 67 | // specified for push / fetch 68 | namedRemote, err := r.local.Remote(remoteName) 69 | if err != nil { 70 | return err 71 | } 72 | 73 | err = namedRemote.Config().Validate() 74 | if err != nil { 75 | return fmt.Errorf("Invalid remote config: %v", err) 76 | } 77 | 78 | remote := git.NewRemote(r.local.Storer, &config.RemoteConfig{ 79 | Name: namedRemote.Config().Name, 80 | Fetch: namedRemote.Config().Fetch, 81 | URLs: []string{remoteUrl}, 82 | }) 83 | 84 | stdinReader := bufio.NewReader(r.stdin) 85 | 86 | for { 87 | var err error 88 | 89 | command, err := stdinReader.ReadString('\n') 90 | if err != nil { 91 | return err 92 | } 93 | command = strings.TrimSpace(command) 94 | commandParts := strings.Split(command, " ") 95 | 96 | log.Infof("received command on stdin %s", command) 97 | 98 | args := strings.TrimSpace(strings.TrimPrefix(command, commandParts[0])) 99 | command = commandParts[0] 100 | 101 | switch command { 102 | case "capabilities": 103 | r.respond(strings.Join([]string{ 104 | "*push", 105 | "*fetch", 106 | }, "\n") + "\n") 107 | r.respond("\n") 108 | case "list": 109 | refs, err := remote.List(&git.ListOptions{}) 110 | 111 | if err == transport.ErrRepositoryNotFound && args == "for-push" { 112 | r.respond("\n") 113 | continue 114 | } 115 | 116 | if err == transport.ErrRepositoryNotFound { 117 | return fmt.Errorf(msg.RepoNotFound) 118 | } 119 | 120 | if err == transport.ErrEmptyRemoteRepository || len(refs) == 0 { 121 | r.respond("\n") 122 | continue 123 | } 124 | 125 | if err != nil { 126 | return err 127 | } 128 | 129 | var head string 130 | 131 | listResponse := make([]string, len(refs)) 132 | for i, ref := range refs { 133 | listResponse[i] = fmt.Sprintf("%s %s", ref.Hash(), ref.Name()) 134 | 135 | // TODO: set default branch in repo chaintree which 136 | // would become head here 137 | // 138 | // if master head exists, use that 139 | if ref.Name() == "refs/heads/master" { 140 | head = ref.Name().String() 141 | } 142 | } 143 | 144 | sort.Slice(listResponse, func(i, j int) bool { 145 | return strings.Split(listResponse[i], " ")[1] < strings.Split(listResponse[j], " ")[1] 146 | }) 147 | 148 | // if head is empty, use last as default 149 | if head == "" { 150 | head = strings.Split(listResponse[len(listResponse)-1], " ")[1] 151 | } 152 | 153 | r.respond("@%s HEAD\n", head) 154 | r.respond("%s\n", strings.Join(listResponse, "\n")) 155 | r.respond("\n") 156 | case "push": 157 | refSpec := config.RefSpec(args) 158 | 159 | endpoint, err := transport.NewEndpoint(remote.Config().URLs[0]) 160 | if err != nil { 161 | return err 162 | } 163 | 164 | auth, err := r.auth() 165 | if err != nil { 166 | return err 167 | } 168 | 169 | log.Debugf("auth for push: %s %s", auth.Name(), auth.String()) 170 | 171 | err = remote.PushContext(ctx, &git.PushOptions{ 172 | RemoteName: remote.Config().Name, 173 | RefSpecs: []config.RefSpec{refSpec}, 174 | Auth: auth, 175 | }) 176 | 177 | if err == transport.ErrRepositoryNotFound { 178 | err = nil // reset err back to nil 179 | client, err := dgit.Default() 180 | if err != nil { 181 | return err 182 | } 183 | 184 | _, err = client.CreateRepoTree(ctx, endpoint, auth) 185 | if err != nil { 186 | return err 187 | } 188 | 189 | // Retry push now that repo exists 190 | err = remote.PushContext(ctx, &git.PushOptions{ 191 | RemoteName: remote.Config().Name, 192 | RefSpecs: []config.RefSpec{refSpec}, 193 | Auth: auth, 194 | }) 195 | } 196 | 197 | dst := refSpec.Dst(plumbing.ReferenceName("*")) 198 | if err != nil && err != git.NoErrAlreadyUpToDate { 199 | r.respond("error %s %s\n", dst, err.Error()) 200 | break 201 | } 202 | 203 | r.respond("ok %s\n", dst) 204 | r.respond("\n") 205 | case "fetch": 206 | splitArgs := strings.Split(args, " ") 207 | if len(splitArgs) != 2 { 208 | return fmt.Errorf("incorrect arguments for fetch, received %s, expected 'hash refname'", args) 209 | } 210 | 211 | refName := plumbing.ReferenceName(splitArgs[1]) 212 | 213 | refSpecs := []config.RefSpec{} 214 | 215 | log.Debugf("remote fetch config %v", remote.Config().Name) 216 | 217 | for _, fetchRefSpec := range remote.Config().Fetch { 218 | if !fetchRefSpec.Match(refName) { 219 | continue 220 | } 221 | 222 | newRefStr := "" 223 | if fetchRefSpec.IsForceUpdate() { 224 | newRefStr += "+" 225 | } 226 | newRefStr += refName.String() + ":" + fetchRefSpec.Dst(refName).String() 227 | 228 | newRef := config.RefSpec(newRefStr) 229 | 230 | if err := newRef.Validate(); err != nil { 231 | return err 232 | } 233 | 234 | log.Debugf("attempting to fetch on %s", newRef.String()) 235 | refSpecs = append(refSpecs, newRef) 236 | } 237 | 238 | err := remote.FetchContext(ctx, &git.FetchOptions{ 239 | RemoteName: remote.Config().Name, 240 | RefSpecs: refSpecs, 241 | }) 242 | if err != nil && err != git.NoErrAlreadyUpToDate { 243 | return err 244 | } 245 | log.Debugf("fetch complete") 246 | r.respond("\n") 247 | // Connect can be used for upload / receive pack 248 | // case "connect": 249 | // r.respond("fallback\n") 250 | case "": // command stream terminated, return out 251 | return nil 252 | default: 253 | return fmt.Errorf("Command '%s' not handled", command) 254 | } 255 | } 256 | } 257 | 258 | func (r *Runner) respond(format string, a ...interface{}) (n int, err error) { 259 | log.Infof("responding to git:") 260 | resp := bufio.NewScanner(strings.NewReader(fmt.Sprintf(format, a...))) 261 | for resp.Scan() { 262 | log.Infof(" " + resp.Text()) 263 | } 264 | return fmt.Fprintf(r.stdout, format, a...) 265 | } 266 | 267 | func (r *Runner) userMessage(format string, a ...interface{}) (n int, err error) { 268 | log.Infof("responding to user:") 269 | resp := bufio.NewScanner(strings.NewReader(fmt.Sprintf(format, a...))) 270 | for resp.Scan() { 271 | log.Infof(" " + resp.Text()) 272 | } 273 | return fmt.Fprintf(r.stderr, format+"\n", a...) 274 | } 275 | 276 | func (r *Runner) auth() (transport.AuthMethod, error) { 277 | var err error 278 | 279 | // mainly used for github actions 280 | envAuth, err := r.authFromEnv() 281 | if envAuth != nil || err != nil { 282 | return envAuth, err 283 | } 284 | 285 | if r.keyring == nil { 286 | r.keyring, err = keyring.NewDefault() 287 | 288 | // TODO: if no keyring available, prompt user for dgit password 289 | if err != nil { 290 | return nil, err 291 | } 292 | } 293 | 294 | repoConfig, err := r.local.Config() 295 | if err != nil { 296 | return nil, err 297 | } 298 | 299 | dgitConfig := repoConfig.Merged.Section(constants.DgitConfigSection) 300 | 301 | var username string 302 | if dgitConfig != nil { 303 | username = dgitConfig.Option("username") 304 | } 305 | 306 | envUsername := os.Getenv("DGIT_USERNAME") 307 | if envUsername != "" { 308 | log.Warningf("[DEPRECATION] - DGIT_USERNAME is deprecated, please use DG_USERNAME") 309 | username = envUsername 310 | } 311 | envUsername = os.Getenv("DG_USERNAME") 312 | if envUsername != "" { 313 | username = envUsername 314 | } 315 | 316 | if username == "" { 317 | return nil, fmt.Errorf(msg.Parse(msg.UserNotConfigured, map[string]interface{}{ 318 | "configSection": constants.DgitConfigSection, 319 | })) 320 | } 321 | 322 | privateKey, err := r.keyring.FindPrivateKey(username) 323 | if err == keyring.ErrKeyNotFound { 324 | return nil, fmt.Errorf(msg.Parse(msg.PrivateKeyNotFound, map[string]interface{}{ 325 | "keyringProvider": r.keyring.Name(), 326 | })) 327 | } 328 | 329 | return dgit.NewPrivateKeyAuth(privateKey), nil 330 | } 331 | 332 | func (r *Runner) authFromEnv() (transport.AuthMethod, error) { 333 | privateKeyEnv, ok := os.LookupEnv("DG_PRIVATE_KEY") 334 | if !ok { 335 | // TODO: remove backward compatible usage 336 | privateKeyEnv, ok = os.LookupEnv("DGIT_PRIVATE_KEY") 337 | if !ok { 338 | log.Warningf("[DEPRECATION] - DGIT_PRIVATE_KEY is deprecated, please use DG_PRIVATE_KEY") 339 | return nil, nil 340 | } 341 | } 342 | 343 | privateKeyBytes, err := hexutil.Decode(privateKeyEnv) 344 | if err != nil { 345 | return nil, fmt.Errorf("error hex decoding DG_PRIVATE_KEY: %v", err) 346 | } 347 | 348 | privateKey, err := crypto.ToECDSA(privateKeyBytes) 349 | if err != nil { 350 | return nil, fmt.Errorf("error unmarshalling DG_PRIVATE_KEY into ECDSA private key: %v", err) 351 | } 352 | 353 | return dgit.NewPrivateKeyAuth(privateKey), nil 354 | } 355 | -------------------------------------------------------------------------------- /initializer/initializer.go: -------------------------------------------------------------------------------- 1 | package initializer 2 | 3 | import ( 4 | "context" 5 | "crypto/ecdsa" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "os" 10 | "path" 11 | "regexp" 12 | "strings" 13 | 14 | "github.com/ethereum/go-ethereum/crypto" 15 | "github.com/go-git/go-git/v5" 16 | "github.com/go-git/go-git/v5/config" 17 | configformat "github.com/go-git/go-git/v5/plumbing/format/config" 18 | "github.com/go-git/go-git/v5/plumbing/transport" 19 | logging "github.com/ipfs/go-log" 20 | "github.com/manifoldco/promptui" 21 | "github.com/quorumcontrol/chaintree/nodestore" 22 | tupelo "github.com/quorumcontrol/tupelo/sdk/gossip/client" 23 | "github.com/tyler-smith/go-bip39" 24 | 25 | "github.com/quorumcontrol/dgit/constants" 26 | "github.com/quorumcontrol/dgit/keyring" 27 | "github.com/quorumcontrol/dgit/msg" 28 | "github.com/quorumcontrol/dgit/transport/dgit" 29 | "github.com/quorumcontrol/dgit/tupelo/namedtree" 30 | "github.com/quorumcontrol/dgit/tupelo/repotree" 31 | "github.com/quorumcontrol/dgit/tupelo/usertree" 32 | ) 33 | 34 | var log = logging.Logger("decentragit.initializer") 35 | 36 | var validRepoName = regexp.MustCompile(`^[a-zA-Z0-9_-]+\/[a-zA-Z0-9_-]+`) 37 | 38 | var promptTemplates = &promptui.PromptTemplates{ 39 | Prompt: `{{ . }} `, 40 | Confirm: `{{ . }} {{ "Y/n" | bold }} `, 41 | Valid: `{{ . }} `, 42 | Invalid: `{{ . }} `, 43 | Success: `{{ . }} `, 44 | } 45 | 46 | type Options struct { 47 | Repo *dgit.Repo 48 | Tupelo *tupelo.Client 49 | NodeStore nodestore.DagStore 50 | } 51 | 52 | type Initializer struct { 53 | stdin io.ReadCloser 54 | stdout io.WriteCloser 55 | stderr io.WriteCloser 56 | keyring *keyring.Keyring 57 | auth transport.AuthMethod 58 | repo *dgit.Repo 59 | tupelo *tupelo.Client 60 | nodestore nodestore.DagStore 61 | } 62 | 63 | func Init(ctx context.Context, opts *Options, args []string) error { 64 | i := &Initializer{ 65 | stdin: os.Stdin, 66 | stdout: os.Stdout, 67 | stderr: os.Stderr, 68 | repo: opts.Repo, 69 | tupelo: opts.Tupelo, 70 | nodestore: opts.NodeStore, 71 | } 72 | return i.Init(ctx, args) 73 | } 74 | 75 | func (i *Initializer) Init(ctx context.Context, args []string) error { 76 | var err error 77 | 78 | // load up auth and notify user if new 79 | _, err = i.getAuth(ctx) 80 | if err != nil { 81 | return err 82 | } 83 | 84 | // determine endpoint, prompt user as needed 85 | _, err = i.getEndpoint() 86 | if err != nil { 87 | return err 88 | } 89 | 90 | _, err = i.findOrCreateRepoTree(ctx) 91 | if err != nil { 92 | return err 93 | } 94 | 95 | err = i.addDgitPushToRemote(ctx, "origin") 96 | if err != nil { 97 | return err 98 | } 99 | 100 | err = i.addDgitRemote(ctx) 101 | if err != nil { 102 | return err 103 | } 104 | 105 | msg.Fprint(i.stdout, msg.FinalInstructions, map[string]interface{}{ 106 | "repo": i.repo.MustName(), 107 | "repourl": i.repo.MustURL(), 108 | }) 109 | fmt.Fprintln(os.Stdout) 110 | 111 | return nil 112 | } 113 | 114 | func (i *Initializer) findOrRequestUsername() (string, error) { 115 | repoConfig, err := i.repo.Config() 116 | if err != nil { 117 | return "", fmt.Errorf("could not get repo config: %w", err) 118 | } 119 | 120 | dgitConfig := repoConfig.Merged.Section(constants.DgitConfigSection) 121 | 122 | var username string 123 | if dgitConfig != nil { 124 | username = dgitConfig.Option("username") 125 | } 126 | 127 | // try looking up the GitHub username for the default 128 | if username == "" { 129 | if ghConfig := repoConfig.Merged.Section("github"); ghConfig != nil { 130 | username = ghConfig.Option("user") 131 | } 132 | } 133 | 134 | prompt := promptui.Prompt{ 135 | Label: stripNewLines(msg.Parse(msg.UsernamePrompt, nil)), 136 | Templates: promptTemplates, 137 | Default: username, 138 | Stdin: i.stdin, 139 | Stdout: i.stdout, 140 | Validate: func(input string) error { 141 | if input == "" { 142 | return fmt.Errorf("cannot be blank") 143 | } 144 | return nil 145 | }, 146 | } 147 | username, err = prompt.Run() 148 | fmt.Fprintln(i.stdout) 149 | if err != nil { 150 | return "", fmt.Errorf("bad username: %w", err) 151 | } 152 | username = stripNewLines(username) 153 | 154 | globalUsername := repoConfig.Merged.GlobalConfig().Section(constants.DgitConfigSection).Option("username") 155 | 156 | if username != "" && username != globalUsername { 157 | log.Debugf("adding decentragit.username to repo config") 158 | newConfig := repoConfig.Merged.AddOption(configformat.LocalScope, constants.DgitConfigSection, configformat.NoSubsection, "username", username) 159 | repoConfig.Merged.SetLocalConfig(newConfig) 160 | err = repoConfig.Validate() 161 | if err != nil { 162 | return "", err 163 | } 164 | err = i.repo.Storer.SetConfig(repoConfig) 165 | if err != nil { 166 | return "", err 167 | } 168 | } 169 | 170 | return username, nil 171 | } 172 | 173 | func (i *Initializer) getAuth(ctx context.Context) (transport.AuthMethod, error) { 174 | if i.auth != nil { 175 | return i.auth, nil 176 | } 177 | 178 | var err error 179 | i.keyring, err = keyring.NewDefault() 180 | // TODO: if no keyring available, prompt user for dgit password 181 | if err != nil { 182 | return nil, fmt.Errorf("Error with keyring: %v", err) 183 | } 184 | 185 | username, err := i.findOrRequestUsername() 186 | if err != nil { 187 | return nil, err 188 | } 189 | 190 | privateKey, err := i.keyring.FindPrivateKey(username) 191 | if errors.Is(err, keyring.ErrKeyNotFound) { 192 | privateKey, err = i.createOrRecoverPrivateKey(ctx, username) 193 | } 194 | if err != nil { 195 | return nil, fmt.Errorf("Error fetching key from keyring: %v", err) 196 | } 197 | 198 | i.auth = dgit.NewPrivateKeyAuth(privateKey) 199 | return i.auth, nil 200 | } 201 | 202 | func (i *Initializer) createPrivateKey(ctx context.Context, username string) (*ecdsa.PrivateKey, error) { 203 | entropy, err := bip39.NewEntropy(256) 204 | if err != nil { 205 | return nil, fmt.Errorf("error generating entropy for mnemoic seed: %w", err) 206 | } 207 | mnemonic, err := bip39.NewMnemonic(entropy) 208 | if err != nil { 209 | return nil, fmt.Errorf("error generating mnemoic seed: %w", err) 210 | } 211 | seed := bip39.NewSeed(mnemonic, username) 212 | 213 | privateKey, err := i.keyring.CreatePrivateKey(username, seed) 214 | if err != nil { 215 | return nil, fmt.Errorf("error creating private key: %w", err) 216 | } 217 | 218 | i.auth = dgit.NewPrivateKeyAuth(privateKey) 219 | 220 | opts := &usertree.Options{ 221 | Name: username, 222 | Tupelo: i.tupelo, 223 | Owners: []string{i.auth.String()}, 224 | } 225 | 226 | userTree, err := usertree.Create(ctx, opts) 227 | if err != nil { 228 | i.keyring.DeletePrivateKey(username) 229 | return nil, err 230 | } 231 | 232 | msg.Fprint(i.stdout, msg.Welcome, map[string]interface{}{ 233 | "username": username, 234 | "did": userTree.Did(), 235 | }) 236 | fmt.Fprintln(i.stdout) 237 | 238 | seedSlice := strings.Split(string(mnemonic), " ") 239 | 240 | msg.Fprint(i.stdout, msg.UserSeedPhraseCreated, map[string]interface{}{ 241 | "username": username, 242 | "seed": " " + strings.Join(seedSlice[0:8], " ") + "\n " + strings.Join(seedSlice[8:16], " ") + "\n " + strings.Join(seedSlice[16:], " "), 243 | }) 244 | fmt.Fprintln(i.stdout) 245 | 246 | return privateKey, nil 247 | } 248 | 249 | func (i *Initializer) recoverPrivateKey(ctx context.Context, username string, userTree *usertree.UserTree) (*ecdsa.PrivateKey, error) { 250 | seedCleaner := func(str string) string { 251 | str = stripNewLines(str) 252 | str = strings.TrimSpace(str) 253 | return strings.ToLower(str) 254 | } 255 | 256 | prompt := promptui.Prompt{ 257 | Label: stripNewLines(msg.Parse(msg.PromptRecoveryPhrase, map[string]interface{}{"username": username})), 258 | Templates: promptTemplates, 259 | Stdin: i.stdin, 260 | Stdout: i.stdout, 261 | Validate: func(input string) error { 262 | seedSlice := strings.Split(seedCleaner(input), " ") 263 | if len(seedSlice) != 24 { 264 | return fmt.Errorf(msg.PromptInvalidRecoveryPhrase) 265 | } 266 | return nil 267 | }, 268 | } 269 | 270 | seed, err := prompt.Run() 271 | fmt.Fprintln(i.stdout) 272 | if err != nil { 273 | return nil, err 274 | } 275 | seed = seedCleaner(seed) 276 | 277 | privateKey, err := i.keyring.CreatePrivateKey(username, bip39.NewSeed(seed, username)) 278 | if err != nil { 279 | return nil, fmt.Errorf("error creating private key: %w", err) 280 | } 281 | 282 | isOwner, err := userTree.IsOwner(ctx, crypto.PubkeyToAddress(privateKey.PublicKey).String()) 283 | if err != nil { 284 | return nil, fmt.Errorf("error checking chaintree ownership: %w", err) 285 | } 286 | 287 | if !isOwner { 288 | msg.Fprint(i.stdout, msg.IncorrectRecoveryPhrase, map[string]interface{}{ 289 | "username": username, 290 | }) 291 | fmt.Fprintln(i.stdout) 292 | i.keyring.DeletePrivateKey(username) 293 | return i.recoverPrivateKey(ctx, username, userTree) 294 | } 295 | 296 | i.auth = dgit.NewPrivateKeyAuth(privateKey) 297 | 298 | msg.Fprint(i.stdout, msg.UserRestored, map[string]interface{}{ 299 | "username": username, 300 | }) 301 | fmt.Fprintln(i.stdout) 302 | 303 | return nil, nil 304 | } 305 | 306 | func (i *Initializer) createOrRecoverPrivateKey(ctx context.Context, username string) (*ecdsa.PrivateKey, error) { 307 | userTree, err := usertree.Find(ctx, username, i.tupelo) 308 | if errors.Is(err, usertree.ErrNotFound) { 309 | return i.createPrivateKey(ctx, username) 310 | } 311 | if err != nil { 312 | return nil, err 313 | } 314 | return i.recoverPrivateKey(ctx, username, userTree) 315 | } 316 | 317 | func (i *Initializer) findOrCreateRepoTree(ctx context.Context) (*repotree.RepoTree, error) { 318 | client, err := dgit.Default() 319 | if err != nil { 320 | return nil, err 321 | } 322 | 323 | _, err = i.getEndpoint() 324 | if err != nil { 325 | return nil, err 326 | } 327 | 328 | tree, err := client.FindRepoTree(ctx, i.repo.MustName()) 329 | // repo already exists, return out 330 | if err == nil && tree != nil { 331 | return tree, nil 332 | } 333 | // a real error, return 334 | if err != namedtree.ErrNotFound { 335 | return nil, err 336 | } 337 | 338 | auth, err := i.getAuth(ctx) 339 | if err != nil { 340 | return nil, err 341 | } 342 | 343 | // repo doesn't exist, create it 344 | log.Debugf("creating new repo tree with endpoint %+v and auth %+v", i.repo.MustEndpoint(), auth) 345 | newTree, err := client.CreateRepoTree(ctx, i.repo.MustEndpoint(), auth) 346 | if errors.Is(err, usertree.ErrNotFound) { 347 | return nil, fmt.Errorf(msg.Parse(msg.UserNotFound, map[string]interface{}{ 348 | "user": strings.Split(i.repo.MustName(), "/")[0], 349 | })) 350 | } 351 | if err != nil { 352 | return nil, err 353 | } 354 | 355 | msg.Fprint(i.stdout, msg.RepoCreated, map[string]interface{}{ 356 | "did": newTree.Did(), 357 | "repo": i.repo.MustName(), 358 | }) 359 | fmt.Fprintln(i.stdout) 360 | 361 | return newTree, nil 362 | } 363 | 364 | func (i *Initializer) getEndpoint() (*transport.Endpoint, error) { 365 | rep, err := i.repo.Endpoint() 366 | 367 | if err == dgit.ErrEndpointNotFound { 368 | rep, err = i.determineDgitEndpoint() 369 | } 370 | 371 | if err != nil { 372 | return nil, err 373 | } 374 | 375 | if rep != nil { 376 | i.repo.SetEndpoint(rep) 377 | } 378 | 379 | return rep, nil 380 | } 381 | 382 | func (i *Initializer) determineDgitEndpoint() (*transport.Endpoint, error) { 383 | var dgitEndpoint *transport.Endpoint 384 | 385 | remotes, err := i.repo.Remotes() 386 | if err != nil { 387 | return nil, err 388 | } 389 | 390 | if len(remotes) > 0 { 391 | otherEndpoint, err := transport.NewEndpoint(remotes[0].Config().URLs[0]) 392 | if err != nil { 393 | return nil, err 394 | } 395 | 396 | repoFullPath := strings.ToLower(strings.TrimSuffix(otherEndpoint.Path, ".git")) 397 | repoUser := strings.Split(repoFullPath, "/")[0] 398 | repoName := strings.TrimPrefix(repoFullPath, repoUser+"/") 399 | dgitEndpoint, err = newDgitEndpoint(repoUser, repoName) 400 | if err != nil { 401 | return nil, err 402 | } 403 | 404 | prompt := promptui.Prompt{ 405 | Label: stripNewLines(msg.Parse(msg.PromptRepoNameConfirm, map[string]interface{}{ 406 | "remote": "origin", 407 | "repo": repoNameFor(dgitEndpoint), 408 | })), 409 | Templates: promptTemplates, 410 | IsConfirm: true, 411 | Default: "y", 412 | Stdin: i.stdin, 413 | Stdout: i.stdout, 414 | } 415 | _, err = prompt.Run() 416 | fmt.Fprintln(i.stdout) 417 | // if err is abort, continue on below 418 | if err != promptui.ErrAbort { 419 | return dgitEndpoint, err 420 | } 421 | } 422 | 423 | var suggestedRepoName string 424 | username, _ := i.repo.Username() 425 | cwd, _ := os.Getwd() 426 | if username != "" && cwd != "" { 427 | suggestedRepoName = username + "/" + path.Base(cwd) 428 | } 429 | 430 | prompt := promptui.Prompt{ 431 | Label: stripNewLines(msg.Parse(msg.PromptRepoName, nil)), 432 | Templates: promptTemplates, 433 | Default: suggestedRepoName, 434 | Stdin: i.stdin, 435 | Stdout: i.stdout, 436 | Validate: func(input string) error { 437 | input = stripNewLines(input) 438 | if !validRepoName.MatchString(input) { 439 | return fmt.Errorf(stripNewLines(msg.Parse(msg.PromptRepoNameInvalid, nil))) 440 | } 441 | return nil 442 | }, 443 | } 444 | 445 | result, err := prompt.Run() 446 | fmt.Fprintln(i.stdout) 447 | if err != nil { 448 | return nil, err 449 | } 450 | result = stripNewLines(strings.ToLower(result)) 451 | repoUser := strings.Split(result, "/")[0] 452 | repoName := strings.TrimPrefix(result, repoUser+"/") 453 | return newDgitEndpoint(repoUser, repoName) 454 | } 455 | 456 | func (i *Initializer) addDgitPushToRemote(ctx context.Context, remoteName string) error { 457 | remote, err := i.repo.Remote(remoteName) 458 | 459 | if err == git.ErrRemoteNotFound { 460 | return nil 461 | } 462 | if err != nil { 463 | return err 464 | } 465 | remoteConfig := remote.Config() 466 | 467 | _, err = i.getEndpoint() 468 | if err != nil { 469 | return err 470 | } 471 | 472 | for _, url := range remoteConfig.URLs { 473 | if url == i.repo.MustURL() { 474 | // already has dgit url, no need to add another 475 | return nil 476 | } 477 | } 478 | 479 | msg.Fprint(i.stdout, msg.AddDgitToRemote, map[string]interface{}{ 480 | "remote": remoteConfig.Name, 481 | "repo": i.repo.MustName(), 482 | "repourl": i.repo.MustURL(), 483 | }) 484 | fmt.Fprintln(i.stdout) 485 | 486 | prompt := promptui.Prompt{ 487 | Label: stripNewLines(msg.Parse(msg.AddDgitToRemoteConfirm, nil)), 488 | Default: "y", 489 | Templates: promptTemplates, 490 | IsConfirm: true, 491 | Stdin: i.stdin, 492 | Stdout: i.stdout, 493 | } 494 | _, err = prompt.Run() 495 | fmt.Fprintln(i.stdout) 496 | if err != nil && err != promptui.ErrAbort { 497 | return err 498 | } 499 | // user doesn't want dgit in origin, return out :( 500 | if err == promptui.ErrAbort { 501 | return nil 502 | } 503 | 504 | remoteConfig.URLs = append(remoteConfig.URLs, i.repo.MustURL()) 505 | 506 | newConfig, err := i.repo.Config() 507 | if err != nil { 508 | return err 509 | } 510 | newConfig.Remotes[remoteConfig.Name] = remoteConfig 511 | err = newConfig.Validate() 512 | if err != nil { 513 | return err 514 | } 515 | 516 | err = i.repo.Storer.SetConfig(newConfig) 517 | if err != nil { 518 | return err 519 | } 520 | 521 | msg.Fprint(i.stdout, msg.AddedDgitToRemote, map[string]interface{}{ 522 | "remote": remoteConfig.Name, 523 | "repo": i.repo.MustName(), 524 | "repourl": i.repo.MustURL(), 525 | }) 526 | fmt.Fprintln(i.stdout) 527 | return nil 528 | } 529 | 530 | func (i *Initializer) addDgitRemote(ctx context.Context) error { 531 | _, err := i.repo.Remote(constants.DgitRemote) 532 | if err != git.ErrRemoteNotFound { 533 | return err 534 | } 535 | 536 | _, err = i.getEndpoint() 537 | if err != nil { 538 | return err 539 | } 540 | 541 | remoteConfig := &config.RemoteConfig{ 542 | Name: constants.DgitRemote, 543 | URLs: []string{i.repo.MustURL()}, 544 | } 545 | err = remoteConfig.Validate() 546 | if err != nil { 547 | return err 548 | } 549 | 550 | msg.Fprint(i.stdout, msg.AddDgitRemote, map[string]interface{}{ 551 | "remote": remoteConfig.Name, 552 | "repo": i.repo.MustName(), 553 | "repourl": i.repo.MustURL(), 554 | }) 555 | fmt.Fprintln(i.stdout) 556 | 557 | prompt := promptui.Prompt{ 558 | Label: stripNewLines(msg.Parse(msg.AddDgitRemoteConfirm, nil)), 559 | Default: "y", 560 | Templates: promptTemplates, 561 | IsConfirm: true, 562 | Stdin: i.stdin, 563 | Stdout: i.stdout, 564 | } 565 | _, err = prompt.Run() 566 | fmt.Fprintln(i.stdout) 567 | // user doesn't want dgit remote, return out :( 568 | if err == promptui.ErrAbort { 569 | return nil 570 | } 571 | if err != nil { 572 | return err 573 | } 574 | 575 | newConfig, err := i.repo.Config() 576 | if err != nil { 577 | return err 578 | } 579 | newConfig.Remotes[remoteConfig.Name] = remoteConfig 580 | err = newConfig.Validate() 581 | if err != nil { 582 | return err 583 | } 584 | 585 | err = i.repo.Storer.SetConfig(newConfig) 586 | if err != nil { 587 | return err 588 | } 589 | 590 | msg.Fprint(i.stdout, msg.AddedDgitRemote, map[string]interface{}{ 591 | "remote": remoteConfig.Name, 592 | "repo": i.repo.MustName(), 593 | "repourl": i.repo.MustURL(), 594 | }) 595 | fmt.Fprintln(i.stdout) 596 | return nil 597 | } 598 | 599 | func newDgitEndpoint(user string, repo string) (*transport.Endpoint, error) { 600 | // the New(String()) is for parsing validation 601 | return transport.NewEndpoint((&transport.Endpoint{ 602 | Protocol: dgit.Protocol(), 603 | Host: user, 604 | Path: repo, 605 | }).String()) 606 | } 607 | 608 | func repoNameFor(e *transport.Endpoint) string { 609 | return e.Host + e.Path 610 | } 611 | 612 | func stripNewLines(s string) string { 613 | replacement := " " 614 | return strings.TrimSpace(strings.NewReplacer( 615 | "\r\n", replacement, 616 | "\r", replacement, 617 | "\n", replacement, 618 | "\v", replacement, 619 | "\f", replacement, 620 | "\u0085", replacement, 621 | "\u2028", replacement, 622 | "\u2029", replacement, 623 | ).Replace(s)) 624 | } 625 | --------------------------------------------------------------------------------