Skip to content

Commit b7bf40e

Browse files
committed
fix: add migrated gh command
1 parent a45f16c commit b7bf40e

3 files changed

Lines changed: 201 additions & 0 deletions

File tree

internal/cli/gh_compat_helpers.go

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
package cli
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"fmt"
7+
"os"
8+
"os/exec"
9+
"path/filepath"
10+
"strings"
11+
12+
"github.com/openclaw/gitcrawl/internal/config"
13+
)
14+
15+
func (a *App) writeJSONValue(value any, jqExpr string) error {
16+
data, err := json.MarshalIndent(value, "", " ")
17+
if err != nil {
18+
return err
19+
}
20+
if strings.TrimSpace(jqExpr) == "" {
21+
_, err = fmt.Fprintf(a.Stdout, "%s\n", data)
22+
return err
23+
}
24+
jqPath, err := exec.LookPath("jq")
25+
if err != nil {
26+
return localGHUnsupported(fmt.Errorf("--jq requires jq executable"))
27+
}
28+
cmd := exec.Command(jqPath, jqExpr)
29+
cmd.Stdin = bytes.NewReader(data)
30+
cmd.Stdout = a.Stdout
31+
cmd.Stderr = a.Stderr
32+
return cmd.Run()
33+
}
34+
35+
func (a *App) ghCommandCacheDir() (string, error) {
36+
cfg, err := config.Load(a.configPath)
37+
if err != nil {
38+
cfg = config.Default()
39+
}
40+
dir := filepath.Join(cfg.CacheDir, "octopool-migrated-gh")
41+
if err := os.MkdirAll(dir, 0o755); err != nil {
42+
return "", err
43+
}
44+
return dir, nil
45+
}
46+
47+
func tryGHCommandCacheLock(path string) (*os.File, bool) {
48+
lock, err := os.OpenFile(path, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o600)
49+
if err != nil {
50+
return nil, false
51+
}
52+
_, _ = fmt.Fprintf(lock, "%d\n", os.Getpid())
53+
return lock, true
54+
}
55+
56+
func writeAtomicFile(path string, data []byte, mode os.FileMode) error {
57+
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
58+
return err
59+
}
60+
tmp, err := os.CreateTemp(filepath.Dir(path), "."+filepath.Base(path)+".tmp-*")
61+
if err != nil {
62+
return err
63+
}
64+
tmpPath := tmp.Name()
65+
defer func() {
66+
_ = os.Remove(tmpPath)
67+
}()
68+
if _, err := tmp.Write(data); err != nil {
69+
_ = tmp.Close()
70+
return err
71+
}
72+
if err := tmp.Chmod(mode); err != nil {
73+
_ = tmp.Close()
74+
return err
75+
}
76+
if err := tmp.Close(); err != nil {
77+
return err
78+
}
79+
return os.Rename(tmpPath, path)
80+
}
81+
82+
func normalizeGHAPIRoute(args []string) string {
83+
path := ghAPIPathArg(args)
84+
path = strings.TrimPrefix(path, "https://api.github.com/")
85+
path = strings.TrimPrefix(path, "http://api.github.com/")
86+
path = strings.TrimPrefix(path, "/")
87+
if before, _, found := strings.Cut(path, "?"); found {
88+
path = before
89+
}
90+
if path == "" {
91+
return "api"
92+
}
93+
parts := strings.Split(path, "/")
94+
for index, part := range parts {
95+
if part == "" {
96+
continue
97+
}
98+
switch {
99+
case index > 0 && parts[index-1] == "commits" && isHexString(part) && len(part) >= 7:
100+
parts[index] = ":sha"
101+
case isDecimalString(part):
102+
parts[index] = ":id"
103+
case index >= 2 && parts[index-2] == "repos":
104+
parts[index-1] = ":owner"
105+
parts[index] = ":repo"
106+
}
107+
}
108+
return "api " + strings.Join(parts, "/")
109+
}
110+
111+
func ghAPIPathArg(args []string) string {
112+
for index := 0; index < len(args); index++ {
113+
arg := args[index]
114+
switch arg {
115+
case "-X", "--method", "-H", "--header", "--hostname", "--jq", "-q", "--preview", "--template", "-t", "--input", "--cache", "-f", "-F", "--field", "--raw-field":
116+
index++
117+
continue
118+
case "--paginate":
119+
continue
120+
default:
121+
if strings.HasPrefix(arg, "--cache=") || strings.HasPrefix(arg, "-") {
122+
continue
123+
}
124+
return strings.TrimSpace(arg)
125+
}
126+
}
127+
return ""
128+
}
129+
130+
func isDecimalString(value string) bool {
131+
if value == "" {
132+
return false
133+
}
134+
for _, r := range value {
135+
if r < '0' || r > '9' {
136+
return false
137+
}
138+
}
139+
return true
140+
}
141+
142+
func isHexString(value string) bool {
143+
if value == "" {
144+
return false
145+
}
146+
for _, r := range value {
147+
if (r < '0' || r > '9') && (r < 'a' || r > 'f') && (r < 'A' || r > 'F') {
148+
return false
149+
}
150+
}
151+
return true
152+
}

internal/cli/gh_migrated.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package cli
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
)
8+
9+
var errLocalGHUnsupported = errors.New("local gh shim unsupported")
10+
11+
func (a *App) runGHShim(_ context.Context, _ []string) error {
12+
_, _ = fmt.Fprintln(a.Stderr, "gitcrawl gh moved to octopool.")
13+
_, _ = fmt.Fprintln(a.Stderr, "Run: octopool login")
14+
_, _ = fmt.Fprintln(a.Stderr, "Then use: octopool gh ... or symlink octopool as gh.")
15+
return usageErr(errors.New("gitcrawl gh moved to octopool"))
16+
}
17+
18+
func localGHUnsupported(err error) error {
19+
if err == nil {
20+
return errLocalGHUnsupported
21+
}
22+
return fmt.Errorf("%w: %v", errLocalGHUnsupported, err)
23+
}

internal/cli/gh_migrated_test.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package cli
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"strings"
7+
"testing"
8+
)
9+
10+
func TestGHShimPrintsOctopoolMigration(t *testing.T) {
11+
app := New()
12+
var stderr bytes.Buffer
13+
app.Stderr = &stderr
14+
15+
err := app.Run(context.Background(), []string{"gh", "api", "repos/openclaw/openclaw/pulls/1"})
16+
if err == nil {
17+
t.Fatal("expected migration error")
18+
}
19+
got := stderr.String()
20+
if !strings.Contains(got, "gitcrawl gh moved to octopool") {
21+
t.Fatalf("stderr = %q", got)
22+
}
23+
if !strings.Contains(got, "octopool login") {
24+
t.Fatalf("stderr = %q", got)
25+
}
26+
}

0 commit comments

Comments
 (0)