package models
import (
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"sort"
"strconv"
"strings"
)
const (
tree = "tree"
blob = "blob"
defaultDesc = "Unnamed repository; edit this file 'description' to name the repository.\n"
)
type GitObject struct {
ObjectType string
Path string
}
type ObjectSort []GitObject
func (o ObjectSort) Len() int { return len(o) }
func (o ObjectSort) Swap(i, j int) { o[i], o[j] = o[j], o[i] }
func (o ObjectSort) Less(i, j int) bool {
if o[i].ObjectType == tree && o[j].ObjectType == blob {
return true
}
if o[j].ObjectType == tree && o[i].ObjectType == blob {
return false
}
// Same ObjectType
return o[i].Path < o[j].Path
}
type Repo struct {
Name string
Description string
}
type RepoModel struct {
RepoDir string
}
func (m *RepoModel) IsRepo(name string) bool {
path := filepath.Join(m.RepoDir, name)
finfo, err := os.Stat(path)
if err != nil {
return false
}
if !finfo.IsDir() {
return false
}
// This command prints false to stdout if inside a git work tree,
// but only errors if we're not in a repo at all
cmd := exec.Command("git", "rev-parse", "--is-inside-git-dir")
cmd.Dir = path
err = cmd.Run()
if err != nil {
return false
}
return true
}
func (m *RepoModel) getRepoDesc(name string) string {
cmd := exec.Command("git", "rev-parse", "--absolute-git-dir")
cmd.Dir = filepath.Join(m.RepoDir, name)
dotGitDirB, err := cmd.Output()
if err != nil {
return ""
}
dotGitDir := strings.TrimSpace(string(dotGitDirB))
desc := ""
descBytes, err := os.ReadFile(filepath.Join(dotGitDir, "description"))
if err == nil {
desc = string(descBytes)
if desc == defaultDesc {
return ""
}
}
return desc
}
func (m *RepoModel) getTrueName(name string) (string, error) {
if m.IsRepo(name) {
return name, nil
}
if m.IsRepo(name + ".git") {
return name + ".git", nil
}
return "", ErrNoRepo
}
func (m *RepoModel) GetByName(name string) (Repo, error) {
var r Repo
r.Name = name
if !m.IsRepo(name) {
if m.IsRepo(name + ".git") {
name += ".git"
} else {
return Repo{}, ErrNoRepo
}
}
r.Description = m.getRepoDesc(name)
return r, nil
}
func (m *RepoModel) GetAll() ([]Repo, error) {
entries, err := os.ReadDir(m.RepoDir)
if err != nil {
return nil, err
}
repos := []Repo{}
for _, e := range entries {
name := e.Name()
if !m.IsRepo(name) {
if m.IsRepo(name + ".git") {
name += ".git"
} else {
continue
}
}
desc := m.getRepoDesc(name)
repos = append(repos, Repo{strings.TrimSuffix(e.Name(), ".git"), desc})
}
return repos, nil
}
func (m *RepoModel) HasCommits(r Repo) bool {
var path string
if m.IsRepo(r.Name + ".git") {
path = filepath.Join(m.RepoDir, r.Name+".git")
} else if m.IsRepo(r.Name) {
path = filepath.Join(m.RepoDir, r.Name)
} else {
return false
}
cmd := exec.Command("git", "log")
cmd.Dir = path
err := cmd.Run()
if err != nil {
return false
}
return true
}
func (m *RepoModel) ListRepoTree(r Repo, tree string, dir string) ([]string, error) {
var path string
if m.IsRepo(r.Name + ".git") {
path = filepath.Join(m.RepoDir, r.Name+".git")
} else if m.IsRepo(r.Name) {
path = filepath.Join(m.RepoDir, r.Name)
} else {
return nil, ErrNoRepo
}
commitDirPath := ""
if strings.TrimSpace(tree) == "" {
commitDirPath += "HEAD"
}
if strings.TrimSpace(dir) != "" {
commitDirPath += ":" + dir
}
cmd := exec.Command("git", "ls-tree", "--format=\"%(objecttype):%(path)\"", commitDirPath)
cmd.Dir = path
out, err := cmd.Output()
if err != nil {
return nil, err
}
lines := strings.Split(string(out), "\n")
for i, line := range lines {
parts := strings.Split(line, ":")
if parts[0] == "tree" {
lines[i] = parts[1] + "/"
} else {
lines[i] = parts[1]
}
}
return lines, nil
}
func (m *RepoModel) GetHeadBranch(repoName string) (string, error) {
trueName, err := m.getTrueName(repoName)
if err != nil {
return "", err
}
cmd := exec.Command("git", "symbolic-ref", "HEAD")
cmd.Dir = filepath.Join(m.RepoDir, trueName)
out, err := cmd.Output()
if err != nil {
return "", err
}
return strings.TrimSpace(filepath.Base(string(out))), nil
}
// This should return err on error, I am being lazy by writing it this way
// Will fix when it becomes a problem
func (m *RepoModel) CountRevs(r Repo) int {
trueName, err := m.getTrueName(r.Name)
if err != nil {
return 0
}
cmd := exec.Command("git", "rev-list", "--count", "HEAD")
cmd.Dir = filepath.Join(m.RepoDir, trueName)
out, err := cmd.Output()
if err != nil {
return 0
}
count, err := strconv.Atoi(strings.TrimSpace(string(out)))
if err != nil {
return 0
}
return count
}
func (m *RepoModel) GetTreeObjects(repoName string, tree string, path string) ([]GitObject, error) {
trueName, err := m.getTrueName(repoName)
if err != nil {
return []GitObject{}, err
}
treePath := tree
if strings.TrimSpace(tree) == "" {
treePath += "HEAD"
}
if strings.TrimSpace(path) != "" {
treePath += ":" + path
}
cmd := exec.Command("git", "ls-tree", "-t", "--format=%(objecttype):%(path)", treePath)
cmd.Dir = filepath.Join(m.RepoDir, trueName)
out, err := cmd.Output()
if err != nil {
return nil, err
}
objs := []GitObject{}
lines := strings.SplitSeq(string(out), "\n")
for line := range lines {
if strings.Contains(line, ":") {
parts := strings.Split(line, ":")
objs = append(objs, GitObject{ObjectType: parts[0], Path: parts[1]})
}
}
sort.Sort(ObjectSort(objs))
return objs, nil
}
func (m *RepoModel) GetBlobObjectText(repoName string, tree string, path string) (string, error) {
trueName, err := m.getTrueName(repoName)
if err != nil {
return "", err
}
treePath := ""
tree = strings.TrimSpace(tree)
if tree == "" {
treePath += "HEAD"
} else {
treePath += tree
}
if strings.TrimSpace(path) == "" {
return "", errors.New("No path provided")
}
treePath += ":" + path
cmd := exec.Command("git", "show", treePath)
fmt.Println("Command: ", cmd.String())
cmd.Dir = filepath.Join(m.RepoDir, trueName)
out, err := cmd.Output()
if err != nil {
return "", err
}
return string(out), nil
}
func (m *RepoModel) ListRepoHead(r Repo) ([]string, error) {
return m.ListRepoTree(r, "", "")
}
func (m *RepoModel) CreateRepo(name, description string) error {
// check if directory already exists
if _, err := os.Stat(filepath.Join(m.RepoDir, name)); err != nil {
if !os.IsNotExist(err) {
return err
}
} else {
return ErrDuplicateRepo
}
cmd := exec.Command("git", "init", "--bare", name)
cmd.Dir = m.RepoDir
err := cmd.Run()
if err != nil {
return err
}
dpath := filepath.Join(m.RepoDir, name, "description")
df, err := os.Create(dpath)
// TODO: Identify if the error occured here or later and proceed
// with the knowledge that the repo exists
if err != nil {
return err
}
_, err = df.WriteString(description)
if err != nil {
return err
}
return nil
}