mirror of
https://gh-proxy.org/https://github.com/klinsmaya/multica.git
synced 2026-11-09 09:56:47 +08:00
* feat(pr): only a closing keyword completes an issue on merge (MUL-7672) #8758 made every linked PR count toward auto-complete, so a title-only "MUL-123: ..." PR moved its issue to Done on merge. Restore the earlier contract: a PR completes its issue only when its title or body puts a closing keyword (Closes/Fixes/Resolves) right before the identifier. - Body closing keywords link again; a bare body mention still links nothing. - close_intent is recorded on the link and follows the PR text until the merge/close event, then freezes; a link first made after merge carries none. - The decision requires every linked PR merged and at least one with close intent; a new no_close_intent state explains why a merge won't complete. - Everything else from #8758 stays: manual link/remove, per-issue and workspace switches, edge-triggered evaluation, the result line. - Docs (4 languages), UI copy (5 languages) and the multica-platform skill describe the keyword rule again. Co-authored-by: multica-agent <[email protected]> * fix(pr): sync close intent on every link of the PR (MUL-7672) Review of #8794 found two ways a withdrawn closing keyword still completed the issue: - close_intent was refreshed only for identifiers the PR still claimed, so a manual link, or an automatic link whose merge event (carrying the final text) arrived before the edit event, kept a stale true. - the refresh sat behind the auto-link toggle, so turning auto-link off froze whatever intent was recorded. Replace the per-link update with one PR-wide sync: until the merge/close event, every link of the PR (automatic or manual) gets close_intent exactly when the current title/body closes its issue. It runs whenever GitHub features are on, independent of auto-link, which now only decides which links are created. Co-authored-by: multica-agent <[email protected]> * fix(pr): decide close intent apart from auto-link ownership (MUL-7672) Re-review of #8794: the PR-wide close intent sync reused the auto-link verdict, and resolvePRLinkPolicy skips workspaces with auto-link off. With an installation bound to several workspaces, turning auto-link off in one of them therefore cleared a closing keyword only that workspace resolves, and the merge no longer completed the issue. The policy now reads every bound workspace with GitHub on, and records the identifier's sole resolver next to the auto-linking owner. Links still follow the owner (auto-link off workspaces are not competing claimants); close intent is permitted for the owner or the sole resolver, so an identifier that resolves in more than one workspace stays withheld. Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: multica-agent <[email protected]>
786 lines
27 KiB
Go
786 lines
27 KiB
Go
// Code generated by sqlc. DO NOT EDIT.
|
|
// versions:
|
|
// sqlc v1.31.1
|
|
// source: vcs.sql
|
|
|
|
package db
|
|
|
|
import (
|
|
"context"
|
|
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
)
|
|
|
|
const deleteVCSConnection = `-- name: DeleteVCSConnection :exec
|
|
WITH target AS (
|
|
SELECT vcs_connection.id FROM vcs_connection WHERE vcs_connection.id = $1 AND vcs_connection.workspace_id = $2
|
|
),
|
|
cleared_links AS (
|
|
DELETE FROM issue_vcs_pull_request
|
|
WHERE pull_request_id IN (
|
|
SELECT vcs_pull_request.id FROM vcs_pull_request
|
|
WHERE vcs_pull_request.connection_id IN (SELECT target.id FROM target)
|
|
)
|
|
),
|
|
cleared_statuses AS (
|
|
DELETE FROM vcs_commit_status WHERE connection_id IN (SELECT target.id FROM target)
|
|
),
|
|
cleared_prs AS (
|
|
DELETE FROM vcs_pull_request WHERE connection_id IN (SELECT target.id FROM target)
|
|
)
|
|
DELETE FROM vcs_connection WHERE vcs_connection.id = $1 AND vcs_connection.workspace_id = $2
|
|
`
|
|
|
|
type DeleteVCSConnectionParams struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
}
|
|
|
|
// These tables carry no FKs, so the cascade that once removed the connection's
|
|
// mirrored PRs, their issue links, and CI statuses is gone (migration 213). Do
|
|
// that cleanup explicitly here, in one statement so it commits or rolls back
|
|
// atomically with the connection row. The target CTE also scopes every child
|
|
// delete to a connection that actually belongs to the workspace, so a wrong
|
|
// workspace_id is a no-op rather than deleting another tenant's child rows.
|
|
func (q *Queries) DeleteVCSConnection(ctx context.Context, arg DeleteVCSConnectionParams) error {
|
|
_, err := q.db.Exec(ctx, deleteVCSConnection, arg.ID, arg.WorkspaceID)
|
|
return err
|
|
}
|
|
|
|
const findVCSPullRequestByURL = `-- name: FindVCSPullRequestByURL :one
|
|
SELECT id, workspace_id, connection_id, provider, repo_owner, repo_name, pr_number, title, state, html_url, branch, head_sha, author_login, author_avatar_url, merged_at, closed_at, pr_created_at, pr_updated_at, additions, deletions, changed_files, created_at, updated_at FROM vcs_pull_request
|
|
WHERE workspace_id = $1
|
|
AND lower(rtrim(html_url, '/')) = lower($2::text)
|
|
ORDER BY pr_updated_at DESC
|
|
LIMIT 1
|
|
`
|
|
|
|
type FindVCSPullRequestByURLParams struct {
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
HtmlUrl string `json:"html_url"`
|
|
}
|
|
|
|
func (q *Queries) FindVCSPullRequestByURL(ctx context.Context, arg FindVCSPullRequestByURLParams) (VcsPullRequest, error) {
|
|
row := q.db.QueryRow(ctx, findVCSPullRequestByURL, arg.WorkspaceID, arg.HtmlUrl)
|
|
var i VcsPullRequest
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.ConnectionID,
|
|
&i.Provider,
|
|
&i.RepoOwner,
|
|
&i.RepoName,
|
|
&i.PrNumber,
|
|
&i.Title,
|
|
&i.State,
|
|
&i.HtmlUrl,
|
|
&i.Branch,
|
|
&i.HeadSha,
|
|
&i.AuthorLogin,
|
|
&i.AuthorAvatarUrl,
|
|
&i.MergedAt,
|
|
&i.ClosedAt,
|
|
&i.PrCreatedAt,
|
|
&i.PrUpdatedAt,
|
|
&i.Additions,
|
|
&i.Deletions,
|
|
&i.ChangedFiles,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const getVCSConnectionByID = `-- name: GetVCSConnectionByID :one
|
|
SELECT id, workspace_id, provider, instance_url, account_login, access_token_encrypted, webhook_secret_encrypted, connected_by_id, created_at, updated_at FROM vcs_connection
|
|
WHERE id = $1
|
|
`
|
|
|
|
func (q *Queries) GetVCSConnectionByID(ctx context.Context, id pgtype.UUID) (VcsConnection, error) {
|
|
row := q.db.QueryRow(ctx, getVCSConnectionByID, id)
|
|
var i VcsConnection
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.Provider,
|
|
&i.InstanceUrl,
|
|
&i.AccountLogin,
|
|
&i.AccessTokenEncrypted,
|
|
&i.WebhookSecretEncrypted,
|
|
&i.ConnectedByID,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const getVCSPullRequestByKey = `-- name: GetVCSPullRequestByKey :one
|
|
SELECT id, workspace_id, connection_id, provider, repo_owner, repo_name, pr_number, title, state, html_url, branch, head_sha, author_login, author_avatar_url, merged_at, closed_at, pr_created_at, pr_updated_at, additions, deletions, changed_files, created_at, updated_at FROM vcs_pull_request
|
|
WHERE connection_id = $1 AND repo_owner = $2 AND repo_name = $3 AND pr_number = $4
|
|
`
|
|
|
|
type GetVCSPullRequestByKeyParams struct {
|
|
ConnectionID pgtype.UUID `json:"connection_id"`
|
|
RepoOwner string `json:"repo_owner"`
|
|
RepoName string `json:"repo_name"`
|
|
PrNumber int32 `json:"pr_number"`
|
|
}
|
|
|
|
// The stored row before a webhook upsert, so the handler can tell a merge
|
|
// transition apart from a later event on an already-merged PR.
|
|
func (q *Queries) GetVCSPullRequestByKey(ctx context.Context, arg GetVCSPullRequestByKeyParams) (VcsPullRequest, error) {
|
|
row := q.db.QueryRow(ctx, getVCSPullRequestByKey,
|
|
arg.ConnectionID,
|
|
arg.RepoOwner,
|
|
arg.RepoName,
|
|
arg.PrNumber,
|
|
)
|
|
var i VcsPullRequest
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.ConnectionID,
|
|
&i.Provider,
|
|
&i.RepoOwner,
|
|
&i.RepoName,
|
|
&i.PrNumber,
|
|
&i.Title,
|
|
&i.State,
|
|
&i.HtmlUrl,
|
|
&i.Branch,
|
|
&i.HeadSha,
|
|
&i.AuthorLogin,
|
|
&i.AuthorAvatarUrl,
|
|
&i.MergedAt,
|
|
&i.ClosedAt,
|
|
&i.PrCreatedAt,
|
|
&i.PrUpdatedAt,
|
|
&i.Additions,
|
|
&i.Deletions,
|
|
&i.ChangedFiles,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const getVCSPullRequestInWorkspace = `-- name: GetVCSPullRequestInWorkspace :one
|
|
SELECT id, workspace_id, connection_id, provider, repo_owner, repo_name, pr_number, title, state, html_url, branch, head_sha, author_login, author_avatar_url, merged_at, closed_at, pr_created_at, pr_updated_at, additions, deletions, changed_files, created_at, updated_at FROM vcs_pull_request
|
|
WHERE id = $1 AND workspace_id = $2
|
|
`
|
|
|
|
type GetVCSPullRequestInWorkspaceParams struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
}
|
|
|
|
func (q *Queries) GetVCSPullRequestInWorkspace(ctx context.Context, arg GetVCSPullRequestInWorkspaceParams) (VcsPullRequest, error) {
|
|
row := q.db.QueryRow(ctx, getVCSPullRequestInWorkspace, arg.ID, arg.WorkspaceID)
|
|
var i VcsPullRequest
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.ConnectionID,
|
|
&i.Provider,
|
|
&i.RepoOwner,
|
|
&i.RepoName,
|
|
&i.PrNumber,
|
|
&i.Title,
|
|
&i.State,
|
|
&i.HtmlUrl,
|
|
&i.Branch,
|
|
&i.HeadSha,
|
|
&i.AuthorLogin,
|
|
&i.AuthorAvatarUrl,
|
|
&i.MergedAt,
|
|
&i.ClosedAt,
|
|
&i.PrCreatedAt,
|
|
&i.PrUpdatedAt,
|
|
&i.Additions,
|
|
&i.Deletions,
|
|
&i.ChangedFiles,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const linkIssueToVCSPullRequest = `-- name: LinkIssueToVCSPullRequest :execrows
|
|
|
|
INSERT INTO issue_vcs_pull_request (
|
|
issue_id, pull_request_id, linked_by_type, linked_by_id
|
|
) VALUES (
|
|
$1, $2, 'system', NULL
|
|
)
|
|
ON CONFLICT (issue_id, pull_request_id) DO NOTHING
|
|
`
|
|
|
|
type LinkIssueToVCSPullRequestParams struct {
|
|
IssueID pgtype.UUID `json:"issue_id"`
|
|
PullRequestID pgtype.UUID `json:"pull_request_id"`
|
|
}
|
|
|
|
// =====================
|
|
// Issue ↔ VCS PR link
|
|
// =====================
|
|
// Mirrors LinkIssueToPullRequest: automatic link, 1 only when new.
|
|
func (q *Queries) LinkIssueToVCSPullRequest(ctx context.Context, arg LinkIssueToVCSPullRequestParams) (int64, error) {
|
|
result, err := q.db.Exec(ctx, linkIssueToVCSPullRequest, arg.IssueID, arg.PullRequestID)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return result.RowsAffected(), nil
|
|
}
|
|
|
|
const linkIssueToVCSPullRequestManually = `-- name: LinkIssueToVCSPullRequestManually :execrows
|
|
INSERT INTO issue_vcs_pull_request (
|
|
issue_id, pull_request_id, linked_by_type, linked_by_id
|
|
) VALUES (
|
|
$1, $2, 'member', $3
|
|
)
|
|
ON CONFLICT (issue_id, pull_request_id) DO UPDATE SET
|
|
linked_by_type = 'member',
|
|
linked_by_id = EXCLUDED.linked_by_id
|
|
WHERE issue_vcs_pull_request.linked_by_type IS DISTINCT FROM 'member'
|
|
`
|
|
|
|
type LinkIssueToVCSPullRequestManuallyParams struct {
|
|
IssueID pgtype.UUID `json:"issue_id"`
|
|
PullRequestID pgtype.UUID `json:"pull_request_id"`
|
|
LinkedByID pgtype.UUID `json:"linked_by_id"`
|
|
}
|
|
|
|
func (q *Queries) LinkIssueToVCSPullRequestManually(ctx context.Context, arg LinkIssueToVCSPullRequestManuallyParams) (int64, error) {
|
|
result, err := q.db.Exec(ctx, linkIssueToVCSPullRequestManually, arg.IssueID, arg.PullRequestID, arg.LinkedByID)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return result.RowsAffected(), nil
|
|
}
|
|
|
|
const listAutoLinkedIssueIDsForVCSPullRequest = `-- name: ListAutoLinkedIssueIDsForVCSPullRequest :many
|
|
SELECT issue_id FROM issue_vcs_pull_request
|
|
WHERE pull_request_id = $1
|
|
AND COALESCE(linked_by_type, 'system') <> 'member'
|
|
`
|
|
|
|
func (q *Queries) ListAutoLinkedIssueIDsForVCSPullRequest(ctx context.Context, pullRequestID pgtype.UUID) ([]pgtype.UUID, error) {
|
|
rows, err := q.db.Query(ctx, listAutoLinkedIssueIDsForVCSPullRequest, pullRequestID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []pgtype.UUID{}
|
|
for rows.Next() {
|
|
var issue_id pgtype.UUID
|
|
if err := rows.Scan(&issue_id); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, issue_id)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listIssueIDsForVCSPRHead = `-- name: ListIssueIDsForVCSPRHead :many
|
|
SELECT DISTINCT ipr.issue_id
|
|
FROM vcs_pull_request pr
|
|
JOIN issue_vcs_pull_request ipr ON ipr.pull_request_id = pr.id
|
|
WHERE pr.connection_id = $1 AND pr.head_sha = $2 AND pr.head_sha <> ''
|
|
`
|
|
|
|
type ListIssueIDsForVCSPRHeadParams struct {
|
|
ConnectionID pgtype.UUID `json:"connection_id"`
|
|
HeadSha string `json:"head_sha"`
|
|
}
|
|
|
|
// Issues linked to any PR whose head sha matches the given status, so a
|
|
// commit-status event can fan out a PR-card refresh to the right issues.
|
|
func (q *Queries) ListIssueIDsForVCSPRHead(ctx context.Context, arg ListIssueIDsForVCSPRHeadParams) ([]pgtype.UUID, error) {
|
|
rows, err := q.db.Query(ctx, listIssueIDsForVCSPRHead, arg.ConnectionID, arg.HeadSha)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []pgtype.UUID{}
|
|
for rows.Next() {
|
|
var issue_id pgtype.UUID
|
|
if err := rows.Scan(&issue_id); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, issue_id)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listIssueIDsForVCSPullRequest = `-- name: ListIssueIDsForVCSPullRequest :many
|
|
SELECT issue_id FROM issue_vcs_pull_request
|
|
WHERE pull_request_id = $1
|
|
`
|
|
|
|
func (q *Queries) ListIssueIDsForVCSPullRequest(ctx context.Context, pullRequestID pgtype.UUID) ([]pgtype.UUID, error) {
|
|
rows, err := q.db.Query(ctx, listIssueIDsForVCSPullRequest, pullRequestID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []pgtype.UUID{}
|
|
for rows.Next() {
|
|
var issue_id pgtype.UUID
|
|
if err := rows.Scan(&issue_id); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, issue_id)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listVCSConnectionsByWorkspace = `-- name: ListVCSConnectionsByWorkspace :many
|
|
|
|
SELECT id, workspace_id, provider, instance_url, account_login, access_token_encrypted, webhook_secret_encrypted, connected_by_id, created_at, updated_at FROM vcs_connection
|
|
WHERE workspace_id = $1
|
|
ORDER BY created_at ASC
|
|
`
|
|
|
|
// =====================
|
|
// VCS Connection (Forgejo / Gitea / GitLab)
|
|
// =====================
|
|
func (q *Queries) ListVCSConnectionsByWorkspace(ctx context.Context, workspaceID pgtype.UUID) ([]VcsConnection, error) {
|
|
rows, err := q.db.Query(ctx, listVCSConnectionsByWorkspace, workspaceID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []VcsConnection{}
|
|
for rows.Next() {
|
|
var i VcsConnection
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.Provider,
|
|
&i.InstanceUrl,
|
|
&i.AccountLogin,
|
|
&i.AccessTokenEncrypted,
|
|
&i.WebhookSecretEncrypted,
|
|
&i.ConnectedByID,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listVCSPullRequestsByIssue = `-- name: ListVCSPullRequestsByIssue :many
|
|
WITH checks AS (
|
|
SELECT
|
|
pr.id AS pr_id,
|
|
COUNT(*)::bigint AS total,
|
|
SUM(CASE WHEN cs.state = 'failed' THEN 1 ELSE 0 END)::bigint AS failed,
|
|
SUM(CASE WHEN cs.state = 'passed' THEN 1 ELSE 0 END)::bigint AS passed,
|
|
SUM(CASE WHEN cs.state = 'pending' THEN 1 ELSE 0 END)::bigint AS pending
|
|
FROM vcs_pull_request pr
|
|
JOIN issue_vcs_pull_request ipr ON ipr.pull_request_id = pr.id
|
|
JOIN vcs_commit_status cs
|
|
ON cs.connection_id = pr.connection_id
|
|
AND cs.sha = pr.head_sha
|
|
AND pr.head_sha <> ''
|
|
WHERE ipr.issue_id = $1
|
|
GROUP BY pr.id
|
|
)
|
|
SELECT
|
|
pr.id, pr.workspace_id, pr.connection_id, pr.provider, pr.repo_owner, pr.repo_name, pr.pr_number, pr.title, pr.state, pr.html_url, pr.branch, pr.head_sha, pr.author_login, pr.author_avatar_url, pr.merged_at, pr.closed_at, pr.pr_created_at, pr.pr_updated_at, pr.additions, pr.deletions, pr.changed_files, pr.created_at, pr.updated_at,
|
|
COALESCE(ipr.linked_by_type, 'system')::text AS linked_by_type,
|
|
COALESCE(c.total, 0)::bigint AS checks_total,
|
|
COALESCE(c.passed, 0)::bigint AS checks_passed,
|
|
COALESCE(c.failed, 0)::bigint AS checks_failed,
|
|
COALESCE(c.pending, 0)::bigint AS checks_pending
|
|
FROM vcs_pull_request pr
|
|
JOIN issue_vcs_pull_request ipr ON ipr.pull_request_id = pr.id
|
|
LEFT JOIN checks c ON c.pr_id = pr.id
|
|
WHERE ipr.issue_id = $1
|
|
ORDER BY pr.pr_created_at DESC
|
|
`
|
|
|
|
type ListVCSPullRequestsByIssueRow struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
ConnectionID pgtype.UUID `json:"connection_id"`
|
|
Provider string `json:"provider"`
|
|
RepoOwner string `json:"repo_owner"`
|
|
RepoName string `json:"repo_name"`
|
|
PrNumber int32 `json:"pr_number"`
|
|
Title string `json:"title"`
|
|
State string `json:"state"`
|
|
HtmlUrl string `json:"html_url"`
|
|
Branch pgtype.Text `json:"branch"`
|
|
HeadSha string `json:"head_sha"`
|
|
AuthorLogin pgtype.Text `json:"author_login"`
|
|
AuthorAvatarUrl pgtype.Text `json:"author_avatar_url"`
|
|
MergedAt pgtype.Timestamptz `json:"merged_at"`
|
|
ClosedAt pgtype.Timestamptz `json:"closed_at"`
|
|
PrCreatedAt pgtype.Timestamptz `json:"pr_created_at"`
|
|
PrUpdatedAt pgtype.Timestamptz `json:"pr_updated_at"`
|
|
Additions int32 `json:"additions"`
|
|
Deletions int32 `json:"deletions"`
|
|
ChangedFiles int32 `json:"changed_files"`
|
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
|
LinkedByType string `json:"linked_by_type"`
|
|
ChecksTotal int64 `json:"checks_total"`
|
|
ChecksPassed int64 `json:"checks_passed"`
|
|
ChecksFailed int64 `json:"checks_failed"`
|
|
ChecksPending int64 `json:"checks_pending"`
|
|
}
|
|
|
|
// Aggregates each PR's commit statuses for its CURRENT head sha into
|
|
// passed/failed/pending counts. vcs_commit_status holds one row per
|
|
// (connection, sha, context) with a normalized state, so a count by state is
|
|
// correct. Statuses for an old head sha stay stored but are excluded by the
|
|
// head_sha join, so a stale run can't pollute the bar.
|
|
func (q *Queries) ListVCSPullRequestsByIssue(ctx context.Context, issueID pgtype.UUID) ([]ListVCSPullRequestsByIssueRow, error) {
|
|
rows, err := q.db.Query(ctx, listVCSPullRequestsByIssue, issueID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []ListVCSPullRequestsByIssueRow{}
|
|
for rows.Next() {
|
|
var i ListVCSPullRequestsByIssueRow
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.ConnectionID,
|
|
&i.Provider,
|
|
&i.RepoOwner,
|
|
&i.RepoName,
|
|
&i.PrNumber,
|
|
&i.Title,
|
|
&i.State,
|
|
&i.HtmlUrl,
|
|
&i.Branch,
|
|
&i.HeadSha,
|
|
&i.AuthorLogin,
|
|
&i.AuthorAvatarUrl,
|
|
&i.MergedAt,
|
|
&i.ClosedAt,
|
|
&i.PrCreatedAt,
|
|
&i.PrUpdatedAt,
|
|
&i.Additions,
|
|
&i.Deletions,
|
|
&i.ChangedFiles,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.LinkedByType,
|
|
&i.ChecksTotal,
|
|
&i.ChecksPassed,
|
|
&i.ChecksFailed,
|
|
&i.ChecksPending,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const rotateVCSConnectionWebhookSecret = `-- name: RotateVCSConnectionWebhookSecret :one
|
|
UPDATE vcs_connection
|
|
SET webhook_secret_encrypted = $3,
|
|
updated_at = now()
|
|
WHERE id = $1 AND workspace_id = $2
|
|
RETURNING id, workspace_id, provider, instance_url, account_login, access_token_encrypted, webhook_secret_encrypted, connected_by_id, created_at, updated_at
|
|
`
|
|
|
|
type RotateVCSConnectionWebhookSecretParams struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
WebhookSecretEncrypted string `json:"webhook_secret_encrypted"`
|
|
}
|
|
|
|
func (q *Queries) RotateVCSConnectionWebhookSecret(ctx context.Context, arg RotateVCSConnectionWebhookSecretParams) (VcsConnection, error) {
|
|
row := q.db.QueryRow(ctx, rotateVCSConnectionWebhookSecret, arg.ID, arg.WorkspaceID, arg.WebhookSecretEncrypted)
|
|
var i VcsConnection
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.Provider,
|
|
&i.InstanceUrl,
|
|
&i.AccountLogin,
|
|
&i.AccessTokenEncrypted,
|
|
&i.WebhookSecretEncrypted,
|
|
&i.ConnectedByID,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const syncVCSPullRequestCloseIntent = `-- name: SyncVCSPullRequestCloseIntent :exec
|
|
UPDATE issue_vcs_pull_request
|
|
SET close_intent = (issue_id = ANY($1::uuid[]))
|
|
WHERE pull_request_id = $2
|
|
AND close_intent <> (issue_id = ANY($1::uuid[]))
|
|
`
|
|
|
|
type SyncVCSPullRequestCloseIntentParams struct {
|
|
ClosingIssueIds []pgtype.UUID `json:"closing_issue_ids"`
|
|
PullRequestID pgtype.UUID `json:"pull_request_id"`
|
|
}
|
|
|
|
// Mirrors SyncPullRequestCloseIntent.
|
|
func (q *Queries) SyncVCSPullRequestCloseIntent(ctx context.Context, arg SyncVCSPullRequestCloseIntentParams) error {
|
|
_, err := q.db.Exec(ctx, syncVCSPullRequestCloseIntent, arg.ClosingIssueIds, arg.PullRequestID)
|
|
return err
|
|
}
|
|
|
|
const unlinkIssueFromVCSPullRequest = `-- name: UnlinkIssueFromVCSPullRequest :execrows
|
|
DELETE FROM issue_vcs_pull_request
|
|
WHERE issue_id = $1 AND pull_request_id = $2
|
|
`
|
|
|
|
type UnlinkIssueFromVCSPullRequestParams struct {
|
|
IssueID pgtype.UUID `json:"issue_id"`
|
|
PullRequestID pgtype.UUID `json:"pull_request_id"`
|
|
}
|
|
|
|
func (q *Queries) UnlinkIssueFromVCSPullRequest(ctx context.Context, arg UnlinkIssueFromVCSPullRequestParams) (int64, error) {
|
|
result, err := q.db.Exec(ctx, unlinkIssueFromVCSPullRequest, arg.IssueID, arg.PullRequestID)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return result.RowsAffected(), nil
|
|
}
|
|
|
|
const upsertVCSCommitStatus = `-- name: UpsertVCSCommitStatus :exec
|
|
|
|
INSERT INTO vcs_commit_status (
|
|
connection_id, sha, context, state, target_url, description, updated_at
|
|
) VALUES (
|
|
$1, $2, $3, $4, $6, $7, $5
|
|
)
|
|
ON CONFLICT (connection_id, sha, context) DO UPDATE SET
|
|
state = EXCLUDED.state,
|
|
target_url = EXCLUDED.target_url,
|
|
description = EXCLUDED.description,
|
|
updated_at = EXCLUDED.updated_at
|
|
WHERE EXCLUDED.updated_at >= vcs_commit_status.updated_at
|
|
`
|
|
|
|
type UpsertVCSCommitStatusParams struct {
|
|
ConnectionID pgtype.UUID `json:"connection_id"`
|
|
Sha string `json:"sha"`
|
|
Context string `json:"context"`
|
|
State string `json:"state"`
|
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
|
TargetUrl pgtype.Text `json:"target_url"`
|
|
Description pgtype.Text `json:"description"`
|
|
}
|
|
|
|
// =====================
|
|
// VCS commit status (CI)
|
|
// =====================
|
|
// One row per (connection, sha, context); a redelivery or state transition
|
|
// overwrites in place. updated_at guards against an older event overwriting a
|
|
// newer one for the same context. state is normalized (passed/failed/pending).
|
|
func (q *Queries) UpsertVCSCommitStatus(ctx context.Context, arg UpsertVCSCommitStatusParams) error {
|
|
_, err := q.db.Exec(ctx, upsertVCSCommitStatus,
|
|
arg.ConnectionID,
|
|
arg.Sha,
|
|
arg.Context,
|
|
arg.State,
|
|
arg.UpdatedAt,
|
|
arg.TargetUrl,
|
|
arg.Description,
|
|
)
|
|
return err
|
|
}
|
|
|
|
const upsertVCSConnection = `-- name: UpsertVCSConnection :one
|
|
INSERT INTO vcs_connection (
|
|
workspace_id, provider, instance_url, account_login,
|
|
access_token_encrypted, webhook_secret_encrypted, connected_by_id
|
|
) VALUES (
|
|
$1, $2, $3, $4, $5, $6, $7
|
|
)
|
|
ON CONFLICT (workspace_id, instance_url) DO UPDATE SET
|
|
provider = EXCLUDED.provider,
|
|
account_login = EXCLUDED.account_login,
|
|
access_token_encrypted = EXCLUDED.access_token_encrypted,
|
|
webhook_secret_encrypted = EXCLUDED.webhook_secret_encrypted,
|
|
connected_by_id = EXCLUDED.connected_by_id,
|
|
updated_at = now()
|
|
RETURNING id, workspace_id, provider, instance_url, account_login, access_token_encrypted, webhook_secret_encrypted, connected_by_id, created_at, updated_at
|
|
`
|
|
|
|
type UpsertVCSConnectionParams struct {
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
Provider string `json:"provider"`
|
|
InstanceUrl string `json:"instance_url"`
|
|
AccountLogin string `json:"account_login"`
|
|
AccessTokenEncrypted string `json:"access_token_encrypted"`
|
|
WebhookSecretEncrypted string `json:"webhook_secret_encrypted"`
|
|
ConnectedByID pgtype.UUID `json:"connected_by_id"`
|
|
}
|
|
|
|
// Reconnecting the same instance rotates the stored token/secret, provider,
|
|
// and identity in place rather than creating a duplicate row.
|
|
func (q *Queries) UpsertVCSConnection(ctx context.Context, arg UpsertVCSConnectionParams) (VcsConnection, error) {
|
|
row := q.db.QueryRow(ctx, upsertVCSConnection,
|
|
arg.WorkspaceID,
|
|
arg.Provider,
|
|
arg.InstanceUrl,
|
|
arg.AccountLogin,
|
|
arg.AccessTokenEncrypted,
|
|
arg.WebhookSecretEncrypted,
|
|
arg.ConnectedByID,
|
|
)
|
|
var i VcsConnection
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.Provider,
|
|
&i.InstanceUrl,
|
|
&i.AccountLogin,
|
|
&i.AccessTokenEncrypted,
|
|
&i.WebhookSecretEncrypted,
|
|
&i.ConnectedByID,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const upsertVCSPullRequest = `-- name: UpsertVCSPullRequest :one
|
|
|
|
INSERT INTO vcs_pull_request (
|
|
workspace_id, connection_id, provider, repo_owner, repo_name, pr_number,
|
|
title, state, html_url, branch, author_login, author_avatar_url,
|
|
merged_at, closed_at, pr_created_at, pr_updated_at,
|
|
additions, deletions, changed_files, head_sha
|
|
) VALUES (
|
|
$1, $2, $3, $4, $5, $6,
|
|
$7, $8, $9, $16, $17, $18,
|
|
$19, $20, $10, $11,
|
|
$12, $13, $14, $15
|
|
)
|
|
ON CONFLICT (connection_id, repo_owner, repo_name, pr_number) DO UPDATE SET
|
|
workspace_id = CASE WHEN EXCLUDED.pr_updated_at >= vcs_pull_request.pr_updated_at THEN EXCLUDED.workspace_id ELSE vcs_pull_request.workspace_id END,
|
|
provider = CASE WHEN EXCLUDED.pr_updated_at >= vcs_pull_request.pr_updated_at THEN EXCLUDED.provider ELSE vcs_pull_request.provider END,
|
|
title = CASE WHEN EXCLUDED.pr_updated_at >= vcs_pull_request.pr_updated_at THEN EXCLUDED.title ELSE vcs_pull_request.title END,
|
|
state = CASE WHEN EXCLUDED.pr_updated_at >= vcs_pull_request.pr_updated_at THEN EXCLUDED.state ELSE vcs_pull_request.state END,
|
|
html_url = CASE WHEN EXCLUDED.pr_updated_at >= vcs_pull_request.pr_updated_at THEN EXCLUDED.html_url ELSE vcs_pull_request.html_url END,
|
|
branch = CASE WHEN EXCLUDED.pr_updated_at >= vcs_pull_request.pr_updated_at THEN EXCLUDED.branch ELSE vcs_pull_request.branch END,
|
|
author_login = CASE WHEN EXCLUDED.pr_updated_at >= vcs_pull_request.pr_updated_at THEN EXCLUDED.author_login ELSE vcs_pull_request.author_login END,
|
|
author_avatar_url = CASE WHEN EXCLUDED.pr_updated_at >= vcs_pull_request.pr_updated_at THEN EXCLUDED.author_avatar_url ELSE vcs_pull_request.author_avatar_url END,
|
|
merged_at = CASE WHEN EXCLUDED.pr_updated_at >= vcs_pull_request.pr_updated_at THEN EXCLUDED.merged_at ELSE vcs_pull_request.merged_at END,
|
|
closed_at = CASE WHEN EXCLUDED.pr_updated_at >= vcs_pull_request.pr_updated_at THEN EXCLUDED.closed_at ELSE vcs_pull_request.closed_at END,
|
|
pr_updated_at = CASE WHEN EXCLUDED.pr_updated_at >= vcs_pull_request.pr_updated_at THEN EXCLUDED.pr_updated_at ELSE vcs_pull_request.pr_updated_at END,
|
|
additions = CASE WHEN EXCLUDED.pr_updated_at >= vcs_pull_request.pr_updated_at THEN EXCLUDED.additions ELSE vcs_pull_request.additions END,
|
|
deletions = CASE WHEN EXCLUDED.pr_updated_at >= vcs_pull_request.pr_updated_at THEN EXCLUDED.deletions ELSE vcs_pull_request.deletions END,
|
|
changed_files = CASE WHEN EXCLUDED.pr_updated_at >= vcs_pull_request.pr_updated_at THEN EXCLUDED.changed_files ELSE vcs_pull_request.changed_files END,
|
|
head_sha = CASE WHEN EXCLUDED.pr_updated_at >= vcs_pull_request.pr_updated_at THEN EXCLUDED.head_sha ELSE vcs_pull_request.head_sha END,
|
|
updated_at = now()
|
|
RETURNING id, workspace_id, connection_id, provider, repo_owner, repo_name, pr_number, title, state, html_url, branch, head_sha, author_login, author_avatar_url, merged_at, closed_at, pr_created_at, pr_updated_at, additions, deletions, changed_files, created_at, updated_at
|
|
`
|
|
|
|
type UpsertVCSPullRequestParams struct {
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
ConnectionID pgtype.UUID `json:"connection_id"`
|
|
Provider string `json:"provider"`
|
|
RepoOwner string `json:"repo_owner"`
|
|
RepoName string `json:"repo_name"`
|
|
PrNumber int32 `json:"pr_number"`
|
|
Title string `json:"title"`
|
|
State string `json:"state"`
|
|
HtmlUrl string `json:"html_url"`
|
|
PrCreatedAt pgtype.Timestamptz `json:"pr_created_at"`
|
|
PrUpdatedAt pgtype.Timestamptz `json:"pr_updated_at"`
|
|
Additions int32 `json:"additions"`
|
|
Deletions int32 `json:"deletions"`
|
|
ChangedFiles int32 `json:"changed_files"`
|
|
HeadSha string `json:"head_sha"`
|
|
Branch pgtype.Text `json:"branch"`
|
|
AuthorLogin pgtype.Text `json:"author_login"`
|
|
AuthorAvatarUrl pgtype.Text `json:"author_avatar_url"`
|
|
MergedAt pgtype.Timestamptz `json:"merged_at"`
|
|
ClosedAt pgtype.Timestamptz `json:"closed_at"`
|
|
}
|
|
|
|
// =====================
|
|
// VCS Pull Request
|
|
// =====================
|
|
// pr_updated_at guards against an out-of-order webhook redelivery regressing
|
|
// the PR state, mirroring the updated_at guard on UpsertVCSCommitStatus. Each
|
|
// mutable column is applied only when the incoming event is at least as new as
|
|
// the stored row; a stale event keeps the existing values. The row is still
|
|
// touched (and thus RETURNED) either way, so callers always get the current PR
|
|
// — the webhook needs pr.id to link the issue even on a stale redelivery.
|
|
func (q *Queries) UpsertVCSPullRequest(ctx context.Context, arg UpsertVCSPullRequestParams) (VcsPullRequest, error) {
|
|
row := q.db.QueryRow(ctx, upsertVCSPullRequest,
|
|
arg.WorkspaceID,
|
|
arg.ConnectionID,
|
|
arg.Provider,
|
|
arg.RepoOwner,
|
|
arg.RepoName,
|
|
arg.PrNumber,
|
|
arg.Title,
|
|
arg.State,
|
|
arg.HtmlUrl,
|
|
arg.PrCreatedAt,
|
|
arg.PrUpdatedAt,
|
|
arg.Additions,
|
|
arg.Deletions,
|
|
arg.ChangedFiles,
|
|
arg.HeadSha,
|
|
arg.Branch,
|
|
arg.AuthorLogin,
|
|
arg.AuthorAvatarUrl,
|
|
arg.MergedAt,
|
|
arg.ClosedAt,
|
|
)
|
|
var i VcsPullRequest
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.ConnectionID,
|
|
&i.Provider,
|
|
&i.RepoOwner,
|
|
&i.RepoName,
|
|
&i.PrNumber,
|
|
&i.Title,
|
|
&i.State,
|
|
&i.HtmlUrl,
|
|
&i.Branch,
|
|
&i.HeadSha,
|
|
&i.AuthorLogin,
|
|
&i.AuthorAvatarUrl,
|
|
&i.MergedAt,
|
|
&i.ClosedAt,
|
|
&i.PrCreatedAt,
|
|
&i.PrUpdatedAt,
|
|
&i.Additions,
|
|
&i.Deletions,
|
|
&i.ChangedFiles,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
)
|
|
return i, err
|
|
}
|