Skip to content

Commit dcce548

Browse files
feat: project-group view command support -f options (#536)
* feat: project-group view command support -f options * refactor to use util.GenerateWebURL
1 parent 6254eb8 commit dcce548

6 files changed

Lines changed: 137 additions & 44 deletions

File tree

pkg/cmd/buildinformation/view/view.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"github.com/OctopusDeploy/cli/pkg/factory"
1313
"github.com/OctopusDeploy/cli/pkg/output"
1414
"github.com/OctopusDeploy/cli/pkg/usage"
15+
"github.com/OctopusDeploy/cli/pkg/util"
1516
"github.com/OctopusDeploy/cli/pkg/util/flag"
1617
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/buildinformation"
1718
"github.com/pkg/browser"
@@ -160,7 +161,7 @@ func viewRun(opts *ViewOptions, cmd *cobra.Command) error {
160161
}
161162
}
162163

163-
link := fmt.Sprintf("%s/app#/%s/library/buildinformation/%s", opts.Host, opts.Client.GetSpaceID(), b.GetID())
164+
link := util.GenerateWebURL(opts.Host, opts.Client.GetSpaceID(), fmt.Sprintf("library/buildinformation/%s", b.GetID()))
164165
s.WriteString(fmt.Sprintf("\nView this build information in Octopus Deploy: %s\n", output.Blue(link)))
165166

