From c632ddb35410101980952d6f63aa086f00f7bc89 Mon Sep 17 00:00:00 2001 From: Maxim Slipenko Date: Thu, 8 May 2025 18:04:51 +0000 Subject: [PATCH] add the ability to specify repository ref (#80) closes #75 Reviewed-on: https://gitea.plemya-x.ru/Plemya-x/ALR/pulls/80 Co-authored-by: Maxim Slipenko Co-committed-by: Maxim Slipenko --- assets/coverage-badge.svg | 4 +- e2e-tests/issue_75_ref_specify_test.go | 62 +++++++++++++++ internal/translations/default.pot | 8 +- internal/translations/po/ru/default.po | 8 +- internal/types/config.go | 1 + pkg/repos/pull.go | 102 ++++++++++++++++++------- pkg/repos/utils.go | 62 +++++++++++++++ 7 files changed, 210 insertions(+), 37 deletions(-) create mode 100644 e2e-tests/issue_75_ref_specify_test.go diff --git a/assets/coverage-badge.svg b/assets/coverage-badge.svg index 1a4a8cc..b891de3 100644 --- a/assets/coverage-badge.svg +++ b/assets/coverage-badge.svg @@ -11,7 +11,7 @@ coverage coverage - 16.4% - 16.4% + 17.0% + 17.0% diff --git a/e2e-tests/issue_75_ref_specify_test.go b/e2e-tests/issue_75_ref_specify_test.go new file mode 100644 index 0000000..623381c --- /dev/null +++ b/e2e-tests/issue_75_ref_specify_test.go @@ -0,0 +1,62 @@ +// 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 . + +//go:build e2e + +package e2etests_test + +import ( + "testing" + + "github.com/alecthomas/assert/v2" + "github.com/efficientgo/e2e" +) + +func TestE2EIssue75InstallWithDeps(t *testing.T) { + dockerMultipleRun( + t, + "issue-75-ref-specify", + COMMON_SYSTEMS, + func(t *testing.T, r e2e.Runnable) { + err := r.Exec(e2e.NewCommand( + "sudo", + "alr", + "addrepo", + "--name", + "alr-repo", + "--url", + "https://gitea.plemya-x.ru/Maks1mS/repo-for-tests.git", + )) + assert.NoError(t, err) + + err = r.Exec(e2e.NewCommand( + "sudo", "alr", "ref", + )) + assert.NoError(t, err) + + // TODO: replace with alr command when it be added + err = r.Exec(e2e.NewCommand( + "sudo", "sh", "-c", "sed -i 's/ref = .*/ref = \"bd26236cd7\"/' /etc/alr/alr.toml", + )) + assert.NoError(t, err) + + err = r.Exec(e2e.NewCommand( + "sh", "-c", "test $(alr list | wc -l) -eq 2 || exit 1", + )) + assert.NoError(t, err) + }, + ) +} diff --git a/internal/translations/default.pot b/internal/translations/default.pot index bc63d5b..400e5e4 100644 --- a/internal/translations/default.pot +++ b/internal/translations/default.pot @@ -413,19 +413,19 @@ msgstr "" msgid "Executing %s()" msgstr "" -#: pkg/repos/pull.go:79 +#: pkg/repos/pull.go:80 msgid "Pulling repository" msgstr "" -#: pkg/repos/pull.go:103 +#: pkg/repos/pull.go:116 msgid "Repository up to date" msgstr "" -#: pkg/repos/pull.go:160 +#: pkg/repos/pull.go:207 msgid "Git repository does not appear to be a valid ALR repo" msgstr "" -#: pkg/repos/pull.go:176 +#: pkg/repos/pull.go:223 msgid "" "ALR repo's minimum ALR version is greater than the current version. Try " "updating ALR if something doesn't work." diff --git a/internal/translations/po/ru/default.po b/internal/translations/po/ru/default.po index 5afa67b..6180148 100644 --- a/internal/translations/po/ru/default.po +++ b/internal/translations/po/ru/default.po @@ -425,19 +425,19 @@ msgstr "Выполнение build()" msgid "Executing %s()" msgstr "Выполнение %s()" -#: pkg/repos/pull.go:79 +#: pkg/repos/pull.go:80 msgid "Pulling repository" msgstr "Скачивание репозитория" -#: pkg/repos/pull.go:103 +#: pkg/repos/pull.go:116 msgid "Repository up to date" msgstr "Репозиторий уже обновлён" -#: pkg/repos/pull.go:160 +#: pkg/repos/pull.go:207 msgid "Git repository does not appear to be a valid ALR repo" msgstr "Репозиторий Git не поддерживается репозиторием ALR" -#: pkg/repos/pull.go:176 +#: pkg/repos/pull.go:223 msgid "" "ALR repo's minimum ALR version is greater than the current version. Try " "updating ALR if something doesn't work." diff --git a/internal/types/config.go b/internal/types/config.go index 55480e5..f6a6238 100644 --- a/internal/types/config.go +++ b/internal/types/config.go @@ -34,4 +34,5 @@ type Config struct { type Repo struct { Name string `toml:"name"` URL string `toml:"url"` + Ref string `toml:"ref"` } diff --git a/pkg/repos/pull.go b/pkg/repos/pull.go index 881b87f..fc254be 100644 --- a/pkg/repos/pull.go +++ b/pkg/repos/pull.go @@ -33,6 +33,7 @@ import ( "github.com/go-git/go-billy/v5" "github.com/go-git/go-billy/v5/osfs" "github.com/go-git/go-git/v5" + gitConfig "github.com/go-git/go-git/v5/config" "github.com/go-git/go-git/v5/plumbing" "github.com/leonelquinteros/gotext" "github.com/pelletier/go-toml/v2" @@ -88,6 +89,14 @@ func (rs *Repos) Pull(ctx context.Context, repos []types.Repo) error { return err } + err = r.FetchContext(ctx, &git.FetchOptions{ + Progress: os.Stderr, + Force: true, + }) + if err != nil && !errors.Is(err, git.NoErrAlreadyUpToDate) { + return err + } + w, err := r.Worktree() if err != nil { return err @@ -98,34 +107,41 @@ func (rs *Repos) Pull(ctx context.Context, repos []types.Repo) error { return err } - err = w.PullContext(ctx, &git.PullOptions{Progress: os.Stderr}) - if errors.Is(err, git.NoErrAlreadyUpToDate) { + revHash, err := resolveHash(r, repo.Ref) + if err != nil { + return fmt.Errorf("error resolving hash: %w", err) + } + + if old.Hash() == *revHash { slog.Info(gotext.Get("Repository up to date"), "name", repo.Name) - } else if err != nil { + } + + err = w.Checkout(&git.CheckoutOptions{ + Hash: plumbing.NewHash(revHash.String()), + Force: true, + }) + if err != nil { return err } repoFS = w.Filesystem - // Make sure the DB is created even if the repo is up to date - if !errors.Is(err, git.NoErrAlreadyUpToDate) || rs.db.IsEmpty(ctx) { - new, err := r.Head() + new, err := r.Head() + if err != nil { + return err + } + + // 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 rs.db.IsEmpty(ctx) { + err = rs.processRepoFull(ctx, repo, repoDir) if err != nil { return err } - - // 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 rs.db.IsEmpty(ctx) { - err = rs.processRepoFull(ctx, repo, repoDir) - if err != nil { - return err - } - } else { - err = rs.processRepoChanges(ctx, repo, r, w, old, new) - if err != nil { - return err - } + } else { + err = rs.processRepoChanges(ctx, repo, r, w, old, new) + if err != nil { + return err } } } else { @@ -139,9 +155,40 @@ func (rs *Repos) Pull(ctx context.Context, repos []types.Repo) error { return err } - _, err = git.PlainCloneContext(ctx, repoDir, false, &git.CloneOptions{ - URL: repoURL.String(), + r, err := git.PlainInit(repoDir, false) + if err != nil { + return err + } + + _, err = r.CreateRemote(&gitConfig.RemoteConfig{ + Name: git.DefaultRemoteName, + URLs: []string{repoURL.String()}, + }) + if err != nil { + return err + } + + err = r.FetchContext(ctx, &git.FetchOptions{ Progress: os.Stderr, + Force: true, + }) + if err != nil { + return err + } + + w, err := r.Worktree() + if err != nil { + return err + } + + revHash, err := resolveHash(r, repo.Ref) + if err != nil { + return fmt.Errorf("error resolving hash: %w", err) + } + + err = w.Checkout(&git.CheckoutOptions{ + Hash: plumbing.NewHash(revHash.String()), + Force: true, }) if err != nil { return err @@ -268,7 +315,8 @@ func (rs *Repos) processRepoChangesRunner(repoDir, scriptDir string) (*interp.Ru interp.StatHandler(handlers.RestrictedStat(repoDir)), interp.OpenHandler(handlers.RestrictedOpen(repoDir)), interp.StdIO(handlers.NopRWC{}, handlers.NopRWC{}, handlers.NopRWC{}), - interp.Dir(scriptDir), + // Use temp dir instead script dir because runner may be for deleted file + interp.Dir(os.TempDir()), ) } @@ -285,7 +333,7 @@ func (rs *Repos) processRepoChanges(ctx context.Context, repo types.Repo, r *git patch, err := oldCommit.Patch(newCommit) if err != nil { - return err + return fmt.Errorf("error to create patch: %w", err) } var actions []action @@ -319,6 +367,7 @@ func (rs *Repos) processRepoChanges(ctx context.Context, repo types.Repo, r *git }, ) default: + slog.Debug("unexpected, but I'll try to do") actions = append(actions, action{ Type: actionUpdate, File: to.Path(), @@ -332,7 +381,7 @@ func (rs *Repos) processRepoChanges(ctx context.Context, repo types.Repo, r *git for _, action := range actions { runner, err := rs.processRepoChangesRunner(repoDir, filepath.Dir(filepath.Join(repoDir, action.File))) if err != nil { - return err + return fmt.Errorf("error creating process repo changes runner: %w", err) } switch action.Type { @@ -340,7 +389,6 @@ func (rs *Repos) processRepoChanges(ctx context.Context, repo types.Repo, r *git if filepath.Base(action.File) != "alr.sh" { continue } - scriptFl, err := oldCommit.File(action.File) if err != nil { return nil @@ -378,7 +426,7 @@ func (rs *Repos) processRepoChanges(ctx context.Context, repo types.Repo, r *git err = rs.updatePkg(ctx, repo, runner, r) if err != nil { - return err + return fmt.Errorf("error updatePkg: %w", err) } } } diff --git a/pkg/repos/utils.go b/pkg/repos/utils.go index 7df2b08..bc2315a 100644 --- a/pkg/repos/utils.go +++ b/pkg/repos/utils.go @@ -18,12 +18,18 @@ package repos import ( "context" + "fmt" "io" "path/filepath" "reflect" "strings" + "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" + "mvdan.cc/sh/v3/interp" "mvdan.cc/sh/v3/syntax" @@ -137,3 +143,59 @@ func resolveOverrides(runner *interp.Runner, pkg *db.Package) { } } } + +func getHeadReference(r *git.Repository) (plumbing.ReferenceName, error) { + remote, err := r.Remote(git.DefaultRemoteName) + if err != nil { + return "", err + } + + endpoint, err := transport.NewEndpoint(remote.Config().URLs[0]) + if err != nil { + return "", err + } + + gitClient, err := client.NewClient(endpoint) + if err != nil { + return "", err + } + + session, err := gitClient.NewUploadPackSession(endpoint, nil) + if err != nil { + return "", err + } + + info, err := session.AdvertisedReferences() + if err != nil { + return "", err + } + + refs, err := info.AllReferences() + if err != nil { + return "", err + } + + return refs["HEAD"].Target(), nil +} + +func resolveHash(r *git.Repository, ref string) (*plumbing.Hash, error) { + var err error + + if ref == "" { + reference, err := getHeadReference(r) + if err != nil { + return nil, fmt.Errorf("failed to get head reference %w", err) + } + ref = reference.Short() + } + + hsh, err := r.ResolveRevision(git.DefaultRemoteName + "/" + plumbing.Revision(ref)) + if err != nil { + hsh, err = r.ResolveRevision(plumbing.Revision(ref)) + if err != nil { + return nil, err + } + } + + return hsh, nil +}