2022-11-30 19:45:13 +00:00
package repos
import (
"context"
2022-11-30 22:34:52 +00:00
"errors"
"io"
2022-11-30 19:45:13 +00:00
"net/url"
"os"
"path/filepath"
"strings"
"github.com/go-git/go-billy/v5"
"github.com/go-git/go-billy/v5/osfs"
"github.com/go-git/go-git/v5"
2022-11-30 22:34:52 +00:00
"github.com/go-git/go-git/v5/plumbing"
2022-12-01 19:08:20 +00:00
"github.com/go-git/go-git/v5/plumbing/format/diff"
2022-12-24 20:56:02 +00:00
"github.com/jmoiron/sqlx"
2022-11-30 19:45:13 +00:00
"github.com/pelletier/go-toml/v2"
"go.arsenm.dev/logger/log"
2022-11-30 22:34:52 +00:00
"go.arsenm.dev/lure/distro"
2022-11-30 19:45:13 +00:00
"go.arsenm.dev/lure/download"
"go.arsenm.dev/lure/internal/config"
2022-11-30 22:34:52 +00:00
"go.arsenm.dev/lure/internal/db"
"go.arsenm.dev/lure/internal/shutils"
"go.arsenm.dev/lure/internal/shutils/decoder"
2022-11-30 19:45:13 +00:00
"go.arsenm.dev/lure/internal/types"
"go.arsenm.dev/lure/vercmp"
2022-12-03 21:09:42 +00:00
"mvdan.cc/sh/v3/expand"
2022-11-30 22:34:52 +00:00
"mvdan.cc/sh/v3/interp"
"mvdan.cc/sh/v3/syntax"
2022-11-30 19:45:13 +00:00
)
2022-12-01 03:33:40 +00:00
// Pull pulls the provided repositories. If a repo doesn't exist, it will be cloned
// and its packages will be written to the DB. If it does exist, it will be pulled.
// In this case, only changed packages will be processed.
2022-12-24 20:56:02 +00:00
func Pull ( ctx context . Context , gdb * sqlx . DB , repos [ ] types . Repo ) error {
2022-11-30 19:45:13 +00:00
for _ , repo := range repos {
repoURL , err := url . Parse ( repo . URL )
if err != nil {
return err
}
log . Info ( "Pulling repository" ) . Str ( "name" , repo . Name ) . Send ( )
repoDir := filepath . Join ( config . RepoDir , repo . Name )
var repoFS billy . Filesystem
gitDir := filepath . Join ( repoDir , ".git" )
// Only pull repos that contain valid git repos
if fi , err := os . Stat ( gitDir ) ; err == nil && fi . IsDir ( ) {
r , err := git . PlainOpen ( repoDir )
if err != nil {
return err
}
w , err := r . Worktree ( )
if err != nil {
return err
}
2022-11-30 22:34:52 +00:00
old , err := r . Head ( )
if err != nil {
return err
}
2022-11-30 19:45:13 +00:00
err = w . PullContext ( ctx , & git . PullOptions { Progress : os . Stderr } )
2022-11-30 22:34:52 +00:00
if errors . Is ( err , git . NoErrAlreadyUpToDate ) {
2022-11-30 19:45:13 +00:00
log . Info ( "Repository up to date" ) . Str ( "name" , repo . Name ) . Send ( )
} else if err != nil {
return err
}
repoFS = w . Filesystem
2022-11-30 22:34:52 +00:00
2022-12-01 06:38:22 +00:00
// Make sure the DB is created even if the repo is up to date
if ! errors . Is ( err , git . NoErrAlreadyUpToDate ) || ! config . DBPresent {
2022-11-30 22:34:52 +00:00
new , err := r . Head ( )
if err != nil {
return err
}
2022-12-01 06:38:22 +00:00
// If the DB was not present at startup, that means it's
// empty. In this case, we need to update the DB fully
// rather than just incrementally.
if config . DBPresent {
2022-12-03 21:09:42 +00:00
err = processRepoChanges ( ctx , repo , r , w , old , new , gdb )
2022-12-01 06:38:22 +00:00
if err != nil {
return err
}
} else {
err = processRepoFull ( ctx , repo , repoDir , gdb )
if err != nil {
return err
}
2022-11-30 22:34:52 +00:00
}
}
2022-11-30 19:45:13 +00:00
} else {
err = os . RemoveAll ( repoDir )
if err != nil {
return err
}
err = os . MkdirAll ( repoDir , 0 o755 )
if err != nil {
return err
}
if ! strings . HasPrefix ( repoURL . Scheme , "git+" ) {
repoURL . Scheme = "git+" + repoURL . Scheme
}
err = download . Get ( ctx , download . GetOptions {
SourceURL : repoURL . String ( ) ,
Destination : repoDir ,
} )
if err != nil {
return err
}
2022-11-30 22:34:52 +00:00
err = processRepoFull ( ctx , repo , repoDir , gdb )
if err != nil {
return err
}
2022-11-30 19:45:13 +00:00
repoFS = osfs . New ( repoDir )
}
fl , err := repoFS . Open ( "lure-repo.toml" )
if err != nil {
log . Warn ( "Git repository does not appear to be a valid LURE repo" ) . Str ( "repo" , repo . Name ) . Send ( )
continue
}
var repoCfg types . RepoConfig
err = toml . NewDecoder ( fl ) . Decode ( & repoCfg )
if err != nil {
return err
}
fl . Close ( )
currentVer , _ , _ := strings . Cut ( config . Version , "-" )
if vercmp . Compare ( currentVer , repoCfg . Repo . MinVersion ) == - 1 {
log . Warn ( "LURE repo's minumum LURE version is greater than the current version. Try updating LURE if something doesn't work." ) . Str ( "repo" , repo . Name ) . Send ( )
}
}
return nil
}
2022-11-30 22:34:52 +00:00
2022-12-01 05:47:07 +00:00
type actionType uint8
2022-11-30 22:34:52 +00:00
const (
2022-12-01 05:47:07 +00:00
actionDelete actionType = iota
actionUpdate
2022-11-30 22:34:52 +00:00
)
2022-12-01 05:47:07 +00:00
type action struct {
Type actionType
2022-11-30 22:34:52 +00:00
File string
}
2022-12-24 20:56:02 +00:00
func processRepoChanges ( ctx context . Context , repo types . Repo , r * git . Repository , w * git . Worktree , old , new * plumbing . Reference , gdb * sqlx . DB ) error {
2022-11-30 22:34:52 +00:00
oldCommit , err := r . CommitObject ( old . Hash ( ) )
if err != nil {
return err
}
newCommit , err := r . CommitObject ( new . Hash ( ) )
if err != nil {
return err
}
patch , err := oldCommit . Patch ( newCommit )
if err != nil {
return err
}
2022-12-01 05:47:07 +00:00
var actions [ ] action
2022-11-30 22:34:52 +00:00
for _ , fp := range patch . FilePatches ( ) {
from , to := fp . Files ( )
2022-12-01 19:08:20 +00:00
if ! isValid ( from , to ) {
continue
}
2022-11-30 22:34:52 +00:00
if to == nil {
2022-12-01 05:47:07 +00:00
actions = append ( actions , action {
Type : actionDelete ,
2022-11-30 22:34:52 +00:00
File : from . Path ( ) ,
} )
} else if from == nil {
2022-12-01 05:47:07 +00:00
actions = append ( actions , action {
Type : actionUpdate ,
2022-11-30 22:34:52 +00:00
File : to . Path ( ) ,
} )
} else {
if from . Path ( ) != to . Path ( ) {
actions = append ( actions ,
2022-12-01 05:47:07 +00:00
action {
Type : actionDelete ,
2022-11-30 22:34:52 +00:00
File : from . Path ( ) ,
} ,
2022-12-01 05:47:07 +00:00
action {
Type : actionUpdate ,
2022-11-30 22:34:52 +00:00
File : to . Path ( ) ,
} ,
)
} else {
2022-12-01 05:47:07 +00:00
actions = append ( actions , action {
Type : actionUpdate ,
2022-11-30 22:34:52 +00:00
File : to . Path ( ) ,
} )
}
}
}
2022-12-03 21:09:42 +00:00
repoDir := w . Filesystem . Root ( )
2022-11-30 22:34:52 +00:00
parser := syntax . NewParser ( )
for _ , action := range actions {
2022-12-03 21:09:42 +00:00
env := append ( os . Environ ( ) , "scriptdir=" + filepath . Dir ( filepath . Join ( repoDir , action . File ) ) )
runner , err := interp . New (
interp . Env ( expand . ListEnviron ( env ... ) ) ,
interp . ExecHandler ( shutils . NopExec ) ,
interp . ReadDirHandler ( shutils . RestrictedReadDir ( repoDir ) ) ,
interp . StatHandler ( shutils . RestrictedStat ( repoDir ) ) ,
interp . OpenHandler ( shutils . RestrictedOpen ( repoDir ) ) ,
interp . StdIO ( shutils . NopRWC { } , shutils . NopRWC { } , shutils . NopRWC { } ) ,
)
if err != nil {
return err
}
2022-11-30 22:34:52 +00:00
switch action . Type {
2022-12-01 05:47:07 +00:00
case actionDelete :
2022-12-03 21:09:42 +00:00
if filepath . Base ( action . File ) != "lure.sh" {
continue
}
2022-11-30 22:34:52 +00:00
scriptFl , err := oldCommit . File ( action . File )
if err != nil {
return nil
}
r , err := scriptFl . Reader ( )
if err != nil {
return nil
}
var pkg db . Package
err = parseScript ( ctx , parser , runner , r , & pkg )
if err != nil {
return err
}
2022-12-01 03:14:07 +00:00
err = db . DeletePkgs ( gdb , "name = ? AND repository = ?" , pkg . Name , repo . Name )
2022-11-30 22:34:52 +00:00
if err != nil {
return err
}
2022-12-01 05:47:07 +00:00
case actionUpdate :
2022-12-03 21:09:42 +00:00
if filepath . Base ( action . File ) != "lure.sh" {
action . File = filepath . Join ( filepath . Dir ( action . File ) , "lure.sh" )
}
2022-11-30 22:34:52 +00:00
scriptFl , err := newCommit . File ( action . File )
if err != nil {
return nil
}
r , err := scriptFl . Reader ( )
if err != nil {
return nil
}
pkg := db . Package {
2022-12-24 20:56:02 +00:00
Depends : db . NewJSON ( map [ string ] [ ] string { } ) ,
BuildDepends : db . NewJSON ( map [ string ] [ ] string { } ) ,
2022-11-30 22:34:52 +00:00
Repository : repo . Name ,
}
err = parseScript ( ctx , parser , runner , r , & pkg )
if err != nil {
return err
}
resolveOverrides ( runner , & pkg )
err = db . InsertPackage ( gdb , pkg )
if err != nil {
return err
}
}
}
return nil
}
2022-12-01 19:08:20 +00:00
// isValid makes sure the path of the file being updated is valid.
// It checks to make sure the file is not within a nested directory
// and that it is called lure.sh.
func isValid ( from , to diff . File ) bool {
var path string
if from != nil {
path = from . Path ( )
}
if to != nil {
path = to . Path ( )
}
2022-12-03 21:09:42 +00:00
match , _ := filepath . Match ( "*/*.sh" , path )
return match
2022-12-01 19:08:20 +00:00
}
2022-12-24 20:56:02 +00:00
func processRepoFull ( ctx context . Context , repo types . Repo , repoDir string , gdb * sqlx . DB ) error {
2022-11-30 22:34:52 +00:00
glob := filepath . Join ( repoDir , "/*/lure.sh" )
matches , err := filepath . Glob ( glob )
if err != nil {
return err
}
parser := syntax . NewParser ( )
for _ , match := range matches {
2022-12-03 21:09:42 +00:00
env := append ( os . Environ ( ) , "scriptdir=" + filepath . Dir ( match ) )
runner , err := interp . New (
interp . Env ( expand . ListEnviron ( env ... ) ) ,
interp . ExecHandler ( shutils . NopExec ) ,
interp . ReadDirHandler ( shutils . RestrictedReadDir ( repoDir ) ) ,
interp . StatHandler ( shutils . RestrictedStat ( repoDir ) ) ,
interp . OpenHandler ( shutils . RestrictedOpen ( repoDir ) ) ,
interp . StdIO ( shutils . NopRWC { } , shutils . NopRWC { } , shutils . NopRWC { } ) ,
)
if err != nil {
return err
}
2022-11-30 22:34:52 +00:00
scriptFl , err := os . Open ( match )
if err != nil {
return err
}
pkg := db . Package {
2022-12-24 20:56:02 +00:00
Depends : db . NewJSON ( map [ string ] [ ] string { } ) ,
BuildDepends : db . NewJSON ( map [ string ] [ ] string { } ) ,
2022-11-30 22:34:52 +00:00
Repository : repo . Name ,
}
err = parseScript ( ctx , parser , runner , scriptFl , & pkg )
if err != nil {
return err
}
resolveOverrides ( runner , & pkg )
err = db . InsertPackage ( gdb , pkg )
if err != nil {
return err
}
}
return nil
}
func parseScript ( ctx context . Context , parser * syntax . Parser , runner * interp . Runner , r io . ReadCloser , pkg * db . Package ) error {
defer r . Close ( )
fl , err := parser . Parse ( r , "lure.sh" )
if err != nil {
return err
}
runner . Reset ( )
err = runner . Run ( ctx , fl )
if err != nil {
return err
}
d := decoder . New ( & distro . OSRelease { } , runner )
d . Overrides = false
d . LikeDistros = false
return d . DecodeVars ( pkg )
}
func resolveOverrides ( runner * interp . Runner , pkg * db . Package ) {
for name , val := range runner . Vars {
if strings . HasPrefix ( name , "deps" ) {
override := strings . TrimPrefix ( name , "deps" )
override = strings . TrimPrefix ( override , "_" )
2022-12-24 20:56:02 +00:00
pkg . Depends . Val [ override ] = val . List
2022-11-30 22:34:52 +00:00
} else if strings . HasPrefix ( name , "build_deps" ) {
override := strings . TrimPrefix ( name , "build_deps" )
override = strings . TrimPrefix ( override , "_" )
2022-12-24 20:56:02 +00:00
pkg . BuildDepends . Val [ override ] = val . List
2022-11-30 22:34:52 +00:00
} else {
continue
}
}
}