package core import ( "errors" "fmt" "io" "io/fs" "os" "os/exec" "path/filepath" "sort" "strings" ) type App struct { HomeDir string ReposDir string Stdout io.Writer Stderr io.Writer Verbose bool } func New(stdout io.Writer, stderr io.Writer) (*App, error) { home, err := os.UserHomeDir() if err != nil { return nil, fmt.Errorf("resolve home directory: %w", err) } return &App{ HomeDir: home, ReposDir: filepath.Join(home, ".homesick", "repos"), Stdout: stdout, Stderr: stderr, }, nil } func (a *App) Version(version string) error { _, err := fmt.Fprintln(a.Stdout, version) return err } func (a *App) ShowPath(castle string) error { _, err := fmt.Fprintln(a.Stdout, filepath.Join(a.ReposDir, castle)) return err } func (a *App) Clone(uri string, destination string) error { if uri == "" { return errors.New("clone requires URI") } if destination == "" { destination = deriveDestination(uri) } if destination == "" { return fmt.Errorf("unable to derive destination from uri %q", uri) } if err := os.MkdirAll(a.ReposDir, 0o755); err != nil { return fmt.Errorf("create repos directory: %w", err) } destinationPath := filepath.Join(a.ReposDir, destination) if fi, err := os.Stat(uri); err == nil && fi.IsDir() { if err := os.Symlink(uri, destinationPath); err != nil { return fmt.Errorf("symlink local castle: %w", err) } return nil } if err := runGit(a.ReposDir, "clone", "-q", "--config", "push.default=upstream", "--recursive", uri, destination); err != nil { return err } return nil } func (a *App) List() error { if err := os.MkdirAll(a.ReposDir, 0o755); err != nil { return err } var castles []string err := filepath.WalkDir(a.ReposDir, func(path string, d fs.DirEntry, walkErr error) error { if walkErr != nil { return walkErr } if !d.IsDir() || d.Name() != ".git" { return nil } castleRoot := filepath.Dir(path) rel, err := filepath.Rel(a.ReposDir, castleRoot) if err != nil { return err } castles = append(castles, rel) return filepath.SkipDir }) if err != nil { return err } sort.Strings(castles) for _, castle := range castles { castleRoot := filepath.Join(a.ReposDir, castle) remote, remoteErr := gitOutput(castleRoot, "config", "remote.origin.url") if remoteErr != nil { remote = "" } _, writeErr := fmt.Fprintf(a.Stdout, "%s %s\n", castle, strings.TrimSpace(remote)) if writeErr != nil { return writeErr } } return nil } func (a *App) Status(castle string) error { return runGit(filepath.Join(a.ReposDir, castle), "status") } func (a *App) Diff(castle string) error { return runGit(filepath.Join(a.ReposDir, castle), "diff") } func (a *App) Link(string) error { return errors.New("link is not implemented in Go yet") } func (a *App) Unlink(string) error { return errors.New("unlink is not implemented in Go yet") } func (a *App) Track(string, string) error { return errors.New("track is not implemented in Go yet") } func runGit(dir string, args ...string) error { cmd := exec.Command("git", args...) cmd.Dir = dir cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { return fmt.Errorf("git %s failed: %w", strings.Join(args, " "), err) } return nil } func gitOutput(dir string, args ...string) (string, error) { cmd := exec.Command("git", args...) cmd.Dir = dir out, err := cmd.Output() if err != nil { return "", err } return string(out), nil } func deriveDestination(uri string) string { candidate := strings.TrimSpace(uri) candidate = strings.TrimPrefix(candidate, "https://github.com/") candidate = strings.TrimPrefix(candidate, "http://github.com/") candidate = strings.TrimPrefix(candidate, "git://github.com/") if strings.HasPrefix(candidate, "file://") { candidate = strings.TrimPrefix(candidate, "file://") } candidate = strings.TrimSuffix(candidate, ".git") candidate = strings.TrimSuffix(candidate, "/") if candidate == "" { return "" } parts := strings.Split(candidate, "/") last := parts[len(parts)-1] if strings.Contains(last, ":") { a := strings.Split(last, ":") last = a[len(a)-1] } return last }