From 3b6f405c505f7575b0b3d60fbfd596d80cec0a2d Mon Sep 17 00:00:00 2001
From: devfxx <54813298+devfxx@users.noreply.github.com>
Date: Fri, 24 Jan 2025 13:09:11 +0200
Subject: [PATCH] init
---
go.mod | 10 ++
go.sum | 8 ++
main.go | 330 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 348 insertions(+)
create mode 100644 go.mod
create mode 100644 go.sum
create mode 100644 main.go
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..681eae1
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,10 @@
+module issue_cli
+
+go 1.23.4
+
+require (
+ github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
+ github.com/russross/blackfriday/v2 v2.1.0 // indirect
+ github.com/urfave/cli/v2 v2.27.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..5f6b6d2
--- /dev/null
+++ b/main.go
@@ -0,0 +1,330 @@
+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"`
+}
+
+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")
+ }
+
+ // Extract owner, repo, and issue number from the 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]
+
+ // Fetch the issue data from the GitHub API
+ issue, err := getIssue(owner, repo, issueNumber)
+ if err != nil {
+ return err
+ }
+
+ // Fetch the comments for the issue
+ comments, err := getComments(owner, repo, issueNumber)
+ if err != nil {
+ return err
+ }
+
+ // Convert the issue data to the specified format
+ format := c.String("format")
+ var output string
+ if format == "html" {
+ output = convertToHTML(issue, comments)
+ } else {
+ output = convertToMarkdown(issue, comments)
+ }
+
+ // Check if output file path is provided
+ outputFile := c.String("output")
+ if outputFile != "" {
+ // Write the output to the specified file
+ 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 {
+ // Print the output to the console
+ 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 {
+ // Use blackfriday to convert markdown to HTML
+ html := blackfriday.Run([]byte(md))
+
+ // Custom handling for images
+ re := regexp.MustCompile(`!\[Image\]\((https://github.com/user-attachments/assets/[^\)]+)\)`)
+ html = re.ReplaceAll(html, []byte(``))
+
+ // Remove leading and trailing
tags and replace
tags with spaces
+ re = regexp.MustCompile(`(?m)^
(.*?)
$`) + html = re.ReplaceAll(html, []byte("$1")) + html = bytes.ReplaceAll(html, []byte("