Add commands to manage issue dependencies using the Gitea API: - `tea issues dependencies <index>` - list dependencies - `tea issues dependencies add <index> <dep>` - add a dependency - `tea issues dependencies remove <index> <dep>` - remove a dependency Supports cross-repo dependencies with owner/repo#index syntax. Supports all output formats (table, json, csv, etc.). Closes #4 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
228 lines
6.3 KiB
Go
228 lines
6.3 KiB
Go
// Copyright 2025 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package issues
|
|
|
|
import (
|
|
"bytes"
|
|
stdctx "context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"code.gitea.io/sdk/gitea"
|
|
"code.gitea.io/tea/cmd/flags"
|
|
"code.gitea.io/tea/modules/api"
|
|
"code.gitea.io/tea/modules/context"
|
|
"code.gitea.io/tea/modules/print"
|
|
"code.gitea.io/tea/modules/utils"
|
|
|
|
"github.com/urfave/cli/v3"
|
|
)
|
|
|
|
// CmdIssuesDependencies represents the subcommand for issue dependencies
|
|
var CmdIssuesDependencies = cli.Command{
|
|
Name: "dependencies",
|
|
Aliases: []string{"deps", "dep"},
|
|
Usage: "Manage issue dependencies",
|
|
Description: "List, add, or remove dependencies for an issue",
|
|
ArgsUsage: "<issue index>",
|
|
Action: runDependenciesList,
|
|
Commands: []*cli.Command{
|
|
&cmdDependenciesList,
|
|
&cmdDependenciesAdd,
|
|
&cmdDependenciesRemove,
|
|
},
|
|
Flags: flags.AllDefaultFlags,
|
|
}
|
|
|
|
var cmdDependenciesList = cli.Command{
|
|
Name: "list",
|
|
Aliases: []string{"ls"},
|
|
Usage: "List dependencies for an issue",
|
|
Description: "List issues that the specified issue depends on (blockers)",
|
|
ArgsUsage: "<issue index>",
|
|
Action: runDependenciesList,
|
|
Flags: flags.AllDefaultFlags,
|
|
}
|
|
|
|
var cmdDependenciesAdd = cli.Command{
|
|
Name: "add",
|
|
Usage: "Add a dependency to an issue",
|
|
Description: "Make an issue depend on another issue (blocker)",
|
|
ArgsUsage: "<issue index> <dependency index or owner/repo#index>",
|
|
Action: runDependenciesAdd,
|
|
Flags: flags.AllDefaultFlags,
|
|
}
|
|
|
|
var cmdDependenciesRemove = cli.Command{
|
|
Name: "remove",
|
|
Aliases: []string{"rm"},
|
|
Usage: "Remove a dependency from an issue",
|
|
Description: "Remove a dependency from an issue",
|
|
ArgsUsage: "<issue index> <dependency index or owner/repo#index>",
|
|
Action: runDependenciesRemove,
|
|
Flags: flags.AllDefaultFlags,
|
|
}
|
|
|
|
// runDependenciesList lists dependencies for an issue
|
|
func runDependenciesList(ctx stdctx.Context, cmd *cli.Command) error {
|
|
c := context.InitCommand(cmd)
|
|
c.Ensure(context.CtxRequirement{RemoteRepo: true})
|
|
|
|
if cmd.Args().Len() < 1 {
|
|
return fmt.Errorf("issue index required")
|
|
}
|
|
|
|
index, err := utils.ArgToIndex(cmd.Args().First())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
client := api.NewClient(c.Login)
|
|
path := fmt.Sprintf("/repos/%s/%s/issues/%d/dependencies", c.Owner, c.Repo, index)
|
|
|
|
var deps []*gitea.Issue
|
|
if _, err := client.Get(ctx, path, &deps); err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(deps) == 0 {
|
|
fmt.Printf("Issue #%d has no dependencies\n", index)
|
|
return nil
|
|
}
|
|
|
|
print.IssuesPullsList(deps, c.Output, []string{"index", "state", "title", "owner", "repo"})
|
|
return nil
|
|
}
|
|
|
|
// runDependenciesAdd adds a dependency to an issue
|
|
func runDependenciesAdd(ctx stdctx.Context, cmd *cli.Command) error {
|
|
c := context.InitCommand(cmd)
|
|
c.Ensure(context.CtxRequirement{RemoteRepo: true})
|
|
|
|
if cmd.Args().Len() < 2 {
|
|
return fmt.Errorf("usage: tea issues dependencies add <issue index> <dependency index or owner/repo#index>")
|
|
}
|
|
|
|
index, err := utils.ArgToIndex(cmd.Args().First())
|
|
if err != nil {
|
|
return fmt.Errorf("invalid issue index: %w", err)
|
|
}
|
|
|
|
depOwner, depRepo, depIndex, err := parseDependencyArg(cmd.Args().Get(1), c.Owner, c.Repo)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
client := api.NewClient(c.Login)
|
|
path := fmt.Sprintf("/repos/%s/%s/issues/%d/dependencies", c.Owner, c.Repo, index)
|
|
|
|
reqBody := api.IssueDependencyRequest{
|
|
Owner: depOwner,
|
|
Repo: depRepo,
|
|
Index: depIndex,
|
|
}
|
|
|
|
body, err := json.Marshal(reqBody)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal request: %w", err)
|
|
}
|
|
|
|
if _, err := client.Post(ctx, path, bytes.NewReader(body), nil); err != nil {
|
|
return err
|
|
}
|
|
|
|
if depOwner == c.Owner && depRepo == c.Repo {
|
|
fmt.Printf("Added dependency: issue #%d now depends on #%d\n", index, depIndex)
|
|
} else {
|
|
fmt.Printf("Added dependency: issue #%d now depends on %s/%s#%d\n", index, depOwner, depRepo, depIndex)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// runDependenciesRemove removes a dependency from an issue
|
|
func runDependenciesRemove(ctx stdctx.Context, cmd *cli.Command) error {
|
|
c := context.InitCommand(cmd)
|
|
c.Ensure(context.CtxRequirement{RemoteRepo: true})
|
|
|
|
if cmd.Args().Len() < 2 {
|
|
return fmt.Errorf("usage: tea issues dependencies remove <issue index> <dependency index or owner/repo#index>")
|
|
}
|
|
|
|
index, err := utils.ArgToIndex(cmd.Args().First())
|
|
if err != nil {
|
|
return fmt.Errorf("invalid issue index: %w", err)
|
|
}
|
|
|
|
depOwner, depRepo, depIndex, err := parseDependencyArg(cmd.Args().Get(1), c.Owner, c.Repo)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
client := api.NewClient(c.Login)
|
|
path := fmt.Sprintf("/repos/%s/%s/issues/%d/dependencies", c.Owner, c.Repo, index)
|
|
|
|
reqBody := api.IssueDependencyRequest{
|
|
Owner: depOwner,
|
|
Repo: depRepo,
|
|
Index: depIndex,
|
|
}
|
|
|
|
body, err := json.Marshal(reqBody)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal request: %w", err)
|
|
}
|
|
|
|
if _, err := client.Delete(ctx, path, bytes.NewReader(body)); err != nil {
|
|
return err
|
|
}
|
|
|
|
if depOwner == c.Owner && depRepo == c.Repo {
|
|
fmt.Printf("Removed dependency: issue #%d no longer depends on #%d\n", index, depIndex)
|
|
} else {
|
|
fmt.Printf("Removed dependency: issue #%d no longer depends on %s/%s#%d\n", index, depOwner, depRepo, depIndex)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// parseDependencyArg parses a dependency argument which can be:
|
|
// - "123" or "#123" (same repo)
|
|
// - "owner/repo#123" (cross-repo)
|
|
func parseDependencyArg(arg, defaultOwner, defaultRepo string) (owner, repo string, index int64, err error) {
|
|
// Check for cross-repo format: owner/repo#123
|
|
if strings.Contains(arg, "/") && strings.Contains(arg, "#") {
|
|
parts := strings.SplitN(arg, "#", 2)
|
|
if len(parts) != 2 {
|
|
return "", "", 0, fmt.Errorf("invalid dependency format: %s (expected owner/repo#index)", arg)
|
|
}
|
|
|
|
repoPath := parts[0]
|
|
indexStr := parts[1]
|
|
|
|
repoParts := strings.SplitN(repoPath, "/", 2)
|
|
if len(repoParts) != 2 {
|
|
return "", "", 0, fmt.Errorf("invalid dependency format: %s (expected owner/repo#index)", arg)
|
|
}
|
|
|
|
owner = repoParts[0]
|
|
repo = repoParts[1]
|
|
|
|
index, err = strconv.ParseInt(indexStr, 10, 64)
|
|
if err != nil {
|
|
return "", "", 0, fmt.Errorf("invalid issue index: %s", indexStr)
|
|
}
|
|
|
|
return owner, repo, index, nil
|
|
}
|
|
|
|
// Same repo format: 123 or #123
|
|
index, err = utils.ArgToIndex(arg)
|
|
if err != nil {
|
|
return "", "", 0, fmt.Errorf("invalid issue index: %w", err)
|
|
}
|
|
|
|
return defaultOwner, defaultRepo, index, nil
|
|
}
|