Dodo APP: infrastructure to deploy app by pusing to Git repo

Change-Id: I4034c6893255581b014ddb207c844261cb34202b
diff --git a/core/installer/soft/client.go b/core/installer/soft/client.go
index 269f3d3..08103be 100644
--- a/core/installer/soft/client.go
+++ b/core/installer/soft/client.go
@@ -19,6 +19,8 @@
 	"github.com/go-git/go-git/v5/storage/memory"
 )
 
+var ErrorAlreadyExists = errors.New("already exists")
+
 type Client interface {
 	Address() string
 	Signer() ssh.Signer
@@ -32,6 +34,7 @@
 	MakeUserAdmin(name string) error
 	AddReadWriteCollaborator(repo, user string) error
 	AddReadOnlyCollaborator(repo, user string) error
+	AddWebhook(repo, url string, opts ...string) error
 }
 
 type realClient struct {
@@ -131,6 +134,9 @@
 
 func (ss *realClient) AddRepository(name string) error {
 	log.Printf("Adding repository %s", name)
+	if err := ss.RunCommand("repo", "info", name); err == nil {
+		return ErrorAlreadyExists
+	}
 	return ss.RunCommand("repo", "create", name)
 }
 
@@ -144,6 +150,14 @@
 	return ss.RunCommand("repo", "collab", "add", repo, user, "read-only")
 }
 
+func (ss *realClient) AddWebhook(repo, url string, opts ...string) error {
+	log.Printf("Adding webhook %s %s", repo, url)
+	return ss.RunCommand(append(
+		[]string{"repo", "webhook", "create", repo, url},
+		opts...,
+	)...)
+}
+
 type Repository struct {
 	*git.Repository
 	Addr RepositoryAddress
diff --git a/core/installer/soft/repoio.go b/core/installer/soft/repoio.go
index 6a5097a..b916d24 100644
--- a/core/installer/soft/repoio.go
+++ b/core/installer/soft/repoio.go
@@ -3,10 +3,12 @@
 import (
 	"encoding/json"
 	"errors"
+	"fmt"
 	"io"
 	"io/fs"
 	"io/ioutil"
 	"net"
+	"os"
 	"path/filepath"
 	"sync"
 	"time"
@@ -16,6 +18,7 @@
 	"github.com/go-git/go-billy/v5"
 	"github.com/go-git/go-billy/v5/util"
 	"github.com/go-git/go-git/v5"
+	"github.com/go-git/go-git/v5/config"
 	"github.com/go-git/go-git/v5/plumbing/object"
 	gitssh "github.com/go-git/go-git/v5/plumbing/transport/ssh"
 	"golang.org/x/crypto/ssh"
@@ -33,6 +36,8 @@
 
 type doOptions struct {
 	NoCommit bool
+	Force    bool
+	ToBranch string
 }
 
 type DoOption func(*doOptions)
@@ -43,11 +48,42 @@
 	}
 }
 
+func WithForce() DoOption {
+	return func(o *doOptions) {
+		o.Force = true
+	}
+}
+
+func WithCommitToBranch(branch string) DoOption {
+	return func(o *doOptions) {
+		o.ToBranch = branch
+	}
+}
+
+type pushOptions struct {
+	ToBranch string
+	Force    bool
+}
+
+type PushOption func(*pushOptions)
+
+func WithToBranch(branch string) PushOption {
+	return func(o *pushOptions) {
+		o.ToBranch = branch
+	}
+}
+
+func PushWithForce() PushOption {
+	return func(o *pushOptions) {
+		o.Force = true
+	}
+}
+
 type RepoIO interface {
 	RepoFS
 	FullAddress() string
 	Pull() error
-	CommitAndPush(message string) error
+	CommitAndPush(message string, opts ...PushOption) error
 	Do(op DoFn, opts ...DoOption) error
 }
 
@@ -120,8 +156,9 @@
 		return nil
 	}
 	err = wt.Pull(&git.PullOptions{
-		Auth:  auth(r.signer),
-		Force: true,
+		Auth:     auth(r.signer),
+		Force:    true,
+		Progress: os.Stdout,
 	})
 	if err == nil {
 		return nil
@@ -130,10 +167,15 @@
 		return nil
 	}
 	// TODO(gio): check `remote repository is empty`
+	fmt.Println(err)
 	return nil
 }
 
-func (r *repoIO) CommitAndPush(message string) error {
+func (r *repoIO) CommitAndPush(message string, opts ...PushOption) error {
+	var o pushOptions
+	for _, i := range opts {
+		i(&o)
+	}
 	wt, err := r.repo.Worktree()
 	if err != nil {
 		return err
@@ -149,10 +191,17 @@
 	}); err != nil {
 		return err
 	}
-	return r.repo.Push(&git.PushOptions{
+	gopts := &git.PushOptions{
 		RemoteName: "origin",
 		Auth:       auth(r.signer),
-	})
+	}
+	if o.ToBranch != "" {
+		gopts.RefSpecs = []config.RefSpec{config.RefSpec(fmt.Sprintf("refs/heads/master:refs/heads/%s", o.ToBranch))}
+	}
+	if o.Force {
+		gopts.Force = true
+	}
+	return r.repo.Push(gopts)
 }
 
 func (r *repoIO) Do(op DoFn, opts ...DoOption) error {
@@ -169,7 +218,14 @@
 		return err
 	} else {
 		if !o.NoCommit {
-			return r.CommitAndPush(msg)
+			popts := []PushOption{}
+			if o.Force {
+				popts = append(popts, PushWithForce())
+			}
+			if o.ToBranch != "" {
+				popts = append(popts, WithToBranch(o.ToBranch))
+			}
+			return r.CommitAndPush(msg, popts...)
 		}
 	}
 	return nil
@@ -248,3 +304,12 @@
 	}
 	return ret, nil
 }
+
+func ReadFile(repo RepoFS, path string) ([]byte, error) {
+	r, err := repo.Reader(path)
+	if err != nil {
+		return nil, err
+	}
+	defer r.Close()
+	return io.ReadAll(r)
+}