Created
October 3, 2025 21:07
-
-
Save iljavs/17c51132de9ccf8ea10cbfba11ed38c4 to your computer and use it in GitHub Desktop.
Grab all open issues from a github repo and save them to disk locally.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package main | |
| import ( | |
| "context" | |
| "flag" | |
| "fmt" | |
| "os" | |
| "path/filepath" | |
| "regexp" | |
| "strings" | |
| "github.com/google/go-github/v57/github" | |
| "golang.org/x/oauth2" | |
| ) | |
| type config struct { | |
| token string | |
| owner string | |
| repo string | |
| outputDir string | |
| labels []string | |
| } | |
| func main() { | |
| cfg := parseFlags() | |
| if err := os.MkdirAll(cfg.outputDir, 0755); err != nil { | |
| fmt.Printf("Error creating output directory: %v\n", err) | |
| os.Exit(1) | |
| } | |
| client := createGitHubClient(cfg.token) | |
| issues, err := fetchOpenIssues(client, cfg.owner, cfg.repo, cfg.labels) | |
| if err != nil { | |
| fmt.Printf("Error fetching issues: %v\n", err) | |
| os.Exit(1) | |
| } | |
| fmt.Printf("Found %d issues\n", len(issues)) | |
| printIssueTitles(issues) | |
| savedCount := saveIssuesToFiles(issues, cfg.outputDir) | |
| fmt.Printf("\nSuccessfully exported %d issues to %s\n", | |
| savedCount, cfg.outputDir) | |
| } | |
| func parseFlags() config { | |
| token := flag.String("token", "", "GitHub Personal Access Token (required)") | |
| owner := flag.String("owner", "", "Repository owner (required)") | |
| repo := flag.String("repo", "", "Repository name (required)") | |
| labels := flag.String("labels", "", | |
| "Comma-separated list of labels to filter issues (optional)") | |
| outputDir := flag.String("output", "issues", | |
| "Output directory for markdown files") | |
| flag.Parse() | |
| if *token == "" || *owner == "" || *repo == "" { | |
| fmt.Println("Error: -token, -owner, and -repo flags are required") | |
| flag.Usage() | |
| os.Exit(1) | |
| } | |
| return config{ | |
| token: *token, | |
| owner: *owner, | |
| repo: *repo, | |
| outputDir: *outputDir, | |
| labels: strings.Split(*labels, ","), | |
| } | |
| } | |
| func createGitHubClient(token string) *github.Client { | |
| ctx := context.Background() | |
| ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token}) | |
| tc := oauth2.NewClient(ctx, ts) | |
| return github.NewClient(tc) | |
| } | |
| func fetchOpenIssues( | |
| client *github.Client, | |
| owner, repo string, | |
| labels []string) ([]*github.Issue, error) { | |
| ctx := context.Background() | |
| opt := &github.IssueListByRepoOptions{ | |
| State: "open", | |
| Labels: labels, | |
| ListOptions: github.ListOptions{ | |
| PerPage: 100, | |
| }, | |
| } | |
| var allIssues []*github.Issue | |
| for { | |
| issues, resp, err := client.Issues.ListByRepo(ctx, owner, repo, opt) | |
| if err != nil { | |
| return nil, err | |
| } | |
| allIssues = append(allIssues, issues...) | |
| if resp.NextPage == 0 { | |
| break | |
| } | |
| opt.Page = resp.NextPage | |
| } | |
| return allIssues, nil | |
| } | |
| func printIssueTitles(issues []*github.Issue) { | |
| fmt.Println("\nOpen Issues:") | |
| fmt.Println("------------") | |
| i := 1 | |
| for _, issue := range issues { | |
| if !issue.IsPullRequest() { | |
| fmt.Printf("%d: %s\n", i, *issue.Title) | |
| i++ | |
| } | |
| } | |
| fmt.Println() | |
| } | |
| func saveIssuesToFiles(issues []*github.Issue, outputDir string) int { | |
| savedCount := 0 | |
| for _, issue := range issues { | |
| if issue.IsPullRequest() { | |
| continue | |
| } | |
| filename := sanitizeFilename(fmt.Sprintf("%s.md", *issue.Title)) | |
| filepath := filepath.Join(outputDir, filename) | |
| content := "" | |
| if issue.Body != nil { | |
| content = *issue.Body | |
| } | |
| if err := os.WriteFile(filepath, []byte(content), 0644); err != nil { | |
| fmt.Printf("Error writing issue #%d: %v\n", *issue.Number, err) | |
| continue | |
| } | |
| fmt.Printf("Saved issue #%d: %s\n", *issue.Number, *issue.Title) | |
| savedCount++ | |
| } | |
| return savedCount | |
| } | |
| func sanitizeFilename(filename string) string { | |
| sanitized := strings.ReplaceAll(filename, "`", "") | |
| sanitized = strings.ReplaceAll(sanitized, "'", "") | |
| reg := regexp.MustCompile(`[<>:"/\\|?*]`) | |
| sanitized = reg.ReplaceAllString(sanitized, "-") | |
| sanitized = strings.ReplaceAll(sanitized, " ", "_") | |
| sanitized = strings.ReplaceAll(sanitized, "→", "-") | |
| sanitized = strings.ReplaceAll(sanitized, "->", "-") | |
| asciiOnly := strings.Map( | |
| func(r rune) rune { | |
| if r > 127 { | |
| return -1 | |
| } | |
| return r | |
| }, | |
| sanitized) | |
| if len(asciiOnly) > 200 { | |
| asciiOnly = asciiOnly[:200] | |
| } | |
| if !strings.HasSuffix(asciiOnly, ".md") { | |
| asciiOnly += ".md" | |
| } | |
| return asciiOnly | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment