feat: support mirrors
This commit is contained in:
		| @@ -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 | ||||
| } | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user