diff --git a/assets/coverage-badge.svg b/assets/coverage-badge.svg index a9e856e..e1e8356 100644 --- a/assets/coverage-badge.svg +++ b/assets/coverage-badge.svg @@ -11,7 +11,7 @@ coverage coverage - 19.3% - 19.3% + 18.8% + 18.8% diff --git a/e2e-tests/issue_78_mirrors_test.go b/e2e-tests/issue_78_mirrors_test.go new file mode 100644 index 0000000..3d1f046 --- /dev/null +++ b/e2e-tests/issue_78_mirrors_test.go @@ -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 . + +//go:build e2e + +package e2etests_test + +import ( + "testing" + + "github.com/efficientgo/e2e" +) + +func TestE2EIssue78Mirrors(t *testing.T) { + dockerMultipleRun( + t, + "issue-78-mirrors", + COMMON_SYSTEMS, + func(t *testing.T, r e2e.Runnable) { + defaultPrepare(t, r) + execShouldNoError(t, r, "sudo", "alr", "repo", "mirror", "add", REPO_NAME_FOR_E2E_TESTS, "https://gitea.plemya-x.ru/Maks1mS/repo-for-tests.git") + execShouldNoError(t, r, "sudo", "alr", "repo", "set-url", REPO_NAME_FOR_E2E_TESTS, "https://example.com") + execShouldNoError(t, r, "sudo", "alr", "ref") + execShouldNoError(t, r, "sudo", "alr", "repo", "mirror", "clear", REPO_NAME_FOR_E2E_TESTS) + execShouldError(t, r, "sudo", "alr", "ref") + + execShouldNoError(t, r, "sudo", "alr", "repo", "mirror", "add", REPO_NAME_FOR_E2E_TESTS, "https://gitea.plemya-x.ru/Maks1mS/repo-for-tests.git") + execShouldNoError(t, r, "sudo", "alr", "repo", "mirror", "rm", "--partial", REPO_NAME_FOR_E2E_TESTS, "gitea.plemya-x.ru/Maks1mS") + execShouldError(t, r, "sudo", "alr", "ref") + + execShouldNoError(t, r, "sudo", "alr", "repo", "mirror", "add", REPO_NAME_FOR_E2E_TESTS, "https://gitea.plemya-x.ru/Maks1mS/repo-for-tests.git") + execShouldNoError(t, r, "sudo", "alr", "repo", "mirror", "rm", REPO_NAME_FOR_E2E_TESTS, "https://gitea.plemya-x.ru/Maks1mS/repo-for-tests.git") + execShouldError(t, r, "sudo", "alr", "ref") + + execShouldNoError(t, r, "sudo", "alr", "repo", "mirror", "add", REPO_NAME_FOR_E2E_TESTS, "https://gitea.plemya-x.ru/Maks1mS/repo-for-tests.git") + execShouldNoError(t, r, "sudo", "alr", "repo", "mirror", "rm", REPO_NAME_FOR_E2E_TESTS, "https://gitea.plemya-x.ru/Maks1mS/repo-for-tests.git") + execShouldError(t, r, "sudo", "alr", "ref") + }, + ) +} diff --git a/internal/repos/pull.go b/internal/repos/pull.go index 7c2b54c..d9beccb 100644 --- a/internal/repos/pull.go +++ b/internal/repos/pull.go @@ -31,7 +31,6 @@ import ( "strings" "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" @@ -69,159 +68,212 @@ func (rs *Repos) Pull(ctx context.Context, repos []types.Repo) error { } for _, repo := range repos { - repoURL, err := url.Parse(repo.URL) + urls := []string{repo.URL} + urls = append(urls, repo.Mirrors...) + + var lastErr error + + for i, repoURL := range urls { + if i > 0 { + slog.Info(gotext.Get("Trying mirror"), "repo", repo.Name, "mirror", repoURL) + } + + err := rs.pullRepoFromURL(ctx, repoURL, repo) + if err != nil { + lastErr = err + slog.Warn(gotext.Get("Failed to pull from URL"), "repo", repo.Name, "url", repoURL, "error", err) + continue + } + + // Success + return nil + } + + return fmt.Errorf("failed to pull repository %s from any URL: %w", repo.Name, lastErr) + } + + return nil +} + +func readGitRepo(repoDir, repoUrl string) (*git.Repository, bool, error) { + gitDir := filepath.Join(repoDir, ".git") + if fi, err := os.Stat(gitDir); err == nil && fi.IsDir() { + r, err := git.PlainOpen(repoDir) + if err == nil { + err = updateRemoteURL(r, repoUrl) + if err == nil { + _, err := r.Head() + if err == nil { + return r, false, nil + } + + if errors.Is(err, plumbing.ErrReferenceNotFound) { + return r, true, nil + } + + slog.Debug("error getting HEAD, reinitializing...", "err", err) + } + } + + slog.Debug("error while reading repo, reinitializing...", "err", err) + } + + if err := os.RemoveAll(repoDir); err != nil { + return nil, false, fmt.Errorf("failed to remove repo directory: %w", err) + } + + if err := os.MkdirAll(repoDir, 0o755); err != nil { + return nil, false, fmt.Errorf("failed to create repo directory: %w", err) + } + + r, err := git.PlainInit(repoDir, false) + if err != nil { + return nil, false, fmt.Errorf("failed to initialize git repo: %w", err) + } + + _, err = r.CreateRemote(&gitConfig.RemoteConfig{ + Name: git.DefaultRemoteName, + URLs: []string{repoUrl}, + }) + if err != nil { + return nil, false, err + } + + return r, true, nil +} + +func (rs *Repos) pullRepoFromURL(ctx context.Context, rawRepoUrl string, repo types.Repo) error { + repoURL, err := url.Parse(rawRepoUrl) + if err != nil { + return fmt.Errorf("invalid URL %s: %w", rawRepoUrl, err) + } + + slog.Info(gotext.Get("Pulling repository"), "name", repo.Name) + repoDir := filepath.Join(rs.cfg.GetPaths().RepoDir, repo.Name) + + var repoFS billy.Filesystem + + r, freshGit, err := readGitRepo(repoDir, repoURL.String()) + if err != nil { + return fmt.Errorf("failed to open repo") + } + + err = r.FetchContext(ctx, &git.FetchOptions{ + Progress: os.Stderr, + Force: true, + }) + if err != nil && !errors.Is(err, git.NoErrAlreadyUpToDate) { + return err + } + + var old *plumbing.Reference + + 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) + } + + if !freshGit { + old, err = r.Head() if err != nil { return err } - slog.Info(gotext.Get("Pulling repository"), "name", repo.Name) - repoDir := filepath.Join(rs.cfg.GetPaths().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 - } - - 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 - } - - old, err := r.Head() - if err != nil { - return err - } - - 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) - } - - err = w.Checkout(&git.CheckoutOptions{ - Hash: plumbing.NewHash(revHash.String()), - Force: true, - }) - if err != nil { - return err - } - repoFS = w.Filesystem - - 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() { - 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 = os.RemoveAll(repoDir) - if err != nil { - return err - } - - err = os.MkdirAll(repoDir, 0o755) - if err != nil { - return err - } - - 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 - } - - err = rs.processRepoFull(ctx, repo, repoDir) - if err != nil { - return err - } - - repoFS = osfs.New(repoDir) + if old.Hash() == *revHash { + slog.Info(gotext.Get("Repository up to date"), "name", repo.Name) } + } - fl, err := repoFS.Open("alr-repo.toml") - if err != nil { - slog.Warn(gotext.Get("Git repository does not appear to be a valid ALR repo"), "repo", repo.Name) - continue - } + err = w.Checkout(&git.CheckoutOptions{ + Hash: plumbing.NewHash(revHash.String()), + Force: true, + }) + if err != nil { + return err + } + repoFS = w.Filesystem - var repoCfg types.RepoConfig - err = toml.NewDecoder(fl).Decode(&repoCfg) + 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() || freshGit { + err = rs.processRepoFull(ctx, repo, repoDir) if err != nil { return err } - fl.Close() - - // If the version doesn't have a "v" prefix, it's not a standard version. - // It may be "unknown" or a git version, but either way, there's no way - // to compare it to the repo version, so only compare versions with the "v". - if strings.HasPrefix(config.Version, "v") { - if vercmp.Compare(config.Version, repoCfg.Repo.MinVersion) == -1 { - slog.Warn(gotext.Get("ALR repo's minimum ALR version is greater than the current version. Try updating ALR if something doesn't work."), "repo", repo.Name) - } + } else { + err = rs.processRepoChanges(ctx, repo, r, w, old, new) + if err != nil { + return err } } + fl, err := repoFS.Open("alr-repo.toml") + if err != nil { + slog.Warn(gotext.Get("Git repository does not appear to be a valid ALR repo"), "repo", repo.Name) + return nil + } + + var repoCfg types.RepoConfig + err = toml.NewDecoder(fl).Decode(&repoCfg) + if err != nil { + return err + } + fl.Close() + + // If the version doesn't have a "v" prefix, it's not a standard version. + // It may be "unknown" or a git version, but either way, there's no way + // to compare it to the repo version, so only compare versions with the "v". + if strings.HasPrefix(config.Version, "v") { + if vercmp.Compare(config.Version, repoCfg.Repo.MinVersion) == -1 { + slog.Warn(gotext.Get("ALR repo's minimum ALR version is greater than the current version. Try updating ALR if something doesn't work."), "repo", repo.Name) + } + } + + return nil +} + +func updateRemoteURL(r *git.Repository, newURL string) error { + cfg, err := r.Config() + if err != nil { + return err + } + + remote, ok := cfg.Remotes[git.DefaultRemoteName] + if !ok || len(remote.URLs) == 0 { + return fmt.Errorf("no remote '%s' found", git.DefaultRemoteName) + } + + currentURL := remote.URLs[0] + if currentURL == newURL { + return nil + } + + slog.Debug("Updating remote URL", "old", currentURL, "new", newURL) + + err = r.DeleteRemote(git.DefaultRemoteName) + if err != nil { + return fmt.Errorf("failed to delete old remote: %w", err) + } + + _, err = r.CreateRemote(&gitConfig.RemoteConfig{ + Name: git.DefaultRemoteName, + URLs: []string{newURL}, + }) + if err != nil { + return fmt.Errorf("failed to create new remote: %w", err) + } + return nil } diff --git a/internal/repos/pull_test.go b/internal/repos/pull_test.go index 56e306a..b921e88 100644 --- a/internal/repos/pull_test.go +++ b/internal/repos/pull_test.go @@ -26,7 +26,6 @@ import ( "testing" "gitea.plemya-x.ru/Plemya-x/ALR/internal/config" - "gitea.plemya-x.ru/Plemya-x/ALR/internal/db" database "gitea.plemya-x.ru/Plemya-x/ALR/internal/db" "gitea.plemya-x.ru/Plemya-x/ALR/internal/repos" "gitea.plemya-x.ru/Plemya-x/ALR/pkg/types" @@ -35,7 +34,7 @@ import ( type TestEnv struct { Ctx context.Context Cfg *TestALRConfig - Db *db.Database + Db *database.Database } type TestALRConfig struct { diff --git a/internal/translations/default.pot b/internal/translations/default.pot index 1fe7729..b1c0bb1 100644 --- a/internal/translations/default.pot +++ b/internal/translations/default.pot @@ -383,19 +383,27 @@ msgstr "" msgid "ERROR" msgstr "" -#: internal/repos/pull.go:77 +#: internal/repos/pull.go:78 +msgid "Trying mirror" +msgstr "" + +#: internal/repos/pull.go:84 +msgid "Failed to pull from URL" +msgstr "" + +#: internal/repos/pull.go:151 msgid "Pulling repository" msgstr "" -#: internal/repos/pull.go:113 +#: internal/repos/pull.go:188 msgid "Repository up to date" msgstr "" -#: internal/repos/pull.go:204 +#: internal/repos/pull.go:223 msgid "Git repository does not appear to be a valid ALR repo" msgstr "" -#: internal/repos/pull.go:220 +#: internal/repos/pull.go:239 msgid "" "ALR repo's minimum ALR version is greater than the current version. Try " "updating ALR if something doesn't work." @@ -461,63 +469,104 @@ msgstr "" msgid "Pull all repositories that have changed" msgstr "" -#: repo.go:39 +#: repo.go:41 msgid "Manage repos" msgstr "" -#: repo.go:51 repo.go:269 +#: repo.go:55 repo.go:625 msgid "Remove an existing repository" msgstr "" -#: repo.go:53 +#: repo.go:57 repo.go:521 msgid "" msgstr "" -#: repo.go:83 +#: repo.go:102 repo.go:465 repo.go:568 msgid "Repo \"%s\" does not exist" msgstr "" -#: repo.go:90 +#: repo.go:109 msgid "Error removing repo directory" msgstr "" -#: repo.go:94 repo.go:161 repo.go:219 +#: repo.go:113 repo.go:180 repo.go:253 repo.go:316 repo.go:389 repo.go:504 +#: repo.go:576 msgid "Error saving config" msgstr "" -#: repo.go:113 +#: repo.go:132 msgid "Error removing packages from database" msgstr "" -#: repo.go:124 repo.go:239 +#: repo.go:143 repo.go:595 msgid "Add a new repository" msgstr "" -#: repo.go:125 +#: repo.go:144 repo.go:270 repo.go:345 repo.go:402 msgid " " msgstr "" -#: repo.go:150 +#: repo.go:169 msgid "Repo \"%s\" already exists" msgstr "" -#: repo.go:187 +#: repo.go:206 msgid "Set the reference of the repository" msgstr "" -#: repo.go:188 +#: repo.go:207 msgid " " msgstr "" -#: repo.go:246 +#: repo.go:269 +msgid "Set the main url of the repository" +msgstr "" + +#: repo.go:332 +msgid "Manage mirrors of repos" +msgstr "" + +#: repo.go:344 +msgid "Add a mirror URL to repository" +msgstr "" + +#: repo.go:401 +msgid "Remove mirror from the repository" +msgstr "" + +#: repo.go:420 +msgid "Ignore if mirror does not exist" +msgstr "" + +#: repo.go:425 +msgid "Match partial URL (e.g., github.com instead of full URL)" +msgstr "" + +#: repo.go:490 +msgid "No mirrors containing \"%s\" found in repo \"%s\"" +msgstr "" + +#: repo.go:492 +msgid "URL \"%s\" does not exist in repo \"%s\"" +msgstr "" + +#: repo.go:508 repo.go:580 +msgid "Removed %d mirrors from repo \"%s\"\n" +msgstr "" + +#: repo.go:520 +msgid "Remove all mirrors from the repository" +msgstr "" + +#: repo.go:602 msgid "Name of the new repo" msgstr "" -#: repo.go:252 +#: repo.go:608 msgid "URL of the new repo" msgstr "" -#: repo.go:276 +#: repo.go:632 msgid "Name of the repo to be deleted" msgstr "" diff --git a/internal/translations/po/ru/default.po b/internal/translations/po/ru/default.po index f473162..b8db48a 100644 --- a/internal/translations/po/ru/default.po +++ b/internal/translations/po/ru/default.po @@ -5,15 +5,15 @@ msgid "" msgstr "" "Project-Id-Version: unnamed project\n" -"PO-Revision-Date: 2025-06-15 16:05+0300\n" +"PO-Revision-Date: 2025-06-19 18:54+0300\n" "Last-Translator: Maxim Slipenko \n" "Language-Team: Russian\n" "Language: ru\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" -"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " +"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" "X-Generator: Gtranslator 48.0\n" #: build.go:42 @@ -356,8 +356,8 @@ msgid "" "This command is deprecated and would be removed in the future, use \"%s\" " "instead!" msgstr "" -"Эта команда устарела и будет удалена в будущем, используйте вместо нее \"%s" -"\"!" +"Эта команда устарела и будет удалена в будущем, используйте вместо нее " +"\"%s\"!" #: internal/db/db.go:76 msgid "Database version mismatch; resetting" @@ -397,19 +397,27 @@ msgstr "%s %s загружается — %s/с\n" msgid "ERROR" msgstr "ОШИБКА" -#: internal/repos/pull.go:77 +#: internal/repos/pull.go:78 +msgid "Trying mirror" +msgstr "Пробую зеркало" + +#: internal/repos/pull.go:84 +msgid "Failed to pull from URL" +msgstr "Не удалось извлечь из URL" + +#: internal/repos/pull.go:151 msgid "Pulling repository" msgstr "Скачивание репозитория" -#: internal/repos/pull.go:113 +#: internal/repos/pull.go:188 msgid "Repository up to date" msgstr "Репозиторий уже обновлён" -#: internal/repos/pull.go:204 +#: internal/repos/pull.go:223 msgid "Git repository does not appear to be a valid ALR repo" msgstr "Репозиторий Git не поддерживается репозиторием ALR" -#: internal/repos/pull.go:220 +#: internal/repos/pull.go:239 msgid "" "ALR repo's minimum ALR version is greater than the current version. Try " "updating ALR if something doesn't work." @@ -477,63 +485,104 @@ msgstr "Ошибка при запуске приложения" msgid "Pull all repositories that have changed" msgstr "Скачать все изменённые репозитории" -#: repo.go:39 +#: repo.go:41 msgid "Manage repos" msgstr "Управление репозиториями" -#: repo.go:51 repo.go:269 +#: repo.go:55 repo.go:625 msgid "Remove an existing repository" msgstr "Удалить существующий репозиторий" -#: repo.go:53 +#: repo.go:57 repo.go:521 msgid "" msgstr "<имя>" -#: repo.go:83 +#: repo.go:102 repo.go:465 repo.go:568 msgid "Repo \"%s\" does not exist" msgstr "Репозитория \"%s\" не существует" -#: repo.go:90 +#: repo.go:109 msgid "Error removing repo directory" msgstr "Ошибка при удалении каталога репозитория" -#: repo.go:94 repo.go:161 repo.go:219 +#: repo.go:113 repo.go:180 repo.go:253 repo.go:316 repo.go:389 repo.go:504 +#: repo.go:576 msgid "Error saving config" msgstr "Ошибка при сохранении конфигурации" -#: repo.go:113 +#: repo.go:132 msgid "Error removing packages from database" msgstr "Ошибка при удалении пакетов из базы данных" -#: repo.go:124 repo.go:239 +#: repo.go:143 repo.go:595 msgid "Add a new repository" msgstr "Добавить новый репозиторий" -#: repo.go:125 +#: repo.go:144 repo.go:270 repo.go:345 repo.go:402 msgid " " msgstr "<имя> " -#: repo.go:150 +#: repo.go:169 msgid "Repo \"%s\" already exists" msgstr "Репозиторий \"%s\" уже существует" -#: repo.go:187 +#: repo.go:206 msgid "Set the reference of the repository" msgstr "Установить ссылку на версию репозитория" -#: repo.go:188 +#: repo.go:207 msgid " " msgstr "<имя> <ссылка_на_версию>" -#: repo.go:246 +#: repo.go:269 +msgid "Set the main url of the repository" +msgstr "Установить главный URL репозитория" + +#: repo.go:332 +msgid "Manage mirrors of repos" +msgstr "Управление зеркалами репозитория" + +#: repo.go:344 +msgid "Add a mirror URL to repository" +msgstr "Добавить зеркало репозитория" + +#: repo.go:401 +msgid "Remove mirror from the repository" +msgstr "Удалить зеркало из репозитория" + +#: repo.go:420 +msgid "Ignore if mirror does not exist" +msgstr "Игнорировать, если зеркала не существует" + +#: repo.go:425 +msgid "Match partial URL (e.g., github.com instead of full URL)" +msgstr "Соответствует частичному URL (например, github.com вместо полного URL)" + +#: repo.go:490 +msgid "No mirrors containing \"%s\" found in repo \"%s\"" +msgstr "В репозитории \"%s\" не найдено зеркал, содержащих \"%s\"" + +#: repo.go:492 +msgid "URL \"%s\" does not exist in repo \"%s\"" +msgstr "URL \"%s\" не существует в репозитории \"%s\"" + +#: repo.go:508 repo.go:580 +msgid "Removed %d mirrors from repo \"%s\"\n" +msgstr "Удалены зеркала %d из репозитория \"%s\"\n" + +#: repo.go:520 +msgid "Remove all mirrors from the repository" +msgstr "Удалить все зеркала из репозитория" + +#: repo.go:602 msgid "Name of the new repo" msgstr "Название нового репозитория" -#: repo.go:252 +#: repo.go:608 msgid "URL of the new repo" msgstr "URL-адрес нового репозитория" -#: repo.go:276 +#: repo.go:632 msgid "Name of the repo to be deleted" msgstr "Название репозитория удалён" diff --git a/pkg/types/config.go b/pkg/types/config.go index f6a6238..617df87 100644 --- a/pkg/types/config.go +++ b/pkg/types/config.go @@ -32,7 +32,8 @@ type Config struct { // Repo represents a ALR repo within a configuration file type Repo struct { - Name string `toml:"name"` - URL string `toml:"url"` - Ref string `toml:"ref"` + Name string `toml:"name"` + URL string `toml:"url"` + Ref string `toml:"ref"` + Mirrors []string `toml:"mirrors"` } diff --git a/repo.go b/repo.go index 64123aa..a1d7ea2 100644 --- a/repo.go +++ b/repo.go @@ -20,8 +20,10 @@ package main import ( + "fmt" "os" "path/filepath" + "strings" "github.com/leonelquinteros/gotext" "github.com/urfave/cli/v2" @@ -41,6 +43,8 @@ func RepoCmd() *cli.Command { RemoveRepoCmd(), AddRepoCmd(), SetRepoRefCmd(), + RepoMirrorCmd(), + SetUrlCmd(), }, } } @@ -51,6 +55,21 @@ func RemoveRepoCmd() *cli.Command { Usage: gotext.Get("Remove an existing repository"), Aliases: []string{"rm"}, ArgsUsage: gotext.Get(""), + BashComplete: func(c *cli.Context) { + if c.NArg() == 0 { + // Get repo names from config + ctx := c.Context + deps, err := appbuilder.New(ctx).WithConfig().Build() + if err != nil { + return + } + defer deps.Defer() + + for _, repo := range deps.Cfg.Repos() { + fmt.Println(repo.Name) + } + } + }, Action: utils.RootNeededAction(func(c *cli.Context) error { if c.Args().Len() < 1 { return cliutils.FormatCliExit("missing args", nil) @@ -186,6 +205,21 @@ func SetRepoRefCmd() *cli.Command { Name: "set-ref", Usage: gotext.Get("Set the reference of the repository"), ArgsUsage: gotext.Get(" "), + BashComplete: func(c *cli.Context) { + if c.NArg() == 0 { + // Get repo names from config + ctx := c.Context + deps, err := appbuilder.New(ctx).WithConfig().Build() + if err != nil { + return + } + defer deps.Defer() + + for _, repo := range deps.Cfg.Repos() { + fmt.Println(repo.Name) + } + } + }, Action: utils.RootNeededAction(func(c *cli.Context) error { if c.Args().Len() < 2 { return cliutils.FormatCliExit("missing args", nil) @@ -229,6 +263,328 @@ func SetRepoRefCmd() *cli.Command { } } +func SetUrlCmd() *cli.Command { + return &cli.Command{ + Name: "set-url", + Usage: gotext.Get("Set the main url of the repository"), + ArgsUsage: gotext.Get(" "), + BashComplete: func(c *cli.Context) { + if c.NArg() == 0 { + // Get repo names from config + ctx := c.Context + deps, err := appbuilder.New(ctx).WithConfig().Build() + if err != nil { + return + } + defer deps.Defer() + + for _, repo := range deps.Cfg.Repos() { + fmt.Println(repo.Name) + } + } + }, + Action: utils.RootNeededAction(func(c *cli.Context) error { + if c.Args().Len() < 2 { + return cliutils.FormatCliExit("missing args", nil) + } + + name := c.Args().Get(0) + repoUrl := c.Args().Get(1) + + deps, err := appbuilder. + New(c.Context). + WithConfig(). + WithDB(). + WithReposNoPull(). + Build() + if err != nil { + return err + } + defer deps.Defer() + + repos := deps.Cfg.Repos() + newRepos := []types.Repo{} + for _, repo := range repos { + if repo.Name == name { + repo.URL = repoUrl + } + newRepos = append(newRepos, repo) + } + deps.Cfg.SetRepos(newRepos) + err = deps.Cfg.SaveUserConfig() + if err != nil { + return cliutils.FormatCliExit(gotext.Get("Error saving config"), err) + } + + err = deps.Repos.Pull(c.Context, newRepos) + if err != nil { + return cliutils.FormatCliExit(gotext.Get("Error pulling repositories"), err) + } + + return nil + }), + } +} + +func RepoMirrorCmd() *cli.Command { + return &cli.Command{ + Name: "mirror", + Usage: gotext.Get("Manage mirrors of repos"), + Subcommands: []*cli.Command{ + AddMirror(), + RemoveMirror(), + ClearMirrors(), + }, + } +} + +func AddMirror() *cli.Command { + return &cli.Command{ + Name: "add", + Usage: gotext.Get("Add a mirror URL to repository"), + ArgsUsage: gotext.Get(" "), + BashComplete: func(c *cli.Context) { + if c.NArg() == 0 { + ctx := c.Context + deps, err := appbuilder.New(ctx).WithConfig().Build() + if err != nil { + return + } + defer deps.Defer() + + for _, repo := range deps.Cfg.Repos() { + fmt.Println(repo.Name) + } + } + }, + Action: utils.RootNeededAction(func(c *cli.Context) error { + if c.Args().Len() < 2 { + return cliutils.FormatCliExit("missing args", nil) + } + + name := c.Args().Get(0) + url := c.Args().Get(1) + + deps, err := appbuilder. + New(c.Context). + WithConfig(). + WithDB(). + WithReposNoPull(). + Build() + if err != nil { + return err + } + defer deps.Defer() + + repos := deps.Cfg.Repos() + for i, repo := range repos { + if repo.Name == name { + repos[i].Mirrors = append(repos[i].Mirrors, url) + break + } + } + deps.Cfg.SetRepos(repos) + err = deps.Cfg.SaveUserConfig() + if err != nil { + return cliutils.FormatCliExit(gotext.Get("Error saving config"), err) + } + + return nil + }), + } +} + +func RemoveMirror() *cli.Command { + return &cli.Command{ + Name: "remove", + Aliases: []string{"rm"}, + Usage: gotext.Get("Remove mirror from the repository"), + ArgsUsage: gotext.Get(" "), + BashComplete: func(c *cli.Context) { + ctx := c.Context + deps, err := appbuilder.New(ctx).WithConfig().Build() + if err != nil { + return + } + defer deps.Defer() + + if c.NArg() == 0 { + for _, repo := range deps.Cfg.Repos() { + fmt.Println(repo.Name) + } + } + }, + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "ignore-missing", + Usage: gotext.Get("Ignore if mirror does not exist"), + }, + &cli.BoolFlag{ + Name: "partial", + Aliases: []string{"p"}, + Usage: gotext.Get("Match partial URL (e.g., github.com instead of full URL)"), + }, + }, + Action: utils.RootNeededAction(func(c *cli.Context) error { + if c.Args().Len() < 2 { + return cliutils.FormatCliExit("missing args", nil) + } + + name := c.Args().Get(0) + urlToRemove := c.Args().Get(1) + ignoreMissing := c.Bool("ignore-missing") + partialMatch := c.Bool("partial") + + deps, err := appbuilder. + New(c.Context). + WithConfig(). + WithDB(). + WithReposNoPull(). + Build() + if err != nil { + return err + } + defer deps.Defer() + + reposSlice := deps.Cfg.Repos() + repoIndex := -1 + urlIndicesToRemove := []int{} + + // Находим репозиторий + for i, repo := range reposSlice { + if repo.Name == name { + repoIndex = i + break + } + } + + if repoIndex == -1 { + if ignoreMissing { + return nil // Тихо завершаем, если репозиторий не найден + } + return cliutils.FormatCliExit(gotext.Get("Repo \"%s\" does not exist", name), nil) + } + + // Ищем зеркала для удаления + repo := reposSlice[repoIndex] + for j, mirror := range repo.Mirrors { + var match bool + if partialMatch { + // Частичное совпадение - проверяем, содержит ли зеркало указанную строку + match = strings.Contains(mirror, urlToRemove) + } else { + // Точное совпадение + match = mirror == urlToRemove + } + + if match { + urlIndicesToRemove = append(urlIndicesToRemove, j) + } + } + + if len(urlIndicesToRemove) == 0 { + if ignoreMissing { + return nil + } + if partialMatch { + return cliutils.FormatCliExit(gotext.Get("No mirrors containing \"%s\" found in repo \"%s\"", urlToRemove, name), nil) + } else { + return cliutils.FormatCliExit(gotext.Get("URL \"%s\" does not exist in repo \"%s\"", urlToRemove, name), nil) + } + } + + for i := len(urlIndicesToRemove) - 1; i >= 0; i-- { + urlIndex := urlIndicesToRemove[i] + reposSlice[repoIndex].Mirrors = slices.Delete(reposSlice[repoIndex].Mirrors, urlIndex, urlIndex+1) + } + + deps.Cfg.SetRepos(reposSlice) + err = deps.Cfg.SaveUserConfig() + if err != nil { + return cliutils.FormatCliExit(gotext.Get("Error saving config"), err) + } + + if len(urlIndicesToRemove) > 1 { + fmt.Println(gotext.Get("Removed %d mirrors from repo \"%s\"\n", len(urlIndicesToRemove), name)) + } + + return nil + }), + } +} + +func ClearMirrors() *cli.Command { + return &cli.Command{ + Name: "clear", + Aliases: []string{"rm-all"}, + Usage: gotext.Get("Remove all mirrors from the repository"), + ArgsUsage: gotext.Get(""), + BashComplete: func(c *cli.Context) { + if c.NArg() == 0 { + // Get repo names from config + ctx := c.Context + deps, err := appbuilder.New(ctx).WithConfig().Build() + if err != nil { + return + } + defer deps.Defer() + + for _, repo := range deps.Cfg.Repos() { + fmt.Println(repo.Name) + } + } + }, + Action: utils.RootNeededAction(func(c *cli.Context) error { + if c.Args().Len() < 1 { + return cliutils.FormatCliExit("missing args", nil) + } + + name := c.Args().Get(0) + + deps, err := appbuilder. + New(c.Context). + WithConfig(). + WithDB(). + WithReposNoPull(). + Build() + if err != nil { + return err + } + defer deps.Defer() + + reposSlice := deps.Cfg.Repos() + repoIndex := -1 + urlIndicesToRemove := []int{} + + // Находим репозиторий + for i, repo := range reposSlice { + if repo.Name == name { + repoIndex = i + break + } + } + + if repoIndex == -1 { + return cliutils.FormatCliExit(gotext.Get("Repo \"%s\" does not exist", name), nil) + } + + reposSlice[repoIndex].Mirrors = []string{} + + deps.Cfg.SetRepos(reposSlice) + err = deps.Cfg.SaveUserConfig() + if err != nil { + return cliutils.FormatCliExit(gotext.Get("Error saving config"), err) + } + + if len(urlIndicesToRemove) > 1 { + fmt.Println(gotext.Get("Removed %d mirrors from repo \"%s\"\n", len(urlIndicesToRemove), name)) + } + + return nil + }), + } +} + // TODO: remove // // Deprecated: use "alr repo add"