From 3a8657e7702d5126e58d62923a76e105ebaeaeeb Mon Sep 17 00:00:00 2001 From: Riyyi Date: Sat, 20 Jul 2024 18:30:42 +0200 Subject: [PATCH] Add option to fetch and store issues --- main.go | 9 +++- src/api.go | 52 ++++--------------- src/issues.go | 135 +++++++++++++++++++++++++++++++++++++++++++++++++ src/request.go | 37 ++++++++++++++ src/secrets.go | 3 +- 5 files changed, 193 insertions(+), 43 deletions(-) create mode 100644 src/issues.go create mode 100644 src/request.go diff --git a/main.go b/main.go index 291013f..88eff61 100644 --- a/main.go +++ b/main.go @@ -25,7 +25,9 @@ import ( type Args struct { Decl string `arg:"-d,--decl" help:"Generate travel declaration table" placeholder:"MONTH"` Process bool `arg:"-p,--process" help:"Process specified file and call Jira API"` - File string `arg:"positional,required" help:"the worklog file to process"` + Issues bool `arg:"-i,--issues" help:"Store issues in specified file"` + // ------------------------------------- + File string `arg:"positional,required" help:"the file to perform the action on"` } func (Args) Description() string { @@ -69,4 +71,9 @@ func main() { src.File.Parse(args.File, job, false) fmt.Println(decl.Result()) } + + if args.Issues { + var issues src.IssueData = src.MakeIssueData() + issues.GenerateIssuesFile(args.File) + } } diff --git a/src/api.go b/src/api.go index fc1a53f..e44a6f0 100644 --- a/src/api.go +++ b/src/api.go @@ -7,12 +7,7 @@ package src import ( - "bytes" - "encoding/base64" - "encoding/json" "fmt" - "io" - "net/http" "strconv" "time" ) @@ -21,17 +16,17 @@ var Api api type api struct {} -func (api) CallApi(date string, from_time string, to_time string, item_id string, description string) error { - if item_id == "break" || item_id == "lunch" || item_id == "pauze" { return nil } +func (api) CallApi(date string, fromTime string, toTime string, itemID string, description string) error { + if itemID == "break" || itemID == "lunch" || itemID == "pauze" { return nil } - if date == "" || from_time == "" || to_time == "" || item_id == "" { - return fmt.Errorf("incomplete log entry: %s, %s-%s, %s, %s", date, from_time, to_time, item_id, description) + if date == "" || fromTime == "" || toTime == "" || itemID == "" { + return fmt.Errorf("incomplete log entry: %s, %s-%s, %s, %s", date, fromTime, toTime, itemID, description) } - time1, err := time.Parse("15:04", from_time) + time1, err := time.Parse("15:04", fromTime) if err != nil { return fmt.Errorf("error parsing from_time: %s", err) } - time2, err := time.Parse("15:04", to_time) + time2, err := time.Parse("15:04", toTime) if err != nil { return fmt.Errorf("error parsing to_time: %s", err) } // Convert local timezone to UTC time @@ -41,42 +36,17 @@ func (api) CallApi(date string, from_time string, to_time string, item_id string duration := time2.Sub(time1) seconds := int(duration.Seconds()) - if seconds < 0 { return fmt.Errorf("from_time is later than to_time: %s > %s", from_time, to_time) } + if seconds < 0 { return fmt.Errorf("from_time is later than to_time: %s > %s", fromTime, toTime) } - var url string = base_url + "/rest/api/2/issue/" + item_id + "/worklog" + var url string = baseUrl + "/rest/api/2/issue/" + itemID + "/worklog" data := map[string]string{ "comment": description, - "started": fmt.Sprintf("%sT%s:00.000+0000", date, from_time), // "2021-01-17T12:34:00.000+0000", + "started": fmt.Sprintf("%sT%s:00.000+0000", date, fromTime), // "2021-01-17T12:34:00.000+0000", "timeSpentSeconds": strconv.Itoa(seconds), } - json_data, err := json.Marshal(data) - if err != nil { - return fmt.Errorf("error marshaling JSON: %s", err) - } - - req, err := http.NewRequest("POST", url, bytes.NewBuffer(json_data)) - if err != nil { - return fmt.Errorf("error creating request: %s", err) - } - - auth := username + ":" + password - authHeader := "Basic " + base64.StdEncoding.EncodeToString([]byte(auth)) - req.Header.Set("Authorization", authHeader) - req.Header.Set("Content-Type", "application/json") - - client := &http.Client{} - resp, err := client.Do(req) - if err != nil { return fmt.Errorf("error making request: %s", err) } - defer resp.Body.Close() - - body, err := io.ReadAll(resp.Body) - if err != nil { return fmt.Errorf("error reading response body: %s", err) } - - if resp.Status != "201 Created" { - return fmt.Errorf("invalid Jira request:\n%s", string(body)) - } + _, err = Request(url, data, 201) // "Created" - return nil + return err } diff --git a/src/issues.go b/src/issues.go new file mode 100644 index 0000000..9dd5061 --- /dev/null +++ b/src/issues.go @@ -0,0 +1,135 @@ +package src + +import ( + "bufio" + "encoding/json" + "fmt" + "os" + "path/filepath" +) + +type IssueData struct { + totalIssues []issueResponse +} + +// Constructor +func MakeIssueData() IssueData { + return IssueData {} +} + +func (self *IssueData) GenerateIssuesFile(path string) { + // Safety check + verify(filepath.Base(path) != "worklog.org", "protected from overwriting worklog file!") + + // Get all issues of the active sprint, and its subtasks + self.fetchIssues(0) + + // Store the issues + outputFile, err := os.Create(path) + assert(err) + defer outputFile.Close() + writer := bufio.NewWriter(outputFile) + defer writer.Flush() + self.writeIssues(writer) + + // Clear the results + self.totalIssues = self.totalIssues[:0] +} + +// ----------------------------------------- + +type issuesResponse struct { + StartAt int `json:"startAt"` + MaxResults int `json:"maxResults"` + Total int `json:"total"` + Issues []issueResponse `json:"issues"` +} + +type issueResponse struct { + Key string `json:"key"` + Fields fieldsResponse `json:"fields"` +} + +type fieldsResponse struct { + Summary string `json:"summary"` + SubTasks []subTaskResponse `json:"subtasks"` +} + +type subTaskResponse struct { + Key string `json:"key"` + Fields subTaskFieldsResponse `json:"fields"` +} + +type subTaskFieldsResponse struct { + Summary string `json:"summary"` +} + +// ----------------------------------------- + +func (self *IssueData) fetchIssues(startAt int) error { + var url string = baseUrl + "/rest/api/2/search" + var maxResults int = 100 + data := map[string]interface{}{ + "fields": []string{ "key", "summary", "subtasks" }, + "jql": `project = "` + projectName + `" AND sprint IN openSprints() AND issuetype != "Sub-task" ORDER BY created ASC`, + "maxResults": maxResults, + "startAt": startAt, + } + + body, err := Request(url, data, 200) // "OK" + if err != nil { return err } + + var result issuesResponse + err = json.Unmarshal(body, &result) + if err != nil { fmt.Println("NOPE!"); return err } + + // Add fetched issues + self.totalIssues = append(self.totalIssues, result.Issues...) + + // Pagination, if more results + if startAt + maxResults < result.Total { + self.fetchIssues(startAt + maxResults) + } + + return nil +} + +func (self* IssueData) writeIssues(writer *bufio.Writer) { + // Issues + writer.WriteString(".\n") + var count int = len(self.totalIssues) + for i, issue := range(self.totalIssues) { + if i == count - 1 { + writer.WriteString("└") + } else { + writer.WriteString("├") + } + writer.WriteString("── ") + writer.WriteString(issue.Key) + writer.WriteString(" ") + writer.WriteString(issue.Fields.Summary) + writer.WriteString("\n") + + // Subtasks + var subtaskCount int = len(issue.Fields.SubTasks) + for j, subtask := range(issue.Fields.SubTasks) { + // Last issue + if i == count - 1 { + writer.WriteString(" ") + } else { + writer.WriteString("│ ") + } + // Last subtask + if j == subtaskCount - 1 { + writer.WriteString("└") + } else { + writer.WriteString("├") + } + writer.WriteString("── ") + writer.WriteString(subtask.Key) + writer.WriteString(" ") + writer.WriteString(subtask.Fields.Summary) + writer.WriteString("\n") + } + } +} diff --git a/src/request.go b/src/request.go new file mode 100644 index 0000000..bb37df9 --- /dev/null +++ b/src/request.go @@ -0,0 +1,37 @@ +package src + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "net/http" +) + +func Request[T ~map[string]string | ~map[string]interface{}](url string, data T, status int) ([]byte, error) { + jsonData, err := json.Marshal(data) + if err != nil { return nil, fmt.Errorf("error marshaling JSON: %s", err) } + + req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + if err != nil { return nil, fmt.Errorf("error creating request: %s", err) } + + auth := username + ":" + password + authHeader := "Basic " + base64.StdEncoding.EncodeToString([]byte(auth)) + req.Header.Set("Authorization", authHeader) + req.Header.Set("Content-Type", "application/json") + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { return nil, fmt.Errorf("error making request: %s", err) } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { return nil, fmt.Errorf("error reading response body: %s", err) } + + if resp.StatusCode != status { + return nil, fmt.Errorf("invalid Jira request:\n%s", string(body)) + } + + return body, nil +} diff --git a/src/secrets.go b/src/secrets.go index e0d84ce..9879291 100644 --- a/src/secrets.go +++ b/src/secrets.go @@ -9,4 +9,5 @@ package src var username string = "" // email var password string = "" // API key -var base_url string = "" // base URL +var baseUrl string = "" +var projectName string = ""