package gitw import ( "errors" "fmt" "strings" "time" "gitea.yves-biener.de/yves-biener/gitwarrior/internal/diff" "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 issue_comments = append(issue_comments, gitea.Comment{ Body: strings.ReplaceAll(issue.Body, "\r\n", "\n"), Created_at: issue.Created_at, Updated_at: issue.Created_at, }) for _, comment := range comments { if comment.Issue_url == issue.Html_url { issue_comments = append(issue_comments, comment) } } project.issues = append(project.issues, Issue{ git_issue: issue, comments: issue_comments, }) } 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 := false // make this a parameter / cli flag var tasks []taskwarrior.Task var task taskwarrior.Task var filter taskwarrior.Filter // NOTE: merge tasks for _, issue := range project.issues { filter.Reset() filter.IncludeGitId(issue.git_issue.Id) filter.IncludeGitType(taskwarrior.ISSUE) git_tasks, err := taskwarrior.GetTasks(filter) if err != nil { return err } if len(git_tasks) > 1 { return errors.New("Git issue id was at least used twice in taskwarrior tasks.") } else if len(git_tasks) == 0 { // NOTE: ignore closed issues which do not have a taskwarrior task if issue.git_issue.State == string(gitea.CLOSED) { continue } // NOTE: this task does not yet exist task, err = issue.IntoTask(project.repository) if err != nil { return err } fmt.Printf("\tCreated task: '%s'\n", task.Description) tasks = append(tasks, task) } else { // NOTE: there is excactly one git_task task = issue.MergeTask(git_tasks[0]) fmt.Printf("\tUpdated task: '%s'\n", task.Description) tasks = append(tasks, task) } } if !dry_run { if err := taskwarrior.UpdateTasks(tasks); err != nil { return err } } else { for _, task := range tasks { fmt.Printf("\t%#v\n\n", task) } } tasks = nil // NOTE: reset tasks after successfully updating the issues // TODO: merge milestones for _, milestone := range project.milestones { filter.Reset() filter.IncludeGitId(milestone.Id) filter.IncludeGitType(taskwarrior.MILESTONE) git_tasks, err := taskwarrior.GetTasks(filter) if err != nil { return err } if len(git_tasks) > 1 { return errors.New("Git milestone id was at least used twice in taskwarrior tasks.") } else if len(git_tasks) == 0 { // NOTE: ignore closed milestones which do not have a taskwarrior task if milestone.State == string(gitea.CLOSED) { continue } // NOTE: this milestone does not yet exist task = milestone.IntoTask(project.repository) for _, issue := range project.issues { if issue.git_issue.State == "closed" || issue.git_issue.Milestone.Id != milestone.Id { continue } // link to the corresponding task filter.Reset() filter.IncludeGitId(issue.git_issue.Id) filter.IncludeGitType(taskwarrior.ISSUE) tasks, err := taskwarrior.GetTasks(filter) if err != nil { return err } if len(tasks) != 1 { return errors.New("Git issue id used for this milestone does not exist") } task.Depends = append(task.Depends, tasks[0].Uuid) } fmt.Printf("\tCreated milestone: '%s'\n", task.Description) tasks = append(tasks, task) } else { // NOTE: there is exactly one git_task task = milestone.MergeTask(git_tasks[0]) fmt.Printf("\tUpdated milestone: '%s'\n", task.Description) tasks = append(tasks, task) } } if !dry_run { return taskwarrior.UpdateTasks(tasks) } else { for _, task := range tasks { fmt.Printf("\t%#v\n\n", task) } } return nil } type Issue struct { git_issue gitea.Issue comments []gitea.Comment } func (issue *Issue) IntoTask(repository gitea.Repository) (task taskwarrior.Task, err error) { // transform current issue into new taskwarrior.Task task = taskwarrior.NewTask( issue.git_issue.Title, repository.Name, issue.git_issue.Id, taskwarrior.ISSUE, issue.git_issue.Labels..., ) task.Entry = taskwarrior.GoTimeToTaskTime(issue.git_issue.Created_at) task.Modified = taskwarrior.GoTimeToTaskTime(issue.git_issue.Updated_at) for _, comment := range issue.comments { task.AppendComment(comment.Body, comment.Updated_at) } return } // TODO: implement merging of git issue or milestone into a taskwarrior task func (issue *Issue) MergeTask(task taskwarrior.Task) taskwarrior.Task { last_update := taskwarrior.TaskTimeToGoTime(task.Last_gitw_update) if issue.git_issue.Updated_at.After(last_update) { // there are changes we need to merge if taskwarrior.TaskTimeToGoTime(task.Modified).After(last_update) { // NOTE: this means that there are local modifications which are not yet pushed if task.Description != issue.git_issue.Title { task.Description = diff.Prompt(task.Description, issue.git_issue.Title, task.Description) } if task.Status == "completed" || issue.git_issue.State == "closed" { if !(task.Status == "completed" && issue.git_issue.State == "closed" || task.Status != "completed" && issue.git_issue.State != "closed") { task.Status = diff.Prompt(task.Status, issue.git_issue.State, task.Status) } } // 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 } if i < len(task.Annotations) { task.Annotations[i].Description = description } else { task.AppendComment(description, time.Now().In(time.Local)) } } } else { // NOTE: there are no modifications between the last received update and the current version, hence we just accept theirs fully task.Description = issue.git_issue.Title if issue.git_issue.State == "closed" { // otherwise do not update the value task.Status = "completed" task.End = taskwarrior.GoTimeToTaskTime(issue.git_issue.Closed_at) } annotations := len(task.Annotations) for i, comment := range issue.comments { if comment.Updated_at.After(last_update) { if i < annotations { task.Annotations[i].Description = comment.Body task.Annotations[i].Entry = taskwarrior.GoTimeToTaskTime(comment.Updated_at) } else { task.AppendComment(comment.Body, comment.Updated_at) } } } } task.Last_gitw_update = taskwarrior.GoTimeToTaskTime(time.Now().In(time.Local)) } return task }