feat(issues): add dependency management commands
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>
This commit is contained in:
227
cmd/issues/dependencies.go
Normal file
227
cmd/issues/dependencies.go
Normal file
@@ -0,0 +1,227 @@
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user