From d3a068fe2b389768dce0777b59256bcfe7170c65 Mon Sep 17 00:00:00 2001 From: "Urko." Date: Fri, 30 Aug 2024 13:45:18 +0200 Subject: [PATCH] initial commit --- .gitignore | 2 + README.md | 89 ++++++++++++++++++ app.example.yml | 12 +++ cmd/clean/main.go | 160 ++++++++++++++++++++++++++++++++ cmd/collaborator/main.go | 143 ++++++++++++++++++++++++++++ cmd/comments/main.go | 130 ++++++++++++++++++++++++++ cmd/issue/main.go | 156 +++++++++++++++++++++++++++++++ cmd/org/main.go | 137 +++++++++++++++++++++++++++ cmd/users/main.go | 149 +++++++++++++++++++++++++++++ go.mod | 10 ++ go.sum | 16 ++++ internal/domain/collaborator.go | 5 + internal/domain/comment.go | 5 + internal/domain/issue.go | 15 +++ internal/domain/org.go | 12 +++ internal/domain/user.go | 42 +++++++++ kit/config/config.go | 50 ++++++++++ 17 files changed, 1133 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 app.example.yml create mode 100644 cmd/clean/main.go create mode 100644 cmd/collaborator/main.go create mode 100644 cmd/comments/main.go create mode 100644 cmd/issue/main.go create mode 100644 cmd/org/main.go create mode 100644 cmd/users/main.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/domain/collaborator.go create mode 100644 internal/domain/comment.go create mode 100644 internal/domain/issue.go create mode 100644 internal/domain/org.go create mode 100644 internal/domain/user.go create mode 100644 kit/config/config.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e08427d --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.notes +app.yml \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..577434a --- /dev/null +++ b/README.md @@ -0,0 +1,89 @@ +# gogstea + +## migration from gogs to gitea with less pain + +High level operation: + +The main idea is to do a copy. The script will take the users from csv data from gogs, previously obtained, and will create gitea users interacting with gitea API + +```go +source := gogs.mysql --> select * from user; --> csv + +dst:= gitea.postgres --> API /admin gitea --> go script read from csv and http post to gitea api +copy(dst, source) +``` + +### users migration + +Export results of this query to .csv to migrate the users + +```sql +SELECT * FROM user +WHERE email !=''; +``` + +**Execution:** + +```sh +CONFIG_FILE=./app.yml go run ./cmd/users/main.go +``` + +### orgs migration + +Export results of this query to .csv to migrate the users + +```sql +SELECT + u.name, + ou.org_id, + uo.name AS owner_name, + uo.email AS owner_email +FROM org_user ou +INNER JOIN user u ON ou.org_id = u.id +INNER JOIN user uo ON uo.id = ou.is_owner +``` + +**Execution:** + +```sh +CONFIG_FILE=./app.yml go run ./cmd/org/main.go +``` + +### repositories migration + +You have to options: You can use the API to import them or like I did: manually import using the `unadopted` button from the UI. + +**Execution:** + +```sh +CONFIG_FILE=./app.yml go run ./cmd/org/main.go +``` + +### collaborators migration + +Export results of this query to .csv to migrate the repositories + +```sql +SELECT + r.name AS repo_name, + uo.name AS repo_owner, + u.name AS collaborator_user, + co.mode, + CASE + WHEN co.mode = 1 THEN 'read' + WHEN co.mode = 2 THEN 'write' + WHEN co.mode = 3 THEN 'admin' + WHEN co.mode = 4 THEN 'owner' + ELSE 'failed' + END AS permission +FROM collaboration co +INNER JOIN repository r ON r.id = co.repo_id +INNER JOIN user uo ON uo.id = r.owner_id +INNER JOIN user u ON u.id = co.user_id; +``` + +**Execution:** + +```sh +CONFIG_FILE=./app.yml go run ./cmd/collaborator/main.go +``` diff --git a/app.example.yml b/app.example.yml new file mode 100644 index 0000000..a3d214f --- /dev/null +++ b/app.example.yml @@ -0,0 +1,12 @@ +gitea: + url: "https://gitea.com/api/v1" + api_key: "YOUR-API-KEY-HERE" +users: + default_password: "SOME-STRONG-PASSS" + csv_path: "/home/user/gogstea/csv/user.csv" +organizations: + csv_path: "/home/user/gogstea/csv/organizations.csv" +issues: + csv_path: "/home/user/gogstea/csv/issues.csv" +collaborators: + csv_path: "/home/user/gogstea/csv/collaborators.csv" \ No newline at end of file diff --git a/cmd/clean/main.go b/cmd/clean/main.go new file mode 100644 index 0000000..6905a1e --- /dev/null +++ b/cmd/clean/main.go @@ -0,0 +1,160 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "os" + "os/signal" + "path" + "runtime" + "syscall" + "time" + + "gitea.urkob.com/mcr-swiss/gogstea/kit/config" +) + +type user struct { + ID int `json:"id"` + Login string `json:"login"` + LoginName string `json:"login_name"` + SourceID int `json:"source_id"` + FullName string `json:"full_name"` + Email string `json:"email"` + AvatarURL string `json:"avatar_url"` + HTMLURL string `json:"html_url"` + Language string `json:"language"` + IsAdmin bool `json:"is_admin"` + LastLogin time.Time `json:"last_login"` + Created time.Time `json:"created"` + Restricted bool `json:"restricted"` + Active bool `json:"active"` + ProhibitLogin bool `json:"prohibit_login"` + Location string `json:"location"` + Website string `json:"website"` + Description string `json:"description"` + Visibility string `json:"visibility"` + FollowersCount int `json:"followers_count"` + FollowingCount int `json:"following_count"` + StarredReposCount int `json:"starred_repos_count"` + Username string `json:"username"` +} + +func main() { + ctx, cancel := context.WithCancel(signalContext(context.Background())) + defer cancel() + + cfgFile := os.Getenv("CONFIG_FILE") + if cfgFile == "" { + // Get root path + _, filename, _, _ := runtime.Caller(0) + cfgFile = path.Join(path.Dir(filename), "configs", "app.yml") + } + cfg, err := config.LoadConfig(cfgFile) + if err != nil { + panic(err) + } + + wd, err := os.Getwd() + if err != nil { + panic(err) + } + + outputFile, err := os.Create(wd + "/output.txt") + if err != nil { + panic(err) + } + defer outputFile.Close() + + cli := http.DefaultClient + + parsedURL, err := url.Parse(fmt.Sprintf(cfg.Gitea.URL + "/admin/users")) + if err != nil { + panic(err) + } + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, parsedURL.String(), http.NoBody) + if err != nil { + panic(err) + } + + // TODO: see how to pass the admin token if so + req.Header.Add("Authorization", "token "+cfg.Gitea.ApiKey) + req.Header.Add("Content-Type", "application/json") + resp, err := cli.Do(req) + if err != nil { + panic(err) + } + + if resp.StatusCode != http.StatusOK { + bts, _ := io.ReadAll(resp.Body) + + panic(fmt.Errorf("%d | %s: %w", resp.StatusCode, string(bts), err)) + } + + bts, err := io.ReadAll(resp.Body) + if err != nil { + panic(err) + } + + var users []user + if err := json.Unmarshal(bts, &users); err != nil { + panic(err) + } + + for _, u := range users { + if u.Username == "mcradmin" { + continue + } + + // Delete + parsedURL, err := url.Parse(fmt.Sprintf("%s/admin/users/%s", cfg.Gitea.URL, u.Username)) + if err != nil { + panic(err) + } + + req, err := http.NewRequestWithContext(ctx, http.MethodDelete, parsedURL.String(), http.NoBody) + if err != nil { + panic(err) + } + + req.Header.Add("Authorization", "token "+cfg.Gitea.ApiKey) + req.Header.Add("Content-Type", "application/json") + resp, err := cli.Do(req) + if err != nil { + panic(err) + } + + if resp.StatusCode != http.StatusOK { + bts, _ := io.ReadAll(resp.Body) + + if _, err := outputFile.WriteString(fmt.Sprintf("FAILED %d | %s | %d: %s\n", u.ID, u.Email, resp.StatusCode, string(bts))); err != nil { + panic(err) + } + continue + } + + if _, err := outputFile.WriteString(fmt.Sprintf("DELETED %d | %s\n", u.ID, u.Email)); err != nil { + panic(err) + } + } +} + +func signalContext(ctx context.Context) context.Context { + ctx, cancel := context.WithCancel(ctx) + + sigs := make(chan os.Signal, 1) + signal.Notify(sigs, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + go func() { + <-sigs + signal.Stop(sigs) + close(sigs) + cancel() + }() + + return ctx +} diff --git a/cmd/collaborator/main.go b/cmd/collaborator/main.go new file mode 100644 index 0000000..5ed5af0 --- /dev/null +++ b/cmd/collaborator/main.go @@ -0,0 +1,143 @@ +package main + +import ( + "bytes" + "context" + "encoding/csv" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "os" + "os/signal" + "path" + "runtime" + "slices" + "syscall" + + "gitea.urkob.com/mcr-swiss/gogstea/internal/domain" + "gitea.urkob.com/mcr-swiss/gogstea/kit/config" +) + +var okStatuses = []int{ + http.StatusOK, + http.StatusCreated, + http.StatusNoContent, +} + +func main() { + ctx, cancel := context.WithCancel(signalContext(context.Background())) + defer cancel() + + cfgFile := os.Getenv("CONFIG_FILE") + if cfgFile == "" { + // Get root path + _, filename, _, _ := runtime.Caller(0) + cfgFile = path.Join(path.Dir(filename), "configs", "app.yml") + } + cfg, err := config.LoadConfig(cfgFile) + if err != nil { + panic(err) + } + + wd, err := os.Getwd() + if err != nil { + panic(err) + } + + f1, err := os.Open(cfg.Collaborators.CSVPath) + if err != nil { + panic(err) + } + defer f1.Close() + + r1 := csv.NewReader(f1) + r1.Comma = ',' // Set the delimiter to comma + r1.LazyQuotes = true + r1.TrimLeadingSpace = false + + outputFile, err := os.Create(wd + "/collab-output.txt") + if err != nil { + panic(err) + } + defer outputFile.Close() + + for { + record, err := r1.Read() + if err != nil { + if !errors.Is(err, io.EOF) { + panic(err) + } + break // Stop on EOF or other errors. + } + + cli := http.DefaultClient + + parsedURL, err := url.Parse(fmt.Sprintf("%s/repos/%s/%s/collaborators/%s", cfg.Gitea.URL, record[1], record[0], record[2])) + if err != nil { + panic(err) + } + + // permission, err := strconv.Atoi(record[3]) + // if err != nil { + // panic(err) + // } + + collabReq := domain.CollaboratorCreateRequest{ + Permission: record[4], + } + + bts, err := json.Marshal(collabReq) + if err != nil { + panic(err) + } + + req, err := http.NewRequestWithContext(ctx, http.MethodPut, parsedURL.String(), bytes.NewReader(bts)) + if err != nil { + panic(err) + } + + req.Header.Add("Authorization", "token "+cfg.Gitea.ApiKey) + req.Header.Add("Content-Type", "application/json") + resp, err := cli.Do(req) + if err != nil { + panic(err) + } + + if !slices.Contains(okStatuses, resp.StatusCode) { + bts, _ := io.ReadAll(resp.Body) + + if _, err := outputFile.WriteString(fmt.Sprintf("ERROR COLLAB | %d | %s | %s\n", resp.StatusCode, parsedURL.String(), string(bts))); err != nil { + panic(err) + } + continue + } + + bts, err = io.ReadAll(resp.Body) + if err != nil { + panic(err) + } + + if _, err := outputFile.WriteString(fmt.Sprintf("OK COLLAB | %s \n", string(bts))); err != nil { + panic(err) + } + } +} + +func signalContext(ctx context.Context) context.Context { + ctx, cancel := context.WithCancel(ctx) + + sigs := make(chan os.Signal, 1) + signal.Notify(sigs, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + go func() { + <-sigs + signal.Stop(sigs) + close(sigs) + cancel() + }() + + return ctx +} diff --git a/cmd/comments/main.go b/cmd/comments/main.go new file mode 100644 index 0000000..8ab3d5d --- /dev/null +++ b/cmd/comments/main.go @@ -0,0 +1,130 @@ +package main + +import ( + "bytes" + "context" + "encoding/csv" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "os" + "os/signal" + "path" + "runtime" + "syscall" + + "gitea.urkob.com/mcr-swiss/gogstea/internal/domain" + "gitea.urkob.com/mcr-swiss/gogstea/kit/config" +) + +func main() { + ctx, cancel := context.WithCancel(signalContext(context.Background())) + defer cancel() + + cfgFile := os.Getenv("CONFIG_FILE") + if cfgFile == "" { + // Get root path + _, filename, _, _ := runtime.Caller(0) + cfgFile = path.Join(path.Dir(filename), "configs", "app.yml") + } + cfg, err := config.LoadConfig(cfgFile) + if err != nil { + panic(err) + } + + wd, err := os.Getwd() + if err != nil { + panic(err) + } + + f1, err := os.Open(cfg.Issues.CSVPath) + if err != nil { + panic(err) + } + defer f1.Close() + + r1 := csv.NewReader(f1) + r1.Comma = ',' // Set the delimiter to comma + r1.LazyQuotes = true + r1.TrimLeadingSpace = false + + outputFile, err := os.Create(wd + "/comments-output.txt") + if err != nil { + panic(err) + } + defer outputFile.Close() + + for { + record, err := r1.Read() + if err != nil { + if !errors.Is(err, io.EOF) { + panic(err) + } + break // Stop on EOF or other errors. + } + + cli := http.DefaultClient + + parsedURL, err := url.Parse(fmt.Sprintf("%s/repos/{owner}/{repo}/issues", cfg.Gitea.URL, record[1], record[0])) + if err != nil { + panic(err) + } + + issue := domain.CommentCreateRequest{ + Body: "", + } + + bts, err := json.Marshal(issue) + if err != nil { + panic(err) + } + req, err := http.NewRequestWithContext(ctx, http.MethodPost, parsedURL.String(), bytes.NewReader(bts)) + if err != nil { + panic(err) + } + + req.Header.Add("Authorization", "token "+cfg.Gitea.ApiKey) + req.Header.Add("Content-Type", "application/json") + resp, err := cli.Do(req) + if err != nil { + panic(err) + } + + if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated { + bts, _ := io.ReadAll(resp.Body) + + if _, err := outputFile.WriteString(fmt.Sprintf("ERROR ISSUE | %d | %s\n", resp.StatusCode, string(bts))); err != nil { + panic(err) + } + continue + } + + bts, err = io.ReadAll(resp.Body) + if err != nil { + panic(err) + } + + if _, err := outputFile.WriteString(fmt.Sprintf("OK ISSUE | %s \n", string(bts))); err != nil { + panic(err) + } + } +} + +func signalContext(ctx context.Context) context.Context { + ctx, cancel := context.WithCancel(ctx) + + sigs := make(chan os.Signal, 1) + signal.Notify(sigs, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + go func() { + <-sigs + signal.Stop(sigs) + close(sigs) + cancel() + }() + + return ctx +} diff --git a/cmd/issue/main.go b/cmd/issue/main.go new file mode 100644 index 0000000..ade1e5d --- /dev/null +++ b/cmd/issue/main.go @@ -0,0 +1,156 @@ +package main + +import ( + "bytes" + "context" + "encoding/csv" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "os" + "os/signal" + "path" + "runtime" + "strconv" + "syscall" + "time" + + "gitea.urkob.com/mcr-swiss/gogstea/internal/domain" + "gitea.urkob.com/mcr-swiss/gogstea/kit/config" +) + +func main() { + ctx, cancel := context.WithCancel(signalContext(context.Background())) + defer cancel() + + cfgFile := os.Getenv("CONFIG_FILE") + if cfgFile == "" { + // Get root path + _, filename, _, _ := runtime.Caller(0) + cfgFile = path.Join(path.Dir(filename), "configs", "app.yml") + } + cfg, err := config.LoadConfig(cfgFile) + if err != nil { + panic(err) + } + + wd, err := os.Getwd() + if err != nil { + panic(err) + } + + f1, err := os.Open(cfg.Issues.CSVPath) + if err != nil { + panic(err) + } + defer f1.Close() + + r1 := csv.NewReader(f1) + r1.Comma = ',' // Set the delimiter to comma + r1.LazyQuotes = true + r1.TrimLeadingSpace = false + + outputFile, err := os.Create(wd + "/issues-output.txt") + if err != nil { + panic(err) + } + defer outputFile.Close() + + for { + record, err := r1.Read() + if err != nil { + if !errors.Is(err, io.EOF) { + panic(err) + } + break // Stop on EOF or other errors. + } + + cli := http.DefaultClient + + parsedURL, err := url.Parse(fmt.Sprintf("%s/repos/%s/%s/issues", cfg.Gitea.URL, record[1], record[0])) + if err != nil { + panic(err) + } + + isClosedInt, err := strconv.Atoi(record[7]) + if err != nil { + panic(err) + } + + var isClosed bool + switch isClosedInt { + case 0: + isClosed = false + case 1: + isClosed = false + default: + panic(fmt.Errorf("isclosed is %d", isClosedInt)) + } + + issue := domain.IssueCreateRequest{ + Assignee: record[2], + Assignees: nil, + Body: record[4], + Closed: isClosed, + DueDate: time.Time{}, + Labels: []int{}, + Milestone: 0, + Ref: "", + Title: record[3], + } + + bts, err := json.Marshal(issue) + if err != nil { + panic(err) + } + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, parsedURL.String(), bytes.NewReader(bts)) + if err != nil { + panic(err) + } + + req.Header.Add("Authorization", "token "+cfg.Gitea.ApiKey) + req.Header.Add("Content-Type", "application/json") + resp, err := cli.Do(req) + if err != nil { + panic(err) + } + + if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated { + bts, _ := io.ReadAll(resp.Body) + + if _, err := outputFile.WriteString(fmt.Sprintf("ERROR ISSUE | %d | %s | %s\n", resp.StatusCode, parsedURL.String(), string(bts))); err != nil { + panic(err) + } + continue + } + + bts, err = io.ReadAll(resp.Body) + if err != nil { + panic(err) + } + + if _, err := outputFile.WriteString(fmt.Sprintf("OK ISSUE | %s \n", string(bts))); err != nil { + panic(err) + } + } +} + +func signalContext(ctx context.Context) context.Context { + ctx, cancel := context.WithCancel(ctx) + + sigs := make(chan os.Signal, 1) + signal.Notify(sigs, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + go func() { + <-sigs + signal.Stop(sigs) + close(sigs) + cancel() + }() + + return ctx +} diff --git a/cmd/org/main.go b/cmd/org/main.go new file mode 100644 index 0000000..e1a5b36 --- /dev/null +++ b/cmd/org/main.go @@ -0,0 +1,137 @@ +package main + +import ( + "bytes" + "context" + "encoding/csv" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "os" + "os/signal" + "path" + "runtime" + "syscall" + + "gitea.urkob.com/mcr-swiss/gogstea/internal/domain" + "gitea.urkob.com/mcr-swiss/gogstea/kit/config" +) + +func main() { + ctx, cancel := context.WithCancel(signalContext(context.Background())) + defer cancel() + + cfgFile := os.Getenv("CONFIG_FILE") + if cfgFile == "" { + // Get root path + _, filename, _, _ := runtime.Caller(0) + cfgFile = path.Join(path.Dir(filename), "configs", "app.yml") + } + cfg, err := config.LoadConfig(cfgFile) + if err != nil { + panic(err) + } + + wd, err := os.Getwd() + if err != nil { + panic(err) + } + + f1, err := os.Open(cfg.Organizations.CSVPath) + if err != nil { + panic(err) + } + defer f1.Close() + + r1 := csv.NewReader(f1) + r1.Comma = ',' // Set the delimiter to comma + r1.LazyQuotes = true + r1.TrimLeadingSpace = false + + outputFile, err := os.Create(wd + "/org-output.txt") + if err != nil { + panic(err) + } + defer outputFile.Close() + + for { + record, err := r1.Read() + if err != nil { + if !errors.Is(err, io.EOF) { + panic(err) + } + break // Stop on EOF or other errors. + } + + cli := http.DefaultClient + + parsedURL, err := url.Parse(fmt.Sprintf("%s/admin/users/%s/orgs", cfg.Gitea.URL, record[2])) + if err != nil { + panic(err) + } + + bodyReq := domain.OrgCreateRequest{ + Description: record[0], + Email: "", + FullName: "", + Location: "", + RepoAdminChangeTeamAccess: true, + Username: record[0], + Visibility: "private", + Website: "", + } + + bts, err := json.Marshal(bodyReq) + if err != nil { + panic(err) + } + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, parsedURL.String(), bytes.NewReader(bts)) + if err != nil { + panic(err) + } + + req.Header.Add("Authorization", "token "+cfg.Gitea.ApiKey) + req.Header.Add("Content-Type", "application/json") + resp, err := cli.Do(req) + if err != nil { + panic(err) + } + + if resp.StatusCode != http.StatusOK { + bts, _ := io.ReadAll(resp.Body) + + if _, err := outputFile.WriteString(fmt.Sprintf("%s | %d: %s\n", record[3], resp.StatusCode, string(bts))); err != nil { + panic(err) + } + continue + } + bts, err = io.ReadAll(resp.Body) + if err != nil { + panic(err) + } + + if _, err := outputFile.WriteString(fmt.Sprintf("OK REPO %s | %s | OK\n", bodyReq.Username, string(bts))); err != nil { + panic(err) + } + } +} + +func signalContext(ctx context.Context) context.Context { + ctx, cancel := context.WithCancel(ctx) + + sigs := make(chan os.Signal, 1) + signal.Notify(sigs, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + go func() { + <-sigs + signal.Stop(sigs) + close(sigs) + cancel() + }() + + return ctx +} diff --git a/cmd/users/main.go b/cmd/users/main.go new file mode 100644 index 0000000..63771f8 --- /dev/null +++ b/cmd/users/main.go @@ -0,0 +1,149 @@ +package main + +import ( + "bytes" + "context" + "encoding/csv" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "os" + "os/signal" + "path" + "runtime" + "syscall" + + "gitea.urkob.com/mcr-swiss/gogstea/internal/domain" + "gitea.urkob.com/mcr-swiss/gogstea/kit/config" +) + +func main() { + ctx, cancel := context.WithCancel(signalContext(context.Background())) + defer cancel() + + cfgFile := os.Getenv("CONFIG_FILE") + if cfgFile == "" { + // Get root path + _, filename, _, _ := runtime.Caller(0) + cfgFile = path.Join(path.Dir(filename), "configs", "app.yml") + } + cfg, err := config.LoadConfig(cfgFile) + if err != nil { + panic(err) + } + + wd, err := os.Getwd() + if err != nil { + panic(err) + } + + // STEP 1 MIGRATE USERS + { + f1, err := os.Open(cfg.Users.CSVPath) + if err != nil { + panic(err) + } + defer f1.Close() + + r1 := csv.NewReader(f1) + r1.Comma = ',' // Set the delimiter to comma + r1.LazyQuotes = true + r1.TrimLeadingSpace = false + + outputFile, err := os.Create(wd + "/output.txt") + if err != nil { + panic(err) + } + defer outputFile.Close() + + for { + record, err := r1.Read() + if err != nil { + if !errors.Is(err, io.EOF) { + panic(err) + } + break // Stop on EOF or other errors. + } + + cli := http.DefaultClient + + parsedURL, err := url.Parse(fmt.Sprintf(cfg.Gitea.URL + "/admin/users")) + if err != nil { + panic(err) + } + + u := domain.UserCreateRequest{ + Email: record[4], + FullName: record[2], + LoginName: record[1], + MustChangePassword: true, + Password: cfg.Users.DefaultPassword, + Restricted: false, + SendNotify: false, + SourceID: 0, + Username: record[1], + } + + bts, err := json.Marshal(u) + if err != nil { + panic(err) + } + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, parsedURL.String(), bytes.NewReader(bts)) + if err != nil { + panic(err) + } + + req.Header.Add("Authorization", "token "+cfg.Gitea.ApiKey) + req.Header.Add("Content-Type", "application/json") + resp, err := cli.Do(req) + if err != nil { + panic(err) + } + + if resp.StatusCode != http.StatusOK { + bts, _ := io.ReadAll(resp.Body) + + if _, err := outputFile.WriteString(fmt.Sprintf("%s | %d: %s\n", u.Email, resp.StatusCode, string(bts))); err != nil { + panic(err) + } + continue + } + + bts, err = io.ReadAll(resp.Body) + if err != nil { + panic(err) + } + + var uResp domain.UserGet + if err := json.Unmarshal(bts, &uResp); err != nil { + panic(err) + } + + if _, err := outputFile.WriteString(fmt.Sprintf("USER %d | %s | OK\n", uResp.ID, uResp.Email)); err != nil { + panic(err) + } + + } + } + +} + +func signalContext(ctx context.Context) context.Context { + ctx, cancel := context.WithCancel(ctx) + + sigs := make(chan os.Signal, 1) + signal.Notify(sigs, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + go func() { + <-sigs + signal.Stop(sigs) + close(sigs) + cancel() + }() + + return ctx +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..383f6d3 --- /dev/null +++ b/go.mod @@ -0,0 +1,10 @@ +module gitea.urkob.com/mcr-swiss/gogstea + +go 1.23.0 + +require ( + go.uber.org/zap v1.27.0 + gopkg.in/yaml.v3 v3.0.1 +) + +require go.uber.org/multierr v1.10.0 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..d66a2b7 --- /dev/null +++ b/go.sum @@ -0,0 +1,16 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= +go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/domain/collaborator.go b/internal/domain/collaborator.go new file mode 100644 index 0000000..0ba4143 --- /dev/null +++ b/internal/domain/collaborator.go @@ -0,0 +1,5 @@ +package domain + +type CollaboratorCreateRequest struct { + Permission string `json:"permission"` +} diff --git a/internal/domain/comment.go b/internal/domain/comment.go new file mode 100644 index 0000000..27b1786 --- /dev/null +++ b/internal/domain/comment.go @@ -0,0 +1,5 @@ +package domain + +type CommentCreateRequest struct { + Body string `json:"body"` +} diff --git a/internal/domain/issue.go b/internal/domain/issue.go new file mode 100644 index 0000000..4445d86 --- /dev/null +++ b/internal/domain/issue.go @@ -0,0 +1,15 @@ +package domain + +import "time" + +type IssueCreateRequest struct { + Assignee string `json:"assignee"` + Assignees []string `json:"assignees"` + Body string `json:"body"` + Closed bool `json:"closed"` + DueDate time.Time `json:"due_date"` + Labels []int `json:"labels"` + Milestone int `json:"milestone"` + Ref string `json:"ref"` + Title string `json:"title"` +} diff --git a/internal/domain/org.go b/internal/domain/org.go new file mode 100644 index 0000000..e09caf6 --- /dev/null +++ b/internal/domain/org.go @@ -0,0 +1,12 @@ +package domain + +type OrgCreateRequest struct { + Description string `json:"description"` + Email string `json:"email"` + FullName string `json:"full_name"` + Location string `json:"location"` + RepoAdminChangeTeamAccess bool `json:"repo_admin_change_team_access"` + Username string `json:"username"` + Visibility string `json:"visibility"` + Website string `json:"website"` +} diff --git a/internal/domain/user.go b/internal/domain/user.go new file mode 100644 index 0000000..dec3715 --- /dev/null +++ b/internal/domain/user.go @@ -0,0 +1,42 @@ +package domain + +import "time" + +type UserCreateRequest struct { + CreatedAt time.Time `json:"created_at"` + Email string `json:"email"` + FullName string `json:"full_name"` + LoginName string `json:"login_name"` + MustChangePassword bool `json:"must_change_password"` + Password string `json:"password"` + Restricted bool `json:"restricted"` + SendNotify bool `json:"send_notify"` + SourceID int `json:"source_id"` + Username string `json:"username"` +} + +type UserGet struct { + ID int `json:"id"` + Login string `json:"login"` + LoginName string `json:"login_name"` + SourceID int `json:"source_id"` + FullName string `json:"full_name"` + Email string `json:"email"` + AvatarURL string `json:"avatar_url"` + HTMLURL string `json:"html_url"` + Language string `json:"language"` + IsAdmin bool `json:"is_admin"` + LastLogin time.Time `json:"last_login"` + Created time.Time `json:"created"` + Restricted bool `json:"restricted"` + Active bool `json:"active"` + ProhibitLogin bool `json:"prohibit_login"` + Location string `json:"location"` + Website string `json:"website"` + Description string `json:"description"` + Visibility string `json:"visibility"` + FollowersCount int `json:"followers_count"` + FollowingCount int `json:"following_count"` + StarredReposCount int `json:"starred_repos_count"` + Username string `json:"username"` +} diff --git a/kit/config/config.go b/kit/config/config.go new file mode 100644 index 0000000..d63b328 --- /dev/null +++ b/kit/config/config.go @@ -0,0 +1,50 @@ +package config + +import ( + "os" + + "go.uber.org/zap" + "gopkg.in/yaml.v3" +) + +type Config struct { + Gitea struct { + URL string `yaml:"url"` + ApiKey string `yaml:"api_key"` + } `yaml:"gitea"` + Users struct { + DefaultPassword string `yaml:"default_password"` + CSVPath string `yaml:"csv_path"` + } `yaml:"users"` + Organizations struct { + CSVPath string `yaml:"csv_path"` + } `yaml:"organizations"` + Issues struct { + CSVPath string `yaml:"csv_path"` + } `yaml:"issues"` + Collaborators struct { + CSVPath string `yaml:"csv_path"` + } `yaml:"collaborators"` +} + +func LoadConfig(path string) (*Config, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, err + } + + var config Config + if err := yaml.Unmarshal(data, &config); err != nil { + return nil, err + } + + return &config, nil +} + +func NewZapCfg(dev bool) zap.Config { + prodConfig := zap.NewProductionConfig() + if dev { + prodConfig = zap.NewDevelopmentConfig() + } + return prodConfig +}