Merge pull request #219 from MichaelMure/global-config

Michael Muré created

Global config, global bridge token

Change summary

bridge/bridges.go                       |  12 +
bridge/core/bridge.go                   |   6 
bridge/core/token.go                    | 182 +++++++++++++++++++++++++++
bridge/github/github.go                 |   4 
bridge/gitlab/gitlab.go                 |   4 
bridge/launchpad/launchpad.go           |   4 
commands/bridge_auth.go                 |  53 +++++++
commands/bridge_auth_add.go             |  74 ++++++++++
commands/bridge_auth_rm.go              |  36 +++++
commands/bridge_auth_show.go            |  37 +++++
doc/man/git-bug-bridge-auth-add-token.1 |  33 ++++
doc/man/git-bug-bridge-auth-rm.1        |  29 ++++
doc/man/git-bug-bridge-auth-show.1      |  29 ++++
doc/man/git-bug-bridge-auth.1           |  29 ++++
doc/man/git-bug-bridge.1                |   2 
doc/md/git-bug_bridge.md                |   1 
doc/md/git-bug_bridge_auth.md           |  25 +++
doc/md/git-bug_bridge_auth_add-token.md |  23 +++
doc/md/git-bug_bridge_auth_rm.md        |  22 +++
doc/md/git-bug_bridge_auth_show.md      |  22 +++
entity/id.go                            |  18 ++
misc/bash_completion/git-bug            |  88 +++++++++++++
misc/powershell_completion/git-bug      |  18 ++
misc/zsh_completion/git-bug             |  49 +++++++
repository/config.go                    |   2 
repository/config_git.go                |   2 
repository/git.go                       |  12 +
repository/repo.go                      |   6 
28 files changed, 798 insertions(+), 24 deletions(-)

Detailed changes

bridge/bridges.go 🔗

@@ -3,13 +3,19 @@ package bridge
 
 import (
 	"github.com/MichaelMure/git-bug/bridge/core"
-	_ "github.com/MichaelMure/git-bug/bridge/github"
-	_ "github.com/MichaelMure/git-bug/bridge/gitlab"
-	_ "github.com/MichaelMure/git-bug/bridge/launchpad"
+	"github.com/MichaelMure/git-bug/bridge/github"
+	"github.com/MichaelMure/git-bug/bridge/gitlab"
+	"github.com/MichaelMure/git-bug/bridge/launchpad"
 	"github.com/MichaelMure/git-bug/cache"
 	"github.com/MichaelMure/git-bug/repository"
 )
 