166167
if opts.Web.Value {

pkg/cmd/project/view/view.go

Lines changed: 25 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"github.com/OctopusDeploy/cli/pkg/factory"
1212
"github.com/OctopusDeploy/cli/pkg/output"
1313
"github.com/OctopusDeploy/cli/pkg/usage"
14+
"github.com/OctopusDeploy/cli/pkg/util"
1415
"github.com/OctopusDeploy/cli/pkg/util/flag"
1516
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/client"
1617
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/projects"
@@ -22,13 +23,6 @@ const (
2223
FlagWeb = "web"
2324
)
2425

25-
// joinURL joins host and path, ensuring there's exactly one slash between them
26-
func joinURL(host, path string) string {
27-
host = strings.TrimSuffix(host, "/")
28-
path = strings.TrimPrefix(path, "/")
29-
return host + "/" + path
30-
}
31-
3226
type ViewFlags struct {
3327
Web *flag.Flag[bool]
3428
}
@@ -91,21 +85,26 @@ func viewRun(opts *ViewOptions) error {
9185
return err
9286
}
9387

88+
// Use basic format as default for project view when no -f flag is specified
89+
if !opts.Command.Flags().Changed(constants.FlagOutputFormat) {
90+
opts.Command.Flags().Set(constants.FlagOutputFormat, constants.OutputFormatBasic)
91+
}
92+
9493
return output.PrintResource(project, opts.Command, output.Mappers[*projects.Project]{
9594
Json: func(p *projects.Project) any {
9695
cacBranch := "Not version controlled"
9796
if p.IsVersionControlled {
9897
cacBranch = p.PersistenceSettings.(projects.GitPersistenceSettings).DefaultBranch()
9998
}
100-
99+
101100
return ProjectAsJson{
102-
Id: p.GetID(),
103-
Name: p.Name,
104-
Slug: p.Slug,
105-
Description: p.Description,
106-
IsVersionControlled: p.IsVersionControlled,
107-
VersionControlBranch: cacBranch,
108-
WebUrl: joinURL(opts.Host, p.Links["Web"]),
101+
Id: p.GetID(),
102+
Name: p.Name,
103+
Slug: p.Slug,
104+
Description: p.Description,
105+
IsVersionControlled: p.IsVersionControlled,
106+
VersionControlBranch: cacBranch,
107+
WebUrl: util.GenerateWebURL(opts.Host, p.SpaceID, fmt.Sprintf("projects/%s", p.GetID())),
109108
}
110109
},
111110
Table: output.TableDefinition[*projects.Project]{
@@ -115,18 +114,18 @@ func viewRun(opts *ViewOptions) error {
115114
if description == "" {
116115
description = constants.NoDescription
117116
}
118-
117+
119118
cacBranch := "Not version controlled"
120119
if p.IsVersionControlled {
121120
cacBranch = p.PersistenceSettings.(projects.GitPersistenceSettings).DefaultBranch()
122121
}
123-
122+
124123
return []string{
125124
output.Bold(p.Name),
126125
p.Slug,
127126
description,
128127
cacBranch,
129-
output.Blue(joinURL(opts.Host, p.Links["Web"])),
128+
output.Blue(util.GenerateWebURL(opts.Host, p.SpaceID, fmt.Sprintf("projects/%s", p.GetID()))),
130129
}
131130
},
132131
},
@@ -143,36 +142,36 @@ type ProjectAsJson struct {
143142
Description string `json:"Description"`
144143
IsVersionControlled bool `json:"IsVersionControlled"`
145144
VersionControlBranch string `json:"VersionControlBranch"`
146-
WebUrl string `json:"WebUrl"`
145+
WebUrl string `json:"WebUrl"`
147146
}
148147

149148
func formatProjectForBasic(opts *ViewOptions, project *projects.Project) string {
150149
var result strings.Builder
151-
150+
152151
// header
153152
result.WriteString(fmt.Sprintf("%s %s\n", output.Bold(project.Name), output.Dimf("(%s)", project.Slug)))
154-
153+
155154
// version control branch
156155
cacBranch := "Not version controlled"
157156
if project.IsVersionControlled {
158157
cacBranch = project.PersistenceSettings.(projects.GitPersistenceSettings).DefaultBranch()
159158
}
160159
result.WriteString(fmt.Sprintf("Version control branch: %s\n", output.Cyan(cacBranch)))
161-
160+
162161
// description
163162
if project.Description == "" {
164163
result.WriteString(fmt.Sprintln(output.Dim(constants.NoDescription)))
165164
} else {
166165
result.WriteString(fmt.Sprintln(output.Dim(project.Description)))
167166
}
168-
167+
169168
// footer with web URL
170-
url := joinURL(opts.Host, project.Links["Web"])
169+
url := util.GenerateWebURL(opts.Host, project.SpaceID, fmt.Sprintf("projects/%s", project.GetID()))
171170
result.WriteString(fmt.Sprintf("View this project in Octopus Deploy: %s\n", output.Blue(url)))
172-
171+
173172
if opts.flags.Web.Value {
174173
browser.OpenURL(url)
175174
}
176-
175+
177176
return result.String()
178177
}

pkg/cmd/projectgroup/view/view.go

Lines changed: 90 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,18 @@ import (
44
"fmt"
55
"github.com/OctopusDeploy/cli/pkg/apiclient"
66
"io"
7+
"strings"
78

89
"github.com/MakeNowJust/heredoc/v2"
910
"github.com/OctopusDeploy/cli/pkg/constants"
1011
"github.com/OctopusDeploy/cli/pkg/factory"
1112
"github.com/OctopusDeploy/cli/pkg/output"
1213
"github.com/OctopusDeploy/cli/pkg/usage"
14+
"github.com/OctopusDeploy/cli/pkg/util"
1315
"github.com/OctopusDeploy/cli/pkg/util/flag"
1416
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/client"
17+
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/projectgroups"
18+
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/projects"
1519
"github.com/pkg/browser"
1620
"github.com/spf13/cobra"
1721
)
@@ -36,6 +40,7 @@ type ViewOptions struct {
3640
out io.Writer
3741
idOrName string
3842
flags *ViewFlags
43+
Command *cobra.Command
3944
}
4045

4146
func NewCmdView(f factory.Factory) *cobra.Command {
@@ -61,6 +66,7 @@ func NewCmdView(f factory.Factory) *cobra.Command {
6166
cmd.OutOrStdout(),
6267
args[0],
6368
viewFlags,
69+
cmd,
6470
}
6571

6672
return viewRun(opts)
@@ -79,31 +85,99 @@ func viewRun(opts *ViewOptions) error {
7985
return err
8086
}
8187

82-
fmt.Fprintf(opts.out, "%s %s\n", output.Bold(projectGroup.GetName()), output.Dimf("(%s)", projectGroup.GetID()))
83-
84-
if projectGroup.Description == "" {
85-
fmt.Fprintln(opts.out, output.Dim(constants.NoDescription))
86-
} else {
87-
fmt.Fprintln(opts.out, output.Dim(projectGroup.Description))
88-
}
89-
9088
projects, err := opts.Client.ProjectGroups.GetProjects(projectGroup)
9189
if err != nil {
9290
return err
9391
}
94-
fmt.Fprintf(opts.out, output.Cyan("\nProjects:\n"))
95-
for _, project := range projects {
96-
fmt.Fprintf(opts.out, "%s (%s)\n", output.Bold(project.GetName()), project.Slug)
92+
93+
// Use basic format as default for project group view when no -f flag is specified
94+
if !opts.Command.Flags().Changed(constants.FlagOutputFormat) {
95+
opts.Command.Flags().Set(constants.FlagOutputFormat, constants.OutputFormatBasic)
9796
}
9897

99-
url := fmt.Sprintf("%s/app#/%s/projects?projectGroupId=%s", opts.Host, projectGroup.SpaceID, projectGroup.GetID())
98+
return output.PrintResource(projectGroup, opts.Command, output.Mappers[*projectgroups.ProjectGroup]{
99+
Json: func(pg *projectgroups.ProjectGroup) any {
100+
projectList := make([]ProjectInfo, 0, len(projects))
101+
for _, project := range projects {
102+
projectList = append(projectList, ProjectInfo{
103+
Id: project.GetID(),
104+
Name: project.GetName(),
105+
Slug: project.Slug,
106+
})
107+
}
108+
109+
return ProjectGroupAsJson{
110+
Id: pg.GetID(),
111+
Name: pg.GetName(),
112+
Description: pg.Description,
113+
Projects: projectList,
114+
WebUrl: util.GenerateWebURL(opts.Host, pg.SpaceID, fmt.Sprintf("projects?projectGroupId=%s", pg.GetID())),
115+
}
116+
},
117+
Table: output.TableDefinition[*projectgroups.ProjectGroup]{
118+
Header: []string{"NAME", "DESCRIPTION", "PROJECTS COUNT", "WEB URL"},
119+
Row: func(pg *projectgroups.ProjectGroup) []string {
120+
description := pg.Description
121+
if description == "" {
122+
description = constants.NoDescription
123+
}
124+
125+
url := util.GenerateWebURL(opts.Host, pg.SpaceID, fmt.Sprintf("projects?projectGroupId=%s", pg.GetID()))
126+
127+
return []string{
128+
output.Bold(pg.GetName()),
129+
description,
130+
fmt.Sprintf("%d", len(projects)),
131+
output.Blue(url),
132+
}
133+
},
134+
},
135+
Basic: func(pg *projectgroups.ProjectGroup) string {
136+
return formatProjectGroupForBasic(opts, pg, projects)
137+
},
138+
})
139+
}
140+
141+
type ProjectInfo struct {
142+
Id string `json:"Id"`
143+
Name string `json:"Name"`
144+
Slug string `json:"Slug"`
145+
}
100146

101-
// footer
102-
fmt.Fprintf(opts.out, "\nView this project group in Octopus Deploy: %s\n", output.Blue(url))
147+
type ProjectGroupAsJson struct {
148+
Id string `json:"Id"`
149+
Name string `json:"Name"`
150+
Description string `json:"Description"`
151+
Projects []ProjectInfo `json:"Projects"`
152+
WebUrl string `json:"WebUrl"`
153+
}
103154

155+
func formatProjectGroupForBasic(opts *ViewOptions, projectGroup *projectgroups.ProjectGroup, projects []*projects.Project) string {
156+
var result strings.Builder
157+
158+
// header
159+
result.WriteString(fmt.Sprintf("%s %s\n", output.Bold(projectGroup.GetName()), output.Dimf("(%s)", projectGroup.GetID())))
160+
161+
// description
162+
if projectGroup.Description == "" {
163+
result.WriteString(fmt.Sprintln(output.Dim(constants.NoDescription)))
164+
} else {
165+
result.WriteString(fmt.Sprintln(output.Dim(projectGroup.Description)))
166+
}
167+
168+
// projects
169+
result.WriteString(fmt.Sprintf(output.Cyan("\nProjects:\n")))
170+
for _, project := range projects {
171+
result.WriteString(fmt.Sprintf("%s (%s)\n", output.Bold(project.GetName()), project.Slug))
172+
}
173+
174+
// footer with web URL
175+
url := util.GenerateWebURL(opts.Host, projectGroup.SpaceID, fmt.Sprintf("projects?projectGroupId=%s", projectGroup.GetID()))
176+
result.WriteString(fmt.Sprintf("\nView this project group in Octopus Deploy: %s\n", output.Blue(url)))
177+
104178
if opts.flags.Web.Value {
105179
browser.OpenURL(url)
106180
}
107-
108-
return nil
181+
182+
return result.String()
109183
}

pkg/cmd/space/view/view.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,11 @@ func viewRun(opts *ViewOptions) error {
5959
return err
6060
}
6161

62+
// Use basic format as default for space view when no -f flag is specified
63+
if !opts.Command.Flags().Changed(constants.FlagOutputFormat) {
64+
opts.Command.Flags().Set(constants.FlagOutputFormat, constants.OutputFormatBasic)
65+
}
66+
6267
host := opts.Host
6368
return output.PrintResource(space, opts.Command, output.Mappers[*spaces.Space]{
6469
Json: func(item *spaces.Space) any {

pkg/cmd/tenant/view/view.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"github.com/OctopusDeploy/cli/pkg/factory"
1515
"github.com/OctopusDeploy/cli/pkg/output"
1616
"github.com/OctopusDeploy/cli/pkg/usage"
17+
"github.com/OctopusDeploy/cli/pkg/util"
1718
"github.com/OctopusDeploy/cli/pkg/util/flag"
1819
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/client"
1920
"github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/tenants"
@@ -145,7 +146,7 @@ func viewRun(opts *ViewOptions, cmd *cobra.Command) error {
145146
s.WriteString(fmt.Sprintln("Tenant is enabled"))
146147
}
147148

148-
link := fmt.Sprintf("%s/app#/%s/tenants/%s/overview", opts.Host, tenant.SpaceID, tenant.ID)
149+
link := util.GenerateWebURL(opts.Host, tenant.SpaceID, fmt.Sprintf("tenants/%s/overview", tenant.ID))
149150
// footer
150151
s.WriteString(fmt.Sprintf("View this tenant in Octopus Deploy: %s\n", output.Blue(link)))
151152

pkg/util/util.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"fmt"
66
"slices"
77
"sort"
8+
"strings"
89
)
910

1011
// SliceContains returns true if it finds an item in the slice that is equal to the target
@@ -293,3 +294,15 @@ func PrintJSON(obj interface{}) {
293294
bytes, _ := json.MarshalIndent(obj, "\t", "\t")
294295
fmt.Println(string(bytes))
295296
}
297+
298+
// GenerateWebURL creates a web URL for Octopus Deploy resources
299+
// host: base URL (e.g., "http://localhost:8066")
300+
// spaceID: space identifier (e.g., "Spaces-1")
301+
// path: resource-specific path (e.g., "projects?projectGroupId=ProjectGroups-1")
302+
func GenerateWebURL(host, spaceID, path string) string {
303+
// Ensure host doesn't end with slash
304+
host = strings.TrimSuffix(host, "/")
305+
// Ensure path doesn't start with slash
306+
path = strings.TrimPrefix(path, "/")
307+
return fmt.Sprintf("%s/app#/%s/%s", host, spaceID, path)
308+
}

0 commit comments

Comments
 (0)