├── 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 | [](http://badges.mit-license.org)
3 | [](CODE_OF_CONDUCT.md)
4 |
5 |
6 |
7 |
8 |
9 |
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 |
--------------------------------------------------------------------------------