Add option to fetch and store issues
This commit is contained in:
@@ -25,7 +25,9 @@ import (
|
|||||||
type Args struct {
|
type Args struct {
|
||||||
Decl string `arg:"-d,--decl" help:"Generate travel declaration table" placeholder:"MONTH"`
|
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"`
|
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 {
|
func (Args) Description() string {
|
||||||
@@ -69,4 +71,9 @@ func main() {
|
|||||||
src.File.Parse(args.File, job, false)
|
src.File.Parse(args.File, job, false)
|
||||||
fmt.Println(decl.Result())
|
fmt.Println(decl.Result())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if args.Issues {
|
||||||
|
var issues src.IssueData = src.MakeIssueData()
|
||||||
|
issues.GenerateIssuesFile(args.File)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-42
@@ -7,12 +7,7 @@
|
|||||||
package src
|
package src
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"encoding/base64"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -21,17 +16,17 @@ var Api api
|
|||||||
|
|
||||||
type api struct {}
|
type api struct {}
|
||||||
|
|
||||||
func (api) CallApi(date string, from_time string, to_time string, item_id string, description string) error {
|
func (api) CallApi(date string, fromTime string, toTime string, itemID string, description string) error {
|
||||||
if item_id == "break" || item_id == "lunch" || item_id == "pauze" { return nil }
|
if itemID == "break" || itemID == "lunch" || itemID == "pauze" { return nil }
|
||||||
|
|
||||||
if date == "" || from_time == "" || to_time == "" || item_id == "" {
|
if date == "" || fromTime == "" || toTime == "" || itemID == "" {
|
||||||
return fmt.Errorf("incomplete log entry: %s, %s-%s, %s, %s", date, from_time, to_time, item_id, description)
|
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) }
|
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) }
|
if err != nil { return fmt.Errorf("error parsing to_time: %s", err) }
|
||||||
|
|
||||||
// Convert local timezone to UTC time
|
// 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)
|
duration := time2.Sub(time1)
|
||||||
seconds := int(duration.Seconds())
|
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{
|
data := map[string]string{
|
||||||
"comment": description,
|
"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),
|
"timeSpentSeconds": strconv.Itoa(seconds),
|
||||||
}
|
}
|
||||||
|
|
||||||
json_data, err := json.Marshal(data)
|
_, err = Request(url, data, 201) // "Created"
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("error marshaling JSON: %s", err)
|
return 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))
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
+135
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
+2
-1
@@ -9,4 +9,5 @@ package src
|
|||||||
var username string = "" // email
|
var username string = "" // email
|
||||||
var password string = "" // API key
|
var password string = "" // API key
|
||||||
|
|
||||||
var base_url string = "" // base URL
|
var baseUrl string = ""
|
||||||
|
var projectName string = ""
|
||||||
|
|||||||
Reference in New Issue
Block a user