diff --git a/main.go b/main.go index aed252b..6a41ef7 100644 --- a/main.go +++ b/main.go @@ -112,6 +112,25 @@ func main() { root = "." } + // Handle GitHub URLs - clone to temp dir (but prefer local paths if they exist) + var tempDir string + var remoteURL, repoName string + _, localPathErr := os.Stat(root) + if isGitHubURL(root) && localPathErr != nil { + // Only clone if it looks like a URL AND doesn't exist locally + // This preserves ~/go/src/github.com/user/repo style paths + remoteURL = root + repoName = extractRepoName(root) + var err error + tempDir, err = cloneRepo(root) + if err != nil { + fmt.Fprintf(os.Stderr, "Error cloning repo: %v\n", err) + os.Exit(1) + } + defer os.RemoveAll(tempDir) + root = tempDir + } + absRoot, err := filepath.Abs(root) if err != nil { fmt.Fprintf(os.Stderr, "Error getting absolute path: %v\n", err) @@ -204,15 +223,17 @@ func main() { } project := scanner.Project{ - Root: absRoot, - Mode: mode, - Animate: *animateMode, - Files: files, - DiffRef: activeDiffRef, - Impact: impact, - Depth: *depthLimit, - Only: only, - Exclude: exclude, + Root: absRoot, + Name: repoName, + RemoteURL: remoteURL, + Mode: mode, + Animate: *animateMode, + Files: files, + DiffRef: activeDiffRef, + Impact: impact, + Depth: *depthLimit, + Only: only, + Exclude: exclude, } // Render or output JSON @@ -440,3 +461,51 @@ func runDaemon(root string) { daemon.Stop() watch.RemovePID(root) } + +// isGitHubURL checks if the input looks like a GitHub repo URL +func isGitHubURL(s string) bool { + s = strings.ToLower(s) + return strings.HasPrefix(s, "github.com/") || + strings.HasPrefix(s, "https://github.com/") || + strings.HasPrefix(s, "http://github.com/") || + strings.HasPrefix(s, "gitlab.com/") || + strings.HasPrefix(s, "https://gitlab.com/") +} + +// cloneRepo clones a git repo to a temp directory (shallow clone) +func cloneRepo(url string) (string, error) { + // Normalize URL + if !strings.HasPrefix(url, "https://") && !strings.HasPrefix(url, "http://") { + url = "https://" + url + } + + // Create temp dir + tempDir, err := os.MkdirTemp("", "codemap-") + if err != nil { + return "", fmt.Errorf("failed to create temp dir: %w", err) + } + + // Shallow clone (quiet) + cmd := exec.Command("git", "clone", "--depth", "1", "--single-branch", "-q", url, tempDir) + if err := cmd.Run(); err != nil { + os.RemoveAll(tempDir) + return "", fmt.Errorf("git clone failed: %w", err) + } + + return tempDir, nil +} + +// extractRepoName extracts "owner/repo" from a GitHub URL +func extractRepoName(url string) string { + // Remove protocol + url = strings.TrimPrefix(url, "https://") + url = strings.TrimPrefix(url, "http://") + // Remove host + url = strings.TrimPrefix(url, "github.com/") + url = strings.TrimPrefix(url, "gitlab.com/") + // Remove trailing .git + url = strings.TrimSuffix(url, ".git") + // Remove trailing slashes + url = strings.TrimSuffix(url, "/") + return url +} diff --git a/render/skyline.go b/render/skyline.go index 20cd9f9..ee9ee0b 100644 --- a/render/skyline.go +++ b/render/skyline.go @@ -209,7 +209,10 @@ func createBuildings(sorted []extAgg, width int) []building { // Skyline renders the city skyline visualization func Skyline(project scanner.Project, animate bool) { files := project.Files - projectName := filepath.Base(project.Root) + projectName := project.Name + if projectName == "" { + projectName = filepath.Base(project.Root) + } width, _, err := term.GetSize(int(os.Stdout.Fd())) if err != nil || width <= 0 { diff --git a/render/tree.go b/render/tree.go index dfd78d3..023c07b 100644 --- a/render/tree.go +++ b/render/tree.go @@ -106,7 +106,10 @@ func formatSize(size int64) string { // Tree renders the file tree to stdout func Tree(project scanner.Project) { files := project.Files - projectName := filepath.Base(project.Root) + projectName := project.Name + if projectName == "" { + projectName = filepath.Base(project.Root) + } isDiffMode := project.DiffRef != "" maxDepth := project.Depth // 0 = unlimited @@ -188,6 +191,12 @@ func Tree(project scanner.Project) { fmt.Printf("│ %-*s │\n", innerWidth-2, extLine) } + // Remote URL indicator + if project.RemoteURL != "" { + remoteLine := fmt.Sprintf("↳ %s", project.RemoteURL) + fmt.Printf("│ %-*s │\n", innerWidth-2, remoteLine) + } + fmt.Printf("╰%s╯\n", strings.Repeat("─", innerWidth)) // Build and render tree diff --git a/scanner/types.go b/scanner/types.go index ad31b89..15dc988 100644 --- a/scanner/types.go +++ b/scanner/types.go @@ -17,15 +17,17 @@ type FileInfo struct { // Project represents the root of the codebase for tree/skyline mode. type Project struct { - Root string `json:"root"` - Mode string `json:"mode"` - Animate bool `json:"animate"` - Files []FileInfo `json:"files"` - DiffRef string `json:"diff_ref,omitempty"` - Impact []ImpactInfo `json:"impact,omitempty"` - Depth int `json:"depth,omitempty"` // Max tree depth (0 = unlimited) - Only []string `json:"only,omitempty"` // Extension filter (e.g., ["swift", "go"]) - Exclude []string `json:"exclude,omitempty"` // Exclusion patterns (e.g., [".xcassets", "Fonts"]) + Root string `json:"root"` + Name string `json:"name,omitempty"` // Display name (overrides filepath.Base(Root)) + RemoteURL string `json:"remote_url,omitempty"` // Source URL if cloned from remote + Mode string `json:"mode"` + Animate bool `json:"animate"` + Files []FileInfo `json:"files"` + DiffRef string `json:"diff_ref,omitempty"` + Impact []ImpactInfo `json:"impact,omitempty"` + Depth int `json:"depth,omitempty"` // Max tree depth (0 = unlimited) + Only []string `json:"only,omitempty"` // Extension filter (e.g., ["swift", "go"]) + Exclude []string `json:"exclude,omitempty"` // Exclusion patterns (e.g., [".xcassets", "Fonts"]) } // FileAnalysis holds extracted info about a single file for deps mode.