forked from Gitlink/gitlink-cli
fix(output): 表头取所有行字段并集并固定核心列顺序
printSliceTable 原先只从第一行取表头,导致列时有时无;改为取所有行并集。新增 protected/last_commit/commit_time 到优先表头,固定分支核心列顺序。
This commit is contained in:
parent
f9efae89a3
commit
f248ed9d04
|
|
@ -150,15 +150,19 @@ func printSliceTable(w io.Writer, items []interface{}) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// Collect headers from first item
|
||||
first, ok := items[0].(map[string]interface{})
|
||||
if !ok {
|
||||
// If the first item isn't a map, fall back to JSON (unchanged behavior).
|
||||
if _, ok := items[0].(map[string]interface{}); !ok {
|
||||
data, _ := json.MarshalIndent(items, "", " ")
|
||||
fmt.Fprintln(w, string(data))
|
||||
return nil
|
||||
}
|
||||
|
||||
headers := collectKeys(first)
|
||||
// Headers are the UNION of every row's keys, so a column appears whenever
|
||||
// any row carries the field. GitLink payloads (e.g. branch lists) do not
|
||||
// always include every key on every item; taking headers from items[0]
|
||||
// alone made columns like `protected` appear or vanish depending on which
|
||||
// item happened to rank first.
|
||||
headers := collectKeysUnion(items)
|
||||
omitted := 0
|
||||
if len(headers) > maxTableColumns {
|
||||
omitted = len(headers) - maxTableColumns
|
||||
|
|
@ -240,20 +244,24 @@ func writeMapRow(tw *tabwriter.Writer, key string, v interface{}, depth int) {
|
|||
// columns a user actually scans.
|
||||
var preferredHeaders = []string{
|
||||
"id", "name", "login", "title", "subject", "number",
|
||||
"status", "state", "type", "time_ago", "created_time",
|
||||
"created_at", "updated_at",
|
||||
"status", "state", "protected", "last_commit", "commit_time",
|
||||
"type", "time_ago", "created_time", "created_at", "updated_at",
|
||||
}
|
||||
|
||||
func collectKeys(m map[string]interface{}) []string {
|
||||
keys := make([]string, 0, len(m))
|
||||
// orderKeySet returns the keys of keySet ordered by preferredHeaders: priority
|
||||
// fields come first in their defined order, the rest follow in set-iteration
|
||||
// order. Keys absent from preferredHeaders still appear, just after the
|
||||
// priority fields.
|
||||
func orderKeySet(keySet map[string]bool) []string {
|
||||
keys := make([]string, 0, len(keySet))
|
||||
seen := map[string]bool{}
|
||||
for _, k := range preferredHeaders {
|
||||
if _, ok := m[k]; ok {
|
||||
keys = append(keys, k)
|
||||
seen[k] = true
|
||||
for _, p := range preferredHeaders {
|
||||
if keySet[p] {
|
||||
keys = append(keys, p)
|
||||
seen[p] = true
|
||||
}
|
||||
}
|
||||
for k := range m {
|
||||
for k := range keySet {
|
||||
if !seen[k] {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
|
|
@ -261,6 +269,32 @@ func collectKeys(m map[string]interface{}) []string {
|
|||
return keys
|
||||
}
|
||||
|
||||
// collectKeysUnion merges the keys of every map in items into a single
|
||||
// priority-ordered header list. Used by printSliceTable so a column shows
|
||||
// whenever any row has the field — preventing columns from disappearing when
|
||||
// the first row happens to omit a key (e.g. `protected` on GitLink branches).
|
||||
func collectKeysUnion(items []interface{}) []string {
|
||||
keySet := map[string]bool{}
|
||||
for _, it := range items {
|
||||
m, ok := it.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
for k := range m {
|
||||
keySet[k] = true
|
||||
}
|
||||
}
|
||||
return orderKeySet(keySet)
|
||||
}
|
||||
|
||||
func collectKeys(m map[string]interface{}) []string {
|
||||
keySet := make(map[string]bool, len(m))
|
||||
for k := range m {
|
||||
keySet[k] = true
|
||||
}
|
||||
return orderKeySet(keySet)
|
||||
}
|
||||
|
||||
func formatValue(v interface{}) string {
|
||||
if v == nil {
|
||||
return ""
|
||||
|
|
|
|||
|
|
@ -305,3 +305,64 @@ func TestFormatNestedMap_LoginAndSha(t *testing.T) {
|
|||
t.Errorf("expected sha to be truncated, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestPrintSliceTable_HeaderUnionAcrossRows 验证当各行字段集合不一致时
|
||||
// (GitLink 分支列表的典型情况:并非每个分支都返回 protected 字段),
|
||||
// 表头应取所有行的字段并集,而不是仅取第一行。否则 protected 列会因落在
|
||||
// 第一位的样本恰好缺失而「时有时无」。
|
||||
func TestPrintSliceTable_HeaderUnionAcrossRows(t *testing.T) {
|
||||
items := []interface{}{
|
||||
map[string]interface{}{"name": "develop"}, // 第一行缺 protected
|
||||
map[string]interface{}{"name": "main", "protected": false}, // 第二行带 protected
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
if err := printSliceTable(&buf, items); err != nil {
|
||||
t.Fatalf("printSliceTable returned error: %v", err)
|
||||
}
|
||||
out := buf.String()
|
||||
if !strings.Contains(out, "protected") {
|
||||
t.Errorf("expected header to include \"protected\" from row union (not just first row); got:\n%s", out)
|
||||
}
|
||||
if !strings.Contains(out, "develop") || !strings.Contains(out, "main") {
|
||||
t.Errorf("expected both rows present in output; got:\n%s", out)
|
||||
}
|
||||
}
|
||||
|
||||
// TestPrintSliceTable_ProtectedSurvivesColumnCap 验证当列数超过上限时,
|
||||
// protected(是否保护分支)作为优先字段会被保留、不被截断丢弃——否则在
|
||||
// 字段较多的分支列表里这一列仍可能消失。
|
||||
func TestPrintSliceTable_ProtectedSurvivesColumnCap(t *testing.T) {
|
||||
row := map[string]interface{}{}
|
||||
cols := []string{"a1", "a2", "a3", "a4", "a5", "a6", "a7", "a8", "id", "name", "protected"}
|
||||
for _, c := range cols {
|
||||
row[c] = "v"
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
if err := printSliceTable(&buf, []interface{}{row}); err != nil {
|
||||
t.Fatalf("printSliceTable returned error: %v", err)
|
||||
}
|
||||
out := buf.String()
|
||||
if !strings.Contains(out, "protected") {
|
||||
t.Errorf("expected priority field \"protected\" to survive the column cap; got:\n%s", out)
|
||||
}
|
||||
}
|
||||
|
||||
// TestOrderKeySet_BranchColumnOrderIsStable 守护分支核心列的固定顺序
|
||||
// name / protected / last_commit / commit_time。当前若 last_commit /
|
||||
// commit_time 不在 preferredHeaders,orderKeySet 会把这两个非优先键按 map
|
||||
// 遍历序追加,导致列顺序随机——重复调用必暴露不一致。
|
||||
func TestOrderKeySet_BranchColumnOrderIsStable(t *testing.T) {
|
||||
want := []string{"name", "protected", "last_commit", "commit_time"}
|
||||
keys := map[string]bool{"name": true, "protected": true, "last_commit": true, "commit_time": true}
|
||||
for i := 0; i < 100; i++ {
|
||||
got := orderKeySet(keys)
|
||||
if len(got) != len(want) {
|
||||
t.Fatalf("iteration %d: got %d keys %v, want %v", i, len(got), got, want)
|
||||
}
|
||||
for j, w := range want {
|
||||
if got[j] != w {
|
||||
t.Fatalf("iteration %d: got[%d]=%q, want %q (full %v)", i, j, got[j], w, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue