├── .github └── workflows │ └── ci.yaml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── github ├── client.go ├── client_test.go ├── comments.go ├── commits.go ├── diff.go ├── error.go ├── events.go ├── hooks.go ├── import.go ├── issues.go ├── labels.go ├── logger.go ├── members.go ├── milestones.go ├── mock_client.go ├── path.go ├── project_cards.go ├── project_columns.go ├── projects.go ├── pulls.go ├── repo.go ├── review_comments.go ├── reviews.go └── users.go ├── go.mod ├── go.sum ├── main.go ├── migrator ├── builder.go ├── builder_events.go ├── comment_filter.go ├── diff.go ├── diff_test.go ├── hooks.go ├── issues.go ├── issues_buffer.go ├── labels.go ├── migrator.go ├── migrator_test.go ├── milestones.go ├── plural.go ├── project_cards.go ├── project_columns.go ├── projects.go ├── repos.go ├── test.yaml └── users.go └── repo ├── comments.go ├── comments_test.go ├── commits.go ├── commits_test.go ├── diff.go ├── diff_test.go ├── events.go ├── events_test.go ├── get.go ├── get_test.go ├── hooks.go ├── hooks_test.go ├── import.go ├── issues.go ├── issues_test.go ├── labels.go ├── labels_test.go ├── members.go ├── members_test.go ├── milestones.go ├── milestones_test.go ├── project_cards.go ├── project_cards_test.go ├── project_columns.go ├── project_columns_test.go ├── projects.go ├── projects_test.go ├── pulls.go ├── pulls_test.go ├── repo.go ├── review_comments.go ├── review_comments_test.go ├── reviews.go ├── reviews_test.go ├── update.go ├── update_test.go └── user.go /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | test: 11 | name: Test 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v3 16 | - name: Setup Go 17 | uses: actions/setup-go@v3 18 | with: 19 | go-version: 1.x 20 | - name: Test 21 | run: make test 22 | - name: Lint 23 | run: make lint 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /github-migrator 2 | /*.sh 3 | *.exe 4 | *.test 5 | *.out 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019-2022 itchyny 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BIN := github-migrator 2 | GOBIN ?= $(shell go env GOPATH)/bin 3 | 4 | .PHONY: all 5 | all: build 6 | 7 | .PHONY: build 8 | build: 9 | go build -o $(BIN) . 10 | 11 | .PHONY: install 12 | install: 13 | go install ./... 14 | 15 | .PHONY: test 16 | test: build 17 | go test -v -race ./... 18 | 19 | .PHONY: lint 20 | lint: $(GOBIN)/staticcheck 21 | go vet ./... 22 | staticcheck -checks all,-ST1000 ./... 23 | 24 | $(GOBIN)/staticcheck: 25 | go install honnef.co/go/tools/cmd/staticcheck@latest 26 | 27 | .PHONY: clean 28 | clean: 29 | go clean 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # github-migrator 2 | This tool migrates a GitHub repository to another. 3 | This is especially useful to move a repository from GitHub Enterprise to github.com. 4 | 5 | ![](https://user-images.githubusercontent.com/375258/71414326-cda1b480-2699-11ea-9de9-411e954bdb70.jpg) 6 | 7 | ## Usage 8 | ```bash 9 | export GITHUB_MIGRATOR_SOURCE_API_TOKEN=xxx 10 | export GITHUB_MIGRATOR_SOURCE_API_ENDPOINT=http://localhost/api/v3 # This might be the endpoint of GitHub Enterprise 11 | export GITHUB_MIGRATOR_TARGET_API_TOKEN=yyy 12 | # export GITHUB_MIGRATOR_TARGET_API_ENDPOINT=https://api.github.com # No need to specify the endpoint of github.com 13 | # export GITHUB_MIGRATOR_TARGET_PROXY_URL=http://proxyIp:proxyPort # If you need proxy URL 14 | go run . [old-owner]/[source] [new-owner]/[target] 15 | ``` 16 | Be sure to use this tool before pushing the git tree to the new origin (otherwise the links in the merged commits are lost). 17 | 18 | ### Other options 19 | Sometimes same user has different user id on GitHub and Enterprise. 20 | ```bash 21 | export GITHUB_MIGRATOR_USER_MAPPING=user-before1:user-after1,user-before2:user-after2,user-before3:user-after3 22 | ``` 23 | 24 | ## Requirements 25 | - Go 1.17+ 26 | - API tokens to access the source and target repositories. 27 | 28 | ## Features 29 | - Issues 30 | - Issue description with the link to the original repository 31 | - Issue comments with the user name and icon (within the comment) 32 | - Created dates, Labels 33 | - Issue numbers are same as the original repository 34 | - Various events (including title changes, issue locking, assignments, review requests and branch deletion in a pull request) 35 | - Pull requests 36 | - A pull request is converted to an issue 37 | - Comments and review comments are migrated as issue comments 38 | - Created dates, Labels 39 | - Pull request numbers (issue numbers) are same as the original repository 40 | - Number of changed files, insertions and deletions 41 | - Entire diff (excluding large file diffs) 42 | - Commits list and link to the corresponding /compare/ page 43 | - Repository information 44 | - Description, Homepage (only when the target repository has blank description, homepage) 45 | - Labels 46 | - Label name, description and colors 47 | - Label changes in issue and pull request 48 | - Projects 49 | - Projects, columns and cards 50 | - Note that column automations are not migrated (cannot be set via API) 51 | - Milestones 52 | - Milestone titles, description and due date 53 | - Connect issues to milestones 54 | - Webhooks 55 | - Webhook URL, content type and events the hooks is trigger for. 56 | - All the other things will be lost 57 | - Images posted to issue and pull request comments. 58 | - Emoji reactions to issue and pull request comments 59 | - Diffs (split) view of pull requests 60 | - Wiki 61 | - Default branch, Protection rules 62 | - Notifications, Integrations 63 | 64 | ## Bug Tracker 65 | Report bug at [Issues・itchyny/github-migrator - GitHub](https://github.com/itchyny/github-migrator/issues). 66 | 67 | ## Author 68 | itchyny (https://github.com/itchyny) 69 | 70 | ## License 71 | This software is released under the MIT License, see LICENSE. 72 | 73 | ## Previous works and references 74 | - [fastlane/monorepo: Scripts to migrate to a monorepo](https://github.com/fastlane/monorepo) 75 | - This tool greatly influenced me, especially for investigating the usage of the import api. 76 | - [aereal/migrate-gh-repo: migrate GitHub (incl. Enterprise) repositories with idempotent-like manner](https://github.com/aereal/migrate-gh-repo) 77 | - For the idea of keeping the issue and pull request numbers. 78 | - [Complete issue import API walkthrough with curl](https://gist.github.com/jonmagic/5282384165e0f86ef105) 79 | - Comprehensive tutorial for using the import api (which is not listed in the official api document yet). 80 | -------------------------------------------------------------------------------- /github/client.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "bytes" 5 | "crypto/tls" 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "io" 10 | "net/http" 11 | "net/url" 12 | "time" 13 | 14 | "github.com/tomnomnom/linkheader" 15 | ) 16 | 17 | // Client represents a GitHub client. 18 | type Client interface { 19 | GetLogin() (*User, error) 20 | ListUsers() Users 21 | GetUser(string) (*User, error) 22 | ListMembers(string) Members 23 | GetRepo(string) (*Repo, error) 24 | UpdateRepo(string, *UpdateRepoParams) (*Repo, error) 25 | ListLabels(string) Labels 26 | CreateLabel(string, *CreateLabelParams) (*Label, error) 27 | UpdateLabel(string, string, *UpdateLabelParams) (*Label, error) 28 | ListIssues(string, *ListIssuesParams) Issues 29 | GetIssue(string, int) (*Issue, error) 30 | AddAssignees(string, int, []string) error 31 | ListComments(string, int) Comments 32 | ListEvents(string, int) Events 33 | ListPullReqs(string, *ListPullReqsParams) PullReqs 34 | GetPullReq(string, int) (*PullReq, error) 35 | ListPullReqCommits(string, int) Commits 36 | GetDiff(string, string) (string, error) 37 | GetCompare(string, string, string) (string, error) 38 | ListReviews(string, int) Reviews 39 | GetReview(string, int, int) (*Review, error) 40 | ListReviewComments(string, int) ReviewComments 41 | ListProjects(string, *ListProjectsParams) Projects 42 | GetProject(int) (*Project, error) 43 | CreateProject(string, *CreateProjectParams) (*Project, error) 44 | UpdateProject(int, *UpdateProjectParams) (*Project, error) 45 | DeleteProject(int) error 46 | ListProjectColumns(int) ProjectColumns 47 | GetProjectColumn(int) (*ProjectColumn, error) 48 | CreateProjectColumn(int, string) (*ProjectColumn, error) 49 | UpdateProjectColumn(int, string) (*ProjectColumn, error) 50 | ListProjectCards(int) ProjectCards 51 | GetProjectCard(int) (*ProjectCard, error) 52 | CreateProjectCard(int, *CreateProjectCardParams) (*ProjectCard, error) 53 | UpdateProjectCard(int, *UpdateProjectCardParams) (*ProjectCard, error) 54 | MoveProjectCard(int, *MoveProjectCardParams) (*ProjectCard, error) 55 | ListMilestones(string, *ListMilestonesParams) Milestones 56 | GetMilestone(string, int) (*Milestone, error) 57 | CreateMilestone(string, *CreateMilestoneParams) (*Milestone, error) 58 | UpdateMilestone(string, int, *UpdateMilestoneParams) (*Milestone, error) 59 | DeleteMilestone(string, int) error 60 | ListHooks(string) Hooks 61 | GetHook(string, int) (*Hook, error) 62 | CreateHook(string, *CreateHookParams) (*Hook, error) 63 | UpdateHook(string, int, *UpdateHookParams) (*Hook, error) 64 | Import(string, *Import) (*ImportResult, error) 65 | GetImport(string, int) (*ImportResult, error) 66 | } 67 | 68 | // New creates a new GitHub client. 69 | func New(token, endpoint, proxy string, opts ...ClientOption) Client { 70 | cli := &http.Client{Transport: &http.Transport{ 71 | TLSClientConfig: &tls.Config{ 72 | InsecureSkipVerify: endpoint != "https://api.github.com", 73 | }, 74 | }} 75 | if proxy != "" { 76 | proxyURL, err := url.Parse(proxy) 77 | if err != nil { 78 | panic(err) 79 | } 80 | cli.Transport.(*http.Transport).Proxy = http.ProxyURL(proxyURL) 81 | } 82 | c := &client{token, endpoint, cli, &Logger{}} 83 | for _, opt := range opts { 84 | opt(c) 85 | } 86 | return c 87 | } 88 | 89 | // ClientOption is an option of client. 90 | type ClientOption func(*client) 91 | 92 | // ClientLogger returns a client option to set the logger. 93 | func ClientLogger(l *Logger) ClientOption { 94 | return func(c *client) { 95 | c.logger = l 96 | } 97 | } 98 | 99 | type client struct { 100 | token, endpoint string 101 | client *http.Client 102 | logger *Logger 103 | } 104 | 105 | func (c *client) url(path string) string { 106 | return c.endpoint + path 107 | } 108 | 109 | func (c *client) do(method, path string, body interface{}) (*http.Response, error) { 110 | var retryCnt int 111 | duration := time.Minute 112 | for { 113 | res, retry, err := c.doOnce(method, path, body) 114 | if err == nil || !retry || retryCnt >= 7 { 115 | return res, err 116 | } 117 | retryCnt++ 118 | if retryCnt > 2 { 119 | duration *= 2 120 | if duration > 10*time.Minute { 121 | duration = 10 * time.Minute 122 | } 123 | } 124 | time.Sleep(duration) 125 | } 126 | } 127 | 128 | func (c *client) doOnce(method, path string, body interface{}) (*http.Response, bool, error) { 129 | var b io.Reader 130 | if body != nil { 131 | bs, err := json.Marshal(body) 132 | if err != nil { 133 | return nil, false, err 134 | } 135 | b = bytes.NewReader(bs) 136 | } 137 | req, err := c.request(method, path, b) 138 | if err != nil { 139 | return nil, false, err 140 | } 141 | return c.doReq(req) 142 | } 143 | 144 | func (c *client) request(method, path string, body io.Reader) (*http.Request, error) { 145 | req, err := http.NewRequest(method, path, body) 146 | if err != nil { 147 | return nil, err 148 | } 149 | req.Header.Add("Authorization", "token "+c.token) 150 | req.Header.Add("Accept", "application/vnd.github.golden-comet-preview+json") 151 | req.Header.Add("Accept", "application/vnd.github.symmetra-preview+json") 152 | req.Header.Add("Accept", "application/vnd.github.comfort-fade-preview+json") 153 | req.Header.Add("Accept", "application/vnd.github.sailor-v-preview+json") 154 | req.Header.Add("Accept", "application/vnd.github.starfox-preview+json") 155 | req.Header.Add("Accept", "application/vnd.github.inertia-preview+json") 156 | req.Header.Add("User-Agent", "github-migrator") 157 | return req, nil 158 | } 159 | 160 | func (c *client) doReq(req *http.Request) (*http.Response, bool, error) { 161 | c.logger.preRequest(req) 162 | res, err := c.client.Do(req) 163 | c.logger.postRequest(res, err) 164 | if err != nil { 165 | return nil, true, err 166 | } 167 | if res.StatusCode < 200 || 400 <= res.StatusCode { 168 | return nil, 500 <= res.StatusCode, getError(res) 169 | } 170 | return res, false, nil 171 | } 172 | 173 | func getError(res *http.Response) error { 174 | defer res.Body.Close() 175 | var r struct { 176 | Message string `json:"message"` 177 | Errors apiErrors `json:"errors"` 178 | } 179 | if err := json.NewDecoder(res.Body).Decode(&r); err != nil { 180 | return err 181 | } 182 | if len(r.Errors) == 0 { 183 | return errors.New(r.Message) 184 | } 185 | return fmt.Errorf("%s: %w", r.Message, r.Errors) 186 | } 187 | 188 | func (c *client) get(path string, v interface{}) error { 189 | res, err := c.do("GET", path, nil) 190 | if err != nil { 191 | return err 192 | } 193 | defer res.Body.Close() 194 | if err := json.NewDecoder(res.Body).Decode(&v); err != nil { 195 | return err 196 | } 197 | return nil 198 | } 199 | 200 | func (c *client) post(path string, body, v interface{}) error { 201 | res, err := c.do("POST", path, body) 202 | if err != nil { 203 | return err 204 | } 205 | defer res.Body.Close() 206 | if err := json.NewDecoder(res.Body).Decode(&v); err != nil { 207 | return err 208 | } 209 | return nil 210 | } 211 | 212 | func (c *client) patch(path string, body, v interface{}) error { 213 | res, err := c.do("PATCH", path, body) 214 | if err != nil { 215 | return err 216 | } 217 | defer res.Body.Close() 218 | if err := json.NewDecoder(res.Body).Decode(&v); err != nil { 219 | return err 220 | } 221 | return nil 222 | } 223 | 224 | func (c *client) delete(path string) error { 225 | res, err := c.do("DELETE", path, nil) 226 | if err != nil { 227 | return err 228 | } 229 | defer res.Body.Close() 230 | return nil 231 | } 232 | 233 | func (c *client) getList(path string, v interface{}) (string, error) { 234 | res, err := c.do("GET", path, nil) 235 | if err != nil { 236 | return "", err 237 | } 238 | defer res.Body.Close() 239 | if err := json.NewDecoder(res.Body).Decode(&v); err != nil { 240 | return "", err 241 | } 242 | return getNext(res.Header), nil 243 | } 244 | 245 | func getNext(header http.Header) string { 246 | xs := header["Link"] 247 | if len(xs) == 0 { 248 | return "" 249 | } 250 | links := linkheader.Parse(xs[0]) 251 | for _, link := range links { 252 | if link.Rel == "next" { 253 | return link.URL 254 | } 255 | } 256 | return "" 257 | } 258 | -------------------------------------------------------------------------------- /github/client_test.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import "testing" 4 | 5 | func TestNew(t *testing.T) { 6 | var _ Client = New("token", "http://localhost", "") 7 | } 8 | -------------------------------------------------------------------------------- /github/comments.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | ) 7 | 8 | // Comment represents a comment. 9 | type Comment struct { 10 | Body string `json:"body"` 11 | HTMLURL string `json:"html_url"` 12 | User *User `json:"user"` 13 | CreatedAt string `json:"created_at"` 14 | UpdatedAt string `json:"updated_at"` 15 | } 16 | 17 | // Comments represents a collection of comments. 18 | type Comments <-chan interface{} 19 | 20 | // Next emits the next Comment. 21 | func (cs Comments) Next() (*Comment, error) { 22 | for x := range cs { 23 | switch x := x.(type) { 24 | case error: 25 | return nil, x 26 | case *Comment: 27 | return x, nil 28 | } 29 | break 30 | } 31 | return nil, io.EOF 32 | } 33 | 34 | // CommentsFromSlice creates Comments from a slice. 35 | func CommentsFromSlice(xs []*Comment) Comments { 36 | cs := make(chan interface{}) 37 | go func() { 38 | defer close(cs) 39 | for _, c := range xs { 40 | cs <- c 41 | } 42 | }() 43 | return cs 44 | } 45 | 46 | // CommentsToSlice collects Comments. 47 | func CommentsToSlice(cs Comments) ([]*Comment, error) { 48 | xs := []*Comment{} 49 | for { 50 | c, err := cs.Next() 51 | if err != nil { 52 | if err != io.EOF { 53 | return nil, err 54 | } 55 | return xs, nil 56 | } 57 | xs = append(xs, c) 58 | } 59 | } 60 | 61 | // ListComments lists the comments of an issue. 62 | func (c *client) ListComments(repo string, issueNumber int) Comments { 63 | cs := make(chan interface{}) 64 | go func() { 65 | defer close(cs) 66 | path := c.url(fmt.Sprintf("/repos/%s/issues/%d/comments?per_page=100", repo, issueNumber)) 67 | for { 68 | var xs []*Comment 69 | next, err := c.getList(path, &xs) 70 | if err != nil { 71 | cs <- fmt.Errorf("ListComments %s/issues/%d: %w", repo, issueNumber, err) 72 | break 73 | } 74 | for _, x := range xs { 75 | cs <- x 76 | } 77 | if next == "" { 78 | break 79 | } 80 | path = next 81 | } 82 | }() 83 | return Comments(cs) 84 | } 85 | -------------------------------------------------------------------------------- /github/commits.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | ) 7 | 8 | // Commit represents a commit. 9 | type Commit struct { 10 | SHA string `json:"sha"` 11 | HTMLURL string `json:"html_url"` 12 | Commit struct { 13 | Author *CommitUser `json:"author"` 14 | Committer *CommitUser `json:"committer"` 15 | Message string `json:"message"` 16 | } `json:"commit"` 17 | Author *User `json:"author"` 18 | Committer *User `json:"committer"` 19 | Parents []struct { 20 | URL string `json:"url"` 21 | SHA string `json:"sha"` 22 | } `json:"parents"` 23 | } 24 | 25 | // CommitUser represents a commit user. 26 | type CommitUser struct { 27 | Name string `json:"name"` 28 | Email string `json:"email"` 29 | Date string `json:"date"` 30 | } 31 | 32 | // Commits represents a collection of commits. 33 | type Commits <-chan interface{} 34 | 35 | // Next emits the next Commit. 36 | func (cs Commits) Next() (*Commit, error) { 37 | for x := range cs { 38 | switch x := x.(type) { 39 | case error: 40 | return nil, x 41 | case *Commit: 42 | return x, nil 43 | } 44 | break 45 | } 46 | return nil, io.EOF 47 | } 48 | 49 | // CommitsFromSlice creates Commits from a slice. 50 | func CommitsFromSlice(xs []*Commit) Commits { 51 | cs := make(chan interface{}) 52 | go func() { 53 | defer close(cs) 54 | for _, p := range xs { 55 | cs <- p 56 | } 57 | }() 58 | return cs 59 | } 60 | 61 | // CommitsToSlice collects Commits. 62 | func CommitsToSlice(cs Commits) ([]*Commit, error) { 63 | xs := []*Commit{} 64 | for { 65 | p, err := cs.Next() 66 | if err != nil { 67 | if err != io.EOF { 68 | return nil, err 69 | } 70 | return xs, nil 71 | } 72 | xs = append(xs, p) 73 | } 74 | } 75 | 76 | // ListPullReqCommits lists the commits of a pull request. 77 | func (c *client) ListPullReqCommits(repo string, pullNumber int) Commits { 78 | cs := make(chan interface{}) 79 | go func() { 80 | defer close(cs) 81 | path := c.url(fmt.Sprintf("/repos/%s/pulls/%d/commits?per_page=100", repo, pullNumber)) 82 | for { 83 | var xs []*Commit 84 | next, err := c.getList(path, &xs) 85 | if err != nil { 86 | cs <- fmt.Errorf("ListPullReqCommits %s/pull/%d: %w", repo, pullNumber, err) 87 | break 88 | } 89 | for _, x := range xs { 90 | cs <- x 91 | } 92 | if next == "" { 93 | break 94 | } 95 | path = next 96 | } 97 | }() 98 | return Commits(cs) 99 | } 100 | -------------------------------------------------------------------------------- /github/diff.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "strings" 7 | ) 8 | 9 | const maxDiffSize = 1 * 1024 * 1024 10 | 11 | func (c *client) GetDiff(repo string, sha string) (string, error) { 12 | return c.getDiff("GetDiff", fmt.Sprintf("/repos/%s/commits/%s", repo, sha)) 13 | } 14 | 15 | func (c *client) GetCompare(repo string, base, head string) (string, error) { 16 | return c.getDiff("GetCompare", fmt.Sprintf("/repos/%s/compare/%s...%s", repo, base, head)) 17 | } 18 | 19 | func (c *client) getDiff(name, path string) (string, error) { 20 | req, err := c.request("GET", c.url(path), nil) 21 | if err != nil { 22 | return "", err 23 | } 24 | req.Header.Add("Accept", "application/vnd.github.v3.diff") 25 | res, _, err := c.doReq(req) 26 | if err != nil { 27 | return "", fmt.Errorf("%s %s: %w", name, strings.TrimPrefix(path, "/repos/"), err) 28 | } 29 | defer res.Body.Close() 30 | 31 | bs, err := io.ReadAll(&io.LimitedReader{R: res.Body, N: maxDiffSize}) 32 | if err != nil { 33 | return "", err 34 | } 35 | return string(bs), nil 36 | } 37 | -------------------------------------------------------------------------------- /github/error.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import "fmt" 4 | 5 | type apiError struct { 6 | Resource string `json:"resource"` 7 | Code string `json:"code"` 8 | Field string `json:"field"` 9 | Value string `json:"value"` 10 | } 11 | 12 | func (e *apiError) Error() string { 13 | if e.Value == "" { 14 | return fmt.Sprintf("%s (%s.%s)", e.Code, e.Resource, e.Field) 15 | } 16 | return fmt.Sprintf("%s (%s.%s = %q)", e.Code, e.Resource, e.Field, e.Value) 17 | } 18 | 19 | type apiErrors []apiError 20 | 21 | func (es apiErrors) Error() string { 22 | var s string 23 | for i, e := range es { 24 | if i > 0 { 25 | s += ", " 26 | } 27 | s += e.Error() 28 | } 29 | return s 30 | } 31 | -------------------------------------------------------------------------------- /github/events.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | ) 7 | 8 | // Event represents an event. 9 | type Event struct { 10 | ID int `json:"id"` 11 | Actor *User `json:"actor"` 12 | Event string `json:"event"` 13 | Label *EventLabel `json:"label"` 14 | CommitID string `json:"commit_id"` 15 | Rename *EventRename `json:"rename"` 16 | LockReason string `json:"lock_reason"` 17 | Assignee *User `json:"assignee"` 18 | Assignees []*User `json:"assignees"` 19 | Assigner *User `json:"assigner"` 20 | Reviewer *User `json:"requested_reviewer"` 21 | Reviewers []*User `json:"requested_reviewers"` 22 | RequestedTeam *EventTeam `json:"requested_team"` 23 | DismissedReview *EventDismissedReview `json:"dismissed_review"` 24 | ProjectCard *EventProjectCard `json:"project_card"` 25 | Milestone *EventMilestone `json:"milestone"` 26 | CreatedAt string `json:"created_at"` 27 | } 28 | 29 | // EventLabel ... 30 | type EventLabel struct { 31 | Name string `json:"name"` 32 | Color string `json:"color"` 33 | } 34 | 35 | // EventRename ... 36 | type EventRename struct { 37 | From string `json:"from"` 38 | To string `json:"to"` 39 | } 40 | 41 | // EventTeam ... 42 | type EventTeam struct { 43 | ID int `json:"id"` 44 | Name string `json:"name"` 45 | } 46 | 47 | // EventDismissedReview ... 48 | type EventDismissedReview struct { 49 | State string `json:"state"` 50 | ReviewID int `json:"review_id"` 51 | DismissalMessage string `json:"dismissal_message"` 52 | } 53 | 54 | // EventProjectCard ... 55 | type EventProjectCard struct { 56 | ID int `json:"id"` 57 | ProjectID int `json:"project_id"` 58 | ColumnName string `json:"column_name"` 59 | PreviousColumnName string `json:"previous_column_name"` 60 | } 61 | 62 | // EventMilestone ... 63 | type EventMilestone struct { 64 | Title string `json:"title"` 65 | } 66 | 67 | // Events represents a collection of events. 68 | type Events <-chan interface{} 69 | 70 | // Next emits the next Event. 71 | func (es Events) Next() (*Event, error) { 72 | for x := range es { 73 | switch x := x.(type) { 74 | case error: 75 | return nil, x 76 | case *Event: 77 | return x, nil 78 | } 79 | break 80 | } 81 | return nil, io.EOF 82 | } 83 | 84 | // EventsFromSlice creates Events from a slice. 85 | func EventsFromSlice(xs []*Event) Events { 86 | es := make(chan interface{}) 87 | go func() { 88 | defer close(es) 89 | for _, e := range xs { 90 | es <- e 91 | } 92 | }() 93 | return es 94 | } 95 | 96 | // EventsToSlice collects Events. 97 | func EventsToSlice(es Events) ([]*Event, error) { 98 | xs := []*Event{} 99 | for { 100 | e, err := es.Next() 101 | if err != nil { 102 | if err != io.EOF { 103 | return nil, err 104 | } 105 | return xs, nil 106 | } 107 | xs = append(xs, e) 108 | } 109 | } 110 | 111 | // ListEvents lists the events of an issue. 112 | func (c *client) ListEvents(repo string, issueNumber int) Events { 113 | es := make(chan interface{}) 114 | go func() { 115 | defer close(es) 116 | path := c.url(fmt.Sprintf("/repos/%s/issues/%d/events?per_page=100", repo, issueNumber)) 117 | for { 118 | var xs []*Event 119 | next, err := c.getList(path, &xs) 120 | if err != nil { 121 | es <- fmt.Errorf("ListEvents %s/issues/%d: %w", repo, issueNumber, err) 122 | break 123 | } 124 | for _, x := range xs { 125 | es <- x 126 | } 127 | if next == "" { 128 | break 129 | } 130 | path = next 131 | } 132 | }() 133 | return Events(es) 134 | } 135 | -------------------------------------------------------------------------------- /github/hooks.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | ) 7 | 8 | // Hook represents a hook. 9 | type Hook struct { 10 | Type string `json:"type"` 11 | ID int `json:"id"` 12 | Name string `json:"name"` 13 | Active bool `json:"active"` 14 | Events []string `json:"events"` 15 | Config *HookConfig `json:"config"` 16 | CreatedAt string `json:"created_at"` 17 | UpdatedAt string `json:"updated_at"` 18 | } 19 | 20 | // HookConfig ... 21 | type HookConfig struct { 22 | ContentType string `json:"content_type"` 23 | URL string `json:"url"` 24 | InsecureSsl string `json:"insecure_ssl"` 25 | Secret string `json:"secret,omitempty"` 26 | } 27 | 28 | // Hooks represents a collection of hooks. 29 | type Hooks <-chan interface{} 30 | 31 | // Next emits the next Hook. 32 | func (hs Hooks) Next() (*Hook, error) { 33 | for x := range hs { 34 | switch x := x.(type) { 35 | case error: 36 | return nil, x 37 | case *Hook: 38 | return x, nil 39 | } 40 | break 41 | } 42 | return nil, io.EOF 43 | } 44 | 45 | // HooksFromSlice creates Hooks from a slice. 46 | func HooksFromSlice(xs []*Hook) Hooks { 47 | hs := make(chan interface{}) 48 | go func() { 49 | defer close(hs) 50 | for _, h := range xs { 51 | hs <- h 52 | } 53 | }() 54 | return hs 55 | } 56 | 57 | // HooksToSlice collects Hooks. 58 | func HooksToSlice(hs Hooks) ([]*Hook, error) { 59 | xs := []*Hook{} 60 | for { 61 | h, err := hs.Next() 62 | if err != nil { 63 | if err != io.EOF { 64 | return nil, err 65 | } 66 | return xs, nil 67 | } 68 | xs = append(xs, h) 69 | } 70 | } 71 | 72 | // ListHooks lists the hooks. 73 | func (c *client) ListHooks(repo string) Hooks { 74 | hs := make(chan interface{}) 75 | go func() { 76 | defer close(hs) 77 | path := c.url(fmt.Sprintf("/repos/%s/hooks?per_page=100", repo)) 78 | for { 79 | var xs []*Hook 80 | next, err := c.getList(path, &xs) 81 | if err != nil { 82 | if err.Error() != "Not Found" { 83 | hs <- fmt.Errorf("ListHooks %s: %w", repo, err) 84 | } 85 | break 86 | } 87 | for _, x := range xs { 88 | hs <- x 89 | } 90 | if next == "" { 91 | break 92 | } 93 | path = next 94 | } 95 | }() 96 | return Hooks(hs) 97 | } 98 | 99 | // GetHook gets the hook. 100 | func (c *client) GetHook(repo string, hookID int) (*Hook, error) { 101 | var r Hook 102 | if err := c.get(c.url(fmt.Sprintf("/repos/%s/hooks/%d", repo, hookID)), &r); err != nil { 103 | return nil, fmt.Errorf("GetHook %s: %w", fmt.Sprintf("%s/hooks/%d", repo, hookID), err) 104 | } 105 | return &r, nil 106 | } 107 | 108 | // CreateHookParams represents the parameter for CreateHook API. 109 | type CreateHookParams struct { 110 | Name string `json:"name"` 111 | Active bool `json:"active"` 112 | Events []string `json:"events"` 113 | Config *HookConfig `json:"config"` 114 | } 115 | 116 | // CreateHook creates a hook. 117 | func (c *client) CreateHook(repo string, params *CreateHookParams) (*Hook, error) { 118 | params.Name = "web" 119 | var r Hook 120 | if err := c.post(c.url(fmt.Sprintf("/repos/%s/hooks", repo)), params, &r); err != nil { 121 | return nil, fmt.Errorf("CreateHook %s: %w", fmt.Sprintf("%s/hooks", repo), err) 122 | } 123 | return &r, nil 124 | } 125 | 126 | // UpdateHookParams represents the parameter for UpdateHook API. 127 | type UpdateHookParams struct { 128 | Active bool `json:"active"` 129 | Events []string `json:"events"` 130 | Config *HookConfig `json:"config"` 131 | } 132 | 133 | // UpdateHook updates the hook. 134 | func (c *client) UpdateHook(repo string, hookID int, params *UpdateHookParams) (*Hook, error) { 135 | var r Hook 136 | if err := c.patch(c.url(fmt.Sprintf("/repos/%s/hooks/%d", repo, hookID)), params, &r); err != nil { 137 | return nil, fmt.Errorf("UpdateHook %s: %w", fmt.Sprintf("%s/hooks/%d", repo, hookID), err) 138 | } 139 | return &r, nil 140 | } 141 | -------------------------------------------------------------------------------- /github/import.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import "fmt" 4 | 5 | // Import represents an importing object. 6 | type Import struct { 7 | Issue *ImportIssue `json:"issue"` 8 | Comments []*ImportComment `json:"comments"` 9 | } 10 | 11 | // ImportIssue represents an importing issue. 12 | type ImportIssue struct { 13 | Title string `json:"title"` 14 | Body string `json:"body"` 15 | CreatedAt string `json:"created_at"` 16 | UpdatedAt string `json:"updated_at"` 17 | Closed bool `json:"closed"` 18 | ClosedAt string `json:"closed_at,omitempty"` 19 | Labels []string `json:"labels,omitempty"` 20 | Assignee string `json:"assignee,omitempty"` 21 | Milestone int `json:"milestone,omitempty"` 22 | } 23 | 24 | // ImportComment represents an importing comment. 25 | type ImportComment struct { 26 | Body string `json:"body"` 27 | CreatedAt string `json:"created_at"` 28 | } 29 | 30 | // ImportResult represents the result of import. 31 | type ImportResult struct { 32 | ID int `json:"id"` 33 | Status string `json:"status"` 34 | URL string `json:"url"` 35 | ImportIssuesURL string `json:"import_issues_url"` 36 | RepositoryURL string `json:"repository_url"` 37 | CreatedAt string `json:"created_at"` 38 | UpdatedAt string `json:"updated_at"` 39 | Errors apiErrors `json:"errors"` 40 | } 41 | 42 | // Import imports an importing object. 43 | func (c *client) Import(repo string, params *Import) (*ImportResult, error) { 44 | var r ImportResult 45 | if err := c.post(c.url(fmt.Sprintf("/repos/%s/import/issues", repo)), params, &r); err != nil { 46 | return nil, fmt.Errorf("Import %s: %w", fmt.Sprintf("%s/import/issues", repo), err) 47 | } 48 | return &r, nil 49 | } 50 | 51 | // GetImport gets the importing status. 52 | func (c *client) GetImport(repo string, id int) (*ImportResult, error) { 53 | var r ImportResult 54 | if err := c.get(c.url(fmt.Sprintf("/repos/%s/import/issues/%d", repo, id)), &r); err != nil { 55 | return nil, fmt.Errorf("GetImport %s: %w", fmt.Sprintf("%s/import/issues/%d", repo, id), err) 56 | } 57 | return &r, nil 58 | } 59 | -------------------------------------------------------------------------------- /github/issues.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "strconv" 8 | ) 9 | 10 | // Issue represents an issue. 11 | type Issue struct { 12 | ID int `json:"id"` 13 | Number int `json:"number"` 14 | Title string `json:"title"` 15 | State IssueState `json:"state"` 16 | Body string `json:"body"` 17 | HTMLURL string `json:"html_url"` 18 | User *User `json:"user"` 19 | Assignee *User `json:"assignee"` 20 | Assignees []*User `json:"assignees"` 21 | CreatedAt string `json:"created_at"` 22 | UpdatedAt string `json:"updated_at"` 23 | ClosedAt string `json:"closed_at,omitempty"` 24 | ClosedBy *User `json:"closed_by,omitempty"` 25 | Labels []*Label `json:"labels"` 26 | PullRequest *IssuePullRequest `json:"pull_request"` 27 | Milestone *Milestone `json:"milestone"` 28 | } 29 | 30 | // IssueState ... 31 | type IssueState int 32 | 33 | // IssueState ... 34 | const ( 35 | IssueStateOpen IssueState = iota + 1 36 | IssueStateClosed 37 | ) 38 | 39 | var stringToIssueState = map[string]IssueState{ 40 | "open": IssueStateOpen, 41 | "closed": IssueStateClosed, 42 | } 43 | 44 | var issueStateToString = map[IssueState]string{ 45 | IssueStateOpen: "open", 46 | IssueStateClosed: "closed", 47 | } 48 | 49 | // UnmarshalJSON implements json.Unmarshaler 50 | func (t *IssueState) UnmarshalJSON(b []byte) error { 51 | var s string 52 | if err := json.Unmarshal(b, &s); err != nil { 53 | return err 54 | } 55 | if x, ok := stringToIssueState[s]; ok { 56 | *t = x 57 | return nil 58 | } 59 | return fmt.Errorf("unknown issue state: %q", s) 60 | } 61 | 62 | // MarshalJSON implements json.Marshaler 63 | func (t IssueState) MarshalJSON() ([]byte, error) { 64 | return json.Marshal(t.String()) 65 | } 66 | 67 | // String implements Stringer 68 | func (t IssueState) String() string { 69 | return issueStateToString[t] 70 | } 71 | 72 | // GoString implements GoString 73 | func (t IssueState) GoString() string { 74 | return strconv.Quote(t.String()) 75 | } 76 | 77 | // IssueType ... 78 | type IssueType int 79 | 80 | // IssueType ... 81 | const ( 82 | IssueTypeIssue IssueType = iota + 1 83 | IssueTypePullReq 84 | ) 85 | 86 | func (t IssueType) String() string { 87 | switch t { 88 | case IssueTypeIssue: 89 | return "issue" 90 | case IssueTypePullReq: 91 | return "pull request" 92 | default: 93 | return "" 94 | } 95 | } 96 | 97 | // Type returns IssueTypePullReq ("pull request") or IssueTypeIssue ("issue"). 98 | func (i *Issue) Type() IssueType { 99 | if i.PullRequest != nil { 100 | return IssueTypePullReq 101 | } 102 | return IssueTypeIssue 103 | } 104 | 105 | // IssuePullRequest represents the pull request information of an issue. 106 | type IssuePullRequest struct { 107 | URL string `json:"url"` 108 | HTMLURL string `json:"html_url"` 109 | DiffURL string `json:"diff_url"` 110 | PatchURL string `json:"patch_url"` 111 | } 112 | 113 | // Issues represents a collection of issues. 114 | type Issues <-chan interface{} 115 | 116 | // Next emits the next Issue. 117 | func (is Issues) Next() (*Issue, error) { 118 | for x := range is { 119 | switch x := x.(type) { 120 | case error: 121 | return nil, x 122 | case *Issue: 123 | return x, nil 124 | } 125 | break 126 | } 127 | return nil, io.EOF 128 | } 129 | 130 | // IssuesFromSlice creates Issues from a slice. 131 | func IssuesFromSlice(xs []*Issue) Issues { 132 | is := make(chan interface{}) 133 | go func() { 134 | defer close(is) 135 | for _, i := range xs { 136 | is <- i 137 | } 138 | }() 139 | return is 140 | } 141 | 142 | // IssuesToSlice collects Issues. 143 | func IssuesToSlice(is Issues) ([]*Issue, error) { 144 | xs := []*Issue{} 145 | for { 146 | i, err := is.Next() 147 | if err != nil { 148 | if err != io.EOF { 149 | return nil, err 150 | } 151 | return xs, nil 152 | } 153 | xs = append(xs, i) 154 | } 155 | } 156 | 157 | // ListIssuesParams represents the parameter for ListIssues API. 158 | type ListIssuesParams struct { 159 | Filter ListIssuesParamFilter 160 | State ListIssuesParamState 161 | Sort ListIssuesParamSort 162 | Direction ListIssuesParamDirection 163 | } 164 | 165 | // ListIssuesParamFilter ... 166 | type ListIssuesParamFilter int 167 | 168 | // ListIssuesParamFilter ... 169 | const ( 170 | ListIssuesParamFilterDefault ListIssuesParamFilter = iota + 1 171 | ListIssuesParamFilterAssigned 172 | ListIssuesParamFilterCreated 173 | ListIssuesParamFilterMentioned 174 | ListIssuesParamFilterSubscribed 175 | ListIssuesParamFilterAll 176 | ) 177 | 178 | func (f ListIssuesParamFilter) String() string { 179 | switch f { 180 | case ListIssuesParamFilterAssigned: 181 | return "assigned" 182 | case ListIssuesParamFilterCreated: 183 | return "created" 184 | case ListIssuesParamFilterMentioned: 185 | return "mentioned" 186 | case ListIssuesParamFilterSubscribed: 187 | return "subscribed" 188 | case ListIssuesParamFilterAll: 189 | return "all" 190 | default: 191 | return "" 192 | } 193 | } 194 | 195 | // ListIssuesParamState ... 196 | type ListIssuesParamState int 197 | 198 | // ListIssuesParamState ... 199 | const ( 200 | ListIssuesParamStateDefault ListIssuesParamState = iota + 1 201 | ListIssuesParamStateOpen 202 | ListIssuesParamStateClosed 203 | ListIssuesParamStateAll 204 | ) 205 | 206 | func (f ListIssuesParamState) String() string { 207 | switch f { 208 | case ListIssuesParamStateOpen: 209 | return "open" 210 | case ListIssuesParamStateClosed: 211 | return "closed" 212 | case ListIssuesParamStateAll: 213 | return "all" 214 | default: 215 | return "" 216 | } 217 | } 218 | 219 | // ListIssuesParamSort ... 220 | type ListIssuesParamSort int 221 | 222 | // ListIssuesParamSort ... 223 | const ( 224 | ListIssuesParamSortDefault ListIssuesParamSort = iota + 1 225 | ListIssuesParamSortCreated 226 | ListIssuesParamSortUpdated 227 | ListIssuesParamSortComments 228 | ) 229 | 230 | func (f ListIssuesParamSort) String() string { 231 | switch f { 232 | case ListIssuesParamSortCreated: 233 | return "created" 234 | case ListIssuesParamSortUpdated: 235 | return "updated" 236 | case ListIssuesParamSortComments: 237 | return "comments" 238 | default: 239 | return "" 240 | } 241 | } 242 | 243 | // ListIssuesParamDirection ... 244 | type ListIssuesParamDirection int 245 | 246 | // ListIssuesParamDirection ... 247 | const ( 248 | ListIssuesParamDirectionDefault ListIssuesParamDirection = iota + 1 249 | ListIssuesParamDirectionAsc 250 | ListIssuesParamDirectionDesc 251 | ) 252 | 253 | func (f ListIssuesParamDirection) String() string { 254 | switch f { 255 | case ListIssuesParamDirectionAsc: 256 | return "asc" 257 | case ListIssuesParamDirectionDesc: 258 | return "desc" 259 | default: 260 | return "" 261 | } 262 | } 263 | 264 | func listIssuesPath(repo string, params *ListIssuesParams) string { 265 | return newPath(fmt.Sprintf("/repos/%s/issues", repo)). 266 | query("filter", params.Filter.String()). 267 | query("state", params.State.String()). 268 | query("sort", params.Sort.String()). 269 | query("direction", params.Direction.String()). 270 | query("per_page", "100"). 271 | String() 272 | } 273 | 274 | // ListIssues lists the issues. 275 | func (c *client) ListIssues(repo string, params *ListIssuesParams) Issues { 276 | is := make(chan interface{}) 277 | go func() { 278 | defer close(is) 279 | path := c.url(listIssuesPath(repo, params)) 280 | for { 281 | var xs []*Issue 282 | next, err := c.getList(path, &xs) 283 | if err != nil { 284 | is <- fmt.Errorf("ListIssues %s: %w", repo, err) 285 | break 286 | } 287 | for _, x := range xs { 288 | is <- x 289 | } 290 | if next == "" { 291 | break 292 | } 293 | path = next 294 | } 295 | }() 296 | return Issues(is) 297 | } 298 | 299 | func (c *client) GetIssue(repo string, issueNumber int) (*Issue, error) { 300 | var r Issue 301 | if err := c.get(c.url(fmt.Sprintf("/repos/%s/issues/%d", repo, issueNumber)), &r); err != nil { 302 | return nil, fmt.Errorf("GetIssue %s: %w", fmt.Sprintf("%s/issues/%d", repo, issueNumber), err) 303 | } 304 | return &r, nil 305 | } 306 | 307 | func (c *client) AddAssignees(repo string, issueNumber int, assignees []string) error { 308 | var r Issue 309 | params := map[string][]string{"assignees": assignees} 310 | if err := c.post(c.url(fmt.Sprintf("/repos/%s/issues/%d/assignees", repo, issueNumber)), params, &r); err != nil { 311 | return fmt.Errorf("AddAssignees %s: %w", fmt.Sprintf("%s/issues/%d/assignees", repo, issueNumber), err) 312 | } 313 | return nil 314 | } 315 | -------------------------------------------------------------------------------- /github/labels.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | ) 7 | 8 | // Label represents a label. 9 | type Label struct { 10 | ID int `json:"id"` 11 | Name string `json:"name"` 12 | Description string `json:"description"` 13 | Color string `json:"color"` 14 | Default bool `json:"default"` 15 | } 16 | 17 | // Labels represents a collection of labels. 18 | type Labels <-chan interface{} 19 | 20 | // Next emits the next Label. 21 | func (ls Labels) Next() (*Label, error) { 22 | for x := range ls { 23 | switch x := x.(type) { 24 | case error: 25 | return nil, x 26 | case *Label: 27 | return x, nil 28 | } 29 | break 30 | } 31 | return nil, io.EOF 32 | } 33 | 34 | // LabelsFromSlice creates Labels from a slice. 35 | func LabelsFromSlice(xs []*Label) Labels { 36 | ls := make(chan interface{}) 37 | go func() { 38 | defer close(ls) 39 | for _, l := range xs { 40 | ls <- l 41 | } 42 | }() 43 | return ls 44 | } 45 | 46 | // LabelsToSlice collects Labels. 47 | func LabelsToSlice(ls Labels) ([]*Label, error) { 48 | xs := []*Label{} 49 | for { 50 | l, err := ls.Next() 51 | if err != nil { 52 | if err != io.EOF { 53 | return nil, err 54 | } 55 | return xs, nil 56 | } 57 | xs = append(xs, l) 58 | } 59 | } 60 | 61 | // ListLabels lists the labels of an issue. 62 | func (c *client) ListLabels(repo string) Labels { 63 | ls := make(chan interface{}) 64 | go func() { 65 | defer close(ls) 66 | path := c.url(fmt.Sprintf("/repos/%s/labels?per_page=100", repo)) 67 | for { 68 | var xs []*Label 69 | next, err := c.getList(path, &xs) 70 | if err != nil { 71 | ls <- fmt.Errorf("ListLabels %s: %w", repo, err) 72 | break 73 | } 74 | for _, x := range xs { 75 | ls <- x 76 | } 77 | if next == "" { 78 | break 79 | } 80 | path = next 81 | } 82 | }() 83 | return Labels(ls) 84 | } 85 | 86 | // CreateLabelParams represents the parameter for CreateLabel API. 87 | type CreateLabelParams struct { 88 | Name string `json:"name"` 89 | Description string `json:"description"` 90 | Color string `json:"color"` 91 | } 92 | 93 | func (c *client) CreateLabel(repo string, params *CreateLabelParams) (*Label, error) { 94 | var r Label 95 | if err := c.post(c.url(fmt.Sprintf("/repos/%s/labels", repo)), params, &r); err != nil { 96 | return nil, fmt.Errorf("CreateLabel %s: %w", fmt.Sprintf("%s/labels", repo), err) 97 | } 98 | return &r, nil 99 | } 100 | 101 | // UpdateLabelParams represents the parameter for UpdateLabel API. 102 | type UpdateLabelParams struct { 103 | Name string `json:"new_name"` 104 | Description string `json:"description"` 105 | Color string `json:"color"` 106 | } 107 | 108 | func (c *client) UpdateLabel(repo, name string, params *UpdateLabelParams) (*Label, error) { 109 | var r Label 110 | if err := c.patch(c.url(fmt.Sprintf("/repos/%s/labels/%s", repo, name)), params, &r); err != nil { 111 | return nil, fmt.Errorf("UpdateLabel %s: %w", fmt.Sprintf("%s/labels/%s", repo, name), err) 112 | } 113 | return &r, nil 114 | } 115 | -------------------------------------------------------------------------------- /github/logger.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import "net/http" 4 | 5 | // Logger ... 6 | type Logger struct { 7 | preRequestCallback func(*http.Request) 8 | postRequestCallback func(*http.Response, error) 9 | } 10 | 11 | // LoggerOption is an option of Logger. 12 | type LoggerOption func(*Logger) 13 | 14 | // NewLogger creates a new Logger. 15 | func NewLogger(opts ...LoggerOption) *Logger { 16 | l := &Logger{} 17 | for _, opt := range opts { 18 | opt(l) 19 | } 20 | return l 21 | } 22 | 23 | func (l *Logger) preRequest(r *http.Request) { 24 | if l.preRequestCallback != nil { 25 | l.preRequestCallback(r) 26 | } 27 | } 28 | 29 | // LoggerPreRequest ... 30 | func LoggerPreRequest(callback func(*http.Request)) LoggerOption { 31 | return func(l *Logger) { 32 | l.preRequestCallback = callback 33 | } 34 | } 35 | 36 | func (l *Logger) postRequest(r *http.Response, err error) { 37 | if l.postRequestCallback != nil { 38 | l.postRequestCallback(r, err) 39 | } 40 | } 41 | 42 | // LoggerPostRequest ... 43 | func LoggerPostRequest(callback func(*http.Response, error)) LoggerOption { 44 | return func(l *Logger) { 45 | l.postRequestCallback = callback 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /github/members.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | ) 7 | 8 | // Member represents a member. 9 | type Member User 10 | 11 | // ToUser converts *Member to *User. 12 | func (m *Member) ToUser() *User { 13 | u := User(*m) 14 | return &u 15 | } 16 | 17 | // Members represents a collection of comments. 18 | type Members <-chan interface{} 19 | 20 | // Next emits the next Member. 21 | func (ms Members) Next() (*Member, error) { 22 | for x := range ms { 23 | switch x := x.(type) { 24 | case error: 25 | return nil, x 26 | case *Member: 27 | return x, nil 28 | } 29 | break 30 | } 31 | return nil, io.EOF 32 | } 33 | 34 | // MembersFromSlice creates Members from a slice. 35 | func MembersFromSlice(xs []*Member) Members { 36 | ms := make(chan interface{}) 37 | go func() { 38 | defer close(ms) 39 | for _, m := range xs { 40 | ms <- m 41 | } 42 | }() 43 | return ms 44 | } 45 | 46 | // MembersToSlice collects Members. 47 | func MembersToSlice(ms Members) ([]*Member, error) { 48 | xs := []*Member{} 49 | for { 50 | m, err := ms.Next() 51 | if err != nil { 52 | if err != io.EOF { 53 | return nil, err 54 | } 55 | return xs, nil 56 | } 57 | xs = append(xs, m) 58 | } 59 | } 60 | 61 | // ListMembers lists the members of the organization. 62 | func (c *client) ListMembers(org string) Members { 63 | ms := make(chan interface{}) 64 | go func() { 65 | defer close(ms) 66 | path := c.url(fmt.Sprintf("/orgs/%s/members?per_page=100", org)) 67 | for { 68 | var xs []*Member 69 | next, err := c.getList(path, &xs) 70 | if err != nil { 71 | if err.Error() != "Not Found" { 72 | ms <- fmt.Errorf("ListMembers %s: %w", org, err) 73 | } 74 | break 75 | } 76 | for _, x := range xs { 77 | ms <- x 78 | } 79 | if next == "" { 80 | break 81 | } 82 | path = next 83 | } 84 | }() 85 | return Members(ms) 86 | } 87 | -------------------------------------------------------------------------------- /github/milestones.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "sort" 8 | "strconv" 9 | ) 10 | 11 | // Milestone represents a milestone. 12 | type Milestone struct { 13 | ID int `json:"id"` 14 | HTMLURL string `json:"html_url"` 15 | Number int `json:"number"` 16 | Title string `json:"title"` 17 | Description string `json:"description"` 18 | State MilestoneState `json:"state"` 19 | OpenMilestones int `json:"open_milestones"` 20 | ClosedMilestones int `json:"closed_milestones"` 21 | Creator *User `json:"creator"` 22 | CreatedAt string `json:"created_at"` 23 | UpdatedAt string `json:"updated_at"` 24 | ClosedAt string `json:"closed_at"` 25 | DueOn string `json:"due_on"` 26 | } 27 | 28 | // MilestoneState ... 29 | type MilestoneState int 30 | 31 | // MilestoneState ... 32 | const ( 33 | MilestoneStateOpen MilestoneState = iota + 1 34 | MilestoneStateClosed 35 | ) 36 | 37 | var stringToMilestoneState = map[string]MilestoneState{ 38 | "open": MilestoneStateOpen, 39 | "closed": MilestoneStateClosed, 40 | } 41 | 42 | var milestoneStateToString = map[MilestoneState]string{ 43 | MilestoneStateOpen: "open", 44 | MilestoneStateClosed: "closed", 45 | } 46 | 47 | // UnmarshalJSON implements json.Unmarshaler 48 | func (t *MilestoneState) UnmarshalJSON(b []byte) error { 49 | var s string 50 | if err := json.Unmarshal(b, &s); err != nil { 51 | return err 52 | } 53 | if x, ok := stringToMilestoneState[s]; ok { 54 | *t = x 55 | return nil 56 | } 57 | return fmt.Errorf("unknown milestone state: %q", s) 58 | } 59 | 60 | // MarshalJSON implements json.Marshaler 61 | func (t MilestoneState) MarshalJSON() ([]byte, error) { 62 | return json.Marshal(t.String()) 63 | } 64 | 65 | // String implements Stringer 66 | func (t MilestoneState) String() string { 67 | return milestoneStateToString[t] 68 | } 69 | 70 | // GoString implements GoString 71 | func (t MilestoneState) GoString() string { 72 | return strconv.Quote(t.String()) 73 | } 74 | 75 | // Milestones represents a collection of milestones. 76 | type Milestones <-chan interface{} 77 | 78 | // Next emits the next Milestone. 79 | func (ms Milestones) Next() (*Milestone, error) { 80 | for x := range ms { 81 | switch x := x.(type) { 82 | case error: 83 | return nil, x 84 | case *Milestone: 85 | return x, nil 86 | } 87 | break 88 | } 89 | return nil, io.EOF 90 | } 91 | 92 | // MilestonesFromSlice creates Milestones from a slice. 93 | func MilestonesFromSlice(xs []*Milestone) Milestones { 94 | ms := make(chan interface{}) 95 | go func() { 96 | defer close(ms) 97 | for _, p := range xs { 98 | ms <- p 99 | } 100 | }() 101 | return ms 102 | } 103 | 104 | // MilestonesToSlice collects Milestones. 105 | func MilestonesToSlice(ms Milestones) ([]*Milestone, error) { 106 | xs := []*Milestone{} 107 | for { 108 | p, err := ms.Next() 109 | if err != nil { 110 | if err != io.EOF { 111 | return nil, err 112 | } 113 | sort.Slice(xs, func(i, j int) bool { 114 | return xs[i].Number < xs[j].Number 115 | }) 116 | return xs, nil 117 | } 118 | xs = append(xs, p) 119 | } 120 | } 121 | 122 | // ListMilestonesParams represents the parameter for ListMilestones API. 123 | type ListMilestonesParams struct { 124 | State ListMilestonesParamState 125 | Direction ListMilestonesParamDirection 126 | Sort ListMilestonesParamSort 127 | } 128 | 129 | // ListMilestonesParamState ... 130 | type ListMilestonesParamState int 131 | 132 | // ListMilestonesParamState ... 133 | const ( 134 | ListMilestonesParamStateDefault ListMilestonesParamState = iota + 1 135 | ListMilestonesParamStateOpen 136 | ListMilestonesParamStateClosed 137 | ListMilestonesParamStateAll 138 | ) 139 | 140 | func (f ListMilestonesParamState) String() string { 141 | switch f { 142 | case ListMilestonesParamStateOpen: 143 | return "open" 144 | case ListMilestonesParamStateClosed: 145 | return "closed" 146 | case ListMilestonesParamStateAll: 147 | return "all" 148 | default: 149 | return "" 150 | } 151 | } 152 | 153 | // ListMilestonesParamSort ... 154 | type ListMilestonesParamSort int 155 | 156 | // ListMilestonesParamSort ... 157 | const ( 158 | ListMilestonesParamSortDefault ListMilestonesParamSort = iota + 1 159 | ListMilestonesParamSortDueOn 160 | ListMilestonesParamSortCompleteness 161 | ) 162 | 163 | func (f ListMilestonesParamSort) String() string { 164 | switch f { 165 | case ListMilestonesParamSortDueOn: 166 | return "due_on" 167 | case ListMilestonesParamSortCompleteness: 168 | return "completeness" 169 | default: 170 | return "" 171 | } 172 | } 173 | 174 | // ListMilestonesParamDirection ... 175 | type ListMilestonesParamDirection int 176 | 177 | // ListMilestonesParamDirection ... 178 | const ( 179 | ListMilestonesParamDirectionDefault ListMilestonesParamDirection = iota + 1 180 | ListMilestonesParamDirectionAsc 181 | ListMilestonesParamDirectionDesc 182 | ) 183 | 184 | func (f ListMilestonesParamDirection) String() string { 185 | switch f { 186 | case ListMilestonesParamDirectionAsc: 187 | return "asc" 188 | case ListMilestonesParamDirectionDesc: 189 | return "desc" 190 | default: 191 | return "" 192 | } 193 | } 194 | 195 | func listMilestonesPath(repo string, params *ListMilestonesParams) string { 196 | return newPath(fmt.Sprintf("/repos/%s/milestones", repo)). 197 | query("state", params.State.String()). 198 | query("sort", params.Sort.String()). 199 | query("direction", params.Direction.String()). 200 | query("per_page", "100"). 201 | String() 202 | } 203 | 204 | // ListMilestones lists the milestones. 205 | func (c *client) ListMilestones(repo string, params *ListMilestonesParams) Milestones { 206 | ms := make(chan interface{}) 207 | go func() { 208 | defer close(ms) 209 | path := c.url(listMilestonesPath(repo, params)) 210 | for { 211 | var xs []*Milestone 212 | next, err := c.getList(path, &xs) 213 | if err != nil { 214 | ms <- fmt.Errorf("ListMilestones %s: %w", repo, err) 215 | break 216 | } 217 | for _, x := range xs { 218 | ms <- x 219 | } 220 | if next == "" { 221 | break 222 | } 223 | path = next 224 | } 225 | }() 226 | return Milestones(ms) 227 | } 228 | 229 | func (c *client) GetMilestone(repo string, milestoneNumber int) (*Milestone, error) { 230 | var r Milestone 231 | if err := c.get(c.url(fmt.Sprintf("/repos/%s/milestones/%d", repo, milestoneNumber)), &r); err != nil { 232 | return nil, fmt.Errorf("GetMilestone %s: %w", fmt.Sprintf("%s/milestones/%d", repo, milestoneNumber), err) 233 | } 234 | return &r, nil 235 | } 236 | 237 | // CreateMilestoneParams represents the parameter for CreateMilestone API. 238 | type CreateMilestoneParams struct { 239 | Title string `json:"title"` 240 | Description string `json:"description"` 241 | State MilestoneState `json:"state"` 242 | DueOn string `json:"due_on,omitempty"` 243 | } 244 | 245 | // CreateMilestone creates a milestone. 246 | func (c *client) CreateMilestone(repo string, params *CreateMilestoneParams) (*Milestone, error) { 247 | var r Milestone 248 | if err := c.post(c.url(fmt.Sprintf("/repos/%s/milestones", repo)), params, &r); err != nil { 249 | return nil, fmt.Errorf("CreateMilestone %s: %w", fmt.Sprintf("%s/milestones", repo), err) 250 | } 251 | return &r, nil 252 | } 253 | 254 | // UpdateMilestoneParams represents the parameter for UpdateMilestone API. 255 | type UpdateMilestoneParams CreateMilestoneParams 256 | 257 | // UpdateMilestone updates the milestone. 258 | func (c *client) UpdateMilestone(repo string, milestoneNumber int, params *UpdateMilestoneParams) (*Milestone, error) { 259 | var r Milestone 260 | if err := c.patch(c.url(fmt.Sprintf("/repos/%s/milestones/%d", repo, milestoneNumber)), params, &r); err != nil { 261 | return nil, fmt.Errorf("UpdateMilestone %s: %w", fmt.Sprintf("%s/milestones/%d", repo, milestoneNumber), err) 262 | } 263 | return &r, nil 264 | } 265 | 266 | // DeleteMilestone deletes the milestone. 267 | func (c *client) DeleteMilestone(repo string, milestoneNumber int) error { 268 | if err := c.delete(c.url(fmt.Sprintf("/repos/%s/milestones/%d", repo, milestoneNumber))); err != nil { 269 | return fmt.Errorf("DeleteMilestone %s: %w", fmt.Sprintf("%s/milestones/%d", repo, milestoneNumber), err) 270 | } 271 | return nil 272 | } 273 | -------------------------------------------------------------------------------- /github/path.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import "net/url" 4 | 5 | type path struct { 6 | path string 7 | params url.Values 8 | } 9 | 10 | func newPath(x string) path { 11 | return path{path: x, params: url.Values{}} 12 | } 13 | 14 | func (p path) query(key, value string) path { 15 | if value != "" { 16 | p.params.Add(key, value) 17 | } 18 | return p 19 | } 20 | 21 | func (p path) String() string { 22 | if len(p.params) == 0 { 23 | return p.path 24 | } 25 | return p.path + "?" + p.params.Encode() 26 | } 27 | -------------------------------------------------------------------------------- /github/project_cards.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | // ProjectCard represents a project card. 12 | type ProjectCard struct { 13 | ID int `json:"id"` 14 | Note string `json:"note"` 15 | Archived bool `json:"archived"` 16 | Creator *User `json:"creator"` 17 | ContentURL string `json:"content_url"` 18 | CreatedAt string `json:"created_at"` 19 | UpdatedAt string `json:"updated_at"` 20 | } 21 | 22 | // GetIssueNumber ... 23 | func (c *ProjectCard) GetIssueNumber() int { 24 | if i := strings.LastIndexByte(c.ContentURL, '/'); i >= 0 { 25 | j, err := strconv.Atoi(c.ContentURL[i+1:]) 26 | if err != nil { 27 | return -1 28 | } 29 | return j 30 | } 31 | return -1 32 | } 33 | 34 | // ProjectCards represents a collection of project cards. 35 | type ProjectCards <-chan interface{} 36 | 37 | // Next emits the next ProjectCard. 38 | func (ps ProjectCards) Next() (*ProjectCard, error) { 39 | for x := range ps { 40 | switch x := x.(type) { 41 | case error: 42 | return nil, x 43 | case *ProjectCard: 44 | return x, nil 45 | } 46 | break 47 | } 48 | return nil, io.EOF 49 | } 50 | 51 | // ProjectCardsFromSlice creates ProjectCards from a slice. 52 | func ProjectCardsFromSlice(xs []*ProjectCard) ProjectCards { 53 | ps := make(chan interface{}) 54 | go func() { 55 | defer close(ps) 56 | for _, p := range xs { 57 | ps <- p 58 | } 59 | }() 60 | return ps 61 | } 62 | 63 | // ProjectCardsToSlice collects ProjectCards. 64 | func ProjectCardsToSlice(ps ProjectCards) ([]*ProjectCard, error) { 65 | xs := []*ProjectCard{} 66 | for { 67 | p, err := ps.Next() 68 | if err != nil { 69 | if err != io.EOF { 70 | return nil, err 71 | } 72 | return xs, nil 73 | } 74 | xs = append(xs, p) 75 | } 76 | } 77 | 78 | // ListProjectCards lists the project cards. 79 | func (c *client) ListProjectCards(columnID int) ProjectCards { 80 | ps := make(chan interface{}) 81 | go func() { 82 | defer close(ps) 83 | path := c.url(fmt.Sprintf("/projects/columns/%d/cards?per_page=100", columnID)) 84 | for { 85 | var xs []*ProjectCard 86 | next, err := c.getList(path, &xs) 87 | if err != nil { 88 | ps <- fmt.Errorf("ListProjectCards %d: %w", columnID, err) 89 | break 90 | } 91 | for _, x := range xs { 92 | ps <- x 93 | } 94 | if next == "" { 95 | break 96 | } 97 | path = next 98 | } 99 | }() 100 | return ProjectCards(ps) 101 | } 102 | 103 | func (c *client) GetProjectCard(projectCardID int) (*ProjectCard, error) { 104 | var r ProjectCard 105 | if err := c.get(c.url(fmt.Sprintf("/projects/columns/cards/%d", projectCardID)), &r); err != nil { 106 | return nil, fmt.Errorf("GetProjectCard %s: %w", fmt.Sprintf("projects/columns/cards/%d", projectCardID), err) 107 | } 108 | return &r, nil 109 | } 110 | 111 | // CreateProjectCardParams represents the parameter for CreateProjectCard API. 112 | type CreateProjectCardParams struct { 113 | Note string `json:"note,omitempty"` 114 | ContentID int `json:"content_id,omitempty"` 115 | ContentType ProjectCardContentType `json:"content_type,omitempty"` 116 | } 117 | 118 | // ProjectCardContentType ... 119 | type ProjectCardContentType int 120 | 121 | // ProjectCardContentType ... 122 | const ( 123 | ProjectCardContentTypeIssue ProjectCardContentType = iota + 1 124 | ProjectCardContentTypePullRequest 125 | ) 126 | 127 | var stringToProjectCardContentType = map[string]ProjectCardContentType{ 128 | "Issue": ProjectCardContentTypeIssue, 129 | "PullRequest": ProjectCardContentTypePullRequest, 130 | } 131 | 132 | var projectCardContentTypeToString = map[ProjectCardContentType]string{ 133 | ProjectCardContentTypeIssue: "Issue", 134 | ProjectCardContentTypePullRequest: "PullRequest", 135 | } 136 | 137 | // UnmarshalJSON implements json.Unmarshaler 138 | func (t *ProjectCardContentType) UnmarshalJSON(b []byte) error { 139 | var s string 140 | if err := json.Unmarshal(b, &s); err != nil { 141 | return err 142 | } 143 | if x, ok := stringToProjectCardContentType[s]; ok { 144 | *t = x 145 | return nil 146 | } 147 | return fmt.Errorf("unknown project card content type: %q", s) 148 | } 149 | 150 | // MarshalJSON implements json.Marshaler 151 | func (t ProjectCardContentType) MarshalJSON() ([]byte, error) { 152 | return json.Marshal(t.String()) 153 | } 154 | 155 | // String implements Stringer 156 | func (t ProjectCardContentType) String() string { 157 | return projectCardContentTypeToString[t] 158 | } 159 | 160 | // GoString implements GoString 161 | func (t ProjectCardContentType) GoString() string { 162 | return strconv.Quote(t.String()) 163 | } 164 | 165 | // CreateProjectCard creates a project card. 166 | func (c *client) CreateProjectCard(columnID int, params *CreateProjectCardParams) (*ProjectCard, error) { 167 | var r ProjectCard 168 | if err := c.post(c.url(fmt.Sprintf("/projects/columns/%d/cards", columnID)), params, &r); err != nil { 169 | return nil, fmt.Errorf("CreateProjectCard %s: %w", fmt.Sprintf("projects/columns/%d/cards", columnID), err) 170 | } 171 | return &r, nil 172 | } 173 | 174 | // UpdateProjectCardParams represents the parameter for UpdateProjectCard API. 175 | type UpdateProjectCardParams struct { 176 | Note string `json:"note,omitempty"` 177 | Archived bool `json:"archived,omitempty"` 178 | } 179 | 180 | // UpdateProjectCard updates the project card. 181 | func (c *client) UpdateProjectCard(projectCardID int, params *UpdateProjectCardParams) (*ProjectCard, error) { 182 | var r ProjectCard 183 | if err := c.patch(c.url(fmt.Sprintf("/projects/columns/cards/%d", projectCardID)), params, &r); err != nil { 184 | return nil, fmt.Errorf("UpdateProjectCard %s: %w", fmt.Sprintf("projects/columns/cards/%d", projectCardID), err) 185 | } 186 | return &r, nil 187 | } 188 | 189 | // MoveProjectCardParams represents the parameter for MoveProjectCard API. 190 | type MoveProjectCardParams struct { 191 | Position string `json:"position"` 192 | ColumnID bool `json:"column_id,omitempty"` 193 | } 194 | 195 | // MoveProjectCard moves the project card. 196 | func (c *client) MoveProjectCard(projectCardID int, params *MoveProjectCardParams) (*ProjectCard, error) { 197 | var r ProjectCard 198 | if err := c.post(c.url(fmt.Sprintf("/projects/columns/cards/%d/moves", projectCardID)), params, &r); err != nil { 199 | return nil, fmt.Errorf("MoveProjectCard %s: %w", fmt.Sprintf("projects/columns/cards/%d/moves", projectCardID), err) 200 | } 201 | return &r, nil 202 | } 203 | -------------------------------------------------------------------------------- /github/project_columns.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | ) 7 | 8 | // ProjectColumn represents a project column. 9 | type ProjectColumn struct { 10 | ID int `json:"id"` 11 | Name string `json:"name"` 12 | URL string `json:"url"` 13 | CreatedAt string `json:"created_at"` 14 | UpdatedAt string `json:"updated_at"` 15 | } 16 | 17 | // ProjectColumns represents a collection of project columns. 18 | type ProjectColumns <-chan interface{} 19 | 20 | // Next emits the next ProjectColumn. 21 | func (ps ProjectColumns) Next() (*ProjectColumn, error) { 22 | for x := range ps { 23 | switch x := x.(type) { 24 | case error: 25 | return nil, x 26 | case *ProjectColumn: 27 | return x, nil 28 | } 29 | break 30 | } 31 | return nil, io.EOF 32 | } 33 | 34 | // ProjectColumnsFromSlice creates ProjectColumns from a slice. 35 | func ProjectColumnsFromSlice(xs []*ProjectColumn) ProjectColumns { 36 | ps := make(chan interface{}) 37 | go func() { 38 | defer close(ps) 39 | for _, p := range xs { 40 | ps <- p 41 | } 42 | }() 43 | return ps 44 | } 45 | 46 | // ProjectColumnsToSlice collects ProjectColumns. 47 | func ProjectColumnsToSlice(ps ProjectColumns) ([]*ProjectColumn, error) { 48 | xs := []*ProjectColumn{} 49 | for { 50 | p, err := ps.Next() 51 | if err != nil { 52 | if err != io.EOF { 53 | return nil, err 54 | } 55 | return xs, nil 56 | } 57 | xs = append(xs, p) 58 | } 59 | } 60 | 61 | // ListProjectColumns lists the project columns. 62 | func (c *client) ListProjectColumns(projectID int) ProjectColumns { 63 | ps := make(chan interface{}) 64 | go func() { 65 | defer close(ps) 66 | path := c.url(fmt.Sprintf("/projects/%d/columns?per_page=100", projectID)) 67 | for { 68 | var xs []*ProjectColumn 69 | next, err := c.getList(path, &xs) 70 | if err != nil { 71 | ps <- fmt.Errorf("ListProjectColumns %d: %w", projectID, err) 72 | break 73 | } 74 | for _, x := range xs { 75 | ps <- x 76 | } 77 | if next == "" { 78 | break 79 | } 80 | path = next 81 | } 82 | }() 83 | return ProjectColumns(ps) 84 | } 85 | 86 | func (c *client) GetProjectColumn(projectColumnID int) (*ProjectColumn, error) { 87 | var r ProjectColumn 88 | if err := c.get(c.url(fmt.Sprintf("/projects/columns/%d", projectColumnID)), &r); err != nil { 89 | return nil, fmt.Errorf("GetProjectColumn %s: %w", fmt.Sprintf("projects/columns/%d", projectColumnID), err) 90 | } 91 | return &r, nil 92 | } 93 | 94 | // CreateProjectColumn creates a project column. 95 | func (c *client) CreateProjectColumn(projectID int, name string) (*ProjectColumn, error) { 96 | var r ProjectColumn 97 | if err := c.post(c.url(fmt.Sprintf("/projects/%d/columns", projectID)), map[string]string{"name": name}, &r); err != nil { 98 | return nil, err 99 | } 100 | return &r, nil 101 | } 102 | 103 | // UpdateProjectColumn updates the project column. 104 | func (c *client) UpdateProjectColumn(projectColumnID int, name string) (*ProjectColumn, error) { 105 | var r ProjectColumn 106 | if err := c.patch(c.url(fmt.Sprintf("/projects/columns/%d", projectColumnID)), map[string]string{"name": name}, &r); err != nil { 107 | return nil, fmt.Errorf("UpdateProjectColumn %s: %w", fmt.Sprintf("projects/columns/%d", projectColumnID), err) 108 | } 109 | return &r, nil 110 | } 111 | -------------------------------------------------------------------------------- /github/projects.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "sort" 8 | "strconv" 9 | ) 10 | 11 | // Project represents a project. 12 | type Project struct { 13 | ID int `json:"id"` 14 | Name string `json:"name"` 15 | Body string `json:"body"` 16 | Number int `json:"number"` 17 | State ProjectState `json:"state"` 18 | OwnerURL string `json:"owner_url"` 19 | URL string `json:"url"` 20 | HTMLURL string `json:"html_url"` 21 | ColumnsURL string `json:"columns_url"` 22 | Creator *User `json:"creator"` 23 | CreatedAt string `json:"created_at"` 24 | UpdatedAt string `json:"updated_at"` 25 | } 26 | 27 | // ProjectState ... 28 | type ProjectState int 29 | 30 | // ProjectState ... 31 | const ( 32 | ProjectStateOpen ProjectState = iota + 1 33 | ProjectStateClosed 34 | ) 35 | 36 | var stringToProjectState = map[string]ProjectState{ 37 | "open": ProjectStateOpen, 38 | "closed": ProjectStateClosed, 39 | } 40 | 41 | var projectStateToString = map[ProjectState]string{ 42 | ProjectStateOpen: "open", 43 | ProjectStateClosed: "closed", 44 | } 45 | 46 | // UnmarshalJSON implements json.Unmarshaler 47 | func (t *ProjectState) UnmarshalJSON(b []byte) error { 48 | var s string 49 | if err := json.Unmarshal(b, &s); err != nil { 50 | return err 51 | } 52 | if x, ok := stringToProjectState[s]; ok { 53 | *t = x 54 | return nil 55 | } 56 | return fmt.Errorf("unknown project state: %q", s) 57 | } 58 | 59 | // MarshalJSON implements json.Marshaler 60 | func (t ProjectState) MarshalJSON() ([]byte, error) { 61 | return json.Marshal(t.String()) 62 | } 63 | 64 | // String implements Stringer 65 | func (t ProjectState) String() string { 66 | return projectStateToString[t] 67 | } 68 | 69 | // GoString implements GoString 70 | func (t ProjectState) GoString() string { 71 | return strconv.Quote(t.String()) 72 | } 73 | 74 | // Projects represents a collection of projects. 75 | type Projects <-chan interface{} 76 | 77 | // Next emits the next Project. 78 | func (ps Projects) Next() (*Project, error) { 79 | for x := range ps { 80 | switch x := x.(type) { 81 | case error: 82 | return nil, x 83 | case *Project: 84 | return x, nil 85 | } 86 | break 87 | } 88 | return nil, io.EOF 89 | } 90 | 91 | // ProjectsFromSlice creates Projects from a slice. 92 | func ProjectsFromSlice(xs []*Project) Projects { 93 | ps := make(chan interface{}) 94 | go func() { 95 | defer close(ps) 96 | for _, p := range xs { 97 | ps <- p 98 | } 99 | }() 100 | return ps 101 | } 102 | 103 | // ProjectsToSlice collects Projects. 104 | func ProjectsToSlice(ps Projects) ([]*Project, error) { 105 | xs := []*Project{} 106 | for { 107 | p, err := ps.Next() 108 | if err != nil { 109 | if err != io.EOF { 110 | return nil, err 111 | } 112 | sort.Slice(xs, func(i, j int) bool { 113 | return xs[i].Number < xs[j].Number 114 | }) 115 | return xs, nil 116 | } 117 | xs = append(xs, p) 118 | } 119 | } 120 | 121 | // ListProjectsParams represents the parameter for ListProjects API. 122 | type ListProjectsParams struct { 123 | State ListProjectsParamState 124 | } 125 | 126 | // ListProjectsParamState ... 127 | type ListProjectsParamState int 128 | 129 | // ListProjectsParamState ... 130 | const ( 131 | ListProjectsParamStateDefault ListProjectsParamState = iota + 1 132 | ListProjectsParamStateOpen 133 | ListProjectsParamStateClosed 134 | ListProjectsParamStateAll 135 | ) 136 | 137 | func (f ListProjectsParamState) String() string { 138 | switch f { 139 | case ListProjectsParamStateOpen: 140 | return "open" 141 | case ListProjectsParamStateClosed: 142 | return "closed" 143 | case ListProjectsParamStateAll: 144 | return "all" 145 | default: 146 | return "" 147 | } 148 | } 149 | 150 | func listProjectsPath(repo string, params *ListProjectsParams) string { 151 | return newPath(fmt.Sprintf("/repos/%s/projects", repo)). 152 | query("state", params.State.String()). 153 | query("per_page", "100"). 154 | String() 155 | } 156 | 157 | // ListProjects lists the projects. 158 | func (c *client) ListProjects(repo string, params *ListProjectsParams) Projects { 159 | ps := make(chan interface{}) 160 | go func() { 161 | defer close(ps) 162 | path := c.url(listProjectsPath(repo, params)) 163 | for { 164 | var xs []*Project 165 | next, err := c.getList(path, &xs) 166 | if err != nil { 167 | ps <- fmt.Errorf("ListProjects %s: %w", repo, err) 168 | break 169 | } 170 | for _, x := range xs { 171 | ps <- x 172 | } 173 | if next == "" { 174 | break 175 | } 176 | path = next 177 | } 178 | }() 179 | return Projects(ps) 180 | } 181 | 182 | func (c *client) GetProject(projectID int) (*Project, error) { 183 | var r Project 184 | if err := c.get(c.url(fmt.Sprintf("/projects/%d", projectID)), &r); err != nil { 185 | return nil, fmt.Errorf("GetProject %s: %w", fmt.Sprintf("projects/%d", projectID), err) 186 | } 187 | return &r, nil 188 | } 189 | 190 | // CreateProjectParams represents the parameter for CreateProject API. 191 | type CreateProjectParams struct { 192 | Name string `json:"name"` 193 | Body string `json:"body"` 194 | } 195 | 196 | // CreateProject creates a project. 197 | func (c *client) CreateProject(repo string, params *CreateProjectParams) (*Project, error) { 198 | var r Project 199 | if err := c.post(c.url(fmt.Sprintf("/repos/%s/projects", repo)), params, &r); err != nil { 200 | return nil, fmt.Errorf("CreateProject %s: %w", fmt.Sprintf("%s/projects", repo), err) 201 | } 202 | return &r, nil 203 | } 204 | 205 | // UpdateProjectParams represents the parameter for UpdateProject API. 206 | type UpdateProjectParams struct { 207 | Name string `json:"name,omitempty"` 208 | Body string `json:"body,omitempty"` 209 | State ProjectState `json:"state,omitempty"` 210 | } 211 | 212 | // UpdateProject updates the project. 213 | func (c *client) UpdateProject(projectID int, params *UpdateProjectParams) (*Project, error) { 214 | var r Project 215 | if err := c.patch(c.url(fmt.Sprintf("/projects/%d", projectID)), params, &r); err != nil { 216 | return nil, fmt.Errorf("UpdateProject %s: %w", fmt.Sprintf("projects/%d", projectID), err) 217 | } 218 | return &r, nil 219 | } 220 | 221 | // DeleteProject deletes the project. 222 | func (c *client) DeleteProject(projectID int) error { 223 | if err := c.delete(c.url(fmt.Sprintf("/projects/%d", projectID))); err != nil { 224 | return fmt.Errorf("DeleteProject %s: %w", fmt.Sprintf("/projects/%d", projectID), err) 225 | } 226 | return nil 227 | } 228 | -------------------------------------------------------------------------------- /github/pulls.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | ) 7 | 8 | // PullReq represents a pull request. 9 | type PullReq struct { 10 | Issue 11 | Merged bool `json:"merged"` 12 | MergedAt string `json:"merged_at"` 13 | MergedBy *User `json:"merged_by"` 14 | MergeCommitSHA string `json:"merge_commit_sha"` 15 | Draft bool `json:"draft"` 16 | Head *PullReqRef `json:"head"` 17 | Base *PullReqRef `json:"base"` 18 | Commits int `json:"commits"` 19 | Additions int `json:"additions"` 20 | Deletions int `json:"deletions"` 21 | ChangedFiles int `json:"changed_files"` 22 | } 23 | 24 | // PullReqRef ... 25 | type PullReqRef struct { 26 | SHA string `json:"sha"` 27 | Ref string `json:"ref"` 28 | User *User `json:"user"` 29 | Repo *Repo `json:"repo"` 30 | } 31 | 32 | // PullReqs represents a collection of pull requests. 33 | type PullReqs <-chan interface{} 34 | 35 | // Next emits the next PullReq. 36 | func (ps PullReqs) Next() (*PullReq, error) { 37 | for x := range ps { 38 | switch x := x.(type) { 39 | case error: 40 | return nil, x 41 | case *PullReq: 42 | return x, nil 43 | } 44 | break 45 | } 46 | return nil, io.EOF 47 | } 48 | 49 | // PullReqsFromSlice creates PullReqs from a slice. 50 | func PullReqsFromSlice(xs []*PullReq) PullReqs { 51 | ps := make(chan interface{}) 52 | go func() { 53 | defer close(ps) 54 | for _, p := range xs { 55 | ps <- p 56 | } 57 | }() 58 | return ps 59 | } 60 | 61 | // PullReqsToSlice collects PullReqs. 62 | func PullReqsToSlice(ps PullReqs) ([]*PullReq, error) { 63 | xs := []*PullReq{} 64 | for { 65 | p, err := ps.Next() 66 | if err != nil { 67 | if err != io.EOF { 68 | return nil, err 69 | } 70 | return xs, nil 71 | } 72 | xs = append(xs, p) 73 | } 74 | } 75 | 76 | // ListPullReqsParams represents the parameter for ListPullReqs API. 77 | type ListPullReqsParams struct { 78 | State ListPullReqsParamState 79 | Head string 80 | Base string 81 | Sort ListPullReqsParamSort 82 | Direction ListPullReqsParamDirection 83 | } 84 | 85 | // ListPullReqsParamState ... 86 | type ListPullReqsParamState int 87 | 88 | // ListPullReqsParamState ... 89 | const ( 90 | ListPullReqsParamStateDefault ListPullReqsParamState = iota + 1 91 | ListPullReqsParamStateOpen 92 | ListPullReqsParamStateClosed 93 | ListPullReqsParamStateAll 94 | ) 95 | 96 | func (f ListPullReqsParamState) String() string { 97 | switch f { 98 | case ListPullReqsParamStateOpen: 99 | return "open" 100 | case ListPullReqsParamStateClosed: 101 | return "closed" 102 | case ListPullReqsParamStateAll: 103 | return "all" 104 | default: 105 | return "" 106 | } 107 | } 108 | 109 | // ListPullReqsParamSort ... 110 | type ListPullReqsParamSort int 111 | 112 | // ListPullReqsParamSort ... 113 | const ( 114 | ListPullReqsParamSortDefault ListPullReqsParamSort = iota + 1 115 | ListPullReqsParamSortCreated 116 | ListPullReqsParamSortUpdated 117 | ListPullReqsParamSortPopularity 118 | ListPullReqsParamSortLongRunning 119 | ) 120 | 121 | func (f ListPullReqsParamSort) String() string { 122 | switch f { 123 | case ListPullReqsParamSortCreated: 124 | return "created" 125 | case ListPullReqsParamSortUpdated: 126 | return "updated" 127 | case ListPullReqsParamSortPopularity: 128 | return "popularity" 129 | case ListPullReqsParamSortLongRunning: 130 | return "long-running" 131 | default: 132 | return "" 133 | } 134 | } 135 | 136 | // ListPullReqsParamDirection ... 137 | type ListPullReqsParamDirection int 138 | 139 | // ListPullReqsParamDirection ... 140 | const ( 141 | ListPullReqsParamDirectionDefault ListPullReqsParamDirection = iota + 1 142 | ListPullReqsParamDirectionAsc 143 | ListPullReqsParamDirectionDesc 144 | ) 145 | 146 | func (f ListPullReqsParamDirection) String() string { 147 | switch f { 148 | case ListPullReqsParamDirectionAsc: 149 | return "asc" 150 | case ListPullReqsParamDirectionDesc: 151 | return "desc" 152 | default: 153 | return "" 154 | } 155 | } 156 | 157 | func listPullReqsPath(repo string, params *ListPullReqsParams) string { 158 | return newPath(fmt.Sprintf("/repos/%s/pulls", repo)). 159 | query("state", params.State.String()). 160 | query("head", params.Head). 161 | query("base", params.Base). 162 | query("sort", params.Sort.String()). 163 | query("direction", params.Direction.String()). 164 | query("per_page", "100"). 165 | String() 166 | } 167 | 168 | // ListPullReqs lists the pull requests. 169 | func (c *client) ListPullReqs(repo string, params *ListPullReqsParams) PullReqs { 170 | ps := make(chan interface{}) 171 | go func() { 172 | defer close(ps) 173 | path := c.url(listPullReqsPath(repo, params)) 174 | for { 175 | var xs []*PullReq 176 | next, err := c.getList(path, &xs) 177 | if err != nil { 178 | ps <- fmt.Errorf("ListPullReqs %s: %w", repo, err) 179 | break 180 | } 181 | for _, x := range xs { 182 | ps <- x 183 | } 184 | if next == "" { 185 | break 186 | } 187 | path = next 188 | } 189 | }() 190 | return PullReqs(ps) 191 | } 192 | 193 | func (c *client) GetPullReq(repo string, pullNumber int) (*PullReq, error) { 194 | var r PullReq 195 | if err := c.get(c.url(fmt.Sprintf("/repos/%s/pulls/%d", repo, pullNumber)), &r); err != nil { 196 | return nil, fmt.Errorf("GetPullReq %s: %w", fmt.Sprintf("%s/pulls/%d", repo, pullNumber), err) 197 | } 198 | return &r, nil 199 | } 200 | -------------------------------------------------------------------------------- /github/repo.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // Repo represents a repository. 8 | type Repo struct { 9 | Name string `json:"name"` 10 | FullName string `json:"full_name"` 11 | Description string `json:"description"` 12 | Homepage string `json:"homepage"` 13 | HTMLURL string `json:"html_url"` 14 | Private bool `json:"private"` 15 | } 16 | 17 | func (c *client) GetRepo(repo string) (*Repo, error) { 18 | var r Repo 19 | if err := c.get(c.url(fmt.Sprintf("/repos/%s", repo)), &r); err != nil { 20 | return nil, fmt.Errorf("GetRepo %s: %w", repo, err) 21 | } 22 | return &r, nil 23 | } 24 | 25 | // UpdateRepoParams represents a parameter on updating a repository. 26 | type UpdateRepoParams struct { 27 | Name string `json:"name"` 28 | Description string `json:"description"` 29 | Homepage string `json:"homepage"` 30 | Private bool `json:"private"` 31 | } 32 | 33 | // UpdateRepo updates a repository. 34 | func (c *client) UpdateRepo(repo string, params *UpdateRepoParams) (*Repo, error) { 35 | var r Repo 36 | if err := c.patch(c.url(fmt.Sprintf("/repos/%s", repo)), params, &r); err != nil { 37 | return nil, fmt.Errorf("UpdateRepo %s: %w", repo, err) 38 | } 39 | return &r, nil 40 | } 41 | -------------------------------------------------------------------------------- /github/review_comments.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | ) 7 | 8 | // ReviewComment represents a review comment. 9 | type ReviewComment struct { 10 | ID int `json:"id"` 11 | Path string `json:"path"` 12 | Body string `json:"body"` 13 | DiffHunk string `json:"diff_hunk"` 14 | HTMLURL string `json:"html_url"` 15 | User *User `json:"user"` 16 | InReplyToID int `json:"in_reply_to_id"` 17 | CreatedAt string `json:"created_at"` 18 | UpdatedAt string `json:"updated_at"` 19 | } 20 | 21 | // ReviewComments represents a collection of review comments. 22 | type ReviewComments <-chan interface{} 23 | 24 | // Next emits the next ReviewComment. 25 | func (cs ReviewComments) Next() (*ReviewComment, error) { 26 | for x := range cs { 27 | switch x := x.(type) { 28 | case error: 29 | return nil, x 30 | case *ReviewComment: 31 | return x, nil 32 | } 33 | break 34 | } 35 | return nil, io.EOF 36 | } 37 | 38 | // ReviewCommentsFromSlice creates ReviewComments from a slice. 39 | func ReviewCommentsFromSlice(xs []*ReviewComment) ReviewComments { 40 | cs := make(chan interface{}) 41 | go func() { 42 | defer close(cs) 43 | for _, c := range xs { 44 | cs <- c 45 | } 46 | }() 47 | return cs 48 | } 49 | 50 | // ReviewCommentsToSlice collects ReviewComments. 51 | func ReviewCommentsToSlice(cs ReviewComments) ([]*ReviewComment, error) { 52 | xs := []*ReviewComment{} 53 | for { 54 | c, err := cs.Next() 55 | if err != nil { 56 | if err != io.EOF { 57 | return nil, err 58 | } 59 | return xs, nil 60 | } 61 | xs = append(xs, c) 62 | } 63 | } 64 | 65 | // ListReviewComments lists the review comments of a pull request. 66 | func (c *client) ListReviewComments(repo string, pullNumber int) ReviewComments { 67 | cs := make(chan interface{}) 68 | go func() { 69 | defer close(cs) 70 | path := c.url(fmt.Sprintf("/repos/%s/pulls/%d/comments?per_page=100", repo, pullNumber)) 71 | for { 72 | var xs []*ReviewComment 73 | next, err := c.getList(path, &xs) 74 | if err != nil { 75 | cs <- fmt.Errorf("ListReviewComments %s: %w", repo, err) 76 | break 77 | } 78 | for _, x := range xs { 79 | cs <- x 80 | } 81 | if next == "" { 82 | break 83 | } 84 | path = next 85 | } 86 | }() 87 | return ReviewComments(cs) 88 | } 89 | -------------------------------------------------------------------------------- /github/reviews.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "strconv" 8 | ) 9 | 10 | // Review represents a review. 11 | type Review struct { 12 | ID int `json:"id"` 13 | State ReviewState `json:"state"` 14 | Body string `json:"body"` 15 | HTMLURL string `json:"html_url"` 16 | User *User `json:"user"` 17 | CommitID string `json:"commit_id"` 18 | SubmittedAt string `json:"submitted_at"` 19 | } 20 | 21 | // ReviewState ... 22 | type ReviewState int 23 | 24 | // ReviewState ... 25 | const ( 26 | ReviewStateApproved ReviewState = iota + 1 27 | ReviewStateChangesRequested 28 | ReviewStateCommented 29 | ReviewStatePending 30 | ReviewStateDismissed 31 | ) 32 | 33 | var stringToReviewState = map[string]ReviewState{ 34 | "APPROVED": ReviewStateApproved, 35 | "CHANGES_REQUESTED": ReviewStateChangesRequested, 36 | "COMMENTED": ReviewStateCommented, 37 | "PENDING": ReviewStatePending, 38 | "DISMISSED": ReviewStateDismissed, 39 | } 40 | 41 | var reviewStateToString = map[ReviewState]string{ 42 | ReviewStateApproved: "APPROVED", 43 | ReviewStateChangesRequested: "CHANGES_REQUESTED", 44 | ReviewStateCommented: "COMMENTED", 45 | ReviewStatePending: "PENDING", 46 | ReviewStateDismissed: "DISMISSED", 47 | } 48 | 49 | // UnmarshalJSON implements json.Unmarshaler 50 | func (t *ReviewState) UnmarshalJSON(b []byte) error { 51 | var s string 52 | if err := json.Unmarshal(b, &s); err != nil { 53 | return err 54 | } 55 | if x, ok := stringToReviewState[s]; ok { 56 | *t = x 57 | return nil 58 | } 59 | return fmt.Errorf("unknown review state: %q", s) 60 | } 61 | 62 | // MarshalJSON implements json.Marshaler 63 | func (t ReviewState) MarshalJSON() ([]byte, error) { 64 | return json.Marshal(t.String()) 65 | } 66 | 67 | // String implements Stringer 68 | func (t ReviewState) String() string { 69 | return reviewStateToString[t] 70 | } 71 | 72 | // GoString implements GoString 73 | func (t ReviewState) GoString() string { 74 | return strconv.Quote(t.String()) 75 | } 76 | 77 | // Reviews represents a collection of reviews. 78 | type Reviews <-chan interface{} 79 | 80 | // Next emits the next Review. 81 | func (rs Reviews) Next() (*Review, error) { 82 | for x := range rs { 83 | switch x := x.(type) { 84 | case error: 85 | return nil, x 86 | case *Review: 87 | return x, nil 88 | } 89 | break 90 | } 91 | return nil, io.EOF 92 | } 93 | 94 | // ReviewsFromSlice creates Reviews from a slice. 95 | func ReviewsFromSlice(xs []*Review) Reviews { 96 | rs := make(chan interface{}) 97 | go func() { 98 | defer close(rs) 99 | for _, p := range xs { 100 | rs <- p 101 | } 102 | }() 103 | return rs 104 | } 105 | 106 | // ReviewsToSlice collects Reviews. 107 | func ReviewsToSlice(rs Reviews) ([]*Review, error) { 108 | xs := []*Review{} 109 | for { 110 | p, err := rs.Next() 111 | if err != nil { 112 | if err != io.EOF { 113 | return nil, err 114 | } 115 | return xs, nil 116 | } 117 | xs = append(xs, p) 118 | } 119 | } 120 | 121 | // ListReviews lists the reviews. 122 | func (c *client) ListReviews(repo string, pullNumber int) Reviews { 123 | rs := make(chan interface{}) 124 | go func() { 125 | defer close(rs) 126 | path := c.url(fmt.Sprintf("/repos/%s/pulls/%d/reviews?per_page=100", repo, pullNumber)) 127 | for { 128 | var xs []*Review 129 | next, err := c.getList(path, &xs) 130 | if err != nil { 131 | rs <- fmt.Errorf("ListReviews %s/pull/%d: %w", repo, pullNumber, err) 132 | break 133 | } 134 | for _, x := range xs { 135 | rs <- x 136 | } 137 | if next == "" { 138 | break 139 | } 140 | path = next 141 | } 142 | }() 143 | return Reviews(rs) 144 | } 145 | 146 | // GetReview gets the review. 147 | func (c *client) GetReview(repo string, pullNumber, reviewID int) (*Review, error) { 148 | var r Review 149 | if err := c.get(c.url(fmt.Sprintf("/repos/%s/pulls/%d/reviews/%d", repo, pullNumber, reviewID)), &r); err != nil { 150 | return nil, fmt.Errorf("GetReview %s: %w", fmt.Sprintf("%s/pulls/%d/reviews/%d", repo, pullNumber, reviewID), err) 151 | } 152 | return &r, nil 153 | } 154 | -------------------------------------------------------------------------------- /github/users.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | ) 7 | 8 | // User represents a user. 9 | type User struct { 10 | Login string `json:"login"` 11 | HTMLURL string `json:"html_url"` 12 | } 13 | 14 | // GetLogin ... 15 | func (c *client) GetLogin() (*User, error) { 16 | var r User 17 | if err := c.get(c.url("/user"), &r); err != nil { 18 | return nil, fmt.Errorf("GetLogin %s: %w", "/user", err) 19 | } 20 | return &r, nil 21 | } 22 | 23 | // Users represents a collection of users. 24 | type Users <-chan interface{} 25 | 26 | // Next emits the next User. 27 | func (cs Users) Next() (*User, error) { 28 | for x := range cs { 29 | switch x := x.(type) { 30 | case error: 31 | return nil, x 32 | case *User: 33 | return x, nil 34 | } 35 | break 36 | } 37 | return nil, io.EOF 38 | } 39 | 40 | // UsersFromSlice creates Users from a slice. 41 | func UsersFromSlice(xs []*User) Users { 42 | cs := make(chan interface{}) 43 | go func() { 44 | defer close(cs) 45 | for _, p := range xs { 46 | cs <- p 47 | } 48 | }() 49 | return cs 50 | } 51 | 52 | // UsersToSlice collects Users. 53 | func UsersToSlice(cs Users) ([]*User, error) { 54 | xs := []*User{} 55 | for { 56 | p, err := cs.Next() 57 | if err != nil { 58 | if err != io.EOF { 59 | return nil, err 60 | } 61 | return xs, nil 62 | } 63 | xs = append(xs, p) 64 | } 65 | } 66 | 67 | // ListUsers lists all the users. 68 | func (c *client) ListUsers() Users { 69 | cs := make(chan interface{}) 70 | go func() { 71 | defer close(cs) 72 | path := c.url("/users?per_page=100") 73 | for { 74 | var xs []*User 75 | next, err := c.getList(path, &xs) 76 | if err != nil { 77 | cs <- fmt.Errorf("ListUsers /users: %w", err) 78 | break 79 | } 80 | for _, x := range xs { 81 | cs <- x 82 | } 83 | if next == "" { 84 | break 85 | } 86 | path = next 87 | } 88 | }() 89 | return Users(cs) 90 | } 91 | 92 | // GetUser ... 93 | func (c *client) GetUser(name string) (*User, error) { 94 | var r User 95 | if err := c.get(c.url(fmt.Sprintf("/users/%s", name)), &r); err != nil { 96 | return nil, fmt.Errorf("GetUser %s: %w", fmt.Sprintf("/user/%s", name), err) 97 | } 98 | return &r, nil 99 | } 100 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/itchyny/github-migrator 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/stretchr/testify v1.5.1 7 | github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 8 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c 9 | ) 10 | 11 | require ( 12 | github.com/davecgh/go-spew v1.1.0 // indirect 13 | github.com/pmezard/go-difflib v1.0.0 // indirect 14 | gopkg.in/yaml.v2 v2.2.2 // indirect 15 | ) 16 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 5 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 6 | github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= 7 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 8 | github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 h1:nrZ3ySNYwJbSpD6ce9duiP+QkD3JuLCcWkdaehUS/3Y= 9 | github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80/go.mod h1:iFyPdL66DjUD96XmzVL3ZntbzcflLnznH0fr99w5VqE= 10 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 11 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 12 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 13 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 14 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 15 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 16 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "os" 7 | "strings" 8 | 9 | "github.com/itchyny/github-migrator/github" 10 | "github.com/itchyny/github-migrator/migrator" 11 | "github.com/itchyny/github-migrator/repo" 12 | ) 13 | 14 | const name = "github-migrator" 15 | 16 | func main() { 17 | if err := run(os.Args[1:]); err != nil { 18 | fmt.Fprintf(os.Stderr, "%s: %s\n", name, err) 19 | os.Exit(1) 20 | } 21 | } 22 | 23 | func run(args []string) error { 24 | if len(args) != 2 { 25 | return fmt.Errorf("usage: %s ", name) 26 | } 27 | mig, err := createMigrator(args[0], args[1]) 28 | if err != nil { 29 | return err 30 | } 31 | return mig.Migrate() 32 | } 33 | 34 | func createGitHubClient(tokenEnv, endpointEnv, proxyEnv string) (github.Client, error) { 35 | token := os.Getenv(tokenEnv) 36 | if token == "" { 37 | return nil, fmt.Errorf("GitHub token not found (specify %s)", tokenEnv) 38 | } 39 | endpoint := os.Getenv(endpointEnv) 40 | if endpoint == "" { 41 | endpoint = "https://api.github.com" 42 | } 43 | proxy := os.Getenv(proxyEnv) 44 | cli := github.New( 45 | token, endpoint, proxy, 46 | github.ClientLogger( 47 | github.NewLogger( 48 | github.LoggerPreRequest(func(req *http.Request) { 49 | fmt.Printf("===> %s: %s\n", req.Method, req.URL) 50 | }), 51 | github.LoggerPostRequest(func(res *http.Response, err error) { 52 | if err != nil { 53 | var suffix string 54 | if res != nil { 55 | suffix = fmt.Sprintf(": %s: %s", res.Request.Method, res.Request.URL) 56 | } 57 | fmt.Printf("<=== %s%s\n", err, suffix) 58 | return 59 | } 60 | fmt.Printf("<=== %s: %s: %s\n", res.Status, res.Request.Method, res.Request.URL) 61 | }), 62 | ), 63 | ), 64 | ) 65 | user, err := cli.GetLogin() 66 | if err != nil { 67 | return nil, fmt.Errorf("%s (or you may want to set %s)", err, endpointEnv) 68 | } 69 | fmt.Printf("[<>] login succeeded: %s\n", user.Login) 70 | return cli, nil 71 | } 72 | 73 | func createMigrator(sourcePath, targetPath string) (migrator.Migrator, error) { 74 | sourceCli, err := createGitHubClient( 75 | "GITHUB_MIGRATOR_SOURCE_API_TOKEN", 76 | "GITHUB_MIGRATOR_SOURCE_API_ENDPOINT", 77 | "GITHUB_MIGRATOR_SOURCE_PROXY_URL", 78 | ) 79 | if err != nil { 80 | return nil, err 81 | } 82 | targetCli, err := createGitHubClient( 83 | "GITHUB_MIGRATOR_TARGET_API_TOKEN", 84 | "GITHUB_MIGRATOR_TARGET_API_ENDPOINT", 85 | "GITHUB_MIGRATOR_TARGET_PROXY_URL", 86 | ) 87 | if err != nil { 88 | return nil, err 89 | } 90 | source := repo.New(sourceCli, sourcePath) 91 | target := repo.New(targetCli, targetPath) 92 | return migrator.New(source, target, createUserMapping()), nil 93 | } 94 | 95 | func createUserMapping() map[string]string { 96 | m := make(map[string]string) 97 | for _, src := range strings.Split(os.Getenv("GITHUB_MIGRATOR_USER_MAPPING"), ",") { 98 | xs := strings.Split(strings.TrimSpace(src), ":") 99 | if len(xs) == 2 && len(xs[0]) > 0 && len(xs[1]) > 0 { 100 | m[xs[0]] = xs[1] 101 | } 102 | } 103 | return m 104 | } 105 | -------------------------------------------------------------------------------- /migrator/builder.go: -------------------------------------------------------------------------------- 1 | package migrator 2 | 3 | import ( 4 | "fmt" 5 | "html" 6 | "strings" 7 | "time" 8 | 9 | "github.com/itchyny/github-migrator/github" 10 | ) 11 | 12 | type builder struct { 13 | *migrator 14 | issue *github.Issue 15 | pullReq *github.PullReq 16 | comments []*github.Comment 17 | events []*github.Event 18 | commits []*github.Commit 19 | commitDiff string 20 | reviews []*github.Review 21 | reviewComments []*github.ReviewComment 22 | skipAssignee bool 23 | } 24 | 25 | func (m *migrator) buildImport( 26 | issue *github.Issue, pullReq *github.PullReq, 27 | comments []*github.Comment, events []*github.Event, 28 | commits []*github.Commit, commitDiff string, 29 | reviews []*github.Review, reviewComments []*github.ReviewComment, 30 | skipAssignee bool, 31 | ) (*github.Import, error) { 32 | return (&builder{ 33 | migrator: m, 34 | issue: issue, 35 | pullReq: pullReq, 36 | comments: comments, 37 | events: events, 38 | commits: commits, 39 | commitDiff: commitDiff, 40 | reviews: reviews, 41 | reviewComments: reviewComments, 42 | skipAssignee: skipAssignee, 43 | }).build() 44 | } 45 | 46 | func (b *builder) build() (*github.Import, error) { 47 | importIssue := &github.ImportIssue{ 48 | Title: b.issue.Title, 49 | Body: b.buildImportBody(), 50 | CreatedAt: b.issue.CreatedAt, 51 | UpdatedAt: b.issue.UpdatedAt, 52 | Closed: b.issue.State != github.IssueStateOpen, 53 | ClosedAt: b.issue.ClosedAt, 54 | Labels: b.buildImportLabels(b.issue), 55 | } 56 | if !b.skipAssignee && b.issue.Assignee != nil { 57 | target := b.commentFilters.apply(b.issue.Assignee.Login) 58 | isMember, err := b.isTargetMember(target) 59 | if err != nil { 60 | return nil, err 61 | } 62 | if isMember { 63 | importIssue.Assignee = target 64 | } 65 | } 66 | if b.issue.Milestone != nil { 67 | if l, ok := b.milestoneByTitle[b.issue.Milestone.Title]; ok { 68 | importIssue.Milestone = l.Number 69 | } 70 | } 71 | comments, err := b.buildImportComments() 72 | if err != nil { 73 | return nil, err 74 | } 75 | return &github.Import{Issue: importIssue, Comments: comments}, nil 76 | } 77 | 78 | func (b *builder) buildImportBody() string { 79 | var suffix string 80 | if b.issue.Body != "" { 81 | suffix = "\n\n" + b.commentFilters.apply(b.issue.Body) 82 | } 83 | action := fmt.Sprintf("created the original %s
\n", b.issue.Type()) 84 | if b.pullReq != nil { 85 | action += b.buildCompareLinkTag(b.targetRepo, b.pullReq.Base.SHA, b.pullReq.Head.SHA) + 86 | " " + b.buildPullRequestRefs() + "
\n" 87 | } 88 | action += "imported from " + buildIssueLinkTag(b.sourceRepo, b.issue) 89 | tableRows := [][]string{ 90 | { 91 | b.buildImageTag(b.issue.User, 35), 92 | fmt.Sprintf("@%s %s", b.getUserLogin(b.issue.User), action), 93 | }, 94 | } 95 | if len(b.commitDiff) > 0 { 96 | tableRows = append(tableRows, []string{b.buildDiffDetails()}) 97 | } 98 | if len(b.commits) > 0 { 99 | tableRows = append(tableRows, []string{b.buildCommitDetails()}) 100 | } 101 | return b.buildTable(2, tableRows...) + suffix 102 | } 103 | 104 | func (b *builder) buildDiffDetails() string { 105 | summary := plural(b.pullReq.ChangedFiles, "file") + " changed" 106 | if b.pullReq.Additions > 0 { 107 | summary += ", " + plural(b.pullReq.Additions, "insertion") + "(+)" 108 | } 109 | if b.pullReq.Deletions > 0 { 110 | summary += ", " + plural(b.pullReq.Deletions, "deletion") + "(-)" 111 | } 112 | return b.buildDetails(" ", summary, "\n```diff\n"+ 113 | escapeBackQuotes(truncateDiff(b.commitDiff))+ 114 | "```\n") 115 | } 116 | 117 | func (b *builder) buildCommitDetails() string { 118 | summary := plural(b.pullReq.Commits, "commit") 119 | var commitRows [][]string 120 | for i, c := range b.commits { 121 | if i > 90 && len(b.commits) > 100 { 122 | commitRows = append(commitRows, []string{ 123 | fmt.Sprintf("more %d commits", len(b.commits)-i), 124 | }) 125 | break 126 | } 127 | var dateString string 128 | committer := c.Committer 129 | if committer == nil { 130 | committer = c.Author 131 | } 132 | if committer == nil { 133 | committer = &github.User{Login: c.Commit.Committer.Name} 134 | } 135 | t, err := time.Parse(time.RFC3339, c.Commit.Committer.Date) 136 | if err == nil { 137 | dateString = t.Format(" on Mon 2, 2006") 138 | } 139 | commitRows = append(commitRows, []string{ 140 | html.EscapeString(c.Commit.Message) + "
\n" + 141 | b.buildImageTag(committer, 16) + 142 | fmt.Sprintf(" @%s committed%s", b.commentFilters.apply(committer.Login), dateString) + 143 | fmt.Sprintf(` %s`, b.commentFilters.apply(c.HTMLURL), c.SHA[:7]), 144 | }) 145 | } 146 | return b.buildDetails("", summary, b.buildTable(1, commitRows...)) 147 | } 148 | 149 | func (b *builder) buildImportComments() ([]*github.ImportComment, error) { 150 | issueComments := b.buildImportIssueComments() 151 | eventComments, err := b.buildImportEventComments() 152 | if err != nil { 153 | return nil, err 154 | } 155 | reviewComments := b.buildImportReviewComments() 156 | importReviews := b.buildImportReviews() 157 | return append( 158 | append( 159 | append( 160 | issueComments, 161 | eventComments..., 162 | ), 163 | reviewComments..., 164 | ), 165 | importReviews..., 166 | ), nil 167 | } 168 | 169 | func (b *builder) buildImportIssueComments() []*github.ImportComment { 170 | xs := make([]*github.ImportComment, len(b.comments)) 171 | for i, c := range b.comments { 172 | xs[i] = &github.ImportComment{ 173 | Body: b.buildUserActionBody(c.User, "commented", c.Body), 174 | CreatedAt: c.CreatedAt, 175 | } 176 | } 177 | return xs 178 | } 179 | 180 | func (b *builder) buildImportReviews() []*github.ImportComment { 181 | var xs []*github.ImportComment 182 | for _, c := range b.reviews { 183 | var action string 184 | if c.State == github.ReviewStateApproved { 185 | action = "approved" 186 | } else if c.State == github.ReviewStateChangesRequested { 187 | action = "requested changes" 188 | } else if c.State == github.ReviewStateDismissed { 189 | action = "commented" 190 | } else { 191 | continue 192 | } 193 | xs = append(xs, &github.ImportComment{ 194 | Body: b.buildUserActionBody(c.User, action, c.Body), 195 | CreatedAt: c.SubmittedAt, 196 | }) 197 | } 198 | return xs 199 | } 200 | 201 | func (b *builder) buildImportReviewComments() []*github.ImportComment { 202 | var xs []*github.ImportComment 203 | indexByID := make(map[int]int) 204 | for _, c := range b.reviewComments { 205 | if i, ok := indexByID[c.InReplyToID]; ok { 206 | indexByID[c.ID] = i 207 | xs[i].Body += "\n\n" + b.buildUserActionBody(c.User, "commented", c.Body) 208 | continue 209 | } 210 | indexByID[c.ID] = len(xs) 211 | diffBody := strings.Join([]string{"```diff", "# " + c.Path, c.DiffHunk, "```"}, "\n") 212 | xs = append(xs, &github.ImportComment{ 213 | Body: diffBody + "\n\n" + b.buildUserActionBody(c.User, "commented", c.Body), 214 | CreatedAt: c.CreatedAt, 215 | }) 216 | } 217 | return xs 218 | } 219 | 220 | func (b *builder) buildPullRequestRefs() string { 221 | return fmt.Sprintf( 222 | "into %s from %s", 223 | html.EscapeString(b.pullReq.Base.Ref), 224 | html.EscapeString(b.pullReq.Head.Ref), 225 | ) 226 | } 227 | 228 | func (b *builder) buildUserActionBody(user *github.User, action, body string) string { 229 | var suffix string 230 | if body != "" { 231 | suffix = "\n\n" + b.commentFilters.apply(body) 232 | } 233 | return b.buildTable(2, []string{ 234 | b.buildImageTag(user, 35), 235 | fmt.Sprintf("@%s %s", b.getUserLogin(user), action), 236 | }) + suffix 237 | } 238 | 239 | func (b *builder) buildImageTag(user *github.User, width int) string { 240 | target := b.getUserLogin(user) 241 | if !b.isAvailableUser(target) { 242 | target = "github" 243 | } 244 | return fmt.Sprintf(``, target, width) 245 | } 246 | 247 | func (b *builder) buildTable(width int, xss ...[]string) string { 248 | s := new(strings.Builder) 249 | s.WriteString("\n") 250 | for i, xs := range xss { 251 | if i > 0 { 252 | s.WriteString("\n") 253 | } 254 | s.WriteString("\n") 255 | for i, x := range xs { 256 | if i == len(xs)-1 && len(xs) < width { 257 | s.WriteString(fmt.Sprintf(" \n") 269 | } 270 | s.WriteString("\n") 271 | } 272 | s.WriteString("
\n", width-i)) 258 | } else if i == 0 && len(xs) == 2 && strings.HasPrefix(x, `\n") 260 | } else { 261 | s.WriteString(" \n") 262 | } 263 | x := makeIndent(" ", x) 264 | if !strings.HasSuffix(x, "\n") { 265 | x += "\n" 266 | } 267 | s.WriteString(x) 268 | s.WriteString("
\n") 273 | return s.String() 274 | } 275 | 276 | func (b *builder) buildDetails(indent, summary, details string) string { 277 | s := new(strings.Builder) 278 | s.WriteString(indent + "
\n") 279 | s.WriteString(fmt.Sprintf(indent+" %s\n", summary)) 280 | s.WriteString(makeIndent(indent+" ", details)) 281 | s.WriteString(indent + "
\n") 282 | return s.String() 283 | } 284 | 285 | func makeIndent(indent, str string) string { 286 | if strings.Contains(str, "```") { 287 | return str 288 | } 289 | xs := strings.Split(str, "\n") 290 | for i, x := range xs { 291 | if x == "" { 292 | break // avoid indented code block 293 | } 294 | xs[i] = indent + x 295 | } 296 | return strings.Join(xs, "\n") 297 | } 298 | 299 | func buildIssueLinkTag(repo *github.Repo, issue *github.Issue) string { 300 | return fmt.Sprintf(`%s#%d`, issue.HTMLURL, repo.FullName, issue.Number) 301 | } 302 | 303 | func (b *builder) buildCommitLinkTag(repo *github.Repo, sha string) string { 304 | return fmt.Sprintf(`%s`, repo.HTMLURL, sha, sha[:7]) 305 | } 306 | 307 | func (b *builder) buildCompareLinkTag(repo *github.Repo, base, head string) string { 308 | return fmt.Sprintf(`%s...%s`, repo.HTMLURL, base, head, base[:7], head[:7]) 309 | } 310 | 311 | func (b *builder) buildImportLabels(issue *github.Issue) []string { 312 | xs := []string{} 313 | for _, l := range issue.Labels { 314 | xs = append(xs, l.Name) 315 | } 316 | return xs 317 | } 318 | 319 | func (b *builder) isAvailableUser(name string) bool { 320 | if name == "ghost" { 321 | return true 322 | } 323 | u, _ := b.lookupUser(name) 324 | return u != nil 325 | } 326 | 327 | func (b *builder) getUserLogin(user *github.User) string { 328 | if user == nil { 329 | return "ghost" 330 | } 331 | return b.commentFilters.apply(user.Login) 332 | } 333 | -------------------------------------------------------------------------------- /migrator/builder_events.go: -------------------------------------------------------------------------------- 1 | package migrator 2 | 3 | import ( 4 | "fmt" 5 | "html" 6 | "math" 7 | "strings" 8 | "time" 9 | 10 | "github.com/itchyny/github-migrator/github" 11 | ) 12 | 13 | func (b *builder) buildImportEventComments() ([]*github.ImportComment, error) { 14 | xs := make([]*github.ImportComment, 0, len(b.events)) 15 | egs := groupEventsByCreated(b.events) 16 | for _, eg := range egs { 17 | body, err := b.buildImportEventGroupBody(eg) 18 | if err != nil { 19 | return nil, err 20 | } 21 | if body != "" { 22 | xs = append(xs, &github.ImportComment{ 23 | Body: b.buildUserActionBody(getEventUser(eg[0]), body, ""), 24 | CreatedAt: eg[0].CreatedAt, 25 | }) 26 | } 27 | } 28 | return xs, nil 29 | } 30 | 31 | func getEventUser(e *github.Event) *github.User { 32 | switch e.Event { 33 | case "assigned", "unassigned": 34 | return e.Assigner 35 | default: 36 | return e.Actor 37 | } 38 | } 39 | 40 | func groupEventsByCreated(xs []*github.Event) [][]*github.Event { 41 | ess := make([][]*github.Event, 0, len(xs)) 42 | eventGroupTypes := map[string]int{ 43 | "closed": 1, 44 | "merged": 1, 45 | "reopened": 1, 46 | "labeled": 2, 47 | "unlabeled": 2, 48 | "renamed": 3, 49 | "head_ref_deleted": 4, 50 | "head_ref_restored": 4, 51 | "head_ref_force_pushed": 5, 52 | "base_ref_force_pushed": 5, 53 | "locked": 6, 54 | "unlocked": 6, 55 | "pinned": 7, 56 | "unpinned": 7, 57 | "assigned": 8, 58 | "unassigned": 8, 59 | "review_requested": 9, 60 | "review_request_removed": 9, 61 | "review_dismissed": 9, 62 | "ready_for_review": 9, 63 | "convert_to_draft": 9, 64 | "converted_note_to_issue": 10, 65 | "added_to_project": 10, 66 | "moved_columns_in_project": 10, 67 | "removed_from_project": 10, 68 | "milestoned": 11, 69 | "demilestoned": 11, 70 | "deployed": 12, 71 | } 72 | for _, x := range xs { 73 | if _, ok := eventGroupTypes[x.Event]; !ok || getEventUser(x) == nil { 74 | continue 75 | } 76 | var appended bool 77 | for i, es := range ess { 78 | if getEventUser(es[0]).Login == getEventUser(x).Login && 79 | nearTime(es[0].CreatedAt, x.CreatedAt) && 80 | eventGroupTypes[es[0].Event] == eventGroupTypes[x.Event] { 81 | ess[i] = append(ess[i], x) 82 | appended = true 83 | break 84 | } 85 | } 86 | if appended { 87 | continue 88 | } 89 | ess = append(ess, []*github.Event{x}) 90 | } 91 | return ess 92 | } 93 | 94 | func nearTime(s1, s2 string) bool { 95 | t1, err := time.Parse(time.RFC3339, s1) 96 | if err != nil { 97 | panic(err) 98 | } 99 | t2, err := time.Parse(time.RFC3339, s2) 100 | if err != nil { 101 | panic(err) 102 | } 103 | diff := t1.Sub(t2) 104 | return math.Abs(float64(diff)) < float64(10*time.Second) 105 | } 106 | 107 | func (b *builder) buildImportEventGroupBody(eg []*github.Event) (string, error) { 108 | var actions []string 109 | var merged bool 110 | var addedLabels []string 111 | var removedLabels []string 112 | 113 | for _, e := range eg { 114 | switch e.Event { 115 | case "closed": 116 | if !merged { 117 | if b.pullReq == nil { 118 | actions = append(actions, "closed the issue") 119 | } else { 120 | actions = append(actions, "closed the pull request without merging") 121 | } 122 | } 123 | case "merged": 124 | merged = true 125 | actions = append(actions, 126 | fmt.Sprintf( 127 | "merged the pull request
\ncommit %s ", 128 | b.buildCommitLinkTag(b.targetRepo, e.CommitID), 129 | )+b.buildPullRequestRefs(), 130 | ) 131 | case "reopened": 132 | actions = append(actions, fmt.Sprintf("reopened the %s", b.issue.Type())) 133 | case "labeled": 134 | addedLabels = append(addedLabels, e.Label.Name) 135 | case "unlabeled": 136 | removedLabels = append(removedLabels, e.Label.Name) 137 | case "renamed": 138 | actions = append(actions, 139 | fmt.Sprintf( 140 | "changed the title %s %s", 141 | html.EscapeString(e.Rename.From), html.EscapeString(e.Rename.To), 142 | ), 143 | ) 144 | case "head_ref_deleted": 145 | actions = append(actions, 146 | fmt.Sprintf( 147 | "deleted the %s branch", 148 | html.EscapeString(b.pullReq.Head.Ref), 149 | ), 150 | ) 151 | case "head_ref_restored": 152 | actions = append(actions, 153 | fmt.Sprintf( 154 | "restored the %s branch", 155 | html.EscapeString(b.pullReq.Head.Ref), 156 | ), 157 | ) 158 | case "head_ref_force_pushed", "base_ref_force_pushed": 159 | ref := b.pullReq.Head.Ref 160 | if e.Event == "base_ref_force_pushed" { 161 | ref = b.pullReq.Base.Ref 162 | } 163 | actions = append(actions, 164 | fmt.Sprintf( 165 | "force-pushed the %s branch", 166 | html.EscapeString(ref), 167 | ), 168 | ) 169 | case "locked": 170 | actions = append(actions, 171 | fmt.Sprintf( 172 | "locked as %s and limited conversation to collaborators", 173 | html.EscapeString(strings.ReplaceAll(e.LockReason, "-", " ")), 174 | ), 175 | ) 176 | case "unlocked": 177 | actions = append(actions, "unlocked this conversation") 178 | case "pinned", "unpinned": 179 | actions = append(actions, e.Event+` this issue`) 180 | case "assigned", "unassigned": 181 | if len(eg) == 1 && len(e.Assignees) <= 1 && e.Assigner.Login == e.Assignee.Login { 182 | if e.Event == "assigned" { 183 | return "self-assigned this", nil 184 | } 185 | return "removed their assignment", nil 186 | } 187 | var targets []*github.User 188 | if len(e.Assignees) > 0 { 189 | targets = append(targets, e.Assignees...) 190 | } else { 191 | targets = append(targets, e.Assignee) 192 | } 193 | actions = append(actions, e.Event+" "+b.mentionAll(targets)) 194 | case "review_requested", "review_request_removed": 195 | var actionStr string 196 | if e.Event == "review_requested" { 197 | actionStr = "requested a review" 198 | } else { 199 | actionStr = "removed the request for review" 200 | } 201 | if e.RequestedTeam != nil { 202 | actions = append(actions, 203 | fmt.Sprintf( 204 | `%s from %s`, 205 | actionStr, 206 | b.commentFilters.apply(e.RequestedTeam.Name), 207 | ), 208 | ) 209 | break 210 | } 211 | if len(eg) == 1 && len(e.Reviewers) <= 1 && e.Actor.Login == e.Reviewer.Login { 212 | if e.Event == "review_requested" { 213 | return "self-requested a review", nil 214 | } 215 | return "removed their request for review", nil 216 | } 217 | var targets []*github.User 218 | if len(e.Reviewers) > 0 { 219 | targets = append(targets, e.Reviewers...) 220 | } else { 221 | targets = append(targets, e.Reviewer) 222 | } 223 | actions = append(actions, actionStr+" from "+b.mentionAll(targets)) 224 | case "review_dismissed": 225 | var target *github.User 226 | for _, r := range b.reviews { 227 | if r.ID == e.DismissedReview.ReviewID { 228 | target = r.User 229 | break 230 | } 231 | } 232 | if target != nil { 233 | actions = append(actions, 234 | fmt.Sprintf( 235 | `dismissed @%s's review
%s`, 236 | b.commentFilters.apply(target.Login), 237 | html.EscapeString(e.DismissedReview.DismissalMessage), 238 | ), 239 | ) 240 | } else { 241 | actions = append(actions, 242 | fmt.Sprintf( 243 | `dismissed a review
%s`, 244 | html.EscapeString(e.DismissedReview.DismissalMessage), 245 | ), 246 | ) 247 | } 248 | case "ready_for_review": 249 | actions = append(actions, "marked this pull request as ready for review") 250 | case "convert_to_draft": 251 | actions = append(actions, "marked this pull request as draft") 252 | case "converted_note_to_issue": 253 | p, err := b.getProject(e.ProjectCard.ProjectID) 254 | if err != nil { 255 | return "", err 256 | } 257 | actions = append(actions, 258 | fmt.Sprintf( 259 | `created this issue from a note in %s (%s)`, 260 | b.lookupMigratedProject(p).HTMLURL, html.EscapeString(p.Name), 261 | html.EscapeString(e.ProjectCard.ColumnName), 262 | ), 263 | ) 264 | case "added_to_project": 265 | p, err := b.getProject(e.ProjectCard.ProjectID) 266 | if err != nil { 267 | return "", err 268 | } 269 | actions = append(actions, 270 | fmt.Sprintf( 271 | `added this to %s in %s`, 272 | html.EscapeString(e.ProjectCard.ColumnName), 273 | b.lookupMigratedProject(p).HTMLURL, html.EscapeString(p.Name), 274 | ), 275 | ) 276 | case "moved_columns_in_project": 277 | p, err := b.getProject(e.ProjectCard.ProjectID) 278 | if err != nil { 279 | return "", err 280 | } 281 | actions = append(actions, 282 | fmt.Sprintf( 283 | `moved this from %s to %s in %s`, 284 | html.EscapeString(e.ProjectCard.PreviousColumnName), 285 | html.EscapeString(e.ProjectCard.ColumnName), 286 | b.lookupMigratedProject(p).HTMLURL, html.EscapeString(p.Name), 287 | ), 288 | ) 289 | case "removed_from_project": 290 | p, err := b.getProject(e.ProjectCard.ProjectID) 291 | if err != nil { 292 | return "", err 293 | } 294 | actions = append(actions, 295 | fmt.Sprintf( 296 | `removed this from %s in %s`, 297 | html.EscapeString(e.ProjectCard.ColumnName), 298 | b.lookupMigratedProject(p).HTMLURL, html.EscapeString(p.Name), 299 | ), 300 | ) 301 | case "milestoned", "demilestoned": 302 | var actionStr string 303 | if e.Event == "milestoned" { 304 | actionStr = "added this to" 305 | } else { 306 | actionStr = "removed this from" 307 | } 308 | if m := b.milestoneByTitle[e.Milestone.Title]; m != nil { 309 | actions = append(actions, 310 | fmt.Sprintf( 311 | `%s the %s milestone`, 312 | actionStr, 313 | m.HTMLURL, 314 | html.EscapeString(e.Milestone.Title), 315 | ), 316 | ) 317 | } else { 318 | actions = append(actions, 319 | fmt.Sprintf( 320 | `%s the %s milestone`, 321 | actionStr, 322 | html.EscapeString(e.Milestone.Title), 323 | ), 324 | ) 325 | } 326 | case "deployed": 327 | actions = append(actions, `deployed this`) 328 | } 329 | } 330 | 331 | var action string 332 | if len(actions) > 0 { 333 | for i, a := range actions { 334 | if i > 0 { 335 | if i == len(actions)-1 { 336 | action += " and " 337 | } else { 338 | action += ", " 339 | } 340 | } 341 | action += a 342 | } 343 | return action, nil 344 | } 345 | 346 | if len(addedLabels) > 0 { 347 | action += "added " + quoteLabels(addedLabels) 348 | } 349 | if len(removedLabels) > 0 { 350 | if action != "" { 351 | action += " and " 352 | } 353 | action += "removed " + quoteLabels(removedLabels) 354 | } 355 | if len(addedLabels) > 0 || len(removedLabels) > 0 { 356 | action += pluralUnit(len(addedLabels)+len(removedLabels), " label") 357 | } 358 | return action, nil 359 | } 360 | 361 | func (b *builder) mentionAll(users []*github.User) string { 362 | var s string 363 | for i, u := range users { 364 | if i > 0 { 365 | s += " " 366 | } 367 | s += "@" + b.commentFilters.apply(u.Login) 368 | } 369 | return s 370 | } 371 | 372 | func quoteLabels(xs []string) string { 373 | ys := make([]string, len(xs)) 374 | for i, x := range xs { 375 | ys[i] = "" + html.EscapeString(x) + "" 376 | } 377 | return strings.Join(ys, " ") 378 | } 379 | 380 | func (b *builder) lookupMigratedProject(orig *github.Project) *github.Project { 381 | if !strings.HasPrefix(orig.HTMLURL, b.sourceRepo.HTMLURL+"/projects/") { 382 | return orig 383 | } 384 | found := lookupProject(b.targetProjects, orig) 385 | if found != nil { 386 | return found 387 | } 388 | return orig 389 | } 390 | -------------------------------------------------------------------------------- /migrator/comment_filter.go: -------------------------------------------------------------------------------- 1 | package migrator 2 | 3 | import ( 4 | "net/url" 5 | "regexp" 6 | "strings" 7 | 8 | "github.com/itchyny/github-migrator/github" 9 | ) 10 | 11 | type commentFilter func(string) string 12 | 13 | func newRepoURLFilter(sourceRepo, targetRepo *github.Repo) commentFilter { 14 | sourceURL, _ := url.Parse(sourceRepo.HTMLURL) 15 | targetURL, _ := url.Parse(targetRepo.HTMLURL) 16 | replaceImageLinks := sourceURL.Scheme != targetURL.Scheme || sourceURL.Host != targetURL.Host 17 | var imageMarkdownPattern, imageHTMLPattern *regexp.Regexp 18 | if replaceImageLinks { 19 | urlPatten := sourceURL.Scheme + `://` + regexp.QuoteMeta(sourceURL.Host) + `[^"<>()]+` 20 | imageMarkdownPattern = regexp.MustCompile(`(?i)!\[[^]]*\]\((` + urlPatten + `)\)`) 21 | imageHTMLPattern = regexp.MustCompile(`(?i)]*\bsrc="(` + urlPatten + `)"[^<>]*>`) 22 | } 23 | return commentFilter(func(src string) string { 24 | src = strings.ReplaceAll(src, sourceRepo.HTMLURL, targetRepo.HTMLURL) 25 | if replaceImageLinks { 26 | src = imageMarkdownPattern.ReplaceAllString(src, `$0`) 27 | src = imageHTMLPattern.ReplaceAllString(src, `$0`) 28 | } 29 | return src 30 | }) 31 | } 32 | 33 | func newUserMappingFilter(userMapping map[string]string, targetRepo *github.Repo) commentFilter { 34 | if len(userMapping) == 0 { 35 | return commentFilter(func(src string) string { 36 | return src 37 | }) 38 | } 39 | froms := make([]string, 0, len(userMapping)) 40 | tos := make([]string, 0, len(userMapping)) 41 | userMappingRev := make(map[string]string, len(userMapping)) 42 | for k, v := range userMapping { 43 | froms = append(froms, k) 44 | tos = append(tos, v) 45 | userMappingRev[v] = k 46 | } 47 | re1 := regexp.MustCompile(buildPattern(froms)) 48 | re2 := regexp.MustCompile(buildPattern(tos)) 49 | re3 := regexp.MustCompile(`https?://[-.a-zA-Z0-9/_%]*` + buildPattern(tos)) 50 | targetURL, _ := url.Parse(targetRepo.HTMLURL) 51 | return commentFilter(func(src string) string { 52 | src = re1.ReplaceAllStringFunc(src, func(from string) string { 53 | return userMapping[from] 54 | }) 55 | src = re3.ReplaceAllStringFunc(src, func(url string) string { 56 | if strings.Contains(url, "://"+targetURL.Host+"/") { 57 | return url 58 | } 59 | return re2.ReplaceAllStringFunc(url, func(to string) string { 60 | return userMappingRev[to] 61 | }) 62 | }) 63 | return src 64 | }) 65 | } 66 | 67 | func buildPattern(xs []string) string { 68 | var pattern strings.Builder 69 | pattern.WriteString(`\b(`) 70 | for i, x := range xs { 71 | if i > 0 { 72 | pattern.WriteByte('|') 73 | } 74 | pattern.WriteString(regexp.QuoteMeta(x)) 75 | } 76 | pattern.WriteString(`)\b`) 77 | return pattern.String() 78 | } 79 | 80 | type commentFilters []commentFilter 81 | 82 | func newCommentFilters(fs ...commentFilter) commentFilters { 83 | return commentFilters(fs) 84 | } 85 | 86 | func (fs commentFilters) apply(src string) string { 87 | for _, f := range fs { 88 | src = f(src) 89 | } 90 | return src 91 | } 92 | -------------------------------------------------------------------------------- /migrator/diff.go: -------------------------------------------------------------------------------- 1 | package migrator 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | ) 7 | 8 | const ( 9 | truncateLength = 10000 10 | totalTruncateLength = 60000 11 | ) 12 | 13 | // Since import fails on too large diff, truncate it. 14 | // You may wonder building the diff from the api (without vnd.github.v3.diff header), 15 | // but it's impossible to build the complete diff even if the changes are small. 16 | func truncateDiff(diff string) string { 17 | s := new(strings.Builder) 18 | var i, j int 19 | for { 20 | i = strings.Index(diff, "\nindex ") 21 | if i < 0 { 22 | if len(diff) > truncateLength { 23 | s.WriteString("Too large diff\n") 24 | break 25 | } 26 | s.WriteString(diff) 27 | break 28 | } 29 | i++ // newline 30 | 31 | j = strings.Index(diff[i:], "\n") 32 | if j < 0 { 33 | if len(diff) > truncateLength { 34 | s.WriteString("Too large diff\n") 35 | break 36 | } 37 | s.WriteString(diff) 38 | break 39 | } 40 | j++ // newline 41 | s.WriteString(diff[:i+j]) 42 | diff = diff[i+j:] 43 | 44 | i = strings.Index(diff, "\ndiff ") 45 | if i < 0 { 46 | if len(diff) > truncateLength { 47 | s.WriteString("Too large diff\n") 48 | break 49 | } 50 | s.WriteString(diff) 51 | break 52 | } 53 | i++ // newline 54 | if i > truncateLength { 55 | s.WriteString("Too large diff\n") 56 | diff = diff[i:] 57 | continue 58 | } 59 | s.WriteString(diff[:i]) 60 | diff = diff[i:] 61 | } 62 | str := s.String() 63 | if len(str) > totalTruncateLength { 64 | str = str[:totalTruncateLength] + "\n\nToo large diff\n" 65 | } 66 | return str 67 | } 68 | 69 | var backquoteRe = regexp.MustCompile("((?:^|\n) *)```") 70 | 71 | func escapeBackQuotes(src string) string { 72 | return backquoteRe.ReplaceAllString(src, "$1\u00a0```") 73 | } 74 | -------------------------------------------------------------------------------- /migrator/diff_test.go: -------------------------------------------------------------------------------- 1 | package migrator 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestTruncateDiff(t *testing.T) { 11 | testCases := []struct { 12 | src, expected string 13 | }{ 14 | { 15 | src: `diff --git a/README.md b/README.md 16 | index 1234567..89abcde 100644 17 | --- a/README.md 18 | +++ b/README.md 19 | @@ -1,6 +1,16 @@ 20 | # README 21 | -deleted 22 | +added 23 | diff --git a/CHANGELOG.md b/CHANGELOG.md 24 | index 1234567..89abcde 100644 25 | --- a/CHANGELOG.md 26 | +++ b/CHANGELOG.md 27 | @@ -1,6 +1,16 @@ 28 | # CHANGELOG 29 | -deleted 30 | +added 31 | `, 32 | expected: `diff --git a/README.md b/README.md 33 | index 1234567..89abcde 100644 34 | --- a/README.md 35 | +++ b/README.md 36 | @@ -1,6 +1,16 @@ 37 | # README 38 | -deleted 39 | +added 40 | diff --git a/CHANGELOG.md b/CHANGELOG.md 41 | index 1234567..89abcde 100644 42 | --- a/CHANGELOG.md 43 | +++ b/CHANGELOG.md 44 | @@ -1,6 +1,16 @@ 45 | # CHANGELOG 46 | -deleted 47 | +added 48 | `, 49 | }, 50 | { 51 | src: `diff --git a/README.md b/README.md 52 | index 1234567..89abcde 100644 53 | --- a/README.md 54 | +++ b/README.md 55 | @@ -1,6 +1,16 @@ 56 | # README 57 | ` + strings.Repeat("\n", 20000), 58 | expected: `diff --git a/README.md b/README.md 59 | index 1234567..89abcde 100644 60 | Too large diff 61 | `, 62 | }, 63 | { 64 | src: `diff --git a/README.md b/README.md 65 | index 1234567..89abcde 100644 66 | --- a/README.md 67 | +++ b/README.md 68 | @@ -1,6 +1,16 @@ 69 | # README 70 | ` + strings.Repeat("\n", 20000) + ` 71 | +added 72 | diff --git a/CHANGELOG.md b/CHANGELOG.md 73 | index 1234567..89abcde 100644 74 | --- a/CHANGELOG.md 75 | +++ b/CHANGELOG.md 76 | @@ -1,6 +1,16 @@ 77 | # CHANGELOG 78 | -deleted 79 | +added 80 | `, 81 | expected: `diff --git a/README.md b/README.md 82 | index 1234567..89abcde 100644 83 | Too large diff 84 | diff --git a/CHANGELOG.md b/CHANGELOG.md 85 | index 1234567..89abcde 100644 86 | --- a/CHANGELOG.md 87 | +++ b/CHANGELOG.md 88 | @@ -1,6 +1,16 @@ 89 | # CHANGELOG 90 | -deleted 91 | +added 92 | `, 93 | }, 94 | { 95 | src: strings.Repeat(`diff --git a/README.md b/README.md 96 | index 1234567..89abcde 100644 97 | --- a/README.md 98 | +++ b/README.md 99 | @@ -1,6 +1,16 @@ 100 | # README 101 | `+strings.Repeat("\n", 5000)+` 102 | +added 103 | `, 20), 104 | expected: strings.Repeat(`diff --git a/README.md b/README.md 105 | index 1234567..89abcde 100644 106 | --- a/README.md 107 | +++ b/README.md 108 | @@ -1,6 +1,16 @@ 109 | # README 110 | `+strings.Repeat("\n", 5000)+` 111 | +added 112 | `, 20)[:60000] + "\n\nToo large diff\n", 113 | }, 114 | } 115 | for _, tc := range testCases { 116 | assert.Equal(t, tc.expected, truncateDiff(tc.src)) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /migrator/hooks.go: -------------------------------------------------------------------------------- 1 | package migrator 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | 7 | "github.com/itchyny/github-migrator/github" 8 | ) 9 | 10 | func (m *migrator) migrateHooks() error { 11 | sourceHooks, err := github.HooksToSlice(m.source.ListHooks()) 12 | if err != nil { 13 | return err 14 | } 15 | targetHooks, err := github.HooksToSlice(m.target.ListHooks()) 16 | if err != nil { 17 | return err 18 | } 19 | for _, sourceHook := range sourceHooks { 20 | fmt.Printf("[=>] migrating a hook: %s\n", sourceHook.Config.URL) 21 | var exists bool 22 | for _, targetHook := range targetHooks { 23 | if sourceHook.Name == targetHook.Name && 24 | sourceHook.Config.URL == targetHook.Config.URL { 25 | if sourceHook.Active != targetHook.Active || 26 | !reflect.DeepEqual(sourceHook.Events, targetHook.Events) || 27 | !reflect.DeepEqual(sourceHook.Config, targetHook.Config) { 28 | fmt.Printf("[|>] updating an existing hook: %s\n", targetHook.Config.URL) 29 | if _, err := m.target.UpdateHook(targetHook.ID, &github.UpdateHookParams{ 30 | Active: sourceHook.Active, 31 | Events: sourceHook.Events, 32 | Config: sourceHook.Config, 33 | }); err != nil { 34 | return err 35 | } 36 | } else { 37 | fmt.Printf("[--] skipping: %s (already exists)\n", sourceHook.Config.URL) 38 | } 39 | exists = true 40 | break 41 | } 42 | } 43 | if exists { 44 | continue 45 | } 46 | fmt.Printf("[>>] creating a new hook: %s\n", sourceHook.Config.URL) 47 | if _, err := m.target.CreateHook(&github.CreateHookParams{ 48 | Active: sourceHook.Active, 49 | Events: sourceHook.Events, 50 | Config: sourceHook.Config, 51 | }); err != nil { 52 | return err 53 | } 54 | } 55 | return nil 56 | } 57 | -------------------------------------------------------------------------------- /migrator/issues.go: -------------------------------------------------------------------------------- 1 | package migrator 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "strings" 8 | "time" 9 | 10 | "github.com/itchyny/github-migrator/github" 11 | ) 12 | 13 | var ( 14 | beforeImportIssueDuration = 500 * time.Millisecond 15 | waitImportIssueInitialDuration = 1 * time.Second 16 | ) 17 | 18 | func (m *migrator) migrateIssues() error { 19 | sourceIssues := m.source.ListIssues() 20 | targetIssuesBuffer := newIssuesBuffer(m.target.ListIssues()) 21 | var lastIssueNumber int 22 | for { 23 | issue, err := sourceIssues.Next() 24 | if err != nil { 25 | if err != io.EOF { 26 | return err 27 | } 28 | break 29 | } 30 | for ; issue.Number > lastIssueNumber; lastIssueNumber++ { 31 | issue := issue 32 | var deleted bool 33 | if deleted = issue.Number > lastIssueNumber+1; deleted { 34 | issue = &github.Issue{ 35 | Number: lastIssueNumber + 1, 36 | HTMLURL: fmt.Sprintf("%s/issues/%d", m.sourceRepo.HTMLURL, lastIssueNumber+1), 37 | CreatedAt: issue.CreatedAt, 38 | UpdatedAt: issue.CreatedAt, 39 | ClosedAt: issue.CreatedAt, 40 | } 41 | } 42 | result, err := m.migrateIssue(issue, targetIssuesBuffer, deleted, false) 43 | if err != nil { 44 | return err 45 | } 46 | if result != nil { 47 | if err := m.waitImportIssue(result.ID, issue); err != nil { 48 | if !strings.Contains(err.Error(), "Issue.assignee") { 49 | return fmt.Errorf("importing %s failed: %w", issue.HTMLURL, err) 50 | } 51 | result, err := m.migrateIssue(issue, targetIssuesBuffer, deleted, true) 52 | if err != nil { 53 | return err 54 | } 55 | if result != nil { 56 | if err := m.waitImportIssue(result.ID, issue); err != nil { 57 | return fmt.Errorf("importing %s failed: %w", issue.HTMLURL, err) 58 | } 59 | } 60 | } 61 | } 62 | } 63 | } 64 | return nil 65 | } 66 | 67 | func (m *migrator) migrateIssue( 68 | sourceIssue *github.Issue, targetIssuesBuffer *issuesBuffer, deleted, skipAssignee bool, 69 | ) (*github.ImportResult, error) { 70 | fmt.Printf("[=>] migrating an issue: %s\n", sourceIssue.HTMLURL) 71 | targetIssue, err := targetIssuesBuffer.get(sourceIssue.Number) 72 | if err != nil { 73 | return nil, err 74 | } 75 | if targetIssue != nil { 76 | fmt.Printf("[--] skipping: %s (already exists)\n", targetIssue.HTMLURL) 77 | m.cacheIssueID(targetIssue.Number, targetIssue.ID) 78 | return nil, nil 79 | } 80 | time.Sleep(beforeImportIssueDuration) 81 | if deleted { 82 | fmt.Printf("[>>] creating a new issue: (original: %s is deleted)\n", sourceIssue.HTMLURL) 83 | return m.target.Import(&github.Import{ 84 | Issue: &github.ImportIssue{ 85 | Title: "[Deleted issue]", 86 | Body: fmt.Sprintf(` 87 | 88 | 89 | 90 |
This issue was imported from %s, which has already been deleted.
91 | `, buildIssueLinkTag(m.sourceRepo, sourceIssue)), 92 | CreatedAt: sourceIssue.CreatedAt, 93 | UpdatedAt: sourceIssue.UpdatedAt, 94 | Closed: true, 95 | ClosedAt: sourceIssue.ClosedAt, 96 | }, 97 | Comments: []*github.ImportComment{}, 98 | }) 99 | } 100 | comments, err := github.CommentsToSlice(m.source.ListComments(sourceIssue.Number)) 101 | if err != nil { 102 | return nil, err 103 | } 104 | events, err := github.EventsToSlice(m.source.ListEvents(sourceIssue.Number)) 105 | if err != nil { 106 | return nil, err 107 | } 108 | var sourcePullReq *github.PullReq 109 | var commits []*github.Commit 110 | var commitDiff string 111 | var reviews []*github.Review 112 | var reviewComments []*github.ReviewComment 113 | if sourceIssue.PullRequest != nil { 114 | sourcePullReq, err = m.source.GetPullReq(sourceIssue.Number) 115 | if err != nil { 116 | return nil, err 117 | } 118 | commits, err = github.CommitsToSlice(m.source.ListPullReqCommits(sourceIssue.Number)) 119 | if err != nil { 120 | return nil, err 121 | } 122 | commitDiff, err = m.source.NewPath(sourcePullReq.Base.Repo.FullName). 123 | GetCompare(sourcePullReq.Base.SHA, sourcePullReq.Head.SHA) 124 | if err != nil { 125 | return nil, err 126 | } 127 | reviews, err = github.ReviewsToSlice(m.source.ListReviews(sourceIssue.Number)) 128 | if err != nil { 129 | return nil, err 130 | } 131 | reviewComments, err = github.ReviewCommentsToSlice(m.source.ListReviewComments(sourceIssue.Number)) 132 | if err != nil { 133 | return nil, err 134 | } 135 | } 136 | imp, err := m.buildImport( 137 | sourceIssue, sourcePullReq, comments, events, 138 | commits, commitDiff, reviews, reviewComments, 139 | skipAssignee, 140 | ) 141 | if err != nil { 142 | return nil, err 143 | } 144 | fmt.Printf("[>>] creating a new issue: (original: %s)\n", sourceIssue.HTMLURL) 145 | return m.target.Import(imp) 146 | } 147 | 148 | func (m *migrator) waitImportIssue(id int, issue *github.Issue) error { 149 | var retry int 150 | duration := waitImportIssueInitialDuration 151 | for { 152 | time.Sleep(duration) 153 | if retry > 1 { 154 | duration *= 2 155 | if duration > 10*time.Second { 156 | duration = 10 * time.Second 157 | } 158 | } 159 | res, err := m.target.GetImport(id) 160 | if err != nil { 161 | return err 162 | } 163 | switch res.Status { 164 | case "imported": 165 | fmt.Printf("[<>] checking status: %s (importing %s)\n", res.Status, issue.HTMLURL) 166 | return nil 167 | case "failed": 168 | fmt.Printf("[!!] checking status: %s (importing %s)\n", res.Status, issue.HTMLURL) 169 | if len(res.Errors) != 0 { 170 | return fmt.Errorf("failed status: %w", res.Errors) 171 | } 172 | return errors.New("failed status") 173 | default: 174 | fmt.Printf("[??] checking status: %s (importing %s)\n", res.Status, issue.HTMLURL) 175 | } 176 | retry++ 177 | if retry >= 60 { 178 | return errors.New("reached maximum retry count") 179 | } 180 | } 181 | } 182 | 183 | func (m *migrator) cacheIssueID(number, id int) { 184 | if m.issueIDByNumbers == nil { 185 | m.issueIDByNumbers = make(map[int]int) 186 | } 187 | m.issueIDByNumbers[number] = id 188 | } 189 | 190 | func (m *migrator) getTargetIssueID(number int) (int, error) { 191 | if id, ok := m.issueIDByNumbers[number]; ok { 192 | return id, nil 193 | } 194 | issue, err := m.target.GetIssue(number) 195 | if err != nil { 196 | return 0, err 197 | } 198 | if m.issueIDByNumbers == nil { 199 | m.issueIDByNumbers = make(map[int]int) 200 | } 201 | m.issueIDByNumbers[number] = issue.ID 202 | return issue.ID, nil 203 | } 204 | -------------------------------------------------------------------------------- /migrator/issues_buffer.go: -------------------------------------------------------------------------------- 1 | package migrator 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/itchyny/github-migrator/github" 7 | ) 8 | 9 | type issuesBuffer struct { 10 | src github.Issues 11 | issues []*github.Issue 12 | } 13 | 14 | func newIssuesBuffer(is github.Issues) *issuesBuffer { 15 | return &issuesBuffer{src: is} 16 | } 17 | 18 | func (ib *issuesBuffer) get(num int) (*github.Issue, error) { 19 | for _, issue := range ib.issues { 20 | if issue.Number == num { 21 | return issue, nil 22 | } 23 | } 24 | for { 25 | issue, err := ib.src.Next() 26 | if err != nil { 27 | if err != io.EOF { 28 | return nil, err 29 | } 30 | break 31 | } 32 | ib.issues = append(ib.issues, issue) 33 | if issue.Number == num { 34 | return issue, nil 35 | } else if issue.Number > num { 36 | return nil, nil 37 | } 38 | } 39 | return nil, nil 40 | } 41 | -------------------------------------------------------------------------------- /migrator/labels.go: -------------------------------------------------------------------------------- 1 | package migrator 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/itchyny/github-migrator/github" 8 | ) 9 | 10 | func (m *migrator) migrateLabels() error { 11 | sourceLabels, err := github.LabelsToSlice(m.source.ListLabels()) 12 | if err != nil { 13 | return err 14 | } 15 | targetLabels, err := github.LabelsToSlice(m.target.ListLabels()) 16 | if err != nil { 17 | return err 18 | } 19 | for _, sourceLabel := range sourceLabels { 20 | fmt.Printf("[=>] migrating a label: %s\n", sourceLabel.Name) 21 | var exists bool 22 | for _, targetLabel := range targetLabels { 23 | if strings.EqualFold(sourceLabel.Name, targetLabel.Name) { 24 | if sourceLabel.Description != targetLabel.Description || 25 | sourceLabel.Color != targetLabel.Color { 26 | fmt.Printf("[|>] updating an existing label: %s\n", targetLabel.Name) 27 | if _, err := m.target.UpdateLabel(targetLabel.Name, &github.UpdateLabelParams{ 28 | Name: sourceLabel.Name, 29 | Description: sourceLabel.Description, 30 | Color: sourceLabel.Color, 31 | }); err != nil { 32 | return err 33 | } 34 | } else { 35 | fmt.Printf("[--] skipping: %s (already exists)\n", sourceLabel.Name) 36 | } 37 | exists = true 38 | break 39 | } 40 | } 41 | if exists { 42 | continue 43 | } 44 | fmt.Printf("[>>] creating a new label: %s\n", sourceLabel.Name) 45 | if _, err := m.target.CreateLabel(&github.CreateLabelParams{ 46 | Name: sourceLabel.Name, 47 | Description: sourceLabel.Description, 48 | Color: sourceLabel.Color, 49 | }); err != nil { 50 | return err 51 | } 52 | } 53 | return nil 54 | } 55 | -------------------------------------------------------------------------------- /migrator/migrator.go: -------------------------------------------------------------------------------- 1 | package migrator 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/itchyny/github-migrator/github" 7 | "github.com/itchyny/github-migrator/repo" 8 | ) 9 | 10 | // Migrator represents a GitHub migrator. 11 | type Migrator interface { 12 | Migrate() error 13 | } 14 | 15 | // New creates a new Migrator. 16 | func New(source, target *repo.Repo, userMapping map[string]string) Migrator { 17 | return &migrator{source: source, target: target, userMapping: userMapping} 18 | } 19 | 20 | type migrator struct { 21 | source, target *repo.Repo 22 | userMapping map[string]string 23 | sourceRepo, targetRepo *github.Repo 24 | commentFilters commentFilters 25 | targetMembers []*github.Member 26 | targetProjects []*github.Project 27 | projectByIDs map[int]*github.Project 28 | userByNames map[string]*github.User 29 | errorUserByNames map[string]error 30 | issueIDByNumbers map[int]int 31 | milestoneByTitle map[string]*github.Milestone 32 | } 33 | 34 | // Migrate the repository. 35 | func (m *migrator) Migrate() (err error) { 36 | if m.sourceRepo, err = m.source.Get(); err != nil { 37 | return err 38 | } 39 | if m.targetRepo, err = m.target.Get(); err != nil { 40 | return err 41 | } 42 | m.commentFilters = newCommentFilters( 43 | newRepoURLFilter(m.sourceRepo, m.targetRepo), 44 | newUserMappingFilter(m.userMapping, m.targetRepo), 45 | ) 46 | if m.targetMembers, err = github.MembersToSlice(m.target.ListMembers()); err != nil { 47 | return err 48 | } 49 | if err = m.migrateRepo(); err != nil { 50 | return err 51 | } 52 | if err = m.migrateLabels(); err != nil { 53 | return err 54 | } 55 | // projects and columns should be imported before issues 56 | if err = m.migrateProjects(); err != nil { 57 | return err 58 | } 59 | if projects, err := github.ProjectsToSlice(m.target.ListProjects()); err != nil { 60 | if !strings.Contains(err.Error(), "Projects are disabled for this repository") { 61 | return err 62 | } 63 | m.targetProjects = []*github.Project{} 64 | } else { 65 | m.targetProjects = projects 66 | } 67 | // milestones should be imported before issues 68 | if err = m.migrateMilestones(); err != nil { 69 | return err 70 | } 71 | if err = m.migrateIssues(); err != nil { 72 | return err 73 | } 74 | // projects cards should be imported after issues 75 | if err = m.migrateProjectCards(); err != nil { 76 | return err 77 | } 78 | if err = m.migrateHooks(); err != nil { 79 | return err 80 | } 81 | return nil 82 | } 83 | -------------------------------------------------------------------------------- /migrator/migrator_test.go: -------------------------------------------------------------------------------- 1 | package migrator 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "os" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/require" 12 | "gopkg.in/yaml.v3" 13 | 14 | "github.com/itchyny/github-migrator/github" 15 | "github.com/itchyny/github-migrator/repo" 16 | ) 17 | 18 | func init() { 19 | beforeImportIssueDuration = 0 20 | waitImportIssueInitialDuration = 0 21 | waitProjectColumnDuration = 0 22 | waitProjectCardDuration = 0 23 | } 24 | 25 | type testRepo struct { 26 | Repo *github.Repo 27 | UpdateRepo *github.Repo `json:"update_repo"` 28 | Members []*github.Member `json:"members"` 29 | UserByNames map[string]*github.User `json:"users"` 30 | Labels []*github.Label `json:"labels"` 31 | CreateLabels []*github.Label `json:"create_labels"` 32 | UpdateLabels []*github.Label `json:"update_labels"` 33 | Issues []struct { 34 | *github.PullReq 35 | Comments []*github.Comment `json:"comments"` 36 | Events []*github.Event `json:"events"` 37 | Commits []*github.Commit `json:"commit_details"` 38 | Reviews []*github.Review `json:"reviews"` 39 | ReviewComments []*github.ReviewComment `json:"review_comments"` 40 | } 41 | Compare map[string]string 42 | Imports []*github.Import `json:"imports"` 43 | Projects []*struct { 44 | *github.Project 45 | Columns []*testProjectColumn `json:"columns"` 46 | } `json:"projects"` 47 | CreateProjects []*github.Project `json:"create_projects"` 48 | UpdateProjects []*github.Project `json:"update_projects"` 49 | CreateProjectColumns []*github.ProjectColumn `json:"create_project_columns"` 50 | CreateProjectCards []*github.CreateProjectCardParams `json:"create_project_cards"` 51 | Milestones []*github.Milestone `json:"milestones"` 52 | CreateMilestones []*github.Milestone `json:"create_milestones"` 53 | UpdateMilestones []*github.Milestone `json:"update_milestones"` 54 | Hooks []*github.Hook `json:"hooks"` 55 | CreateHooks []*github.Hook `json:"create_hooks"` 56 | UpdateHooks []*github.Hook `json:"update_hooks"` 57 | } 58 | 59 | type testProjectColumn struct { 60 | *github.ProjectColumn 61 | Cards []*github.ProjectCard `json:"cards"` 62 | } 63 | 64 | func (r *testRepo) build(t *testing.T, isTarget bool) *repo.Repo { 65 | projects := make([]*github.Project, len(r.Projects)) 66 | for i, p := range r.Projects { 67 | projects[i] = p.Project 68 | } 69 | milestones := make([]*github.Milestone, len(r.Milestones)) 70 | copy(milestones, r.Milestones) 71 | 72 | return repo.New(github.NewMockClient( 73 | 74 | github.MockGetUser(func(name string) (*github.User, error) { 75 | assert.True(t, isTarget) 76 | if u, ok := r.UserByNames[name]; ok { 77 | return u, nil 78 | } 79 | return nil, fmt.Errorf("user not found: %s", name) 80 | }), 81 | github.MockListMembers(func(string) github.Members { 82 | assert.True(t, isTarget) 83 | return github.MembersFromSlice(r.Members) 84 | }), 85 | 86 | github.MockGetRepo(func(string) (*github.Repo, error) { 87 | return r.Repo, nil 88 | }), 89 | github.MockUpdateRepo(func(_ string, params *github.UpdateRepoParams) (*github.Repo, error) { 90 | assert.True(t, isTarget) 91 | assert.NotNil(t, r.UpdateRepo) 92 | assert.Equal(t, r.UpdateRepo.Name, params.Name) 93 | assert.Equal(t, r.UpdateRepo.Description, params.Description) 94 | assert.Equal(t, r.UpdateRepo.Homepage, params.Homepage) 95 | assert.Equal(t, r.UpdateRepo.Private, params.Private) 96 | return r.UpdateRepo, nil 97 | }), 98 | 99 | github.MockListLabels(func(string) github.Labels { 100 | return github.LabelsFromSlice(r.Labels) 101 | }), 102 | github.MockCreateLabel((func(i int) func(string, *github.CreateLabelParams) (*github.Label, error) { 103 | return func(_ string, params *github.CreateLabelParams) (*github.Label, error) { 104 | defer func() { i++ }() 105 | assert.True(t, isTarget) 106 | require.Greater(t, len(r.CreateLabels), i) 107 | assert.Equal(t, r.CreateLabels[i].Name, params.Name) 108 | assert.Equal(t, r.CreateLabels[i].Color, params.Color) 109 | assert.Equal(t, r.CreateLabels[i].Description, params.Description) 110 | return nil, nil 111 | } 112 | })(0)), 113 | github.MockUpdateLabel((func(i int) func(string, string, *github.UpdateLabelParams) (*github.Label, error) { 114 | return func(path, name string, params *github.UpdateLabelParams) (*github.Label, error) { 115 | defer func() { i++ }() 116 | assert.True(t, isTarget) 117 | require.Greater(t, len(r.UpdateLabels), i) 118 | assert.Equal(t, r.UpdateLabels[i].Name, name) 119 | assert.Equal(t, r.UpdateLabels[i].Name, params.Name) 120 | assert.Equal(t, r.UpdateLabels[i].Color, params.Color) 121 | assert.Equal(t, r.UpdateLabels[i].Description, params.Description) 122 | return nil, nil 123 | } 124 | })(0)), 125 | 126 | github.MockListIssues(func(string, *github.ListIssuesParams) github.Issues { 127 | xs := make([]*github.Issue, len(r.Issues)) 128 | for i, s := range r.Issues { 129 | xs[i] = &s.PullReq.Issue 130 | } 131 | return github.IssuesFromSlice(xs) 132 | }), 133 | github.MockGetIssue(func(_ string, issueNumber int) (*github.Issue, error) { 134 | for _, i := range r.Issues { 135 | if i.Number == issueNumber { 136 | return &i.PullReq.Issue, nil 137 | } 138 | } 139 | panic(fmt.Sprintf("unexpected issue number: %d", issueNumber)) 140 | }), 141 | github.MockListComments(func(_ string, issueNumber int) github.Comments { 142 | assert.True(t, !isTarget) 143 | for _, s := range r.Issues { 144 | if s.Issue.Number == issueNumber { 145 | return github.CommentsFromSlice(s.Comments) 146 | } 147 | } 148 | panic(fmt.Sprintf("unexpected issue number: %d", issueNumber)) 149 | }), 150 | github.MockListEvents(func(_ string, issueNumber int) github.Events { 151 | assert.True(t, !isTarget) 152 | for _, s := range r.Issues { 153 | if s.Issue.Number == issueNumber { 154 | return github.EventsFromSlice(s.Events) 155 | } 156 | } 157 | panic(fmt.Sprintf("unexpected issue number: %d", issueNumber)) 158 | }), 159 | 160 | github.MockGetPullReq(func(_ string, pullNumber int) (*github.PullReq, error) { 161 | assert.True(t, !isTarget) 162 | for _, s := range r.Issues { 163 | if s.PullReq.Number == pullNumber { 164 | return s.PullReq, nil 165 | } 166 | } 167 | panic(fmt.Sprintf("unexpected pull request number: %d", pullNumber)) 168 | }), 169 | github.MockListPullReqCommits(func(_ string, pullNumber int) github.Commits { 170 | assert.True(t, !isTarget) 171 | for _, s := range r.Issues { 172 | if s.PullReq.Number == pullNumber { 173 | return github.CommitsFromSlice(s.Commits) 174 | } 175 | } 176 | panic(fmt.Sprintf("unexpected pull request number: %d", pullNumber)) 177 | }), 178 | github.MockGetCompare(func(_ string, base, head string) (string, error) { 179 | assert.True(t, !isTarget) 180 | if diff, ok := r.Compare[base+"..."+head]; ok { 181 | return diff, nil 182 | } 183 | panic(fmt.Sprintf("unexpected compare: %s...%s", base, head)) 184 | }), 185 | github.MockListReviews(func(_ string, pullNumber int) github.Reviews { 186 | assert.True(t, !isTarget) 187 | for _, s := range r.Issues { 188 | if s.PullReq.Number == pullNumber { 189 | return github.ReviewsFromSlice(s.Reviews) 190 | } 191 | } 192 | panic(fmt.Sprintf("unexpected pull request number: %d", pullNumber)) 193 | }), 194 | github.MockListReviewComments(func(_ string, pullNumber int) github.ReviewComments { 195 | assert.True(t, !isTarget) 196 | for _, s := range r.Issues { 197 | if s.PullReq.Number == pullNumber { 198 | return github.ReviewCommentsFromSlice(s.ReviewComments) 199 | } 200 | } 201 | panic(fmt.Sprintf("unexpected pull request number: %d", pullNumber)) 202 | }), 203 | 204 | github.MockListProjects(func(string, *github.ListProjectsParams) github.Projects { 205 | return github.ProjectsFromSlice(projects) 206 | }), 207 | github.MockGetProject(func(projectID int) (*github.Project, error) { 208 | assert.True(t, !isTarget) 209 | for _, p := range r.Projects { 210 | if p.ID == projectID { 211 | return p.Project, nil 212 | } 213 | } 214 | panic(fmt.Sprintf("unexpected project id: %d", projectID)) 215 | }), 216 | github.MockCreateProject((func(i int) func(string, *github.CreateProjectParams) (*github.Project, error) { 217 | return func(_ string, params *github.CreateProjectParams) (*github.Project, error) { 218 | defer func() { i++ }() 219 | assert.True(t, isTarget) 220 | require.Greater(t, len(r.CreateProjects), i) 221 | assert.Equal(t, r.CreateProjects[i].Name, params.Name) 222 | assert.Equal(t, r.CreateProjects[i].Body, params.Body) 223 | projects = append(projects, r.CreateProjects[i]) 224 | return r.CreateProjects[i], nil 225 | } 226 | })(0)), 227 | github.MockUpdateProject((func(i int) func(int, *github.UpdateProjectParams) (*github.Project, error) { 228 | return func(projectID int, params *github.UpdateProjectParams) (*github.Project, error) { 229 | defer func() { i++ }() 230 | assert.True(t, isTarget) 231 | require.Greater(t, len(r.UpdateProjects), i) 232 | assert.Equal(t, "", params.Name) 233 | assert.Equal(t, r.UpdateProjects[i].Body, params.Body) 234 | assert.Equal(t, r.UpdateProjects[i].State, params.State) 235 | return r.UpdateProjects[i], nil 236 | } 237 | })(0)), 238 | 239 | github.MockListProjectColumns(func(projectID int) github.ProjectColumns { 240 | for _, p := range r.Projects { 241 | if p.ID == projectID { 242 | cs := make([]*github.ProjectColumn, len(p.Columns)) 243 | for i, c := range p.Columns { 244 | cs[i] = c.ProjectColumn 245 | } 246 | return github.ProjectColumnsFromSlice(cs) 247 | } 248 | } 249 | return github.ProjectColumnsFromSlice([]*github.ProjectColumn{}) 250 | }), 251 | github.MockCreateProjectColumn((func(i int) func(int, string) (*github.ProjectColumn, error) { 252 | return func(projectID int, name string) (*github.ProjectColumn, error) { 253 | defer func() { i++ }() 254 | for _, p := range r.Projects { 255 | if p.ID == projectID { 256 | p.Columns = append(p.Columns, &testProjectColumn{ 257 | ProjectColumn: &github.ProjectColumn{ 258 | ID: -1, 259 | Name: name, 260 | }, 261 | }) 262 | } 263 | } 264 | assert.True(t, isTarget) 265 | require.Greater(t, len(r.CreateProjectColumns), i) 266 | assert.Equal(t, r.CreateProjectColumns[i].Name, name) 267 | return r.CreateProjectColumns[i], nil 268 | } 269 | })(0)), 270 | 271 | github.MockListProjectCards(func(columnID int) github.ProjectCards { 272 | for _, p := range r.Projects { 273 | for _, c := range p.Columns { 274 | if c.ID == columnID { 275 | return github.ProjectCardsFromSlice(c.Cards) 276 | } 277 | } 278 | } 279 | return github.ProjectCardsFromSlice([]*github.ProjectCard{}) 280 | }), 281 | github.MockCreateProjectCard((func(i int) func(int, *github.CreateProjectCardParams) (*github.ProjectCard, error) { 282 | return func(projectID int, params *github.CreateProjectCardParams) (*github.ProjectCard, error) { 283 | defer func() { i++ }() 284 | assert.True(t, isTarget) 285 | require.Greater(t, len(r.CreateProjectCards), i) 286 | assert.Equal(t, r.CreateProjectCards[i], params) 287 | return nil, nil 288 | } 289 | })(0)), 290 | 291 | github.MockListMilestones(func(string, *github.ListMilestonesParams) github.Milestones { 292 | return github.MilestonesFromSlice(milestones) 293 | }), 294 | github.MockCreateMilestone((func(i int) func(string, *github.CreateMilestoneParams) (*github.Milestone, error) { 295 | return func(_ string, params *github.CreateMilestoneParams) (*github.Milestone, error) { 296 | defer func() { i++ }() 297 | assert.True(t, isTarget) 298 | require.Greater(t, len(r.CreateMilestones), i) 299 | assert.Equal(t, r.CreateMilestones[i].Title, params.Title) 300 | assert.Equal(t, r.CreateMilestones[i].Description, params.Description) 301 | assert.Equal(t, r.CreateMilestones[i].State, params.State) 302 | milestones = append(milestones, r.CreateMilestones[i]) 303 | return r.CreateMilestones[i], nil 304 | } 305 | })(0)), 306 | github.MockUpdateMilestone((func(i int) func(string, int, *github.UpdateMilestoneParams) (*github.Milestone, error) { 307 | return func(_ string, milestoneNumber int, params *github.UpdateMilestoneParams) (*github.Milestone, error) { 308 | defer func() { i++ }() 309 | assert.True(t, isTarget) 310 | require.Greater(t, len(r.UpdateMilestones), i) 311 | assert.Equal(t, r.UpdateMilestones[i].Number, milestoneNumber) 312 | assert.Equal(t, r.UpdateMilestones[i].Title, params.Title) 313 | assert.Equal(t, r.UpdateMilestones[i].Description, params.Description) 314 | assert.Equal(t, r.UpdateMilestones[i].State, params.State) 315 | return r.UpdateMilestones[i], nil 316 | } 317 | })(0)), 318 | github.MockDeleteMilestone((func(i int) func(string, int) error { 319 | return func(string, int) error { 320 | return nil 321 | } 322 | })(0)), 323 | 324 | github.MockListHooks(func(string) github.Hooks { 325 | return github.HooksFromSlice(r.Hooks) 326 | }), 327 | github.MockCreateHook((func(i int) func(string, *github.CreateHookParams) (*github.Hook, error) { 328 | return func(_ string, params *github.CreateHookParams) (*github.Hook, error) { 329 | defer func() { i++ }() 330 | assert.True(t, isTarget) 331 | require.Greater(t, len(r.CreateHooks), i) 332 | assert.Equal(t, r.CreateHooks[i].Events, params.Events) 333 | assert.Equal(t, r.CreateHooks[i].Config, params.Config) 334 | assert.Equal(t, r.CreateHooks[i].Active, params.Active) 335 | return nil, nil 336 | } 337 | })(0)), 338 | github.MockUpdateHook((func(i int) func(string, int, *github.UpdateHookParams) (*github.Hook, error) { 339 | return func(_ string, hookID int, params *github.UpdateHookParams) (*github.Hook, error) { 340 | defer func() { i++ }() 341 | assert.True(t, isTarget) 342 | require.Greater(t, len(r.UpdateHooks), i) 343 | assert.Equal(t, r.UpdateHooks[i].Events, params.Events) 344 | assert.Equal(t, r.UpdateHooks[i].Config, params.Config) 345 | assert.Equal(t, r.UpdateHooks[i].Active, params.Active) 346 | return nil, nil 347 | } 348 | })(0)), 349 | 350 | github.MockImport((func(i int) func(string, *github.Import) (*github.ImportResult, error) { 351 | return func(_ string, x *github.Import) (*github.ImportResult, error) { 352 | defer func() { i++ }() 353 | assert.True(t, isTarget) 354 | require.Greater(t, len(r.Imports), i) 355 | assert.Equal(t, r.Imports[i], x) 356 | return &github.ImportResult{ 357 | ID: 12345, 358 | Status: "pending", 359 | URL: "http://localhost/repo/example/target/import/issues/12345", 360 | }, nil 361 | } 362 | })(0)), 363 | github.MockGetImport(func(_ string, id int) (*github.ImportResult, error) { 364 | assert.True(t, isTarget) 365 | assert.Equal(t, 12345, id) 366 | return &github.ImportResult{ 367 | ID: 12345, 368 | Status: "imported", 369 | URL: "http://localhost/repo/example/target/import/issues/12345", 370 | }, nil 371 | }), 372 | ), r.Repo.FullName) 373 | } 374 | 375 | func TestMigratorMigrate(t *testing.T) { 376 | f, err := os.Open("test.yaml") 377 | require.NoError(t, err) 378 | defer f.Close() 379 | 380 | var testCases []struct { 381 | Name string `json:"name"` 382 | Source *testRepo `json:"source"` 383 | Target *testRepo `json:"target"` 384 | UserMapping map[string]string `json:"user_mapping"` 385 | } 386 | require.NoError(t, decodeYAML(f, &testCases)) 387 | 388 | for _, tc := range testCases { 389 | t.Run(tc.Name, func(t *testing.T) { 390 | source := tc.Source.build(t, false) 391 | target := tc.Target.build(t, true) 392 | migrator := New(source, target, tc.UserMapping) 393 | assert.Nil(t, migrator.Migrate()) 394 | }) 395 | } 396 | } 397 | 398 | func decodeYAML(r io.Reader, d interface{}) error { 399 | // decode to interface once to use json tags 400 | var m interface{} 401 | if err := yaml.NewDecoder(r).Decode(&m); err != nil { 402 | return err 403 | } 404 | bs, err := json.Marshal(m) 405 | if err != nil { 406 | return err 407 | } 408 | return json.Unmarshal(bs, d) 409 | } 410 | -------------------------------------------------------------------------------- /migrator/milestones.go: -------------------------------------------------------------------------------- 1 | package migrator 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/itchyny/github-migrator/github" 8 | ) 9 | 10 | func (m *migrator) migrateMilestones() error { 11 | sourceMilestones, err := github.MilestonesToSlice( 12 | m.source.ListMilestones(&github.ListMilestonesParams{ 13 | State: github.ListMilestonesParamStateAll, 14 | }), 15 | ) 16 | if err != nil { 17 | return err 18 | } 19 | targetMilestones, err := github.MilestonesToSlice( 20 | m.target.ListMilestones(&github.ListMilestonesParams{ 21 | State: github.ListMilestonesParamStateAll, 22 | }), 23 | ) 24 | if err != nil { 25 | return err 26 | } 27 | var largestMilestoneNumber int 28 | for _, l := range targetMilestones { 29 | if largestMilestoneNumber < l.Number { 30 | largestMilestoneNumber = l.Number 31 | } 32 | } 33 | var deletedMilestones []int 34 | for _, l := range sourceMilestones { 35 | fmt.Printf("[=>] migrating a milestone: %s\n", l.Title) 36 | for l.Number > largestMilestoneNumber+1 { 37 | n, err := m.target.CreateMilestone(&github.CreateMilestoneParams{ 38 | Title: fmt.Sprintf("[Deleted milestone %d]", largestMilestoneNumber+1), // must be unique 39 | State: github.MilestoneStateClosed, 40 | }) 41 | if err != nil { 42 | return err 43 | } 44 | largestMilestoneNumber = n.Number 45 | deletedMilestones = append(deletedMilestones, n.Number) 46 | } 47 | n := lookupMilestone(targetMilestones, l) 48 | if n == nil { 49 | fmt.Printf("[>>] creating a new milestone: %s\n", l.Title) 50 | if n, err = m.target.CreateMilestone(&github.CreateMilestoneParams{ 51 | Title: l.Title, Description: l.Description, 52 | State: l.State, DueOn: l.DueOn, 53 | }); err != nil { 54 | return err 55 | } 56 | largestMilestoneNumber = n.Number 57 | } 58 | if l.Description != n.Description || l.State != n.State || normalizeTimeToPST(l.DueOn) != normalizeTimeToPST(n.DueOn) { 59 | fmt.Printf("[|>] updating an existing milestone: %s\n", l.Title) 60 | if _, err = m.target.UpdateMilestone(n.Number, &github.UpdateMilestoneParams{ 61 | Title: l.Title, 62 | Description: l.Description, 63 | State: l.State, 64 | DueOn: l.DueOn, 65 | }); err != nil { 66 | return err 67 | } 68 | } 69 | } 70 | for _, number := range deletedMilestones { 71 | if err := m.target.DeleteMilestone(number); err != nil { 72 | return err 73 | } 74 | } 75 | targetMilestones, err = github.MilestonesToSlice( 76 | m.target.ListMilestones(&github.ListMilestonesParams{ 77 | State: github.ListMilestonesParamStateAll, 78 | }), 79 | ) 80 | if err != nil { 81 | return err 82 | } 83 | m.milestoneByTitle = make(map[string]*github.Milestone) 84 | for _, l := range targetMilestones { 85 | m.milestoneByTitle[l.Title] = l 86 | } 87 | return nil 88 | } 89 | 90 | func lookupMilestone(ps []*github.Milestone, l *github.Milestone) *github.Milestone { 91 | for _, n := range ps { 92 | if l.Title == n.Title { 93 | return n 94 | } 95 | } 96 | return nil 97 | } 98 | 99 | // https://github.community/t5/How-to-use-Git-and-GitHub/Milestone-quot-Due-On-quot-field-defaults-to-7-00-when-set-by-v3/m-p/6922 100 | func normalizeTimeToPST(s string) string { 101 | t, err := time.Parse(time.RFC3339, s) 102 | if err != nil { 103 | return s 104 | } 105 | pst, err := time.LoadLocation("America/Los_Angeles") 106 | if err != nil { 107 | return s 108 | } 109 | return time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, pst).In(time.UTC).Format(time.RFC3339) 110 | } 111 | -------------------------------------------------------------------------------- /migrator/plural.go: -------------------------------------------------------------------------------- 1 | package migrator 2 | 3 | import "fmt" 4 | 5 | func pluralUnit(count int, unit string) string { 6 | if count == 1 { 7 | return unit 8 | } 9 | return unit + "s" 10 | } 11 | 12 | func plural(count int, unit string) string { 13 | return fmt.Sprint(count) + " " + pluralUnit(count, unit) 14 | } 15 | -------------------------------------------------------------------------------- /migrator/project_cards.go: -------------------------------------------------------------------------------- 1 | package migrator 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "strings" 7 | "time" 8 | 9 | "github.com/itchyny/github-migrator/github" 10 | ) 11 | 12 | var waitProjectCardDuration = 100 * time.Millisecond 13 | 14 | func (m *migrator) migrateProjectCards() error { 15 | sourceProjects, err := github.ProjectsToSlice(m.source.ListProjects()) 16 | if err != nil { 17 | if strings.Contains(err.Error(), "Projects are disabled for this repository") { 18 | return nil // do nothing 19 | } 20 | return err 21 | } 22 | if len(sourceProjects) == 0 { 23 | return nil 24 | } 25 | targetProjects, err := github.ProjectsToSlice(m.target.ListProjects()) 26 | if err != nil { 27 | return err 28 | } 29 | for _, p := range sourceProjects { 30 | fmt.Printf("[=>] migrating cards in a project: %s\n", p.Name) 31 | q := lookupProject(targetProjects, p) 32 | if q == nil { 33 | return fmt.Errorf("project not found: %s", p.Name) 34 | } 35 | if err := m.migrateProjectCardsInProject(p.ID, q.ID); err != nil { 36 | return err 37 | } 38 | } 39 | return nil 40 | } 41 | 42 | func (m *migrator) migrateProjectCardsInProject(sourceID, targetID int) error { 43 | sourceColumns := m.source.ListProjectColumns(sourceID) 44 | targetColumns, err := github.ProjectColumnsToSlice( 45 | m.target.ListProjectColumns(targetID), 46 | ) 47 | if err != nil { 48 | return err 49 | } 50 | for { 51 | c, err := sourceColumns.Next() 52 | if err != nil { 53 | if err != io.EOF { 54 | return err 55 | } 56 | return nil 57 | } 58 | fmt.Printf("[=>] migrating cards in a project column: %s\n", c.Name) 59 | d := lookupProjectColumn(targetColumns, c) 60 | if d == nil { 61 | return fmt.Errorf("project card not found: %s", c.Name) 62 | } 63 | if err := m.migrateProjectCardsInColumn(c.ID, d.ID); err != nil { 64 | return err 65 | } 66 | } 67 | } 68 | 69 | func (m *migrator) migrateProjectCardsInColumn(sourceID, targetID int) error { 70 | sourceCards, err := github.ProjectCardsToSlice( 71 | m.source.ListProjectCards(sourceID), 72 | ) 73 | if err != nil { 74 | return err 75 | } 76 | targetCards, err := github.ProjectCardsToSlice( 77 | m.target.ListProjectCards(targetID), 78 | ) 79 | if err != nil { 80 | return err 81 | } 82 | reverseProjectCards(sourceCards) 83 | for _, c := range sourceCards { 84 | fmt.Printf("[=>] migrating a card: %s\n", m.getCardInfo(c)) 85 | if lookupProjectCard(targetCards, c) != nil { 86 | fmt.Printf("[--] skipping: %s (already exists)\n", m.getCardInfo(c)) 87 | continue 88 | } 89 | fmt.Printf("[>>] creating a new card: %s\n", m.getCardInfo(c)) 90 | var params *github.CreateProjectCardParams 91 | if issueNumber := c.GetIssueNumber(); issueNumber > 0 { 92 | id, err := m.getTargetIssueID(issueNumber) 93 | if err != nil { 94 | return err 95 | } 96 | params = &github.CreateProjectCardParams{ 97 | ContentID: id, 98 | ContentType: github.ProjectCardContentTypeIssue, 99 | } 100 | } else { 101 | params = &github.CreateProjectCardParams{ 102 | Note: m.commentFilters.apply(c.Note), 103 | } 104 | } 105 | if _, err := m.target.CreateProjectCard(targetID, params); err != nil { 106 | return err 107 | } 108 | time.Sleep(waitProjectCardDuration) 109 | } 110 | return nil 111 | } 112 | 113 | func lookupProjectCard(cs []*github.ProjectCard, c *github.ProjectCard) *github.ProjectCard { 114 | for _, d := range cs { 115 | if c.Note != "" && c.Note == d.Note || c.GetIssueNumber() == d.GetIssueNumber() { 116 | return d 117 | } 118 | } 119 | return nil 120 | } 121 | 122 | func reverseProjectCards(cs []*github.ProjectCard) { 123 | for left, right := 0, len(cs)-1; left < right; left, right = left+1, right-1 { 124 | cs[left], cs[right] = cs[right], cs[left] 125 | } 126 | } 127 | 128 | func (m *migrator) getCardInfo(c *github.ProjectCard) string { 129 | if issueNumber := c.GetIssueNumber(); issueNumber > 0 { 130 | return fmt.Sprintf("%s/issues/%d", m.targetRepo.FullName, issueNumber) 131 | } 132 | xs := strings.Split(c.Note, "\n") 133 | if len(xs) > 0 { 134 | return xs[0] 135 | } 136 | return c.Note 137 | } 138 | -------------------------------------------------------------------------------- /migrator/project_columns.go: -------------------------------------------------------------------------------- 1 | package migrator 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "time" 7 | 8 | "github.com/itchyny/github-migrator/github" 9 | ) 10 | 11 | var waitProjectColumnDuration = 100 * time.Millisecond 12 | 13 | func (m *migrator) migrateProjectColumns(sourceID, targetID int) error { 14 | sourceColumns := m.source.ListProjectColumns(sourceID) 15 | targetColumns, err := github.ProjectColumnsToSlice( 16 | m.target.ListProjectColumns(targetID), 17 | ) 18 | if err != nil { 19 | return err 20 | } 21 | for { 22 | c, err := sourceColumns.Next() 23 | if err != nil { 24 | if err != io.EOF { 25 | return err 26 | } 27 | return nil 28 | } 29 | fmt.Printf("[=>] migrating a project column: %s\n", c.Name) 30 | d := lookupProjectColumn(targetColumns, c) 31 | if d == nil { 32 | fmt.Printf("[>>] creating a new project column: %s\n", c.Name) 33 | if _, err = m.target.CreateProjectColumn(targetID, c.Name); err != nil { 34 | return err 35 | } 36 | } 37 | time.Sleep(waitProjectColumnDuration) 38 | } 39 | } 40 | 41 | func lookupProjectColumn(ps []*github.ProjectColumn, c *github.ProjectColumn) *github.ProjectColumn { 42 | for _, d := range ps { 43 | if c.Name == d.Name { 44 | return d 45 | } 46 | } 47 | return nil 48 | } 49 | -------------------------------------------------------------------------------- /migrator/projects.go: -------------------------------------------------------------------------------- 1 | package migrator 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/itchyny/github-migrator/github" 8 | ) 9 | 10 | func (m *migrator) migrateProjects() error { 11 | sourceProjects, err := github.ProjectsToSlice(m.source.ListProjects()) 12 | if err != nil { 13 | if strings.Contains(err.Error(), "Projects are disabled for this repository") { 14 | return nil // do nothing 15 | } 16 | return err 17 | } 18 | if len(sourceProjects) == 0 { 19 | return nil 20 | } 21 | targetProjects, err := github.ProjectsToSlice(m.target.ListProjects()) 22 | if err != nil { 23 | return err 24 | } 25 | var largestProjectNumber int 26 | for _, l := range targetProjects { 27 | if largestProjectNumber < l.Number { 28 | largestProjectNumber = l.Number 29 | } 30 | } 31 | for _, p := range sourceProjects { 32 | fmt.Printf("[=>] migrating a project: %s\n", p.Name) 33 | for p.Number > largestProjectNumber+1 { 34 | q, err := m.target.CreateProject(&github.CreateProjectParams{ 35 | Name: "[Deleted project]", 36 | }) 37 | if err != nil { 38 | return err 39 | } 40 | largestProjectNumber = q.Number 41 | if err := m.target.DeleteProject(q.ID); err != nil { 42 | return err 43 | } 44 | } 45 | q := lookupProject(targetProjects, p) 46 | body := m.commentFilters.apply(p.Body) 47 | if q == nil { 48 | fmt.Printf("[>>] creating a new project: %s\n", p.Name) 49 | if q, err = m.target.CreateProject(&github.CreateProjectParams{ 50 | Name: p.Name, Body: body, 51 | }); err != nil { 52 | return err 53 | } 54 | largestProjectNumber = q.Number 55 | } 56 | if body != q.Body || p.State != q.State { 57 | fmt.Printf("[|>] updating an existing project: %s\n", p.Name) 58 | if q, err = m.target.UpdateProject(q.ID, &github.UpdateProjectParams{ 59 | // Do not update name. 60 | Body: body, State: p.State, 61 | }); err != nil { 62 | return err 63 | } 64 | } 65 | if err := m.migrateProjectColumns(p.ID, q.ID); err != nil { 66 | return err 67 | } 68 | } 69 | return nil 70 | } 71 | 72 | func (m *migrator) getProject(id int) (*github.Project, error) { 73 | if p, ok := m.projectByIDs[id]; ok { 74 | return p, nil 75 | } 76 | p, err := m.source.GetProject(id) 77 | if err != nil { 78 | return nil, err 79 | } 80 | if m.projectByIDs == nil { 81 | m.projectByIDs = make(map[int]*github.Project) 82 | } 83 | m.projectByIDs[id] = p 84 | return p, nil 85 | } 86 | 87 | func lookupProject(ps []*github.Project, p *github.Project) *github.Project { 88 | for _, q := range ps { 89 | if p.Name == q.Name { 90 | return q 91 | } 92 | } 93 | return nil 94 | } 95 | -------------------------------------------------------------------------------- /migrator/repos.go: -------------------------------------------------------------------------------- 1 | package migrator 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/itchyny/github-migrator/github" 7 | ) 8 | 9 | func (m *migrator) migrateRepo() error { 10 | fmt.Printf( 11 | "[=>] migrating: %s (%s) => %s (%s)\n", 12 | m.sourceRepo.Name, m.sourceRepo.HTMLURL, 13 | m.targetRepo.Name, m.targetRepo.HTMLURL, 14 | ) 15 | 16 | if params, ok := buildUpdateRepoParams(m.sourceRepo, m.targetRepo); ok { 17 | fmt.Printf("[|>] updating the repository: %s\n", m.targetRepo.HTMLURL) 18 | if _, err := m.target.Update(params); err != nil { 19 | return err 20 | } 21 | } 22 | return nil 23 | } 24 | 25 | func buildUpdateRepoParams(sourceRepo, targetRepo *github.Repo) (*github.UpdateRepoParams, bool) { 26 | var update bool 27 | params := &github.UpdateRepoParams{ 28 | Name: targetRepo.Name, 29 | Description: targetRepo.Description, 30 | Homepage: targetRepo.Homepage, 31 | Private: targetRepo.Private, 32 | } 33 | if params.Description == "" && sourceRepo.Description != "" { 34 | params.Description = sourceRepo.Description 35 | update = true 36 | } 37 | if params.Homepage == "" && sourceRepo.Homepage != "" { 38 | params.Homepage = sourceRepo.Homepage 39 | update = true 40 | } 41 | // other fields should not be overwritten unless examined carefully 42 | return params, update 43 | } 44 | -------------------------------------------------------------------------------- /migrator/users.go: -------------------------------------------------------------------------------- 1 | package migrator 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/itchyny/github-migrator/github" 7 | ) 8 | 9 | func (m *migrator) isTargetMember(name string) (bool, error) { 10 | if strings.HasPrefix(m.targetRepo.FullName, name+"/") { 11 | return true, nil 12 | } 13 | for _, member := range m.targetMembers { 14 | if member.Login == name { 15 | return true, nil 16 | } 17 | } 18 | return false, nil 19 | } 20 | 21 | func (m *migrator) lookupUser(name string) (*github.User, error) { 22 | if u, ok := m.userByNames[name]; ok { 23 | return u, nil 24 | } 25 | if err, ok := m.errorUserByNames[name]; ok { 26 | return nil, err 27 | } 28 | for _, member := range m.targetMembers { 29 | if member.Login == name { 30 | return member.ToUser(), nil 31 | } 32 | } 33 | u, err := m.target.GetUser(name) 34 | if err != nil { 35 | if m.errorUserByNames == nil { 36 | m.errorUserByNames = make(map[string]error) 37 | } 38 | m.errorUserByNames[name] = err 39 | return nil, err 40 | } 41 | if m.userByNames == nil { 42 | m.userByNames = make(map[string]*github.User) 43 | } 44 | m.userByNames[name] = u 45 | return u, nil 46 | } 47 | -------------------------------------------------------------------------------- /repo/comments.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import "github.com/itchyny/github-migrator/github" 4 | 5 | // ListComments lists the comments. 6 | func (r *Repo) ListComments(issueNumber int) github.Comments { 7 | return r.cli.ListComments(r.path, issueNumber) 8 | } 9 | -------------------------------------------------------------------------------- /repo/comments_test.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | 8 | "github.com/itchyny/github-migrator/github" 9 | ) 10 | 11 | func TestRepoListComments(t *testing.T) { 12 | expected := []*github.Comment{ 13 | { 14 | Body: "Example body 1", 15 | HTMLURL: "http://localhost/example/test/issues/1#issuecomment-1", 16 | }, 17 | { 18 | Body: "Example body 2", 19 | HTMLURL: "http://localhost/example/test/issues/1#issuecomment-2", 20 | }, 21 | } 22 | repo := New(github.NewMockClient( 23 | github.MockListComments(func(string, int) github.Comments { 24 | return github.CommentsFromSlice(expected) 25 | }), 26 | ), "example/test") 27 | got, err := github.CommentsToSlice(repo.ListComments(1)) 28 | assert.Nil(t, err) 29 | assert.Equal(t, got, expected) 30 | } 31 | -------------------------------------------------------------------------------- /repo/commits.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import "github.com/itchyny/github-migrator/github" 4 | 5 | // ListPullReqCommits lists the commits of a pull request. 6 | func (r *Repo) ListPullReqCommits(pullNumber int) github.Commits { 7 | return r.cli.ListPullReqCommits(r.path, pullNumber) 8 | } 9 | -------------------------------------------------------------------------------- /repo/commits_test.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | 8 | "github.com/itchyny/github-migrator/github" 9 | ) 10 | 11 | func TestRepoListPullReqCommits(t *testing.T) { 12 | expected := []*github.Commit{ 13 | { 14 | HTMLURL: "http://localhost/example/test/commit/xxx", 15 | SHA: "xxx", 16 | }, 17 | { 18 | HTMLURL: "http://localhost/example/test/commit/yyy", 19 | SHA: "yyy", 20 | }, 21 | } 22 | repo := New(github.NewMockClient( 23 | github.MockListPullReqCommits(func(string, int) github.Commits { 24 | return github.CommitsFromSlice(expected) 25 | }), 26 | ), "example/test") 27 | got, err := github.CommitsToSlice(repo.ListPullReqCommits(10)) 28 | assert.Nil(t, err) 29 | assert.Equal(t, got, expected) 30 | } 31 | -------------------------------------------------------------------------------- /repo/diff.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | // GetDiff gets the diff. 4 | func (r *Repo) GetDiff(sha string) (string, error) { 5 | return r.cli.GetDiff(r.path, sha) 6 | } 7 | 8 | // GetCompare gets the compare. 9 | func (r *Repo) GetCompare(base, head string) (string, error) { 10 | return r.cli.GetCompare(r.path, base, head) 11 | } 12 | -------------------------------------------------------------------------------- /repo/diff_test.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | 8 | "github.com/itchyny/github-migrator/github" 9 | ) 10 | 11 | func TestRepoGetDiff(t *testing.T) { 12 | expected := `diff --git a/README.md b/README.md 13 | index 1234567..89abcde 100644 14 | --- a/README.md 15 | +++ b/README.md 16 | @@ -1,6 +1,16 @@ 17 | # README 18 | -deleted 19 | +added 20 | ` 21 | repo := New(github.NewMockClient( 22 | github.MockGetDiff(func(string, string) (string, error) { 23 | return expected, nil 24 | }), 25 | ), "example/test") 26 | got, err := repo.GetDiff("xxxyyy") 27 | assert.Nil(t, err) 28 | assert.Equal(t, got, expected) 29 | } 30 | 31 | func TestRepoGetCompare(t *testing.T) { 32 | expected := `diff --git a/README.md b/README.md 33 | index 1234567..89abcde 100644 34 | --- a/README.md 35 | +++ b/README.md 36 | @@ -1,6 +1,16 @@ 37 | # README 38 | -deleted 39 | +added 40 | ` 41 | repo := New(github.NewMockClient( 42 | github.MockGetCompare(func(string, string, string) (string, error) { 43 | return expected, nil 44 | }), 45 | ), "example/test") 46 | got, err := repo.GetCompare("xxxyyy", "zzzwww") 47 | assert.Nil(t, err) 48 | assert.Equal(t, got, expected) 49 | } 50 | -------------------------------------------------------------------------------- /repo/events.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import "github.com/itchyny/github-migrator/github" 4 | 5 | // ListEvents lists the events. 6 | func (r *Repo) ListEvents(issueNumber int) github.Events { 7 | return r.cli.ListEvents(r.path, issueNumber) 8 | } 9 | -------------------------------------------------------------------------------- /repo/events_test.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | 8 | "github.com/itchyny/github-migrator/github" 9 | ) 10 | 11 | func TestRepoListEvents(t *testing.T) { 12 | expected := []*github.Event{ 13 | { 14 | Actor: &github.User{Login: "user-1"}, 15 | Event: "labeled", 16 | Label: &github.EventLabel{Name: "label-1"}, 17 | }, 18 | { 19 | Actor: &github.User{Login: "user-2"}, 20 | Event: "labeled", 21 | Label: &github.EventLabel{Name: "label-2"}, 22 | }, 23 | } 24 | repo := New(github.NewMockClient( 25 | github.MockListEvents(func(string, int) github.Events { 26 | return github.EventsFromSlice(expected) 27 | }), 28 | ), "example/test") 29 | got, err := github.EventsToSlice(repo.ListEvents(1)) 30 | assert.Nil(t, err) 31 | assert.Equal(t, got, expected) 32 | } 33 | -------------------------------------------------------------------------------- /repo/get.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import "github.com/itchyny/github-migrator/github" 4 | 5 | // Get the repository. 6 | func (r *Repo) Get() (*github.Repo, error) { 7 | return r.cli.GetRepo(r.path) 8 | } 9 | -------------------------------------------------------------------------------- /repo/get_test.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | 8 | "github.com/itchyny/github-migrator/github" 9 | ) 10 | 11 | func TestRepoGet(t *testing.T) { 12 | expected := &github.Repo{ 13 | Name: "test", 14 | Description: "Test repository.", 15 | HTMLURL: "http://localhost/example/test", 16 | } 17 | repo := New(github.NewMockClient( 18 | github.MockGetRepo(func(string) (*github.Repo, error) { 19 | return expected, nil 20 | }), 21 | ), "example/test") 22 | got, err := repo.Get() 23 | assert.Nil(t, err) 24 | assert.Equal(t, got, expected) 25 | } 26 | -------------------------------------------------------------------------------- /repo/hooks.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import "github.com/itchyny/github-migrator/github" 4 | 5 | // ListHooks lists the hooks. 6 | func (r *Repo) ListHooks() github.Hooks { 7 | return r.cli.ListHooks(r.path) 8 | } 9 | 10 | // GetHook gets the hook. 11 | func (r *Repo) GetHook(hookID int) (*github.Hook, error) { 12 | return r.cli.GetHook(r.path, hookID) 13 | } 14 | 15 | // CreateHook creates a hook. 16 | func (r *Repo) CreateHook(params *github.CreateHookParams) (*github.Hook, error) { 17 | return r.cli.CreateHook(r.path, params) 18 | } 19 | 20 | // UpdateHook updates the hook. 21 | func (r *Repo) UpdateHook(hookID int, params *github.UpdateHookParams) (*github.Hook, error) { 22 | return r.cli.UpdateHook(r.path, hookID, params) 23 | } 24 | -------------------------------------------------------------------------------- /repo/hooks_test.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | 8 | "github.com/itchyny/github-migrator/github" 9 | ) 10 | 11 | func TestRepoListHooks(t *testing.T) { 12 | expected := []*github.Hook{ 13 | { 14 | ID: 10, 15 | Name: "Test hook 1", 16 | }, 17 | { 18 | ID: 10, 19 | Name: "Test hook 1", 20 | }, 21 | } 22 | repo := New(github.NewMockClient( 23 | github.MockListHooks(func(string) github.Hooks { 24 | return github.HooksFromSlice(expected) 25 | }), 26 | ), "example/test") 27 | got, err := github.HooksToSlice(repo.ListHooks()) 28 | assert.Nil(t, err) 29 | assert.Equal(t, got, expected) 30 | } 31 | 32 | func TestRepoGetHook(t *testing.T) { 33 | expected := &github.Hook{ 34 | ID: 1, 35 | Name: "Test hook 1", 36 | } 37 | repo := New(github.NewMockClient( 38 | github.MockGetHook(func(string, int) (*github.Hook, error) { 39 | return expected, nil 40 | }), 41 | ), "example/test") 42 | got, err := repo.GetHook(1) 43 | assert.Nil(t, err) 44 | assert.Equal(t, got, expected) 45 | } 46 | 47 | func TestRepoCreateHook(t *testing.T) { 48 | expected := &github.Hook{ 49 | ID: 1, 50 | Name: "Test hook 1", 51 | Active: true, 52 | } 53 | repo := New(github.NewMockClient( 54 | github.MockCreateHook(func(string, *github.CreateHookParams) (*github.Hook, error) { 55 | return expected, nil 56 | }), 57 | ), "example/test") 58 | got, err := repo.CreateHook(&github.CreateHookParams{ 59 | Active: true, 60 | }) 61 | assert.Nil(t, err) 62 | assert.Equal(t, got, expected) 63 | } 64 | 65 | func TestRepoUpdateHook(t *testing.T) { 66 | expected := &github.Hook{ 67 | ID: 1, 68 | Name: "Test hook 1", 69 | Active: true, 70 | } 71 | repo := New(github.NewMockClient( 72 | github.MockUpdateHook(func(string, int, *github.UpdateHookParams) (*github.Hook, error) { 73 | return expected, nil 74 | }), 75 | ), "example/test") 76 | got, err := repo.UpdateHook(1, &github.UpdateHookParams{ 77 | Active: true, 78 | }) 79 | assert.Nil(t, err) 80 | assert.Equal(t, got, expected) 81 | } 82 | -------------------------------------------------------------------------------- /repo/import.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import "github.com/itchyny/github-migrator/github" 4 | 5 | // Import an object. 6 | func (r *Repo) Import(x *github.Import) (*github.ImportResult, error) { 7 | return r.cli.Import(r.path, x) 8 | } 9 | 10 | // GetImport gets the importing status. 11 | func (r *Repo) GetImport(id int) (*github.ImportResult, error) { 12 | return r.cli.GetImport(r.path, id) 13 | } 14 | -------------------------------------------------------------------------------- /repo/issues.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import "github.com/itchyny/github-migrator/github" 4 | 5 | // ListIssues lists the issues. 6 | func (r *Repo) ListIssues() github.Issues { 7 | return r.cli.ListIssues(r.path, &github.ListIssuesParams{ 8 | Filter: github.ListIssuesParamFilterAll, 9 | State: github.ListIssuesParamStateAll, 10 | Direction: github.ListIssuesParamDirectionAsc, 11 | }) 12 | } 13 | 14 | // GetIssue gets the issue. 15 | func (r *Repo) GetIssue(issueNumber int) (*github.Issue, error) { 16 | return r.cli.GetIssue(r.path, issueNumber) 17 | } 18 | 19 | // AddAssignees assigns users to the issue. 20 | func (r *Repo) AddAssignees(issueNumber int, assignees []string) error { 21 | return r.cli.AddAssignees(r.path, issueNumber, assignees) 22 | } 23 | -------------------------------------------------------------------------------- /repo/issues_test.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | 8 | "github.com/itchyny/github-migrator/github" 9 | ) 10 | 11 | func TestRepoListIssues(t *testing.T) { 12 | expected := []*github.Issue{ 13 | { 14 | Number: 1, 15 | Title: "Example title 1", 16 | State: github.IssueStateClosed, 17 | Body: "Example body 1", 18 | HTMLURL: "http://localhost/example/test/issues/1", 19 | }, 20 | { 21 | Number: 2, 22 | Title: "Example title 2", 23 | State: github.IssueStateOpen, 24 | Body: "Example body 2", 25 | HTMLURL: "http://localhost/example/test/issues/2", 26 | }, 27 | } 28 | repo := New(github.NewMockClient( 29 | github.MockListIssues(func(string, *github.ListIssuesParams) github.Issues { 30 | return github.IssuesFromSlice(expected) 31 | }), 32 | ), "example/test") 33 | got, err := github.IssuesToSlice(repo.ListIssues()) 34 | assert.Nil(t, err) 35 | assert.Equal(t, got, expected) 36 | } 37 | 38 | func TestRepoGetIssue(t *testing.T) { 39 | expected := &github.Issue{ 40 | Number: 1, 41 | Title: "Example title 1", 42 | State: github.IssueStateClosed, 43 | Body: "Example body 1", 44 | HTMLURL: "http://localhost/example/test/issue/1", 45 | ClosedBy: &github.User{ 46 | Login: "test-user", 47 | }, 48 | } 49 | repo := New(github.NewMockClient( 50 | github.MockGetIssue(func(string, int) (*github.Issue, error) { 51 | return expected, nil 52 | }), 53 | ), "example/test") 54 | got, err := repo.GetIssue(1) 55 | assert.Nil(t, err) 56 | assert.Equal(t, got, expected) 57 | } 58 | -------------------------------------------------------------------------------- /repo/labels.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import "github.com/itchyny/github-migrator/github" 4 | 5 | // ListLabels lists the labels. 6 | func (r *Repo) ListLabels() github.Labels { 7 | return r.cli.ListLabels(r.path) 8 | } 9 | 10 | // CreateLabel creates a new label. 11 | func (r *Repo) CreateLabel(params *github.CreateLabelParams) (*github.Label, error) { 12 | return r.cli.CreateLabel(r.path, params) 13 | } 14 | 15 | // UpdateLabel creates a new label. 16 | func (r *Repo) UpdateLabel(name string, params *github.UpdateLabelParams) (*github.Label, error) { 17 | return r.cli.UpdateLabel(r.path, name, params) 18 | } 19 | -------------------------------------------------------------------------------- /repo/labels_test.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | 8 | "github.com/itchyny/github-migrator/github" 9 | ) 10 | 11 | func TestRepoListLabels(t *testing.T) { 12 | expected := []*github.Label{ 13 | { 14 | ID: 1, 15 | Name: "bug", 16 | Description: "This is a bug.", 17 | Color: "fc2929", 18 | Default: false, 19 | }, 20 | { 21 | ID: 2, 22 | Name: "design", 23 | Description: "This is a design issue.", 24 | Color: "bfdadc", 25 | Default: false, 26 | }, 27 | } 28 | repo := New(github.NewMockClient( 29 | github.MockListLabels(func(string) github.Labels { 30 | return github.LabelsFromSlice(expected) 31 | }), 32 | ), "example/test") 33 | got, err := github.LabelsToSlice(repo.ListLabels()) 34 | assert.Nil(t, err) 35 | assert.Equal(t, got, expected) 36 | } 37 | 38 | func TestRepoCreateLabel(t *testing.T) { 39 | expected := &github.Label{ 40 | ID: 1, 41 | Name: "bug", 42 | Description: "This is a bug.", 43 | Color: "fc2929", 44 | Default: false, 45 | } 46 | repo := New(github.NewMockClient( 47 | github.MockCreateLabel(func(string, *github.CreateLabelParams) (*github.Label, error) { 48 | return expected, nil 49 | }), 50 | ), "example/test") 51 | got, err := repo.CreateLabel(&github.CreateLabelParams{ 52 | Name: "bug", 53 | Description: "This is a bug.", 54 | Color: "fc2929", 55 | }) 56 | assert.Nil(t, err) 57 | assert.Equal(t, got, expected) 58 | } 59 | 60 | func TestRepoUpdateLabel(t *testing.T) { 61 | expected := &github.Label{ 62 | ID: 1, 63 | Name: "warn", 64 | Description: "This is a warning.", 65 | Color: "fcfc29", 66 | Default: false, 67 | } 68 | repo := New(github.NewMockClient( 69 | github.MockUpdateLabel(func(string, string, *github.UpdateLabelParams) (*github.Label, error) { 70 | return expected, nil 71 | }), 72 | ), "example/test") 73 | got, err := repo.UpdateLabel("bug", &github.UpdateLabelParams{ 74 | Name: "warn", 75 | Description: "This is a warning.", 76 | Color: "fcfc29", 77 | }) 78 | assert.Nil(t, err) 79 | assert.Equal(t, got, expected) 80 | } 81 | -------------------------------------------------------------------------------- /repo/members.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/itchyny/github-migrator/github" 7 | ) 8 | 9 | // ListMembers lists the members. 10 | func (r *Repo) ListMembers() github.Members { 11 | return r.cli.ListMembers(strings.Split(r.path, "/")[0]) 12 | } 13 | -------------------------------------------------------------------------------- /repo/members_test.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | 8 | "github.com/itchyny/github-migrator/github" 9 | ) 10 | 11 | func TestListMembers(t *testing.T) { 12 | expected := []*github.Member{ 13 | { 14 | Login: "user1", 15 | }, 16 | { 17 | Login: "user2", 18 | }, 19 | } 20 | repo := New(github.NewMockClient( 21 | github.MockListMembers(func(string) github.Members { 22 | return github.MembersFromSlice(expected) 23 | }), 24 | ), "example") 25 | got, err := github.MembersToSlice(repo.ListMembers()) 26 | assert.Nil(t, err) 27 | assert.Equal(t, got, expected) 28 | } 29 | -------------------------------------------------------------------------------- /repo/milestones.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import "github.com/itchyny/github-migrator/github" 4 | 5 | // ListMilestones lists the milestones. 6 | func (r *Repo) ListMilestones(params *github.ListMilestonesParams) github.Milestones { 7 | return r.cli.ListMilestones(r.path, params) 8 | } 9 | 10 | // GetMilestone gets the milestone. 11 | func (r *Repo) GetMilestone(milestoneNumber int) (*github.Milestone, error) { 12 | return r.cli.GetMilestone(r.path, milestoneNumber) 13 | } 14 | 15 | // CreateMilestone creates a milestone. 16 | func (r *Repo) CreateMilestone(params *github.CreateMilestoneParams) (*github.Milestone, error) { 17 | return r.cli.CreateMilestone(r.path, params) 18 | } 19 | 20 | // UpdateMilestone updates the milestone. 21 | func (r *Repo) UpdateMilestone(milestoneNumber int, params *github.UpdateMilestoneParams) (*github.Milestone, error) { 22 | return r.cli.UpdateMilestone(r.path, milestoneNumber, params) 23 | } 24 | 25 | // DeleteMilestone deletes the milestone. 26 | func (r *Repo) DeleteMilestone(milestoneNumber int) error { 27 | return r.cli.DeleteMilestone(r.path, milestoneNumber) 28 | } 29 | -------------------------------------------------------------------------------- /repo/milestones_test.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | 8 | "github.com/itchyny/github-migrator/github" 9 | ) 10 | 11 | func TestRepoListMilestones(t *testing.T) { 12 | expected := []*github.Milestone{ 13 | { 14 | ID: 10, 15 | Title: "Test milestone 1", 16 | }, 17 | { 18 | ID: 10, 19 | Title: "Test milestone 1", 20 | }, 21 | } 22 | repo := New(github.NewMockClient( 23 | github.MockListMilestones(func(string, *github.ListMilestonesParams) github.Milestones { 24 | return github.MilestonesFromSlice(expected) 25 | }), 26 | ), "example/test") 27 | got, err := github.MilestonesToSlice(repo.ListMilestones(nil)) 28 | assert.Nil(t, err) 29 | assert.Equal(t, got, expected) 30 | } 31 | 32 | func TestRepoGetMilestone(t *testing.T) { 33 | expected := &github.Milestone{ 34 | ID: 1, 35 | Title: "Test milestone 1", 36 | } 37 | repo := New(github.NewMockClient( 38 | github.MockGetMilestone(func(string, int) (*github.Milestone, error) { 39 | return expected, nil 40 | }), 41 | ), "example/test") 42 | got, err := repo.GetMilestone(1) 43 | assert.Nil(t, err) 44 | assert.Equal(t, got, expected) 45 | } 46 | 47 | func TestRepoCreateMilestone(t *testing.T) { 48 | expected := &github.Milestone{ 49 | ID: 1, 50 | Title: "Test milestone 1", 51 | } 52 | repo := New(github.NewMockClient( 53 | github.MockCreateMilestone(func(string, *github.CreateMilestoneParams) (*github.Milestone, error) { 54 | return expected, nil 55 | }), 56 | ), "example/test") 57 | got, err := repo.CreateMilestone(&github.CreateMilestoneParams{}) 58 | assert.Nil(t, err) 59 | assert.Equal(t, got, expected) 60 | } 61 | 62 | func TestRepoUpdateMilestone(t *testing.T) { 63 | expected := &github.Milestone{ 64 | ID: 1, 65 | Title: "Test milestone 1", 66 | } 67 | repo := New(github.NewMockClient( 68 | github.MockUpdateMilestone(func(string, int, *github.UpdateMilestoneParams) (*github.Milestone, error) { 69 | return expected, nil 70 | }), 71 | ), "example/test") 72 | got, err := repo.UpdateMilestone(1, &github.UpdateMilestoneParams{}) 73 | assert.Nil(t, err) 74 | assert.Equal(t, got, expected) 75 | } 76 | 77 | func TestRepoDeleteMilestone(t *testing.T) { 78 | repo := New(github.NewMockClient( 79 | github.MockDeleteMilestone(func(string, int) error { 80 | return nil 81 | }), 82 | ), "example/test") 83 | err := repo.DeleteMilestone(1) 84 | assert.Nil(t, err) 85 | } 86 | -------------------------------------------------------------------------------- /repo/project_cards.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import "github.com/itchyny/github-migrator/github" 4 | 5 | // ListProjectCards lists the project cards. 6 | func (r *Repo) ListProjectCards(columnID int) github.ProjectCards { 7 | return r.cli.ListProjectCards(columnID) 8 | } 9 | 10 | // GetProjectCard gets the project card. 11 | func (r *Repo) GetProjectCard(projectCardID int) (*github.ProjectCard, error) { 12 | return r.cli.GetProjectCard(projectCardID) 13 | } 14 | 15 | // CreateProjectCard creates a project card. 16 | func (r *Repo) CreateProjectCard(columnID int, params *github.CreateProjectCardParams) (*github.ProjectCard, error) { 17 | return r.cli.CreateProjectCard(columnID, params) 18 | } 19 | 20 | // UpdateProjectCard updates the project card.. 21 | func (r *Repo) UpdateProjectCard(projectCardID int, params *github.UpdateProjectCardParams) (*github.ProjectCard, error) { 22 | return r.cli.UpdateProjectCard(projectCardID, params) 23 | } 24 | 25 | // MoveProjectCard moves the project card.. 26 | func (r *Repo) MoveProjectCard(projectCardID int, params *github.MoveProjectCardParams) (*github.ProjectCard, error) { 27 | return r.cli.MoveProjectCard(projectCardID, params) 28 | } 29 | -------------------------------------------------------------------------------- /repo/project_cards_test.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | 8 | "github.com/itchyny/github-migrator/github" 9 | ) 10 | 11 | func TestRepoListProjectCards(t *testing.T) { 12 | expected := []*github.ProjectCard{ 13 | { 14 | ID: 1, 15 | Note: "Test project card 1", 16 | }, 17 | { 18 | ID: 2, 19 | Note: "Test project card 2", 20 | }, 21 | } 22 | repo := New(github.NewMockClient( 23 | github.MockListProjectCards(func(int) github.ProjectCards { 24 | return github.ProjectCardsFromSlice(expected) 25 | }), 26 | ), "example/test") 27 | got, err := github.ProjectCardsToSlice(repo.ListProjectCards(1)) 28 | assert.Nil(t, err) 29 | assert.Equal(t, got, expected) 30 | } 31 | 32 | func TestRepoGetProjectCard(t *testing.T) { 33 | expected := &github.ProjectCard{ 34 | ID: 1, 35 | Note: "Test project card 1", 36 | } 37 | repo := New(github.NewMockClient( 38 | github.MockGetProjectCard(func(int) (*github.ProjectCard, error) { 39 | return expected, nil 40 | }), 41 | ), "example/test") 42 | got, err := repo.GetProjectCard(1) 43 | assert.Nil(t, err) 44 | assert.Equal(t, got, expected) 45 | } 46 | 47 | func TestRepoCreateProjectCard(t *testing.T) { 48 | expected := &github.ProjectCard{ 49 | ID: 1, 50 | Note: "Test project card 1", 51 | } 52 | repo := New(github.NewMockClient( 53 | github.MockCreateProjectCard(func(int, *github.CreateProjectCardParams) (*github.ProjectCard, error) { 54 | return expected, nil 55 | }), 56 | ), "example/test") 57 | got, err := repo.CreateProjectCard(10, nil) 58 | assert.Nil(t, err) 59 | assert.Equal(t, got, expected) 60 | } 61 | 62 | func TestRepoUpdateProjectCard(t *testing.T) { 63 | expected := &github.ProjectCard{ 64 | ID: 1, 65 | Note: "Test project card 1", 66 | } 67 | repo := New(github.NewMockClient( 68 | github.MockUpdateProjectCard(func(int, *github.UpdateProjectCardParams) (*github.ProjectCard, error) { 69 | return expected, nil 70 | }), 71 | ), "example/test") 72 | got, err := repo.UpdateProjectCard(1, nil) 73 | assert.Nil(t, err) 74 | assert.Equal(t, got, expected) 75 | } 76 | 77 | func TestRepoMoveProjectCard(t *testing.T) { 78 | expected := &github.ProjectCard{ 79 | ID: 1, 80 | Note: "Test project card 1", 81 | } 82 | repo := New(github.NewMockClient( 83 | github.MockMoveProjectCard(func(int, *github.MoveProjectCardParams) (*github.ProjectCard, error) { 84 | return expected, nil 85 | }), 86 | ), "example/test") 87 | got, err := repo.MoveProjectCard(1, nil) 88 | assert.Nil(t, err) 89 | assert.Equal(t, got, expected) 90 | } 91 | -------------------------------------------------------------------------------- /repo/project_columns.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import "github.com/itchyny/github-migrator/github" 4 | 5 | // ListProjectColumns lists the project columns. 6 | func (r *Repo) ListProjectColumns(projectID int) github.ProjectColumns { 7 | return r.cli.ListProjectColumns(projectID) 8 | } 9 | 10 | // GetProjectColumn gets the project column. 11 | func (r *Repo) GetProjectColumn(projectColumnID int) (*github.ProjectColumn, error) { 12 | return r.cli.GetProjectColumn(projectColumnID) 13 | } 14 | 15 | // CreateProjectColumn creates a project column. 16 | func (r *Repo) CreateProjectColumn(projectID int, name string) (*github.ProjectColumn, error) { 17 | return r.cli.CreateProjectColumn(projectID, name) 18 | } 19 | 20 | // UpdateProjectColumn updates the project column.. 21 | func (r *Repo) UpdateProjectColumn(projectColumnID int, name string) (*github.ProjectColumn, error) { 22 | return r.cli.UpdateProjectColumn(projectColumnID, name) 23 | } 24 | -------------------------------------------------------------------------------- /repo/project_columns_test.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | 8 | "github.com/itchyny/github-migrator/github" 9 | ) 10 | 11 | func TestRepoListProjectColumns(t *testing.T) { 12 | expected := []*github.ProjectColumn{ 13 | { 14 | ID: 1, 15 | Name: "Test project column 1", 16 | }, 17 | { 18 | ID: 2, 19 | Name: "Test project column 2", 20 | }, 21 | } 22 | repo := New(github.NewMockClient( 23 | github.MockListProjectColumns(func(int) github.ProjectColumns { 24 | return github.ProjectColumnsFromSlice(expected) 25 | }), 26 | ), "example/test") 27 | got, err := github.ProjectColumnsToSlice(repo.ListProjectColumns(1)) 28 | assert.Nil(t, err) 29 | assert.Equal(t, got, expected) 30 | } 31 | 32 | func TestRepoGetProjectColumn(t *testing.T) { 33 | expected := &github.ProjectColumn{ 34 | ID: 1, 35 | Name: "Test project column 1", 36 | } 37 | repo := New(github.NewMockClient( 38 | github.MockGetProjectColumn(func(int) (*github.ProjectColumn, error) { 39 | return expected, nil 40 | }), 41 | ), "example/test") 42 | got, err := repo.GetProjectColumn(1) 43 | assert.Nil(t, err) 44 | assert.Equal(t, got, expected) 45 | } 46 | 47 | func TestRepoCreateProjectColumn(t *testing.T) { 48 | expected := &github.ProjectColumn{ 49 | ID: 1, 50 | Name: "Test project column 1", 51 | } 52 | repo := New(github.NewMockClient( 53 | github.MockCreateProjectColumn(func(int, string) (*github.ProjectColumn, error) { 54 | return expected, nil 55 | }), 56 | ), "example/test") 57 | got, err := repo.CreateProjectColumn(10, "Test project column 1") 58 | assert.Nil(t, err) 59 | assert.Equal(t, got, expected) 60 | } 61 | 62 | func TestRepoUpdateProjectColumn(t *testing.T) { 63 | expected := &github.ProjectColumn{ 64 | ID: 1, 65 | Name: "Test project column 1", 66 | } 67 | repo := New(github.NewMockClient( 68 | github.MockUpdateProjectColumn(func(int, string) (*github.ProjectColumn, error) { 69 | return expected, nil 70 | }), 71 | ), "example/test") 72 | got, err := repo.UpdateProjectColumn(1, "Test project column 1") 73 | assert.Nil(t, err) 74 | assert.Equal(t, got, expected) 75 | } 76 | -------------------------------------------------------------------------------- /repo/projects.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import "github.com/itchyny/github-migrator/github" 4 | 5 | // ListProjects lists the projects. 6 | func (r *Repo) ListProjects() github.Projects { 7 | return r.cli.ListProjects(r.path, &github.ListProjectsParams{ 8 | State: github.ListProjectsParamStateAll, 9 | }) 10 | } 11 | 12 | // GetProject gets the project. 13 | func (r *Repo) GetProject(projectID int) (*github.Project, error) { 14 | return r.cli.GetProject(projectID) 15 | } 16 | 17 | // CreateProject creates a project. 18 | func (r *Repo) CreateProject(params *github.CreateProjectParams) (*github.Project, error) { 19 | return r.cli.CreateProject(r.path, params) 20 | } 21 | 22 | // UpdateProject updates the project. 23 | func (r *Repo) UpdateProject(projectID int, params *github.UpdateProjectParams) (*github.Project, error) { 24 | return r.cli.UpdateProject(projectID, params) 25 | } 26 | 27 | // DeleteProject deletes the project. 28 | func (r *Repo) DeleteProject(projectID int) error { 29 | return r.cli.DeleteProject(projectID) 30 | } 31 | -------------------------------------------------------------------------------- /repo/projects_test.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | 8 | "github.com/itchyny/github-migrator/github" 9 | ) 10 | 11 | func TestRepoListProjects(t *testing.T) { 12 | expected := []*github.Project{ 13 | { 14 | ID: 10, 15 | Name: "Test project 1", 16 | }, 17 | { 18 | ID: 10, 19 | Name: "Test project 1", 20 | }, 21 | } 22 | repo := New(github.NewMockClient( 23 | github.MockListProjects(func(string, *github.ListProjectsParams) github.Projects { 24 | return github.ProjectsFromSlice(expected) 25 | }), 26 | ), "example/test") 27 | got, err := github.ProjectsToSlice(repo.ListProjects()) 28 | assert.Nil(t, err) 29 | assert.Equal(t, got, expected) 30 | } 31 | 32 | func TestRepoGetProject(t *testing.T) { 33 | expected := &github.Project{ 34 | ID: 1, 35 | Name: "Test project 1", 36 | } 37 | repo := New(github.NewMockClient( 38 | github.MockGetProject(func(projectID int) (*github.Project, error) { 39 | return expected, nil 40 | }), 41 | ), "example/test") 42 | got, err := repo.GetProject(1) 43 | assert.Nil(t, err) 44 | assert.Equal(t, got, expected) 45 | } 46 | 47 | func TestRepoCreateProject(t *testing.T) { 48 | expected := &github.Project{ 49 | ID: 1, 50 | Name: "Test project 1", 51 | Body: "Test body", 52 | State: github.ProjectStateClosed, 53 | } 54 | repo := New(github.NewMockClient( 55 | github.MockCreateProject(func(string, *github.CreateProjectParams) (*github.Project, error) { 56 | return expected, nil 57 | }), 58 | ), "example/test") 59 | got, err := repo.CreateProject(&github.CreateProjectParams{ 60 | Name: "Test project 1", 61 | Body: "Test body", 62 | }) 63 | assert.Nil(t, err) 64 | assert.Equal(t, got, expected) 65 | } 66 | 67 | func TestRepoUpdateProject(t *testing.T) { 68 | expected := &github.Project{ 69 | ID: 1, 70 | Name: "Test project 1", 71 | Body: "Test body", 72 | State: github.ProjectStateClosed, 73 | } 74 | repo := New(github.NewMockClient( 75 | github.MockUpdateProject(func(projectID int, params *github.UpdateProjectParams) (*github.Project, error) { 76 | return expected, nil 77 | }), 78 | ), "example/test") 79 | got, err := repo.UpdateProject(1, &github.UpdateProjectParams{ 80 | Name: "Test project 1", 81 | Body: "Test body", 82 | State: github.ProjectStateClosed, 83 | }) 84 | assert.Nil(t, err) 85 | assert.Equal(t, got, expected) 86 | } 87 | 88 | func TestRepoDeleteProject(t *testing.T) { 89 | repo := New(github.NewMockClient( 90 | github.MockDeleteProject(func(int) error { 91 | return nil 92 | }), 93 | ), "example/test") 94 | err := repo.DeleteProject(1) 95 | assert.Nil(t, err) 96 | } 97 | -------------------------------------------------------------------------------- /repo/pulls.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import "github.com/itchyny/github-migrator/github" 4 | 5 | // ListPullReqs lists the pull requests. 6 | func (r *Repo) ListPullReqs() github.PullReqs { 7 | return r.cli.ListPullReqs(r.path, &github.ListPullReqsParams{ 8 | State: github.ListPullReqsParamStateAll, 9 | Direction: github.ListPullReqsParamDirectionAsc, 10 | }) 11 | } 12 | 13 | // GetPullReq gets the pull request. 14 | func (r *Repo) GetPullReq(pullNumber int) (*github.PullReq, error) { 15 | return r.cli.GetPullReq(r.path, pullNumber) 16 | } 17 | -------------------------------------------------------------------------------- /repo/pulls_test.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | 8 | "github.com/itchyny/github-migrator/github" 9 | ) 10 | 11 | func TestRepoListPullReqs(t *testing.T) { 12 | expected := []*github.PullReq{ 13 | { 14 | Issue: github.Issue{ 15 | Number: 1, 16 | Title: "Example title 1", 17 | State: github.IssueStateClosed, 18 | Body: "Example body 1", 19 | HTMLURL: "http://localhost/example/test/pull/1", 20 | }, 21 | Merged: false, 22 | Draft: true, 23 | }, 24 | { 25 | Issue: github.Issue{ 26 | Number: 2, 27 | Title: "Example title 2", 28 | State: github.IssueStateOpen, 29 | Body: "Example body 2", 30 | HTMLURL: "http://localhost/example/test/pull/2", 31 | }, 32 | Merged: true, 33 | MergedBy: &github.User{ 34 | Login: "sample-user-1", 35 | }, 36 | Draft: false, 37 | }, 38 | } 39 | repo := New(github.NewMockClient( 40 | github.MockListPullReqs(func(string, *github.ListPullReqsParams) github.PullReqs { 41 | return github.PullReqsFromSlice(expected) 42 | }), 43 | ), "example/test") 44 | got, err := github.PullReqsToSlice(repo.ListPullReqs()) 45 | assert.Nil(t, err) 46 | assert.Equal(t, got, expected) 47 | } 48 | 49 | func TestRepoGetPullReq(t *testing.T) { 50 | expected := &github.PullReq{ 51 | Issue: github.Issue{ 52 | Number: 1, 53 | Title: "Example title 1", 54 | State: github.IssueStateClosed, 55 | Body: "Example body 1", 56 | HTMLURL: "http://localhost/example/test/pull/1", 57 | }, 58 | Merged: false, 59 | Draft: true, 60 | } 61 | repo := New(github.NewMockClient( 62 | github.MockGetPullReq(func(string, int) (*github.PullReq, error) { 63 | return expected, nil 64 | }), 65 | ), "example/test") 66 | got, err := repo.GetPullReq(1) 67 | assert.Nil(t, err) 68 | assert.Equal(t, got, expected) 69 | } 70 | -------------------------------------------------------------------------------- /repo/repo.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import "github.com/itchyny/github-migrator/github" 4 | 5 | // Repo represents a GitHub repository. 6 | type Repo struct { 7 | cli github.Client 8 | path string 9 | } 10 | 11 | // New creates a new Repo. 12 | func New(cli github.Client, path string) *Repo { 13 | return &Repo{cli: cli, path: path} 14 | } 15 | 16 | // NewPath creates a new Repo with the same client. 17 | func (r *Repo) NewPath(path string) *Repo { 18 | return New(r.cli, path) 19 | } 20 | -------------------------------------------------------------------------------- /repo/review_comments.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import "github.com/itchyny/github-migrator/github" 4 | 5 | // ListReviewComments lists the review comments. 6 | func (r *Repo) ListReviewComments(pullNumber int) github.ReviewComments { 7 | return r.cli.ListReviewComments(r.path, pullNumber) 8 | } 9 | -------------------------------------------------------------------------------- /repo/review_comments_test.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | 8 | "github.com/itchyny/github-migrator/github" 9 | ) 10 | 11 | func TestRepoListReviewComments(t *testing.T) { 12 | expected := []*github.ReviewComment{ 13 | { 14 | Body: "Example body 1", 15 | }, 16 | { 17 | Body: "Example body 2", 18 | }, 19 | } 20 | repo := New(github.NewMockClient( 21 | github.MockListReviewComments(func(_ string, pullNumber int) github.ReviewComments { 22 | assert.Equal(t, pullNumber, 1) 23 | return github.ReviewCommentsFromSlice(expected) 24 | }), 25 | ), "example/test") 26 | got, err := github.ReviewCommentsToSlice(repo.ListReviewComments(1)) 27 | assert.Nil(t, err) 28 | assert.Equal(t, got, expected) 29 | } 30 | -------------------------------------------------------------------------------- /repo/reviews.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import "github.com/itchyny/github-migrator/github" 4 | 5 | // ListReviews lists the reviews. 6 | func (r *Repo) ListReviews(pullNumber int) github.Reviews { 7 | return r.cli.ListReviews(r.path, pullNumber) 8 | } 9 | 10 | // GetReview lists the reviews. 11 | func (r *Repo) GetReview(pullNumber, reviewID int) (*github.Review, error) { 12 | return r.cli.GetReview(r.path, pullNumber, reviewID) 13 | } 14 | -------------------------------------------------------------------------------- /repo/reviews_test.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | 8 | "github.com/itchyny/github-migrator/github" 9 | ) 10 | 11 | func TestRepoListReviews(t *testing.T) { 12 | expected := []*github.Review{ 13 | { 14 | ID: 1, 15 | State: github.ReviewStateApproved, 16 | Body: "Example body 1", 17 | }, 18 | { 19 | ID: 2, 20 | State: github.ReviewStateChangesRequested, 21 | Body: "Example body 2", 22 | }, 23 | } 24 | repo := New(github.NewMockClient( 25 | github.MockListReviews(func(_ string, pullNumber int) github.Reviews { 26 | assert.Equal(t, pullNumber, 1) 27 | return github.ReviewsFromSlice(expected) 28 | }), 29 | ), "example/test") 30 | got, err := github.ReviewsToSlice(repo.ListReviews(1)) 31 | assert.Nil(t, err) 32 | assert.Equal(t, got, expected) 33 | } 34 | 35 | func TestRepoGetReview(t *testing.T) { 36 | expected := &github.Review{ 37 | ID: 1, 38 | State: github.ReviewStateApproved, 39 | Body: "Example body 1", 40 | } 41 | repo := New(github.NewMockClient( 42 | github.MockGetReview(func(string, int, int) (*github.Review, error) { 43 | return expected, nil 44 | }), 45 | ), "example/test") 46 | got, err := repo.GetReview(1, 2) 47 | assert.Nil(t, err) 48 | assert.Equal(t, got, expected) 49 | } 50 | -------------------------------------------------------------------------------- /repo/update.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import "github.com/itchyny/github-migrator/github" 4 | 5 | // Update the repository. 6 | func (r *Repo) Update(params *github.UpdateRepoParams) (*github.Repo, error) { 7 | return r.cli.UpdateRepo(r.path, params) 8 | } 9 | -------------------------------------------------------------------------------- /repo/update_test.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | 8 | "github.com/itchyny/github-migrator/github" 9 | ) 10 | 11 | func TestRepoUpdate(t *testing.T) { 12 | expected := &github.Repo{ 13 | Name: "test", 14 | FullName: "example/test", 15 | Description: "New description", 16 | HTMLURL: "http://localhost/example/test", 17 | Homepage: "http://localhost/new", 18 | Private: true, 19 | } 20 | repo := New(github.NewMockClient( 21 | github.MockGetRepo(func(string) (*github.Repo, error) { 22 | return &github.Repo{ 23 | Name: "test", 24 | FullName: "example/test", 25 | Description: "Test repository.", 26 | HTMLURL: "http://localhost/example/test", 27 | Homepage: "http://localhost/", 28 | Private: false, 29 | }, nil 30 | }), 31 | github.MockUpdateRepo(func(_ string, params *github.UpdateRepoParams) (*github.Repo, error) { 32 | assert.Equal(t, params.Name, "test") 33 | assert.Equal(t, params.Description, "New description") 34 | assert.Equal(t, params.Homepage, "http://localhost/new") 35 | assert.Equal(t, params.Private, true) 36 | return expected, nil 37 | }), 38 | ), "example/test") 39 | got, err := repo.Update(&github.UpdateRepoParams{ 40 | Name: "test", 41 | Description: "New description", 42 | Homepage: "http://localhost/new", 43 | Private: true, 44 | }) 45 | assert.Nil(t, err) 46 | assert.Equal(t, got, expected) 47 | } 48 | -------------------------------------------------------------------------------- /repo/user.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import "github.com/itchyny/github-migrator/github" 4 | 5 | // GetUser gets a user. 6 | func (r *Repo) GetUser(name string) (*github.User, error) { 7 | return r.cli.GetUser(name) 8 | } 9 | --------------------------------------------------------------------------------