From dacf6772ff9999927faad759a76c2421485146b2 Mon Sep 17 00:00:00 2001 From: Crayon2000 Date: Mon, 8 Sep 2025 09:31:25 -0400 Subject: [PATCH 01/25] Add applications --- cli/application/bitbucket.go | 23 +++++++++++++++++++++++ cli/application/gitbucket.go | 23 +++++++++++++++++++++++ cli/application/github.go | 23 +++++++++++++++++++++++ cli/application/gitlab.go | 23 +++++++++++++++++++++++ cli/application/gogs.go | 23 +++++++++++++++++++++++ cli/application/interface.go | 18 ++++++++++++++++++ cli/main.go | 10 ++++++++++ 7 files changed, 143 insertions(+) create mode 100644 cli/application/bitbucket.go create mode 100644 cli/application/gitbucket.go create mode 100644 cli/application/github.go create mode 100644 cli/application/gitlab.go create mode 100644 cli/application/gogs.go create mode 100644 cli/application/interface.go diff --git a/cli/application/bitbucket.go b/cli/application/bitbucket.go new file mode 100644 index 0000000..e07555b --- /dev/null +++ b/cli/application/bitbucket.go @@ -0,0 +1,23 @@ +package application + +type BitbucketApp struct{} + +func (g *BitbucketApp) PrepareRequest() error { + // TODO: implement + return nil +} + +func (g *BitbucketApp) GetOrganizations() ([]string, error) { + // TODO: implement + return nil, nil +} + +func (g *BitbucketApp) GetAuthenticatedUser() (string, error) { + // TODO: implement + return "", nil +} + +func (g *BitbucketApp) CreateRepo(name string) error { + // TODO: implement + return nil +} diff --git a/cli/application/gitbucket.go b/cli/application/gitbucket.go new file mode 100644 index 0000000..7d74e2f --- /dev/null +++ b/cli/application/gitbucket.go @@ -0,0 +1,23 @@ +package application + +type GitBucketApp struct{} + +func (g *GitBucketApp) PrepareRequest() error { + // TODO: implement + return nil +} + +func (g *GitBucketApp) GetOrganizations() ([]string, error) { + // TODO: implement + return nil, nil +} + +func (g *GitBucketApp) GetAuthenticatedUser() (string, error) { + // TODO: implement + return "", nil +} + +func (g *GitBucketApp) CreateRepo(name string) error { + // TODO: implement + return nil +} diff --git a/cli/application/github.go b/cli/application/github.go new file mode 100644 index 0000000..82057a9 --- /dev/null +++ b/cli/application/github.go @@ -0,0 +1,23 @@ +package application + +type GitHubApp struct{} + +func (g *GitHubApp) PrepareRequest() error { + // TODO: implement + return nil +} + +func (g *GitHubApp) GetOrganizations() ([]string, error) { + // TODO: implement + return nil, nil +} + +func (g *GitHubApp) GetAuthenticatedUser() (string, error) { + // TODO: implement + return "", nil +} + +func (g *GitHubApp) CreateRepo(name string) error { + // TODO: implement + return nil +} diff --git a/cli/application/gitlab.go b/cli/application/gitlab.go new file mode 100644 index 0000000..2844d7a --- /dev/null +++ b/cli/application/gitlab.go @@ -0,0 +1,23 @@ +package application + +type GitLabApp struct{} + +func (g *GitLabApp) PrepareRequest() error { + // TODO: implement + return nil +} + +func (g *GitLabApp) GetOrganizations() ([]string, error) { + // TODO: implement + return nil, nil +} + +func (g *GitLabApp) GetAuthenticatedUser() (string, error) { + // TODO: implement + return "", nil +} + +func (g *GitLabApp) CreateRepo(name string) error { + // TODO: implement + return nil +} diff --git a/cli/application/gogs.go b/cli/application/gogs.go new file mode 100644 index 0000000..6e0d1c6 --- /dev/null +++ b/cli/application/gogs.go @@ -0,0 +1,23 @@ +package application + +type GogsApp struct{} + +func (g *GogsApp) PrepareRequest() error { + // TODO: implement + return nil +} + +func (g *GogsApp) GetOrganizations() ([]string, error) { + // TODO: implement + return nil, nil +} + +func (g *GogsApp) GetAuthenticatedUser() (string, error) { + // TODO: implement + return "", nil +} + +func (g *GogsApp) CreateRepo(name string) error { + // TODO: implement + return nil +} diff --git a/cli/application/interface.go b/cli/application/interface.go new file mode 100644 index 0000000..10811c7 --- /dev/null +++ b/cli/application/interface.go @@ -0,0 +1,18 @@ +package application + +type Application interface { + PrepareRequest() error + GetOrganizations() ([]string, error) + GetAuthenticatedUser() (string, error) + CreateRepo(name string) error +} + +type ApplicationType string + +const ( + AppGogs ApplicationType = "Gogs" + AppGitBucket ApplicationType = "GitBucket" + AppGitHub ApplicationType = "GitHub" + AppGitLab ApplicationType = "GitLab" + AppBitbucket ApplicationType = "Bitbucket" +) diff --git a/cli/main.go b/cli/main.go index 4d99a48..36dc8e4 100644 --- a/cli/main.go +++ b/cli/main.go @@ -3,10 +3,20 @@ package main import ( "flag" "fmt" + "gitconduit-cli/application" "os" ) func main() { + // Create a GitHub application and call PrepareRequest + githubApp := &application.GitHubApp{} + if err := githubApp.PrepareRequest(); err != nil { + fmt.Fprintf(os.Stderr, "PrepareRequest failed: %v\n", err) + os.Exit(1) + } + + os.Exit(1) + // Define the main arguments cloneAndPushCmd := flag.NewFlagSet("cloneandpush", flag.ExitOnError) From 6462662786e8aca062fcc4eeb44120a9fd84d44c Mon Sep 17 00:00:00 2001 From: Crayon2000 Date: Sun, 14 Sep 2025 17:34:55 -0400 Subject: [PATCH 02/25] Add GetAuthenticatedUser and GetOrganizations --- cli/application/bitbucket.go | 57 ++++++++++++++-- cli/application/gitbucket.go | 78 ++++++++++++++++++++-- cli/application/github.go | 122 ++++++++++++++++++++++++++++++++--- cli/application/gitlab.go | 57 ++++++++++++++-- cli/application/gogs.go | 78 ++++++++++++++++++++-- cli/application/interface.go | 37 ++++++++++- cli/main.go | 19 +++--- 7 files changed, 402 insertions(+), 46 deletions(-) diff --git a/cli/application/bitbucket.go b/cli/application/bitbucket.go index e07555b..263a1c5 100644 --- a/cli/application/bitbucket.go +++ b/cli/application/bitbucket.go @@ -1,13 +1,14 @@ package application -type BitbucketApp struct{} - -func (g *BitbucketApp) PrepareRequest() error { - // TODO: implement - return nil +type BitbucketApp struct { + ApiUrl string + Token string + User string + Username string + Password string } -func (g *BitbucketApp) GetOrganizations() ([]string, error) { +func (g *BitbucketApp) GetOrganizations() ([]Organization, error) { // TODO: implement return nil, nil } @@ -21,3 +22,47 @@ func (g *BitbucketApp) CreateRepo(name string) error { // TODO: implement return nil } + +func (g *BitbucketApp) GetApplicationName() string { + return "BitBucket" +} + +func (g *BitbucketApp) GetApiUrl() string { + return g.ApiUrl +} + +func (g *BitbucketApp) SetApiUrl(url string) { + g.ApiUrl = url +} + +func (g *BitbucketApp) GetToken() string { + return g.Token +} + +func (g *BitbucketApp) SetToken(token string) { + g.Token = token +} + +func (g *BitbucketApp) GetUser() string { + return g.User +} + +func (g *BitbucketApp) SetUser(user string) { + g.User = user +} + +func (g *BitbucketApp) GetUsername() string { + return g.Username +} + +func (g *BitbucketApp) SetUsername(username string) { + g.Username = username +} + +func (g *BitbucketApp) GetPassword() string { + return g.Password +} + +func (g *BitbucketApp) SetPassword(password string) { + g.Password = password +} diff --git a/cli/application/gitbucket.go b/cli/application/gitbucket.go index 7d74e2f..2ae14d2 100644 --- a/cli/application/gitbucket.go +++ b/cli/application/gitbucket.go @@ -1,23 +1,87 @@ package application -type GitBucketApp struct{} +import ( + "encoding/json" + "io" + "net/http" +) -func (g *GitBucketApp) PrepareRequest() error { - // TODO: implement - return nil +type GitBucketApp struct { + ApiUrl string + Token string + User string + Username string + Password string } -func (g *GitBucketApp) GetOrganizations() ([]string, error) { +func (g *GitBucketApp) GetOrganizations() ([]Organization, error) { // TODO: implement return nil, nil } func (g *GitBucketApp) GetAuthenticatedUser() (string, error) { - // TODO: implement - return "", nil + req, _ := http.NewRequest("GET", g.ApiUrl+"/user", nil) + req.Header.Set("Authorization", "Bearer "+g.Token) + resp, err := http.DefaultClient.Do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + body, _ := io.ReadAll(resp.Body) + var data struct { + Login string `json:"login"` + } + if err := json.Unmarshal(body, &data); err != nil { + return "", err + } + return data.Login, nil } func (g *GitBucketApp) CreateRepo(name string) error { // TODO: implement return nil } + +func (g *GitBucketApp) GetApplicationName() string { + return "GitBucket" +} + +func (g *GitBucketApp) GetApiUrl() string { + return g.ApiUrl +} + +func (g *GitBucketApp) SetApiUrl(url string) { + g.ApiUrl = url +} + +func (g *GitBucketApp) GetToken() string { + return g.Token +} + +func (g *GitBucketApp) SetToken(token string) { + g.Token = token +} + +func (g *GitBucketApp) GetUser() string { + return g.User +} + +func (g *GitBucketApp) SetUser(user string) { + g.User = user +} + +func (g *GitBucketApp) GetUsername() string { + return g.Username +} + +func (g *GitBucketApp) SetUsername(username string) { + g.Username = username +} + +func (g *GitBucketApp) GetPassword() string { + return g.Password +} + +func (g *GitBucketApp) SetPassword(password string) { + g.Password = password +} diff --git a/cli/application/github.go b/cli/application/github.go index 82057a9..02f0b04 100644 --- a/cli/application/github.go +++ b/cli/application/github.go @@ -1,23 +1,127 @@ package application -type GitHubApp struct{} +import ( + "encoding/json" + "fmt" + "io" + "net/http" +) -func (g *GitHubApp) PrepareRequest() error { - // TODO: implement - return nil +type GitHubApp struct { + ApiUrl string + Token string + User string + Username string + Password string } -func (g *GitHubApp) GetOrganizations() ([]string, error) { - // TODO: implement - return nil, nil +func (g *GitHubApp) GetOrganizations() ([]Organization, error) { + req, _ := http.NewRequest("GET", g.ApiUrl+"/user/orgs", nil) + req.Header.Set("Authorization", "Bearer "+g.Token) + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + // GitBucket may return 404 if not supported + if resp.StatusCode == http.StatusNotFound { + return nil, nil // treat as "no orgs" + } + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected status: %d", resp.StatusCode) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + var rawOrgs []map[string]interface{} + if err := json.Unmarshal(body, &rawOrgs); err != nil { + return nil, err + } + + var orgs []Organization + for _, obj := range rawOrgs { + org := Organization{} + // Try "login" or "username" for compatibility + if login, ok := obj["login"].(string); ok { + org.Name = login + } else if username, ok := obj["username"].(string); ok { + org.Name = username + } + if desc, ok := obj["description"].(string); ok { + org.Description = desc + } + orgs = append(orgs, org) + } + return orgs, nil } func (g *GitHubApp) GetAuthenticatedUser() (string, error) { - // TODO: implement - return "", nil + req, _ := http.NewRequest("GET", g.ApiUrl+"/user", nil) + req.Header.Set("Authorization", "Bearer "+g.Token) + resp, err := http.DefaultClient.Do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + body, _ := io.ReadAll(resp.Body) + var data struct { + Login string `json:"login"` + } + if err := json.Unmarshal(body, &data); err != nil { + return "", err + } + return data.Login, nil } func (g *GitHubApp) CreateRepo(name string) error { // TODO: implement return nil } + +func (g *GitHubApp) GetApplicationName() string { + return "GitHub" +} + +func (g *GitHubApp) GetApiUrl() string { + return g.ApiUrl +} + +func (g *GitHubApp) SetApiUrl(url string) { + g.ApiUrl = url +} + +func (g *GitHubApp) GetToken() string { + return g.Token +} + +func (g *GitHubApp) SetToken(token string) { + g.Token = token +} + +func (g *GitHubApp) GetUser() string { + return g.User +} + +func (g *GitHubApp) SetUser(user string) { + g.User = user +} + +func (g *GitHubApp) GetUsername() string { + return g.Username +} + +func (g *GitHubApp) SetUsername(username string) { + g.Username = username +} + +func (g *GitHubApp) GetPassword() string { + return g.Password +} + +func (g *GitHubApp) SetPassword(password string) { + g.Password = password +} diff --git a/cli/application/gitlab.go b/cli/application/gitlab.go index 2844d7a..5c1bf8a 100644 --- a/cli/application/gitlab.go +++ b/cli/application/gitlab.go @@ -1,13 +1,14 @@ package application -type GitLabApp struct{} - -func (g *GitLabApp) PrepareRequest() error { - // TODO: implement - return nil +type GitLabApp struct { + ApiUrl string + Token string + User string + Username string + Password string } -func (g *GitLabApp) GetOrganizations() ([]string, error) { +func (g *GitLabApp) GetOrganizations() ([]Organization, error) { // TODO: implement return nil, nil } @@ -21,3 +22,47 @@ func (g *GitLabApp) CreateRepo(name string) error { // TODO: implement return nil } + +func (g *GitLabApp) GetApplicationName() string { + return "GitLab" +} + +func (g *GitLabApp) GetApiUrl() string { + return g.ApiUrl +} + +func (g *GitLabApp) SetApiUrl(url string) { + g.ApiUrl = url +} + +func (g *GitLabApp) GetToken() string { + return g.Token +} + +func (g *GitLabApp) SetToken(token string) { + g.Token = token +} + +func (g *GitLabApp) GetUser() string { + return g.User +} + +func (g *GitLabApp) SetUser(user string) { + g.User = user +} + +func (g *GitLabApp) GetUsername() string { + return g.Username +} + +func (g *GitLabApp) SetUsername(username string) { + g.Username = username +} + +func (g *GitLabApp) GetPassword() string { + return g.Password +} + +func (g *GitLabApp) SetPassword(password string) { + g.Password = password +} diff --git a/cli/application/gogs.go b/cli/application/gogs.go index 6e0d1c6..a49df6e 100644 --- a/cli/application/gogs.go +++ b/cli/application/gogs.go @@ -1,23 +1,87 @@ package application -type GogsApp struct{} +import ( + "encoding/json" + "io" + "net/http" +) -func (g *GogsApp) PrepareRequest() error { - // TODO: implement - return nil +type GogsApp struct { + ApiUrl string + Token string + User string + Username string + Password string } -func (g *GogsApp) GetOrganizations() ([]string, error) { +func (g *GogsApp) GetOrganizations() ([]Organization, error) { // TODO: implement return nil, nil } func (g *GogsApp) GetAuthenticatedUser() (string, error) { - // TODO: implement - return "", nil + req, _ := http.NewRequest("GET", g.ApiUrl+"/user", nil) + req.Header.Set("Authorization", "Bearer "+g.Token) + resp, err := http.DefaultClient.Do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + body, _ := io.ReadAll(resp.Body) + var data struct { + Login string `json:"login"` + } + if err := json.Unmarshal(body, &data); err != nil { + return "", err + } + return data.Login, nil } func (g *GogsApp) CreateRepo(name string) error { // TODO: implement return nil } + +func (g *GogsApp) GetApplicationName() string { + return "Gogs" +} + +func (g *GogsApp) GetApiUrl() string { + return g.ApiUrl +} + +func (g *GogsApp) SetApiUrl(url string) { + g.ApiUrl = url +} + +func (g *GogsApp) GetToken() string { + return g.Token +} + +func (g *GogsApp) SetToken(token string) { + g.Token = token +} + +func (g *GogsApp) GetUser() string { + return g.User +} + +func (g *GogsApp) SetUser(user string) { + g.User = user +} + +func (g *GogsApp) GetUsername() string { + return g.Username +} + +func (g *GogsApp) SetUsername(username string) { + g.Username = username +} + +func (g *GogsApp) GetPassword() string { + return g.Password +} + +func (g *GogsApp) SetPassword(password string) { + g.Password = password +} diff --git a/cli/application/interface.go b/cli/application/interface.go index 10811c7..02c06ee 100644 --- a/cli/application/interface.go +++ b/cli/application/interface.go @@ -1,10 +1,26 @@ package application +type Organization struct { + Name string + Description string +} + type Application interface { - PrepareRequest() error - GetOrganizations() ([]string, error) + GetOrganizations() ([]Organization, error) GetAuthenticatedUser() (string, error) CreateRepo(name string) error + + GetApplicationName() string + GetApiUrl() string + SetApiUrl(url string) + GetToken() string + SetToken(token string) + GetUser() string + SetUser(user string) + GetUsername() string + SetUsername(username string) + GetPassword() string + SetPassword(password string) } type ApplicationType string @@ -16,3 +32,20 @@ const ( AppGitLab ApplicationType = "GitLab" AppBitbucket ApplicationType = "Bitbucket" ) + +func NewApplication(appType ApplicationType) Application { + switch appType { + case AppGogs: + return &GogsApp{} + case AppGitBucket: + return &GitBucketApp{} + case AppGitHub: + return &GitHubApp{ApiUrl: "https://api.github.com"} + case AppGitLab: + return &GitLabApp{ApiUrl: "https://gitlab.com/api/v4"} + case AppBitbucket: + return &BitbucketApp{ApiUrl: "https://api.bitbucket.org/2.0"} + default: + return nil + } +} diff --git a/cli/main.go b/cli/main.go index 36dc8e4..95b27b3 100644 --- a/cli/main.go +++ b/cli/main.go @@ -8,15 +8,6 @@ import ( ) func main() { - // Create a GitHub application and call PrepareRequest - githubApp := &application.GitHubApp{} - if err := githubApp.PrepareRequest(); err != nil { - fmt.Fprintf(os.Stderr, "PrepareRequest failed: %v\n", err) - os.Exit(1) - } - - os.Exit(1) - // Define the main arguments cloneAndPushCmd := flag.NewFlagSet("cloneandpush", flag.ExitOnError) @@ -29,6 +20,16 @@ func main() { destpassword := cloneAndPushCmd.String("destpassword", "", "Destination password for authentication") hasWiki := cloneAndPushCmd.Bool("haswiki", false, "Set to true if the repository has a wiki (default: false)") + sourceApp := application.NewApplication(application.AppGitHub) + sourceApp.SetToken("") + _, err := sourceApp.GetOrganizations() + if err != nil { + fmt.Fprintf(os.Stderr, "GetOrganizations failed: %v\n", err) + os.Exit(1) + } + + os.Exit(1) + if len(os.Args) < 2 { fmt.Println("Expected 'cloneandpush' subcommands") os.Exit(1) From 59b4d36e7172e0f51692963c94e9c8dbc19ab83f Mon Sep 17 00:00:00 2001 From: Crayon2000 Date: Sun, 14 Sep 2025 23:07:58 -0400 Subject: [PATCH 03/25] Add common code --- cli/application/bitbucket.go | 39 ++++++++----- cli/application/common.go | 70 +++++++++++++++++++++++ cli/application/gitbucket.go | 64 +++++++++------------ cli/application/github.go | 104 +++++++++-------------------------- cli/application/gitlab.go | 39 ++++++++----- cli/application/gogs.go | 64 +++++++++------------ cli/application/interface.go | 56 ++++++++++++++++++- cli/main.go | 2 - 8 files changed, 249 insertions(+), 189 deletions(-) create mode 100644 cli/application/common.go diff --git a/cli/application/bitbucket.go b/cli/application/bitbucket.go index 263a1c5..bdb619d 100644 --- a/cli/application/bitbucket.go +++ b/cli/application/bitbucket.go @@ -1,11 +1,7 @@ package application type BitbucketApp struct { - ApiUrl string - Token string - User string - Username string - Password string + config AppConfig } func (g *BitbucketApp) GetOrganizations() ([]Organization, error) { @@ -18,6 +14,11 @@ func (g *BitbucketApp) GetAuthenticatedUser() (string, error) { return "", nil } +func (g *BitbucketApp) GetRepositories() ([]Repository, error) { + // TODO: implement + return nil, nil +} + func (g *BitbucketApp) CreateRepo(name string) error { // TODO: implement return nil @@ -28,41 +29,49 @@ func (g *BitbucketApp) GetApplicationName() string { } func (g *BitbucketApp) GetApiUrl() string { - return g.ApiUrl + return g.config.ApiUrl } func (g *BitbucketApp) SetApiUrl(url string) { - g.ApiUrl = url + g.config.ApiUrl = url } func (g *BitbucketApp) GetToken() string { - return g.Token + return g.config.Token } func (g *BitbucketApp) SetToken(token string) { - g.Token = token + g.config.Token = token } func (g *BitbucketApp) GetUser() string { - return g.User + return g.config.User } func (g *BitbucketApp) SetUser(user string) { - g.User = user + g.config.User = user } func (g *BitbucketApp) GetUsername() string { - return g.Username + return g.config.Username } func (g *BitbucketApp) SetUsername(username string) { - g.Username = username + g.config.Username = username } func (g *BitbucketApp) GetPassword() string { - return g.Password + return g.config.Password } func (g *BitbucketApp) SetPassword(password string) { - g.Password = password + g.config.Password = password +} + +func (g *BitbucketApp) GetEndpoint() ApiEndpoint { + return g.config.Endpoint +} + +func (g *BitbucketApp) SetEndpoint(endpoint ApiEndpoint) { + g.config.Endpoint = endpoint } diff --git a/cli/application/common.go b/cli/application/common.go new file mode 100644 index 0000000..c091048 --- /dev/null +++ b/cli/application/common.go @@ -0,0 +1,70 @@ +package application + +import ( + "encoding/json" + "fmt" + "io" + "net/http" +) + +func GetOrganizations(config AppConfig) ([]Organization, error) { + req, _ := http.NewRequest("GET", config.ApiUrl+"/user/orgs", nil) + req.Header.Set("Authorization", "Bearer "+config.Token) + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + // GitBucket may return 404 if not supported + if resp.StatusCode == http.StatusNotFound { + return nil, nil // treat as "no orgs" + } + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected status: %d", resp.StatusCode) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + var rawOrgs []map[string]any + if err := json.Unmarshal(body, &rawOrgs); err != nil { + return nil, err + } + + var orgs []Organization + for _, obj := range rawOrgs { + org := Organization{} + // Try "login" or "username" for compatibility + if login, ok := obj["login"].(string); ok { + org.Name = login + } else if username, ok := obj["username"].(string); ok { + org.Name = username + } + if desc, ok := obj["description"].(string); ok { + org.Description = desc + } + orgs = append(orgs, org) + } + return orgs, nil +} + +func GetAuthenticatedUser(config AppConfig) (string, error) { + req, _ := http.NewRequest("GET", config.ApiUrl+"/user", nil) + req.Header.Set("Authorization", "Bearer "+config.Token) + resp, err := http.DefaultClient.Do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + body, _ := io.ReadAll(resp.Body) + var data struct { + Login string `json:"login"` + } + if err := json.Unmarshal(body, &data); err != nil { + return "", err + } + return data.Login, nil +} diff --git a/cli/application/gitbucket.go b/cli/application/gitbucket.go index 2ae14d2..cf89796 100644 --- a/cli/application/gitbucket.go +++ b/cli/application/gitbucket.go @@ -1,40 +1,20 @@ package application -import ( - "encoding/json" - "io" - "net/http" -) - type GitBucketApp struct { - ApiUrl string - Token string - User string - Username string - Password string + config AppConfig } func (g *GitBucketApp) GetOrganizations() ([]Organization, error) { - // TODO: implement - return nil, nil + return GetOrganizations(g.config) } func (g *GitBucketApp) GetAuthenticatedUser() (string, error) { - req, _ := http.NewRequest("GET", g.ApiUrl+"/user", nil) - req.Header.Set("Authorization", "Bearer "+g.Token) - resp, err := http.DefaultClient.Do(req) - if err != nil { - return "", err - } - defer resp.Body.Close() - body, _ := io.ReadAll(resp.Body) - var data struct { - Login string `json:"login"` - } - if err := json.Unmarshal(body, &data); err != nil { - return "", err - } - return data.Login, nil + return GetAuthenticatedUser(g.config) +} + +func (g *GitBucketApp) GetRepositories() ([]Repository, error) { + // TODO: implement + return nil, nil } func (g *GitBucketApp) CreateRepo(name string) error { @@ -47,41 +27,49 @@ func (g *GitBucketApp) GetApplicationName() string { } func (g *GitBucketApp) GetApiUrl() string { - return g.ApiUrl + return g.config.ApiUrl } func (g *GitBucketApp) SetApiUrl(url string) { - g.ApiUrl = url + g.config.ApiUrl = url } func (g *GitBucketApp) GetToken() string { - return g.Token + return g.config.Token } func (g *GitBucketApp) SetToken(token string) { - g.Token = token + g.config.Token = token } func (g *GitBucketApp) GetUser() string { - return g.User + return g.config.User } func (g *GitBucketApp) SetUser(user string) { - g.User = user + g.config.User = user } func (g *GitBucketApp) GetUsername() string { - return g.Username + return g.config.Username } func (g *GitBucketApp) SetUsername(username string) { - g.Username = username + g.config.Username = username } func (g *GitBucketApp) GetPassword() string { - return g.Password + return g.config.Password } func (g *GitBucketApp) SetPassword(password string) { - g.Password = password + g.config.Password = password +} + +func (g *GitBucketApp) GetEndpoint() ApiEndpoint { + return g.config.Endpoint +} + +func (g *GitBucketApp) SetEndpoint(endpoint ApiEndpoint) { + g.config.Endpoint = endpoint } diff --git a/cli/application/github.go b/cli/application/github.go index 02f0b04..5b735dd 100644 --- a/cli/application/github.go +++ b/cli/application/github.go @@ -1,80 +1,20 @@ package application -import ( - "encoding/json" - "fmt" - "io" - "net/http" -) - type GitHubApp struct { - ApiUrl string - Token string - User string - Username string - Password string + config AppConfig } func (g *GitHubApp) GetOrganizations() ([]Organization, error) { - req, _ := http.NewRequest("GET", g.ApiUrl+"/user/orgs", nil) - req.Header.Set("Authorization", "Bearer "+g.Token) - resp, err := http.DefaultClient.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - // GitBucket may return 404 if not supported - if resp.StatusCode == http.StatusNotFound { - return nil, nil // treat as "no orgs" - } - if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("unexpected status: %d", resp.StatusCode) - } - - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, err - } - - var rawOrgs []map[string]interface{} - if err := json.Unmarshal(body, &rawOrgs); err != nil { - return nil, err - } - - var orgs []Organization - for _, obj := range rawOrgs { - org := Organization{} - // Try "login" or "username" for compatibility - if login, ok := obj["login"].(string); ok { - org.Name = login - } else if username, ok := obj["username"].(string); ok { - org.Name = username - } - if desc, ok := obj["description"].(string); ok { - org.Description = desc - } - orgs = append(orgs, org) - } - return orgs, nil + return GetOrganizations(g.config) } func (g *GitHubApp) GetAuthenticatedUser() (string, error) { - req, _ := http.NewRequest("GET", g.ApiUrl+"/user", nil) - req.Header.Set("Authorization", "Bearer "+g.Token) - resp, err := http.DefaultClient.Do(req) - if err != nil { - return "", err - } - defer resp.Body.Close() - body, _ := io.ReadAll(resp.Body) - var data struct { - Login string `json:"login"` - } - if err := json.Unmarshal(body, &data); err != nil { - return "", err - } - return data.Login, nil + return GetAuthenticatedUser(g.config) +} + +func (g *GitHubApp) GetRepositories() ([]Repository, error) { + // TODO: implement + return nil, nil } func (g *GitHubApp) CreateRepo(name string) error { @@ -87,41 +27,49 @@ func (g *GitHubApp) GetApplicationName() string { } func (g *GitHubApp) GetApiUrl() string { - return g.ApiUrl + return g.config.ApiUrl } func (g *GitHubApp) SetApiUrl(url string) { - g.ApiUrl = url + g.config.ApiUrl = url } func (g *GitHubApp) GetToken() string { - return g.Token + return g.config.Token } func (g *GitHubApp) SetToken(token string) { - g.Token = token + g.config.Token = token } func (g *GitHubApp) GetUser() string { - return g.User + return g.config.User } func (g *GitHubApp) SetUser(user string) { - g.User = user + g.config.User = user } func (g *GitHubApp) GetUsername() string { - return g.Username + return g.config.Username } func (g *GitHubApp) SetUsername(username string) { - g.Username = username + g.config.Username = username } func (g *GitHubApp) GetPassword() string { - return g.Password + return g.config.Password } func (g *GitHubApp) SetPassword(password string) { - g.Password = password + g.config.Password = password +} + +func (g *GitHubApp) GetEndpoint() ApiEndpoint { + return g.config.Endpoint +} + +func (g *GitHubApp) SetEndpoint(endpoint ApiEndpoint) { + g.config.Endpoint = endpoint } diff --git a/cli/application/gitlab.go b/cli/application/gitlab.go index 5c1bf8a..b939d6f 100644 --- a/cli/application/gitlab.go +++ b/cli/application/gitlab.go @@ -1,11 +1,7 @@ package application type GitLabApp struct { - ApiUrl string - Token string - User string - Username string - Password string + config AppConfig } func (g *GitLabApp) GetOrganizations() ([]Organization, error) { @@ -18,6 +14,11 @@ func (g *GitLabApp) GetAuthenticatedUser() (string, error) { return "", nil } +func (g *GitLabApp) GetRepositories() ([]Repository, error) { + // TODO: implement + return nil, nil +} + func (g *GitLabApp) CreateRepo(name string) error { // TODO: implement return nil @@ -28,41 +29,49 @@ func (g *GitLabApp) GetApplicationName() string { } func (g *GitLabApp) GetApiUrl() string { - return g.ApiUrl + return g.config.ApiUrl } func (g *GitLabApp) SetApiUrl(url string) { - g.ApiUrl = url + g.config.ApiUrl = url } func (g *GitLabApp) GetToken() string { - return g.Token + return g.config.Token } func (g *GitLabApp) SetToken(token string) { - g.Token = token + g.config.Token = token } func (g *GitLabApp) GetUser() string { - return g.User + return g.config.User } func (g *GitLabApp) SetUser(user string) { - g.User = user + g.config.User = user } func (g *GitLabApp) GetUsername() string { - return g.Username + return g.config.Username } func (g *GitLabApp) SetUsername(username string) { - g.Username = username + g.config.Username = username } func (g *GitLabApp) GetPassword() string { - return g.Password + return g.config.Password } func (g *GitLabApp) SetPassword(password string) { - g.Password = password + g.config.Password = password +} + +func (g *GitLabApp) GetEndpoint() ApiEndpoint { + return g.config.Endpoint +} + +func (g *GitLabApp) SetEndpoint(endpoint ApiEndpoint) { + g.config.Endpoint = endpoint } diff --git a/cli/application/gogs.go b/cli/application/gogs.go index a49df6e..a451238 100644 --- a/cli/application/gogs.go +++ b/cli/application/gogs.go @@ -1,40 +1,20 @@ package application -import ( - "encoding/json" - "io" - "net/http" -) - type GogsApp struct { - ApiUrl string - Token string - User string - Username string - Password string + config AppConfig } func (g *GogsApp) GetOrganizations() ([]Organization, error) { - // TODO: implement - return nil, nil + return GetOrganizations(g.config) } func (g *GogsApp) GetAuthenticatedUser() (string, error) { - req, _ := http.NewRequest("GET", g.ApiUrl+"/user", nil) - req.Header.Set("Authorization", "Bearer "+g.Token) - resp, err := http.DefaultClient.Do(req) - if err != nil { - return "", err - } - defer resp.Body.Close() - body, _ := io.ReadAll(resp.Body) - var data struct { - Login string `json:"login"` - } - if err := json.Unmarshal(body, &data); err != nil { - return "", err - } - return data.Login, nil + return GetAuthenticatedUser(g.config) +} + +func (g *GogsApp) GetRepositories() ([]Repository, error) { + // TODO: implement + return nil, nil } func (g *GogsApp) CreateRepo(name string) error { @@ -47,41 +27,49 @@ func (g *GogsApp) GetApplicationName() string { } func (g *GogsApp) GetApiUrl() string { - return g.ApiUrl + return g.config.ApiUrl } func (g *GogsApp) SetApiUrl(url string) { - g.ApiUrl = url + g.config.ApiUrl = url } func (g *GogsApp) GetToken() string { - return g.Token + return g.config.Token } func (g *GogsApp) SetToken(token string) { - g.Token = token + g.config.Token = token } func (g *GogsApp) GetUser() string { - return g.User + return g.config.User } func (g *GogsApp) SetUser(user string) { - g.User = user + g.config.User = user } func (g *GogsApp) GetUsername() string { - return g.Username + return g.config.Username } func (g *GogsApp) SetUsername(username string) { - g.Username = username + g.config.Username = username } func (g *GogsApp) GetPassword() string { - return g.Password + return g.config.Password } func (g *GogsApp) SetPassword(password string) { - g.Password = password + g.config.Password = password +} + +func (g *GogsApp) GetEndpoint() ApiEndpoint { + return g.config.Endpoint +} + +func (g *GogsApp) SetEndpoint(endpoint ApiEndpoint) { + g.config.Endpoint = endpoint } diff --git a/cli/application/interface.go b/cli/application/interface.go index 02c06ee..a4dc19b 100644 --- a/cli/application/interface.go +++ b/cli/application/interface.go @@ -1,13 +1,46 @@ package application +// Represents an organization. type Organization struct { Name string Description string } +// Represents a user. +type User struct { + Login string +} + +// Represents a repository. +type Repository struct { + Owner User + Name string + FullName string + Private bool + Description string + Fork bool + CloneUrl string + MirrorUrl string + OpenIssueCount int + HasWiki bool + HasIssues bool + HasProjects bool + HasDownloads bool + Homepage string +} + +// Represents an issue. +type Issue struct { + Title string + Body string + State string + Number int +} + type Application interface { GetOrganizations() ([]Organization, error) GetAuthenticatedUser() (string, error) + GetRepositories() ([]Repository, error) CreateRepo(name string) error GetApplicationName() string @@ -23,6 +56,23 @@ type Application interface { SetPassword(password string) } +type ApiEndpoint int + +const ( + EndpointUser ApiEndpoint = iota + EndpointOrganization +) + +// Application configuration +type AppConfig struct { + ApiUrl string + Token string + User string + Username string + Password string + Endpoint ApiEndpoint +} + type ApplicationType string const ( @@ -40,11 +90,11 @@ func NewApplication(appType ApplicationType) Application { case AppGitBucket: return &GitBucketApp{} case AppGitHub: - return &GitHubApp{ApiUrl: "https://api.github.com"} + return &GitHubApp{config: AppConfig{ApiUrl: "https://api.github.com"}} case AppGitLab: - return &GitLabApp{ApiUrl: "https://gitlab.com/api/v4"} + return &GitLabApp{config: AppConfig{ApiUrl: "https://gitlab.com/api/v4"}} case AppBitbucket: - return &BitbucketApp{ApiUrl: "https://api.bitbucket.org/2.0"} + return &BitbucketApp{config: AppConfig{ApiUrl: "https://api.bitbucket.org/2.0"}} default: return nil } diff --git a/cli/main.go b/cli/main.go index 95b27b3..47ed344 100644 --- a/cli/main.go +++ b/cli/main.go @@ -25,9 +25,7 @@ func main() { _, err := sourceApp.GetOrganizations() if err != nil { fmt.Fprintf(os.Stderr, "GetOrganizations failed: %v\n", err) - os.Exit(1) } - os.Exit(1) if len(os.Args) < 2 { From c841352e2863c22413544b6542a51bec1d16195b Mon Sep 17 00:00:00 2001 From: Crayon2000 Date: Mon, 15 Sep 2025 15:35:48 -0400 Subject: [PATCH 04/25] Add GetRepositories function --- cli/application/bitbucket.go | 2 +- cli/application/common.go | 180 +++++++++++++++++++++++++++++++++++ cli/application/gitbucket.go | 5 +- cli/application/github.go | 5 +- cli/application/gitlab.go | 2 +- cli/application/gogs.go | 5 +- cli/application/interface.go | 2 +- 7 files changed, 189 insertions(+), 12 deletions(-) diff --git a/cli/application/bitbucket.go b/cli/application/bitbucket.go index bdb619d..86d968c 100644 --- a/cli/application/bitbucket.go +++ b/cli/application/bitbucket.go @@ -14,7 +14,7 @@ func (g *BitbucketApp) GetAuthenticatedUser() (string, error) { return "", nil } -func (g *BitbucketApp) GetRepositories() ([]Repository, error) { +func (g *BitbucketApp) GetRepositories(endpoint ApiEndpoint, owner string, authUser string) ([]Repository, error) { // TODO: implement return nil, nil } diff --git a/cli/application/common.go b/cli/application/common.go index c091048..64f875c 100644 --- a/cli/application/common.go +++ b/cli/application/common.go @@ -2,9 +2,11 @@ package application import ( "encoding/json" + "errors" "fmt" "io" "net/http" + "regexp" ) func GetOrganizations(config AppConfig) ([]Organization, error) { @@ -68,3 +70,181 @@ func GetAuthenticatedUser(config AppConfig) (string, error) { } return data.Login, nil } + +func GetNextUrl(resp *http.Response) string { + // Get the Link header + linkHeader := resp.Header.Get("Link") + if linkHeader == "" { + return "" + } + // Regular expression to match the 'next' URL + re := regexp.MustCompile(`<(\S+)>;\s*rel="next"`) + match := re.FindStringSubmatch(linkHeader) + if len(match) >= 2 { + return match[1] + } + return "" +} + +// JsonToUser parses a JSON object to fill a User struct. +func JsonToUser(obj map[string]any, u *User) error { + if obj == nil { + return errors.New("invalid JSON input for User") + } + if login, ok := obj["login"].(string); ok { + u.Login = login + } + return nil +} + +// JsonToRepo parses a JSON object to fill a Repository struct. +func JsonToRepo(obj map[string]any, repo *Repository) error { + if obj == nil { + return errors.New("invalid JSON input for Repository") + } + + // Owner (required) + if ownerObj, ok := obj["owner"].(map[string]any); ok { + if err := JsonToUser(ownerObj, &repo.Owner); err != nil { + return err + } + } else { + return errors.New("owner not found") + } + + // Name (required) + if name, ok := obj["name"].(string); ok { + repo.Name = name + } else { + return errors.New("name not found") + } + + // FullName (required) + if fullName, ok := obj["full_name"].(string); ok { + repo.FullName = fullName + } else { + return errors.New("full_name not found") + } + + // Private (required) + if priv, ok := obj["private"].(bool); ok { + repo.Private = priv + } else { + return errors.New("private not found") + } + + // Description + if desc, ok := obj["description"].(string); ok { + repo.Description = desc + } else { + repo.Description = "" + } + + // Fork + if fork, ok := obj["fork"].(bool); ok { + repo.Fork = fork + } else { + repo.Fork = false // Default is false + } + + // CloneUrl (required) + if cloneURL, ok := obj["clone_url"].(string); ok { + repo.CloneUrl = cloneURL + } else { + return errors.New("clone_url not found") + } + + // MirrorUrl + if mirrorURL, ok := obj["mirror_url"].(string); ok { + repo.MirrorUrl = mirrorURL + } else { + repo.MirrorUrl = "" + } + + // OpenIssueCount + if issues, ok := obj["open_issues_count"].(float64); ok { + repo.OpenIssueCount = int(issues) + } else { + repo.OpenIssueCount = 0 + } + + // HasWiki (default true) + if hasWiki, ok := obj["has_wiki"].(bool); ok { + repo.HasWiki = hasWiki + } else { + repo.HasWiki = true + } + + // HasIssues (default true) + if hasIssues, ok := obj["has_issues"].(bool); ok { + repo.HasIssues = hasIssues + } else { + repo.HasIssues = true + } + + // HasProjects (default true) + if hasProjects, ok := obj["has_projects"].(bool); ok { + repo.HasProjects = hasProjects + } else { + repo.HasProjects = true + } + + // HasDownloads (default true) + if hasDownloads, ok := obj["has_downloads"].(bool); ok { + repo.HasDownloads = hasDownloads + } else { + repo.HasDownloads = true + } + + // Homepage + if homepage, ok := obj["homepage"].(string); ok { + repo.Homepage = homepage + } else { + repo.Homepage = "" + } + + return nil +} + +func GetRepositories(config AppConfig, endpoint ApiEndpoint, owner string, authUser string) ([]Repository, error) { + url := config.ApiUrl + if endpoint == EndpointOrganization { + url += "/orgs/" + owner + "/repos" + } else { + if owner == authUser { + url += "/user/repos" + } else { + url += "/users/" + owner + "/repos" + } + } + + var repos []Repository + + for url != "" { + req, _ := http.NewRequest("GET", url, nil) + req.Header.Set("Authorization", "Bearer "+config.Token) + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + body, _ := io.ReadAll(resp.Body) + + var obj []map[string]any + if err := json.Unmarshal(body, &obj); err != nil { + return nil, err + } + + for _, item := range obj { + repo := Repository{} + JsonToRepo(item, &repo) + if endpoint == EndpointUser && repo.Owner.Login != owner { + continue + } + repos = append(repos, repo) + } + + url = GetNextUrl(resp) + } + return repos, nil +} diff --git a/cli/application/gitbucket.go b/cli/application/gitbucket.go index cf89796..31d7044 100644 --- a/cli/application/gitbucket.go +++ b/cli/application/gitbucket.go @@ -12,9 +12,8 @@ func (g *GitBucketApp) GetAuthenticatedUser() (string, error) { return GetAuthenticatedUser(g.config) } -func (g *GitBucketApp) GetRepositories() ([]Repository, error) { - // TODO: implement - return nil, nil +func (g *GitBucketApp) GetRepositories(endpoint ApiEndpoint, owner string, authUser string) ([]Repository, error) { + return GetRepositories(g.config, endpoint, owner, authUser) } func (g *GitBucketApp) CreateRepo(name string) error { diff --git a/cli/application/github.go b/cli/application/github.go index 5b735dd..41869c5 100644 --- a/cli/application/github.go +++ b/cli/application/github.go @@ -12,9 +12,8 @@ func (g *GitHubApp) GetAuthenticatedUser() (string, error) { return GetAuthenticatedUser(g.config) } -func (g *GitHubApp) GetRepositories() ([]Repository, error) { - // TODO: implement - return nil, nil +func (g *GitHubApp) GetRepositories(endpoint ApiEndpoint, owner string, authUser string) ([]Repository, error) { + return GetRepositories(g.config, endpoint, owner, authUser) } func (g *GitHubApp) CreateRepo(name string) error { diff --git a/cli/application/gitlab.go b/cli/application/gitlab.go index b939d6f..dbb5b1a 100644 --- a/cli/application/gitlab.go +++ b/cli/application/gitlab.go @@ -14,7 +14,7 @@ func (g *GitLabApp) GetAuthenticatedUser() (string, error) { return "", nil } -func (g *GitLabApp) GetRepositories() ([]Repository, error) { +func (g *GitLabApp) GetRepositories(endpoint ApiEndpoint, owner string, authUser string) ([]Repository, error) { // TODO: implement return nil, nil } diff --git a/cli/application/gogs.go b/cli/application/gogs.go index a451238..adcfd1e 100644 --- a/cli/application/gogs.go +++ b/cli/application/gogs.go @@ -12,9 +12,8 @@ func (g *GogsApp) GetAuthenticatedUser() (string, error) { return GetAuthenticatedUser(g.config) } -func (g *GogsApp) GetRepositories() ([]Repository, error) { - // TODO: implement - return nil, nil +func (g *GogsApp) GetRepositories(endpoint ApiEndpoint, owner string, authUser string) ([]Repository, error) { + return GetRepositories(g.config, endpoint, owner, authUser) } func (g *GogsApp) CreateRepo(name string) error { diff --git a/cli/application/interface.go b/cli/application/interface.go index a4dc19b..3e5aea8 100644 --- a/cli/application/interface.go +++ b/cli/application/interface.go @@ -40,7 +40,7 @@ type Issue struct { type Application interface { GetOrganizations() ([]Organization, error) GetAuthenticatedUser() (string, error) - GetRepositories() ([]Repository, error) + GetRepositories(endpoint ApiEndpoint, owner string, authUser string) ([]Repository, error) CreateRepo(name string) error GetApplicationName() string From 26ededd78cf0b85284247c80a7cfbd0811f33f4d Mon Sep 17 00:00:00 2001 From: Crayon2000 Date: Mon, 15 Sep 2025 19:41:20 -0400 Subject: [PATCH 05/25] Add GetIssues --- cli/application/bitbucket.go | 5 +++ cli/application/common.go | 61 ++++++++++++++++++++++++++++++++++++ cli/application/gitbucket.go | 4 +++ cli/application/github.go | 4 +++ cli/application/gitlab.go | 5 +++ cli/application/gogs.go | 4 +++ cli/application/interface.go | 1 + 7 files changed, 84 insertions(+) diff --git a/cli/application/bitbucket.go b/cli/application/bitbucket.go index 86d968c..f626f32 100644 --- a/cli/application/bitbucket.go +++ b/cli/application/bitbucket.go @@ -19,6 +19,11 @@ func (g *BitbucketApp) GetRepositories(endpoint ApiEndpoint, owner string, authU return nil, nil } +func (g *BitbucketApp) GetIssues(repo Repository) ([]Issue, error) { + // TODO: implement + return nil, nil +} + func (g *BitbucketApp) CreateRepo(name string) error { // TODO: implement return nil diff --git a/cli/application/common.go b/cli/application/common.go index 64f875c..20a7ca8 100644 --- a/cli/application/common.go +++ b/cli/application/common.go @@ -248,3 +248,64 @@ func GetRepositories(config AppConfig, endpoint ApiEndpoint, owner string, authU } return repos, nil } + +// JsonToIssue parses a JSON object (map[string]interface{}) to fill an Issue struct. +func JsonToIssue(obj map[string]any, issue *Issue) error { + if obj == nil { + return errors.New("invalid JSON input") + } + + // Title + issue.Title = "" + if title, ok := obj["title"].(string); ok { + issue.Title = title + } + + // Body + issue.Body = "" + if body, ok := obj["body"].(string); ok { + issue.Body = body + } + + // State + issue.State = "" + if state, ok := obj["state"].(string); ok { + issue.State = state + } + + // Number + if number, ok := obj["number"].(float64); ok { + issue.Number = int(number) + } else { + issue.Number = 0 + } + + return nil +} + +func GetIssues(config AppConfig, repo Repository) ([]Issue, error) { + url := config.ApiUrl + "/repos/" + repo.Owner.Login + "/" + repo.Name + "/issues" + var issues []Issue + + req, _ := http.NewRequest("GET", url, nil) + req.Header.Set("Authorization", "Bearer "+config.Token) + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + body, _ := io.ReadAll(resp.Body) + + var obj []map[string]any + if err := json.Unmarshal(body, &obj); err != nil { + return nil, err + } + + for _, item := range obj { + issue := Issue{} + JsonToIssue(item, &issue) + issues = append(issues, issue) + } + + return issues, nil +} diff --git a/cli/application/gitbucket.go b/cli/application/gitbucket.go index 31d7044..b7f68ec 100644 --- a/cli/application/gitbucket.go +++ b/cli/application/gitbucket.go @@ -16,6 +16,10 @@ func (g *GitBucketApp) GetRepositories(endpoint ApiEndpoint, owner string, authU return GetRepositories(g.config, endpoint, owner, authUser) } +func (g *GitBucketApp) GetIssues(repo Repository) ([]Issue, error) { + return GetIssues(g.config, repo) +} + func (g *GitBucketApp) CreateRepo(name string) error { // TODO: implement return nil diff --git a/cli/application/github.go b/cli/application/github.go index 41869c5..c7fa1c8 100644 --- a/cli/application/github.go +++ b/cli/application/github.go @@ -16,6 +16,10 @@ func (g *GitHubApp) GetRepositories(endpoint ApiEndpoint, owner string, authUser return GetRepositories(g.config, endpoint, owner, authUser) } +func (g *GitHubApp) GetIssues(repo Repository) ([]Issue, error) { + return GetIssues(g.config, repo) +} + func (g *GitHubApp) CreateRepo(name string) error { // TODO: implement return nil diff --git a/cli/application/gitlab.go b/cli/application/gitlab.go index dbb5b1a..4566cc6 100644 --- a/cli/application/gitlab.go +++ b/cli/application/gitlab.go @@ -19,6 +19,11 @@ func (g *GitLabApp) GetRepositories(endpoint ApiEndpoint, owner string, authUser return nil, nil } +func (g *GitLabApp) GetIssues(repo Repository) ([]Issue, error) { + // TODO: implement + return nil, nil +} + func (g *GitLabApp) CreateRepo(name string) error { // TODO: implement return nil diff --git a/cli/application/gogs.go b/cli/application/gogs.go index adcfd1e..425b6dc 100644 --- a/cli/application/gogs.go +++ b/cli/application/gogs.go @@ -16,6 +16,10 @@ func (g *GogsApp) GetRepositories(endpoint ApiEndpoint, owner string, authUser s return GetRepositories(g.config, endpoint, owner, authUser) } +func (g *GogsApp) GetIssues(repo Repository) ([]Issue, error) { + return GetIssues(g.config, repo) +} + func (g *GogsApp) CreateRepo(name string) error { // TODO: implement return nil diff --git a/cli/application/interface.go b/cli/application/interface.go index 3e5aea8..86dd4ef 100644 --- a/cli/application/interface.go +++ b/cli/application/interface.go @@ -41,6 +41,7 @@ type Application interface { GetOrganizations() ([]Organization, error) GetAuthenticatedUser() (string, error) GetRepositories(endpoint ApiEndpoint, owner string, authUser string) ([]Repository, error) + GetIssues(repo Repository) ([]Issue, error) CreateRepo(name string) error GetApplicationName() string From 281119d9ea186df6277b76fe4cb55a91c796c69d Mon Sep 17 00:00:00 2001 From: Crayon2000 Date: Tue, 23 Sep 2025 01:10:17 -0400 Subject: [PATCH 06/25] Add code for CreateRepo --- cli/application/bitbucket.go | 4 +-- cli/application/common.go | 62 ++++++++++++++++++++++++++++++++++++ cli/application/gitbucket.go | 5 ++- cli/application/github.go | 5 ++- cli/application/gitlab.go | 4 +-- cli/application/gogs.go | 5 ++- cli/application/interface.go | 2 +- cli/main.go | 4 +++ 8 files changed, 77 insertions(+), 14 deletions(-) diff --git a/cli/application/bitbucket.go b/cli/application/bitbucket.go index f626f32..ac34891 100644 --- a/cli/application/bitbucket.go +++ b/cli/application/bitbucket.go @@ -24,9 +24,9 @@ func (g *BitbucketApp) GetIssues(repo Repository) ([]Issue, error) { return nil, nil } -func (g *BitbucketApp) CreateRepo(name string) error { +func (g *BitbucketApp) CreateRepo(endpoint ApiEndpoint, owner string, source Repository) (Repository, error) { // TODO: implement - return nil + return Repository{}, nil } func (g *BitbucketApp) GetApplicationName() string { diff --git a/cli/application/common.go b/cli/application/common.go index 20a7ca8..756c623 100644 --- a/cli/application/common.go +++ b/cli/application/common.go @@ -7,6 +7,7 @@ import ( "io" "net/http" "regexp" + "strings" ) func GetOrganizations(config AppConfig) ([]Organization, error) { @@ -309,3 +310,64 @@ func GetIssues(config AppConfig, repo Repository) ([]Issue, error) { return issues, nil } + +// RepoToJson converts a Repository struct to a GitHub-compatible JSON string for the repo API. +// Only the allowed fields are included in the output JSON. +func RepoToJson(repo Repository) (string, error) { + // Create a map with the relevant fields + out := map[string]any{ + "name": repo.Name, + "description": repo.Description, + "homepage": repo.Homepage, + "private": repo.Private, + "has_issues": repo.HasIssues, + "has_projects": repo.HasProjects, + "has_wiki": repo.HasWiki, + "has_downloads": repo.HasDownloads, + } + + // Marshal to JSON with indentation (for readability; remove for compact) + jsonBytes, err := json.MarshalIndent(out, "", " ") + if err != nil { + return "", err + } + return string(jsonBytes), nil +} + +func CreateRepo(config AppConfig, endpoint ApiEndpoint, owner string, source Repository) (Repository, error) { + url := config.ApiUrl + if endpoint == EndpointOrganization { + url += "/orgs/" + owner + "/repos" + } else { + url += "/user/repos" + } + + jsonData, err := RepoToJson(source) + if err != nil { + return Repository{}, err + } + + req, _ := http.NewRequest("POST", url, strings.NewReader(jsonData)) + req.Header.Set("Authorization", "Bearer "+config.Token) + req.Header.Set("Content-Type", "application/json") + resp, err := http.DefaultClient.Do(req) + if err != nil { + return Repository{}, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusCreated { + return Repository{}, fmt.Errorf("failed to create repo: %s", resp.Status) + } + + body, _ := io.ReadAll(resp.Body) + var obj map[string]any + if err := json.Unmarshal(body, &obj); err != nil { + return Repository{}, err + } + var repo Repository + if err := JsonToRepo(obj, &repo); err != nil { + return Repository{}, err + } + return repo, nil +} diff --git a/cli/application/gitbucket.go b/cli/application/gitbucket.go index b7f68ec..7c6f97b 100644 --- a/cli/application/gitbucket.go +++ b/cli/application/gitbucket.go @@ -20,9 +20,8 @@ func (g *GitBucketApp) GetIssues(repo Repository) ([]Issue, error) { return GetIssues(g.config, repo) } -func (g *GitBucketApp) CreateRepo(name string) error { - // TODO: implement - return nil +func (g *GitBucketApp) CreateRepo(endpoint ApiEndpoint, owner string, source Repository) (Repository, error) { + return CreateRepo(g.config, endpoint, owner, source) } func (g *GitBucketApp) GetApplicationName() string { diff --git a/cli/application/github.go b/cli/application/github.go index c7fa1c8..a84c643 100644 --- a/cli/application/github.go +++ b/cli/application/github.go @@ -20,9 +20,8 @@ func (g *GitHubApp) GetIssues(repo Repository) ([]Issue, error) { return GetIssues(g.config, repo) } -func (g *GitHubApp) CreateRepo(name string) error { - // TODO: implement - return nil +func (g *GitHubApp) CreateRepo(endpoint ApiEndpoint, owner string, source Repository) (Repository, error) { + return CreateRepo(g.config, endpoint, owner, source) } func (g *GitHubApp) GetApplicationName() string { diff --git a/cli/application/gitlab.go b/cli/application/gitlab.go index 4566cc6..305db0e 100644 --- a/cli/application/gitlab.go +++ b/cli/application/gitlab.go @@ -24,9 +24,9 @@ func (g *GitLabApp) GetIssues(repo Repository) ([]Issue, error) { return nil, nil } -func (g *GitLabApp) CreateRepo(name string) error { +func (g *GitLabApp) CreateRepo(endpoint ApiEndpoint, owner string, source Repository) (Repository, error) { // TODO: implement - return nil + return Repository{}, nil } func (g *GitLabApp) GetApplicationName() string { diff --git a/cli/application/gogs.go b/cli/application/gogs.go index 425b6dc..116ae40 100644 --- a/cli/application/gogs.go +++ b/cli/application/gogs.go @@ -20,9 +20,8 @@ func (g *GogsApp) GetIssues(repo Repository) ([]Issue, error) { return GetIssues(g.config, repo) } -func (g *GogsApp) CreateRepo(name string) error { - // TODO: implement - return nil +func (g *GogsApp) CreateRepo(endpoint ApiEndpoint, owner string, source Repository) (Repository, error) { + return CreateRepo(g.config, endpoint, owner, source) } func (g *GogsApp) GetApplicationName() string { diff --git a/cli/application/interface.go b/cli/application/interface.go index 86dd4ef..18f422f 100644 --- a/cli/application/interface.go +++ b/cli/application/interface.go @@ -42,7 +42,7 @@ type Application interface { GetAuthenticatedUser() (string, error) GetRepositories(endpoint ApiEndpoint, owner string, authUser string) ([]Repository, error) GetIssues(repo Repository) ([]Issue, error) - CreateRepo(name string) error + CreateRepo(endpoint ApiEndpoint, owner string, source Repository) (Repository, error) GetApplicationName() string GetApiUrl() string diff --git a/cli/main.go b/cli/main.go index 47ed344..3b92e8e 100644 --- a/cli/main.go +++ b/cli/main.go @@ -26,6 +26,10 @@ func main() { if err != nil { fmt.Fprintf(os.Stderr, "GetOrganizations failed: %v\n", err) } + _, err = sourceApp.GetAuthenticatedUser() + if err != nil { + fmt.Fprintf(os.Stderr, "GetAuthenticatedUser failed: %v\n", err) + } os.Exit(1) if len(os.Args) < 2 { From cea205c51eb944e57742330917ef0c7ba4130c09 Mon Sep 17 00:00:00 2001 From: Crayon2000 Date: Sun, 28 Sep 2025 23:44:16 -0400 Subject: [PATCH 07/25] Fix GitBucket and Gogs --- cli/application/common.go | 16 +++++++++++----- cli/application/interface.go | 1 + 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/cli/application/common.go b/cli/application/common.go index 756c623..5c4331d 100644 --- a/cli/application/common.go +++ b/cli/application/common.go @@ -10,9 +10,10 @@ import ( "strings" ) +// GetOrganizations retrieves the list of organizations for the authenticated user. func GetOrganizations(config AppConfig) ([]Organization, error) { req, _ := http.NewRequest("GET", config.ApiUrl+"/user/orgs", nil) - req.Header.Set("Authorization", "Bearer "+config.Token) + req.Header.Set("Authorization", "token "+config.Token) resp, err := http.DefaultClient.Do(req) if err != nil { return nil, err @@ -54,9 +55,10 @@ func GetOrganizations(config AppConfig) ([]Organization, error) { return orgs, nil } +// GetAuthenticatedUser retrieves the username of the authenticated user. func GetAuthenticatedUser(config AppConfig) (string, error) { req, _ := http.NewRequest("GET", config.ApiUrl+"/user", nil) - req.Header.Set("Authorization", "Bearer "+config.Token) + req.Header.Set("Authorization", "token "+config.Token) resp, err := http.DefaultClient.Do(req) if err != nil { return "", err @@ -72,6 +74,7 @@ func GetAuthenticatedUser(config AppConfig) (string, error) { return data.Login, nil } +// GetNextUrl extracts the 'next' URL from the Link header for pagination. func GetNextUrl(resp *http.Response) string { // Get the Link header linkHeader := resp.Header.Get("Link") @@ -207,6 +210,7 @@ func JsonToRepo(obj map[string]any, repo *Repository) error { return nil } +// GetRepositories retrieves repositories for a given owner and endpoint. func GetRepositories(config AppConfig, endpoint ApiEndpoint, owner string, authUser string) ([]Repository, error) { url := config.ApiUrl if endpoint == EndpointOrganization { @@ -223,7 +227,7 @@ func GetRepositories(config AppConfig, endpoint ApiEndpoint, owner string, authU for url != "" { req, _ := http.NewRequest("GET", url, nil) - req.Header.Set("Authorization", "Bearer "+config.Token) + req.Header.Set("Authorization", "token "+config.Token) resp, err := http.DefaultClient.Do(req) if err != nil { return nil, err @@ -284,12 +288,13 @@ func JsonToIssue(obj map[string]any, issue *Issue) error { return nil } +// GetIssues retrieves issues for a given repository. func GetIssues(config AppConfig, repo Repository) ([]Issue, error) { url := config.ApiUrl + "/repos/" + repo.Owner.Login + "/" + repo.Name + "/issues" var issues []Issue req, _ := http.NewRequest("GET", url, nil) - req.Header.Set("Authorization", "Bearer "+config.Token) + req.Header.Set("Authorization", "token "+config.Token) resp, err := http.DefaultClient.Do(req) if err != nil { return nil, err @@ -334,6 +339,7 @@ func RepoToJson(repo Repository) (string, error) { return string(jsonBytes), nil } +// CreateRepo creates a new repository. func CreateRepo(config AppConfig, endpoint ApiEndpoint, owner string, source Repository) (Repository, error) { url := config.ApiUrl if endpoint == EndpointOrganization { @@ -348,7 +354,7 @@ func CreateRepo(config AppConfig, endpoint ApiEndpoint, owner string, source Rep } req, _ := http.NewRequest("POST", url, strings.NewReader(jsonData)) - req.Header.Set("Authorization", "Bearer "+config.Token) + req.Header.Set("Authorization", "token "+config.Token) req.Header.Set("Content-Type", "application/json") resp, err := http.DefaultClient.Do(req) if err != nil { diff --git a/cli/application/interface.go b/cli/application/interface.go index 18f422f..4038ac4 100644 --- a/cli/application/interface.go +++ b/cli/application/interface.go @@ -84,6 +84,7 @@ const ( AppBitbucket ApplicationType = "Bitbucket" ) +// NewApplication creates a new Application instance based on the given type. func NewApplication(appType ApplicationType) Application { switch appType { case AppGogs: From 0a4811d87a6e982c5a70503648d2639ce3445908 Mon Sep 17 00:00:00 2001 From: Crayon2000 Date: Sat, 4 Oct 2025 14:53:17 -0400 Subject: [PATCH 08/25] Add UI --- cli/main.go | 102 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 101 insertions(+), 1 deletion(-) diff --git a/cli/main.go b/cli/main.go index 3b92e8e..7083228 100644 --- a/cli/main.go +++ b/cli/main.go @@ -5,8 +5,47 @@ import ( "fmt" "gitconduit-cli/application" "os" + "time" + + "github.com/rivo/tview" ) +func showAnimatedLoading(app *tview.Application, nextForm tview.Primitive, loadFunc func()) { + loading := tview.NewTextView() + loading.SetTextAlign(tview.AlignCenter) + loading.SetBorder(true).SetTitle("Loading").SetTitleAlign(tview.AlignLeft) + + app.SetRoot(loading, true) + done := make(chan struct{}) + + // Animation goroutine + go func() { + frames := []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"} + i := 0 + for { + select { + case <-done: + return + default: + app.QueueUpdateDraw(func() { + loading.SetText("Please wait " + frames[i%len(frames)]) + }) + i++ + time.Sleep(400 * time.Millisecond) + } + } + }() + + // Simulate loading + go func() { + loadFunc() + close(done) // Stop animation + app.QueueUpdateDraw(func() { + app.SetRoot(nextForm, true) + }) + }() +} + func main() { // Define the main arguments cloneAndPushCmd := flag.NewFlagSet("cloneandpush", flag.ExitOnError) @@ -20,15 +59,76 @@ func main() { destpassword := cloneAndPushCmd.String("destpassword", "", "Destination password for authentication") hasWiki := cloneAndPushCmd.Bool("haswiki", false, "Set to true if the repository has a wiki (default: false)") + app := tview.NewApplication() + + form1 := tview.NewForm(). + AddDropDown("Application:", []string{"Gogs", "GitBucket", "GitHub"}, 0, nil). + AddInputField("API URL:", "", 50, nil, nil). + AddInputField("Authorization Token:", "", 50, nil, nil). + AddInputField("Username:", "", 50, nil, nil). + AddPasswordField("Password:", "", 50, '*', nil) + form1.SetBorder(true).SetTitle("Source").SetTitleAlign(tview.AlignLeft) + + form2 := tview.NewForm() + + userField := tview.NewInputField().SetLabel("User: ").SetFieldWidth(50) + orgField := tview.NewInputField().SetLabel("Organization:").SetFieldWidth(50) + dropdown := tview.NewDropDown().SetLabel("Type:") + + // Dropdown with callback + dropdown.SetOptions([]string{"User", "Organization"}, func(option string, index int) { + // Remove both, then add back only the selected one + form2.Clear(false) // keep buttons intact + form2.AddFormItem(dropdown) + if option == "User" { + form2.AddFormItem(userField) + } else { + form2.AddFormItem(orgField) + } + }) + + form2.AddFormItem(dropdown). + AddFormItem(userField). + AddButton("Back", func() { + app.SetRoot(form1, true) + }). + AddButton("Quit", func() { + app.Stop() + }) + form2.SetBorder(true).SetTitle("Source Owner").SetTitleAlign(tview.AlignLeft) + + form1. + AddButton("Next", func() { + showAnimatedLoading(app, form2, func() { + time.Sleep(10 * time.Second) // replace with HTTP request + }) + }). + AddButton("Quit", func() { + app.Stop() + }) + + if err := app.SetRoot(form1, true).EnableMouse(true).EnablePaste(true).Run(); err != nil { + panic(err) + } + + os.Exit(0) + sourceApp := application.NewApplication(application.AppGitHub) sourceApp.SetToken("") _, err := sourceApp.GetOrganizations() + // Test getting organizations + orgs, err := sourceApp.GetOrganizations() if err != nil { fmt.Fprintf(os.Stderr, "GetOrganizations failed: %v\n", err) + } else { + fmt.Printf("Organizations: %+v\n", orgs) } - _, err = sourceApp.GetAuthenticatedUser() + // Test getting authenticated user + authUser, err := sourceApp.GetAuthenticatedUser() if err != nil { fmt.Fprintf(os.Stderr, "GetAuthenticatedUser failed: %v\n", err) + } else { + fmt.Printf("Authenticated User: %s\n", authUser) } os.Exit(1) From a933f1a544da3b7cc36f1c486d7a8270da5c3b9c Mon Sep 17 00:00:00 2001 From: Crayon2000 Date: Sun, 5 Oct 2025 01:48:49 -0400 Subject: [PATCH 09/25] Add UI --- cli/main.go | 150 ++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 103 insertions(+), 47 deletions(-) diff --git a/cli/main.go b/cli/main.go index 7083228..a1a4797 100644 --- a/cli/main.go +++ b/cli/main.go @@ -13,7 +13,7 @@ import ( func showAnimatedLoading(app *tview.Application, nextForm tview.Primitive, loadFunc func()) { loading := tview.NewTextView() loading.SetTextAlign(tview.AlignCenter) - loading.SetBorder(true).SetTitle("Loading").SetTitleAlign(tview.AlignLeft) + loading.SetTitle("Loading").SetTitleAlign(tview.AlignLeft).SetBorder(true) app.SetRoot(loading, true) done := make(chan struct{}) @@ -47,65 +47,98 @@ func showAnimatedLoading(app *tview.Application, nextForm tview.Primitive, loadF } func main() { - // Define the main arguments - cloneAndPushCmd := flag.NewFlagSet("cloneandpush", flag.ExitOnError) - - // Define flags for the cloneandpush command - sourcerepo := cloneAndPushCmd.String("sourcerepo", "", "Source repository URL to clone") - sourceusername := cloneAndPushCmd.String("sourceusername", "", "Source username for authentication") - sourcepassword := cloneAndPushCmd.String("sourcepassword", "", "Source password for authentication") - destrepo := cloneAndPushCmd.String("destrepo", "", "Destination repository URL to clone") - destusername := cloneAndPushCmd.String("destusername", "", "Destination username for authentication") - destpassword := cloneAndPushCmd.String("destpassword", "", "Destination password for authentication") - hasWiki := cloneAndPushCmd.Bool("haswiki", false, "Set to true if the repository has a wiki (default: false)") + sourceApplication := application.NewApplication(application.AppGitHub) app := tview.NewApplication() - form1 := tview.NewForm(). - AddDropDown("Application:", []string{"Gogs", "GitBucket", "GitHub"}, 0, nil). - AddInputField("API URL:", "", 50, nil, nil). - AddInputField("Authorization Token:", "", 50, nil, nil). - AddInputField("Username:", "", 50, nil, nil). - AddPasswordField("Password:", "", 50, '*', nil) - form1.SetBorder(true).SetTitle("Source").SetTitleAlign(tview.AlignLeft) - + // Create form1 with controls + form1 := tview.NewForm() + sourceAppDropDown := tview.NewDropDown().SetLabel("Application:"). + SetOptions([]string{"Gogs", "GitBucket", "GitHub"}, nil). + SetCurrentOption(0) + sourceAPIInput := tview.NewInputField().SetLabel("API URL:").SetFieldWidth(50) + sourceAuthTokenInput := tview.NewInputField().SetLabel("Authorization Token:").SetFieldWidth(50) + sourceUsernameInput := tview.NewInputField().SetLabel("Username:").SetFieldWidth(50) + sourcePasswordInput := tview.NewInputField().SetLabel("Password:").SetFieldWidth(50) + + // Create form2 with controls form2 := tview.NewForm() + sourceUserInput := tview.NewInputField().SetLabel("User: ").SetFieldWidth(50) + sourceOrgInput := tview.NewInputField().SetLabel("Organization:").SetFieldWidth(50) + sourceTypeDropDown := tview.NewDropDown().SetLabel("Type:") - userField := tview.NewInputField().SetLabel("User: ").SetFieldWidth(50) - orgField := tview.NewInputField().SetLabel("Organization:").SetFieldWidth(50) - dropdown := tview.NewDropDown().SetLabel("Type:") + beforeForm1 := func() func() { + return func() { + } + } - // Dropdown with callback - dropdown.SetOptions([]string{"User", "Organization"}, func(option string, index int) { - // Remove both, then add back only the selected one - form2.Clear(false) // keep buttons intact - form2.AddFormItem(dropdown) - if option == "User" { - form2.AddFormItem(userField) - } else { - form2.AddFormItem(orgField) + beforeForm2 := func(app *tview.Application, next tview.Primitive) func() { + return func() { + showAnimatedLoading(app, next, func() { + _, opt := sourceAppDropDown.GetCurrentOption() + sourceApplication = application.NewApplication(application.ApplicationType(opt)) + sourceApplication.SetApiUrl(sourceAPIInput.GetText()) + sourceApplication.SetToken(sourceAuthTokenInput.GetText()) + sourceApplication.SetUsername(sourceUsernameInput.GetText()) + sourceApplication.SetPassword(sourcePasswordInput.GetText()) + + //sourceTypeDropDown.SetCurrentOption(0) + authUser, err := sourceApplication.GetAuthenticatedUser() + if err != nil { + fmt.Fprintf(os.Stderr, "GetAuthenticatedUser failed: %v\n", err) + } else { + sourceUserInput.SetText(authUser) + } + orgs, err := sourceApplication.GetOrganizations() + if err != nil { + fmt.Fprintf(os.Stderr, "GetOrganizations failed: %v\n", err) + } else { + if len(orgs) >= 1 { + + } else { + sourceOrgInput.SetText("") + } + fmt.Printf("Organizations: %+v\n", orgs) + } + }) } - }) + } - form2.AddFormItem(dropdown). - AddFormItem(userField). - AddButton("Back", func() { - app.SetRoot(form1, true) - }). + // Setup form1 + form1. + AddFormItem(sourceAppDropDown). + AddFormItem(sourceAPIInput). + AddFormItem(sourceAuthTokenInput). + AddFormItem(sourceUsernameInput). + AddFormItem(sourcePasswordInput). + AddButton("Next", beforeForm2(app, form2)). AddButton("Quit", func() { app.Stop() }) - form2.SetBorder(true).SetTitle("Source Owner").SetTitleAlign(tview.AlignLeft) + form1.SetTitle("Source").SetTitleAlign(tview.AlignLeft).SetBorder(true) - form1. - AddButton("Next", func() { - showAnimatedLoading(app, form2, func() { - time.Sleep(10 * time.Second) // replace with HTTP request - }) + // Setup form2 + form2.AddFormItem(sourceTypeDropDown). + AddFormItem(sourceUserInput). + AddButton("Back", func() { + app.SetRoot(form1, true) }). AddButton("Quit", func() { app.Stop() }) + form2.SetTitle("Source Owner").SetTitleAlign(tview.AlignLeft).SetBorder(true) + sourceTypeDropDown.SetOptions([]string{"User", "Organization"}, func(option string, index int) { + // Remove both, then add back only the selected one + form2.Clear(false) // keep buttons intact + form2.AddFormItem(sourceTypeDropDown) + if index == 0 { + form2.AddFormItem(sourceUserInput) + } else { + form2.AddFormItem(sourceOrgInput) + } + }) + + beforeForm1()() if err := app.SetRoot(form1, true).EnableMouse(true).EnablePaste(true).Run(); err != nil { panic(err) @@ -113,9 +146,20 @@ func main() { os.Exit(0) - sourceApp := application.NewApplication(application.AppGitHub) - sourceApp.SetToken("") - _, err := sourceApp.GetOrganizations() + appType := application.AppGitHub + url := "https://api.github.com" + token := "" + user := "" + username := "" + password := "" + + sourceApp := application.NewApplication(appType) + sourceApp.SetToken(token) + sourceApp.SetApiUrl(url) + sourceApp.SetUser(user) + sourceApp.SetUsername(username) + sourceApp.SetPassword(password) + // Test getting organizations orgs, err := sourceApp.GetOrganizations() if err != nil { @@ -132,6 +176,18 @@ func main() { } os.Exit(1) + // Define the main arguments + cloneAndPushCmd := flag.NewFlagSet("cloneandpush", flag.ExitOnError) + + // Define flags for the cloneandpush command + sourcerepo := cloneAndPushCmd.String("sourcerepo", "", "Source repository URL to clone") + sourceusername := cloneAndPushCmd.String("sourceusername", "", "Source username for authentication") + sourcepassword := cloneAndPushCmd.String("sourcepassword", "", "Source password for authentication") + destrepo := cloneAndPushCmd.String("destrepo", "", "Destination repository URL to clone") + destusername := cloneAndPushCmd.String("destusername", "", "Destination username for authentication") + destpassword := cloneAndPushCmd.String("destpassword", "", "Destination password for authentication") + hasWiki := cloneAndPushCmd.Bool("haswiki", false, "Set to true if the repository has a wiki (default: false)") + if len(os.Args) < 2 { fmt.Println("Expected 'cloneandpush' subcommands") os.Exit(1) From 56c1fbcebc94e3c51cbaed43f96f9436190e8c86 Mon Sep 17 00:00:00 2001 From: Crayon2000 Date: Sun, 5 Oct 2025 22:39:53 -0400 Subject: [PATCH 10/25] Add UI --- cli/main.go | 41 +++++++++++++++++++++++++---------------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/cli/main.go b/cli/main.go index a1a4797..032587b 100644 --- a/cli/main.go +++ b/cli/main.go @@ -10,7 +10,7 @@ import ( "github.com/rivo/tview" ) -func showAnimatedLoading(app *tview.Application, nextForm tview.Primitive, loadFunc func()) { +func showAnimatedLoading(app *tview.Application, prevForm, nextForm tview.Primitive, loadFunc func() error) { loading := tview.NewTextView() loading.SetTextAlign(tview.AlignCenter) loading.SetTitle("Loading").SetTitleAlign(tview.AlignLeft).SetBorder(true) @@ -38,9 +38,19 @@ func showAnimatedLoading(app *tview.Application, nextForm tview.Primitive, loadF // Simulate loading go func() { - loadFunc() + err := loadFunc() close(done) // Stop animation app.QueueUpdateDraw(func() { + if err != nil { + // show modal with error and go back to prevForm + modal := tview.NewModal(). + SetText("Error: " + err.Error()). + AddButtons([]string{"OK"}).SetDoneFunc(func(buttonIndex int, buttonLabel string) { + app.SetRoot(prevForm, true) + }) + app.SetRoot(modal, true) + return + } app.SetRoot(nextForm, true) }) }() @@ -59,7 +69,7 @@ func main() { sourceAPIInput := tview.NewInputField().SetLabel("API URL:").SetFieldWidth(50) sourceAuthTokenInput := tview.NewInputField().SetLabel("Authorization Token:").SetFieldWidth(50) sourceUsernameInput := tview.NewInputField().SetLabel("Username:").SetFieldWidth(50) - sourcePasswordInput := tview.NewInputField().SetLabel("Password:").SetFieldWidth(50) + sourcePasswordInput := tview.NewInputField().SetLabel("Password:").SetFieldWidth(50).SetMaskCharacter('*') // Create form2 with controls form2 := tview.NewForm() @@ -72,9 +82,9 @@ func main() { } } - beforeForm2 := func(app *tview.Application, next tview.Primitive) func() { + beforeForm2 := func(app *tview.Application, prev, next tview.Primitive) func() { return func() { - showAnimatedLoading(app, next, func() { + showAnimatedLoading(app, prev, next, func() error { _, opt := sourceAppDropDown.GetCurrentOption() sourceApplication = application.NewApplication(application.ApplicationType(opt)) sourceApplication.SetApiUrl(sourceAPIInput.GetText()) @@ -85,21 +95,20 @@ func main() { //sourceTypeDropDown.SetCurrentOption(0) authUser, err := sourceApplication.GetAuthenticatedUser() if err != nil { - fmt.Fprintf(os.Stderr, "GetAuthenticatedUser failed: %v\n", err) - } else { - sourceUserInput.SetText(authUser) + return fmt.Errorf("GetAuthenticatedUser failed: %w", err) } + sourceUserInput.SetText(authUser) + orgs, err := sourceApplication.GetOrganizations() if err != nil { - fmt.Fprintf(os.Stderr, "GetOrganizations failed: %v\n", err) + return fmt.Errorf("GetOrganizations failed: %w", err) + } + if len(orgs) >= 1 { + // optionally populate org-related UI } else { - if len(orgs) >= 1 { - - } else { - sourceOrgInput.SetText("") - } - fmt.Printf("Organizations: %+v\n", orgs) + sourceOrgInput.SetText("") } + return nil }) } } @@ -111,7 +120,7 @@ func main() { AddFormItem(sourceAuthTokenInput). AddFormItem(sourceUsernameInput). AddFormItem(sourcePasswordInput). - AddButton("Next", beforeForm2(app, form2)). + AddButton("Next", beforeForm2(app, form1, form2)). AddButton("Quit", func() { app.Stop() }) From be8263a6a67d121fc901b1536c3863d7dd00fec2 Mon Sep 17 00:00:00 2001 From: Crayon2000 Date: Wed, 8 Oct 2025 06:20:19 -0400 Subject: [PATCH 11/25] Add UI --- cli/main.go | 59 ++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 52 insertions(+), 7 deletions(-) diff --git a/cli/main.go b/cli/main.go index 032587b..1f8955e 100644 --- a/cli/main.go +++ b/cli/main.go @@ -58,6 +58,7 @@ func showAnimatedLoading(app *tview.Application, prevForm, nextForm tview.Primit func main() { sourceApplication := application.NewApplication(application.AppGitHub) + // destApplication := application.NewApplication(application.AppGitHub) app := tview.NewApplication() @@ -73,9 +74,13 @@ func main() { // Create form2 with controls form2 := tview.NewForm() + sourceTypeDropDown := tview.NewDropDown().SetLabel("Type:") sourceUserInput := tview.NewInputField().SetLabel("User: ").SetFieldWidth(50) sourceOrgInput := tview.NewInputField().SetLabel("Organization:").SetFieldWidth(50) - sourceTypeDropDown := tview.NewDropDown().SetLabel("Type:") + sourceOrgDropDown := tview.NewDropDown().SetLabel("Organization:") + + // Create form3 with controls + form3 := tview.NewForm() beforeForm1 := func() func() { return func() { @@ -92,7 +97,6 @@ func main() { sourceApplication.SetUsername(sourceUsernameInput.GetText()) sourceApplication.SetPassword(sourcePasswordInput.GetText()) - //sourceTypeDropDown.SetCurrentOption(0) authUser, err := sourceApplication.GetAuthenticatedUser() if err != nil { return fmt.Errorf("GetAuthenticatedUser failed: %w", err) @@ -103,16 +107,42 @@ func main() { if err != nil { return fmt.Errorf("GetOrganizations failed: %w", err) } - if len(orgs) >= 1 { - // optionally populate org-related UI - } else { + orgNames := make([]string, len(orgs)) + for i, org := range orgs { + orgNames[i] = org.Name + } + switch len(orgs) { + case 0: sourceOrgInput.SetText("") + case 1: + sourceOrgInput.SetText(orgNames[0]) + default: + sourceOrgDropDown.SetOptions(orgNames, nil).SetCurrentOption(0) } return nil }) } } + beforeForm3 := func(app *tview.Application, prev, next tview.Primitive) func() { + return func() { + showAnimatedLoading(app, prev, next, func() error { + index, _ := sourceTypeDropDown.GetCurrentOption() + endpoint := application.EndpointUser + if index != 0 { + endpoint = application.EndpointOrganization + } + + _, err := sourceApplication.GetRepositories(endpoint, "", "") + if err != nil { + return fmt.Errorf("GetRepositories failed: %w", err) + } + + return nil + }) + } + } + // Setup form1 form1. AddFormItem(sourceAppDropDown). @@ -132,6 +162,7 @@ func main() { AddButton("Back", func() { app.SetRoot(form1, true) }). + AddButton("Next", beforeForm3(app, form2, form3)). AddButton("Quit", func() { app.Stop() }) @@ -143,9 +174,23 @@ func main() { if index == 0 { form2.AddFormItem(sourceUserInput) } else { - form2.AddFormItem(sourceOrgInput) + if sourceOrgDropDown.GetOptionCount() > 1 { + form2.AddFormItem(sourceOrgDropDown) + } else { + form2.AddFormItem(sourceOrgInput) + } } - }) + }).SetCurrentOption(0) + + // Setup form3 + form3. + AddButton("Back", func() { + app.SetRoot(form2, true) + }). + AddButton("Quit", func() { + app.Stop() + }) + form3.SetTitle("List of repositories").SetTitleAlign(tview.AlignLeft).SetBorder(true) beforeForm1()() From 9a8ea564b57e49315b954f3ada158a4ef8c2aac6 Mon Sep 17 00:00:00 2001 From: Crayon2000 Date: Wed, 8 Oct 2025 07:16:41 -0400 Subject: [PATCH 12/25] Add UI --- cli/main.go | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/cli/main.go b/cli/main.go index 1f8955e..a0d652c 100644 --- a/cli/main.go +++ b/cli/main.go @@ -61,6 +61,7 @@ func main() { // destApplication := application.NewApplication(application.AppGitHub) app := tview.NewApplication() + authUser := "" // Create form1 with controls form1 := tview.NewForm() @@ -81,6 +82,7 @@ func main() { // Create form3 with controls form3 := tview.NewForm() + repoList := tview.NewList() beforeForm1 := func() func() { return func() { @@ -97,7 +99,8 @@ func main() { sourceApplication.SetUsername(sourceUsernameInput.GetText()) sourceApplication.SetPassword(sourcePasswordInput.GetText()) - authUser, err := sourceApplication.GetAuthenticatedUser() + var err error + authUser, err = sourceApplication.GetAuthenticatedUser() if err != nil { return fmt.Errorf("GetAuthenticatedUser failed: %w", err) } @@ -133,11 +136,28 @@ func main() { endpoint = application.EndpointOrganization } - _, err := sourceApplication.GetRepositories(endpoint, "", "") + owner := "" + if endpoint == application.EndpointUser { + owner = sourceUserInput.GetText() + } else { + if sourceOrgDropDown.GetOptionCount() > 1 { + _, owner = sourceOrgDropDown.GetCurrentOption() + } else { + owner = sourceOrgInput.GetText() + } + } + + repositories, err := sourceApplication.GetRepositories(endpoint, owner, authUser) if err != nil { return fmt.Errorf("GetRepositories failed: %w", err) } + repoList.Clear() + for i := range repositories { + repo := repositories[i] + repoList.AddItem(repo.Name, repo.Description, 0, nil) + } + return nil }) } @@ -222,7 +242,7 @@ func main() { fmt.Printf("Organizations: %+v\n", orgs) } // Test getting authenticated user - authUser, err := sourceApp.GetAuthenticatedUser() + authUser, err = sourceApp.GetAuthenticatedUser() if err != nil { fmt.Fprintf(os.Stderr, "GetAuthenticatedUser failed: %v\n", err) } else { From 4242ebd683ea9b033af4dbde42c22a74537f75f5 Mon Sep 17 00:00:00 2001 From: Crayon2000 Date: Wed, 15 Oct 2025 22:14:20 -0400 Subject: [PATCH 13/25] Add UI --- cli/main.go | 54 ++++++++++++++++++++++++++++++++++------------------- 1 file changed, 35 insertions(+), 19 deletions(-) diff --git a/cli/main.go b/cli/main.go index a0d652c..47147c3 100644 --- a/cli/main.go +++ b/cli/main.go @@ -10,12 +10,12 @@ import ( "github.com/rivo/tview" ) -func showAnimatedLoading(app *tview.Application, prevForm, nextForm tview.Primitive, loadFunc func() error) { +func showAnimatedLoading(app *tview.Application, pages *tview.Pages, prevPage, nextPage string, loadFunc func() error) { loading := tview.NewTextView() loading.SetTextAlign(tview.AlignCenter) loading.SetTitle("Loading").SetTitleAlign(tview.AlignLeft).SetBorder(true) - app.SetRoot(loading, true) + pages.AddPage("loading", loading, true, true) done := make(chan struct{}) // Animation goroutine @@ -41,17 +41,19 @@ func showAnimatedLoading(app *tview.Application, prevForm, nextForm tview.Primit err := loadFunc() close(done) // Stop animation app.QueueUpdateDraw(func() { + pages.RemovePage("loading") if err != nil { - // show modal with error and go back to prevForm + // show modal with error and go back to prevPage when dismissed modal := tview.NewModal(). SetText("Error: " + err.Error()). AddButtons([]string{"OK"}).SetDoneFunc(func(buttonIndex int, buttonLabel string) { - app.SetRoot(prevForm, true) + pages.RemovePage("errorModal") + pages.SwitchToPage(prevPage) }) - app.SetRoot(modal, true) + pages.AddPage("errorModal", modal, true, true) return } - app.SetRoot(nextForm, true) + pages.SwitchToPage(nextPage) }) }() } @@ -60,10 +62,13 @@ func main() { sourceApplication := application.NewApplication(application.AppGitHub) // destApplication := application.NewApplication(application.AppGitHub) - app := tview.NewApplication() + pages := tview.NewPages() + app := tview.NewApplication().SetRoot(pages, true) authUser := "" // Create form1 with controls + form1Flex := tview.NewFlex().SetDirection(tview.FlexRow) + form1Flex.SetBorder(true) form1 := tview.NewForm() sourceAppDropDown := tview.NewDropDown().SetLabel("Application:"). SetOptions([]string{"Gogs", "GitBucket", "GitHub"}, nil). @@ -74,6 +79,8 @@ func main() { sourcePasswordInput := tview.NewInputField().SetLabel("Password:").SetFieldWidth(50).SetMaskCharacter('*') // Create form2 with controls + form2Flex := tview.NewFlex().SetDirection(tview.FlexRow) + form2Flex.SetBorder(true) form2 := tview.NewForm() sourceTypeDropDown := tview.NewDropDown().SetLabel("Type:") sourceUserInput := tview.NewInputField().SetLabel("User: ").SetFieldWidth(50) @@ -81,6 +88,8 @@ func main() { sourceOrgDropDown := tview.NewDropDown().SetLabel("Organization:") // Create form3 with controls + form3Flex := tview.NewFlex().SetDirection(tview.FlexRow) + form3Flex.SetBorder(true) form3 := tview.NewForm() repoList := tview.NewList() @@ -89,9 +98,9 @@ func main() { } } - beforeForm2 := func(app *tview.Application, prev, next tview.Primitive) func() { + beforeForm2 := func(app *tview.Application, prevPage, nextPage string) func() { return func() { - showAnimatedLoading(app, prev, next, func() error { + showAnimatedLoading(app, pages, prevPage, nextPage, func() error { _, opt := sourceAppDropDown.GetCurrentOption() sourceApplication = application.NewApplication(application.ApplicationType(opt)) sourceApplication.SetApiUrl(sourceAPIInput.GetText()) @@ -127,9 +136,9 @@ func main() { } } - beforeForm3 := func(app *tview.Application, prev, next tview.Primitive) func() { + beforeForm3 := func(app *tview.Application, prevPage, nextPage string) func() { return func() { - showAnimatedLoading(app, prev, next, func() error { + showAnimatedLoading(app, pages, prevPage, nextPage, func() error { index, _ := sourceTypeDropDown.GetCurrentOption() endpoint := application.EndpointUser if index != 0 { @@ -170,23 +179,27 @@ func main() { AddFormItem(sourceAuthTokenInput). AddFormItem(sourceUsernameInput). AddFormItem(sourcePasswordInput). - AddButton("Next", beforeForm2(app, form1, form2)). + AddButton("Next", beforeForm2(app, "form1", "form2")). AddButton("Quit", func() { app.Stop() }) - form1.SetTitle("Source").SetTitleAlign(tview.AlignLeft).SetBorder(true) + form1Flex.AddItem(tview.NewTextView().SetText("Source").SetTextAlign(tview.AlignCenter), 1, 0, false) + form1Flex.AddItem(form1, 0, 1, true) + pages.AddPage("form1", form1Flex, true, true) // Setup form2 form2.AddFormItem(sourceTypeDropDown). AddFormItem(sourceUserInput). AddButton("Back", func() { - app.SetRoot(form1, true) + pages.SwitchToPage("form1") }). - AddButton("Next", beforeForm3(app, form2, form3)). + AddButton("Next", beforeForm3(app, "form2", "form3")). AddButton("Quit", func() { app.Stop() }) - form2.SetTitle("Source Owner").SetTitleAlign(tview.AlignLeft).SetBorder(true) + form2Flex.AddItem(tview.NewTextView().SetText("Source Owner").SetTextAlign(tview.AlignCenter), 1, 0, false) + form2Flex.AddItem(form2, 0, 1, true) + pages.AddPage("form2", form2Flex, true, false) sourceTypeDropDown.SetOptions([]string{"User", "Organization"}, func(option string, index int) { // Remove both, then add back only the selected one form2.Clear(false) // keep buttons intact @@ -205,16 +218,19 @@ func main() { // Setup form3 form3. AddButton("Back", func() { - app.SetRoot(form2, true) + pages.SwitchToPage("form2") }). AddButton("Quit", func() { app.Stop() }) - form3.SetTitle("List of repositories").SetTitleAlign(tview.AlignLeft).SetBorder(true) + form3Flex.AddItem(tview.NewTextView().SetText("List of repositories").SetTextAlign(tview.AlignCenter), 1, 0, false) + form3Flex.AddItem(repoList, 0, 1, true) + form3Flex.AddItem(form3, 3, 0, false) + pages.AddPage("form3", form3Flex, true, false) beforeForm1()() - if err := app.SetRoot(form1, true).EnableMouse(true).EnablePaste(true).Run(); err != nil { + if err := app.EnableMouse(true).EnablePaste(true).Run(); err != nil { panic(err) } From b8fd766471b86b4b3c21f754915dc9a8a403fa96 Mon Sep 17 00:00:00 2001 From: Crayon2000 Date: Sat, 18 Oct 2025 16:56:08 -0400 Subject: [PATCH 14/25] Add UI --- cli/main.go | 71 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 69 insertions(+), 2 deletions(-) diff --git a/cli/main.go b/cli/main.go index 47147c3..bcd9603 100644 --- a/cli/main.go +++ b/cli/main.go @@ -5,11 +5,53 @@ import ( "fmt" "gitconduit-cli/application" "os" + "strings" "time" "github.com/rivo/tview" ) +// RepoItem holds a repository with its selected state and current label +type RepoItem struct { + Repo application.Repository + Selected bool + Label string +} + +// updateLabel updates the Label field of RepoItem based on its properties +func updateLabel(it *RepoItem) { + label := it.Repo.Name + if it.Repo.Private { + label += " [red:{bk}][white:red]Private[red:{bk}]" + } else { + label += " [green:{bk}][white:green]Public[green:{bk}]" + } + if it.Repo.Fork { + label += " [yellow:{bk}][white:yellow]Fork[yellow:{bk}]" + } + if it.Repo.MirrorUrl != "" { + label += " [cyan:{bk}][white:cyan]Mirror[cyan:{bk}]" + } + it.Label = label + " [-:-:-]" +} + +// updateItem updates the displayed text for a single list item at index i +func updateItem(l *tview.List, it RepoItem, i int) { + checked := "☐ " + if it.Selected { + checked = "☑ " + } + // determine whether this item is currently highlighted + current := l.GetCurrentItem() + var text string + if i == current { + text = strings.ReplaceAll(it.Label, "{bk}", "white") + } else { + text = strings.ReplaceAll(it.Label, "{bk}", l.GetBackgroundColor().Name()) + } + l.SetItemText(i, checked+text, " "+it.Repo.Description) +} + func showAnimatedLoading(app *tview.Application, pages *tview.Pages, prevPage, nextPage string, loadFunc func() error) { loading := tview.NewTextView() loading.SetTextAlign(tview.AlignCenter) @@ -91,7 +133,13 @@ func main() { form3Flex := tview.NewFlex().SetDirection(tview.FlexRow) form3Flex.SetBorder(true) form3 := tview.NewForm() + var repoItems []RepoItem repoList := tview.NewList() + repoList.SetChangedFunc(func(index int, mainText, secondaryText string, shortcut rune) { + for i := 0; i < len(repoItems); i++ { + updateItem(repoList, repoItems[i], i) + } + }) beforeForm1 := func() func() { return func() { @@ -162,10 +210,29 @@ func main() { } repoList.Clear() + repoItems = make([]RepoItem, 0, len(repositories)) for i := range repositories { - repo := repositories[i] - repoList.AddItem(repo.Name, repo.Description, 0, nil) + // default selected + item := RepoItem{Repo: repositories[i], Selected: true} + updateLabel(&item) + // capture values for closure + idx := i + repoItems = append(repoItems, item) + // add list item with initial text + repoList.AddItem("", "", 0, func() { + // toggle selection state + repoItems[idx].Selected = !repoItems[idx].Selected + updateItem(repoList, repoItems[idx], idx) + }) + updateItem(repoList, repoItems[i], i) } + // ensure focus goes to the repo list and first item is selected + app.QueueUpdateDraw(func() { + app.SetFocus(repoList) + if repoList.GetItemCount() > 0 { + repoList.SetCurrentItem(0) + } + }) return nil }) From 8e4f90f440bb8f1308133a30a7429bff3cf846e1 Mon Sep 17 00:00:00 2001 From: Crayon2000 Date: Sun, 19 Oct 2025 02:56:54 -0400 Subject: [PATCH 15/25] Add UI --- cli/main.go | 81 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/cli/main.go b/cli/main.go index bcd9603..b46815f 100644 --- a/cli/main.go +++ b/cli/main.go @@ -141,6 +141,27 @@ func main() { } }) + // Create form4 with controls + form4Flex := tview.NewFlex().SetDirection(tview.FlexRow) + form4Flex.SetBorder(true) + form4 := tview.NewForm() + destAppDropDown := tview.NewDropDown().SetLabel("Application:"). + SetOptions([]string{"Gogs", "GitBucket", "GitHub"}, nil). + SetCurrentOption(0) + destAPIInput := tview.NewInputField().SetLabel("API URL:").SetFieldWidth(50) + destAuthTokenInput := tview.NewInputField().SetLabel("Authorization Token:").SetFieldWidth(50) + destUsernameInput := tview.NewInputField().SetLabel("Username:").SetFieldWidth(50) + destPasswordInput := tview.NewInputField().SetLabel("Password:").SetFieldWidth(50).SetMaskCharacter('*') + + // Create form5 with controls + form5Flex := tview.NewFlex().SetDirection(tview.FlexRow) + form5Flex.SetBorder(true) + form5 := tview.NewForm() + destTypeDropDown := tview.NewDropDown().SetLabel("Type:") + destUserInput := tview.NewInputField().SetLabel("User: ").SetFieldWidth(50) + destOrgInput := tview.NewInputField().SetLabel("Organization:").SetFieldWidth(50) + destOrgDropDown := tview.NewDropDown().SetLabel("Organization:") + beforeForm1 := func() func() { return func() { } @@ -239,6 +260,20 @@ func main() { } } + beforeForm4 := func(_ *tview.Application, _, nextPage string) func() { + return func() { + pages.SwitchToPage(nextPage) + } + } + + beforeForm5 := func(app *tview.Application, prevPage, nextPage string) func() { + return func() { + showAnimatedLoading(app, pages, prevPage, nextPage, func() error { + return nil + }) + } + } + // Setup form1 form1. AddFormItem(sourceAppDropDown). @@ -287,6 +322,7 @@ func main() { AddButton("Back", func() { pages.SwitchToPage("form2") }). + AddButton("Next", beforeForm4(app, "form3", "form4")). AddButton("Quit", func() { app.Stop() }) @@ -295,6 +331,51 @@ func main() { form3Flex.AddItem(form3, 3, 0, false) pages.AddPage("form3", form3Flex, true, false) + // Setup form4 + form4. + AddFormItem(destAppDropDown). + AddFormItem(destAPIInput). + AddFormItem(destAuthTokenInput). + AddFormItem(destUsernameInput). + AddFormItem(destPasswordInput). + AddButton("Back", func() { + pages.SwitchToPage("form3") + }). + AddButton("Next", beforeForm5(app, "form4", "form5")). + AddButton("Quit", func() { + app.Stop() + }) + form4Flex.AddItem(tview.NewTextView().SetText("Destination").SetTextAlign(tview.AlignCenter), 1, 0, false) + form4Flex.AddItem(form4, 0, 1, true) + pages.AddPage("form4", form4Flex, true, false) + + // Setup form5 + form5.AddFormItem(destTypeDropDown). + AddFormItem(destUserInput). + AddButton("Back", func() { + pages.SwitchToPage("form4") + }). + AddButton("Quit", func() { + app.Stop() + }) + form5Flex.AddItem(tview.NewTextView().SetText("Destination Owner").SetTextAlign(tview.AlignCenter), 1, 0, false) + form5Flex.AddItem(form5, 0, 1, true) + pages.AddPage("form5", form5Flex, true, false) + destTypeDropDown.SetOptions([]string{"User", "Organization"}, func(option string, index int) { + // Remove both, then add back only the selected one + form5.Clear(false) // keep buttons intact + form5.AddFormItem(destTypeDropDown) + if index == 0 { + form5.AddFormItem(destUserInput) + } else { + if destOrgDropDown.GetOptionCount() > 1 { + form5.AddFormItem(destOrgDropDown) + } else { + form5.AddFormItem(destOrgInput) + } + } + }).SetCurrentOption(0) + beforeForm1()() if err := app.EnableMouse(true).EnablePaste(true).Run(); err != nil { From a3891b1b5b6aa86903ae42044e6c179cd1c2fcad Mon Sep 17 00:00:00 2001 From: Crayon2000 Date: Wed, 29 Oct 2025 16:04:22 -0400 Subject: [PATCH 16/25] Add Bitbucket support --- cli/application/bitbucket.go | 285 +++++++++++++++++++++++++++++++++-- cli/main.go | 2 +- 2 files changed, 275 insertions(+), 12 deletions(-) diff --git a/cli/application/bitbucket.go b/cli/application/bitbucket.go index ac34891..99a352a 100644 --- a/cli/application/bitbucket.go +++ b/cli/application/bitbucket.go @@ -1,36 +1,299 @@ package application +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "strings" +) + type BitbucketApp struct { config AppConfig } func (g *BitbucketApp) GetOrganizations() ([]Organization, error) { - // TODO: implement - return nil, nil + req, _ := http.NewRequest("GET", g.config.ApiUrl+"/2.0/workspaces", nil) + req.SetBasicAuth(g.config.Username, g.config.Password) + req.Header.Set("Accept", "application/json") + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode == http.StatusNotFound { + return nil, nil // treat as "no teams" + } + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected status: %d", resp.StatusCode) + } + + body, _ := io.ReadAll(resp.Body) + + // Bitbucket workspaces response structure + var response struct { + Values []struct { + Slug string `json:"slug"` + DisplayName string `json:"name"` + } `json:"values"` + } + + if err := json.Unmarshal(body, &response); err != nil { + return nil, err + } + + var orgs []Organization + for _, workspace := range response.Values { + org := Organization{ + Name: workspace.Slug, + Description: workspace.DisplayName, + } + orgs = append(orgs, org) + } + + return orgs, nil } func (g *BitbucketApp) GetAuthenticatedUser() (string, error) { - // TODO: implement - return "", nil + req, _ := http.NewRequest("GET", g.config.ApiUrl+"/2.0/user", nil) + req.SetBasicAuth(g.config.Username, g.config.Password) + req.Header.Set("Accept", "application/json") + resp, err := http.DefaultClient.Do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + body, _ := io.ReadAll(resp.Body) + + // Bitbucket user response structure + var data struct { + Username string `json:"username"` + Nickname string `json:"nickname"` + } + if err := json.Unmarshal(body, &data); err != nil { + return "", err + } + + // Bitbucket uses "username" field instead of "login" + if data.Username != "" { + return data.Username, nil + } + return data.Nickname, nil } func (g *BitbucketApp) GetRepositories(endpoint ApiEndpoint, owner string, authUser string) ([]Repository, error) { - // TODO: implement - return nil, nil + var url string + if endpoint == EndpointOrganization { + // For teams/organizations in Bitbucket + url = g.config.ApiUrl + "/2.0/repositories/" + owner + } else { + // For user repositories + if owner == authUser { + url = g.config.ApiUrl + "/2.0/repositories/" + authUser + } else { + url = g.config.ApiUrl + "/2.0/repositories/" + owner + } + } + + var repos []Repository + + for url != "" { + req, _ := http.NewRequest("GET", url, nil) + req.SetBasicAuth(g.config.Username, g.config.Password) + req.Header.Set("Accept", "application/json") + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + body, _ := io.ReadAll(resp.Body) + + // Bitbucket repositories response structure + var response struct { + Values []struct { + Name string `json:"name"` + FullName string `json:"full_name"` + Description string `json:"description"` + IsPrivate bool `json:"is_private"` + HasWiki bool `json:"has_wiki"` + HasIssues bool `json:"has_issues"` + Owner struct { + Username string `json:"username"` + } `json:"owner"` + Links struct { + Clone []struct { + Name string `json:"name"` + Href string `json:"href"` + } `json:"clone"` + } `json:"links"` + } `json:"values"` + Next string `json:"next"` + } + + if err := json.Unmarshal(body, &response); err != nil { + return nil, err + } + + for _, item := range response.Values { + repo := Repository{ + Name: item.Name, + FullName: item.FullName, + Description: item.Description, + Private: item.IsPrivate, + HasWiki: item.HasWiki, + HasIssues: item.HasIssues, + Owner: User{ + Login: item.Owner.Username, + }, + } + + // Find HTTPS clone URL + for _, link := range item.Links.Clone { + if link.Name == "https" { + repo.CloneUrl = link.Href + break + } + } + + repos = append(repos, repo) + } + + url = response.Next // Bitbucket uses "next" field for pagination + } + + return repos, nil } func (g *BitbucketApp) GetIssues(repo Repository) ([]Issue, error) { - // TODO: implement - return nil, nil + url := g.config.ApiUrl + "/2.0/repositories/" + repo.Owner.Login + "/" + repo.Name + "/issues" + var issues []Issue + + req, _ := http.NewRequest("GET", url, nil) + req.SetBasicAuth(g.config.Username, g.config.Password) + req.Header.Set("Accept", "application/json") + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + body, _ := io.ReadAll(resp.Body) + + // Bitbucket issues response structure + var response struct { + Values []struct { + ID int `json:"id"` + Title string `json:"title"` + Content struct { + Raw string `json:"raw"` + } `json:"content"` + State string `json:"state"` + } `json:"values"` + } + + if err := json.Unmarshal(body, &response); err != nil { + return nil, err + } + + for _, item := range response.Values { + issue := Issue{ + Number: item.ID, + Title: item.Title, + Body: item.Content.Raw, + State: item.State, + } + issues = append(issues, issue) + } + + return issues, nil } func (g *BitbucketApp) CreateRepo(endpoint ApiEndpoint, owner string, source Repository) (Repository, error) { - // TODO: implement - return Repository{}, nil + var url string + if endpoint == EndpointOrganization { + url = g.config.ApiUrl + "/2.0/repositories/" + owner + "/" + source.Name + } else { + url = g.config.ApiUrl + "/2.0/repositories/" + owner + "/" + source.Name + } + + payload := map[string]interface{}{ + "name": source.Name, + "description": source.Description, + "is_private": source.Private, + "has_wiki": source.HasWiki, + "has_issues": source.HasIssues, + } + + jsonData, err := json.Marshal(payload) + if err != nil { + return Repository{}, err + } + + req, _ := http.NewRequest("POST", url, strings.NewReader(string(jsonData))) + req.SetBasicAuth(g.config.Username, g.config.Password) + req.Header.Set("Content-Type", "application/json") + resp, err := http.DefaultClient.Do(req) + if err != nil { + return Repository{}, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusCreated { + return Repository{}, fmt.Errorf("failed to create repo: %s", resp.Status) + } + + body, _ := io.ReadAll(resp.Body) + + // Parse response + var response struct { + Name string `json:"name"` + FullName string `json:"full_name"` + Description string `json:"description"` + IsPrivate bool `json:"is_private"` + HasWiki bool `json:"has_wiki"` + HasIssues bool `json:"has_issues"` + Owner struct { + Username string `json:"username"` + } `json:"owner"` + Links struct { + Clone []struct { + Name string `json:"name"` + Href string `json:"href"` + } `json:"clone"` + } `json:"links"` + } + + if err := json.Unmarshal(body, &response); err != nil { + return Repository{}, err + } + + repo := Repository{ + Name: response.Name, + FullName: response.FullName, + Description: response.Description, + Private: response.IsPrivate, + HasWiki: response.HasWiki, + HasIssues: response.HasIssues, + Owner: User{ + Login: response.Owner.Username, + }, + } + + // Find HTTPS clone URL + for _, link := range response.Links.Clone { + if link.Name == "https" { + repo.CloneUrl = link.Href + break + } + } + + return repo, nil } func (g *BitbucketApp) GetApplicationName() string { - return "BitBucket" + return "Bitbucket" } func (g *BitbucketApp) GetApiUrl() string { diff --git a/cli/main.go b/cli/main.go index b46815f..ae36eb6 100644 --- a/cli/main.go +++ b/cli/main.go @@ -113,7 +113,7 @@ func main() { form1Flex.SetBorder(true) form1 := tview.NewForm() sourceAppDropDown := tview.NewDropDown().SetLabel("Application:"). - SetOptions([]string{"Gogs", "GitBucket", "GitHub"}, nil). + SetOptions([]string{"Gogs", "GitBucket", "GitHub", "Bitbucket"}, nil). SetCurrentOption(0) sourceAPIInput := tview.NewInputField().SetLabel("API URL:").SetFieldWidth(50) sourceAuthTokenInput := tview.NewInputField().SetLabel("Authorization Token:").SetFieldWidth(50) From 715549a7f80b2afbecae757e6ff99d9417685639 Mon Sep 17 00:00:00 2001 From: Crayon2000 Date: Wed, 29 Oct 2025 17:06:33 -0400 Subject: [PATCH 17/25] Add UI --- cli/main.go | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/cli/main.go b/cli/main.go index ae36eb6..b145cd6 100644 --- a/cli/main.go +++ b/cli/main.go @@ -101,8 +101,9 @@ func showAnimatedLoading(app *tview.Application, pages *tview.Pages, prevPage, n } func main() { + appOptions := []string{"Gogs", "GitBucket", "GitHub", "Bitbucket"} sourceApplication := application.NewApplication(application.AppGitHub) - // destApplication := application.NewApplication(application.AppGitHub) + destApplication := application.NewApplication(application.AppGitHub) pages := tview.NewPages() app := tview.NewApplication().SetRoot(pages, true) @@ -113,7 +114,7 @@ func main() { form1Flex.SetBorder(true) form1 := tview.NewForm() sourceAppDropDown := tview.NewDropDown().SetLabel("Application:"). - SetOptions([]string{"Gogs", "GitBucket", "GitHub", "Bitbucket"}, nil). + SetOptions(appOptions, nil). SetCurrentOption(0) sourceAPIInput := tview.NewInputField().SetLabel("API URL:").SetFieldWidth(50) sourceAuthTokenInput := tview.NewInputField().SetLabel("Authorization Token:").SetFieldWidth(50) @@ -146,7 +147,7 @@ func main() { form4Flex.SetBorder(true) form4 := tview.NewForm() destAppDropDown := tview.NewDropDown().SetLabel("Application:"). - SetOptions([]string{"Gogs", "GitBucket", "GitHub"}, nil). + SetOptions(appOptions, nil). SetCurrentOption(0) destAPIInput := tview.NewInputField().SetLabel("API URL:").SetFieldWidth(50) destAuthTokenInput := tview.NewInputField().SetLabel("Authorization Token:").SetFieldWidth(50) @@ -269,6 +270,29 @@ func main() { beforeForm5 := func(app *tview.Application, prevPage, nextPage string) func() { return func() { showAnimatedLoading(app, pages, prevPage, nextPage, func() error { + _, opt := destAppDropDown.GetCurrentOption() + destApplication = application.NewApplication(application.ApplicationType(opt)) + destApplication.SetApiUrl(destAPIInput.GetText()) + destApplication.SetToken(destAuthTokenInput.GetText()) + destApplication.SetUsername(destUsernameInput.GetText()) + destApplication.SetPassword(destPasswordInput.GetText()) + + orgs, err := destApplication.GetOrganizations() + if err != nil { + return fmt.Errorf("GetOrganizations failed: %w", err) + } + orgNames := make([]string, len(orgs)) + for i, org := range orgs { + orgNames[i] = org.Name + } + switch len(orgs) { + case 0: + destOrgInput.SetText("") + case 1: + destOrgInput.SetText(orgNames[0]) + default: + destOrgDropDown.SetOptions(orgNames, nil).SetCurrentOption(0) + } return nil }) } From b299ba908e5f4dc2f61e2f7070270ad3a3eff904 Mon Sep 17 00:00:00 2001 From: Crayon2000 Date: Sun, 2 Nov 2025 18:00:37 -0500 Subject: [PATCH 18/25] Add UI --- cli/application/interface.go | 2 +- cli/main.go | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/cli/application/interface.go b/cli/application/interface.go index 4038ac4..fdba58d 100644 --- a/cli/application/interface.go +++ b/cli/application/interface.go @@ -96,7 +96,7 @@ func NewApplication(appType ApplicationType) Application { case AppGitLab: return &GitLabApp{config: AppConfig{ApiUrl: "https://gitlab.com/api/v4"}} case AppBitbucket: - return &BitbucketApp{config: AppConfig{ApiUrl: "https://api.bitbucket.org/2.0"}} + return &BitbucketApp{config: AppConfig{ApiUrl: "https://api.bitbucket.org"}} default: return nil } diff --git a/cli/main.go b/cli/main.go index b145cd6..c9a0c4a 100644 --- a/cli/main.go +++ b/cli/main.go @@ -163,6 +163,10 @@ func main() { destOrgInput := tview.NewInputField().SetLabel("Organization:").SetFieldWidth(50) destOrgDropDown := tview.NewDropDown().SetLabel("Organization:") + form6Flex := tview.NewFlex().SetDirection(tview.FlexRow) + form6Flex.SetBorder(true) + form6 := tview.NewForm() + beforeForm1 := func() func() { return func() { } @@ -298,6 +302,12 @@ func main() { } } + beforeForm6 := func(_ *tview.Application, _, nextPage string) func() { + return func() { + pages.SwitchToPage(nextPage) + } + } + // Setup form1 form1. AddFormItem(sourceAppDropDown). @@ -379,6 +389,7 @@ func main() { AddButton("Back", func() { pages.SwitchToPage("form4") }). + AddButton("Next", beforeForm6(app, "form5", "form6")). AddButton("Quit", func() { app.Stop() }) @@ -400,6 +411,11 @@ func main() { } }).SetCurrentOption(0) + // Setup form6 + form6Flex.AddItem(tview.NewTextView().SetText("Creating repositories").SetTextAlign(tview.AlignCenter), 1, 0, false) + form6Flex.AddItem(form6, 0, 1, true) + pages.AddPage("form6", form6Flex, true, false) + beforeForm1()() if err := app.EnableMouse(true).EnablePaste(true).Run(); err != nil { From 6c62ccdae8cdc426705fa7e8f3fffdcc6c8ecb9f Mon Sep 17 00:00:00 2001 From: Crayon2000 Date: Sat, 22 Nov 2025 14:15:18 -0500 Subject: [PATCH 19/25] Add UI --- cli/main.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/cli/main.go b/cli/main.go index c9a0c4a..7915395 100644 --- a/cli/main.go +++ b/cli/main.go @@ -281,6 +281,13 @@ func main() { destApplication.SetUsername(destUsernameInput.GetText()) destApplication.SetPassword(destPasswordInput.GetText()) + var err error + authUser, err = destApplication.GetAuthenticatedUser() + if err != nil { + return fmt.Errorf("GetAuthenticatedUser failed: %w", err) + } + destUserInput.SetText(authUser).SetDisabled(true) + orgs, err := destApplication.GetOrganizations() if err != nil { return fmt.Errorf("GetOrganizations failed: %w", err) From 6fe49d49681c980f4ef650e3dda386bdabc6d4cc Mon Sep 17 00:00:00 2001 From: Crayon2000 Date: Sun, 23 Nov 2025 14:23:11 -0500 Subject: [PATCH 20/25] Add UI --- cli/main.go | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/cli/main.go b/cli/main.go index 7915395..a08e4cf 100644 --- a/cli/main.go +++ b/cli/main.go @@ -163,9 +163,18 @@ func main() { destOrgInput := tview.NewInputField().SetLabel("Organization:").SetFieldWidth(50) destOrgDropDown := tview.NewDropDown().SetLabel("Organization:") + // Create form6 with controls form6Flex := tview.NewFlex().SetDirection(tview.FlexRow) form6Flex.SetBorder(true) form6 := tview.NewForm() + outputView := tview.NewTextView() + outputView.SetDynamicColors(true) + outputView.SetWrap(true) + outputView.SetScrollable(true) + outputView.SetBorder(true) + outputView.SetTitle("Log") + outputView.SetTitleAlign(tview.AlignLeft) + outputView.SetText("") beforeForm1 := func() func() { return func() { @@ -419,8 +428,16 @@ func main() { }).SetCurrentOption(0) // Setup form6 + form6. + AddButton("Back", func() { + pages.SwitchToPage("form5") + }). + AddButton("Quit", func() { + app.Stop() + }) form6Flex.AddItem(tview.NewTextView().SetText("Creating repositories").SetTextAlign(tview.AlignCenter), 1, 0, false) - form6Flex.AddItem(form6, 0, 1, true) + form6Flex.AddItem(outputView, 0, 1, true) + form6Flex.AddItem(form6, 3, 0, false) pages.AddPage("form6", form6Flex, true, false) beforeForm1()() From 3da46455bc10302f0938c4beb5eaac18d36531d6 Mon Sep 17 00:00:00 2001 From: Crayon2000 Date: Mon, 8 Dec 2025 16:46:59 -0500 Subject: [PATCH 21/25] Append text to Log --- cli/main.go | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/cli/main.go b/cli/main.go index a08e4cf..8e87587 100644 --- a/cli/main.go +++ b/cli/main.go @@ -174,7 +174,6 @@ func main() { outputView.SetBorder(true) outputView.SetTitle("Log") outputView.SetTitleAlign(tview.AlignLeft) - outputView.SetText("") beforeForm1 := func() func() { return func() { @@ -318,9 +317,31 @@ func main() { } } - beforeForm6 := func(_ *tview.Application, _, nextPage string) func() { + beforeForm6 := func(app *tview.Application, _, nextPage string) func() { return func() { + outputView.SetText("") pages.SwitchToPage(nextPage) + go func() { + for _, item := range repoItems { + if !item.Selected { + continue + } + repoFullName := item.Repo.FullName + app.QueueUpdateDraw(func(repoFullName string) func() { + return func() { + _, _ = fmt.Fprintf(outputView, "====== %s ======\n", repoFullName) + } + }(repoFullName)) + outputView.ScrollToEnd() + + // Simulate work + time.Sleep(1 * time.Second) + } + app.QueueUpdateDraw(func() { + _, _ = outputView.Write([]byte("\n\nCompleted!")) + }) + outputView.ScrollToEnd() + }() } } From 0dfdc5102e1a6f15d726647c615eb9415b1ab812 Mon Sep 17 00:00:00 2001 From: Crayon2000 Date: Wed, 10 Dec 2025 13:38:03 -0500 Subject: [PATCH 22/25] Fix Bitbucket repository creation --- cli/application/bitbucket.go | 31 +++++++++++++++++++++---------- cli/main.go | 26 ++++++++++++++++++++++++-- 2 files changed, 45 insertions(+), 12 deletions(-) diff --git a/cli/application/bitbucket.go b/cli/application/bitbucket.go index 99a352a..4280391 100644 --- a/cli/application/bitbucket.go +++ b/cli/application/bitbucket.go @@ -12,6 +12,20 @@ type BitbucketApp struct { config AppConfig } +// Slugify a string. +func slugify(s string) string { + s = strings.ToLower(s) + s = strings.ReplaceAll(s, " ", "-") + var result strings.Builder + result.Grow(len(s)) + for _, r := range s { + if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '_' || r == '-' || r == '.' { + result.WriteRune(r) + } + } + return result.String() +} + func (g *BitbucketApp) GetOrganizations() ([]Organization, error) { req, _ := http.NewRequest("GET", g.config.ApiUrl+"/2.0/workspaces", nil) req.SetBasicAuth(g.config.Username, g.config.Password) @@ -211,14 +225,10 @@ func (g *BitbucketApp) GetIssues(repo Repository) ([]Issue, error) { } func (g *BitbucketApp) CreateRepo(endpoint ApiEndpoint, owner string, source Repository) (Repository, error) { - var url string - if endpoint == EndpointOrganization { - url = g.config.ApiUrl + "/2.0/repositories/" + owner + "/" + source.Name - } else { - url = g.config.ApiUrl + "/2.0/repositories/" + owner + "/" + source.Name - } + url := g.config.ApiUrl + "/2.0/repositories/" + owner + "/" + slugify(source.Name) - payload := map[string]interface{}{ + payload := map[string]any{ + "scm": "git", "name": source.Name, "description": source.Description, "is_private": source.Private, @@ -234,18 +244,19 @@ func (g *BitbucketApp) CreateRepo(endpoint ApiEndpoint, owner string, source Rep req, _ := http.NewRequest("POST", url, strings.NewReader(string(jsonData))) req.SetBasicAuth(g.config.Username, g.config.Password) req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "application/json") resp, err := http.DefaultClient.Do(req) if err != nil { return Repository{}, err } defer resp.Body.Close() - if resp.StatusCode != http.StatusCreated { + body, _ := io.ReadAll(resp.Body) + + if resp.StatusCode != http.StatusOK { return Repository{}, fmt.Errorf("failed to create repo: %s", resp.Status) } - body, _ := io.ReadAll(resp.Body) - // Parse response var response struct { Name string `json:"name"` diff --git a/cli/main.go b/cli/main.go index 8e87587..5553364 100644 --- a/cli/main.go +++ b/cli/main.go @@ -322,6 +322,23 @@ func main() { outputView.SetText("") pages.SwitchToPage(nextPage) go func() { + index, _ := destTypeDropDown.GetCurrentOption() + endpoint := application.EndpointUser + if index != 0 { + endpoint = application.EndpointOrganization + } + + owner := "" + if endpoint == application.EndpointUser { + owner = destUserInput.GetText() + } else { + if destOrgDropDown.GetOptionCount() > 1 { + _, owner = destOrgDropDown.GetCurrentOption() + } else { + owner = destOrgInput.GetText() + } + } + for _, item := range repoItems { if !item.Selected { continue @@ -334,8 +351,13 @@ func main() { }(repoFullName)) outputView.ScrollToEnd() - // Simulate work - time.Sleep(1 * time.Second) + _, err := destApplication.CreateRepo(endpoint, owner, item.Repo) + if err != nil { + app.QueueUpdateDraw(func() { + _, _ = outputView.Write([]byte("Repository creation failed: " + err.Error() + "\n")) + }) + continue + } } app.QueueUpdateDraw(func() { _, _ = outputView.Write([]byte("\n\nCompleted!")) From 5748bf9c847bd1bf947830578e24a21dda21e4a8 Mon Sep 17 00:00:00 2001 From: Crayon2000 Date: Wed, 10 Dec 2025 20:57:21 -0500 Subject: [PATCH 23/25] Use types for Bitbucket JSON --- cli/application/bitbucket.go | 95 +++++++++++++++++++----------------- 1 file changed, 49 insertions(+), 46 deletions(-) diff --git a/cli/application/bitbucket.go b/cli/application/bitbucket.go index 4280391..c71bb03 100644 --- a/cli/application/bitbucket.go +++ b/cli/application/bitbucket.go @@ -12,6 +12,40 @@ type BitbucketApp struct { config AppConfig } +// LinkPayload represents the JSON payload for a link on Bitbucket. +type LinkPayload struct { + Href string `json:"href"` + Name string `json:"name"` +} + +// RepositoryPayload represents the JSON payload for a repository on Bitbucket. +type RepositoryPayload struct { + Scm string `json:"scm"` + Name string `json:"name"` + FullName string `json:"full_name,omitempty"` + Description string `json:"description"` + IsPrivate bool `json:"is_private"` + HasWiki bool `json:"has_wiki"` + HasIssues bool `json:"has_issues"` + Owner struct { + DisplayName string `json:"display_name"` + UUID string `json:"uuid"` + } `json:"owner"` + Links struct { + Self LinkPayload `json:"self"` + Clone []LinkPayload `json:"clone"` + } `json:"links"` +} + +// RepositoryListPayload represents the JSON payload for a repository list on Bitbucket. +type RepositoryListPayload struct { + Page int `json:"page"` + PageLen int `json:"pagelen"` + Next string `json:"next"` + Previous string `json:"previous"` + Values []RepositoryPayload `json:"values"` +} + // Slugify a string. func slugify(s string) string { s = strings.ToLower(s) @@ -124,28 +158,13 @@ func (g *BitbucketApp) GetRepositories(endpoint ApiEndpoint, owner string, authU body, _ := io.ReadAll(resp.Body) - // Bitbucket repositories response structure - var response struct { - Values []struct { - Name string `json:"name"` - FullName string `json:"full_name"` - Description string `json:"description"` - IsPrivate bool `json:"is_private"` - HasWiki bool `json:"has_wiki"` - HasIssues bool `json:"has_issues"` - Owner struct { - Username string `json:"username"` - } `json:"owner"` - Links struct { - Clone []struct { - Name string `json:"name"` - Href string `json:"href"` - } `json:"clone"` - } `json:"links"` - } `json:"values"` - Next string `json:"next"` + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("failed to get repositories: %s", resp.Status) } + // Bitbucket repositories response structure + var response RepositoryListPayload + if err := json.Unmarshal(body, &response); err != nil { return nil, err } @@ -159,7 +178,7 @@ func (g *BitbucketApp) GetRepositories(endpoint ApiEndpoint, owner string, authU HasWiki: item.HasWiki, HasIssues: item.HasIssues, Owner: User{ - Login: item.Owner.Username, + Login: item.Owner.DisplayName, }, } @@ -227,13 +246,13 @@ func (g *BitbucketApp) GetIssues(repo Repository) ([]Issue, error) { func (g *BitbucketApp) CreateRepo(endpoint ApiEndpoint, owner string, source Repository) (Repository, error) { url := g.config.ApiUrl + "/2.0/repositories/" + owner + "/" + slugify(source.Name) - payload := map[string]any{ - "scm": "git", - "name": source.Name, - "description": source.Description, - "is_private": source.Private, - "has_wiki": source.HasWiki, - "has_issues": source.HasIssues, + payload := RepositoryPayload{ + Scm: "git", + Name: source.Name, + Description: source.Description, + IsPrivate: source.Private, + HasWiki: source.HasWiki, + HasIssues: source.HasIssues, } jsonData, err := json.Marshal(payload) @@ -258,23 +277,7 @@ func (g *BitbucketApp) CreateRepo(endpoint ApiEndpoint, owner string, source Rep } // Parse response - var response struct { - Name string `json:"name"` - FullName string `json:"full_name"` - Description string `json:"description"` - IsPrivate bool `json:"is_private"` - HasWiki bool `json:"has_wiki"` - HasIssues bool `json:"has_issues"` - Owner struct { - Username string `json:"username"` - } `json:"owner"` - Links struct { - Clone []struct { - Name string `json:"name"` - Href string `json:"href"` - } `json:"clone"` - } `json:"links"` - } + var response RepositoryPayload if err := json.Unmarshal(body, &response); err != nil { return Repository{}, err @@ -288,7 +291,7 @@ func (g *BitbucketApp) CreateRepo(endpoint ApiEndpoint, owner string, source Rep HasWiki: response.HasWiki, HasIssues: response.HasIssues, Owner: User{ - Login: response.Owner.Username, + Login: response.Owner.DisplayName, }, } From a22782790b0bc0342c672f4d81461952705c1b85 Mon Sep 17 00:00:00 2001 From: Crayon2000 Date: Mon, 22 Dec 2025 12:47:12 -0500 Subject: [PATCH 24/25] Return error in source_control.go --- cli/main.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cli/main.go b/cli/main.go index 5553364..54efa94 100644 --- a/cli/main.go +++ b/cli/main.go @@ -356,6 +356,7 @@ func main() { app.QueueUpdateDraw(func() { _, _ = outputView.Write([]byte("Repository creation failed: " + err.Error() + "\n")) }) + outputView.ScrollToEnd() continue } } From dd2a930a7cc28b66db8a6eaf47d8f6944edcd9f4 Mon Sep 17 00:00:00 2001 From: Crayon2000 Date: Mon, 22 Dec 2025 13:31:58 -0500 Subject: [PATCH 25/25] Add UI --- cli/go.mod | 11 ++++++++++- cli/go.sum | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/cli/go.mod b/cli/go.mod index 98dbec8..3cccb34 100644 --- a/cli/go.mod +++ b/cli/go.mod @@ -2,7 +2,10 @@ module gitconduit-cli go 1.25 -require github.com/go-git/go-git/v5 v5.16.4 +require ( + github.com/go-git/go-git/v5 v5.16.4 + github.com/rivo/tview v0.42.0 +) require ( dario.cat/mergo v1.0.2 // indirect @@ -11,18 +14,24 @@ require ( github.com/cloudflare/circl v1.6.1 // indirect github.com/cyphar/filepath-securejoin v0.6.1 // indirect github.com/emirpasic/gods v1.18.1 // indirect + github.com/gdamore/encoding v1.0.1 // indirect + github.com/gdamore/tcell/v2 v2.13.5 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/go-billy/v5 v5.7.0 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/kevinburke/ssh_config v1.4.0 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/lucasb-eyer/go-colorful v1.3.0 // indirect github.com/pjbgf/sha1cd v0.5.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect github.com/sergi/go-diff v1.4.0 // indirect github.com/skeema/knownhosts v1.3.2 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect golang.org/x/crypto v0.46.0 // indirect golang.org/x/net v0.48.0 // indirect golang.org/x/sys v0.39.0 // indirect + golang.org/x/term v0.38.0 // indirect + golang.org/x/text v0.32.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect ) diff --git a/cli/go.sum b/cli/go.sum index b1b9905..562b7ee 100644 --- a/cli/go.sum +++ b/cli/go.sum @@ -20,6 +20,10 @@ github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= +github.com/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uhw= +github.com/gdamore/encoding v1.0.1/go.mod h1:0Z0cMFinngz9kS1QfMjCP8TY7em3bZYeeklsSDPivEo= +github.com/gdamore/tcell/v2 v2.13.5 h1:YvWYCSr6gr2Ovs84dXbZLjDuOfQchhj8buOEqY52rpA= +github.com/gdamore/tcell/v2 v2.13.5/go.mod h1:+Wfe208WDdB7INEtCsNrAN6O2m+wsTPk1RAovjaILlo= github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= @@ -47,6 +51,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= +github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= github.com/pjbgf/sha1cd v0.5.0 h1:a+UkboSi1znleCDUNT3M5YxjOnN1fz2FhN48FlwCxs0= @@ -55,6 +61,10 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/tview v0.42.0 h1:b/ftp+RxtDsHSaynXTbJb+/n/BxDEi+W3UfF5jILK6c= +github.com/rivo/tview v0.42.0/go.mod h1:cSfIYfhpSGCjp3r/ECJb+GKS7cGJnqV8vfjQPwoXyfY= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= @@ -69,29 +79,56 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=