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
19 changes: 19 additions & 0 deletions docs/azdo_help_reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -1358,6 +1358,25 @@ Aliases
ls, l
```

### `azdo team list-member [ORGANIZATION/]PROJECT/TEAM [flags]`

List members of a team.

```
-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.
--max-items int Maximum number of members to return across all pages (client-side; 0 = unlimited)
--skip int Number of members to skip (server-side)
-t, --template string Format JSON output using a Go template; see "azdo help formatting"
--top int Maximum number of members to return per page (server-side; 0 = server default)
```

Aliases

```
members
```

### `azdo team show [ORGANIZATION/]PROJECT/TEAM [flags]`

Show details of a team.
Expand Down
1 change: 1 addition & 0 deletions docs/azdo_team.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Manage Azure DevOps teams.
* [azdo team create](./azdo_team_create.md)
* [azdo team delete](./azdo_team_delete.md)
* [azdo team list](./azdo_team_list.md)
* [azdo team list-member](./azdo_team_list-member.md)
* [azdo team show](./azdo_team_show.md)
* [azdo team update](./azdo_team_update.md)

Expand Down
60 changes: 60 additions & 0 deletions docs/azdo_team_list-member.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
## Command `azdo team list-member`

```
azdo team list-member [ORGANIZATION/]PROJECT/TEAM [flags]
```

List members of a team. The TEAM argument accepts the ID (GUID)
or name of the team. Supports server-side paging via --top and
--skip.


### Options


* `-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.

* `--max-items` `int` (default `0`)

Maximum number of members to return across all pages (client-side; 0 = unlimited)

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

Number of members to skip (server-side)

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

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

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

Maximum number of members to return per page (server-side; 0 = server default)


### ALIASES

- `members`

### JSON Fields

`identity`, `isTeamAdmin`

### Examples

```bash
# List members of a team
azdo team list-member Fabrikam/"Fabrikam Engineering"

# List the first 10 members in a specific organization
azdo team list-member MyOrg/Fabrikam/MyTeam --top 10
```

### See also

* [azdo team](./azdo_team.md)
184 changes: 184 additions & 0 deletions internal/cmd/team/listmember/listmember.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
package listmember

import (
"fmt"
"sort"
"strconv"
"strings"

"github.com/MakeNowJust/heredoc/v2"
"github.com/microsoft/azure-devops-go-api/azuredevops/v7/core"
"github.com/microsoft/azure-devops-go-api/azuredevops/v7/webapi"
"github.com/spf13/cobra"
"github.com/tmeckel/azdo-cli/internal/cmd/util"
"github.com/tmeckel/azdo-cli/internal/types"
)

type listOptions struct {
targetArg string
top int
skip int
maxItems int
exporter util.Exporter
}

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

cmd := &cobra.Command{
Use: "list-member [ORGANIZATION/]PROJECT/TEAM",
Short: "List members of a team.",
Long: heredoc.Doc(`
List members of a team. The TEAM argument accepts the ID (GUID)
or name of the team. Supports server-side paging via --top and
--skip.
`),
Example: heredoc.Doc(`
# List members of a team
azdo team list-member Fabrikam/"Fabrikam Engineering"

# List the first 10 members in a specific organization
azdo team list-member MyOrg/Fabrikam/MyTeam --top 10
`),
Aliases: []string{"members"},
Args: util.ExactArgs(1, "team argument required"),
RunE: func(cmd *cobra.Command, args []string) error {
opts.targetArg = args[0]
return runList(ctx, opts)
},
}

cmd.Flags().IntVar(&opts.top, "top", 0, "Maximum number of members to return per page (server-side; 0 = server default)")
cmd.Flags().IntVar(&opts.skip, "skip", 0, "Number of members to skip (server-side)")
cmd.Flags().IntVar(&opts.maxItems, "max-items", 0, "Maximum number of members to return across all pages (client-side; 0 = unlimited)")

util.AddJSONFlags(cmd, &opts.exporter, []string{"identity", "isTeamAdmin"})

return cmd
}

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

ios.StartProgressIndicator()
defer ios.StopProgressIndicator()

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

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

teamID := scope.Targets[0]

members, err := fetchTeamMembers(ctx, client, scope.Project, teamID, opts)
if err != nil {
return err
}

ios.StopProgressIndicator()

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

return renderMembersTable(ctx, members)
}

func fetchTeamMembers(ctx util.CmdContext, client core.Client, project, teamID string, opts *listOptions) ([]webapi.TeamMember, error) {
if opts.maxItems < 0 {
return nil, util.FlagErrorf("--max-items must be >= 0")
}

out := make([]webapi.TeamMember, 0)
skip := opts.skip

for {
args := core.GetTeamMembersWithExtendedPropertiesArgs{
ProjectId: &project,
TeamId: &teamID,
}
if opts.top > 0 {
top := opts.top
args.Top = &top
}
if skip > 0 {
s := skip
args.Skip = &s
}

resp, err := client.GetTeamMembersWithExtendedProperties(ctx.Context(), args)
if err != nil {
return nil, fmt.Errorf("failed to list team members: %w", err)
}
if resp == nil || len(*resp) == 0 {
return out, nil
}

for _, m := range *resp {
out = append(out, m)
if opts.maxItems > 0 && len(out) >= opts.maxItems {
return out, nil
}
}

if opts.top > 0 && len(*resp) < opts.top {
return out, nil
}

skip += opts.top
if opts.top == 0 {
return out, nil
}
}
}

func renderMembersTable(ctx util.CmdContext, members []webapi.TeamMember) error {
sort.Slice(members, func(i, j int) bool {
li := strings.ToLower(fieldIdentityDisplay(members[i].Identity))
lj := strings.ToLower(fieldIdentityDisplay(members[j].Identity))
return li < lj
})

tp, err := ctx.Printer("list")
if err != nil {
return err
}

tp.AddColumns("ID", "DISPLAY NAME", "UNIQUE NAME", "IS TEAM ADMIN")
tp.EndRow()

for _, m := range members {
identity := m.Identity
var id, display, unique string
if identity != nil {
id = types.GetValue(identity.Id, "")
display = types.GetValue(identity.DisplayName, "")
unique = types.GetValue(identity.UniqueName, "")
}
tp.AddField(id)
tp.AddField(display)
tp.AddField(unique)
tp.AddField(strconv.FormatBool(types.GetValue(m.IsTeamAdmin, false)))
tp.EndRow()
}

return tp.Render()
}

func fieldIdentityDisplay(id *webapi.IdentityRef) string {
if id == nil {
return ""
}
if d := types.GetValue(id.DisplayName, ""); d != "" {
return d
}
return types.GetValue(id.UniqueName, "")
}
Loading
Loading