fix(output): --format table 支持资源包裹列表响应(此前 list 命令表格从不生效) #343

Merged
wbtiger merged 1 commits from Taoyouce/gitlink-cli:feat/table-list-rendering into master 2026-07-14 20:43:47 +08:00
3 changed files with 110 additions and 0 deletions

View File

@ -0,0 +1,25 @@
# `--format table` 支持资源包裹列表响应
## 动机
平台所有分页列表端点的响应形状都是资源包裹 map
`{"total_count": N, "<资源名>": [...]}`。此前 `printTable` 遇到含嵌套
结构的 map 一律回落 JSON导致 **所有 list 命令的 `--format table`
实际上从不渲染表格**,与 flag 文案承诺不符。
## 行为
- 检测「恰好一个数组值 + 其余全是标量」的 map列表响应形状
先打印标量摘要行(如 `total_count: 7`,键排序),再把包裹数组
渲染为表格。
- 含嵌套对象的 map如 commit 详情)与既有行为一致回落 JSON。
## 生产实测
`branch +list --format table`forgeplus输出 `total_count: 7`
摘要行 + 7 行分支表格(此前输出整段 JSON
## 测试
formatter_test.go 新增 2 个单测:资源包裹列表渲染表格(含摘要行、
不回落 JSON嵌套对象 map 仍回落 JSON。

View File

@ -6,6 +6,7 @@ import (
"io"
"os"
"reflect"
"sort"
"strings"
"text/tabwriter"
@ -71,6 +72,11 @@ func printTable(w io.Writer, envelope *Envelope) error {
case []interface{}:
return printSliceTable(w, data)
case map[string]interface{}:
// Resource-wrapped list responses ({"total_count": N, "<resource>": [...]})
// render the wrapped array as a table with the scalar fields as a summary.
if items, ok := unwrapListMap(w, data); ok {
return printSliceTable(w, items)
}
// For maps with nested structures, prefer JSON
if hasComplexValues(data) {
return printJSON(w, envelope)
@ -82,6 +88,47 @@ func printTable(w io.Writer, envelope *Envelope) error {
}
}
// unwrapListMap detects a map containing exactly one array value while every
// other value is a scalar (the shape of the platform's paginated list
// responses). It prints the scalar fields as a summary line and returns the
// wrapped array for table rendering.
func unwrapListMap(w io.Writer, m map[string]interface{}) ([]interface{}, bool) {
var items []interface{}
arrays := 0
for _, v := range m {
switch value := v.(type) {
case []interface{}:
arrays++
items = value
case map[string]interface{}:
return nil, false
}
}
if arrays != 1 {
return nil, false
}
scalars := make([]string, 0, len(m))
for _, k := range sortedKeys(m) {
if _, ok := m[k].([]interface{}); ok {
continue
}
scalars = append(scalars, fmt.Sprintf("%s: %s", k, formatValue(m[k])))
}
if len(scalars) > 0 {
fmt.Fprintln(w, strings.Join(scalars, " "))
}
return items, true
}
func sortedKeys(m map[string]interface{}) []string {
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
return keys
}
func hasComplexValues(m map[string]interface{}) bool {
for _, v := range m {
switch v.(type) {

View File

@ -244,3 +244,41 @@ func TestFormatValue(t *testing.T) {
})
}
}
func TestPrintTableUnwrapsResourceWrappedList(t *testing.T) {
env := SuccessEnvelope(map[string]interface{}{
"total_count": 2,
"tags": []interface{}{
map[string]interface{}{"name": "v1.0.0", "id": 1},
map[string]interface{}{"name": "v1.1.0", "id": 2},
},
}, nil)
var buf bytes.Buffer
if err := PrintTo(&buf, env, "table"); err != nil {
t.Fatalf("PrintTo returned error: %v", err)
}
out := buf.String()
if !strings.Contains(out, "total_count: 2") {
t.Fatalf("missing summary line: %s", out)
}
if !strings.Contains(out, "v1.0.0") || !strings.Contains(out, "v1.1.0") {
t.Fatalf("missing table rows: %s", out)
}
if strings.Contains(out, "\"ok\"") {
t.Fatalf("should not fall back to JSON: %s", out)
}
}
func TestPrintTableKeepsJSONForNestedObjects(t *testing.T) {
env := SuccessEnvelope(map[string]interface{}{
"commit": map[string]interface{}{"sha": "abc"},
"files": []interface{}{},
}, nil)
var buf bytes.Buffer
if err := PrintTo(&buf, env, "table"); err != nil {
t.Fatalf("PrintTo returned error: %v", err)
}
if !strings.Contains(buf.String(), "\"ok\"") {
t.Fatalf("nested object map should fall back to JSON: %s", buf.String())
}
}