func upgradeDepDecl()

in go/tools/releaser/upgradedep.go [243:490]


func upgradeDepDecl(ctx context.Context, gh *githubClient, workDir, name string, call *bzl.CallExpr, uploadToMirror bool) (err error) {
	defer func() {
		if err != nil {
			err = fmt.Errorf("upgrading %s: %w", name, err)
		}
	}()

	// Find a '# releaser:upgrade-dep org repo' comment. We could probably
	// figure this out from URLs but this also serves to mark a dependency as
	// being automatically upgradeable.
	orgName, repoName, err := parseUpgradeDepDirective(call)
	if err != nil {
		return err
	}

	// Find attributes we'll need to read or write. We'll modify these directly
	// in the AST. Nothing else should read or write them while we're working.
	attrs := map[string]*bzl.Expr{
		"patches":      nil,
		"sha256":       nil,
		"strip_prefix": nil,
		"urls":         nil,
	}
	var urlsKwarg *bzl.AssignExpr
	for _, arg := range call.List {
		kwarg, ok := arg.(*bzl.AssignExpr)
		if !ok {
			continue
		}
		key := kwarg.LHS.(*bzl.Ident) // required by parser
		if _, ok := attrs[key.Name]; ok {
			attrs[key.Name] = &kwarg.RHS
		}
		if key.Name == "urls" {
			urlsKwarg = kwarg
		}
	}
	for key := range attrs {
		if key == "patches" {
			// Don't add optional attributes.
			continue
		}
		if attrs[key] == nil {
			kwarg := &bzl.AssignExpr{LHS: &bzl.Ident{Name: key}, Op: "="}
			call.List = append(call.List, kwarg)
			attrs[key] = &kwarg.RHS
		}
	}

	// Find the highest tag in semver order, ignoring whether the version has a
	// leading "v" or not. If there are no tags, find the commit at the tip of the
	// default branch.
	tags, err := gh.listTags(ctx, orgName, repoName)
	if err != nil {
		return err
	}

	vname := func(name string) string {
		if !strings.HasPrefix(name, "v") {
			return "v" + name
		}
		return name
	}

	w := 0
	for r := range tags {
		name := vname(*tags[r].Name)
		if name != semver.Canonical(name) {
			continue
		}
		tags[w] = tags[r]
		w++
	}
	tags = tags[:w]

	var highestTag *github.RepositoryTag
	var highestVname string
	for _, tag := range tags {
		name := vname(*tag.Name)
		if highestTag == nil || semver.Compare(name, highestVname) > 0 {
			highestTag = tag
			highestVname = name
		}
	}

	var ghURL, stripPrefix, urlComment string
	date := time.Now().Format("2006-01-02")
	if highestTag != nil {
		// If the tag is part of a release, check whether there is a release
		// artifact we should use.
		release, _, err := gh.Repositories.GetReleaseByTag(ctx, orgName, repoName, *highestTag.Name)
		if err == nil {
			wantNames := []string{
				fmt.Sprintf("%s-%s.tar.gz", repoName, *highestTag.Name),
				fmt.Sprintf("%s-%s.zip", repoName, *highestTag.Name),
			}
		AssetName:
			for _, asset := range release.Assets {
				for _, wantName := range wantNames {
					if *asset.Name == wantName {
						ghURL = asset.GetBrowserDownloadURL()
						stripPrefix = "" // may not always be correct
						break AssetName
					}
				}
			}
		}
		if ghURL == "" {
			ghURL = fmt.Sprintf("https://github.com/%s/%s/archive/%s.zip", orgName, repoName, *highestTag.Name)
			stripPrefix = repoName + "-" + strings.TrimPrefix(*highestTag.Name, "v")
		}
		urlComment = fmt.Sprintf("%s, latest as of %s", *highestTag.Name, date)
	} else {
		repo, _, err := gh.Repositories.Get(ctx, orgName, repoName)
		if err != nil {
			return err
		}
		defaultBranchName := "main"
		if repo.DefaultBranch != nil {
			defaultBranchName = *repo.DefaultBranch
		}
		branch, _, err := gh.Repositories.GetBranch(ctx, orgName, repoName, defaultBranchName)
		if err != nil {
			return err
		}
		ghURL = fmt.Sprintf("https://github.com/%s/%s/archive/%s.zip", orgName, repoName, *branch.Commit.SHA)
		stripPrefix = repoName + "-" + *branch.Commit.SHA
		urlComment = fmt.Sprintf("%s, as of %s", defaultBranchName, date)
	}
	ghURLWithoutScheme := ghURL[len("https://"):]
	mirrorURL := "https://mirror.bazel.build/" + ghURLWithoutScheme

	// Download the archive and find the SHA.
	archiveFile, err := os.CreateTemp("", "")
	if err != nil {
		return err
	}
	defer func() {
		archiveFile.Close()
		if rerr := os.Remove(archiveFile.Name()); err == nil && rerr != nil {
			err = rerr
		}
	}()
	resp, err := http.Get(ghURL)
	if err != nil {
		return err
	}
	hw := sha256.New()
	mw := io.MultiWriter(hw, archiveFile)
	if _, err := io.Copy(mw, resp.Body); err != nil {
		resp.Body.Close()
		return err
	}
	if err := resp.Body.Close(); err != nil {
		return err
	}
	sha256Sum := hex.EncodeToString(hw.Sum(nil))
	if _, err := archiveFile.Seek(0, io.SeekStart); err != nil {
		return err
	}

	// Upload the archive to mirror.bazel.build.
	if uploadToMirror {
		if err := copyFileToMirror(ctx, ghURLWithoutScheme, archiveFile.Name()); err != nil {
			return err
		}
	}

	// If there are patches, re-apply or re-generate them.
	// Patch labels may have "# releaser:patch-cmd name args..." directives
	// that instruct this program to generate the patch by running a commnad
	// in the directory. If there is no such directive, we apply the old patch
	// using "patch". In either case, we'll generate a new patch with "diff".
	// We'll scrub the timestamps to avoid excessive diffs in the PR that
	// updates dependencies.
	rootDir, err := repoRoot()
	if err != nil {
		return err
	}
	if attrs["patches"] != nil {
		if err != nil {
			return err
		}
		patchDir := filepath.Join(workDir, name, "a")
		if err := extractArchive(archiveFile, path.Base(ghURL), patchDir, stripPrefix); err != nil {
			return err
		}

		patchesList, ok := (*attrs["patches"]).(*bzl.ListExpr)
		if !ok {
			return fmt.Errorf("\"patches\" attribute is not a list")
		}
		for patchIndex, patchLabelExpr := range patchesList.List {
			patchLabelValue, comments, err := parsePatchesItem(patchLabelExpr)
			if err != nil {
				return fmt.Errorf("parsing expr %#v : %w", patchLabelExpr, err)
			}

			if !strings.HasPrefix(patchLabelValue, "//third_party:") {
				return fmt.Errorf("patch does not start with '//third_party:': %q", patchLabelValue)
			}
			patchName := patchLabelValue[len("//third_party:"):]
			patchPath := filepath.Join(rootDir, "third_party", patchName)
			prevDir := filepath.Join(workDir, name, string('a'+patchIndex))
			patchDir := filepath.Join(workDir, name, string('a'+patchIndex+1))
			var patchCmd []string
			for _, c := range comments.Before {
				words := strings.Fields(strings.TrimPrefix(c.Token, "#"))
				if len(words) > 0 && words[0] == "releaser:patch-cmd" {
					patchCmd = words[1:]
					break
				}
			}

			if err := copyDir(patchDir, prevDir); err != nil {
				return err
			}
			if patchCmd == nil {
				if err := runForError(ctx, patchDir, "patch", "-Np1", "-i", patchPath); err != nil {
					return err
				}
			} else {
				if err := runForError(ctx, patchDir, patchCmd[0], patchCmd[1:]...); err != nil {
					return err
				}
			}
			patch, _ := runForOutput(ctx, filepath.Join(workDir, name), "diff", "-urN", string('a'+patchIndex), string('a'+patchIndex+1))
			patch = sanitizePatch(patch)
			if err := os.WriteFile(patchPath, patch, 0666); err != nil {
				return err
			}
		}
	}

	// Update the attributes.
	*attrs["sha256"] = &bzl.StringExpr{Value: sha256Sum}
	*attrs["strip_prefix"] = &bzl.StringExpr{Value: stripPrefix}
	*attrs["urls"] = &bzl.ListExpr{
		List: []bzl.Expr{
			&bzl.StringExpr{Value: mirrorURL},
			&bzl.StringExpr{Value: ghURL},
		},
		ForceMultiLine: true,
	}
	urlsKwarg.Before = []bzl.Comment{{Token: "# " + urlComment}}

	return nil
}