| // Copyright 2019 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| // Binary main converts Jiri source manifests (JSON) into Repo manifests (XML). |
| // |
| // This tool is specific to Fuchsia in that it only understands the subset of either |
| // manifest schema sufficient for producing Repo manifests that can be used with go/ab. |
| // |
| // The key elements of the Repo manifest are: |
| // * <remote> |
| // * <default> |
| // * <project> |
| // |
| // <default> is mostly filled from user-input and specifies the default GoB remote and |
| // branch for <project> elements in the manifest. |
| // |
| // <remote> and <project> elements are built from the entries of the "directories" |
| // property from the input source manifest. |
| // |
| // Source manifests are created relative to some working directory, so there is usually |
| // (always?) a directory entry named ".". This is represented in the Repo manifest by a |
| // <project> whose name attribute is the basename of the directory>git_checkout>repo_url. |
| // |
| // For documentation on Repo manifests, see go/repo-manifests-explained and go/repo. |
| // |
| // For documentation on Jiri manifest, see this proto: |
| // https://chromium.googlesource.com/external/github.com/luci/recipes-py/+/refs/heads/master/recipe_engine/source_manifest.proto |
| package main |
| |
| import ( |
| "context" |
| "errors" |
| "flag" |
| "fmt" |
| "html/template" |
| "io" |
| "log" |
| "net/url" |
| "os" |
| "path/filepath" |
| "sort" |
| "strings" |
| |
| "go.fuchsia.dev/infra/cmd/jiri2repo/jiri" |
| "go.fuchsia.dev/infra/cmd/jiri2repo/repo" |
| ) |
| |
| var bin = filepath.Base(os.Args[0]) |
| |
| // Command-line flags. |
| var ( |
| // The release branch name. |
| // |
| // This is used as the <default> revision in the repo manifest. |
| branch string |
| |
| // The GoB host that will house the output Repo manifest. |
| // |
| // This is set as the <default> remote in the repo manifest. |
| defaultRemote string |
| |
| // Where to write output. Defaults to stdout. |
| output string |
| ) |
| |
| const gobHostSuffix = ".googlesource.com" |
| |
| var usageTemplate = template.Must(template.New("usage").Parse(` |
| {{.Cmd}} [options] SOURCE_MANIFEST_PATH |
| |
| Converts a Jiri source manifest to a Repo manifest. |
| |
| EXAMPLES: |
| |
| {{.Cmd}} -branch releases/canary -remote fuchsia ./source_manifest.json |
| {{.Cmd}} -remote fuchsia ./source_manifest.json |
| {{.Cmd}} -remote fuchsia -output default.xml ./source_manifest.json |
| |
| OPTIONS: |
| `)) |
| |
| func usage() { |
| usageArgs := struct{ Cmd string }{Cmd: bin} |
| usageTemplate.Execute(flag.CommandLine.Output(), usageArgs) |
| flag.PrintDefaults() |
| os.Exit(1) |
| } |
| |
| func init() { |
| flag.Usage = usage |
| flag.StringVar(&branch, "branch", "refs/heads/master", "The release branch") |
| flag.StringVar(&output, "output", "", "Optional filepath to write output to. If empty, stdout is used") |
| flag.StringVar(&defaultRemote, "remote", "", "The SSO GoB host that will house the output Repo manifest") |
| } |
| |
| func main() { |
| flag.Parse() |
| if err := validateArgs(); err != nil { |
| log.Print(err) |
| flag.Usage() |
| } |
| |
| if err := execute(context.Background()); err != nil { |
| log.Fatal(err) |
| } |
| } |
| |
| func validateArgs() error { |
| if flag.NArg() != 1 { |
| return errors.New("expected one positional argument") |
| } |
| if branch == "" { |
| return errors.New("missing -branch") |
| } |
| if defaultRemote == "" { |
| return errors.New("missing -remote") |
| } |
| return nil |
| } |
| |
| func execute(ctx context.Context) (err error) { |
| var in jiri.SourceManifest |
| var out repo.Manifest |
| |
| sourceManifestPath := flag.Arg(0) |
| if err := jiri.ReadSourceManifestFile(sourceManifestPath, &in); err != nil { |
| return fmt.Errorf("failed to read %s: %v", sourceManifestPath, err) |
| } |
| |
| defaults := repo.Default{ |
| Branch: branch, |
| Remote: defaultRemote, |
| } |
| |
| if err := convert(in, &out, defaults); err != nil { |
| return fmt.Errorf("conversion failed: %v", err) |
| } |
| |
| var w io.Writer = os.Stdout |
| if output != "" { |
| w, err = os.Create(output) |
| if err != nil { |
| return fmt.Errorf("failed to open output file %q: %v", output, err) |
| } |
| } |
| if err := repo.WriteManifest(w, &out); err != nil { |
| return fmt.Errorf("failed to write Repo manifest: %v", err) |
| } |
| |
| return nil |
| } |
| |
| // Converts the input source manifest to a Repo manifest. Writes the output to out. |
| // |
| // defaults is written as the Repo manifest's <default> element. Any ".googlesource.com" |
| // suffix is stripped from defaults.Remote before writing the output. |
| func convert(in jiri.SourceManifest, out *repo.Manifest, defaults repo.Default) error { |
| // Tracks GoB remote names to avoid outputting duplicate <remote> elements. |
| remotes := make(map[string]bool) |
| |
| // Remotes are recorded without GoB host suffixes in Repo manifests. |
| defaults.Remote = strings.TrimSuffix(defaults.Remote, gobHostSuffix) |
| |
| // Convert each source manifest directory to a <project> and <remote> |
| for name, dir := range in.Directories { |
| project, remote, err := convertDirectory(name, *dir, defaults) |
| if err != nil { |
| return err |
| } |
| out.Project = append(out.Project, *project) |
| if _, ok := remotes[remote.Name]; !ok { |
| remotes[remote.Name] = true |
| out.Remote = append(out.Remote, *remote) |
| } |
| } |
| |
| // If there is no <remote> with the specified GoB remote name, either the user |
| // mistakenly entered the wrong default remote or the output Repo manifest will be |
| // stored on a git host that is separate from any of the repos listed in the manifest. |
| // The second scenario is unlikely; Err on the side of caution and fail. |
| if !remotes[defaults.Remote] { |
| return fmt.Errorf("the input manifest contained no projects from the specified default remote %q", defaults.Remote) |
| } |
| |
| out.Default = defaults |
| out.Comment = fmt.Sprintf("Auto generated by %s. DO NOT EDIT", bin) |
| |
| // Sort sections for deterministic output. |
| sort.Slice(out.Remote, func(a, b int) bool { |
| return out.Remote[a].Name < out.Remote[b].Name |
| }) |
| sort.Slice(out.Project, func(a, b int) bool { |
| return out.Project[a].Name < out.Project[b].Name |
| }) |
| |
| return nil |
| } |
| |
| // convertDirectory converts a Jiri source manifest directory object into a set of Repo |
| // <project> and <remote> elements. |
| // |
| // The directory name is used as the project's name and path. If the directory name is ".", |
| // the basename of directory's repository url is used instead. The empty string is not |
| // allowed and results in an error. |
| func convertDirectory(name string, d jiri.SourceManifest_Directory, defaults repo.Default) (*repo.Project, *repo.Remote, error) { |
| // Resolve the directory name. |
| if name == "." { |
| name = filepath.Base(d.GitCheckout.RepoUrl) |
| } |
| if name == "" { |
| return nil, nil, fmt.Errorf("source manifest directory has no name: %v", d) |
| } |
| |
| checkout := d.GitCheckout |
| reviewURL, err := ssoCodeReviewURL(checkout.RepoUrl) |
| if err != nil { |
| return nil, nil, err |
| } |
| |
| remote := &repo.Remote{ |
| // By convention the remote name is the GoB hostname and not the review hostname. |
| Name: strings.TrimSuffix(reviewURL.Host, "-review"), |
| Fetch: checkout.FetchRef, |
| Review: reviewURL.String(), |
| } |
| project := &repo.Project{ |
| Name: name, |
| Path: name, |
| Remote: remote.Name, |
| Revision: checkout.Revision, |
| } |
| |
| if project.Remote == defaults.Remote { |
| project.Remote = "" |
| } |
| return project, remote, nil |
| } |
| |
| func ssoCodeReviewURL(gobURL string) (*url.URL, error) { |
| u, err := url.Parse(gobURL) |
| if err != nil { |
| return nil, err |
| } |
| // URL.Hostname() returns the empty string if the scheme is empty. |
| if u.Scheme == "" { |
| return nil, fmt.Errorf("invalid URL %q: origin has no scheme", gobURL) |
| } |
| hostname := u.Hostname() |
| if !strings.HasSuffix(hostname, gobHostSuffix) { |
| return nil, fmt.Errorf("cannot compute code review host for non Git-on-Borg hostname %q", hostname) |
| } |
| host := strings.TrimSuffix(hostname, gobHostSuffix) |
| |
| return &url.URL{ |
| Scheme: "sso", |
| Host: host + "-review", |
| Path: "/", |
| }, nil |
| } |