basic clone capabilities
This commit is contained in:
commit
d3647b71d7
3 changed files with 227 additions and 0 deletions
3
go.mod
Normal file
3
go.mod
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
module git.juancwu.dev/juancwu/lazyclone
|
||||||
|
|
||||||
|
go 1.25.1
|
||||||
119
main.go
Normal file
119
main.go
Normal file
|
|
@ -0,0 +1,119 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const GHQDir = "ghq"
|
||||||
|
|
||||||
|
// GitURL holds the parsed structure of the repository
|
||||||
|
type GitURL struct {
|
||||||
|
Original string
|
||||||
|
Host string
|
||||||
|
Path string // This includes namespace/repo (e.g. "juancwu/tools")
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
args := os.Args[1:]
|
||||||
|
|
||||||
|
if len(args) != 1 {
|
||||||
|
printUsage()
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
rawURL := args[0]
|
||||||
|
|
||||||
|
repoInfo, err := parseGitURL(rawURL)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error parsing URL: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
homeDir, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error getting home directory: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
targetDir := filepath.Join(homeDir, GHQDir, repoInfo.Host, repoInfo.Path)
|
||||||
|
|
||||||
|
if err := cloneRepo(repoInfo.Original, targetDir); err != nil {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseGitURL extracts the Host and Path from SSH (SCP-syntax) or HTTP/Standard URLs
|
||||||
|
func parseGitURL(raw string) (*GitURL, error) {
|
||||||
|
raw = strings.TrimSpace(raw)
|
||||||
|
|
||||||
|
// Regex for SCP-like syntax: user@host:path/to/repo.git
|
||||||
|
// Matches: git@github.com:user/repo.git
|
||||||
|
scpSyntax := regexp.MustCompile(`^[\w-]+@([^:]+):(.+?)(?:\.git)?$`)
|
||||||
|
|
||||||
|
if match := scpSyntax.FindStringSubmatch(raw); match != nil {
|
||||||
|
return &GitURL{
|
||||||
|
Original: raw,
|
||||||
|
Host: match[1],
|
||||||
|
Path: match[2],
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Standard URL parsing for http, https, ssh://
|
||||||
|
u, err := url.Parse(raw)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid URL format: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if u.Host == "" {
|
||||||
|
return nil, fmt.Errorf("URL is missing a host (e.g. github.com)")
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanPath := strings.TrimPrefix(u.Path, "/")
|
||||||
|
cleanPath = strings.TrimSuffix(cleanPath, ".git")
|
||||||
|
|
||||||
|
return &GitURL{
|
||||||
|
Original: raw,
|
||||||
|
Host: u.Host,
|
||||||
|
Path: cleanPath,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func cloneRepo(url, targetDir string) error {
|
||||||
|
// Check if directory already exists
|
||||||
|
if _, err := os.Stat(targetDir); !os.IsNotExist(err) {
|
||||||
|
fmt.Printf("Directory already exists: %s\n", targetDir)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Cloning into: %s\n", targetDir)
|
||||||
|
|
||||||
|
cmd := exec.Command("git", "clone", url, targetDir)
|
||||||
|
// Pipe git output directly to the user's terminal
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
cmd.Stdin = os.Stdin
|
||||||
|
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("git clone failed: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func printUsage() {
|
||||||
|
fmt.Println(`Usage: cl <repository_url>
|
||||||
|
|
||||||
|
Description:
|
||||||
|
Clones a git repository into a structured directory tree:
|
||||||
|
~/ghq/<domain>/<user>/<repo>
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
cl git@github.com:user/project.git
|
||||||
|
cl https://gitlab.com/group/subgroup/project.git
|
||||||
|
cl https://git.company.corp/devops/tools.git`)
|
||||||
|
}
|
||||||
105
parseurl_test.go
Normal file
105
parseurl_test.go
Normal file
|
|
@ -0,0 +1,105 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseGitURL(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
inputURL string
|
||||||
|
expectedHost string
|
||||||
|
expectedPath string
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
// 1. Standard HTTPS (Github)
|
||||||
|
{
|
||||||
|
name: "HTTPS Github",
|
||||||
|
inputURL: "https://github.com/juancwu/dotfiles",
|
||||||
|
expectedHost: "github.com",
|
||||||
|
expectedPath: "juancwu/dotfiles",
|
||||||
|
},
|
||||||
|
// 2. SSH Short Syntax (Github)
|
||||||
|
{
|
||||||
|
name: "SSH Github",
|
||||||
|
inputURL: "git@github.com:juancwu/dotfiles",
|
||||||
|
expectedHost: "github.com",
|
||||||
|
expectedPath: "juancwu/dotfiles",
|
||||||
|
},
|
||||||
|
// 3. SSH with .git extension
|
||||||
|
{
|
||||||
|
name: "SSH Github with .git",
|
||||||
|
inputURL: "git@github.com:juancwu/dotfiles.git",
|
||||||
|
expectedHost: "github.com",
|
||||||
|
expectedPath: "juancwu/dotfiles",
|
||||||
|
},
|
||||||
|
// 4. SSH with Tilde (Home directory reference)
|
||||||
|
// Note: The parser captures the path literally.
|
||||||
|
{
|
||||||
|
name: "SSH Github Tilde",
|
||||||
|
inputURL: "git@github.com:~/juancwu/dotfiles",
|
||||||
|
expectedHost: "github.com",
|
||||||
|
expectedPath: "~/juancwu/dotfiles",
|
||||||
|
},
|
||||||
|
// 5. Custom Domain / Subdomain
|
||||||
|
{
|
||||||
|
name: "Custom Domain SSH",
|
||||||
|
inputURL: "git@git.juancwu.dev:juancwu/dotfiles",
|
||||||
|
expectedHost: "git.juancwu.dev",
|
||||||
|
expectedPath: "juancwu/dotfiles",
|
||||||
|
},
|
||||||
|
// 6. Deeply Nested (Gitlab Subgroups)
|
||||||
|
{
|
||||||
|
name: "Gitlab Nested Group HTTPS",
|
||||||
|
inputURL: "https://gitlab.com/organization/backend/services/auth.git",
|
||||||
|
expectedHost: "gitlab.com",
|
||||||
|
expectedPath: "organization/backend/services/auth",
|
||||||
|
},
|
||||||
|
// 7. Standard SSH Scheme (ssh://)
|
||||||
|
{
|
||||||
|
name: "SSH Scheme Standard",
|
||||||
|
inputURL: "ssh://git@github.com/juancwu/dotfiles",
|
||||||
|
expectedHost: "github.com",
|
||||||
|
expectedPath: "juancwu/dotfiles",
|
||||||
|
},
|
||||||
|
// 8. IP Address Host
|
||||||
|
{
|
||||||
|
name: "IP Address Host",
|
||||||
|
inputURL: "http://192.168.1.50/user/repo.git",
|
||||||
|
expectedHost: "192.168.1.50",
|
||||||
|
expectedPath: "user/repo",
|
||||||
|
},
|
||||||
|
// 9. Invalid URL
|
||||||
|
{
|
||||||
|
name: "Empty String",
|
||||||
|
inputURL: "",
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result, err := parseGitURL(tt.inputURL)
|
||||||
|
|
||||||
|
if tt.expectError {
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("Expected error for input '%s', but got nil", tt.inputURL)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Unexpected error for input '%s': %v", tt.inputURL, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.Host != tt.expectedHost {
|
||||||
|
t.Errorf("Host mismatch.\nExpected: %s\nGot: %s", tt.expectedHost, result.Host)
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.Path != tt.expectedPath {
|
||||||
|
t.Errorf("Path mismatch.\nExpected: %s\nGot: %s", tt.expectedPath, result.Path)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue