Skip to content

Commit b808123

Browse files
committed
update git clone
1 parent a119b98 commit b808123

5 files changed

Lines changed: 197 additions & 0 deletions

File tree

AGENTS.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ Git 仓库管理命令的父命令,包含以下子命令:
8989

9090
```bash
9191
spark git update # 更新多个仓库
92+
spark git clone # 克隆 GitHub 仓库(默认 SSH)
9293
spark git submodule add # 添加现有仓库为子模块
9394
spark git sync # 同步子模块
9495
spark git gitcode # 添加 Gitcode 远程
@@ -100,6 +101,23 @@ spark git issues # 从 Markdown 文档/任务创建 GitHub Issue
100101
spark git push-all # 提交并推送所有仓库的更改
101102
```
102103

104+
#### `spark git clone`
105+
使用 `gh repo clone` 克隆 GitHub 仓库,默认走 SSH 协议。
106+
107+
```bash
108+
spark git clone https://github.com/Nutlope/pdf-to-interactive-lesson.git
109+
spark git clone Nutlope/pdf-to-interactive-lesson
110+
spark git clone https://github.com/owner/repo.git my-dir -- --branch main --depth 1
111+
```
112+
113+
支持的输入格式:
114+
- `https://github.com/owner/repo.git`
115+
- `git@github.com:owner/repo.git`
116+
- `github.com/owner/repo`
117+
- `owner/repo`
118+
119+
`--` 之后的额外参数会透传给 `git clone`
120+
103121
#### `spark git update`
104122
扫描指定目录中的所有 Git 仓库并更新到最新版本。
105123

cmd/git/clone.go

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
package git
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"os/exec"
7+
"regexp"
8+
"strings"
9+
10+
"github.com/spf13/cobra"
11+
)
12+
13+
var cloneCmd = &cobra.Command{
14+
Use: "clone <url-or-slug> [directory] [-- <git-args>]",
15+
Short: "Clone a GitHub repository using gh repo clone (defaults to SSH)",
16+
Long: `Clone a GitHub repository using the GitHub CLI ('gh repo clone').
17+
18+
This command converts various GitHub URL formats into the owner/repo slug
19+
and invokes 'gh repo clone', which defaults to SSH for better stability
20+
in regions where HTTPS connections to GitHub are unreliable.
21+
22+
Supported inputs:
23+
- https://github.com/owner/repo.git
24+
- git@github.com:owner/repo.git
25+
- github.com/owner/repo
26+
- owner/repo
27+
28+
Extra arguments after '--' are forwarded to 'git clone'.`,
29+
Example: ` spark git clone https://github.com/Nutlope/pdf-to-interactive-lesson.git
30+
spark git clone Nutlope/pdf-to-interactive-lesson
31+
spark git clone https://github.com/owner/repo.git my-dir -- --branch main --depth 1`,
32+
Args: cobra.MinimumNArgs(1),
33+
RunE: func(cmd *cobra.Command, args []string) error {
34+
input := args[0]
35+
36+
slug, err := parseRepoSlug(input)
37+
if err != nil {
38+
return err
39+
}
40+
41+
cmdPath := cmd.CommandPath()
42+
cmdArgs := resolveCmdArgs(cmdPath)
43+
44+
var dir string
45+
var gitArgs []string
46+
if len(cmdArgs) > 1 {
47+
sepIdx := -1
48+
for i, a := range cmdArgs[1:] {
49+
if a == "--" {
50+
sepIdx = i + 1
51+
break
52+
}
53+
}
54+
if sepIdx == -1 {
55+
dir = cmdArgs[1]
56+
} else {
57+
if sepIdx > 1 {
58+
dir = cmdArgs[1]
59+
}
60+
if sepIdx+1 < len(cmdArgs) {
61+
gitArgs = cmdArgs[sepIdx+1:]
62+
}
63+
}
64+
}
65+
66+
ghArgs := []string{"repo", "clone", slug}
67+
if dir != "" {
68+
ghArgs = append(ghArgs, dir)
69+
}
70+
if len(gitArgs) > 0 {
71+
ghArgs = append(ghArgs, "--")
72+
ghArgs = append(ghArgs, gitArgs...)
73+
}
74+
75+
fmt.Printf("Cloning %s via gh (SSH)...\n", slug)
76+
c := exec.Command("gh", ghArgs...)
77+
c.Stdout = os.Stdout
78+
c.Stderr = os.Stderr
79+
c.Stdin = os.Stdin
80+
return c.Run()
81+
},
82+
}
83+
84+
func resolveCmdArgs(cmdPath string) []string {
85+
parts := strings.Fields(cmdPath)
86+
if len(parts) == 0 {
87+
return os.Args[1:]
88+
}
89+
if len(os.Args) <= len(parts) {
90+
return nil
91+
}
92+
return os.Args[len(parts):]
93+
}
94+
95+
var repoSlugRegex = regexp.MustCompile(`^(?:https?://(?:www\.)?github\.com/|git@github\.com:|(?:www\.)?github\.com/)?([^/]+/[^/]+?)(?:\.git)?/?$`)
96+
97+
func parseRepoSlug(input string) (string, error) {
98+
input = strings.TrimSpace(input)
99+
input = strings.TrimSuffix(input, "/")
100+
101+
matches := repoSlugRegex.FindStringSubmatch(input)
102+
if len(matches) == 2 {
103+
return matches[1], nil
104+
}
105+
106+
return "", fmt.Errorf("unable to parse GitHub repository from %q", input)
107+
}
108+
109+
func init() {
110+
GitCmd.AddCommand(cloneCmd)
111+
}

