This repository has been archived on 2025-10-30. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
gitwarrior/internal/gitw/project.go

446 lines
13 KiB
Go

package gitw
import (
"errors"
"fmt"
"strconv"
"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(dry_run bool) error {
if err := project.fetch(); err != nil {
return err
}
if err := project.merge(dry_run); 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(dry_run bool) error {
if err := project.fetch(); err != nil {
return err
}
// 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 x, task := range tasks {
if task.Git_type == string(taskwarrior.ISSUE) {
var issue gitea.Issue
// TODO: add new issue if necessary
issue.Number = uint(task.Git_number)
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_number)
found = true
break
}
}
if found {
break
}
}
if task.Status == "completed" {
issue.State = string(gitea.CLOSED)
} else {
issue.State = string(gitea.OPEN)
}
if !dry_run {
if err = project.server.UpdateIssue(project.repository, issue); err != nil {
return err
}
} else {
fmt.Printf("\tUpdate Issue: '%s'\n", issue.Title)
}
} else {
var milestone gitea.Milestone
milestone.Id = uint(task.Git_number)
milestone.Title = task.Description
milestone.Description = task.Annotations[0].Description
milestone.Due_on = taskwarrior.TaskTimeToGoTime(task.Due)
// TODO: add new milestone if necessary
if task.Status == "completed" {
milestone.State = string(gitea.CLOSED)
} else {
milestone.State = string(gitea.OPEN)
}
if !dry_run {
if err = project.server.UpdateMilestone(project.repository, milestone); err != nil {
return err
}
} else {
fmt.Printf("\tUpdating Milestone: '%s'\n", milestone.Title)
}
}
comment_ids := strings.Split(task.Git_comment_ids, ",")
for i, annotation := range task.Annotations {
if i == 0 { // that the body of the issue / milestone
continue
}
var comment gitea.Comment
if i >= len(comment_ids) {
// create new comment
var issue gitea.Issue
found_issue := false
for _, git_issue := range project.issues {
if git_issue.git_issue.Number == uint(task.Git_number) {
issue = git_issue.git_issue
found_issue = true
break
}
}
if !found_issue {
return errors.New("Could not find corresponding issue for annotation to create new comment on gitea.")
}
comment.Body = annotation.Description
comment.Created_at = taskwarrior.TaskTimeToGoTime(annotation.Entry)
fmt.Printf("\tAdd comment to '%s'#'%d'\n", issue.Title, issue.Number)
if !dry_run {
if comment, err = project.server.NewComment(project.repository, issue, comment); err != nil {
return err
}
}
// add the corresponding id to the one assigned by git
comment_ids = append(comment_ids, fmt.Sprintf("%d", comment.Id))
continue
}
comment_id, err := strconv.Atoi(comment_ids[i])
if err != nil {
return err
}
if comment_id == 0 {
continue
}
if comment_id < 0 {
// create new comment
var issue gitea.Issue
found_issue := false
for _, git_issue := range project.issues {
if git_issue.git_issue.Number == uint(task.Git_number) {
issue = git_issue.git_issue
found_issue = true
break
}
}
if !found_issue {
return errors.New("Could not find corresponding issue for annotation to create new comment on gitea.")
}
comment.Body = annotation.Description
comment.Created_at = taskwarrior.TaskTimeToGoTime(annotation.Entry)
fmt.Printf("\tAdd comment to '%s'#'%d'\n", issue.Title, issue.Number)
if !dry_run {
if comment, err = project.server.NewComment(project.repository, issue, comment); err != nil {
return err
}
}
// update the corresponding id from -1 to the one assigned by git
comment_ids[i] = fmt.Sprintf("%d", comment.Id)
} else {
// update existing comment
comment.Id = uint(comment_id)
comment.Body = annotation.Description
if !dry_run {
if err = project.server.UpdateComment(project.repository, comment); err != nil {
return err
}
} else {
fmt.Printf("\tUpdating comment: '%d'\n", comment.Id)
}
}
}
if len(task.Annotations) < len(comment_ids) {
// there have been annotations removed, hence the corresponding comments need to be removed as well
for i := len(task.Annotations); i < len(comment_ids); i += 1 {
comment_id, err := strconv.Atoi(comment_ids[i])
if err != nil {
return err
}
if comment_id < 0 {
return errors.New("Tried to delete comment with id < 0")
}
fmt.Printf("\tRemoving comment\n")
if !dry_run {
if err = project.server.DeleteComment(project.repository, uint(comment_id)); err != nil {
return err
}
}
}
comment_ids = comment_ids[:len(task.Annotations)]
}
// update task
tasks[x].Git_comment_ids = strings.Join(comment_ids, ",")
// as we update the task we also update the last update time as well
tasks[x].Last_gitw_update = taskwarrior.GoTimeToTaskTime(time.Now().In(time.Local))
}
if dry_run {
return nil
}
return taskwarrior.UpdateTasks(tasks)
}
func (project *Project) merge(dry_run bool) error {
var tasks []taskwarrior.Task
var task taskwarrior.Task
var filter taskwarrior.Filter
// NOTE: merge tasks
for _, issue := range project.issues {
filter.Reset()
filter.IncludeProjects(project.repository.Name)
filter.IncludeGitNumber(issue.git_issue.Number)
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 {
for _, task := range tasks {
fmt.Printf("\t%#v\n\n", task)
}
} else {
if err := taskwarrior.UpdateTasks(tasks); err != nil {
return err
}
}
tasks = nil // NOTE: reset tasks after successfully updating the issues
// TODO: merge milestones
for _, milestone := range project.milestones {
filter.Reset()
filter.IncludeProjects(project.repository.Name)
filter.IncludeGitNumber(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 == string(gitea.CLOSED) || issue.git_issue.Milestone.Id != milestone.Id {
continue
}
// link to the corresponding task
filter.Reset()
filter.IncludeProjects(project.repository.Name)
filter.IncludeGitNumber(issue.git_issue.Number)
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 {
for _, task := range tasks {
fmt.Printf("\t%#v\n\n", task)
}
} else {
return taskwarrior.UpdateTasks(tasks)
}
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.Number,
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(int(comment.Id), comment.Body, comment.Updated_at)
}
return
}
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 == string(gitea.CLOSED) {
if !(task.Status == "completed" && issue.git_issue.State == string(gitea.CLOSED) || task.Status != "completed" && issue.git_issue.State != string(gitea.CLOSED)) {
task.Status = diff.Prompt(task.Status, issue.git_issue.State, task.Status)
}
}
var annotations []string
for _, annotation := range task.Annotations {
annotations = append(annotations, annotation.Description)
}
var comments []string
for _, comment := range issue.comments {
comments = append(comments, comment.Body)
}
require_merge := false
if len(annotations) == len(comments) {
for i := range annotations {
if annotations[i] != comments[i] {
require_merge = true
}
}
}
if require_merge {
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(-1, 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 == string(gitea.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(-1, comment.Body, comment.Updated_at)
}
}
}
}
task.Last_gitw_update = taskwarrior.GoTimeToTaskTime(time.Now().In(time.Local))
}
return task
}