commit e356cc6c4eab758e60e16b7f83c4406b4be5048c Author: jolts Date: Fri Jan 24 13:17:55 2025 +0200 Cleared History diff --git a/README.md b/README.md new file mode 100644 index 0000000..e0bc275 --- /dev/null +++ b/README.md @@ -0,0 +1,76 @@ +# GitHub Issue Downloader + +This project is a command-line tool that fetches GitHub issue data and reconstructs it in Markdown or HTML format. It includes features such as rendering Markdown in comments, displaying user avatars, and a dark mode toggle in HTML output. + +## Features + +- Fetch GitHub issue and comments +- Render Markdown in issues and comments +- Display user avatars +- Dark mode toggle for HTML output + +## Prerequisites + +- Go 1.23 or later +- Internet connection to access the GitHub API + +## Installation + +1. Clone the repository: + + ```sh + git clone https://f0rk.systems/reso/tools_issue_cli.git + ``` + +2. Change to the project directory: + + ```sh + cd tools_issue_cli§ + ``` + +3. Build the project: + ```sh + go build -o tools_issue_cli + ``` + +## Usage + +### Fetching and Outputting an Issue + +You can use the tool to fetch a GitHub issue and output it in either Markdown or HTML format. + +#### Fetch and Output as Markdown + +To fetch an issue and output it as Markdown: + +```sh +./tools_issue_cli fetch --url https://github.com/owner/repo/issues/1 --output issue.md --format markdown +``` + +#### Fetch and Output as HTML + +To fetch an issue and output it as HTML: + +```sh +./tools_issue_cli fetch --url https://github.com/owner/repo/issues/1 --output issue.html --format html +``` + +### Command-Line Options + +- `--url`: The URL of the GitHub issue to fetch. +- `--output`: The file path to write the output (optional). If not provided, the output is printed to the console. +- `--format`: The output format (`markdown` or `html`). Defaults to `markdown`. + +## Example + +### Fetch an Issue and Output as Markdown + +```sh +./tools_issue_cli fetch --url https://github.com/dnnyxyz/dnnyxyz.github.io/issues/1 --output issue.md --format markdown +``` + +### Fetch an Issue and Output as HTML + +```sh +./tools_issue_cli fetch --url https://github.com/dnnyxyz/dnnyxyz.github.io/issues/1 --output issue.html --format html +``` diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..c618e99 --- /dev/null +++ b/go.mod @@ -0,0 +1,13 @@ +module issue_cli + +go 1.23.4 + +require ( + github.com/russross/blackfriday/v2 v2.1.0 + github.com/urfave/cli/v2 v2.27.5 +) + +require ( + github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect + github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..c8b6c7e --- /dev/null +++ b/go.sum @@ -0,0 +1,8 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= +github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w= +github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..fa7bf54 --- /dev/null +++ b/main.go @@ -0,0 +1,326 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "os" + "regexp" + "strings" + "time" + + "github.com/russross/blackfriday/v2" + "github.com/urfave/cli/v2" +) + +type Issue struct { + Title string `json:"title"` + Body string `json:"body"` + User User `json:"user"` + Number int `json:"number"` + State string `json:"state"` + Comments int `json:"comments"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` +} + +type User struct { + Login string `json:"login"` + AvatarURL string `json:"avatar_url"` +} + +type Comment struct { + Body string `json:"body"` + User User `json:"user"` + CreatedAt string `json:"created_at"` +} + +func main() { + app := &cli.App{ + Name: "github-issue-cli", + Usage: "Fetches GitHub issue data and reconstructs it in Markdown or HTML format", + Commands: []*cli.Command{ + { + Name: "fetch", + Usage: "Fetch a GitHub issue", + Action: fetchIssue, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "url", + Usage: "GitHub issue URL", + Required: true, + }, + &cli.StringFlag{ + Name: "output", + Usage: "Output file to write the markdown or html", + }, + &cli.StringFlag{ + Name: "format", + Usage: "Output format (markdown or html)", + Value: "markdown", + }, + }, + }, + }, + } + + if err := app.Run(os.Args); err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) + } +} + +func fetchIssue(c *cli.Context) error { + url := c.String("url") + if !strings.Contains(url, "github.com") || !strings.Contains(url, "/issues/") { + return fmt.Errorf("invalid GitHub issue URL") + } + + parts := strings.Split(url, "/") + if len(parts) < 5 { + return fmt.Errorf("invalid GitHub issue URL") + } + owner := parts[3] + repo := parts[4] + issueNumber := parts[len(parts)-1] + + issue, err := getIssue(owner, repo, issueNumber) + if err != nil { + return err + } + + comments, err := getComments(owner, repo, issueNumber) + if err != nil { + return err + } + + format := c.String("format") + var output string + if format == "html" { + output = convertToHTML(issue, comments) + } else { + output = convertToMarkdown(issue, comments) + } + + outputFile := c.String("output") + if outputFile != "" { + if err := os.WriteFile(outputFile, []byte(output), 0644); err != nil { + return fmt.Errorf("failed to write to file: %v", err) + } + fmt.Printf("Output written to %s\n", outputFile) + } else { + fmt.Println(output) + } + + return nil +} + +func getIssue(owner, repo, issueNumber string) (Issue, error) { + apiURL := fmt.Sprintf("https://api.github.com/repos/%s/%s/issues/%s", owner, repo, issueNumber) + resp, err := http.Get(apiURL) + if err != nil { + return Issue{}, fmt.Errorf("failed to fetch issue: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return Issue{}, fmt.Errorf("failed to fetch issue: status code %d", resp.StatusCode) + } + + var issue Issue + if err := json.NewDecoder(resp.Body).Decode(&issue); err != nil { + return Issue{}, fmt.Errorf("failed to decode JSON response: %v", err) + } + + return issue, nil +} + +func getComments(owner, repo, issueNumber string) ([]Comment, error) { + apiURL := fmt.Sprintf("https://api.github.com/repos/%s/%s/issues/%s/comments", owner, repo, issueNumber) + resp, err := http.Get(apiURL) + if err != nil { + return nil, fmt.Errorf("failed to fetch comments: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("failed to fetch comments: status code %d", resp.StatusCode) + } + + var comments []Comment + if err := json.NewDecoder(resp.Body).Decode(&comments); err != nil { + return nil, fmt.Errorf("failed to decode JSON response: %v", err) + } + + return comments, nil +} + +func formatDate(isoDate string) (string, error) { + parsedTime, err := time.Parse(time.RFC3339, isoDate) + if err != nil { + return "", err + } + return parsedTime.Format("January 2, 2006 15:04 MST"), nil +} + +func parseMarkdown(md string) string { + html := blackfriday.Run([]byte(md)) + + re := regexp.MustCompile(`!\[Image\]\((https://github.com/user-attachments/assets/[^\)]+)\)`) + html = re.ReplaceAll(html, []byte(`Image`)) + + re = regexp.MustCompile(`(?m)^

