bridge: allow to configure and pull without having set a user first

Michael Muré created

- init() only the importer or exporter as required
- assign a "default user" user Id to credentials at creation if no user has been set
- "bridge auth": also display the user
- "bridge auth show": adapt to a potential "default user" user Id
- "bridge configure": allow to run without a user set
- "bridge pull": allow to run without a user set
- "user adopt": replace "default user" by the actual user id when run

Change summary

bridge/core/auth/credential.go | 30 +++++++++++++++++++++++-
bridge/core/auth/token.go      |  6 ++++
bridge/core/bridge.go          | 43 +++++++++++++++++++++++++----------
bridge/github/config.go        | 17 ++++++++++----
bridge/github/import.go        |  4 +++
bridge/github/import_test.go   |  3 ++
bridge/gitlab/config.go        | 17 ++++++++++----
bridge/gitlab/import.go        |  4 +++
bridge/gitlab/import_test.go   |  3 ++
commands/bridge_auth.go        | 21 +++++++++++-----
commands/bridge_auth_show.go   | 29 ++++++++++++++++++++++++
commands/bridge_configure.go   |  2 
commands/bridge_pull.go        |  2 
commands/user_adopt.go         | 12 ++++++++++
14 files changed, 158 insertions(+), 35 deletions(-)

Detailed changes

bridge/core/auth/credential.go 🔗

@@ -32,9 +32,15 @@ func NewErrMultipleMatchCredential(matching []entity.Id) *entity.ErrMultipleMatc
 	return entity.NewErrMultipleMatch("credential", matching)
 }
 
