Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/azdo_boards_iteration_project.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Project-scoped iteration commands.
* [azdo boards iteration project create](./azdo_boards_iteration_project_create.md)
* [azdo boards iteration project delete](./azdo_boards_iteration_project_delete.md)
* [azdo boards iteration project list](./azdo_boards_iteration_project_list.md)
* [azdo boards iteration project show](./azdo_boards_iteration_project_show.md)

### ALIASES

Expand All @@ -18,6 +19,9 @@ Project-scoped iteration commands.
```bash
# List iterations for a project
azdo boards iteration project list Fabrikam

# Show a specific iteration
azdo boards iteration project show Fabrikam --path "Sprint 1"
```

### See also
Expand Down
70 changes: 70 additions & 0 deletions docs/azdo_boards_iteration_project_show.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
## Command `azdo boards iteration project show`

```
azdo boards iteration project show [ORGANIZATION/]PROJECT [flags]
```

Display the details of a single iteration (sprint) node in a project.
The iteration is identified by its fully-qualified path under /Iteration.


### Options


* `--depth` `int` (default `0`)

Depth of child nodes to fetch (0-10).

* `--include-children`

Include child nodes in the template output.

* `-q`, `--jq` `expression`

Filter JSON output using a jq expression

* `--json` `fields`

Output JSON with the specified fields. Prefix a field with '-' to exclude it.

* `--path` `string`

Iteration path under /Iteration (required).

* `-r`, `--raw`

Dump the raw SDK node to stderr.

* `-t`, `--template` `string`

Format JSON output using a Go template; see "azdo help formatting"


### ALIASES

- `view`
- `status`

### JSON Fields

`_links`, `attributes`, `children`, `hasChildren`, `id`, `identifier`, `name`, `path`, `structureType`, `url`

### Examples

```bash
# Show a top-level iteration
azdo boards iteration project show Fabrikam --path "Sprint 1"

# Show a nested iteration
azdo boards iteration project show myorg/Fabrikam --path "Release 2025/Sprint 1"

# Include child nodes in the template output
azdo boards iteration project show Fabrikam --path "Release 2025" --include-children

# Emit the raw SDK node as JSON
azdo boards iteration project show Fabrikam --path "Sprint 1" --json
```

### See also

* [azdo boards iteration project](./azdo_boards_iteration_project.md)
20 changes: 20 additions & 0 deletions docs/azdo_help_reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,26 @@ Aliases
ls, l
```

##### `azdo boards iteration project show [ORGANIZATION/]PROJECT [flags]`

Show an iteration in a project.

```
--depth int Depth of child nodes to fetch (0-10).
--include-children Include child nodes in the template output.
-q, --jq expression Filter JSON output using a jq expression
--json fields[=*] Output JSON with the specified fields. Prefix a field with '-' to exclude it.
--path string Iteration path under /Iteration (required).
-r, --raw Dump the raw SDK node to stderr.
-t, --template string Format JSON output using a Go template; see "azdo help formatting"
```

Aliases

```
view, status
```

### `azdo boards work-item <command>`

Work with Azure Boards work items.
Expand Down
3 changes: 0 additions & 3 deletions internal/cmd/boards/iteration/project/create/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,6 @@ func runCreate(ctx util.CmdContext, opts *createOptions) error {

ios.StartProgressIndicator()
defer ios.StopProgressIndicator()
if parts := strings.Split(strings.TrimSpace(opts.scopeArg), "/"); len(parts) > 2 {
return util.FlagErrorf("invalid project scope %q: expected [ORGANIZATION/]PROJECT", opts.scopeArg)
}

scope, err := util.ParseProjectScope(ctx, opts.scopeArg)
if err != nil {
Expand Down
4 changes: 0 additions & 4 deletions internal/cmd/boards/iteration/project/delete/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,6 @@ func runDelete(ctx util.CmdContext, opts *deleteOptions) error {
ios.StartProgressIndicator()
defer ios.StopProgressIndicator()

if parts := strings.Split(strings.TrimSpace(opts.scopeArg), "/"); len(parts) > 2 {
return util.FlagErrorf("invalid project scope %q: expected [ORGANIZATION/]PROJECT", opts.scopeArg)
}

scope, err := util.ParseProjectScope(ctx, opts.scopeArg)
if err != nil {
return util.FlagErrorWrap(err)
Expand Down
5 changes: 5 additions & 0 deletions internal/cmd/boards/iteration/project/project.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"github.com/tmeckel/azdo-cli/internal/cmd/boards/iteration/project/create"
"github.com/tmeckel/azdo-cli/internal/cmd/boards/iteration/project/delete"
"github.com/tmeckel/azdo-cli/internal/cmd/boards/iteration/project/list"
"github.com/tmeckel/azdo-cli/internal/cmd/boards/iteration/project/show"
"github.com/tmeckel/azdo-cli/internal/cmd/util"
)

Expand All @@ -17,6 +18,9 @@ func NewCmd(ctx util.CmdContext) *cobra.Command {
Example: heredoc.Doc(`
# List iterations for a project
azdo boards iteration project list Fabrikam

