package main import ( "fmt" "os" "os/exec" "path/filepath" "strings" "sync" alpm "github.com/Jguer/go-alpm" "github.com/leonelquinteros/gotext" "github.com/pkg/errors" "github.com/Jguer/yay/v10/pkg/dep" "github.com/Jguer/yay/v10/pkg/multierror" "github.com/Jguer/yay/v10/pkg/query" "github.com/Jguer/yay/v10/pkg/text" ) const gitDiffRefName = "AUR_SEEN" // Update the YAY_DIFF_REVIEW ref to HEAD. We use this ref to determine which diff were // reviewed by the user func gitUpdateSeenRef(path, name string) error { _, stderr, err := capture(passToGit(filepath.Join(path, name), "update-ref", gitDiffRefName, "HEAD")) if err != nil { return fmt.Errorf("%s %s", stderr, err) } return nil } // Return wether or not we have reviewed a diff yet. It checks for the existence of // YAY_DIFF_REVIEW in the git ref-list func gitHasLastSeenRef(path, name string) bool { _, _, err := capture(passToGit(filepath.Join(path, name), "rev-parse", "--quiet", "--verify", gitDiffRefName)) return err == nil } // Returns the last reviewed hash. If YAY_DIFF_REVIEW exists it will return this hash. // If it does not it will return empty tree as no diff have been reviewed yet. func getLastSeenHash(path, name string) (string, error) { if gitHasLastSeenRef(path, name) { stdout, stderr, err := capture(passToGit(filepath.Join(path, name), "rev-parse", gitDiffRefName)) if err != nil { return "", fmt.Errorf("%s %s", stderr, err) } lines := strings.Split(stdout, "\n") return lines[0], nil } return gitEmptyTree, nil } // Check whether or not a diff exists between the last reviewed diff and // HEAD@{upstream} func gitHasDiff(path, name string) (bool, error) { if gitHasLastSeenRef(path, name) { stdout, stderr, err := capture(passToGit(filepath.Join(path, name), "rev-parse", gitDiffRefName, "HEAD@{upstream}")) if err != nil { return false, fmt.Errorf("%s%s", stderr, err) } lines := strings.Split(stdout, "\n") lastseen := lines[0] upstream := lines[1] return lastseen != upstream, nil } // If YAY_DIFF_REVIEW does not exists, we have never reviewed a diff for this package // and should display it. return true, nil } // TODO: yay-next passes args through the header, use that to unify ABS and AUR func gitDownloadABS(url, path, name string) (bool, error) { if err := os.MkdirAll(path, 0o755); err != nil { return false, err } if _, errExist := os.Stat(filepath.Join(path, name)); os.IsNotExist(errExist) { cmd := passToGit(path, "clone", "--no-progress", "--single-branch", "-b", "packages/"+name, url, name) cmd.Env = append(os.Environ(), "GIT_TERMINAL_PROMPT=0") _, stderr, err := capture(cmd) if err != nil { return false, fmt.Errorf(gotext.Get("error cloning %s: %s", name, stderr)) } return true, nil } else if errExist != nil { return false, fmt.Errorf(gotext.Get("error reading %s", filepath.Join(path, name, ".git"))) } cmd := passToGit(filepath.Join(path, name), "pull", "--ff-only") cmd.Env = append(os.Environ(), "GIT_TERMINAL_PROMPT=0") _, stderr, err := capture(cmd) if err != nil { return false, fmt.Errorf(gotext.Get("error fetching %s: %s", name, stderr)) } return true, nil } func gitDownload(url, path, name string) (bool, error) { _, err := os.Stat(filepath.Join(path, name, ".git")) if os.IsNotExist(err) { cmd := passToGit(path, "clone", "--no-progress", url, name) cmd.Env = append(os.Environ(), "GIT_TERMINAL_PROMPT=0") _, stderr, errCapture := capture(cmd) if errCapture != nil { return false, fmt.Errorf(gotext.Get("error cloning %s: %s", name, stderr)) } return true, nil } else if err != nil { return false, fmt.Errorf(gotext.Get("error reading %s", filepath.Join(path, name, ".git"))) } cmd := passToGit(filepath.Join(path, name), "fetch") cmd.Env = append(os.Environ(), "GIT_TERMINAL_PROMPT=0") _, stderr, err := capture(cmd) if err != nil { return false, fmt.Errorf(gotext.Get("error fetching %s: %s", name, stderr)) } return false, nil } func gitMerge(path, name string) error { _, stderr, err := capture(passToGit(filepath.Join(path, name), "reset", "--hard", "HEAD")) if err != nil { return fmt.Errorf(gotext.Get("error resetting %s: %s", name, stderr)) } _, stderr, err = capture(passToGit(filepath.Join(path, name), "merge", "--no-edit", "--ff")) if err != nil { return fmt.Errorf(gotext.Get("error merging %s: %s", name, stderr)) } return nil } func getPkgbuilds(pkgs []string, alpmHandle *alpm.Handle, force bool) error { missing := false wd, err := os.Getwd() if err != nil { return err } pkgs = query.RemoveInvalidTargets(pkgs, config.Runtime.Mode) aur, repo, err := packageSlices(pkgs, alpmHandle) if err != nil { return err } for n := range aur { _, pkg := text.SplitDBFromName(aur[n]) aur[n] = pkg } info, err := query.AURInfoPrint(aur, config.RequestSplitN) if err != nil { return err } if len(repo) > 0 { missing, err = getPkgbuildsfromABS(repo, wd, alpmHandle, force) if err != nil { return err } } if len(aur) > 0 { allBases := dep.GetBases(info) bases := make([]dep.Base, 0) for _, base := range allBases { name := base.Pkgbase() pkgDest := filepath.Join(wd, name) _, err = os.Stat(pkgDest) if os.IsNotExist(err) { bases = append(bases, base) } else if err != nil { text.Errorln(err) continue } else { if force { if err = os.RemoveAll(pkgDest); err != nil { text.Errorln(err) continue } bases = append(bases, base) } else { text.Warnln(gotext.Get("%s already exists. Use -f/--force to overwrite", pkgDest)) continue } } } if _, err = downloadPkgbuilds(bases, nil, wd); err != nil { return err } missing = missing || len(aur) != len(info) } if missing { err = fmt.Errorf("") } return err } // GetPkgbuild downloads pkgbuild from the ABS. func getPkgbuildsfromABS(pkgs []string, path string, alpmHandle *alpm.Handle, force bool) (bool, error) { var wg sync.WaitGroup var mux sync.Mutex var errs multierror.MultiError names := make(map[string]string) missing := make([]string, 0) downloaded := 0 dbList, err := alpmHandle.SyncDBs() if err != nil { return false, err } for _, pkgN := range pkgs { var pkg *alpm.Package var err error var url string pkgDB, name := text.SplitDBFromName(pkgN) if pkgDB != "" { if db, errSync := alpmHandle.SyncDBByName(pkgDB); errSync == nil { pkg = db.Pkg(name) } } else { _ = dbList.ForEach(func(db alpm.DB) error { if pkg = db.Pkg(name); pkg != nil { return fmt.Errorf("") } return nil }) } if pkg == nil { missing = append(missing, name) continue } name = pkg.Base() if name == "" { name = pkg.Name() } // TODO: Check existence with ls-remote // https://git.archlinux.org/svntogit/packages.git switch pkg.DB().Name() { case "core", "extra", "testing": url = "https://git.archlinux.org/svntogit/packages.git" case "community", "multilib", "community-testing", "multilib-testing": url = "https://git.archlinux.org/svntogit/community.git" default: missing = append(missing, name) continue } _, err = os.Stat(filepath.Join(path, name)) switch { case err != nil && !os.IsNotExist(err): text.Errorln(err) continue case os.IsNotExist(err), force: if err = os.RemoveAll(filepath.Join(path, name)); err != nil { text.Errorln(err) continue } default: text.Warn(gotext.Get("%s already downloaded -- use -f to overwrite", cyan(name))) continue } names[name] = url } if len(missing) != 0 { text.Warnln(gotext.Get("Missing ABS packages:"), cyan(strings.Join(missing, ", "))) } download := func(pkg string, url string) { defer wg.Done() if _, err := gitDownloadABS(url, config.ABSDir, pkg); err != nil { errs.Add(errors.New(gotext.Get("failed to get pkgbuild: %s: %s", cyan(pkg), err.Error()))) return } _, stderr, err := capture(exec.Command("cp", "-r", filepath.Join(config.ABSDir, pkg, "trunk"), filepath.Join(path, pkg))) mux.Lock() downloaded++ if err != nil { errs.Add(errors.New(gotext.Get("failed to link %s: %s", cyan(pkg), stderr))) } else { fmt.Fprintln(os.Stdout, gotext.Get("(%d/%d) Downloaded PKGBUILD from ABS: %s", downloaded, len(names), cyan(pkg))) } mux.Unlock() } count := 0 for name, url := range names { wg.Add(1) go download(name, url) count++ if count%25 == 0 { wg.Wait() } } wg.Wait() return len(missing) != 0, errs.Return() }