/** * AltGit - altgit * * This file is licensed under the Affero General Public License version 3 or * later. See the COPYING file. * * @author Paolo Lulli * @copyright Paolo Lulli 2026 */ package gitrepo import ( "altgit/internal/config" "fmt" "net/url" "os" "os/exec" "path/filepath" "strings" ) func gitAt(path string, args ...string) (string, error) { cmd := exec.Command("git", append([]string{"-C", path}, args...)...) out, err := cmd.Output() return strings.TrimSpace(string(out)), err } func gitCmdStr(args ...string) string { return strings.Join(args, " ") } func ListRepos(cfg config.RepositoryBrowserConfiguration) []RepoInfo { entries, err := os.ReadDir(cfg.Root) if err != nil { return nil } var repos []RepoInfo for _, e := range entries { if !e.IsDir() { continue } p := filepath.Join(cfg.Root, e.Name()) // Must be a git repo if _, err := os.Stat(filepath.Join(p, ".git")); err != nil { // Also accept bare repos if _, err2 := os.Stat(filepath.Join(p, "HEAD")); err2 != nil { continue } } branch, _ := gitAt(p, "rev-parse", "--abbrev-ref", "HEAD") if branch == "" { branch = "main" } last, _ := gitAt(p, "log", "-1", "--pretty=format:%ar", branch) desc := "" if d, err := os.ReadFile(filepath.Join(p, "description")); err == nil { s := strings.TrimSpace(string(d)) if !strings.HasPrefix(s, "Unnamed repository") { desc = s } } repos = append(repos, RepoInfo{ Name: e.Name(), Description: desc, LastCommit: last, Branch: branch, }) } return repos } func ListBranches(rp string) ([]string, string) { out, err := gitAt(rp, "branch", "--format=%(refname:short)") if err != nil { return nil, "HEAD" } cur, _ := gitAt(rp, "rev-parse", "--abbrev-ref", "HEAD") var branches []string for _, b := range strings.Split(out, "\n") { b = strings.TrimSpace(b) if b != "" { branches = append(branches, b) } } if cur == "" { cur = "HEAD" } return branches, cur } func BuildBranchLinks(all []string, activeBr, repoName, tab string) []BranchLink { var links []BranchLink for _, b := range all { var u string switch tab { case "files": u = fmt.Sprintf("/%s/?branch=%s", repoName, url.QueryEscape(b)) default: u = fmt.Sprintf("/%s/%s?branch=%s", repoName, tab, url.QueryEscape(b)) } links = append(links, BranchLink{Name: b, URL: u, Active: b == activeBr}) } return links } func ListFiles(rp, branch string) ([]FileEntry, string, error) { args := []string{"ls-tree", "-r", "--name-only", branch} out, err := gitAt(rp, args...) cmd := gitCmdStr(args...) if err != nil { return nil, cmd, err } var files []FileEntry for _, l := range strings.Split(out, "\n") { l = strings.TrimSpace(l) if l == "" { continue } countOut, _ := gitAt(rp, "log", "--oneline", "--follow", branch, "--", l) count := 0 if countOut != "" { count = len(strings.Split(countOut, "\n")) } files = append(files, FileEntry{Path: l, CommitCount: count}) } return files, cmd, nil } func ListLog(rp, branch string, limit int) ([]CommitEntry, string, error) { args := []string{"log", "--pretty=format:%H\x1f%s\x1f%an\x1f%ad", "--date=short", fmt.Sprintf("-%d", limit), branch} out, err := gitAt(rp, args...) cmd := gitCmdStr("log", fmt.Sprintf("-%d", limit), "--oneline", branch) if err != nil { return nil, cmd, err } return ParseCommits(out), cmd, nil } func ListRefs(rp string) ([]RefEntry, string, error) { args := []string{"for-each-ref", "--format=%(refname:short)\x1f%(objecttype)\x1f%(objectname:short)\x1f%(objectname)", "refs/heads", "refs/tags"} out, err := gitAt(rp, args...) cmd := gitCmdStr("for-each-ref", "refs/heads", "refs/tags") if err != nil { return nil, cmd, err } var refs []RefEntry for _, line := range strings.Split(out, "\n") { line = strings.TrimSpace(line) if line == "" { continue } parts := strings.SplitN(line, "\x1f", 4) if len(parts) < 4 { continue } kind := "branch" if parts[1] == "tag" { kind = "tag" } refs = append(refs, RefEntry{Kind: kind, Name: parts[0], Short: parts[2], Hash: parts[3]}) } return refs, cmd, nil } func FileHistory(rp, path, branch string) ([]CommitEntry, string, error) { args := []string{"log", "--all", "--follow", "--pretty=format:%H\x1f%s\x1f%an\x1f%ad", "--date=short", branch, "--", path} out, err := gitAt(rp, args...) cmd := gitCmdStr("log", "--all", "--follow", "--oneline", branch, "--", path) if err != nil { return nil, cmd, err } return ParseCommits(out), cmd, nil } func FileAtCommit(rp, hash, path string) (string, string, error) { ref := fmt.Sprintf("%s:%s", hash, path) args := []string{"show", ref} out, err := gitAt(rp, args...) return out, gitCmdStr(args...), err } func CommitMeta(rp, hash string) (string, string) { meta, _ := gitAt(rp, "show", "--no-patch", "--pretty=format:%s\x1f%an\x1f%ae\x1f%ad", "--date=format:%Y-%m-%d %H:%M", hash) return meta, gitCmdStr("show", "--stat", hash) } func CommitFiles(rp, hash string) string { out, _ := gitAt(rp, "diff-tree", "--no-commit-id", "-r", "--name-status", hash) return out } func ParseCommits(out string) []CommitEntry { var commits []CommitEntry for _, line := range strings.Split(out, "\n") { line = strings.TrimSpace(line) if line == "" { continue } parts := strings.SplitN(line, "\x1f", 4) if len(parts) < 4 { continue } short := parts[0] if len(short) > 8 { short = short[:8] } commits = append(commits, CommitEntry{ Hash: parts[0], Short: short, Message: parts[1], Author: parts[2], Date: parts[3], }) } return commits }