# Show a specific iteration
azdo boards iteration project show Fabrikam --path "Sprint 1"
`),
Aliases: []string{
"prj",
Expand All @@ -27,6 +31,7 @@ func NewCmd(ctx util.CmdContext) *cobra.Command {
cmd.AddCommand(create.NewCmd(ctx))
cmd.AddCommand(delete.NewCmd(ctx))
cmd.AddCommand(list.NewCmd(ctx))
cmd.AddCommand(show.NewCmd(ctx))

return cmd
}
171 changes: 171 additions & 0 deletions internal/cmd/boards/iteration/project/show/show.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
package show

import (
_ "embed"
"fmt"
"strconv"
"strings"

"github.com/MakeNowJust/heredoc/v2"
"github.com/microsoft/azure-devops-go-api/azuredevops/v7/workitemtracking"
"github.com/spewerspew/spew"
"github.com/spf13/cobra"
"go.uber.org/zap"

"github.com/tmeckel/azdo-cli/internal/cmd/boards/shared"
"github.com/tmeckel/azdo-cli/internal/cmd/util"
"github.com/tmeckel/azdo-cli/internal/template"
"github.com/tmeckel/azdo-cli/internal/types"
)

type showOptions struct {
scopeArg string
path string
depth int
includeChildren bool
raw bool
exporter util.Exporter
}

//go:embed show.tpl
var showTpl string

type templateData struct {
Node *workitemtracking.WorkItemClassificationNode
IncludeChildren bool
}

func NewCmd(ctx util.CmdContext) *cobra.Command {
opts := &showOptions{}

cmd := &cobra.Command{
Use: "show [ORGANIZATION/]PROJECT",
Short: "Show an iteration in a project.",
Long: heredoc.Doc(`
Display the details of a single iteration (sprint) node in a project.
The iteration is identified by its fully-qualified path under /Iteration.
`),
Example: heredoc.Doc(`
# Show a top-level iteration
azdo boards iteration project show Fabrikam --path "Sprint 1"

# Show a nested iteration
azdo boards iteration project show myorg/Fabrikam --path "Release 2025/Sprint 1"

# Include child nodes in the template output
azdo boards iteration project show Fabrikam --path "Release 2025" --include-children

