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.
* 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

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

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

8
src/file.go

@ -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
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 password string = "" // API key
var base_url string = "" // base URL
var baseUrl string = ""
var projectName string = ""

Loading…
Cancel
Save