feat: support mirrors
	
		
			
	
		
	
	
		
	
		
			All checks were successful
		
		
	
	
		
			
				
	
				Pre-commit / pre-commit (pull_request) Successful in 6m8s
				
			
		
		
	
	
				
					
				
			
		
			All checks were successful
		
		
	
	Pre-commit / pre-commit (pull_request) Successful in 6m8s
				
			This commit is contained in:
		| @@ -11,7 +11,7 @@ | |||||||
|     <g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"> |     <g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"> | ||||||
|         <text x="33.5" y="15" fill="#010101" fill-opacity=".3">coverage</text> |         <text x="33.5" y="15" fill="#010101" fill-opacity=".3">coverage</text> | ||||||
|         <text x="33.5" y="14">coverage</text> |         <text x="33.5" y="14">coverage</text> | ||||||
|         <text x="86" y="15" fill="#010101" fill-opacity=".3">19.4%</text> |         <text x="86" y="15" fill="#010101" fill-opacity=".3">18.8%</text> | ||||||
|         <text x="86" y="14">19.4%</text> |         <text x="86" y="14">18.8%</text> | ||||||
|     </g> |     </g> | ||||||
| </svg> | </svg> | ||||||
|   | |||||||
| Before Width: | Height: | Size: 926 B After Width: | Height: | Size: 926 B | 
							
								
								
									
										53
									
								
								e2e-tests/issue_78_mirrors_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								e2e-tests/issue_78_mirrors_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 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") | ||||||