(.*?)

$`) + html = re.ReplaceAll(html, []byte("$1")) + html = bytes.ReplaceAll(html, []byte("
"), []byte(" ")) + + return string(html) +} + +func convertToMarkdown(issue Issue, comments []Comment) string { + var sb strings.Builder + + createdAt, _ := formatDate(issue.CreatedAt) + updatedAt, _ := formatDate(issue.UpdatedAt) + + sb.WriteString(fmt.Sprintf(`# %s + +**Issue Number:** %d +**State:** %s +**Created By:** %s +**Created At:** %s +**Updated At:** %s +**Comments:** %d + +--- + +%s +`, issue.Title, issue.Number, issue.State, issue.User.Login, createdAt, updatedAt, issue.Comments, issue.Body)) + + if len(comments) > 0 { + sb.WriteString("\n## Comments:\n") + for _, comment := range comments { + commentCreatedAt, _ := formatDate(comment.CreatedAt) + sb.WriteString(fmt.Sprintf("\n**%s** commented on %s:\n\n%s\n", comment.User.Login, commentCreatedAt, comment.Body)) + } + } + + return sb.String() +} + +func convertToHTML(issue Issue, comments []Comment) string { + var sb strings.Builder + + createdAt, _ := formatDate(issue.CreatedAt) + updatedAt, _ := formatDate(issue.UpdatedAt) + + sb.WriteString(``) + + sb.WriteString(` +`) + + sb.WriteString(fmt.Sprintf(`
+
+

%s

+

Avatar%s | Issue Number: %d | State: %s | Created At: %s | Updated At: %s | Comments: %d

+
+
+ %s +
+
`, issue.Title, issue.User.AvatarURL, issue.User.Login, issue.Number, issue.State, createdAt, updatedAt, issue.Comments, parseMarkdown(issue.Body))) + + if len(comments) > 0 { + sb.WriteString("

Comments:

") + for _, comment := range comments { + commentCreatedAt, _ := formatDate(comment.CreatedAt) + sb.WriteString(fmt.Sprintf(`
+
Avatar%s commented on %s:
+
%s
+
`, comment.User.AvatarURL, comment.User.Login, commentCreatedAt, parseMarkdown(comment.Body))) + } + } + + return sb.String() +}