+// Special Id to mark a credential as being associated to the default user, whoever it might be.
+// The intended use is for the bridge configuration, to be able to create and store a credential
+// with no identities created yet, and then select one with `git-bug user adopt`
+const DefaultUserId = entity.Id("default-user")
+
 type Credential interface {
 	ID() entity.Id
 	UserId() entity.Id
+	updateUserId(id entity.Id)
 	Target() string
 	Kind() CredentialKind
 	CreateTime() time.Time
@@ -42,7 +48,7 @@ type Credential interface {
 
 	// Return all the specific properties of the credential that need to be saved into the configuration.
 	// This does not include Target, User, Kind and CreateTime.
-	ToConfig() map[string]string
+	toConfig() map[string]string
 }
 
 // Load loads a credential from the repo config
@@ -90,6 +96,7 @@ func LoadWithPrefix(repo repository.RepoConfig, prefix string) (Credential, erro
 	return matching[0], nil
 }
 
+// loadFromConfig is a helper to construct a Credential from the set of git configs
 func loadFromConfig(rawConfigs map[string]string, id entity.Id) (Credential, error) {
 	keyPrefix := fmt.Sprintf("%s.%s.", configKeyPrefix, id)
 
@@ -168,7 +175,7 @@ func PrefixExist(repo repository.RepoConfig, prefix string) bool {
 
 // Store stores a credential in the global git config
 func Store(repo repository.RepoConfig, cred Credential) error {
-	confs := cred.ToConfig()
+	confs := cred.toConfig()
 
 	prefix := fmt.Sprintf("%s.%s.", configKeyPrefix, cred.ID())
 
@@ -213,6 +220,25 @@ func Remove(repo repository.RepoConfig, id entity.Id) error {
 	return repo.GlobalConfig().RemoveAll(keyPrefix)
 }
 
+// ReplaceDefaultUser update all the credential attributed to the temporary "default user"
+// with a real user Id
+func ReplaceDefaultUser(repo repository.RepoConfig, id entity.Id) error {
+	list, err := List(repo, WithUserId(DefaultUserId))
+	if err != nil {
+		return err
+	}
+
+	for _, cred := range list {
+		cred.updateUserId(id)
+		err = Store(repo, cred)
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
 /*
  * Sorting
  */

bridge/core/auth/token.go 🔗

@@ -59,6 +59,10 @@ func (t *Token) UserId() entity.Id {
 	return t.userId
 }
 
+func (t *Token) updateUserId(id entity.Id) {
+	t.userId = id
+}
+
 func (t *Token) Target() string {
 	return t.target
 }
@@ -88,7 +92,7 @@ func (t *Token) Validate() error {
 	return nil
 }
 
-func (t *Token) ToConfig() map[string]string {
+func (t *Token) toConfig() map[string]string {
 	return map[string]string{
 		tokenValueKey: t.Value,
 	}

bridge/core/bridge.go 🔗

@@ -43,13 +43,14 @@ type BridgeParams struct {
 // Bridge is a wrapper around a BridgeImpl that will bind low-level
 // implementation with utility code to provide high-level functions.
 type Bridge struct {
-	Name     string
-	repo     *cache.RepoCache
-	impl     BridgeImpl
-	importer Importer
-	exporter Exporter
-	conf     Configuration
-	initDone bool
+	Name           string
+	repo           *cache.RepoCache
+	impl           BridgeImpl
+	importer       Importer
+	exporter       Exporter
+	conf           Configuration
+	initImportDone bool
+	initExportDone bool
 }
 
 // Register will register a new BridgeImpl
@@ -273,8 +274,25 @@ func (b *Bridge) getExporter() Exporter {
 	return b.exporter
 }
 
-func (b *Bridge) ensureInit() error {
-	if b.initDone {
+func (b *Bridge) ensureImportInit() error {
+	if b.initImportDone {
+		return nil
+	}
+
+	importer := b.getImporter()
+	if importer != nil {
+		err := importer.Init(b.repo, b.conf)
+		if err != nil {
+			return err
+		}
+	}
+
+	b.initImportDone = true
+	return nil
+}
+
+func (b *Bridge) ensureExportInit() error {
+	if b.initExportDone {
 		return nil
 	}
 
@@ -294,8 +312,7 @@ func (b *Bridge) ensureInit() error {
 		}
 	}
 
-	b.initDone = true
-
+	b.initExportDone = true
 	return nil
 }
 
@@ -313,7 +330,7 @@ func (b *Bridge) ImportAllSince(ctx context.Context, since time.Time) (<-chan Im
 		return nil, err
 	}
 
-	err = b.ensureInit()
+	err = b.ensureImportInit()
 	if err != nil {
 		return nil, err
 	}
@@ -367,7 +384,7 @@ func (b *Bridge) ExportAll(ctx context.Context, since time.Time) (<-chan ExportR
 		return nil, err
 	}
 
-	err = b.ensureInit()
+	err = b.ensureExportInit()
 	if err != nil {
 		return nil, err
 	}

bridge/github/config.go 🔗

@@ -25,6 +25,7 @@ import (
 	"github.com/MichaelMure/git-bug/bridge/core/auth"
 	"github.com/MichaelMure/git-bug/cache"
 	"github.com/MichaelMure/git-bug/entity"
+	"github.com/MichaelMure/git-bug/identity"
 	"github.com/MichaelMure/git-bug/repository"
 	"github.com/MichaelMure/git-bug/util/colors"
 	"github.com/MichaelMure/git-bug/util/interrupt"
@@ -89,10 +90,16 @@ func (g *Github) Configure(repo *cache.RepoCache, params core.BridgeParams) (cor
 	}
 
 	user, err := repo.GetUserIdentity()
-	if err != nil {
+	if err != nil && err != identity.ErrNoIdentitySet {
 		return nil, err
 	}
 
+	// default to a "to be filled" user Id if we don't have a valid one yet
+	userId := auth.DefaultUserId
+	if user != nil {
+		userId = user.Id()
+	}
+
 	var cred auth.Credential
 
 	switch {
@@ -101,13 +108,13 @@ func (g *Github) Configure(repo *cache.RepoCache, params core.BridgeParams) (cor
 		if err != nil {
 			return nil, err
 		}
-		if cred.UserId() != user.Id() {
+		if user != nil && cred.UserId() != user.Id() {
 			return nil, fmt.Errorf("selected credential don't match the user")
 		}
 	case params.TokenRaw != "":
-		cred = auth.NewToken(user.Id(), params.TokenRaw, target)
+		cred = auth.NewToken(userId, params.TokenRaw, target)
 	default:
-		cred, err = promptTokenOptions(repo, user.Id(), owner, project)
+		cred, err = promptTokenOptions(repo, userId, owner, project)
 		if err != nil {
 			return nil, err
 		}
@@ -326,7 +333,7 @@ func promptToken() (string, error) {
 			return token, nil
 		}
 
-		fmt.Println("token is invalid")
+		fmt.Println("token has incorrect format")
 	}
 }
 

bridge/github/import.go 🔗

@@ -12,6 +12,7 @@ import (
 	"github.com/MichaelMure/git-bug/bug"
 	"github.com/MichaelMure/git-bug/cache"
 	"github.com/MichaelMure/git-bug/entity"
+	"github.com/MichaelMure/git-bug/identity"
 	"github.com/MichaelMure/git-bug/util/text"
 )
 
@@ -47,6 +48,9 @@ func (gi *githubImporter) Init(repo *cache.RepoCache, conf core.Configuration) e
 	if err == nil {
 		opts = append(opts, auth.WithUserId(user.Id()))
 	}
+	if err == identity.ErrNoIdentitySet {
+		opts = append(opts, auth.WithUserId(auth.DefaultUserId))
+	}
 
 	creds, err := auth.List(repo, opts...)
 	if err != nil {

bridge/github/import_test.go 🔗

@@ -143,6 +143,9 @@ func Test_Importer(t *testing.T) {
 	err = author.Commit(repo)
 	require.NoError(t, err)
 
+	err = identity.SetUserIdentity(repo, author)
+	require.NoError(t, err)
+
 	token := auth.NewToken(author.Id(), envToken, target)
 	err = auth.Store(repo, token)
 	require.NoError(t, err)

bridge/gitlab/config.go 🔗

@@ -19,6 +19,7 @@ import (
 	"github.com/MichaelMure/git-bug/bridge/core/auth"
 	"github.com/MichaelMure/git-bug/cache"
 	"github.com/MichaelMure/git-bug/entity"
+	"github.com/MichaelMure/git-bug/identity"
 	"github.com/MichaelMure/git-bug/repository"
 	"github.com/MichaelMure/git-bug/util/colors"
 )
@@ -65,10 +66,16 @@ func (g *Gitlab) Configure(repo *cache.RepoCache, params core.BridgeParams) (cor
 	}
 
 	user, err := repo.GetUserIdentity()
-	if err != nil {
+	if err != nil && err != identity.ErrNoIdentitySet {
 		return nil, err
 	}
 
+	// default to a "to be filled" user Id if we don't have a valid one yet
+	userId := auth.DefaultUserId
+	if user != nil {
+		userId = user.Id()
+	}
+
 	var cred auth.Credential
 
 	switch {
@@ -77,13 +84,13 @@ func (g *Gitlab) Configure(repo *cache.RepoCache, params core.BridgeParams) (cor
 		if err != nil {
 			return nil, err
 		}
-		if cred.UserId() != user.Id() {
+		if user != nil && cred.UserId() != user.Id() {
 			return nil, fmt.Errorf("selected credential don't match the user")
 		}
 	case params.TokenRaw != "":
-		cred = auth.NewToken(user.Id(), params.TokenRaw, target)
+		cred = auth.NewToken(userId, params.TokenRaw, target)
 	default:
-		cred, err = promptTokenOptions(repo, user.Id())
+		cred, err = promptTokenOptions(repo, userId)
 		if err != nil {
 			return nil, err
 		}
@@ -221,7 +228,7 @@ func promptToken() (string, error) {
 			return token, nil
 		}
 
-		fmt.Println("token format is invalid")
+		fmt.Println("token has incorrect format")
 	}
 }
 

bridge/gitlab/import.go 🔗

@@ -13,6 +13,7 @@ import (
 	"github.com/MichaelMure/git-bug/bug"
 	"github.com/MichaelMure/git-bug/cache"
 	"github.com/MichaelMure/git-bug/entity"
+	"github.com/MichaelMure/git-bug/identity"
 	"github.com/MichaelMure/git-bug/util/text"
 )
 
@@ -42,6 +43,9 @@ func (gi *gitlabImporter) Init(repo *cache.RepoCache, conf core.Configuration) e
 	if err == nil {
 		opts = append(opts, auth.WithUserId(user.Id()))
 	}
+	if err == identity.ErrNoIdentitySet {
+		opts = append(opts, auth.WithUserId(auth.DefaultUserId))
+	}
 
 	creds, err := auth.List(repo, opts...)
 	if err != nil {

bridge/gitlab/import_test.go 🔗

@@ -97,6 +97,9 @@ func TestImport(t *testing.T) {
 	err = author.Commit(repo)
 	require.NoError(t, err)
 
+	err = identity.SetUserIdentity(repo, author)
+	require.NoError(t, err)
+
 	token := auth.NewToken(author.Id(), envToken, target)
 	err = auth.Store(repo, token)
 	require.NoError(t, err)

commands/bridge_auth.go 🔗

@@ -37,14 +37,21 @@ func runBridgeAuth(cmd *cobra.Command, args []string) error {
 			value = cred.Value
 		}
 
-		user, err := backend.ResolveIdentity(cred.UserId())
-		if err != nil {
-			return err
-		}
-		userFmt := user.DisplayName()
+		var userFmt string
+
+		switch cred.UserId() {
+		case auth.DefaultUserId:
+			userFmt = colors.Red("default user")
+		default:
+			user, err := backend.ResolveIdentity(cred.UserId())
+			if err != nil {
+				return err
+			}
+			userFmt = user.DisplayName()
 
-		if cred.UserId() == defaultUser.Id() {
-			userFmt = colors.Red(userFmt)
+			if cred.UserId() == defaultUser.Id() {
+				userFmt = colors.Red(userFmt)
+			}
 		}
 
 		fmt.Printf("%s %s %s %s %s\n",

commands/bridge_auth_show.go 🔗

@@ -7,17 +7,46 @@ import (
 	"github.com/spf13/cobra"
 
 	"github.com/MichaelMure/git-bug/bridge/core/auth"
+	"github.com/MichaelMure/git-bug/cache"
+	"github.com/MichaelMure/git-bug/util/colors"
+	"github.com/MichaelMure/git-bug/util/interrupt"
 )
 
 func runBridgeAuthShow(cmd *cobra.Command, args []string) error {
+	backend, err := cache.NewRepoCache(repo)
+	if err != nil {
+		return err
+	}
+	defer backend.Close()
+	interrupt.RegisterCleaner(backend.Close)
+
 	cred, err := auth.LoadWithPrefix(repo, args[0])
 	if err != nil {
 		return err
 	}
 
+	var userFmt string
+
+	switch cred.UserId() {
+	case auth.DefaultUserId:
+		userFmt = colors.Red("default user")
+	default:
+		user, err := backend.ResolveIdentity(cred.UserId())
+		if err != nil {
+			return err
+		}
+		userFmt = user.DisplayName()
+
+		defaultUser, _ := backend.GetUserIdentity()
+		if cred.UserId() == defaultUser.Id() {
+			userFmt = colors.Red(userFmt)
+		}
+	}
+
 	fmt.Printf("Id: %s\n", cred.ID())
 	fmt.Printf("Target: %s\n", cred.Target())
 	fmt.Printf("Kind: %s\n", cred.Kind())
+	fmt.Printf("User: %s\n", userFmt)
 	fmt.Printf("Creation: %s\n", cred.CreateTime().Format(time.RFC822))
 
 	switch cred := cred.(type) {

commands/bridge_configure.go 🔗

@@ -206,7 +206,7 @@ git bug bridge configure \
     --target=github \
     --url=https://github.com/michaelmure/git-bug \
     --token=$(TOKEN)`,
-	PreRunE: loadRepoEnsureUser,
+	PreRunE: loadRepo,
 	RunE:    runBridgeConfigure,
 }
 

commands/bridge_pull.go 🔗

@@ -138,7 +138,7 @@ func parseSince(since string) (time.Time, error) {
 var bridgePullCmd = &cobra.Command{
 	Use:     "pull [<name>]",
 	Short:   "Pull updates.",
-	PreRunE: loadRepoEnsureUser,
+	PreRunE: loadRepo,
 	RunE:    runBridgePull,
 	Args:    cobra.MaximumNArgs(1),
 }

commands/user_adopt.go 🔗

@@ -4,7 +4,9 @@ import (
 	"fmt"
 	"os"
 
+	"github.com/MichaelMure/git-bug/bridge/core/auth"
 	"github.com/MichaelMure/git-bug/cache"
+	"github.com/MichaelMure/git-bug/identity"
 	"github.com/MichaelMure/git-bug/util/interrupt"
 	"github.com/spf13/cobra"
 )
@@ -24,6 +26,16 @@ func runUserAdopt(cmd *cobra.Command, args []string) error {
 		return err
 	}
 
+	_, err = backend.GetUserIdentity()
+	if err == identity.ErrNoIdentitySet {
+		err = auth.ReplaceDefaultUser(repo, i.Id())
+		if err != nil {
+			return err
+		}
+	} else if err != nil {
+		return err
+	}
+
 	err = backend.SetUserIdentity(i)
 	if err != nil {
 		return err