code.gitea.io/gitea@v1.21.7/services/convert/issue.go (about)

     1  // Copyright 2020 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package convert
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  	"net/url"
    10  	"strings"
    11  
    12  	"code.gitea.io/gitea/models/db"
    13  	issues_model "code.gitea.io/gitea/models/issues"
    14  	repo_model "code.gitea.io/gitea/models/repo"
    15  	user_model "code.gitea.io/gitea/models/user"
    16  	"code.gitea.io/gitea/modules/label"
    17  	"code.gitea.io/gitea/modules/log"
    18  	"code.gitea.io/gitea/modules/setting"
    19  	api "code.gitea.io/gitea/modules/structs"
    20  )
    21  
    22  func ToIssue(ctx context.Context, issue *issues_model.Issue) *api.Issue {
    23  	return toIssue(ctx, issue, WebAssetDownloadURL)
    24  }
    25  
    26  // ToAPIIssue converts an Issue to API format
    27  // it assumes some fields assigned with values:
    28  // Required - Poster, Labels,
    29  // Optional - Milestone, Assignee, PullRequest
    30  func ToAPIIssue(ctx context.Context, issue *issues_model.Issue) *api.Issue {
    31  	return toIssue(ctx, issue, APIAssetDownloadURL)
    32  }
    33  
    34  func toIssue(ctx context.Context, issue *issues_model.Issue, getDownloadURL func(repo *repo_model.Repository, attach *repo_model.Attachment) string) *api.Issue {
    35  	if err := issue.LoadLabels(ctx); err != nil {
    36  		return &api.Issue{}
    37  	}
    38  	if err := issue.LoadPoster(ctx); err != nil {
    39  		return &api.Issue{}
    40  	}
    41  	if err := issue.LoadRepo(ctx); err != nil {
    42  		return &api.Issue{}
    43  	}
    44  
    45  	apiIssue := &api.Issue{
    46  		ID:          issue.ID,
    47  		Index:       issue.Index,
    48  		Poster:      ToUser(ctx, issue.Poster, nil),
    49  		Title:       issue.Title,
    50  		Body:        issue.Content,
    51  		Attachments: toAttachments(issue.Repo, issue.Attachments, getDownloadURL),
    52  		Ref:         issue.Ref,
    53  		State:       issue.State(),
    54  		IsLocked:    issue.IsLocked,
    55  		Comments:    issue.NumComments,
    56  		Created:     issue.CreatedUnix.AsTime(),
    57  		Updated:     issue.UpdatedUnix.AsTime(),
    58  		PinOrder:    issue.PinOrder,
    59  	}
    60  
    61  	if issue.Repo != nil {
    62  		if err := issue.Repo.LoadOwner(ctx); err != nil {
    63  			return &api.Issue{}
    64  		}
    65  		apiIssue.URL = issue.APIURL()
    66  		apiIssue.HTMLURL = issue.HTMLURL()
    67  		apiIssue.Labels = ToLabelList(issue.Labels, issue.Repo, issue.Repo.Owner)
    68  		apiIssue.Repo = &api.RepositoryMeta{
    69  			ID:       issue.Repo.ID,
    70  			Name:     issue.Repo.Name,
    71  			Owner:    issue.Repo.OwnerName,
    72  			FullName: issue.Repo.FullName(),
    73  		}
    74  	}
    75  
    76  	if issue.ClosedUnix != 0 {
    77  		apiIssue.Closed = issue.ClosedUnix.AsTimePtr()
    78  	}
    79  
    80  	if err := issue.LoadMilestone(ctx); err != nil {
    81  		return &api.Issue{}
    82  	}
    83  	if issue.Milestone != nil {
    84  		apiIssue.Milestone = ToAPIMilestone(issue.Milestone)
    85  	}
    86  
    87  	if err := issue.LoadAssignees(ctx); err != nil {
    88  		return &api.Issue{}
    89  	}
    90  	if len(issue.Assignees) > 0 {
    91  		for _, assignee := range issue.Assignees {
    92  			apiIssue.Assignees = append(apiIssue.Assignees, ToUser(ctx, assignee, nil))
    93  		}
    94  		apiIssue.Assignee = ToUser(ctx, issue.Assignees[0], nil) // For compatibility, we're keeping the first assignee as `apiIssue.Assignee`
    95  	}
    96  	if issue.IsPull {
    97  		if err := issue.LoadPullRequest(ctx); err != nil {
    98  			return &api.Issue{}
    99  		}
   100  		if issue.PullRequest != nil {
   101  			apiIssue.PullRequest = &api.PullRequestMeta{
   102  				HasMerged: issue.PullRequest.HasMerged,
   103  			}
   104  			if issue.PullRequest.HasMerged {
   105  				apiIssue.PullRequest.Merged = issue.PullRequest.MergedUnix.AsTimePtr()
   106  			}
   107  		}
   108  	}
   109  	if issue.DeadlineUnix != 0 {
   110  		apiIssue.Deadline = issue.DeadlineUnix.AsTimePtr()
   111  	}
   112  
   113  	return apiIssue
   114  }
   115  
   116  // ToIssueList converts an IssueList to API format
   117  func ToIssueList(ctx context.Context, il issues_model.IssueList) []*api.Issue {
   118  	result := make([]*api.Issue, len(il))
   119  	for i := range il {
   120  		result[i] = ToIssue(ctx, il[i])
   121  	}
   122  	return result
   123  }
   124  
   125  // ToAPIIssueList converts an IssueList to API format
   126  func ToAPIIssueList(ctx context.Context, il issues_model.IssueList) []*api.Issue {
   127  	result := make([]*api.Issue, len(il))
   128  	for i := range il {
   129  		result[i] = ToAPIIssue(ctx, il[i])
   130  	}
   131  	return result
   132  }
   133  
   134  // ToTrackedTime converts TrackedTime to API format
   135  func ToTrackedTime(ctx context.Context, t *issues_model.TrackedTime) (apiT *api.TrackedTime) {
   136  	apiT = &api.TrackedTime{
   137  		ID:      t.ID,
   138  		IssueID: t.IssueID,
   139  		UserID:  t.UserID,
   140  		Time:    t.Time,
   141  		Created: t.Created,
   142  	}
   143  	if t.Issue != nil {
   144  		apiT.Issue = ToAPIIssue(ctx, t.Issue)
   145  	}
   146  	if t.User != nil {
   147  		apiT.UserName = t.User.Name
   148  	}
   149  	return apiT
   150  }
   151  
   152  // ToStopWatches convert Stopwatch list to api.StopWatches
   153  func ToStopWatches(sws []*issues_model.Stopwatch) (api.StopWatches, error) {
   154  	result := api.StopWatches(make([]api.StopWatch, 0, len(sws)))
   155  
   156  	issueCache := make(map[int64]*issues_model.Issue)
   157  	repoCache := make(map[int64]*repo_model.Repository)
   158  	var (
   159  		issue *issues_model.Issue
   160  		repo  *repo_model.Repository
   161  		ok    bool
   162  		err   error
   163  	)
   164  
   165  	for _, sw := range sws {
   166  		issue, ok = issueCache[sw.IssueID]
   167  		if !ok {
   168  			issue, err = issues_model.GetIssueByID(db.DefaultContext, sw.IssueID)
   169  			if err != nil {
   170  				return nil, err
   171  			}
   172  		}
   173  		repo, ok = repoCache[issue.RepoID]
   174  		if !ok {
   175  			repo, err = repo_model.GetRepositoryByID(db.DefaultContext, issue.RepoID)
   176  			if err != nil {
   177  				return nil, err
   178  			}
   179  		}
   180  
   181  		result = append(result, api.StopWatch{
   182  			Created:       sw.CreatedUnix.AsTime(),
   183  			Seconds:       sw.Seconds(),
   184  			Duration:      sw.Duration(),
   185  			IssueIndex:    issue.Index,
   186  			IssueTitle:    issue.Title,
   187  			RepoOwnerName: repo.OwnerName,
   188  			RepoName:      repo.Name,
   189  		})
   190  	}
   191  	return result, nil
   192  }
   193  
   194  // ToTrackedTimeList converts TrackedTimeList to API format
   195  func ToTrackedTimeList(ctx context.Context, tl issues_model.TrackedTimeList) api.TrackedTimeList {
   196  	result := make([]*api.TrackedTime, 0, len(tl))
   197  	for _, t := range tl {
   198  		result = append(result, ToTrackedTime(ctx, t))
   199  	}
   200  	return result
   201  }
   202  
   203  // ToLabel converts Label to API format
   204  func ToLabel(label *issues_model.Label, repo *repo_model.Repository, org *user_model.User) *api.Label {
   205  	result := &api.Label{
   206  		ID:          label.ID,
   207  		Name:        label.Name,
   208  		Exclusive:   label.Exclusive,
   209  		Color:       strings.TrimLeft(label.Color, "#"),
   210  		Description: label.Description,
   211  		IsArchived:  label.IsArchived(),
   212  	}
   213  
   214  	// calculate URL
   215  	if label.BelongsToRepo() && repo != nil {
   216  		if repo != nil {
   217  			result.URL = fmt.Sprintf("%s/labels/%d", repo.APIURL(), label.ID)
   218  		} else {
   219  			log.Error("ToLabel did not get repo to calculate url for label with id '%d'", label.ID)
   220  		}
   221  	} else { // BelongsToOrg
   222  		if org != nil {
   223  			result.URL = fmt.Sprintf("%sapi/v1/orgs/%s/labels/%d", setting.AppURL, url.PathEscape(org.Name), label.ID)
   224  		} else {
   225  			log.Error("ToLabel did not get org to calculate url for label with id '%d'", label.ID)
   226  		}
   227  	}
   228  
   229  	return result
   230  }
   231  
   232  // ToLabelList converts list of Label to API format
   233  func ToLabelList(labels []*issues_model.Label, repo *repo_model.Repository, org *user_model.User) []*api.Label {
   234  	result := make([]*api.Label, len(labels))
   235  	for i := range labels {
   236  		result[i] = ToLabel(labels[i], repo, org)
   237  	}
   238  	return result
   239  }
   240  
   241  // ToAPIMilestone converts Milestone into API Format
   242  func ToAPIMilestone(m *issues_model.Milestone) *api.Milestone {
   243  	apiMilestone := &api.Milestone{
   244  		ID:           m.ID,
   245  		State:        m.State(),
   246  		Title:        m.Name,
   247  		Description:  m.Content,
   248  		OpenIssues:   m.NumOpenIssues,
   249  		ClosedIssues: m.NumClosedIssues,
   250  		Created:      m.CreatedUnix.AsTime(),
   251  		Updated:      m.UpdatedUnix.AsTimePtr(),
   252  	}
   253  	if m.IsClosed {
   254  		apiMilestone.Closed = m.ClosedDateUnix.AsTimePtr()
   255  	}
   256  	if m.DeadlineUnix.Year() < 9999 {
   257  		apiMilestone.Deadline = m.DeadlineUnix.AsTimePtr()
   258  	}
   259  	return apiMilestone
   260  }
   261  
   262  // ToLabelTemplate converts Label to API format
   263  func ToLabelTemplate(label *label.Label) *api.LabelTemplate {
   264  	result := &api.LabelTemplate{
   265  		Name:        label.Name,
   266  		Exclusive:   label.Exclusive,
   267  		Color:       strings.TrimLeft(label.Color, "#"),
   268  		Description: label.Description,
   269  	}
   270  
   271  	return result
   272  }
   273  
   274  // ToLabelTemplateList converts list of Label to API format
   275  func ToLabelTemplateList(labels []*label.Label) []*api.LabelTemplate {
   276  	result := make([]*api.LabelTemplate, len(labels))
   277  	for i := range labels {
   278  		result[i] = ToLabelTemplate(labels[i])
   279  	}
   280  	return result
   281  }