Goit

Simple and lightweight Git web server
git clone http://git.omkov.net/Goit
Log | Tree | Refs | README | Download

Goit/src/goit/repo.go (330 lines, 7.2 KiB) -rw-r--r-- blame download

0123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329
// Copyright (C) 2023, Jakob Wakeling
// All rights reserved.

package goit

import (
	"database/sql"
	"errors"
	"log"
	"os"
	"path/filepath"
	"strings"

	"github.com/Jamozed/Goit/src/util"
	"github.com/go-git/go-git/v5"
	gitconfig "github.com/go-git/go-git/v5/config"
	"github.com/go-git/go-git/v5/plumbing"
)

type Repo struct {
	Id            int64      `json:"id"`
	OwnerId       int64      `json:"owner_id"`
	Name          string     `json:"name"`
	Description   string     `json:"description"`
	DefaultBranch string     `json:"default_branch"`
	Upstream      string     `json:"upstream"`
	Visibility    Visibility `json:"visibility"`
	IsMirror      bool       `json:"is_mirror"`
}

type Visibility int32

const (
	Public  Visibility = 0
	Private Visibility = 1
	Limited Visibility = 2
)

func VisibilityFromString(s string) Visibility {
	switch strings.ToLower(s) {
	case "public":
		return Public
	case "private":
		return Private
	case "limited":
		return Limited
	default:
		return -1
	}
}

func (v Visibility) String() string {
	return [...]string{"public", "private", "limited"}[v]
}

func GetRepos() ([]Repo, error) {
	repos := []Repo{}

	rows, err := db.Query(
		"SELECT id, owner_id, name, description, default_branch, upstream, visibility, is_mirror FROM repos",
	)
	if err != nil {
		return nil, err
	}

	defer rows.Close()

	for rows.Next() {
		r := Repo{}
		if err := rows.Scan(
			&r.Id, &r.OwnerId, &r.Name, &r.Description, &r.DefaultBranch, &r.Upstream, &r.Visibility, &r.IsMirror,
		); err != nil {
			return nil, err
		}

		repos = append(repos, r)
	}

	if rows.Err() != nil {
		return nil, err
	}

	return repos, nil
}

func GetRepo(rid int64) (*Repo, error) {
	r := &Repo{}

	if err := db.QueryRow(
		`SELECT id, owner_id, name, description, default_branch, upstream, visibility, is_mirror FROM repos
		WHERE id = ?`, rid,
	).Scan(
		&r.Id, &r.OwnerId, &r.Name, &r.Description, &r.DefaultBranch, &r.Upstream, &r.Visibility, &r.IsMirror,
	); err != nil {
		if !errors.Is(err, sql.ErrNoRows) {
			return nil, err
		}

		return nil, nil
	} else {
		return r, nil
	}
}

func GetRepoByName(name string) (*Repo, error) {
	r := &Repo{}

	if err := db.QueryRow(
		`SELECT id, owner_id, name, description, default_branch, upstream, visibility, is_mirror FROM repos
		WHERE name = ?`, name,
	).Scan(
		&r.Id, &r.OwnerId, &r.Name, &r.Description, &r.DefaultBranch, &r.Upstream, &r.Visibility, &r.IsMirror,
	); err != nil {
		if !errors.Is(err, sql.ErrNoRows) {
			return nil, err
		}

		return nil, nil
	}

	return r, nil
}

