UpdateIssue updates the issue.
(ctx context.Context, req *connect.Request[v1pb.UpdateIssueRequest])
| 969 | |
| 970 | // UpdateIssue updates the issue. |
| 971 | func (s *IssueService) UpdateIssue(ctx context.Context, req *connect.Request[v1pb.UpdateIssueRequest]) (*connect.Response[v1pb.Issue], error) { |
| 972 | user, ok := GetUserFromContext(ctx) |
| 973 | if !ok { |
| 974 | return nil, connect.NewError(connect.CodeInternal, errors.Errorf("user not found")) |
| 975 | } |
| 976 | if req.Msg.UpdateMask == nil { |
| 977 | return nil, connect.NewError(connect.CodeInvalidArgument, errors.Errorf("update_mask must be set")) |
| 978 | } |
| 979 | |
| 980 | issue, err := s.getIssueMessage(ctx, req.Msg.Issue.Name) |
| 981 | if err != nil { |
| 982 | if connect.CodeOf(err) == connect.CodeNotFound && req.Msg.AllowMissing { |
| 983 | // When allow_missing is true and issue doesn't exist, create a new one |
| 984 | ok, err := s.iamManager.CheckPermission(ctx, permission.IssuesCreate, user, common.GetWorkspaceIDFromContext(ctx)) |
| 985 | if err != nil { |
| 986 | return nil, connect.NewError(connect.CodeInternal, errors.Wrap(err, "failed to check permission")) |
| 987 | } |
| 988 | if !ok { |
| 989 | return nil, connect.NewError(connect.CodePermissionDenied, errors.Errorf("user does not have permission %q", permission.IssuesCreate)) |
| 990 | } |
| 991 | |
| 992 | // Extract project ID from the issue name (format: projects/{project}/issues/{issue}) |
| 993 | parts := strings.Split(req.Msg.Issue.Name, "/") |
| 994 | if len(parts) < 4 || parts[0] != "projects" || parts[2] != "issues" { |
| 995 | return nil, connect.NewError(connect.CodeInvalidArgument, errors.Errorf("invalid issue name format: %q", req.Msg.Issue.Name)) |
| 996 | } |
| 997 | projectID := parts[1] |
| 998 | |
| 999 | return s.CreateIssue(ctx, connect.NewRequest(&v1pb.CreateIssueRequest{ |
| 1000 | Parent: fmt.Sprintf("projects/%s", projectID), |
| 1001 | Issue: req.Msg.Issue, |
| 1002 | })) |
| 1003 | } |
| 1004 | return nil, err |
| 1005 | } |
| 1006 | project, err := s.store.GetProject(ctx, &store.FindProjectMessage{Workspace: common.GetWorkspaceIDFromContext(ctx), ResourceID: &issue.ProjectID}) |
| 1007 | if err != nil { |
| 1008 | return nil, connect.NewError(connect.CodeInternal, errors.Wrapf(err, "failed to get project")) |
| 1009 | } |
| 1010 | if project == nil { |
| 1011 | return nil, connect.NewError(connect.CodeNotFound, errors.Errorf("project %s not found", issue.ProjectID)) |
| 1012 | } |
| 1013 | |
| 1014 | updateMasks := map[string]bool{} |
| 1015 | |
| 1016 | patch := &store.UpdateIssueMessage{} |
| 1017 | var issueCommentCreates []*store.IssueCommentMessage |
| 1018 | for _, path := range req.Msg.UpdateMask.Paths { |
| 1019 | updateMasks[path] = true |
| 1020 | switch path { |
| 1021 | case "title": |
| 1022 | trimmed := strings.TrimSpace(req.Msg.Issue.Title) |
| 1023 | if trimmed == "" { |
| 1024 | return nil, connect.NewError(connect.CodeInvalidArgument, errors.Errorf("title cannot be empty")) |
| 1025 | } |
| 1026 | if trimmed == issue.Title { |
| 1027 | // No-op update — skip both the patch and the audit comment so we |
| 1028 | // don't pollute the timeline with "changed name from X to X". |
nothing calls this directly
no test coverage detected