feat: support single package repository #105
							
								
								
									
										53
									
								
								e2e-tests/issue_76_single_package_repo_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								e2e-tests/issue_76_single_package_repo_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,53 @@ | ||||
| // ALR - Any Linux Repository | ||||
| // Copyright (C) 2025 The ALR Authors | ||||
| // | ||||
| // This program is free software: you can redistribute it and/or modify | ||||
| // it under the terms of the GNU General Public License as published by | ||||
| // the Free Software Foundation, either version 3 of the License, or | ||||
| // (at your option) any later version. | ||||
| // | ||||
| // This program is distributed in the hope that it will be useful, | ||||
| // but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
| // GNU General Public License for more details. | ||||
| // | ||||
| // You should have received a copy of the GNU General Public License | ||||
| // along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||
|  | ||||
| //go:build e2e | ||||
|  | ||||
| package e2etests_test | ||||
|  | ||||
| import ( | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/efficientgo/e2e" | ||||
| ) | ||||
|  | ||||
| func Test75SinglePackageRepo(t *testing.T) { | ||||
| 	dockerMultipleRun( | ||||
| 		t, | ||||
| 		"issue-76-single-package-repo", | ||||
| 		COMMON_SYSTEMS, | ||||
| 		func(t *testing.T, r e2e.Runnable) { | ||||
| 			execShouldNoError(t, r, | ||||
| 				"sudo", | ||||
| 				"alr", | ||||
| 				"repo", | ||||
| 				"add", | ||||
| 				REPO_NAME_FOR_E2E_TESTS, | ||||
| 				"https://gitea.plemya-x.ru/Maks1mS/test-single-package-alr-repo.git", | ||||
| 			) | ||||
| 			execShouldNoError(t, r, "sudo", "alr", "repo", "set-ref", REPO_NAME_FOR_E2E_TESTS, "1075c918be") | ||||
| 			execShouldNoError(t, r, "alr", "ref") | ||||
| 			execShouldNoError(t, r, "sudo", "alr", "in", "test-single-repo") | ||||
| 			execShouldNoError(t, r, "sh", "-c", "alr list -U") | ||||
| 			execShouldNoError(t, r, "sh", "-c", "test $(alr list -U | wc -l) -eq 0 || exit 1") | ||||
| 			execShouldNoError(t, r, "sudo", "alr", "repo", "set-ref", REPO_NAME_FOR_E2E_TESTS, "5e361c50d7") | ||||
| 			execShouldNoError(t, r, "sudo", "alr", "ref") | ||||
| 			execShouldNoError(t, r, "sh", "-c", "test $(alr list -U | wc -l) -eq 1 || exit 1") | ||||
| 			execShouldNoError(t, r, "sudo", "alr", "up") | ||||
| 			execShouldNoError(t, r, "sh", "-c", "test $(alr list -U | wc -l) -eq 0 || exit 1") | ||||
| 		}, | ||||
| 	) | ||||
| } | ||||
| @@ -18,6 +18,7 @@ package build | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
|  | ||||
| 	"gitea.plemya-x.ru/Plemya-x/ALR/pkg/alrsh" | ||||
| @@ -40,10 +41,21 @@ func (s *ScriptResolver) ResolveScript( | ||||
|  | ||||
| 	repodir := s.cfg.GetPaths().RepoDir | ||||
| 	repository = pkg.Repository | ||||
| 	if pkg.BasePkgName != "" { | ||||
| 		script = filepath.Join(repodir, repository, pkg.BasePkgName, "alr.sh") | ||||
|  | ||||
| 	// First, we check if there is a root alr.sh in the repository | ||||
| 	rootScriptPath := filepath.Join(repodir, repository, "alr.sh") | ||||
| 	if _, err := os.Stat(rootScriptPath); err == nil { | ||||
| 		// A repository with a single alr.sh at the root | ||||
| 		script = rootScriptPath | ||||
| 	} else { | ||||
| 		script = filepath.Join(repodir, repository, pkg.Name, "alr.sh") | ||||
| 		// Multi-package repository - we are looking for alr.sh in the subfolder | ||||
| 		var scriptPath string | ||||
| 		if pkg.BasePkgName != "" { | ||||
| 			scriptPath = filepath.Join(repodir, repository, pkg.BasePkgName, "alr.sh") | ||||
| 		} else { | ||||
| 			scriptPath = filepath.Join(repodir, repository, pkg.Name, "alr.sh") | ||||
| 		} | ||||
| 		script = scriptPath | ||||
| 	} | ||||
|  | ||||
| 	return &ScriptInfo{ | ||||
|   | ||||
| @@ -277,7 +277,15 @@ func (rs *Repos) processRepoChanges(ctx context.Context, repo types.Repo, r *git | ||||
| 	for _, fp := range patch.FilePatches() { | ||||
| 		from, to := fp.Files() | ||||
|  | ||||
| 		if !isValid(from, to) { | ||||
| 		var isValidPath bool | ||||
| 		if from != nil { | ||||
| 			isValidPath = isValidScriptPath(from.Path()) | ||||
| 		} | ||||
| 		if to != nil { | ||||
| 			isValidPath = isValidPath || isValidScriptPath(to.Path()) | ||||
| 		} | ||||
|  | ||||
| 		if !isValidPath { | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| @@ -316,55 +324,60 @@ func (rs *Repos) processRepoChanges(ctx context.Context, repo types.Repo, r *git | ||||
| 	parser := syntax.NewParser() | ||||
|  | ||||
| 	for _, action := range actions { | ||||
| 		runner, err := rs.processRepoChangesRunner(repoDir, filepath.Dir(filepath.Join(repoDir, action.File))) | ||||
| 		var scriptDir string | ||||
| 		if filepath.Dir(action.File) == "." { | ||||
| 			scriptDir = repoDir | ||||
| 		} else { | ||||
| 			scriptDir = filepath.Dir(filepath.Join(repoDir, action.File)) | ||||
| 		} | ||||
|  | ||||
| 		runner, err := rs.processRepoChangesRunner(repoDir, scriptDir) | ||||
| 		if err != nil { | ||||
| 			return fmt.Errorf("error creating process repo changes runner: %w", err) | ||||
| 		} | ||||
|  | ||||
| 		switch action.Type { | ||||
| 		case actionDelete: | ||||
| 			if filepath.Base(action.File) != "alr.sh" { | ||||
| 				continue | ||||
| 			} | ||||
| 			scriptFl, err := oldCommit.File(action.File) | ||||
| 			if err != nil { | ||||
| 				return nil | ||||
| 				slog.Warn("Failed to get deleted file from old commit", "file", action.File, "error", err) | ||||
| 				continue | ||||
| 			} | ||||
|  | ||||
| 			r, err := scriptFl.Reader() | ||||
| 			if err != nil { | ||||
| 				return nil | ||||
| 				slog.Warn("Failed to read deleted file", "file", action.File, "error", err) | ||||
| 				continue | ||||
| 			} | ||||
|  | ||||
| 			pkgs, err := parseScript(ctx, repo, parser, runner, r) | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 				return fmt.Errorf("error parsing deleted script %s: %w", action.File, err) | ||||
| 			} | ||||
|  | ||||
| 			for _, pkg := range pkgs { | ||||
| 				err = rs.db.DeletePkgs(ctx, "name = ? AND repository = ?", pkg.Name, repo.Name) | ||||
| 				if err != nil { | ||||
| 					return err | ||||
| 					return fmt.Errorf("error deleting package %s: %w", pkg.Name, err) | ||||
| 				} | ||||
| 			} | ||||
| 		case actionUpdate: | ||||
| 			if filepath.Base(action.File) != "alr.sh" { | ||||
| 				action.File = filepath.Join(filepath.Dir(action.File), "alr.sh") | ||||
| 			} | ||||
|  | ||||
| 		case actionUpdate: | ||||
| 			scriptFl, err := newCommit.File(action.File) | ||||
| 			if err != nil { | ||||
| 				return nil | ||||
| 				slog.Warn("Failed to get updated file from new commit", "file", action.File, "error", err) | ||||
| 				continue | ||||
| 			} | ||||
|  | ||||
| 			r, err := scriptFl.Reader() | ||||
| 			if err != nil { | ||||
| 				return nil | ||||
| 				slog.Warn("Failed to read updated file", "file", action.File, "error", err) | ||||
| 				continue | ||||
| 			} | ||||
|  | ||||
| 			err = rs.updatePkg(ctx, repo, runner, r) | ||||
| 			if err != nil { | ||||
| 				return fmt.Errorf("error updatePkg: %w", err) | ||||
| 				return fmt.Errorf("error updating package from %s: %w", action.File, err) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| @@ -372,27 +385,68 @@ func (rs *Repos) processRepoChanges(ctx context.Context, repo types.Repo, r *git | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func isValidScriptPath(path string) bool { | ||||
| 	if filepath.Base(path) != "alr.sh" { | ||||
| 		return false | ||||
| 	} | ||||
|  | ||||
| 	dir := filepath.Dir(path) | ||||
| 	return dir == "." || !strings.Contains(strings.TrimPrefix(dir, "./"), "/") | ||||
| } | ||||
|  | ||||
| func (rs *Repos) processRepoFull(ctx context.Context, repo types.Repo, repoDir string) error { | ||||
| 	glob := filepath.Join(repoDir, "/*/alr.sh") | ||||
| 	rootScript := filepath.Join(repoDir, "alr.sh") | ||||
| 	if fi, err := os.Stat(rootScript); err == nil && !fi.IsDir() { | ||||
| 		slog.Debug("Found root alr.sh, processing single-script repository", "repo", repo.Name) | ||||
|  | ||||
| 		runner, err := rs.processRepoChangesRunner(repoDir, repoDir) | ||||
| 		if err != nil { | ||||
| 			return fmt.Errorf("error creating runner for root alr.sh: %w", err) | ||||
| 		} | ||||
|  | ||||
| 		scriptFl, err := os.Open(rootScript) | ||||
| 		if err != nil { | ||||
| 			return fmt.Errorf("error opening root alr.sh: %w", err) | ||||
| 		} | ||||
| 		defer scriptFl.Close() | ||||
|  | ||||
| 		err = rs.updatePkg(ctx, repo, runner, scriptFl) | ||||
| 		if err != nil { | ||||
| 			return fmt.Errorf("error processing root alr.sh: %w", err) | ||||
| 		} | ||||
|  | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	glob := filepath.Join(repoDir, "*/alr.sh") | ||||
| 	matches, err := filepath.Glob(glob) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 		return fmt.Errorf("error globbing for alr.sh files: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	if len(matches) == 0 { | ||||
| 		slog.Warn("No alr.sh files found in repository", "repo", repo.Name) | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	slog.Debug("Found multiple alr.sh files, processing multi-package repository", | ||||
| 		"repo", repo.Name, "count", len(matches)) | ||||
|  | ||||
| 	for _, match := range matches { | ||||
| 		runner, err := rs.processRepoChangesRunner(repoDir, filepath.Dir(match)) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 			return fmt.Errorf("error creating runner for %s: %w", match, err) | ||||
| 		} | ||||
|  | ||||
| 		scriptFl, err := os.Open(match) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 			return fmt.Errorf("error opening %s: %w", match, err) | ||||
| 		} | ||||
|  | ||||
| 		err = rs.updatePkg(ctx, repo, runner, scriptFl) | ||||
| 		scriptFl.Close() | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 			return fmt.Errorf("error processing %s: %w", match, err) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
|   | ||||
| @@ -20,11 +20,9 @@ import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"path/filepath" | ||||
|  | ||||
| 	"github.com/go-git/go-git/v5" | ||||
| 	"github.com/go-git/go-git/v5/plumbing" | ||||
| 	"github.com/go-git/go-git/v5/plumbing/format/diff" | ||||
| 	"github.com/go-git/go-git/v5/plumbing/transport" | ||||
| 	"github.com/go-git/go-git/v5/plumbing/transport/client" | ||||
|  | ||||
| @@ -36,22 +34,6 @@ import ( | ||||
| 	"gitea.plemya-x.ru/Plemya-x/ALR/pkg/types" | ||||
| ) | ||||
|  | ||||
| // 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 alr.sh. | ||||
| func isValid(from, to diff.File) bool { | ||||
| 	var path string | ||||
| 	if from != nil { | ||||
| 		path = from.Path() | ||||
| 	} | ||||
| 	if to != nil { | ||||
| 		path = to.Path() | ||||
| 	} | ||||
|  | ||||
| 	match, _ := filepath.Match("*/*.sh", path) | ||||
| 	return match | ||||
| } | ||||
|  | ||||
| func parseScript( | ||||
| 	ctx context.Context, | ||||
| 	repo types.Repo, | ||||
|   | ||||
		Reference in New Issue
	
	Block a user