From 3ed3c53854c49d65ae039222f69345e6a6baa9e6 Mon Sep 17 00:00:00 2001 From: Yves Biener Date: Sat, 21 Oct 2023 11:03:43 +0200 Subject: [PATCH] add(taskwarrior): integrate uda for git integration This implementation adds the necessary fields for the tasks of taskwarrior to be synchronized with git. It provides the necessary conversion of the taskwarrior times and the standard time.Time go format. WIP for #3. --- cmd/gitw/main.go | 56 +---------------- internal/gitw/project.go | 106 +++++++++++++++++++++++++++++++++ internal/taskwarrior/filter.go | 13 ++++ internal/taskwarrior/task.go | 51 +++++++++++++++- 4 files changed, 171 insertions(+), 55 deletions(-) create mode 100644 internal/gitw/project.go diff --git a/cmd/gitw/main.go b/cmd/gitw/main.go index 13c57b1..3d699d2 100644 --- a/cmd/gitw/main.go +++ b/cmd/gitw/main.go @@ -6,13 +6,11 @@ import ( "gitea.yves-biener.de/yves-biener/gitwarrior/internal/gitea" "gitea.yves-biener.de/yves-biener/gitwarrior/internal/gitw" - "gitea.yves-biener.de/yves-biener/gitwarrior/internal/taskwarrior" ) func main() { // TODO: server url may be also be derived from the git configuration? server := gitea.NewGitea("https://gitea.yves-biener.de") - repository, err := gitw.Discover() if err != nil { fmt.Fprintln(os.Stderr, err) @@ -23,61 +21,13 @@ func main() { fmt.Fprintln(os.Stderr, err) os.Exit(-1) } - fmt.Printf("%#v\n", repository) - fmt.Println("---") + project := gitw.NewProject(server, repository) - issue, err := server.GetIssue(repository, 1) - if err != nil { + if err = project.Fetch(); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(-1) } - milestone, err := server.GetMilestone(repository, 1) - if err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(-1) - } - comments, err := server.GetComments(repository) - if err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(-1) - } - - fmt.Printf("%#v\n", issue) - fmt.Println("---") - fmt.Printf("%#v\n", milestone) - fmt.Println("---") - - for _, comment := range comments { - // NOTE: filter comments to only include comments which are related to the current issue - // Order them by their Id's as that's the order they are in the issue (most likely) - if comment.Issue_url == issue.Html_url { - fmt.Printf("%#v\n", comment) - } - } - - var filter taskwarrior.Filter - filter.IncludeProjects("notes") - tasks, err := taskwarrior.GetTasks(filter) - if err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(-1) - } - for _, task := range tasks { - fmt.Printf("%#v\n", task) - } - fmt.Println("---") - - filter.Reset() - filter.IncludeIds(1, 2, 3) - tasks, err = taskwarrior.GetTasks(filter) - if err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(-1) - } - for _, task := range tasks { - fmt.Printf("%#v\n", task) - } - fmt.Println("---") + fmt.Printf("%#v\n", project) // NOTE: this can be used to add / modify tasks // var update_tasks []taskwarrior.Task diff --git a/internal/gitw/project.go b/internal/gitw/project.go new file mode 100644 index 0000000..c91f13b --- /dev/null +++ b/internal/gitw/project.go @@ -0,0 +1,106 @@ +package gitw + +import ( + "errors" + + "gitea.yves-biener.de/yves-biener/gitwarrior/internal/gitea" + "gitea.yves-biener.de/yves-biener/gitwarrior/internal/taskwarrior" +) + +type Project struct { + server gitea.Gitea + repository gitea.Repository + issues []Issue + milestones []gitea.Milestone +} + +func NewProject(server gitea.Gitea, repository gitea.Repository) Project { + return Project{ + server: server, + repository: repository, + } +} + +func (project *Project) Pull() error { + if err := project.Fetch(); err != nil { + return err + } + if err := project.merge(); err != nil { + return err + } + return nil +} + +func (project *Project) Fetch() error { + comments, err := project.server.GetComments(project.repository) + if err != nil { + return err + } + issues, err := project.server.GetIssues(project.repository) + if err != nil { + return err + } + project.milestones, err = project.server.GetMilestones(project.repository) + if err != nil { + return err + } + for _, issue := range issues { + var issue_comments []gitea.Comment + for _, comment := range comments { + if comment.Issue_url == issue.Html_url { + issue_comments = append(issue_comments, comment) + } + } + project.issues = append(project.issues, Issue{ + issue: issue, + comments: issue_comments, + }) + } + return nil +} + +func (project *Project) merge() error { + // TODO: run merge to update the local taskwarrior tasks into the state of the + // issues and milestones + return nil +} + +type Issue struct { + issue gitea.Issue + comments []gitea.Comment +} + +func (i *Issue) into() (taskwarrior.Task, error) { + // TODO: identify if issue is already an existing task + var filter taskwarrior.Filter + filter.IncludeGitId(i.issue.Id) + filter.IncludeGitType(taskwarrior.ISSUE) + tasks, err := taskwarrior.GetTasks(filter) + if err != nil { + // this means that a task for this issue does not exist yet + return taskwarrior.NewTask( + i.issue.Title, + i.issue.Repository.Name, + i.issue.Id, + taskwarrior.ISSUE, + i.issue.Labels..., + ), nil + } else { + // this means that a task exists and it needs to be merged + if len(tasks) != 1 { + return taskwarrior.Task{}, errors.New("Did not find exactly one task for a given issue.Id") + } + return i.merge(tasks[0]), nil + } +} + +func (i *Issue) merge(task taskwarrior.Task) taskwarrior.Task { + // TODO: issue values into task: + // - is the issue more recent than the task? + // - apply changes into task + // - in case of merge conflicts ask user for corresponding action: + // 1. use theirs + // 2. use mine + // 3. use provided value + return task +} diff --git a/internal/taskwarrior/filter.go b/internal/taskwarrior/filter.go index abb7e5a..d6d8f92 100644 --- a/internal/taskwarrior/filter.go +++ b/internal/taskwarrior/filter.go @@ -6,6 +6,11 @@ type Filter struct { filter []string } +type Type string + +const MILESTONE Type = "milestone" +const ISSUE Type = "issue" + func (f *Filter) Reset() { f.filter = nil } @@ -33,3 +38,11 @@ func (f *Filter) IncludeIds(ids ...uint) { f.filter = append(f.filter, fmt.Sprintf("%d", id)) } } + +func (f *Filter) IncludeGitId(id uint) { + f.filter = append(f.filter, fmt.Sprintf("git_id=%d", id)) +} + +func (f *Filter) IncludeGitType(value Type) { + f.filter = append(f.filter, fmt.Sprintf("git_type=%s", value)) +} diff --git a/internal/taskwarrior/task.go b/internal/taskwarrior/task.go index e1c6671..665895a 100644 --- a/internal/taskwarrior/task.go +++ b/internal/taskwarrior/task.go @@ -3,7 +3,11 @@ package taskwarrior import ( "bytes" "encoding/json" + "fmt" + "os" "os/exec" + "strings" + "time" ) /// A task with an Id of 0 is either `completed` or `deleted` which is also @@ -11,6 +15,8 @@ import ( type Task struct { Id uint `json:"id,omitempty"` + Git_id float32 `json:"git_id,omitempty"` // uda + Git_type string `json:"git_type,omitempty"` // uda Project string `json:"project,omitempty"` Tags []string `json:"tags,omitempty"` Description string `json:"description,omitempty"` @@ -29,14 +35,55 @@ type Annotation struct { Entry string `json:"entry,omitempty"` } -func NewTask(description string, project string, tags ...string) Task { +func NewTask(description string, project string, git_id uint, git_type Type, tags ...string) Task { + // TODO: update task struct to include the new user defined value, which shall + // also be provided as an argument return Task{ Project: project, - Tags: tags, Description: description, + Git_id: float32(git_id), + Git_type: string(git_type), + Tags: tags, } } +func TaskTimeToGoTime(t string) time.Time { + // TODO: apply required changes to the string for correct parsing + splits := strings.Split(t, "T") + if len(splits) != 2 { + fmt.Fprintf(os.Stderr, "Expected exactly 2 splits") + os.Exit(-1) + } + date := splits[0] + first := date[0:4] + second := date[4:6] + third := date[6:] + date = strings.Join([]string{first, second, third}, "-") + timestamp := splits[1] + first = timestamp[0:2] + second = timestamp[2:4] + third = timestamp[4 : len(timestamp)-1] + timestamp = strings.Join([]string{first, second, third}, ":") + value := fmt.Sprintf("%sT%s+02:00", date, timestamp) + result, err := time.Parse(time.RFC3339, value) + if err != nil { + fmt.Fprint(os.Stderr, err) + os.Exit(-1) + } + return result +} + +func GoTimeToTaskTime(t time.Time) string { + result := t.Format(time.RFC3339) + // TODO: apply changes to the result + // go: 2023-10-10T19:57:22+02:00 + // task: 20231010T195722Z + result = strings.Replace(result, "-", "", 2) + result = strings.Replace(result, ":", "", 2) + result = strings.ReplaceAll(result, "+02:00", "Z") + return result +} + func GetTasks(filter Filter) ([]Task, error) { filter.filter = append(filter.filter, "export") cmd := exec.Command("task", filter.filter...)