server/admin/sync/importer_gitbin.go

350 lines
10 KiB
Go

//go:build !gitgo
// +build !gitgo
package sync
import (
"bytes"
"errors"
"fmt"
"log"
"net/url"
"os"
"os/exec"
"path"
"strings"
"srs.epita.fr/fic-server/libfic"
)
// GitImporter implements an Importer, where files to imports are located
// inside a local directory from your filesystem, backed by git (binary).
type GitImporter struct {
li LocalImporter
Remote string
Branch string
}
func NewGitImporter(li LocalImporter, remote string, branch string) GitImporter {
if len(branch) == 0 {
branch = "master"
}
return GitImporter{
li: li,
Remote: remote,
Branch: branch,
}
}
func (i GitImporter) Id() *string {
cmdshow := exec.Command("git", "-C", i.li.Base, "show")
var outshow bytes.Buffer
cmdshow.Stdout = &outshow
err := cmdshow.Run()
var commit string
if err != nil {
commit = fmt.Sprintf("error (%s)", err.Error())
} else {
commit, err = outshow.ReadString('\n')
if err == nil {
commit = strings.TrimPrefix(commit, "commit ")
} else {
commit = fmt.Sprintf("error (%s)", err.Error())
}
}
return &commit
}
func (i GitImporter) Init() error {
// Check if the directory exists, create it if needed
if err := i.li.Init(); err != nil {
return err
}
// If the directory is empty, clone it
if n, err := countFileInDir(i.li.Base); err != nil {
return err
} else if n == 0 {
args := []string{"clone", "--recursive", "--depth", "1"}
if i.Branch != "" {
args = append(args, "-b", i.Branch)
}
args = append(args, "--shallow-submodules", i.Remote, i.li.Base)
log.Println("Please wait while creating the local git repository...")
cmdclone := exec.Command("git", args...)
stdout, err := cmdclone.CombinedOutput()
if err != nil {
return fmt.Errorf("%w:\n%s", err, stdout)
}
log.Println("Local git repository successfully cloned")
} else if _, err := os.Stat(path.Join(i.li.Base, ".git")); errors.Is(err, os.ErrNotExist) {
log.Println("[ERR] ", i.li.Base, " is not a valid git repository and it cannot be initialized because it's not empty.")
return nil
}
// Check if the .git directory exists, change the origin remote to our
cmdremote := exec.Command("git", "-C", i.li.Base, "remote", "set-url", "origin", i.Remote)
stdout, err := cmdremote.CombinedOutput()
if err != nil {
return fmt.Errorf("%w:\n%s", err, stdout)
}
return nil
}
func (i GitImporter) Sync() error {
oneGitPull.Lock()
defer oneGitPull.Unlock()
log.Println("Synchronizing local git repository...")
cmdfetch := exec.Command("git", "-C", i.li.Base, "fetch", "origin")
stdout, err := cmdfetch.CombinedOutput()
if err != nil {
log.Printf("Git repository fetch failed: %s\n%s", err, stdout)
return fmt.Errorf("%w:\n%s", err, stdout)
}
cmdclean := exec.Command("git", "-C", i.li.Base, "clean", "-xfde", "*_MERGED")
stdout, err = cmdclean.CombinedOutput()
if err != nil {
log.Printf("Local git repository clean failed: %s\n%s", err, stdout)
return fmt.Errorf("%w:\n%s", err, stdout)
}
if _, err := os.Stat(path.Join(i.li.Base, ".gitmodules")); !errors.Is(err, os.ErrNotExist) {
// We have submodules, clean it
cmdsubclean := exec.Command("git", "-C", i.li.Base, "submodule", "foreach", "--recursive", "git", "clean", "-xfde", "*_MERGED")
stdout, err = cmdsubclean.CombinedOutput()
if err != nil {
log.Printf("Local git repository submodules clean failed: %s\n%s", err, stdout)
return fmt.Errorf("%w:\n%s", err, stdout)
}
// Start by a light hard reset (without submodules, in order to init new ones)
cmdreset := exec.Command("git", "-C", i.li.Base, "reset", "--hard", "origin/"+i.Branch)
stdout, err = cmdreset.CombinedOutput()
if err != nil {
log.Printf("Local git repository reset failed: %s\n%s", err, stdout)
return fmt.Errorf("%w:\n%s", err, stdout)
}
cmdsubinit := exec.Command("git", "-C", i.li.Base, "submodule", "init")
stdout, err = cmdsubinit.CombinedOutput()
if err != nil {
log.Printf("Local git repository submodule init failed: %s\n%s", err, stdout)
return fmt.Errorf("%w:\n%s", err, stdout)
}
cmdsubupdate := exec.Command("git", "-C", i.li.Base, "submodule", "update")
stdout, err = cmdsubupdate.CombinedOutput()
if err != nil {
log.Printf("Local git repository submodule update failed: %s\n%s", err, stdout)
}
}
cmdreset := exec.Command("git", "-C", i.li.Base, "reset", "--hard", "--recurse-submodule", "origin/"+i.Branch)
stdout, err = cmdreset.CombinedOutput()
if err != nil {
log.Printf("Local git repository reset failed: %s\n%s", err, stdout)
return fmt.Errorf("%w:\n%s", err, stdout)
}
if _, err := os.Stat(path.Join(i.li.Base, ".gitmodules")); !errors.Is(err, os.ErrNotExist) {
// Treat submodules
cmdsublfs := exec.Command("git", "-C", i.li.Base, "submodule", "foreach", "--recursive", "git", "lfs", "pull")
stdout, err = cmdsublfs.CombinedOutput()
if err != nil {
log.Printf("Local LFS synchronization failed: %s\n%s", err, stdout)
return fmt.Errorf("%w:\n%s", err, stdout)
}
} else {
cmdlfs := exec.Command("git", "-C", i.li.Base, "lfs", "pull")
stdout, err = cmdlfs.CombinedOutput()
if err != nil {
log.Printf("Local LFS synchronization failed: %s\n%s", err, stdout)
return fmt.Errorf("%w:\n%s", err, stdout)
}
}
log.Println("Local git repository synchronized successfully")
return nil
}
func (i GitImporter) GetThemeLink(th *fic.Theme) (u *url.URL, err error) {
prefix := ""
if _, err = os.Stat(path.Join(i.li.Base, ".gitmodules")); !errors.Is(err, os.ErrNotExist) {
thdir := path.Join(i.li.Base, th.Path)
cmdremote := exec.Command("git", "-C", thdir, "remote", "get-url", "origin")
var stdout []byte
stdout, err = cmdremote.CombinedOutput()
if err != nil {
return
}
u, err = getForgeBaseLink(string(bytes.TrimSpace(stdout)))
// Search .git directory
for {
if _, err = os.Stat(path.Join(thdir, ".git")); !errors.Is(err, os.ErrNotExist) {
break
}
thdir, _ = path.Split(thdir)
}
prefix = strings.TrimPrefix(thdir, i.li.Base)
} else {
u, err = getForgeBaseLink(i.Remote)
}
if err != nil {
return
}
u.Path = path.Join(u.Path, "-", "tree", i.Branch, strings.TrimPrefix(th.Path, prefix))
return
}
func (i GitImporter) GetExerciceLink(e *fic.Exercice) (u *url.URL, err error) {
prefix := ""
if _, err = os.Stat(path.Join(i.li.Base, ".gitmodules")); !errors.Is(err, os.ErrNotExist) {
exdir := path.Join(i.li.Base, e.Path)
cmdremote := exec.Command("git", "-C", exdir, "remote", "get-url", "origin")
var stdout []byte
stdout, err = cmdremote.CombinedOutput()
if err != nil {
return
}
u, err = getForgeBaseLink(string(bytes.TrimSpace(stdout)))
// Search .git directory
for {
if _, err = os.Stat(path.Join(exdir, ".git")); !errors.Is(err, os.ErrNotExist) {
break
}
exdir, _ = path.Split(exdir)
}
prefix = strings.TrimPrefix(exdir, i.li.Base)
} else {
u, err = getForgeBaseLink(i.Remote)
}
if err != nil {
return
}
u.Path = path.Join(u.Path, "-", "tree", i.Branch, strings.TrimPrefix(e.Path, prefix))
return
}
func (i GitImporter) GetSubmodules() ([]GitSubmoduleStatus, error) {
oneGitPull.Lock()
defer oneGitPull.Unlock()
cmdsubmodule := exec.Command("git", "-C", i.li.Base, "submodule", "status")
stdout, err := cmdsubmodule.CombinedOutput()
if err != nil {
log.Printf("Git repository submodule failed: %s\n%s", err, stdout)
return nil, fmt.Errorf("%w:\n%s", err, stdout)
}
var ret []GitSubmoduleStatus
for _, line := range strings.Split(string(stdout), "\n") {
flds := strings.Fields(line)
if len(flds) == 3 {
ret = append(ret, GitSubmoduleStatus{
Hash: flds[0],
Path: flds[1],
Branch: strings.TrimSuffix(strings.TrimPrefix(strings.TrimPrefix(strings.TrimPrefix(strings.TrimPrefix(strings.TrimPrefix(flds[2], "("), "refs/"), "remotes/"), "heads/"), "origin/"), ")"),
})
}
}
return ret, err
}
func (i GitImporter) GetSubmodule(repopath string) (*GitSubmoduleStatus, error) {
oneGitPull.Lock()
defer oneGitPull.Unlock()
if repopath == "" {
cmdsubmodule := exec.Command("git", "-C", i.li.Base, "show", "-q", "--oneline")
stdout, err := cmdsubmodule.CombinedOutput()
if err != nil {
log.Printf("Git repository show failed: %s\n%s", err, stdout)
return nil, fmt.Errorf("%w:\n%s", err, stdout)
}
flds := strings.SplitN(string(stdout), " ", 2)
return &GitSubmoduleStatus{
Hash: flds[0],
Text: strings.TrimSpace(flds[1]),
Path: "",
Branch: i.Branch,
}, nil
} else {
cmdsubmodule := exec.Command("git", "-C", i.li.Base, "submodule", "status", repopath)
stdout, err := cmdsubmodule.CombinedOutput()
if err != nil {
log.Printf("Git repository submodule failed: %s\n%s", err, stdout)
return nil, fmt.Errorf("%w:\n%s", err, stdout)
}
flds := strings.Fields(strings.TrimSpace(string(stdout)))
if len(flds) == 3 {
return &GitSubmoduleStatus{
Hash: flds[0],
Path: flds[1],
Branch: strings.TrimSuffix(strings.TrimPrefix(strings.TrimPrefix(strings.TrimPrefix(strings.TrimPrefix(strings.TrimPrefix(flds[2], "("), "refs/"), "remotes/"), "heads/"), "origin/"), ")"),
}, nil
}
}
return nil, fmt.Errorf("Unable to parse")
}
func (i GitImporter) IsRepositoryUptodate(repopath string) (*GitSubmoduleStatus, error) {
oneGitPull.Lock()
defer oneGitPull.Unlock()
cmdsubmodule := exec.Command("git", "-C", path.Join(i.li.Base, repopath), "fetch", "origin", i.Branch)
stdout, err := cmdsubmodule.CombinedOutput()
if err != nil {
log.Printf("Git repository submodule fetch failed: %s\n%s", err, stdout)
return nil, fmt.Errorf("%w:\n%s", err, stdout)
}
cmdsubmodule = exec.Command("git", "-C", path.Join(i.li.Base, repopath), "show", "-q", "--oneline", "origin/"+i.Branch)
stdout, err = cmdsubmodule.CombinedOutput()
if err != nil {
cmdconfig := exec.Command("git", "-C", path.Join(i.li.Base, repopath), "config", "remote.origin.fetch")
if cfg, err2 := cmdconfig.CombinedOutput(); err2 == nil && !strings.Contains(string(cfg), "+refs/heads/*:refs/remotes/origin/*") {
cmdsubmodule := exec.Command("git", "-C", path.Join(i.li.Base, repopath), "config", "remote.origin.fetch", "+refs/heads/*:refs/remotes/origin/*")
if stdout, err = cmdsubmodule.CombinedOutput(); err != nil {
log.Printf("Git repository submodule config failed: %s\n%s", err, stdout)
return nil, fmt.Errorf("%w:\n%s", err, stdout)
}
} else {
log.Printf("Git repository submodule status failed: %s\n%s", err, stdout)
return nil, fmt.Errorf("%w:\n%s", err, stdout)
}
}
flds := strings.SplitN(string(stdout), " ", 2)
return &GitSubmoduleStatus{
Hash: flds[0],
Text: strings.TrimSpace(flds[1]),
Path: repopath,
Branch: i.Branch,
}, nil
}