Compare commits
4
Commits
cd3ca50a22
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3f6638fc85 | ||
|
|
f02cbaebf7 | ||
|
|
3babed3def | ||
|
|
3a8657e770 |
+30
-6
@@ -1,21 +1,45 @@
|
||||
* Worklog
|
||||
#+TITLE: Worklog
|
||||
#+AUTHOR: Riyyi
|
||||
#+LANGUAGE: en
|
||||
#+OPTIONS: toc:nil
|
||||
|
||||
Register worklog entries to the Jira API.
|
||||
|
||||
* Download
|
||||
** Getting started
|
||||
|
||||
** Clone
|
||||
*** Clone
|
||||
|
||||
#+BEGIN_SRC sh
|
||||
$ git clone https://github.com/riyyi/worklog
|
||||
#+END_SRC
|
||||
|
||||
* Build instructions
|
||||
*** Build instructions
|
||||
|
||||
#+BEGIN_SRC sh
|
||||
$ go build
|
||||
#+END_SRC
|
||||
|
||||
* Gitignore
|
||||
*** Usage
|
||||
|
||||
git update-index --assume-unchanged src/secrets.go
|
||||
#+BEGIN_SRC sh
|
||||
$ worklog --help
|
||||
worklog - process a worklog file
|
||||
|
||||
Usage: worklog [--decl MONTH] [--process] [--issues] FILE
|
||||
|
||||
Positional arguments:
|
||||
FILE the file to perform the action on
|
||||
|
||||
Options:
|
||||
--decl MONTH, -d MONTH
|
||||
Generate travel declaration table
|
||||
--process, -p Process specified file and call Jira API
|
||||
--issues, -i Store issues in specified file
|
||||
--help, -h display this help and exit
|
||||
#+END_SRC
|
||||
|
||||
** Gitignore
|
||||
|
||||
#+BEGIN_SRC sh
|
||||
$ git update-index --assume-unchanged src/secrets.go
|
||||
#+END_SRC
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
+22
-44
@@ -7,12 +7,7 @@
|
||||
package src
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
@@ -21,62 +16,45 @@ 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" || itemID == "T1-break" || itemID == "T1-lunch" || itemID == "T1-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)
|
||||
const layout = "2006-01-02 15:04:05"
|
||||
timestamp1 := date + " " + fromTime + ":00"
|
||||
timestamp2 := date + " " + toTime + ":00"
|
||||
|
||||
location, err := time.LoadLocation("Local")
|
||||
if err != nil {
|
||||
return fmt.Errorf("error loading location: %s", err)
|
||||
}
|
||||
|
||||
time1, err := time.ParseInLocation(layout, timestamp1, location)
|
||||
if err != nil { return fmt.Errorf("error parsing from_time: %s", err) }
|
||||
|
||||
time2, err := time.Parse("15:04", to_time)
|
||||
time2, err := time.ParseInLocation(layout, timestamp2, location)
|
||||
if err != nil { return fmt.Errorf("error parsing to_time: %s", err) }
|
||||
|
||||
// Convert local timezone to UTC time
|
||||
_, offset := time.Now().Zone()
|
||||
time1.Add(-time.Duration(offset) * time.Second);
|
||||
time2.Add(-time.Duration(offset) * time.Second);
|
||||
time1UTC := time1.UTC()
|
||||
|
||||
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("%s:00.000+0000",
|
||||
time1UTC.Format("2006-01-02T15:04")), // "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)
|
||||
}
|
||||
_, err = Request(url, data, 201) // "Created"
|
||||
|
||||
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
|
||||
return err
|
||||
}
|
||||
|
||||
+5
-3
@@ -38,7 +38,7 @@ func (file) Parse(path string, job func(line string, line_number int) string, ov
|
||||
line_number++
|
||||
|
||||
// Write line to output_file
|
||||
if writer != nil {
|
||||
if overwrite && writer != nil {
|
||||
_, err := writer.WriteString(line + "\n")
|
||||
assert(err)
|
||||
}
|
||||
@@ -50,6 +50,8 @@ func (file) Parse(path string, job func(line string, line_number int) string, ov
|
||||
err = scanner.Err()
|
||||
assert(err)
|
||||
|
||||
err = os.Rename(path + ".tmp", path)
|
||||
assert(err)
|
||||
if overwrite {
|
||||
err = os.Rename(path + ".tmp", path)
|
||||
assert(err)
|
||||
}
|
||||
}
|
||||
|
||||
+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")
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -92,7 +92,7 @@ func (self *Process) parseTask(line string, line_number int) error {
|
||||
if err != nil { return err }
|
||||
|
||||
// Set last_time, last_item_id, description
|
||||
if data[3] == "X" {
|
||||
if data[3] == "X" || data[3] == "x" {
|
||||
date.last_time = data[1]
|
||||
date.last_item_id = data[2]
|
||||
date.last_description = data[4]
|
||||
|
||||
@@ -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 password string = "" // API key
|
||||
|
||||
var base_url string = "" // base URL
|
||||
var baseUrl string = ""
|
||||
var projectName string = ""
|
||||
|
||||
Reference in New Issue
Block a user