Detailed changes
@@ -3,6 +3,7 @@ package bridge
import (
"github.com/MichaelMure/git-bug/bridge/core"
+ "github.com/MichaelMure/git-bug/bridge/gitea"
"github.com/MichaelMure/git-bug/bridge/github"
"github.com/MichaelMure/git-bug/bridge/gitlab"
"github.com/MichaelMure/git-bug/bridge/jira"
@@ -12,6 +13,7 @@ import (
)
func init() {
+ core.Register(&gitea.Gitea{})
core.Register(&github.Github{})
core.Register(&gitlab.Gitlab{})
core.Register(&launchpad.Launchpad{})
@@ -5,13 +5,13 @@ import "fmt"
// BridgeParams holds parameters to simplify the bridge configuration without
// having to make terminal prompts.
type BridgeParams struct {
- URL string // complete URL of a repo (Github, Gitlab, , Launchpad)
- BaseURL string // base URL for self-hosted instance ( Gitlab, Jira, )
- Login string // username for the passed credential (Github, Gitlab, Jira, )
- CredPrefix string // ID prefix of the credential to use (Github, Gitlab, Jira, )
- TokenRaw string // pre-existing token to use (Github, Gitlab, , )
- Owner string // owner of the repo (Github, , , )
- Project string // name of the repo or project key (Github, , Jira, Launchpad)
+ URL string // complete URL of a repo (Gitea, Github, Gitlab, , Launchpad)
+ BaseURL string // base URL for self-hosted instance ( , , Gitlab, Jira, )
+ Login string // username for the passed credential (Gitea, Github, Gitlab, Jira, )
+ CredPrefix string // ID prefix of the credential to use (Gitea, Github, Gitlab, Jira, )
+ TokenRaw string // pre-existing token to use (Gitea, Github, Gitlab, , )
+ Owner string // owner of the repo ( , Github, , , )
+ Project string // name of the repo or project key ( , Github, , Jira, Launchpad)
}
func (BridgeParams) fieldWarning(field string, target string) string {
@@ -0,0 +1,305 @@
+package gitea
+
+import (
+ "context"
+ "fmt"
+ "path"
+ "regexp"
+ "sort"
+ "strings"
+
+ "github.com/pkg/errors"
+
+ "github.com/MichaelMure/git-bug/bridge/core"
+ "github.com/MichaelMure/git-bug/bridge/core/auth"
+ "github.com/MichaelMure/git-bug/cache"
+ "github.com/MichaelMure/git-bug/commands/input"
+ "github.com/MichaelMure/git-bug/repository"
+)
+
+var (
+ ErrBadProjectURL = errors.New("bad project url")
+)
+
+func (g *Gitea) ValidParams() map[string]interface{} {
+ return map[string]interface{}{
+ "URL": nil,
+ "Login": nil,
+ "CredPrefix": nil,
+ "TokenRaw": nil,
+ }
+}
+
+func (g *Gitea) Configure(repo *cache.RepoCache, params core.BridgeParams, interactive bool) (core.Configuration, error) {
+ var err error
+ var baseURL, owner, project string
+
+ // get project url
+ switch {
+ case params.URL != "":
+ baseURL, owner, project, err = splitURL(params.URL)
+ if err != nil {
+ return nil, err
+ }
+ default:
+ // terminal prompt
+ if !interactive {
+ return nil, fmt.Errorf("Non-interactive-mode is active. Please specify the gitea project URL via the --url option.")
+ }
+ baseURL, owner, project, err = promptURL(repo)
+ if err != nil {
+ return nil, errors.Wrap(err, "url prompt")
+ }
+ }
+
+ var login string
+ var cred auth.Credential
+
+ switch {
+ case params.CredPrefix != "":
+ cred, err = auth.LoadWithPrefix(repo, params.CredPrefix)
+ if err != nil {
+ return nil, err
+ }
+ l, ok := cred.GetMetadata(auth.MetaKeyLogin)
+ if !ok {
+ return nil, fmt.Errorf("credential doesn't have a login")
+ }
+ login = l
+ case params.TokenRaw != "":
+ token := auth.NewToken(target, params.TokenRaw)
+ login, err = getLoginFromToken(baseURL, token)
+ if err != nil {
+ return nil, err
+ }
+ token.SetMetadata(auth.MetaKeyLogin, login)
+ token.SetMetadata(auth.MetaKeyBaseURL, baseURL)
+ cred = token
+ default:
+ if params.Login == "" {
+ if !interactive {
+ return nil, fmt.Errorf("Non-interactive-mode is active. Please specify the login name via the --login option.")
+ }
+ // TODO: validate username
+ login, err = input.Prompt("Gitea login", "login", input.Required)
+ } else {
+ // TODO: validate username
+ login = params.Login
+ }
+ if err != nil {
+ return nil, err
+ }
+ if !interactive {
+ return nil, fmt.Errorf("Non-interactive-mode is active. Please specify the access token via the --token option.")
+ }
+ cred, err = promptTokenOptions(repo, login, baseURL)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ token, ok := cred.(*auth.Token)
+ if !ok {
+ return nil, fmt.Errorf("the Gitea bridge only handle token credentials")
+ }
+
+ // verify access to the repository with token
+ _, err = validateProject(baseURL, owner, project, token)
+ if err != nil {
+ return nil, errors.Wrap(err, "project validation")
+ }
+
+ conf := make(core.Configuration)
+ conf[core.ConfigKeyTarget] = target
+ conf[confKeyBaseURL] = baseURL
+ conf[confKeyOwner] = owner
+ conf[confKeyProject] = project
+ conf[confKeyDefaultLogin] = login
+
+ err = g.ValidateConfig(conf)
+ if err != nil {
+ return nil, err
+ }
+
+ // don't forget to store the now known valid token
+ if !auth.IdExist(repo, cred.ID()) {
+ err = auth.Store(repo, cred)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ return conf, core.FinishConfig(repo, metaKeyGiteaLogin, login)
+}
+
+func (g *Gitea) ValidateConfig(conf core.Configuration) error {
+ if v, ok := conf[core.ConfigKeyTarget]; !ok {
+ return fmt.Errorf("missing %s key", core.ConfigKeyTarget)
+ } else if v != target {
+ return fmt.Errorf("unexpected target name: %v", v)
+ }
+ if _, ok := conf[confKeyBaseURL]; !ok {
+ return fmt.Errorf("missing %s key", confKeyBaseURL)
+ }
+ if _, ok := conf[confKeyOwner]; !ok {
+ return fmt.Errorf("missing %s key", confKeyOwner)
+ }
+ if _, ok := conf[confKeyProject]; !ok {
+ return fmt.Errorf("missing %s key", confKeyProject)
+ }
+ if _, ok := conf[confKeyDefaultLogin]; !ok {
+ return fmt.Errorf("missing %s key", confKeyDefaultLogin)
+ }
+
+ return nil
+}
+
+func promptTokenOptions(repo repository.RepoKeyring, login, baseURL string) (auth.Credential, error) {
+ creds, err := auth.List(repo,
+ auth.WithTarget(target),
+ auth.WithKind(auth.KindToken),
+ auth.WithMeta(auth.MetaKeyLogin, login),
+ auth.WithMeta(auth.MetaKeyBaseURL, baseURL),
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ cred, index, err := input.PromptCredential(target, "token", creds, []string{
+ "enter my token",
+ })
+ switch {
+ case err != nil:
+ return nil, err
+ case cred != nil:
+ return cred, nil
+ case index == 0:
+ return promptToken(baseURL)
+ default:
+ panic("missed case")
+ }
+}
+
+func promptToken(baseURL string) (*auth.Token, error) {
+ fmt.Printf("You can generate a new token by visiting %s.\n", path.Join(baseURL, "user/settings/applications"))
+ fmt.Println()
+
+ re := regexp.MustCompile(`^[a-z0-9]{40}$`)
+
+ var login string
+
+ validator := func(name string, value string) (complaint string, err error) {
+ if !re.MatchString(value) {
+ return "token has incorrect format", nil
+ }
+ login, err = getLoginFromToken(baseURL, auth.NewToken(target, value))
+ if err != nil {
+ return fmt.Sprintf("token is invalid: %v", err), nil
+ }
+ return "", nil
+ }
+
+ rawToken, err := input.Prompt("Enter token", "token", input.Required, validator)
+ if err != nil {
+ return nil, err
+ }
+
+ token := auth.NewToken(target, rawToken)
+ token.SetMetadata(auth.MetaKeyLogin, login)
+ token.SetMetadata(auth.MetaKeyBaseURL, baseURL)
+
+ return token, nil
+}
+
+func promptURL(repo repository.RepoCommon) (string, string, string, error) {
+ validRemotes, err := getRemoteURLs(repo)
+ if err != nil {
+ return "", "", "", err
+ }
+
+ validator := func(name, value string) (string, error) {
+ _, _, _, err := splitURL(value)
+ if err != nil {
+ return err.Error(), nil
+ }
+ return "", nil
+ }
+
+ url, err := input.PromptURLWithRemote("Gitea project URL", "URL", validRemotes, input.Required, input.IsURL, validator)
+ if err != nil {
+ return "", "", "", err
+ }
+
+ return splitURL(url)
+}
+
+func splitURL(url string) (baseURL, owner, project string, err error) {
+ cleanURL := strings.TrimSuffix(url, ".git")
+
+ re := regexp.MustCompile(`(.*)/([a-zA-Z0-9\-_.]+)/([a-zA-Z0-9\-_.]+)$`)
+
+ res := re.FindStringSubmatch(cleanURL)
+ if res == nil {
+ return "", "", "", ErrBadProjectURL
+ }
+
+ baseURL = res[1]
+ owner = res[2]
+ project = res[3]
+ return
+}
+
+func getRemoteURLs(repo repository.RepoCommon) ([]string, error) {
+ remotes, err := repo.GetRemotes()
+ if err != nil {
+ return nil, err
+ }
+
+ urls := make([]string, 0, len(remotes))
+ for _, url := range remotes {
+ urls = append(urls, url)
+ }
+
+ sort.Strings(urls)
+
+ return urls, nil
+}
+
+func validateProject(baseURL, owner, project string, token *auth.Token) (bool, error) {
+ client, err := buildClient(baseURL, token)
+ if err != nil {
+ return false, err
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
+ defer cancel()
+ client.SetContext(ctx)
+
+ _, _, err = client.GetRepo(owner, project)
+ if err != nil {
+ return false, errors.Wrap(err, "wrong token scope or non-existent project")
+ }
+
+ return true, nil
+}
+
+func getLoginFromToken(baseURL string, token *auth.Token) (string, error) {
+ client, err := buildClient(baseURL, token)
+ if err != nil {
+ return "", err
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
+ defer cancel()
+ client.SetContext(ctx)
+
+ user, _, err := client.GetMyUserInfo()
+ if err != nil {
+ return "", err
+ }
+ if user.UserName == "" {
+ return "", fmt.Errorf("gitea say username is empty")
+ }
+
+ return user.UserName, nil
+}
@@ -0,0 +1,29 @@
+package gitea
+
+import (
+ "context"
+ "syscall"
+ "time"
+
+ "github.com/pkg/errors"
+
+ "github.com/MichaelMure/git-bug/bridge/core"
+ "github.com/MichaelMure/git-bug/cache"
+)
+
+var (
+ ErrMissingIdentityToken = errors.New("missing identity token")
+)
+
+// giteaExporter implement the Exporter interface
+type giteaExporter struct {
+}
+
+// Init .
+func (ge *giteaExporter) Init(_ context.Context, repo *cache.RepoCache, conf core.Configuration) error {
+ return syscall.ENOSYS
+}
+
+func (ge *giteaExporter) ExportAll(ctx context.Context, repo *cache.RepoCache, since time.Time) (<-chan core.ExportResult, error) {
+ return nil, syscall.ENOSYS
+}
@@ -0,0 +1,57 @@
+package gitea
+
+import (
+ "time"
+
+ "code.gitea.io/sdk/gitea"
+
+ "github.com/MichaelMure/git-bug/bridge/core"
+ "github.com/MichaelMure/git-bug/bridge/core/auth"
+)
+
+const (
+ target = "gitea-preview"
+
+ metaKeyGiteaID = "gitea-id"
+ metaKeyGiteaLogin = "gitea-login"
+ metaKeyGiteaOwner = "gitea-owner"
+ metaKeyGiteaProject = "gitea-project"
+ metaKeyGiteaBaseURL = "gitea-base-url"
+
+ confKeyOwner = "owner"
+ confKeyProject = "project"
+ confKeyBaseURL = "base-url"
+ confKeyDefaultLogin = "default-login"
+
+ defaultTimeout = 60 * time.Second
+)
+
+var _ core.BridgeImpl = &Gitea{}
+
+type Gitea struct{}
+
+func (Gitea) Target() string {
+ return target
+}
+
+func (g *Gitea) LoginMetaKey() string {
+ return metaKeyGiteaLogin
+}
+
+func (Gitea) NewImporter() core.Importer {
+ return &giteaImporter{}
+}
+
+func (Gitea) NewExporter() core.Exporter {
+ return nil
+ // return &giteaExporter{}
+}
+
+func buildClient(baseURL string, token *auth.Token) (*gitea.Client, error) {
+ giteaClient, err := gitea.NewClient(baseURL, gitea.SetToken(token.Value))
+ if err != nil {
+ return nil, err
+ }
+
+ return giteaClient, nil
+}
@@ -0,0 +1,189 @@
+package gitea
+
+import (
+ "context"
+ "fmt"
+ "strconv"
+ "time"
+
+ "code.gitea.io/sdk/gitea"
+
+ "github.com/MichaelMure/git-bug/bridge/core"
+ "github.com/MichaelMure/git-bug/bridge/core/auth"
+ "github.com/MichaelMure/git-bug/bridge/gitea/iterator"
+ "github.com/MichaelMure/git-bug/cache"
+ "github.com/MichaelMure/git-bug/entities/bug"
+ "github.com/MichaelMure/git-bug/entity"
+ "github.com/MichaelMure/git-bug/util/text"
+)
+
+// giteaImporter implement the Importer interface
+type giteaImporter struct {
+ conf core.Configuration
+
+ // default client
+ client *gitea.Client
+
+ // iterator
+ iterator *iterator.Iterator
+
+ // send only channel
+ out chan<- core.ImportResult
+}
+
+func (gi *giteaImporter) Init(_ context.Context, repo *cache.RepoCache, conf core.Configuration) error {
+ gi.conf = conf
+
+ creds, err := auth.List(repo,
+ auth.WithTarget(target),
+ auth.WithKind(auth.KindToken),
+ auth.WithMeta(auth.MetaKeyBaseURL, conf[confKeyBaseURL]),
+ auth.WithMeta(auth.MetaKeyLogin, conf[confKeyDefaultLogin]),
+ )
+ if err != nil {
+ return err
+ }
+
+ if len(creds) == 0 {
+ return ErrMissingIdentityToken
+ }
+
+ gi.client, err = buildClient(conf[confKeyBaseURL], creds[0].(*auth.Token))
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// ImportAll iterate over all the configured repository issues (comments) and ensure the creation
+// of the missing issues / comments / label events / title changes ...
+func (gi *giteaImporter) ImportAll(ctx context.Context, repo *cache.RepoCache, since time.Time) (<-chan core.ImportResult, error) {
+ gi.iterator = iterator.NewIterator(ctx, gi.client, 10, gi.conf[confKeyOwner], gi.conf[confKeyProject], defaultTimeout, since)
+ out := make(chan core.ImportResult)
+ gi.out = out
+
+ go func() {
+ defer close(gi.out)
+
+ // Loop over all matching issues
+ for gi.iterator.NextIssue() {
+ issue := gi.iterator.IssueValue()
+
+ // create issue
+ b, err := gi.ensureIssue(repo, issue)
+ if err != nil {
+ err := fmt.Errorf("issue creation: %v", err)
+ out <- core.NewImportError(err, "")
+ return
+ }
+
+ // Loop over all comments
+
+ // Loop over all label events
+
+ if !b.NeedCommit() {
+ out <- core.NewImportNothing(b.Id(), "no imported operation")
+ } else if err := b.Commit(); err != nil {
+ // commit bug state
+ err := fmt.Errorf("bug commit: %v", err)
+ out <- core.NewImportError(err, "")
+ return
+ }
+ }
+
+ if err := gi.iterator.Error(); err != nil {
+ out <- core.NewImportError(err, "")
+ }
+ }()
+
+ return out, nil
+}
+
+func (gi *giteaImporter) ensureIssue(repo *cache.RepoCache, issue *gitea.Issue) (*cache.BugCache, error) {
+ // ensure issue author
+ author, err := gi.ensurePerson(repo, issue.Poster.UserName)
+ if err != nil {
+ return nil, err
+ }
+
+ giteaID := strconv.FormatInt(issue.Index, 10)
+
+ // resolve bug
+ b, err := repo.ResolveBugMatcher(func(excerpt *cache.BugExcerpt) bool {
+ return excerpt.CreateMetadata[core.MetaKeyOrigin] == target &&
+ excerpt.CreateMetadata[metaKeyGiteaID] == giteaID &&
+ excerpt.CreateMetadata[metaKeyGiteaBaseURL] == gi.conf[confKeyBaseURL] &&
+ excerpt.CreateMetadata[metaKeyGiteaOwner] == gi.conf[confKeyOwner] &&
+ excerpt.CreateMetadata[metaKeyGiteaProject] == gi.conf[confKeyProject]
+ })
+ if err == nil {
+ return b, nil
+ }
+ if err != bug.ErrBugNotExist {
+ return nil, err
+ }
+
+ // if bug was never imported, create bug
+ b, _, err = repo.NewBugRaw(
+ author,
+ issue.Created.Unix(),
+ text.CleanupOneLine(issue.Title),
+ text.Cleanup(issue.Body),
+ nil,
+ map[string]string{
+ core.MetaKeyOrigin: target,
+ metaKeyGiteaID: giteaID,
+ metaKeyGiteaOwner: gi.conf[confKeyOwner],
+ metaKeyGiteaProject: gi.conf[confKeyProject],
+ metaKeyGiteaBaseURL: gi.conf[confKeyBaseURL],
+ },
+ )
+
+ if err != nil {
+ return nil, err
+ }
+
+ // importing a new bug
+ gi.out <- core.NewImportBug(b.Id())
+
+ return b, nil
+}
+
+func (gi *giteaImporter) ensurePerson(repo *cache.RepoCache, loginName string) (*cache.IdentityCache, error) {
+ // Look first in the cache
+ i, err := repo.ResolveIdentityImmutableMetadata(metaKeyGiteaLogin, loginName)
+ if err == nil {
+ return i, nil
+ }
+ if entity.IsErrMultipleMatch(err) {
+ return nil, err
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
+ defer cancel()
+ gi.client.SetContext(ctx)
+
+ user, _, err := gi.client.GetUserInfo(loginName)
+ if err != nil {
+ return nil, err
+ }
+
+ i, err = repo.NewIdentityRaw(
+ user.FullName,
+ user.Email,
+ user.UserName,
+ user.AvatarURL,
+ nil,
+ map[string]string{
+ // because Gitea
+ metaKeyGiteaLogin: user.UserName,
+ },
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ gi.out <- core.NewImportIdentity(i.Id())
+ return i, nil
+}
@@ -0,0 +1,87 @@
+package iterator
+
+import (
+ "context"
+
+ "code.gitea.io/sdk/gitea"
+)
+
+type commentIterator struct {
+ issue int64
+ page int
+ lastPage bool
+ index int
+ cache []*gitea.Comment
+}
+
+func newCommentIterator() *commentIterator {
+ ci := &commentIterator{}
+ ci.Reset(-1)
+ return ci
+}
+
+func (ci *commentIterator) Next(ctx context.Context, conf config) (bool, error) {
+ // first query
+ if ci.cache == nil {
+ return ci.getNext(ctx, conf)
+ }
+
+ // move cursor index
+ if ci.index < len(ci.cache)-1 {
+ ci.index++
+ return true, nil
+ }
+
+ return ci.getNext(ctx, conf)
+}
+
+func (ci *commentIterator) Value() *gitea.Comment {
+ return ci.cache[ci.index]
+}
+
+func (ci *commentIterator) getNext(ctx context.Context, conf config) (bool, error) {
+ if ci.lastPage {
+ return false, nil
+ }
+
+ ctx, cancel := context.WithTimeout(ctx, conf.timeout)
+ defer cancel()
+ conf.gc.SetContext(ctx)
+
+ comments, _, err := conf.gc.ListIssueComments(
+ conf.owner,
+ conf.project,
+ ci.issue,
+ gitea.ListIssueCommentOptions{
+ ListOptions: gitea.ListOptions{
+ Page: ci.page,
+ PageSize: conf.capacity,
+ },
+ },
+ )
+
+ if err != nil {
+ ci.Reset(-1)
+ return false, err
+ }
+
+ ci.lastPage = true
+
+ if len(comments) == 0 {
+ return false, nil
+ }
+
+ ci.cache = comments
+ ci.index = 0
+ ci.page++
+
+ return true, nil
+}
+
+func (ci *commentIterator) Reset(issue int64) {
+ ci.issue = issue
+ ci.index = -1
+ ci.page = 1
+ ci.lastPage = false
+ ci.cache = nil
+}
@@ -0,0 +1,96 @@
+package iterator
+
+import (
+ "context"
+ "strconv"
+
+ "code.gitea.io/sdk/gitea"
+)
+
+type issueIterator struct {
+ page int
+ lastPage bool
+ index int
+ cache []*gitea.Issue
+}
+
+func newIssueIterator() *issueIterator {
+ ii := &issueIterator{}
+ ii.Reset()
+ return ii
+}
+
+func (ii *issueIterator) Next(ctx context.Context, conf config) (bool, error) {
+ // first query
+ if ii.cache == nil {
+ return ii.getNext(ctx, conf)
+ }
+
+ // move cursor index
+ if ii.index < len(ii.cache)-1 {
+ ii.index++
+ return true, nil
+ }
+
+ return ii.getNext(ctx, conf)
+}
+
+func (ii *issueIterator) Value() *gitea.Issue {
+ return ii.cache[ii.index]
+}
+
+func (ii *issueIterator) getNext(ctx context.Context, conf config) (bool, error) {
+ if ii.lastPage {
+ return false, nil
+ }
+
+ ctx, cancel := context.WithTimeout(ctx, conf.timeout)
+ defer cancel()
+ conf.gc.SetContext(ctx)
+
+ issues, resp, err := conf.gc.ListRepoIssues(
+ conf.owner,
+ conf.project,
+ gitea.ListIssueOption{
+ ListOptions: gitea.ListOptions{
+ Page: ii.page,
+ PageSize: conf.capacity,
+ },
+ State: gitea.StateAll,
+ Type: gitea.IssueTypeIssue,
+ },
+ )
+
+ if err != nil {
+ ii.Reset()
+ return false, err
+ }
+
+ total, err := strconv.Atoi(resp.Header.Get("X-Total-Count"))
+ if err != nil {
+ ii.Reset()
+ return false, err
+ }
+
+ if total <= ii.page*conf.capacity {
+ ii.lastPage = true
+ }
+
+ // if repository doesn't have any issues
+ if len(issues) == 0 {
+ return false, nil
+ }
+
+ ii.cache = issues
+ ii.index = 0
+ ii.page++
+
+ return true, nil
+}
+
+func (ii *issueIterator) Reset() {
+ ii.index = -1
+ ii.page = 1
+ ii.lastPage = false
+ ii.cache = nil
+}
@@ -0,0 +1,142 @@
+package iterator
+
+import (
+ "context"
+ "time"
+
+ "code.gitea.io/sdk/gitea"
+)
+
+type Iterator struct {
+ // shared context
+ ctx context.Context
+
+ // to pass to sub-iterators
+ conf config
+
+ // sticky error
+ err error
+
+ // issues iterator
+ issue *issueIterator
+
+ // comments iterator
+ comment *commentIterator
+
+ // labels iterator
+ label *labelIterator
+}
+
+type config struct {
+ // gitea api v1 client
+ gc *gitea.Client
+
+ timeout time.Duration
+
+ // if since is given the iterator will query only the issues
+ // updated after this date
+ since time.Time
+
+ // name of the repository owner on Gitea
+ owner string
+
+ // name of the Gitea repository
+ project string
+
+ // number of issues and notes to query at once
+ capacity int
+}
+
+// NewIterator create a new iterator
+func NewIterator(ctx context.Context, client *gitea.Client, capacity int, owner, project string, timeout time.Duration, since time.Time) *Iterator {
+ return &Iterator{
+ ctx: ctx,
+ conf: config{
+ gc: client,
+ timeout: timeout,
+ since: since,
+ owner: owner,
+ project: project,
+ capacity: capacity,
+ },
+ issue: newIssueIterator(),
+ comment: newCommentIterator(),
+ label: newLabelIterator(),
+ }
+}
+
+// Error return last encountered error
+func (i *Iterator) Error() error {
+ return i.err
+}
+
+func (i *Iterator) NextIssue() bool {
+ if i.err != nil {
+ return false
+ }
+
+ if i.ctx.Err() != nil {
+ return false
+ }
+
+ more, err := i.issue.Next(i.ctx, i.conf)
+ if err != nil {
+ i.err = err
+ return false
+ }
+
+ // Also reset the other sub iterators as they would
+ // no longer be valid
+ i.comment.Reset(i.issue.Value().Index)
+ i.label.Reset(i.issue.Value().Index)
+
+ return more
+}
+
+func (i *Iterator) IssueValue() *gitea.Issue {
+ return i.issue.Value()
+}
+
+func (i *Iterator) NextComment() bool {
+ if i.err != nil {
+ return false
+ }
+
+ if i.ctx.Err() != nil {
+ return false
+ }
+
+ more, err := i.comment.Next(i.ctx, i.conf)
+ if err != nil {
+ i.err = err
+ return false
+ }
+
+ return more
+}
+
+func (i *Iterator) CommentValue() *gitea.Comment {
+ return i.comment.Value()
+}
+
+func (i *Iterator) NextLabel() bool {
+ if i.err != nil {
+ return false
+ }
+
+ if i.ctx.Err() != nil {
+ return false
+ }
+
+ more, err := i.label.Next(i.ctx, i.conf)
+ if err != nil {
+ i.err = err
+ return false
+ }
+
+ return more
+}
+
+func (i *Iterator) LabelValue() *gitea.Label {
+ return i.label.Value()
+}
@@ -0,0 +1,86 @@
+package iterator
+
+import (
+ "context"
+
+ "code.gitea.io/sdk/gitea"
+)
+
+type labelIterator struct {
+ issue int64
+ page int
+ lastPage bool
+ index int
+ cache []*gitea.Label
+}
+
+func newLabelIterator() *labelIterator {
+ li := &labelIterator{}
+ li.Reset(-1)
+ return li
+}
+
+func (li *labelIterator) Next(ctx context.Context, conf config) (bool, error) {
+ // first query
+ if li.cache == nil {
+ return li.getNext(ctx, conf)
+ }
+
+ // move cursor index
+ if li.index < len(li.cache)-1 {
+ li.index++
+ return true, nil
+ }
+
+ return li.getNext(ctx, conf)
+}
+
+func (li *labelIterator) Value() *gitea.Label {
+ return li.cache[li.index]
+}
+
+func (li *labelIterator) getNext(ctx context.Context, conf config) (bool, error) {
+ if li.lastPage {
+ return false, nil
+ }
+
+ ctx, cancel := context.WithTimeout(ctx, conf.timeout)
+ defer cancel()
+ conf.gc.SetContext(ctx)
+
+ labels, _, err := conf.gc.GetIssueLabels(
+ conf.owner,
+ conf.project,
+ li.issue,
+ gitea.ListLabelsOptions{
+ ListOptions: gitea.ListOptions{
+ Page: li.page,
+ PageSize: conf.capacity,
+ },
+ },
+ )
+ if err != nil {
+ li.Reset(-1)
+ return false, err
+ }
+
+ li.lastPage = true
+
+ if len(labels) == 0 {
+ return false, nil
+ }
+
+ li.cache = labels
+ li.index = 0
+ li.page++
+
+ return true, nil
+}
+
+func (li *labelIterator) Reset(issue int64) {
+ li.issue = issue
+ li.index = -1
+ li.page = 1
+ li.lastPage = false
+ li.cache = nil
+}
@@ -19,7 +19,7 @@ Store a new token
.SH OPTIONS
.PP
\fB-t\fP, \fB--target\fP=""
- The target of the bridge. Valid values are [github,gitlab,jira,launchpad-preview]
+ The target of the bridge. Valid values are [gitea-preview,github,gitlab,jira,launchpad-preview]
.PP
\fB-l\fP, \fB--login\fP=""
@@ -29,7 +29,7 @@ Configure a new bridge by passing flags or/and using interactive terminal prompt
.PP
\fB-t\fP, \fB--target\fP=""
- The target of the bridge. Valid values are [github,gitlab,jira,launchpad-preview]
+ The target of the bridge. Valid values are [gitea-preview,github,gitlab,jira,launchpad-preview]
.PP
\fB-u\fP, \fB--url\fP=""
@@ -9,7 +9,7 @@ git-bug bridge auth add-token [TOKEN] [flags]
### Options
```
- -t, --target string The target of the bridge. Valid values are [github,gitlab,jira,launchpad-preview]
+ -t, --target string The target of the bridge. Valid values are [gitea-preview,github,gitlab,jira,launchpad-preview]
-l, --login string The login in the remote bug-tracker
-u, --user string The user to add the token to. Default is the current user
-h, --help help for add-token
@@ -72,7 +72,7 @@ git bug bridge configure \
```
-n, --name string A distinctive name to identify the bridge
- -t, --target string The target of the bridge. Valid values are [github,gitlab,jira,launchpad-preview]
+ -t, --target string The target of the bridge. Valid values are [gitea-preview,github,gitlab,jira,launchpad-preview]
-u, --url string The URL of the remote repository
-b, --base-url string The base URL of your remote issue tracker
-l, --login string The login on your remote issue tracker
@@ -3,6 +3,7 @@ module github.com/MichaelMure/git-bug
go 1.18
require (
+ code.gitea.io/sdk/gitea v0.14.0
github.com/99designs/gqlgen v0.17.17
github.com/99designs/keyring v1.2.1
github.com/MichaelMure/go-term-text v0.3.1
@@ -35,6 +36,7 @@ require (
)
require (
+ github.com/hashicorp/go-version v1.2.1 // indirect
github.com/lithammer/dedent v1.1.0 // indirect
github.com/owenrumney/go-sarif v1.0.11 // indirect
github.com/segmentio/fasthash v1.0.3 // indirect
@@ -36,6 +36,8 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
+code.gitea.io/sdk/gitea v0.14.0 h1:m4J352I3p9+bmJUfS+g0odeQzBY/5OXP91Gv6D4fnJ0=
+code.gitea.io/sdk/gitea v0.14.0/go.mod h1:89WiyOX1KEcvjP66sRHdu0RafojGo60bT9UqW17VbWs=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 h1:/vQbFIOMbk2FiG/kXiLl8BRyzTWDw7gX/Hz7Dd5eDMs=
github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4/go.mod h1:hN7oaIRCjzsZ2dE+yG5k+rsdt3qcwykqK6HVGcKwsw4=
@@ -295,6 +297,8 @@ github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerX
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
+github.com/hashicorp/go-version v1.2.1 h1:zEfKbn2+PDgroKdiOzqiE8rsmLqU2uwi5PB5pBJ3TkI=
+github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=