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"
]