# Emit the raw SDK node as JSON
azdo boards iteration project show Fabrikam --path "Sprint 1" --json
`),
Aliases: []string{"view", "status"},
Args: util.ExactArgs(1, "project argument required"),
RunE: func(cmd *cobra.Command, args []string) error {
opts.scopeArg = args[0]
return runShow(ctx, opts)
},
}

cmd.Flags().StringVar(&opts.path, "path", "", "Iteration path under /Iteration (required).")
cmd.Flags().IntVar(&opts.depth, "depth", 0, "Depth of child nodes to fetch (0-10).")
cmd.Flags().BoolVar(&opts.includeChildren, "include-children", false, "Include child nodes in the template output.")
cmd.Flags().BoolVarP(&opts.raw, "raw", "r", false, "Dump the raw SDK node to stderr.")
_ = cmd.MarkFlagRequired("path")
util.AddJSONFlags(cmd, &opts.exporter, []string{
"id", "identifier", "name", "path", "structureType",
"hasChildren", "attributes", "url", "_links", "children",
})

return cmd
}

func runShow(ctx util.CmdContext, opts *showOptions) error {
ios, err := ctx.IOStreams()
if err != nil {
return err
}

ios.StartProgressIndicator()
defer ios.StopProgressIndicator()

if opts.depth < 0 || opts.depth > 10 {
return util.FlagErrorf("--depth must be between 0 and 10")
}

scope, err := util.ParseProjectScope(ctx, opts.scopeArg)
if err != nil {
return util.FlagErrorWrap(err)
}

rawPath := strings.TrimSpace(opts.path)
if rawPath == "" {
return util.FlagErrorf("--path must not be empty")
}
nodePath, err := shared.BuildClassificationPath(scope.Project, true, "Iteration", rawPath)
if err != nil {
return util.FlagErrorf("invalid --path: %w", err)
}
if nodePath == "" {
return util.FlagErrorf("--path must reference a child of /Iteration, not the iteration root")
}

wit, err := ctx.ClientFactory().WorkItemTracking(ctx.Context(), scope.Organization)
if err != nil {
return fmt.Errorf("failed to get classification client: %w", err)
}

zap.L().Debug(
"fetching iteration",
zap.String("organization", scope.Organization),
zap.String("project", scope.Project),
zap.String("path", nodePath),
zap.Int("depth", opts.depth),
)

args := workitemtracking.GetClassificationNodeArgs{
Project: types.ToPtr(scope.Project),
StructureGroup: types.ToPtr(workitemtracking.TreeStructureGroupValues.Iterations),
Path: types.ToPtr(nodePath),
Depth: types.ToPtr(opts.depth),
}
res, err := wit.GetClassificationNode(ctx.Context(), args)
if err != nil {
return fmt.Errorf("failed to get iteration: %w", err)
}
if res == nil {
return fmt.Errorf("iteration node is nil")
}

ios.StopProgressIndicator()

if opts.raw {
spew.NewDefaultConfig().Fdump(ios.ErrOut, res)
return nil
}

if opts.exporter != nil {
return opts.exporter.Write(ios, res)
}

t := template.New(
ios.Out,
ios.TerminalWidth(),
ios.ColorEnabled(),
).
WithTheme(ios.TerminalTheme()).
WithFuncs(map[string]any{
"hasText": template.HasText,
"s": template.StringOrEmpty,
"bool": template.BoolString,
"int": func(v *int) string { return strconv.Itoa(types.GetValue(v, 0)) },
"uuid": template.UUIDString,
})
if err := t.Parse(showTpl); err != nil {
return err
}

return t.ExecuteData(templateData{
Node: res,
IncludeChildren: opts.includeChildren,
})
}
21 changes: 21 additions & 0 deletions internal/cmd/boards/iteration/project/show/show.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{{bold "url:"}} {{if hasText .Node.Url}}{{hyperlink (s .Node.Url) (s .Node.Url)}}{{else}}{{s .Node.Url}}{{end}}
{{bold "id:"}} {{int .Node.Id}}
{{bold "identifier:"}} {{uuid .Node.Identifier}}
{{bold "name:"}} {{s .Node.Name}}
{{bold "path:"}} {{s .Node.Path}}
{{bold "structure:"}} {{s .Node.StructureType}}
{{bold "has children:"}} {{bool .Node.HasChildren}}
{{if .Node.Attributes}}

{{bold "attributes:"}}
{{range $key, $value := .Node.Attributes}}{{printf "%-14s" (printf "%s:" $key)}} {{timefmt "2006-01-02" $value}}
{{end -}}
{{end -}}
{{if .IncludeChildren}}
{{if .Node.Children}}

{{bold "children:"}}
{{range .Node.Children}} - {{s .Name}}{{if hasText .Identifier}} ({{uuid .Identifier}}){{end}}{{if .HasChildren}} (hasChildren: {{bool .HasChildren}}){{end}}
{{end -}}
{{end -}}
{{end -}}
Loading
Loading