feat: add issue delete, pin, and unpin (task 0006)
Add the remaining simple issue mutations. `issue delete` hard-deletes via the DELETE endpoint and is deliberately not idempotent — a nonexistent issue yields ISSUE_NOT_FOUND rather than reporting success. `issue pin` and `issue unpin` read the current pin state (Gitea's pin_order field) and short-circuit to an idempotent no-op with an Already pinned/unpinned message when there is nothing to do.
This commit was merged in pull request #7.
This commit is contained in:
@@ -37,6 +37,9 @@ commands:
|
||||
edit Edit an issue's title, body, labels, assignees, or milestone
|
||||
close Close an issue
|
||||
reopen Reopen a closed issue
|
||||
delete Permanently delete an issue
|
||||
pin Pin an issue to the repository
|
||||
unpin Unpin an issue
|
||||
comment Post a comment on an issue or pull request
|
||||
|
||||
Run \`gitea-axi issue <command> --help\` for the flags of a command.
|
||||
@@ -87,6 +90,46 @@ global flags:
|
||||
--login <name> Select a tea login profile by name
|
||||
`;
|
||||
|
||||
export const ISSUE_DELETE_HELP = `usage: gitea-axi issue delete <number>
|
||||
|
||||
Permanently delete an issue in the current repository. This is a hard delete and
|
||||
requires admin or owner permissions. Deleting a nonexistent issue is an error,
|
||||
not a silent success.
|
||||
|
||||
flags:
|
||||
--help Show this help
|
||||
|
||||
global flags:
|
||||
-R, --repo <OWNER/NAME> Override the repository detected from the git origin remote
|
||||
--login <name> Select a tea login profile by name
|
||||
`;
|
||||
|
||||
export const ISSUE_PIN_HELP = `usage: gitea-axi issue pin <number>
|
||||
|
||||
Pin an issue to the top of the repository's issue list. Pinning an
|
||||
already-pinned issue is a no-op.
|
||||
|
||||
flags:
|
||||
--help Show this help
|
||||
|
||||
global flags:
|
||||
-R, --repo <OWNER/NAME> Override the repository detected from the git origin remote
|
||||
--login <name> Select a tea login profile by name
|
||||
`;
|
||||
|
||||
export const ISSUE_UNPIN_HELP = `usage: gitea-axi issue unpin <number>
|
||||
|
||||
Unpin an issue from the repository's issue list. Unpinning an issue that is not
|
||||
pinned is a no-op.
|
||||
|
||||
flags:
|
||||
--help Show this help
|
||||
|
||||
global flags:
|
||||
-R, --repo <OWNER/NAME> Override the repository detected from the git origin remote
|
||||
--login <name> Select a tea login profile by name
|
||||
`;
|
||||
|
||||
export const ISSUE_CREATE_HELP = `usage: gitea-axi issue create --title <text> [flags]
|
||||
|
||||
Create an issue in the current repository.
|
||||
@@ -857,6 +900,111 @@ async function issueReopen(deps: CliDeps, args: string[]): Promise<string> {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether an issue is pinned. Gitea records pin position in `pin_order`, a
|
||||
* positive integer for a pinned issue and 0 (or absent) for an unpinned one, so
|
||||
* there is no boolean flag to read — the position is the state.
|
||||
*/
|
||||
function isPinned(issue: Issue): boolean {
|
||||
return (issue.pin_order ?? 0) > 0;
|
||||
}
|
||||
|
||||
async function issueDelete(deps: CliDeps, args: string[]): Promise<string> {
|
||||
if (args.includes("--help")) {
|
||||
return ISSUE_DELETE_HELP;
|
||||
}
|
||||
const { positionals } = parseFlags(args, {}, "issue delete");
|
||||
const number = parsePositionalNumber(positionals, "issue delete", "issue");
|
||||
|
||||
const context = await resolveRepoContext(deps);
|
||||
const api = createClient(context);
|
||||
|
||||
// A hard delete, deliberately not idempotent (ADR 0010): a nonexistent issue
|
||||
// is a 404, which classify404 maps to ISSUE_NOT_FOUND rather than reporting a
|
||||
// deletion that never happened.
|
||||
try {
|
||||
await api.repos.issueDelete(context.owner, context.name, number);
|
||||
} catch (error) {
|
||||
throw classifyHttpError(error);
|
||||
}
|
||||
|
||||
return renderDetail({
|
||||
noun: "issue",
|
||||
item: { number, status: "deleted" },
|
||||
help: [suggestCommand(context, "issue list", "to see the remaining issues")],
|
||||
});
|
||||
}
|
||||
|
||||
async function issuePin(deps: CliDeps, args: string[]): Promise<string> {
|
||||
if (args.includes("--help")) {
|
||||
return ISSUE_PIN_HELP;
|
||||
}
|
||||
const { positionals } = parseFlags(args, {}, "issue pin");
|
||||
const number = parsePositionalNumber(positionals, "issue pin", "issue");
|
||||
|
||||
const context = await resolveRepoContext(deps);
|
||||
const api = createClient(context);
|
||||
|
||||
// Read the current pin state first: an already-pinned issue short-circuits to
|
||||
// the idempotent no-op below rather than issuing a redundant POST.
|
||||
const issue = await getIssue(api, context, number);
|
||||
const state = issue.state ?? "open";
|
||||
if (isPinned(issue)) {
|
||||
return renderDetail({
|
||||
noun: "issue",
|
||||
item: { number, state, pinned: true, message: "Already pinned" },
|
||||
help: [suggestCommand(context, `issue unpin ${number}`, "to unpin this issue")],
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
await api.repos.pinIssue(context.owner, context.name, number);
|
||||
} catch (error) {
|
||||
throw classifyHttpError(error);
|
||||
}
|
||||
|
||||
return renderDetail({
|
||||
noun: "issue",
|
||||
item: { number, state, pinned: true },
|
||||
help: [suggestCommand(context, `issue unpin ${number}`, "to unpin this issue")],
|
||||
});
|
||||
}
|
||||
|
||||
async function issueUnpin(deps: CliDeps, args: string[]): Promise<string> {
|
||||
if (args.includes("--help")) {
|
||||
return ISSUE_UNPIN_HELP;
|
||||
}
|
||||
const { positionals } = parseFlags(args, {}, "issue unpin");
|
||||
const number = parsePositionalNumber(positionals, "issue unpin", "issue");
|
||||
|
||||
const context = await resolveRepoContext(deps);
|
||||
const api = createClient(context);
|
||||
|
||||
// Read the current pin state first: an issue that is not pinned short-circuits
|
||||
// to the idempotent no-op below rather than issuing a redundant DELETE.
|
||||
const issue = await getIssue(api, context, number);
|
||||
const state = issue.state ?? "open";
|
||||
if (!isPinned(issue)) {
|
||||
return renderDetail({
|
||||
noun: "issue",
|
||||
item: { number, state, pinned: false, message: "Already unpinned" },
|
||||
help: [suggestCommand(context, `issue pin ${number}`, "to pin this issue")],
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
await api.repos.unpinIssue(context.owner, context.name, number);
|
||||
} catch (error) {
|
||||
throw classifyHttpError(error);
|
||||
}
|
||||
|
||||
return renderDetail({
|
||||
noun: "issue",
|
||||
item: { number, state, pinned: false },
|
||||
help: [suggestCommand(context, `issue pin ${number}`, "to pin this issue")],
|
||||
});
|
||||
}
|
||||
|
||||
export function issueCommand(deps: CliDeps) {
|
||||
return async (args: string[]): Promise<string> => {
|
||||
const [subcommand, ...rest] = args;
|
||||
@@ -881,6 +1029,15 @@ export function issueCommand(deps: CliDeps) {
|
||||
if (subcommand === "reopen") {
|
||||
return issueReopen(deps, rest);
|
||||
}
|
||||
if (subcommand === "delete") {
|
||||
return issueDelete(deps, rest);
|
||||
}
|
||||
if (subcommand === "pin") {
|
||||
return issuePin(deps, rest);
|
||||
}
|
||||
if (subcommand === "unpin") {
|
||||
return issueUnpin(deps, rest);
|
||||
}
|
||||
if (subcommand === "comment") {
|
||||
return issueComment(deps, rest);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user