diff --git a/cmd/gitw/main.go b/cmd/gitw/main.go index 020553c..c58c356 100644 --- a/cmd/gitw/main.go +++ b/cmd/gitw/main.go @@ -29,6 +29,12 @@ func main() { os.Exit(-1) } + fmt.Printf("Push changes to %s\n", repository.Full_name) + if err = project.Push(); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(-1) + } + // NOTE: this can be used to add / modify tasks // var update_tasks []taskwarrior.Task // task := taskwarrior.NewTask( diff --git a/internal/diff/diff.go b/internal/diff/diff.go index 42ae7c6..490116b 100644 --- a/internal/diff/diff.go +++ b/internal/diff/diff.go @@ -1,11 +1,72 @@ package diff -import "fmt" +import ( + "errors" + "fmt" + "os" + "os/exec" + "regexp" + "strings" +) func Prompt(value, theirs, mine string) string { // TODO: create tmp file with the corresponding contents // open tmp file using the $EDITOR environment variable // parse and return output of tmp file after $EDITOR execution has been completed - fmt.Printf("\tPrompting for value: '%s'\n", value) - return value + value = strings.ReplaceAll(value, "\r\n", "\n") + theirs = strings.ReplaceAll(theirs, "\r\n", "\n") + mine = strings.ReplaceAll(mine, "\r\n", "\n") + file, err := os.CreateTemp("", "gitw-*") + if err != nil { + fmt.Fprintln(os.Stderr, err) + return value + } + defer os.Remove(file.Name()) + + // prepare file contents + if _, err := file.WriteString(value); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(-1) + } + if _, err := file.WriteString("\n\n--- theirs ---\n\n"); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(-1) + } + if _, err := file.WriteString(theirs); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(-1) + } + if _, err := file.WriteString("\n\n--- mine ---\n\n"); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(-1) + } + if _, err := file.WriteString(mine); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(-1) + } + file.Close() + + cmd := exec.Command(os.Getenv("EDITOR"), file.Name()) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + fmt.Fprintln(os.Stderr, err) + return value + } + contents, err := os.ReadFile(file.Name()) + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(-1) + } + // TODO: what should I do with the '\r' characters? Who and when should they be handled? + re := regexp.MustCompile("(?s)^(.*)--- theirs ---") + matches := re.FindAllStringSubmatch(string(contents), -1) + if len(matches) > 0 && len(matches[0]) == 2 { + return matches[0][1] + } else { + fmt.Fprintln(os.Stderr, errors.New("No matches found, could not read merged value. Aborting.")) + os.Exit(-1) + return "" // just to make compiler happy + } } diff --git a/internal/gitea/comment.go b/internal/gitea/comment.go index 4f05244..e040899 100644 --- a/internal/gitea/comment.go +++ b/internal/gitea/comment.go @@ -3,6 +3,7 @@ package gitea import ( "bytes" "encoding/json" + "errors" "fmt" "net/http" "strings" @@ -61,6 +62,7 @@ func (gitea *Gitea) GetComment(repo Repository, id uint) (Comment, error) { return comment, err } +// FIXME: how to do basic authorization? func (gitea *Gitea) UpdateComment(repo Repository, comment Comment) error { url := fmt.Sprintf("%s/repos/%s/issues/comments/%d", gitea.Url(), repo.Full_name, comment.Id) json, err := json.Marshal(&map[string]interface{}{ @@ -69,10 +71,20 @@ func (gitea *Gitea) UpdateComment(repo Repository, comment Comment) error { if err != nil { return err } - _, err = http.NewRequest(http.MethodPatch, url, bytes.NewBuffer(json)) + client := &http.Client{} + request, err := http.NewRequest(http.MethodPatch, url, bytes.NewBuffer(json)) + request.Header.Set("content-type", "application/json") if err != nil { return err } + result, err := client.Do(request) + if err != nil { + return err + } + defer result.Body.Close() + if result.StatusCode != 201 { + return errors.New(fmt.Sprintf("\tRequest returned status: %s\n", result.Status)) + } return nil } diff --git a/internal/gitea/issue.go b/internal/gitea/issue.go index f8acad7..18a9b2f 100644 --- a/internal/gitea/issue.go +++ b/internal/gitea/issue.go @@ -3,6 +3,7 @@ package gitea import ( "bytes" "encoding/json" + "errors" "fmt" "net/http" "time" @@ -80,15 +81,16 @@ func (gitea *Gitea) GetIssue(repo Repository, id uint) (Issue, error) { return issue, nil } +// FIXME: how to do basic authorization? func (gitea *Gitea) UpdateIssue(repo Repository, issue Issue) error { url := fmt.Sprintf("%s/repos/%s/issues/%d", gitea.Url(), repo.Full_name, issue.Id) - var payload map[string]interface{} - payload["assignee"] = issue.Assignee - payload["assignees"] = issue.Assignees + payload := make(map[string]interface{}) + // payload["assignee"] = issue.Assignee + // payload["assignees"] = issue.Assignees payload["body"] = issue.Body payload["due_date"] = issue.Due_date - payload["milestone"] = issue.Milestone - payload["ref"] = issue.Ref + payload["milestone"] = issue.Milestone.Id + // payload["ref"] = issue.Ref payload["state"] = issue.State payload["title"] = issue.Title payload["unset_due_date"] = issue.Due_date.Unix() == 0 @@ -96,24 +98,34 @@ func (gitea *Gitea) UpdateIssue(repo Repository, issue Issue) error { if err != nil { return err } - _, err = http.NewRequest(http.MethodPatch, url, bytes.NewBuffer(json)) + client := &http.Client{} + request, err := http.NewRequest(http.MethodPatch, url, bytes.NewBuffer(json)) + request.Header.Set("content-type", "application/json") if err != nil { return err } + result, err := client.Do(request) + if err != nil { + return err + } + defer result.Body.Close() + if result.StatusCode != 201 { + return errors.New(fmt.Sprintf("\tRequest returned status: %s\n", result.Status)) + } return nil } func (gitea *Gitea) NewIssue(repo Repository, issue Issue) (Issue, error) { url := fmt.Sprintf("%s/repos/%s/issues", gitea.Url(), repo.Full_name) - var payload map[string]interface{} - payload["assignee"] = issue.Assignee - payload["assignees"] = issue.Assignees + payload := make(map[string]interface{}) + // payload["assignee"] = issue.Assignee + // payload["assignees"] = issue.Assignees payload["body"] = issue.Body payload["closed"] = issue.State == string(CLOSED) payload["due_date"] = issue.Due_date payload["lables"] = issue.Labels - payload["milestone"] = issue.Milestone.Title != "" - payload["ref"] = issue.Ref + payload["milestone"] = issue.Milestone.Id + // payload["ref"] = issue.Ref payload["title"] = issue.Title json_payload, err := json.Marshal(&payload) var res Issue diff --git a/internal/gitea/milestone.go b/internal/gitea/milestone.go index c91430b..d8db417 100644 --- a/internal/gitea/milestone.go +++ b/internal/gitea/milestone.go @@ -3,6 +3,7 @@ package gitea import ( "bytes" "encoding/json" + "errors" "fmt" "net/http" "time" @@ -113,9 +114,10 @@ func (gitea *Gitea) GetMilestone(repo Repository, id uint) (Milestone, error) { return milestone, nil } +// FIXME: how to do basic authorization? func (gitea *Gitea) UpdateMilestone(repo Repository, milestone Milestone) error { url := fmt.Sprintf("%s/repos/%s/milestones/%d", gitea.Url(), repo.Full_name, milestone.Id) - var payload map[string]interface{} + payload := make(map[string]interface{}) payload["description"] = milestone.Description payload["due_on"] = milestone.Due_on payload["state"] = milestone.State @@ -124,10 +126,20 @@ func (gitea *Gitea) UpdateMilestone(repo Repository, milestone Milestone) error if err != nil { return err } - _, err = http.NewRequest(http.MethodPatch, url, bytes.NewBuffer(json)) + client := &http.Client{} + request, err := http.NewRequest(http.MethodPatch, url, bytes.NewBuffer(json)) + request.Header.Set("content-type", "application/json") if err != nil { return err } + result, err := client.Do(request) + if err != nil { + return err + } + defer result.Body.Close() + if result.StatusCode != 201 { + return errors.New(fmt.Sprintf("\tRequest returned status: %s\n", result.Status)) + } return nil } diff --git a/internal/gitw/project.go b/internal/gitw/project.go index d35e915..31b9cb5 100644 --- a/internal/gitw/project.go +++ b/internal/gitw/project.go @@ -68,9 +68,86 @@ func (project *Project) Fetch() error { return nil } +func (project *Project) Push() error { + // TODO: check if a pull is required before pushing changes + var filter taskwarrior.Filter + filter.IncludeProjects(project.repository.Name) + tasks, err := taskwarrior.GetTasks(filter) + if err != nil { + return err + } + for _, task := range tasks { + if task.Git_type == string(taskwarrior.ISSUE) { + var issue gitea.Issue + issue.Id = uint(task.Git_id) + issue.Title = task.Description + issue.Body = task.Annotations[0].Description + if len(task.Due) > 0 { + issue.Due_date = taskwarrior.TaskTimeToGoTime(task.Due) + } else { + issue.Due_date = time.Unix(0, 0) + } + filter.Reset() + filter.IncludeGitType(taskwarrior.MILESTONE) + filter.IncludeProjects(project.repository.Name) + milestones, err := taskwarrior.GetTasks(filter) + if err != nil { + return err + } + found := false + for _, milestone := range milestones { + for _, dep := range milestone.Depends { + if task.Uuid == dep { + issue.Milestone.Id = uint(milestone.Git_id) + found = true + break + } + } + if found { + break + } + } + if task.Status == "completed" { + issue.State = string(gitea.CLOSED) + } else { + issue.State = string(gitea.OPEN) + } + if err = project.server.UpdateIssue(project.repository, issue); err != nil { + return err + } + } else { + var milestone gitea.Milestone + milestone.Id = uint(task.Git_id) + milestone.Title = task.Description + milestone.Description = task.Annotations[0].Description + milestone.Due_on = taskwarrior.TaskTimeToGoTime(task.Due) + if task.Status == "completed" { + milestone.State = string(gitea.CLOSED) + } else { + milestone.State = string(gitea.OPEN) + } + if err = project.server.UpdateMilestone(project.repository, milestone); err != nil { + return err + } + } + for i, annotation := range task.Annotations { + if i == 0 { // that the body of the issue / milestone + continue + } + var comment gitea.Comment + comment.Id = uint(task.Git_id) + comment.Body = annotation.Description + if err = project.server.UpdateComment(project.repository, comment); err != nil { + return err + } + } + } + return nil +} + // TODO: tasks should include the corresponding time's of the git related issues and milestones func (project *Project) merge() error { - dry_run := true // make this a parameter / cli flag + dry_run := false // make this a parameter / cli flag var tasks []taskwarrior.Task var task taskwarrior.Task var filter taskwarrior.Filter @@ -208,32 +285,26 @@ func (issue *Issue) MergeTask(task taskwarrior.Task) taskwarrior.Task { task.Status = diff.Prompt(task.Status, issue.git_issue.State, task.Status) } } - if len(task.Annotations) != len(issue.comments) { - var annotations []string - annotations = append(annotations, issue.git_issue.Body) - for _, annotation := range task.Annotations { - annotations = append(annotations, annotation.Description) + // TODO: try to automatically derive the correct required changes + var annotations []string + annotations = append(annotations, issue.git_issue.Body) + for _, annotation := range task.Annotations { + annotations = append(annotations, annotation.Description) + } + var comments []string + for _, comment := range issue.comments { + comments = append(comments, comment.Body) + } + // TODO: provide options for theirs, mine and manual edit of the task annotations + annotation_joined := diff.Prompt(strings.Join(annotations, "\n\n"), strings.Join(comments, "\n\n"), strings.Join(annotations, "\n\n")) + for i, description := range strings.Split(annotation_joined, "\n\n") { + if len(description) == 0 { + continue } - var comments []string - for _, comment := range issue.comments { - comments = append(comments, comment.Body) - } - // TODO: how should the user manually enter the values? - // TODO: provide options for theirs, mine and manual edit of the task annotations - // annotation_joined := prompt(strings.Join(annotations, "\n\n"), strings.Join(comments, "\n\n"), strings.Join(annotations, "\n\n")) - } else { - for i := range issue.comments { - // check the modification times? - annotation := task.Annotations[i] - comment := issue.comments[i] - modification_time := taskwarrior.TaskTimeToGoTime(annotation.Entry) - if comment.Updated_at.After(modification_time) { - annotation.Description = comment.Body - annotation.Entry = taskwarrior.GoTimeToTaskTime(comment.Updated_at) - } else { - annotation.Description = diff.Prompt(annotation.Description, comment.Body, annotation.Description) - } - task.Annotations[i] = annotation + if i < len(task.Annotations) { + task.Annotations[i].Description = description + } else { + task.AppendComment(description, time.Now().In(time.Local)) } } } else {