Compare commits

...

3 Commits

  1. 36
      README.org
  2. 9
      main.go
  3. 52
      src/api.go
  4. 8
      src/file.go
  5. 135
      src/issues.go
  6. 37
      src/request.go
  7. 3
      src/secrets.go

36
README.org

@ -1,21 +1,45 @@
* Worklog #+TITLE: Worklog
#+AUTHOR: Riyyi
#+LANGUAGE: en
#+OPTIONS: toc:nil
Register worklog entries to the Jira API. Register worklog entries to the Jira API.
* Download ** Getting started
** Clone *** Clone
#+BEGIN_SRC sh #+BEGIN_SRC sh
$ git clone https://github.com/riyyi/worklog $ git clone https://github.com/riyyi/worklog
#+END_SRC #+END_SRC
* Build instructions *** Build instructions
#+BEGIN_SRC sh #+BEGIN_SRC sh
$ go build $ go build
#+END_SRC #+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

9
main.go

@ -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)
}
} }

52
src/api.go

@ -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)
}
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
} }

8
src/file.go

@ -38,7 +38,7 @@ func (file) Parse(path string, job func(line string, line_number int) string, ov
line_number++ line_number++
// Write line to output_file // Write line to output_file
if writer != nil { if overwrite && writer != nil {
_, err := writer.WriteString(line + "\n") _, err := writer.WriteString(line + "\n")
assert(err) assert(err)
} }
@ -50,6 +50,8 @@ func (file) Parse(path string, job func(line string, line_number int) string, ov
err = scanner.Err() err = scanner.Err()
assert(err) assert(err)
err = os.Rename(path + ".tmp", path) if overwrite {
assert(err) err = os.Rename(path + ".tmp", path)
assert(err)
}
} }

135
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")
}
}
}

37
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
}

3
src/secrets.go

@ -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 = ""

Loading…
Cancel
Save