From 85de81e6b1904b7ae59805c243e0a83bf988527e Mon Sep 17 00:00:00 2001 From: Crayon2000 Date: Mon, 8 Sep 2025 09:31:25 -0400 Subject: [PATCH 01/18] 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 fc051cc..9506684 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 7770ef11ab8166b76c1265239d23548fa4b58c70 Mon Sep 17 00:00:00 2001 From: Crayon2000 Date: Sun, 14 Sep 2025 17:34:55 -0400 Subject: [PATCH 02/18] 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 9506684..4dd6bac 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 238844dc7e6e90ffec819136acdc85d33e06f0f0 Mon Sep 17 00:00:00 2001 From: Crayon2000 Date: Sun, 14 Sep 2025 23:07:58 -0400 Subject: [PATCH 03/18] 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 4dd6bac..1dd6575 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 0b82ac469fd4509e606d8947f95c4e9b45f7a369 Mon Sep 17 00:00:00 2001 From: Crayon2000 Date: Mon, 15 Sep 2025 15:35:48 -0400 Subject: [PATCH 04/18] 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 71a93c36f943d7dc97fbf681b156f7de344301da Mon Sep 17 00:00:00 2001 From: Crayon2000 Date: Mon, 15 Sep 2025 19:41:20 -0400 Subject: [PATCH 05/18] 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 1ad0c8d28cc718d0fd6dbc1db3c85f206facc87d Mon Sep 17 00:00:00 2001 From: Crayon2000 Date: Tue, 23 Sep 2025 01:10:17 -0400 Subject: [PATCH 06/18] 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 1dd6575..fd35a2c 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 91f6f5287ce3d8a316493b96648c9031b227827e Mon Sep 17 00:00:00 2001 From: Crayon2000 Date: Sun, 28 Sep 2025 23:44:16 -0400 Subject: [PATCH 07/18] Fix GitBucket and Gogs --- cli/application/common.go | 16 +++++++++++----- cli/application/interface.go | 1 + cli/source_control.go | 4 ++++ 3 files changed, 16 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: diff --git a/cli/source_control.go b/cli/source_control.go index 0acae89..8be7c1c 100644 --- a/cli/source_control.go +++ b/cli/source_control.go @@ -10,6 +10,7 @@ import ( gitHttp "github.com/go-git/go-git/v5/plumbing/transport/http" ) +// cloneRepository clones a repository to a local directory. func cloneRepository(repo, directory, username, password string) { var err error cloneOptions := &git.CloneOptions{ @@ -32,6 +33,7 @@ func cloneRepository(repo, directory, username, password string) { fmt.Println("Repository cloned successfully.") } +// addRemote adds a new remote to the local repository. func addRemote(directory, repo string) { r, err := git.PlainOpen(directory) if err != nil { @@ -50,6 +52,7 @@ func addRemote(directory, repo string) { fmt.Println("Remote added successfully.") } +// pushToRemote pushes the local repository to the specified remote. func pushToRemote(directory, username, password string) { r, err := git.PlainOpen(directory) if err != nil { @@ -82,6 +85,7 @@ func pushToRemote(directory, username, password string) { fmt.Println("Pushed to remote successfully.") } +// CloneAndPush clones a repository from the source URL and pushes it to the destination URL. func CloneAndPush(sourcerepo, sourceusername, sourcepassword, destrepo, destusername, destpassword string, hasWiki bool) { cnpdirectory := "temp.git" From a875558640b8a26ad496b0acb4b16350c9817c21 Mon Sep 17 00:00:00 2001 From: Crayon2000 Date: Sat, 4 Oct 2025 14:53:17 -0400 Subject: [PATCH 08/18] Add UI --- cli/go.mod | 12 ++++++- cli/go.sum | 73 +++++++++++++++++++++++++++++++++++++ cli/main.go | 102 +++++++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 185 insertions(+), 2 deletions(-) diff --git a/cli/go.mod b/cli/go.mod index a61bd9f..4199004 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.0 +require ( + github.com/go-git/go-git/v5 v5.16.0 + github.com/rivo/tview v0.42.0 +) require ( dario.cat/mergo v1.0.0 // indirect @@ -11,17 +14,24 @@ require ( github.com/cloudflare/circl v1.6.1 // indirect github.com/cyphar/filepath-securejoin v0.4.1 // indirect github.com/emirpasic/gods v1.18.1 // indirect + github.com/gdamore/encoding v1.0.1 // indirect + github.com/gdamore/tcell/v2 v2.8.1 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/go-billy/v5 v5.6.2 // 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.2.0 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect github.com/pjbgf/sha1cd v0.3.2 // indirect + github.com/rivo/uniseg v0.4.7 // indirect github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect github.com/skeema/knownhosts v1.3.1 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect golang.org/x/crypto v0.37.0 // indirect golang.org/x/net v0.39.0 // indirect golang.org/x/sys v0.32.0 // indirect + golang.org/x/term v0.31.0 // indirect + golang.org/x/text v0.24.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect ) diff --git a/cli/go.sum b/cli/go.sum index b92c8fc..cd7a267 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.8.1 h1:KPNxyqclpWpWQlPLx6Xui1pMk8S+7+R37h3g07997NU= +github.com/gdamore/tcell/v2 v2.8.1/go.mod h1:bj8ori1BG3OYMjmb3IklZVWfZUJ1UBQt9JXrOCOhGWw= 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= @@ -32,6 +36,7 @@ github.com/go-git/go-git/v5 v5.16.0 h1:k3kuOEpkc0DeY7xlL6NaaNg39xdgQbtH5mwCafHO9 github.com/go-git/go-git/v5 v5.16.0/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= @@ -45,6 +50,10 @@ 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.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 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.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4= @@ -53,6 +62,12 @@ 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.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +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.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= @@ -67,29 +82,87 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 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.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= 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/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +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.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= +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/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +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.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= 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.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= +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.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= 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/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +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= diff --git a/cli/main.go b/cli/main.go index fd35a2c..5400801 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 2b5062645d432f54336edc11748541b4dc2f3ae4 Mon Sep 17 00:00:00 2001 From: Crayon2000 Date: Sun, 5 Oct 2025 01:48:49 -0400 Subject: [PATCH 09/18] Add UI --- cli/main.go | 150 ++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 103 insertions(+), 47 deletions(-) diff --git a/cli/main.go b/cli/main.go index 5400801..b02dc21 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 1105f1d40e26fefae7e62959db953053fa119ad1 Mon Sep 17 00:00:00 2001 From: Crayon2000 Date: Sun, 5 Oct 2025 22:39:53 -0400 Subject: [PATCH 10/18] Add UI --- cli/main.go | 41 +++++++++++++++++++++++++---------------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/cli/main.go b/cli/main.go index b02dc21..512e97f 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 8f2f8d5665359256997b7c60b104d5a12e796daa Mon Sep 17 00:00:00 2001 From: Crayon2000 Date: Wed, 8 Oct 2025 06:20:19 -0400 Subject: [PATCH 11/18] Add UI --- cli/main.go | 59 ++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 52 insertions(+), 7 deletions(-) diff --git a/cli/main.go b/cli/main.go index 512e97f..e232be6 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 ee45ce9e3db515605d713cdb6357e8d2540ae36a Mon Sep 17 00:00:00 2001 From: Crayon2000 Date: Wed, 8 Oct 2025 07:16:41 -0400 Subject: [PATCH 12/18] Add UI --- cli/main.go | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/cli/main.go b/cli/main.go index e232be6..4501267 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 e5100f9265aceef3d6ae61b328baf3207c6878c4 Mon Sep 17 00:00:00 2001 From: Crayon2000 Date: Wed, 15 Oct 2025 22:14:20 -0400 Subject: [PATCH 13/18] Add UI --- cli/main.go | 54 ++++++++++++++++++++++++++++++++++------------------- 1 file changed, 35 insertions(+), 19 deletions(-) diff --git a/cli/main.go b/cli/main.go index 4501267..43abc3c 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 bdbdc1a3d10c956a77643a73dfacc9c29cdd30c1 Mon Sep 17 00:00:00 2001 From: Crayon2000 Date: Sat, 18 Oct 2025 16:56:08 -0400 Subject: [PATCH 14/18] Add UI --- cli/main.go | 71 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 69 insertions(+), 2 deletions(-) diff --git a/cli/main.go b/cli/main.go index 43abc3c..75d7f57 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 d7ffab34c3a16d6d848ca36bdbfd790d70880eb3 Mon Sep 17 00:00:00 2001 From: Crayon2000 Date: Sun, 19 Oct 2025 02:56:54 -0400 Subject: [PATCH 15/18] Add UI --- cli/main.go | 81 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/cli/main.go b/cli/main.go index 75d7f57..c5b4960 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 44917b74e426f81a93e4aa63c578afb3e131138c Mon Sep 17 00:00:00 2001 From: Crayon2000 Date: Wed, 29 Oct 2025 16:04:22 -0400 Subject: [PATCH 16/18] 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 c5b4960..07ce9b6 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 c8105c15a1c77586528ea91230b7bab7822eb71b Mon Sep 17 00:00:00 2001 From: Crayon2000 Date: Wed, 29 Oct 2025 17:06:33 -0400 Subject: [PATCH 17/18] Add UI --- cli/main.go | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/cli/main.go b/cli/main.go index 07ce9b6..39f0b64 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 c7fc19678abfc8b73c3c10c35a4c60e93546dc6f Mon Sep 17 00:00:00 2001 From: Crayon2000 Date: Sun, 2 Nov 2025 18:00:37 -0500 Subject: [PATCH 18/18] Add UI --- .gitignore | 3 +++ cli/.devcontainer/devcontainer.json | 2 +- cli/application/interface.go | 2 +- cli/main.go | 16 ++++++++++++++++ 4 files changed, 21 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 5f147f3..9a38ccb 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,9 @@ [Dd]ebug/ [Rr]elease/ +# This project +temp.git/ + # Compiled resource file *.res diff --git a/cli/.devcontainer/devcontainer.json b/cli/.devcontainer/devcontainer.json index cb0ef95..1dc0565 100644 --- a/cli/.devcontainer/devcontainer.json +++ b/cli/.devcontainer/devcontainer.json @@ -1,6 +1,6 @@ { "name": "golang", - "image": "golang:1.25.1", + "image": "golang:1.25.3", "customizations": { "vscode": { "extensions": ["golang.go"] 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 39f0b64..34024b9 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 {