cmd/git/clone_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package git
2+
3+
import "testing"
4+
5+
func TestParseRepoSlug(t *testing.T) {
6+
tests := []struct {
7+
input string
8+
want string
9+
wantErr bool
10+
}{
11+
{"https://github.com/Nutlope/pdf-to-interactive-lesson.git", "Nutlope/pdf-to-interactive-lesson", false},
12+
{"https://github.com/owner/repo", "owner/repo", false},
13+
{"http://github.com/owner/repo.git", "owner/repo", false},
14+
{"git@github.com:owner/repo.git", "owner/repo", false},
15+
{"github.com/owner/repo", "owner/repo", false},
16+
{"owner/repo", "owner/repo", false},
17+
{"owner/repo/", "owner/repo", false},
18+
{"https://www.github.com/owner/repo.git", "owner/repo", false},
19+
{"invalid-url", "", true},
20+
{"", "", true},
21+
{"https://github.com/", "", true},
22+
}
23+
24+
for _, tt := range tests {
25+
t.Run(tt.input, func(t *testing.T) {
26+
got, err := parseRepoSlug(tt.input)
27+
if (err != nil) != tt.wantErr {
28+
t.Errorf("parseRepoSlug(%q) error = %v, wantErr %v", tt.input, err, tt.wantErr)
29+
return
30+
}
31+
if got != tt.want {
32+
t.Errorf("parseRepoSlug(%q) = %q, want %q", tt.input, got, tt.want)
33+
}
34+
})
35+
}
36+
}

cmd/git/git.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ var GitCmd = &cobra.Command{
1111
1212
This includes:
1313
- update: Update multiple git repositories
14+
- clone: Clone a GitHub repository via gh repo clone (SSH by default)
1415
- init: Initialize a git repo and create a GitHub remote
1516
- submodule: Add local repos or URLs as git submodules
1617
- sync: Sync all submodules to the latest versions

docs/usage/git.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
```bash
88
spark git update # 更新所有仓库
9+
spark git clone <url-or-slug> [dir] [-- <git-args>] # 克隆 GitHub 仓库(默认 SSH)
910
spark git submodule add [-p <path>] # 添加现有仓库为子模块
1011
spark git submodule add <repo-url> [-n <name>] # 添加远程仓库为子模块
1112
spark git submodule init [-j <n>] [-r] # 初始化(克隆)所有子模块
@@ -24,6 +25,36 @@ spark git push-all # 提交并推送所有仓库的
2425

2526
---
2627

28+
## spark git clone
29+
30+
使用 GitHub CLI (`gh repo clone`) 克隆单个 GitHub 仓库,默认通过 SSH 连接,可减少 HTTPS 被干扰或不稳定导致的失败。
31+
32+
| 输入格式 | 示例 |
33+
|---|---|
34+
| HTTPS URL | `https://github.com/owner/repo.git` |
35+
| SSH URL | `git@github.com:owner/repo.git` |
36+
| 简写域名 | `github.com/owner/repo` |
37+
| owner/repo | `Nutlope/pdf-to-interactive-lesson` |
38+
39+
```bash
40+
# 从 HTTPS URL 克隆
41+
spark git clone https://github.com/Nutlope/pdf-to-interactive-lesson.git
42+
43+
# 使用 owner/repo 简写
44+
spark git clone Nutlope/pdf-to-interactive-lesson
45+
46+
# 指定本地目录名
47+
spark git clone Nutlope/pdf-to-interactive-lesson my-lesson
48+
49+
# 透传额外参数给 git clone(需用 -- 分隔)
50+
spark git clone https://github.com/owner/repo.git -- --branch main --depth 1
51+
spark git clone https://github.com/owner/repo.git my-dir -- --branch main --depth 1
52+
```
53+
54+
**实现细节**:命令会把输入解析为 `owner/repo`,然后执行 `gh repo clone owner/repo [directory] [-- <git-args>...]``gh` 在已登录状态下默认使用 SSH URL。
55+
56+
---
57+
2758
## spark git update
2859

2960
批量更新指定目录下的所有 Git 仓库到最新版本。

0 commit comments

Comments
 (0)