func CreateRepo(repo Repo) (int64, error) {
	tx, err := db.Begin()
	if err != nil {
		return -1, err
	}
	defer tx.Rollback()

	res, err := tx.Exec(
		`INSERT INTO repos (owner_id, name, name_lower, description, default_branch, upstream, visibility, is_mirror)
		VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, repo.OwnerId, repo.Name, strings.ToLower(repo.Name), repo.Description,
		repo.DefaultBranch, repo.Upstream, repo.Visibility, repo.IsMirror,
	)
	if err != nil {
		return -1, err
	}

	r, err := git.PlainInit(RepoPath(repo.Name, true), true)
	if err != nil {
		return -1, err
	}

	ref := plumbing.NewSymbolicReference(plumbing.HEAD, plumbing.NewBranchReferenceName(repo.DefaultBranch))
	if err := r.Storer.SetReference(ref); err != nil {
		os.RemoveAll(RepoPath(repo.Name, true))
		return -1, err
	}

	if repo.Upstream != "" {
		if _, err := r.CreateRemote(&gitconfig.RemoteConfig{
			Name:   "origin",
			URLs:   []string{repo.Upstream},
			Mirror: util.If(repo.IsMirror, true, false),
			Fetch:  []gitconfig.RefSpec{gitconfig.RefSpec("+refs/heads/*:refs/heads/*")},
		}); err != nil {
			os.RemoveAll(RepoPath(repo.Name, true))
			return -1, err
		}
	}

	if err := tx.Commit(); err != nil {
		os.RemoveAll(RepoPath(repo.Name, true))
		return -1, err
	}

	rid, _ := res.LastInsertId()
	return rid, nil
}

func DelRepo(rid int64) error {
	repo, err := GetRepo(rid)
	if err != nil {
		return err
	}

	if err := os.RemoveAll(RepoPath(repo.Name, true)); err != nil {
		return err
	}

	if _, err := db.Exec("DELETE FROM repos WHERE id = ?", rid); err != nil {
		return err
	}

	Cron.RemoveFor(rid)
	Cron.Update()

	return nil
}

func RepoExists(name string) (bool, error) {
	if err := db.QueryRow(
		"SELECT name FROM repos WHERE name_lower = ?", strings.ToLower(name),
	).Scan(&name); err != nil {
		if !errors.Is(err, sql.ErrNoRows) {
			return false, err
		} else {
			return false, nil
		}
	} else {
		return true, nil
	}
}

func UpdateRepo(rid int64, repo Repo) error {
	old, err := GetRepo(rid)
	if err != nil {
		return err
	}

	tx, err := db.Begin()
	if err != nil {
		return err
	}
	defer tx.Rollback()

	if _, err := tx.Exec(
		`UPDATE repos SET name = ?, name_lower = ?, description = ?, default_branch = ?, upstream = ?, visibility = ?,
		is_mirror = ? WHERE id = ?`, repo.Name, strings.ToLower(repo.Name), repo.Description, repo.DefaultBranch,
		repo.Upstream, repo.Visibility, repo.IsMirror, rid,
	); err != nil {
		return err
	}

	if repo.Name != old.Name {
		if err := os.MkdirAll(filepath.Dir(RepoPath(repo.Name, true)), 0o777); err != nil {
			return err
		}

		if err := os.Rename(RepoPath(old.Name, true), RepoPath(repo.Name, true)); err != nil {
			return err
		}
	}

	var r *git.Repository
	if repo.DefaultBranch != old.DefaultBranch {
		// if r == nil {
		r, err = git.PlainOpen(RepoPath(repo.Name, true))
		if err != nil {
			return err
		}
		// }

		ref := plumbing.NewSymbolicReference(plumbing.HEAD, plumbing.NewBranchReferenceName(repo.DefaultBranch))
		if err := r.Storer.SetReference(ref); err != nil {
			return err
		}
	}

	/* If the upstream URL has been removed, remove the remote */
	if repo.Upstream == "" && old.Upstream != "" {
		if r == nil {
			r, err = git.PlainOpen(RepoPath(repo.Name, true))
			if err != nil {
				return err
			}
		}

		if err := r.DeleteRemote("origin"); err != nil {
			return err
		}
	}

	/* If the upstream URL has been added or changed, update the remote */
	if repo.Upstream != "" && repo.Upstream != old.Upstream {
		if r == nil {
			r, err = git.PlainOpen(RepoPath(repo.Name, true))
			if err != nil {
				return err
			}
		}

		if err := r.DeleteRemote("origin"); err != nil && !errors.Is(err, git.ErrRemoteNotFound) {
			return err
		}

		if _, err := r.CreateRemote(&gitconfig.RemoteConfig{
			Name:   "origin",
			URLs:   []string{repo.Upstream},
			Mirror: util.If(repo.IsMirror, true, false),
			Fetch:  []gitconfig.RefSpec{gitconfig.RefSpec("+refs/heads/*:refs/heads/*")},
		}); err != nil {
			log.Println("[repo/update]", err.Error())
		}
	}

	if err := tx.Commit(); err != nil {
		os.Rename(RepoPath(repo.Name, true), RepoPath(old.Name, true))
		log.Println("[repo/update]", "error while editing, check repo \""+old.Name+"\"/\""+repo.Name+"\"")
		return err
	}

	return nil
}

func ChownRepo(rid int64, uid int64) error {
	if _, err := db.Exec("UPDATE repos SET owner_id = ? WHERE id = ?", uid, rid); err != nil {
		return err
	}

	return nil
}

func Pull(rid int64) error {
	repo, err := GetRepo(rid)
	if err != nil {
		return err
	}

	r, err := git.PlainOpen(RepoPath(repo.Name, true))
	if err != nil {
		return err
	}

	if err := r.Fetch(&git.FetchOptions{}); err != nil {
		return err
	}

	return nil
}

func IsVisible(repo *Repo, auth bool, user *User) bool {
	if repo.Visibility == Public || (repo.Visibility == Limited && auth) || (auth && user.Id == repo.OwnerId) {
		return true
	}

	return false
}