|  | 		}, | ||||||
|  | 	) | ||||||
|  | } | ||||||
| @@ -31,7 +31,6 @@ import ( | |||||||
| 	"strings" | 	"strings" | ||||||
|  |  | ||||||
| 	"github.com/go-git/go-billy/v5" | 	"github.com/go-git/go-billy/v5" | ||||||
| 	"github.com/go-git/go-billy/v5/osfs" |  | ||||||
| 	"github.com/go-git/go-git/v5" | 	"github.com/go-git/go-git/v5" | ||||||
| 	gitConfig "github.com/go-git/go-git/v5/config" | 	gitConfig "github.com/go-git/go-git/v5/config" | ||||||
| 	"github.com/go-git/go-git/v5/plumbing" | 	"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 { | 	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 { | 		if err != nil { | ||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		slog.Info(gotext.Get("Pulling repository"), "name", repo.Name) | 		if old.Hash() == *revHash { | ||||||
| 		repoDir := filepath.Join(rs.cfg.GetPaths().RepoDir, repo.Name) | 			slog.Info(gotext.Get("Repository up to date"), "name", 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) |  | ||||||
| 		} | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 		fl, err := repoFS.Open("alr-repo.toml") | 	err = w.Checkout(&git.CheckoutOptions{ | ||||||
| 		if err != nil { | 		Hash:  plumbing.NewHash(revHash.String()), | ||||||
| 			slog.Warn(gotext.Get("Git repository does not appear to be a valid ALR repo"), "repo", repo.Name) | 		Force: true, | ||||||
| 			continue | 	}) | ||||||
| 		} | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	repoFS = w.Filesystem | ||||||
|  |  | ||||||
| 		var repoCfg types.RepoConfig | 	new, err := r.Head() | ||||||
| 		err = toml.NewDecoder(fl).Decode(&repoCfg) | 	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 { | 		if err != nil { | ||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
| 		fl.Close() | 	} else { | ||||||
|  | 		err = rs.processRepoChanges(ctx, repo, r, w, old, new) | ||||||
| 		// If the version doesn't have a "v" prefix, it's not a standard version. | 		if err != nil { | ||||||
| 		// It may be "unknown" or a git version, but either way, there's no way | 			return err | ||||||
| 		// 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) |  | ||||||
| 			} |  | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	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 | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -26,7 +26,6 @@ import ( | |||||||
| 	"testing" | 	"testing" | ||||||
|  |  | ||||||
| 	"gitea.plemya-x.ru/Plemya-x/ALR/internal/config" | 	"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" | 	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/internal/repos" | ||||||
| 	"gitea.plemya-x.ru/Plemya-x/ALR/pkg/types" | 	"gitea.plemya-x.ru/Plemya-x/ALR/pkg/types" | ||||||
| @@ -35,7 +34,7 @@ import ( | |||||||
| type TestEnv struct { | type TestEnv struct { | ||||||
| 	Ctx context.Context | 	Ctx context.Context | ||||||
| 	Cfg *TestALRConfig | 	Cfg *TestALRConfig | ||||||
| 	Db  *db.Database | 	Db  *database.Database | ||||||
| } | } | ||||||
|  |  | ||||||
| type TestALRConfig struct { | type TestALRConfig struct { | ||||||
|   | |||||||
| @@ -383,19 +383,27 @@ msgstr "" | |||||||
| msgid "ERROR" | msgid "ERROR" | ||||||
| msgstr "" | 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" | msgid "Pulling repository" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: internal/repos/pull.go:113 | #: internal/repos/pull.go:188 | ||||||
| msgid "Repository up to date" | msgid "Repository up to date" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: internal/repos/pull.go:204 | #: internal/repos/pull.go:223 | ||||||
| msgid "Git repository does not appear to be a valid ALR repo" | msgid "Git repository does not appear to be a valid ALR repo" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: internal/repos/pull.go:220 | #: internal/repos/pull.go:239 | ||||||
| msgid "" | msgid "" | ||||||
| "ALR repo's minimum ALR version is greater than the current version. Try " | "ALR repo's minimum ALR version is greater than the current version. Try " | ||||||
| "updating ALR if something doesn't work." | "updating ALR if something doesn't work." | ||||||
| @@ -461,63 +469,104 @@ msgstr "" | |||||||
| msgid "Pull all repositories that have changed" | msgid "Pull all repositories that have changed" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: repo.go:39 | #: repo.go:41 | ||||||
| msgid "Manage repos" | msgid "Manage repos" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: repo.go:51 repo.go:269 | #: repo.go:55 repo.go:625 | ||||||
| msgid "Remove an existing repository" | msgid "Remove an existing repository" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: repo.go:53 | #: repo.go:57 repo.go:521 | ||||||
| msgid "<name>" | msgid "<name>" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: repo.go:83 | #: repo.go:102 repo.go:465 repo.go:568 | ||||||
| msgid "Repo \"%s\" does not exist" | msgid "Repo \"%s\" does not exist" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: repo.go:90 | #: repo.go:109 | ||||||
| msgid "Error removing repo directory" | msgid "Error removing repo directory" | ||||||
| msgstr "" | 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" | msgid "Error saving config" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: repo.go:113 | #: repo.go:132 | ||||||
| msgid "Error removing packages from database" | msgid "Error removing packages from database" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: repo.go:124 repo.go:239 | #: repo.go:143 repo.go:595 | ||||||
| msgid "Add a new repository" | msgid "Add a new repository" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: repo.go:125 | #: repo.go:144 repo.go:270 repo.go:345 repo.go:402 | ||||||
| msgid "<name> <url>" | msgid "<name> <url>" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: repo.go:150 | #: repo.go:169 | ||||||
| msgid "Repo \"%s\" already exists" | msgid "Repo \"%s\" already exists" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: repo.go:187 | #: repo.go:206 | ||||||
| msgid "Set the reference of the repository" | msgid "Set the reference of the repository" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: repo.go:188 | #: repo.go:207 | ||||||
| msgid "<name> <ref>" | msgid "<name> <ref>" | ||||||
| msgstr "" | 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" | msgid "Name of the new repo" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: repo.go:252 | #: repo.go:608 | ||||||
| msgid "URL of the new repo" | msgid "URL of the new repo" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: repo.go:276 | #: repo.go:632 | ||||||
| msgid "Name of the repo to be deleted" | msgid "Name of the repo to be deleted" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
|   | |||||||
| @@ -5,15 +5,15 @@ | |||||||
| msgid "" | msgid "" | ||||||
| msgstr "" | msgstr "" | ||||||
| "Project-Id-Version: unnamed project\n" | "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 <maks1ms@alt-gnome.ru>\n" | "Last-Translator: Maxim Slipenko <maks1ms@alt-gnome.ru>\n" | ||||||
| "Language-Team: Russian\n" | "Language-Team: Russian\n" | ||||||
| "Language: ru\n" | "Language: ru\n" | ||||||
| "MIME-Version: 1.0\n" | "MIME-Version: 1.0\n" | ||||||
| "Content-Type: text/plain; charset=UTF-8\n" | "Content-Type: text/plain; charset=UTF-8\n" | ||||||
| "Content-Transfer-Encoding: 8bit\n" | "Content-Transfer-Encoding: 8bit\n" | ||||||
| "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" | "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " | ||||||
| "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" | "n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" | ||||||
| "X-Generator: Gtranslator 48.0\n" | "X-Generator: Gtranslator 48.0\n" | ||||||
|  |  | ||||||
| #: build.go:42 | #: build.go:42 | ||||||
| @@ -356,8 +356,8 @@ msgid "" | |||||||
| "This command is deprecated and would be removed in the future, use \"%s\" " | "This command is deprecated and would be removed in the future, use \"%s\" " | ||||||
| "instead!" | "instead!" | ||||||
| msgstr "" | msgstr "" | ||||||
| "Эта команда устарела и будет удалена в будущем, используйте вместо нее \"%s" | "Эта команда устарела и будет удалена в будущем, используйте вместо нее " | ||||||
| "\"!" | "\"%s\"!" | ||||||
|  |  | ||||||
| #: internal/db/db.go:76 | #: internal/db/db.go:76 | ||||||
| msgid "Database version mismatch; resetting" | msgid "Database version mismatch; resetting" | ||||||
| @@ -397,19 +397,27 @@ msgstr "%s %s загружается — %s/с\n" | |||||||
| msgid "ERROR" | msgid "ERROR" | ||||||
| msgstr "ОШИБКА" | 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" | msgid "Pulling repository" | ||||||
| msgstr "Скачивание репозитория" | msgstr "Скачивание репозитория" | ||||||
|  |  | ||||||
| #: internal/repos/pull.go:113 | #: internal/repos/pull.go:188 | ||||||
| msgid "Repository up to date" | msgid "Repository up to date" | ||||||
| msgstr "Репозиторий уже обновлён" | msgstr "Репозиторий уже обновлён" | ||||||
|  |  | ||||||
| #: internal/repos/pull.go:204 | #: internal/repos/pull.go:223 | ||||||
| msgid "Git repository does not appear to be a valid ALR repo" | msgid "Git repository does not appear to be a valid ALR repo" | ||||||
| msgstr "Репозиторий Git не поддерживается репозиторием ALR" | msgstr "Репозиторий Git не поддерживается репозиторием ALR" | ||||||
|  |  | ||||||
| #: internal/repos/pull.go:220 | #: internal/repos/pull.go:239 | ||||||
| msgid "" | msgid "" | ||||||
| "ALR repo's minimum ALR version is greater than the current version. Try " | "ALR repo's minimum ALR version is greater than the current version. Try " | ||||||
| "updating ALR if something doesn't work." | "updating ALR if something doesn't work." | ||||||
| @@ -477,63 +485,104 @@ msgstr "Ошибка при запуске приложения" | |||||||
| msgid "Pull all repositories that have changed" | msgid "Pull all repositories that have changed" | ||||||
| msgstr "Скачать все изменённые репозитории" | msgstr "Скачать все изменённые репозитории" | ||||||
|  |  | ||||||
| #: repo.go:39 | #: repo.go:41 | ||||||
| msgid "Manage repos" | msgid "Manage repos" | ||||||
| msgstr "Управление репозиториями" | msgstr "Управление репозиториями" | ||||||
|  |  | ||||||
| #: repo.go:51 repo.go:269 | #: repo.go:55 repo.go:625 | ||||||
| msgid "Remove an existing repository" | msgid "Remove an existing repository" | ||||||
| msgstr "Удалить существующий репозиторий" | msgstr "Удалить существующий репозиторий" | ||||||
|  |  | ||||||
| #: repo.go:53 | #: repo.go:57 repo.go:521 | ||||||
| msgid "<name>" | msgid "<name>" | ||||||
| msgstr "<имя>" | msgstr "<имя>" | ||||||
|  |  | ||||||
| #: repo.go:83 | #: repo.go:102 repo.go:465 repo.go:568 | ||||||
| msgid "Repo \"%s\" does not exist" | msgid "Repo \"%s\" does not exist" | ||||||
| msgstr "Репозитория \"%s\" не существует" | msgstr "Репозитория \"%s\" не существует" | ||||||
|  |  | ||||||
| #: repo.go:90 | #: repo.go:109 | ||||||
| msgid "Error removing repo directory" | msgid "Error removing repo directory" | ||||||
| msgstr "Ошибка при удалении каталога репозитория" | 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" | msgid "Error saving config" | ||||||
| msgstr "Ошибка при сохранении конфигурации" | msgstr "Ошибка при сохранении конфигурации" | ||||||
|  |  | ||||||
| #: repo.go:113 | #: repo.go:132 | ||||||
| msgid "Error removing packages from database" | msgid "Error removing packages from database" | ||||||
| msgstr "Ошибка при удалении пакетов из базы данных" | msgstr "Ошибка при удалении пакетов из базы данных" | ||||||
|  |  | ||||||
| #: repo.go:124 repo.go:239 | #: repo.go:143 repo.go:595 | ||||||
| msgid "Add a new repository" | msgid "Add a new repository" | ||||||
| msgstr "Добавить новый репозиторий" | msgstr "Добавить новый репозиторий" | ||||||
|  |  | ||||||
| #: repo.go:125 | #: repo.go:144 repo.go:270 repo.go:345 repo.go:402 | ||||||
| msgid "<name> <url>" | msgid "<name> <url>" | ||||||
| msgstr "<имя> <url>" | msgstr "<имя> <url>" | ||||||
|  |  | ||||||
| #: repo.go:150 | #: repo.go:169 | ||||||
| msgid "Repo \"%s\" already exists" | msgid "Repo \"%s\" already exists" | ||||||
| msgstr "Репозиторий \"%s\" уже существует" | msgstr "Репозиторий \"%s\" уже существует" | ||||||
|  |  | ||||||
| #: repo.go:187 | #: repo.go:206 | ||||||
| msgid "Set the reference of the repository" | msgid "Set the reference of the repository" | ||||||
| msgstr "Установить ссылку на версию репозитория" | msgstr "Установить ссылку на версию репозитория" | ||||||
|  |  | ||||||
| #: repo.go:188 | #: repo.go:207 | ||||||
| msgid "<name> <ref>" | msgid "<name> <ref>" | ||||||
| msgstr "<имя> <ссылка_на_версию>" | 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" | msgid "Name of the new repo" | ||||||
| msgstr "Название нового репозитория" | msgstr "Название нового репозитория" | ||||||
|  |  | ||||||
| #: repo.go:252 | #: repo.go:608 | ||||||
| msgid "URL of the new repo" | msgid "URL of the new repo" | ||||||
| msgstr "URL-адрес нового репозитория" | msgstr "URL-адрес нового репозитория" | ||||||
|  |  | ||||||
| #: repo.go:276 | #: repo.go:632 | ||||||
| msgid "Name of the repo to be deleted" | msgid "Name of the repo to be deleted" | ||||||
| msgstr "Название репозитория  удалён" | msgstr "Название репозитория  удалён" | ||||||
|  |  | ||||||
|   | |||||||
| @@ -32,7 +32,8 @@ type Config struct { | |||||||
|  |  | ||||||
| // Repo represents a ALR repo within a configuration file | // Repo represents a ALR repo within a configuration file | ||||||
| type Repo struct { | type Repo struct { | ||||||
| 	Name string `toml:"name"` | 	Name    string   `toml:"name"` | ||||||
| 	URL  string `toml:"url"` | 	URL     string   `toml:"url"` | ||||||
| 	Ref  string `toml:"ref"` | 	Ref     string   `toml:"ref"` | ||||||
|  | 	Mirrors []string `toml:"mirrors"` | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										356
									
								
								repo.go
									
									
									
									
									
								
							
							
						
						
									
										356
									
								
								repo.go
									
									
									
									
									
								
							| @@ -20,8 +20,10 @@ | |||||||
| package main | package main | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"fmt" | ||||||
| 	"os" | 	"os" | ||||||
| 	"path/filepath" | 	"path/filepath" | ||||||
|  | 	"strings" | ||||||
|  |  | ||||||
| 	"github.com/leonelquinteros/gotext" | 	"github.com/leonelquinteros/gotext" | ||||||
| 	"github.com/urfave/cli/v2" | 	"github.com/urfave/cli/v2" | ||||||
| @@ -41,6 +43,8 @@ func RepoCmd() *cli.Command { | |||||||
| 			RemoveRepoCmd(), | 			RemoveRepoCmd(), | ||||||
| 			AddRepoCmd(), | 			AddRepoCmd(), | ||||||
| 			SetRepoRefCmd(), | 			SetRepoRefCmd(), | ||||||
|  | 			RepoMirrorCmd(), | ||||||
|  | 			SetUrlCmd(), | ||||||
| 		}, | 		}, | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| @@ -51,6 +55,21 @@ func RemoveRepoCmd() *cli.Command { | |||||||
| 		Usage:     gotext.Get("Remove an existing repository"), | 		Usage:     gotext.Get("Remove an existing repository"), | ||||||
| 		Aliases:   []string{"rm"}, | 		Aliases:   []string{"rm"}, | ||||||
| 		ArgsUsage: gotext.Get("<name>"), | 		ArgsUsage: gotext.Get("<name>"), | ||||||
|  | 		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 { | 		Action: utils.RootNeededAction(func(c *cli.Context) error { | ||||||
| 			if c.Args().Len() < 1 { | 			if c.Args().Len() < 1 { | ||||||
| 				return cliutils.FormatCliExit("missing args", nil) | 				return cliutils.FormatCliExit("missing args", nil) | ||||||
| @@ -186,6 +205,21 @@ func SetRepoRefCmd() *cli.Command { | |||||||
| 		Name:      "set-ref", | 		Name:      "set-ref", | ||||||
| 		Usage:     gotext.Get("Set the reference of the repository"), | 		Usage:     gotext.Get("Set the reference of the repository"), | ||||||
| 		ArgsUsage: gotext.Get("<name> <ref>"), | 		ArgsUsage: gotext.Get("<name> <ref>"), | ||||||
|  | 		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 { | 		Action: utils.RootNeededAction(func(c *cli.Context) error { | ||||||
| 			if c.Args().Len() < 2 { | 			if c.Args().Len() < 2 { | ||||||
| 				return cliutils.FormatCliExit("missing args", nil) | 				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("<name> <url>"), | ||||||
|  | 		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("<name> <url>"), | ||||||
|  | 		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("<name> <url>"), | ||||||
|  | 		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("<name>"), | ||||||
|  | 		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 | // TODO: remove | ||||||
| // | // | ||||||
| // Deprecated: use "alr repo add" | // Deprecated: use "alr repo add" | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user