Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 | 1x 1x 1x 1x 4x 2x 2x 2x 3x 2x 2x 7x 3x 1x 2x 2x 1x 1x 1x 1x 1x 1x 1x 1x 1x 4x 4x 4x 2x 2x | /**
* Resolve `@commit` / `@commit:<repo>@<sha>` specs into a concrete
* `{inCommit: {commit, repository}}` payload.
*
* The bare `@commit` form auto-detects:
* 1. Current git HEAD SHA (must be inside a git work tree)
* 2. Current git origin URL → parse to `owner/repo` via {@link parseRemoteUrl}
* 3. Matching Sentry-registered repo (by `externalSlug` first, then `name`)
*
* The explicit form `@commit:<repo>@<sha>` skips steps 1-2 and goes straight
* to step 3 with the user-provided repo name, but still validates that the
* repo is registered in Sentry (otherwise the API rejects the payload).
*
* Every failure mode raises a `ValidationError` with a concrete remediation
* hint — no silent fallback to another resolution mode. Per the design, a
* half-correct `--in` request is worse than a clear error the user can fix.
*/
import { execFileSync } from "node:child_process";
import type { ResolveCommitSpec } from "../../lib/api/issues.js";
import { listRepositoriesCached } from "../../lib/api-client.js";
import { ValidationError } from "../../lib/errors.js";
import {
getHeadCommit,
isInsideGitWorkTree,
parseRemoteUrl,
} from "../../lib/git.js";
import type { SentryRepository } from "../../types/index.js";
/** Fetch the git origin URL without throwing when it's missing. */
function getGitOriginUrl(cwd: string): string | undefined {
try {
return execFileSync("git", ["remote", "get-url", "origin"], {
cwd,
encoding: "utf8",
stdio: ["ignore", "pipe", "ignore"],
}).trim();
} catch {
return;
}
}
/**
* Find the Sentry repo whose `externalSlug` or `name` matches the local
* `owner/repo` derived from `git remote get-url origin`.
*
* Returns `null` when no match is found — the caller surfaces this as
* a user-facing error with an available-repos list.
*/
function findSentryRepoMatchingOrigin(
originOwnerRepo: string,
sentryRepos: SentryRepository[]
): SentryRepository | null {
// Prefer externalSlug (canonical `owner/repo` from the integration)
// then fall back to `name` for repos without that field populated.
const match =
sentryRepos.find((r) => r.externalSlug === originOwnerRepo) ??
sentryRepos.find((r) => r.name === originOwnerRepo);
return match ?? null;
}
/**
* Find the Sentry repo matching a user-provided repo name exactly.
* Checks `name` first (the canonical identifier used by the API's
* InCommitValidator) and then `externalSlug` for convenience.
*/
function findSentryRepoByName(
repoName: string,
sentryRepos: SentryRepository[]
): SentryRepository | null {
return (
sentryRepos.find((r) => r.name === repoName) ??
sentryRepos.find((r) => r.externalSlug === repoName) ??
null
);
}
/** Format a short list of available repos for error messages. */
function formatAvailableRepos(repos: SentryRepository[]): string {
Iif (repos.length === 0) {
return "No repositories are registered in Sentry for this organization.";
}
const MAX = 10;
const names = repos
.slice(0, MAX)
.map((r) => ` - ${r.name}`)
.join("\n");
const more =
repos.length > MAX
? `\n ... and ${repos.length - MAX} more (sentry repo list <org>/)`
: "";
return `Available repositories in this organization:\n${names}${more}`;
}
/**
* Resolve a {@link ResolveCommitSpec} (either auto-detect or explicit) into
* the concrete `{commit, repository}` payload the Sentry API expects.
*
* @throws {ValidationError} When any step of the resolution fails — no
* fallback to `inRelease` or other modes. The error message always names
* the next action the user can take.
*/
export async function resolveCommitSpec(
spec: ResolveCommitSpec,
orgSlug: string,
cwd: string
): Promise<{ commit: string; repository: string }> {
if (spec.kind === "auto") {
if (!isInsideGitWorkTree(cwd)) {
throw new ValidationError(
"--in @commit requires a git repository. Run from inside a checkout, or use --in @next / --in <version> / --in @commit:<repo>@<sha>.",
"in"
);
}
let headSha: string;
try {
headSha = getHeadCommit(cwd);
} catch {
throw new ValidationError(
"--in @commit could not read HEAD (is this a fresh repo with no commits?). Make a commit first, or pass --in @commit:<repo>@<sha> explicitly.",
"in"
);
}
const originUrl = getGitOriginUrl(cwd);
Iif (!originUrl) {
throw new ValidationError(
"--in @commit could not determine the git 'origin' remote. Add an origin remote, or pass --in @commit:<repo>@<sha> explicitly.",
"in"
);
}
const originOwnerRepo = parseRemoteUrl(originUrl);
Iif (!originOwnerRepo) {
throw new ValidationError(
`--in @commit could not parse the origin URL ('${originUrl}') as 'owner/repo'. Use --in @commit:<repo>@<sha> explicitly.`,
"in"
);
}
const sentryRepos = await listRepositoriesCached(orgSlug);
const match = findSentryRepoMatchingOrigin(originOwnerRepo, sentryRepos);
Iif (!match) {
throw new ValidationError(
`--in @commit: no Sentry repository matches local origin '${originOwnerRepo}' in organization '${orgSlug}'.\n\n` +
`${formatAvailableRepos(sentryRepos)}\n\n` +
"Register the repo in Sentry, or pass --in @commit:<repo>@<sha> with a registered name.",
"in"
);
}
return { commit: headSha, repository: match.name };
}
// Explicit: @commit:<repo>@<sha> — validate the repo is registered.
const sentryRepos = await listRepositoriesCached(orgSlug);
const match = findSentryRepoByName(spec.repository, sentryRepos);
if (!match) {
throw new ValidationError(
`--in @commit:${spec.repository}@${spec.commit}: no Sentry repository named '${spec.repository}' in organization '${orgSlug}'.\n\n` +
`${formatAvailableRepos(sentryRepos)}`,
"in"
);
}
return { commit: spec.commit, repository: match.name };
}
|