+func init() {
+	core.Register(&github.Github{})
+	core.Register(&gitlab.Gitlab{})
+	core.Register(&launchpad.Launchpad{})
+}
+
 // Targets return all known bridge implementation target
 func Targets() []string {
 	return core.Targets()

bridge/core/bridge.go 🔗

@@ -71,6 +71,12 @@ func Targets() []string {
 	return result
 }
 
+// TargetExist return true if the given target has a bridge implementation
+func TargetExist(target string) bool {
+	_, ok := bridgeImpl[target]
+	return ok
+}
+
 // Instantiate a new Bridge for a repo, from the given target and name
 func NewBridge(repo *cache.RepoCache, target string, name string) (*Bridge, error) {
 	implType, ok := bridgeImpl[target]

bridge/core/token.go 🔗

@@ -0,0 +1,182 @@
+package core
+
+import (
+	"crypto/sha256"
+	"errors"
+	"fmt"
+	"regexp"
+	"sort"
+	"strings"
+	"time"
+
+	"github.com/MichaelMure/git-bug/entity"
+	"github.com/MichaelMure/git-bug/repository"
+)
+
+const (
+	tokenConfigKeyPrefix = "git-bug.token"
+	tokenValueKey        = "value"
+	tokenTargetKey       = "target"
+	tokenCreateTimeKey   = "createtime"
+)
+
+var ErrTokenNotExist = errors.New("token doesn't exist")
+
+func NewErrMultipleMatchToken(matching []entity.Id) *entity.ErrMultipleMatch {
+	return entity.NewErrMultipleMatch("token", matching)
+}
+
+// Token holds an API access token data
+type Token struct {
+	Value      string
+	Target     string
+	CreateTime time.Time
+}
+
+// NewToken instantiate a new token
+func NewToken(value, target string) *Token {
+	return &Token{
+		Value:      value,
+		Target:     target,
+		CreateTime: time.Now(),
+	}
+}
+
+func (t *Token) ID() entity.Id {
+	sum := sha256.Sum256([]byte(t.Value))
+	return entity.Id(fmt.Sprintf("%x", sum))
+}
+
+// Validate ensure token important fields are valid
+func (t *Token) Validate() error {
+	if t.Value == "" {
+		return fmt.Errorf("missing value")
+	}
+	if t.Target == "" {
+		return fmt.Errorf("missing target")
+	}
+	if t.CreateTime.IsZero() || t.CreateTime.Equal(time.Time{}) {
+		return fmt.Errorf("missing creation time")
+	}
+	if !TargetExist(t.Target) {
+		return fmt.Errorf("unknown target")
+	}
+	return nil
+}
+
+// LoadToken loads a token from the repo config
+func LoadToken(repo repository.RepoCommon, id entity.Id) (*Token, error) {
+	keyPrefix := fmt.Sprintf("git-bug.token.%s.", id)
+
+	// read token config pairs
+	rawconfigs, err := repo.GlobalConfig().ReadAll(keyPrefix)
+	if err != nil {
+		// Not exactly right due to the limitation of ReadAll()
+		return nil, ErrTokenNotExist
+	}
+
+	// trim key prefix
+	configs := make(map[string]string)
+	for key, value := range rawconfigs {
+		newKey := strings.TrimPrefix(key, keyPrefix)
+		configs[newKey] = value
+	}
+
+	token := &Token{}
+
+	token.Value = configs[tokenValueKey]
+	token.Target = configs[tokenTargetKey]
+	if createTime, ok := configs[tokenCreateTimeKey]; ok {
+		if t, err := repository.ParseTimestamp(createTime); err == nil {
+			token.CreateTime = t
+		}
+	}
+
+	return token, nil
+}
+
+// LoadTokenPrefix load a token from the repo config with a prefix
+func LoadTokenPrefix(repo repository.RepoCommon, prefix string) (*Token, error) {
+	tokens, err := ListTokens(repo)
+	if err != nil {
+		return nil, err
+	}
+
+	// preallocate but empty
+	matching := make([]entity.Id, 0, 5)
+
+	for _, id := range tokens {
+		if id.HasPrefix(prefix) {
+			matching = append(matching, id)
+		}
+	}
+
+	if len(matching) > 1 {
+		return nil, NewErrMultipleMatchToken(matching)
+	}
+
+	if len(matching) == 0 {
+		return nil, ErrTokenNotExist
+	}
+
+	return LoadToken(repo, matching[0])
+}
+
+// ListTokens return a map representing the stored tokens in the repo config and global config
+// along with their type (global: true, local:false)
+func ListTokens(repo repository.RepoCommon) ([]entity.Id, error) {
+	configs, err := repo.GlobalConfig().ReadAll(tokenConfigKeyPrefix + ".")
+	if err != nil {
+		return nil, err
+	}
+
+	re, err := regexp.Compile(tokenConfigKeyPrefix + `.([^.]+)`)
+	if err != nil {
+		panic(err)
+	}
+
+	set := make(map[string]interface{})
+
+	for key := range configs {
+		res := re.FindStringSubmatch(key)
+
+		if res == nil {
+			continue
+		}
+
+		set[res[1]] = nil
+	}
+
+	result := make([]entity.Id, 0, len(set))
+	for key := range set {
+		result = append(result, entity.Id(key))
+	}
+
+	sort.Sort(entity.Alphabetical(result))
+
+	return result, nil
+}
+
+// StoreToken stores a token in the repo config
+func StoreToken(repo repository.RepoCommon, token *Token) error {
+	storeValueKey := fmt.Sprintf("git-bug.token.%s.%s", token.ID().String(), tokenValueKey)
+	err := repo.GlobalConfig().StoreString(storeValueKey, token.Value)
+	if err != nil {
+		return err
+	}
+
+	storeTargetKey := fmt.Sprintf("git-bug.token.%s.%s", token.ID().String(), tokenTargetKey)
+	err = repo.GlobalConfig().StoreString(storeTargetKey, token.Target)
+	if err != nil {
+		return err
+	}
+
+	createTimeKey := fmt.Sprintf("git-bug.token.%s.%s", token.ID().String(), tokenCreateTimeKey)
+	return repo.GlobalConfig().StoreTimestamp(createTimeKey, token.CreateTime)
+}
+
+// RemoveToken removes a token from the repo config
+func RemoveToken(repo repository.RepoCommon, id entity.Id) error {
+	keyPrefix := fmt.Sprintf("git-bug.token.%s", id)
+	return repo.GlobalConfig().RemoveAll(keyPrefix)
+}

bridge/github/github.go 🔗

@@ -10,10 +10,6 @@ import (
 	"github.com/MichaelMure/git-bug/bridge/core"
 )
 
-func init() {
-	core.Register(&Github{})
-}
-
 type Github struct{}
 
 func (*Github) Target() string {

bridge/gitlab/gitlab.go 🔗

@@ -23,10 +23,6 @@ const (
 	defaultTimeout = 60 * time.Second
 )
 
-func init() {
-	core.Register(&Gitlab{})
-}
-
 type Gitlab struct{}
 
 func (*Gitlab) Target() string {

bridge/launchpad/launchpad.go 🔗

@@ -5,10 +5,6 @@ import (
 	"github.com/MichaelMure/git-bug/bridge/core"
 )
 
-func init() {
-	core.Register(&Launchpad{})
-}
-
 type Launchpad struct{}
 
 func (*Launchpad) Target() string {

commands/bridge_auth.go 🔗

@@ -0,0 +1,53 @@
+package commands
+
+import (
+	"fmt"
+
+	"github.com/spf13/cobra"
+
+	text "github.com/MichaelMure/go-term-text"
+
+	"github.com/MichaelMure/git-bug/bridge/core"
+	"github.com/MichaelMure/git-bug/util/colors"
+)
+
+func runBridgeAuth(cmd *cobra.Command, args []string) error {
+	tokens, err := core.ListTokens(repo)
+	if err != nil {
+		return err
+	}
+
+	for _, token := range tokens {
+		token, err := core.LoadToken(repo, token)
+		if err != nil {
+			return err
+		}
+		printToken(token)
+	}
+
+	return nil
+}
+
+func printToken(token *core.Token) {
+	targetFmt := text.LeftPadMaxLine(token.Target, 10, 0)
+
+	fmt.Printf("%s %s %s %s\n",
+		colors.Cyan(token.ID().Human()),
+		colors.Yellow(targetFmt),
+		colors.Magenta("token"),
+		token.Value,
+	)
+}
+
+var bridgeAuthCmd = &cobra.Command{
+	Use:     "auth",
+	Short:   "List all known bridge authentication credentials.",
+	PreRunE: loadRepo,
+	RunE:    runBridgeAuth,
+	Args:    cobra.NoArgs,
+}
+
+func init() {
+	bridgeCmd.AddCommand(bridgeAuthCmd)
+	bridgeAuthCmd.Flags().SortFlags = false
+}

commands/bridge_auth_add.go 🔗

@@ -0,0 +1,74 @@
+package commands
+
+import (
+	"bufio"
+	"fmt"
+	"os"
+	"strings"
+
+	"github.com/mattn/go-isatty"
+	"github.com/pkg/errors"
+	"github.com/spf13/cobra"
+
+	"github.com/MichaelMure/git-bug/bridge"
+	"github.com/MichaelMure/git-bug/bridge/core"
+)
+
+var (
+	bridgeAuthAddTokenTarget string
+)
+
+func runBridgeTokenAdd(cmd *cobra.Command, args []string) error {
+	var value string
+
+	if bridgeAuthAddTokenTarget == "" {
+		return fmt.Errorf("auth target is required")
+	}
+
+	if !core.TargetExist(bridgeAuthAddTokenTarget) {
+		return fmt.Errorf("unknown target")
+	}
+
+	if len(args) == 1 {
+		value = args[0]
+	} else {
+		// Read from Stdin
+		if isatty.IsTerminal(os.Stdin.Fd()) {
+			fmt.Println("Enter the token:")
+		}
+		reader := bufio.NewReader(os.Stdin)
+		raw, err := reader.ReadString('\n')
+		if err != nil {
+			return fmt.Errorf("reading from stdin: %v", err)
+		}
+		value = strings.TrimSuffix(raw, "\n")
+	}
+
+	token := core.NewToken(value, bridgeAuthAddTokenTarget)
+	if err := token.Validate(); err != nil {
+		return errors.Wrap(err, "invalid token")
+	}
+
+	err := core.StoreToken(repo, token)
+	if err != nil {
+		return err
+	}
+
+	fmt.Printf("token %s added\n", token.ID())
+	return nil
+}
+
+var bridgeAuthAddTokenCmd = &cobra.Command{
+	Use:     "add-token [<token>]",
+	Short:   "Store a new token",
+	PreRunE: loadRepo,
+	RunE:    runBridgeTokenAdd,
+	Args:    cobra.MaximumNArgs(1),
+}
+
+func init() {
+	bridgeAuthCmd.AddCommand(bridgeAuthAddTokenCmd)
+	bridgeAuthAddTokenCmd.Flags().StringVarP(&bridgeAuthAddTokenTarget, "target", "t", "",
+		fmt.Sprintf("The target of the bridge. Valid values are [%s]", strings.Join(bridge.Targets(), ",")))
+	bridgeAuthAddTokenCmd.Flags().SortFlags = false
+}

commands/bridge_auth_rm.go 🔗

@@ -0,0 +1,36 @@
+package commands
+
+import (
+	"fmt"
+
+	"github.com/spf13/cobra"
+
+	"github.com/MichaelMure/git-bug/bridge/core"
+)
+
+func runBridgeAuthRm(cmd *cobra.Command, args []string) error {
+	token, err := core.LoadTokenPrefix(repo, args[0])
+	if err != nil {
+		return err
+	}
+
+	err = core.RemoveToken(repo, token.ID())
+	if err != nil {
+		return err
+	}
+
+	fmt.Printf("token %s removed\n", token.ID())
+	return nil
+}
+
+var bridgeAuthRmCmd = &cobra.Command{
+	Use:     "rm <id>",
+	Short:   "Remove a credential.",
+	PreRunE: loadRepo,
+	RunE:    runBridgeAuthRm,
+	Args:    cobra.ExactArgs(1),
+}
+
+func init() {
+	bridgeAuthCmd.AddCommand(bridgeAuthRmCmd)
+}

commands/bridge_auth_show.go 🔗

@@ -0,0 +1,37 @@
+package commands
+
+import (
+	"fmt"
+	"time"
+
+	"github.com/spf13/cobra"
+
+	"github.com/MichaelMure/git-bug/bridge/core"
+)
+
+func runBridgeAuthShow(cmd *cobra.Command, args []string) error {
+	token, err := core.LoadTokenPrefix(repo, args[0])
+	if err != nil {
+		return err
+	}
+
+	fmt.Printf("Id: %s\n", token.ID())
+	fmt.Printf("Target: %s\n", token.Target)
+	fmt.Printf("Type: token\n")
+	fmt.Printf("Value: %s\n", token.Value)
+	fmt.Printf("Creation: %s\n", token.CreateTime.Format(time.RFC822))
+
+	return nil
+}
+
+var bridgeAuthShowCmd = &cobra.Command{
+	Use:     "show",
+	Short:   "Display an authentication credential.",
+	PreRunE: loadRepo,
+	RunE:    runBridgeAuthShow,
+	Args:    cobra.ExactArgs(1),
+}
+
+func init() {
+	bridgeAuthCmd.AddCommand(bridgeAuthShowCmd)
+}

doc/man/git-bug-bridge-auth-add-token.1 🔗

@@ -0,0 +1,33 @@
+.TH "GIT-BUG" "1" "Apr 2019" "Generated from git-bug's source code" "" 
+.nh
+.ad l
+
+
+.SH NAME
+.PP
+git\-bug\-bridge\-auth\-add\-token \- Store a new token
+
+
+.SH SYNOPSIS
+.PP
+\fBgit\-bug bridge auth add\-token [<token>] [flags]\fP
+
+
+.SH DESCRIPTION
+.PP
+Store a new token
+
+
+.SH OPTIONS
+.PP
+\fB\-t\fP, \fB\-\-target\fP=""
+    The target of the bridge. Valid values are [github,gitlab,launchpad\-preview]
+
+.PP
+\fB\-h\fP, \fB\-\-help\fP[=false]
+    help for add\-token
+
+
+.SH SEE ALSO
+.PP
+\fBgit\-bug\-bridge\-auth(1)\fP

doc/man/git-bug-bridge-auth-rm.1 🔗

@@ -0,0 +1,29 @@
+.TH "GIT-BUG" "1" "Apr 2019" "Generated from git-bug's source code" "" 
+.nh
+.ad l
+
+
+.SH NAME
+.PP
+git\-bug\-bridge\-auth\-rm \- Remove a credential.
+
+
+.SH SYNOPSIS
+.PP
+\fBgit\-bug bridge auth rm <id> [flags]\fP
+
+
+.SH DESCRIPTION
+.PP
+Remove a credential.
+
+
+.SH OPTIONS
+.PP
+\fB\-h\fP, \fB\-\-help\fP[=false]
+    help for rm
+
+
+.SH SEE ALSO
+.PP
+\fBgit\-bug\-bridge\-auth(1)\fP

doc/man/git-bug-bridge-auth-show.1 🔗

@@ -0,0 +1,29 @@
+.TH "GIT-BUG" "1" "Apr 2019" "Generated from git-bug's source code" "" 
+.nh
+.ad l
+
+
+.SH NAME
+.PP
+git\-bug\-bridge\-auth\-show \- Display an authentication credential.
+
+
+.SH SYNOPSIS
+.PP
+\fBgit\-bug bridge auth show [flags]\fP
+
+
+.SH DESCRIPTION
+.PP
+Display an authentication credential.
+
+
+.SH OPTIONS
+.PP
+\fB\-h\fP, \fB\-\-help\fP[=false]
+    help for show
+
+
+.SH SEE ALSO
+.PP
+\fBgit\-bug\-bridge\-auth(1)\fP

doc/man/git-bug-bridge-auth.1 🔗

@@ -0,0 +1,29 @@
+.TH "GIT-BUG" "1" "Apr 2019" "Generated from git-bug's source code" "" 
+.nh
+.ad l
+
+
+.SH NAME
+.PP
+git\-bug\-bridge\-auth \- List all known bridge authentication credentials.
+
+
+.SH SYNOPSIS
+.PP
+\fBgit\-bug bridge auth [flags]\fP
+
+
+.SH DESCRIPTION
+.PP
+List all known bridge authentication credentials.
+
+
+.SH OPTIONS
+.PP
+\fB\-h\fP, \fB\-\-help\fP[=false]
+    help for auth
+
+
+.SH SEE ALSO
+.PP
+\fBgit\-bug\-bridge(1)\fP, \fBgit\-bug\-bridge\-auth\-add\-token(1)\fP, \fBgit\-bug\-bridge\-auth\-rm(1)\fP, \fBgit\-bug\-bridge\-auth\-show(1)\fP

doc/man/git-bug-bridge.1 🔗

@@ -26,4 +26,4 @@ Configure and use bridges to other bug trackers.
 
 .SH SEE ALSO
 .PP
-\fBgit\-bug(1)\fP, \fBgit\-bug\-bridge\-configure(1)\fP, \fBgit\-bug\-bridge\-pull(1)\fP, \fBgit\-bug\-bridge\-push(1)\fP, \fBgit\-bug\-bridge\-rm(1)\fP
+\fBgit\-bug(1)\fP, \fBgit\-bug\-bridge\-auth(1)\fP, \fBgit\-bug\-bridge\-configure(1)\fP, \fBgit\-bug\-bridge\-pull(1)\fP, \fBgit\-bug\-bridge\-push(1)\fP, \fBgit\-bug\-bridge\-rm(1)\fP

doc/md/git-bug_bridge.md 🔗

@@ -19,6 +19,7 @@ git-bug bridge [flags]
 ### SEE ALSO
 
 * [git-bug](git-bug.md)	 - A bug tracker embedded in Git.
+* [git-bug bridge auth](git-bug_bridge_auth.md)	 - List all known bridge authentication credentials.
 * [git-bug bridge configure](git-bug_bridge_configure.md)	 - Configure a new bridge.
 * [git-bug bridge pull](git-bug_bridge_pull.md)	 - Pull updates.
 * [git-bug bridge push](git-bug_bridge_push.md)	 - Push updates.

doc/md/git-bug_bridge_auth.md 🔗

@@ -0,0 +1,25 @@
+## git-bug bridge auth
+
+List all known bridge authentication credentials.
+
+### Synopsis
+
+List all known bridge authentication credentials.
+
+```
+git-bug bridge auth [flags]
+```
+
+### Options
+
+```
+  -h, --help   help for auth
+```
+
+### SEE ALSO
+
+* [git-bug bridge](git-bug_bridge.md)	 - Configure and use bridges to other bug trackers.
+* [git-bug bridge auth add-token](git-bug_bridge_auth_add-token.md)	 - Store a new token
+* [git-bug bridge auth rm](git-bug_bridge_auth_rm.md)	 - Remove a credential.
+* [git-bug bridge auth show](git-bug_bridge_auth_show.md)	 - Display an authentication credential.
+

doc/md/git-bug_bridge_auth_add-token.md 🔗

@@ -0,0 +1,23 @@
+## git-bug bridge auth add-token
+
+Store a new token
+
+### Synopsis
+
+Store a new token
+
+```
+git-bug bridge auth add-token [<token>] [flags]
+```
+
+### Options
+
+```
+  -t, --target string   The target of the bridge. Valid values are [github,gitlab,launchpad-preview]
+  -h, --help            help for add-token
+```
+
+### SEE ALSO
+
+* [git-bug bridge auth](git-bug_bridge_auth.md)	 - List all known bridge authentication credentials.
+

doc/md/git-bug_bridge_auth_rm.md 🔗

@@ -0,0 +1,22 @@
+## git-bug bridge auth rm
+
+Remove a credential.
+
+### Synopsis
+
+Remove a credential.
+
+```
+git-bug bridge auth rm <id> [flags]
+```
+
+### Options
+
+```
+  -h, --help   help for rm
+```
+
+### SEE ALSO
+
+* [git-bug bridge auth](git-bug_bridge_auth.md)	 - List all known bridge authentication credentials.
+

doc/md/git-bug_bridge_auth_show.md 🔗

@@ -0,0 +1,22 @@
+## git-bug bridge auth show
+
+Display an authentication credential.
+
+### Synopsis
+
+Display an authentication credential.
+
+```
+git-bug bridge auth show [flags]
+```
+
+### Options
+
+```
+  -h, --help   help for show
+```
+
+### SEE ALSO
+
+* [git-bug bridge auth](git-bug_bridge_auth.md)	 - List all known bridge authentication credentials.
+

entity/id.go 🔗

@@ -65,3 +65,21 @@ func (i Id) Validate() error {
 	}
 	return nil
 }
+
+/*
+ * Sorting
+ */
+
+type Alphabetical []Id
+
+func (a Alphabetical) Len() int {
+	return len(a)
+}
+
+func (a Alphabetical) Less(i, j int) bool {
+	return a[i] < a[j]
+}
+
+func (a Alphabetical) Swap(i, j int) {
+	a[i], a[j] = a[j], a[i]
+}

misc/bash_completion/git-bug 🔗

@@ -287,6 +287,93 @@ _git-bug_add()
     noun_aliases=()
 }
 
+_git-bug_bridge_auth_add-token()
+{
+    last_command="git-bug_bridge_auth_add-token"
+
+    command_aliases=()
+
+    commands=()
+
+    flags=()
+    two_word_flags=()
+    local_nonpersistent_flags=()
+    flags_with_completion=()
+    flags_completion=()
+
+    flags+=("--target=")
+    two_word_flags+=("--target")
+    two_word_flags+=("-t")
+    local_nonpersistent_flags+=("--target=")
+
+    must_have_one_flag=()
+    must_have_one_noun=()
+    noun_aliases=()
+}
+
+_git-bug_bridge_auth_rm()
+{
+    last_command="git-bug_bridge_auth_rm"
+
+    command_aliases=()
+
+    commands=()
+
+    flags=()
+    two_word_flags=()
+    local_nonpersistent_flags=()
+    flags_with_completion=()
+    flags_completion=()
+
+
+    must_have_one_flag=()
+    must_have_one_noun=()
+    noun_aliases=()
+}
+
+_git-bug_bridge_auth_show()
+{
+    last_command="git-bug_bridge_auth_show"
+
+    command_aliases=()
+
+    commands=()
+
+    flags=()
+    two_word_flags=()
+    local_nonpersistent_flags=()
+    flags_with_completion=()
+    flags_completion=()
+
+
+    must_have_one_flag=()
+    must_have_one_noun=()
+    noun_aliases=()
+}
+
+_git-bug_bridge_auth()
+{
+    last_command="git-bug_bridge_auth"
+
+    command_aliases=()
+
+    commands=()
+    commands+=("add-token")
+    commands+=("rm")
+    commands+=("show")
+
+    flags=()
+    two_word_flags=()
+    local_nonpersistent_flags=()
+    flags_with_completion=()
+    flags_completion=()
+
+
+    must_have_one_flag=()
+    must_have_one_noun=()
+    noun_aliases=()
+}
+
 _git-bug_bridge_configure()
 {
     last_command="git-bug_bridge_configure"
@@ -407,6 +494,7 @@ _git-bug_bridge()
     command_aliases=()
 
     commands=()
+    commands+=("auth")
     commands+=("configure")
     commands+=("pull")
     commands+=("push")

misc/powershell_completion/git-bug 🔗

@@ -48,12 +48,30 @@ Register-ArgumentCompleter -Native -CommandName 'git-bug' -ScriptBlock {
             break
         }
         'git-bug;bridge' {
+            [CompletionResult]::new('auth', 'auth', [CompletionResultType]::ParameterValue, 'List all known bridge authentication credentials.')
             [CompletionResult]::new('configure', 'configure', [CompletionResultType]::ParameterValue, 'Configure a new bridge.')
             [CompletionResult]::new('pull', 'pull', [CompletionResultType]::ParameterValue, 'Pull updates.')
             [CompletionResult]::new('push', 'push', [CompletionResultType]::ParameterValue, 'Push updates.')
             [CompletionResult]::new('rm', 'rm', [CompletionResultType]::ParameterValue, 'Delete a configured bridge.')
             break
         }
+        'git-bug;bridge;auth' {
+            [CompletionResult]::new('add-token', 'add-token', [CompletionResultType]::ParameterValue, 'Store a new token')
+            [CompletionResult]::new('rm', 'rm', [CompletionResultType]::ParameterValue, 'Remove a credential.')
+            [CompletionResult]::new('show', 'show', [CompletionResultType]::ParameterValue, 'Display an authentication credential.')
+            break
+        }
+        'git-bug;bridge;auth;add-token' {
+            [CompletionResult]::new('-t', 't', [CompletionResultType]::ParameterName, 'The target of the bridge. Valid values are [github,gitlab,launchpad-preview]')
+            [CompletionResult]::new('--target', 'target', [CompletionResultType]::ParameterName, 'The target of the bridge. Valid values are [github,gitlab,launchpad-preview]')
+            break
+        }
+        'git-bug;bridge;auth;rm' {
+            break
+        }
+        'git-bug;bridge;auth;show' {
+            break
+        }
         'git-bug;bridge;configure' {
             [CompletionResult]::new('-n', 'n', [CompletionResultType]::ParameterName, 'A distinctive name to identify the bridge')
             [CompletionResult]::new('--name', 'name', [CompletionResultType]::ParameterName, 'A distinctive name to identify the bridge')

misc/zsh_completion/git-bug 🔗

@@ -114,6 +114,7 @@ function _git-bug_bridge {
   case $state in
   cmnds)
     commands=(
+      "auth:List all known bridge authentication credentials."
       "configure:Configure a new bridge."
       "pull:Pull updates."
       "push:Push updates."
@@ -124,6 +125,9 @@ function _git-bug_bridge {
   esac
 
   case "$words[1]" in
+  auth)
+    _git-bug_bridge_auth
+    ;;
   configure)
     _git-bug_bridge_configure
     ;;
@@ -139,6 +143,51 @@ function _git-bug_bridge {
   esac
 }
 
+
+function _git-bug_bridge_auth {
+  local -a commands
+
+  _arguments -C \
+    "1: :->cmnds" \
+    "*::arg:->args"
+
+  case $state in
+  cmnds)
+    commands=(
+      "add-token:Store a new token"
+      "rm:Remove a credential."
+      "show:Display an authentication credential."
+    )
+    _describe "command" commands
+    ;;
+  esac
+
+  case "$words[1]" in
+  add-token)
+    _git-bug_bridge_auth_add-token
+    ;;
+  rm)
+    _git-bug_bridge_auth_rm
+    ;;
+  show)
+    _git-bug_bridge_auth_show
+    ;;
+  esac
+}
+
+function _git-bug_bridge_auth_add-token {
+  _arguments \
+    '(-t --target)'{-t,--target}'[The target of the bridge. Valid values are [github,gitlab,launchpad-preview]]:'
+}
+
+function _git-bug_bridge_auth_rm {
+  _arguments
+}
+
+function _git-bug_bridge_auth_show {
+  _arguments
+}
+
 function _git-bug_bridge_configure {
   _arguments \
     '(-n --name)'{-n,--name}'[A distinctive name to identify the bridge]:' \

repository/config.go 🔗

@@ -38,7 +38,7 @@ type Config interface {
 	RemoveAll(keyPrefix string) error
 }
 
-func parseTimestamp(s string) (time.Time, error) {
+func ParseTimestamp(s string) (time.Time, error) {
 	timestamp, err := strconv.Atoi(s)
 	if err != nil {
 		return time.Time{}, err

repository/config_git.go 🔗

@@ -116,7 +116,7 @@ func (gc *gitConfig) ReadTimestamp(key string) (time.Time, error) {
 	if err != nil {
 		return time.Time{}, err
 	}
-	return parseTimestamp(value)
+	return ParseTimestamp(value)
 }
 
 func (gc *gitConfig) rmSection(keyPrefix string) error {

repository/git.go 🔗

@@ -15,11 +15,15 @@ import (
 	"github.com/MichaelMure/git-bug/util/lamport"
 )
 
-const createClockFile = "/git-bug/create-clock"
-const editClockFile = "/git-bug/edit-clock"
+const (
+	createClockFile = "/git-bug/create-clock"
+	editClockFile   = "/git-bug/edit-clock"
+)
 
-// ErrNotARepo is the error returned when the git repo root wan't be found
-var ErrNotARepo = errors.New("not a git repository")
+var (
+	// ErrNotARepo is the error returned when the git repo root wan't be found
+	ErrNotARepo = errors.New("not a git repository")
+)
 
 var _ ClockedRepo = &GitRepo{}
 

repository/repo.go 🔗

@@ -10,8 +10,10 @@ import (
 	"github.com/MichaelMure/git-bug/util/lamport"
 )
 
-var ErrNoConfigEntry = errors.New("no config entry for the given key")
-var ErrMultipleConfigEntry = errors.New("multiple config entry for the given key")
+var (
+	ErrNoConfigEntry       = errors.New("no config entry for the given key")
+	ErrMultipleConfigEntry = errors.New("multiple config entry for the given key")
+)
 
 // RepoCommon represent the common function the we want all the repo to implement
 type RepoCommon interface {