blob: d95007df0253d946e749821a33b041998f4e3033 [file] [log] [blame]
// Copyright 2019 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package repo
import (
"context"
"encoding/xml"
"fmt"
"go.chromium.org/chromiumos/infra/go/internal/gerrit"
"go.chromium.org/luci/common/errors"
"io/ioutil"
"log"
"net/http"
"path/filepath"
)
var (
// Name of the root XML file to seek in manifest-internal.
rootXml = "snapshot.xml"
)
// Manifest is a top-level Repo definition file.
type Manifest struct {
Includes []Include `xml:"include"`
Projects []Project `xml:"project"`
Remotes []Remote `xml:"remote"`
Default Default `xml:"default"`
}
// Project is an element of a manifest containing a Gerrit project to source path definition.
type Project struct {
Path string `xml:"path,attr"`
Name string `xml:"name,attr"`
Revision string `xml:"revision,attr"`
Upstream string `xml:"upstream,attr"`
RemoteName string `xml:"remote,attr"`
}
// Include is a manifest element that imports another manifest file.
type Include struct {
Name string `xml:"name,attr"`
}
// Remote is a manifest element that lists a remote.
type Remote struct {
Fetch string `xml:"fetch,attr"`
Name string `xml:"name,attr"`
Revision string `xml:"revision,attr"`
}
// Default is a manifest element that lists the default.
type Default struct {
RemoteName string `xml:"remote,attr"`
Revision string `xml:"revision,attr"`
}
func (m *Manifest) getRemoteByName(name string) *Remote {
for _, remote := range m.Remotes {
if remote.Name == name {
return &remote
}
}
return &Remote{}
}
// LoadManifestFromFile loads the manifest at the given file path into
// a Manifest struct. It also loads all included manifests.
// Returns a map mapping manifest filenames to file contents.
func LoadManifestFromFile(file string) (map[string]*Manifest, error) {
results := make(map[string]*Manifest)
data, err := ioutil.ReadFile(file)
if err != nil {
return nil, errors.Annotate(err, "failed to open and read %s", file).Err()
}
manifest := &Manifest{}
if err = xml.Unmarshal(data, manifest); err != nil {
return nil, errors.Annotate(err, "failed to unmarshal %s", file).Err()
}
for i, project := range manifest.Projects {
// Set default remote on projects without an explicit remote
if project.RemoteName == "" {
project.RemoteName = manifest.Default.RemoteName
}
// Set default revision on projects without an explicit revision
if project.Revision == "" {
remote := manifest.getRemoteByName(project.RemoteName)
if remote.Revision == "" {
project.Revision = manifest.Default.Revision
} else {
project.Revision = remote.Revision
}
}
manifest.Projects[i] = project
}
results[file] = manifest
// Recursively fetch manifests listed in "include" elements.
for _, incl := range manifest.Includes {
// Include paths are relative to the manifest location.
inclPath := filepath.Join(filepath.Dir(file), incl.Name)
subResults, err := LoadManifestFromFile(inclPath)
if err != nil {
return nil, err
}
for k, v := range subResults {
results[k] = v
}
}
return results, nil
}
func fetchManifestRecursive(authedClient *http.Client, ctx context.Context, manifestCommit string, file string) (map[string]*Manifest, error) {
results := make(map[string]*Manifest)
log.Printf("Fetching manifest file %s at revision '%s'", file, manifestCommit)
files, err := gerrit.FetchFilesFromGitiles(
authedClient,
ctx,
"chrome-internal.googlesource.com",
"chromeos/manifest-internal",
manifestCommit,
[]string{file})
if err != nil {
return nil, errors.Annotate(err, "failed to fetch %s", file).Err()
}
manifest := &Manifest{}
if err = xml.Unmarshal([]byte((*files)[file]), manifest); err != nil {
return nil, errors.Annotate(err, "failed to unmarshal %s", file).Err()
}
results[file] = manifest
// Recursively fetch manifests listed in "include" elements.
for _, incl := range manifest.Includes {
subResults, err := fetchManifestRecursive(authedClient, ctx, manifestCommit, incl.Name)
if err != nil {
return nil, err
}
for k, v := range subResults {
results[k] = v
}
}
return results, nil
}
// GetRepoToSourceRootFromManifests constructs a Gerrit project to path mapping by fetching manifest
// XML files from Gitiles.
func GetRepoToSourceRootFromManifests(authedClient *http.Client, ctx context.Context, manifestCommit string) (map[string]string, error) {
manifests, err := fetchManifestRecursive(authedClient, ctx, manifestCommit, rootXml)
if err != nil {
return nil, err
}
repoToSourceRoot := make(map[string]string)
for _, m := range manifests {
for _, p := range m.Projects {
repoToSourceRoot[p.Name] = p.Path
}
}
log.Printf("Found %d repo to source root mappings from manifest files", len(repoToSourceRoot))
return repoToSourceRoot, nil
}
// Return the unique project with the given name (nil if the project DNE).
// Return an error if multiple projects with the given name exist.
func (m *Manifest) GetUniqueProject(name string) (Project, error) {
var project Project
matchingProjects := 0
for _, p := range m.Projects {
if p.Name == name {
matchingProjects++
if matchingProjects > 1 {
return Project{}, fmt.Errorf("multiple projects named %s", name)
}
project = p
}
}
return project, nil
}