blob: 35cdcb5232924e2e713424afd7b6cebfec1776fe [file] [log] [blame]
// Copyright 2017 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.
package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"strings"
"fuchsia.googlesource.com/jiri"
"fuchsia.googlesource.com/jiri/cmdline"
"fuchsia.googlesource.com/jiri/gitutil"
"fuchsia.googlesource.com/jiri/project"
)
type arrayFlag []string
func (i *arrayFlag) String() string {
return strings.Join(*i, ", ")
}
func (i *arrayFlag) Set(value string) error {
*i = append(*i, value)
return nil
}
var editFlags struct {
projects arrayFlag
imports arrayFlag
jsonOutput string
}
type projectChanges struct {
Name string `json:"name"`
Remote string `json:"remote"`
Path string `json:"path"`
OldRev string `json:"old_revision"`
NewRev string `json:"new_revision"`
}
type importChanges struct {
Name string `json:"name"`
Remote string `json:"remote"`
OldRev string `json:"old_revision"`
NewRev string `json:"new_revision"`
}
type editChanges struct {
Projects []projectChanges `json:"projects"`
Imports []importChanges `json:"imports"`
}
func (ec *editChanges) toFile(filename string) error {
if err := os.MkdirAll(filepath.Dir(filename), 0755); err != nil {
return err
}
out, err := json.MarshalIndent(ec, "", " ")
if err != nil {
return fmt.Errorf("failed to serialize JSON output: %s\n", err)
}
err = ioutil.WriteFile(filename, out, 0600)
if err != nil {
return fmt.Errorf("failed write JSON output to %s: %s\n", filename, err)
}
return nil
}
var cmdEdit = &cmdline.Command{
Runner: jiri.RunnerFunc(runEdit),
Name: "edit",
Short: "Edit manifest file",
Long: `Edit manifest file by rolling the revision of provided projects`,
ArgsName: "<manifest>",
ArgsLong: "<manifest> is path of the manifest",
}
func init() {
flags := &cmdEdit.Flags
flags.Var(&editFlags.projects, "project", "List of projects to update. It is of form <project-name>=<newRef> where newRef is optional. It can be specified multiple times.")
flags.Var(&editFlags.imports, "import", "List of imports to update. It is of form <import-name>=<newRef> where newRef is optional. It can be specified multiple times.")
flags.StringVar(&editFlags.jsonOutput, "json-output", "", "File to print changes to, in json format.")
}
func runEdit(jirix *jiri.X, args []string) error {
if len(args) != 1 {
return jirix.UsageErrorf("Wrong number of args")
}
manifestPath, err := filepath.Abs(args[0])
if err != nil {
return err
}
if len(editFlags.projects) == 0 && len(editFlags.imports) == 0 {
return jirix.UsageErrorf("Please provide -project or/and -import flag")
}
projects := make(map[string]string)
imports := make(map[string]string)
for _, p := range editFlags.projects {
s := strings.SplitN(p, "=", 2)
if len(s) == 1 {
projects[s[0]] = ""
} else {
projects[s[0]] = s[1]
}
}
for _, i := range editFlags.imports {
s := strings.SplitN(i, "=", 2)
if len(s) == 1 {
imports[s[0]] = ""
} else {
imports[s[0]] = s[1]
}
}
return updateManifest(jirix, manifestPath, projects, imports)
}
func updateRevision(manifestContent, tag, currentRevision, newRevision, name string) (string, error) {
if currentRevision != "" && currentRevision != "HEAD" {
return strings.Replace(manifestContent, currentRevision, newRevision, 1), nil
}
r, err := regexp.Compile(fmt.Sprintf("( *?)<%s [^<]*?name=%q(.|\\n)*?\\/>", tag, name))
if err != nil {
return "", err
}
t := r.FindStringSubmatch(manifestContent)
if t == nil {
return "", fmt.Errorf("Not able to match %s %q", tag, name)
}
s := t[0]
spaces := t[1]
for i := 0; i < len(tag); i++ {
spaces = spaces + " "
}
us := strings.Replace(s, "/>", fmt.Sprintf("\n%s revision=%q/>", spaces, newRevision), 1)
return strings.Replace(manifestContent, s, us, 1), nil
}
func updateManifest(jirix *jiri.X, manifestPath string, projects, imports map[string]string) error {
ec := &editChanges{
Projects: []projectChanges{},
Imports: []importChanges{},
}
m, err := project.ManifestFromFile(jirix, manifestPath)
if err != nil {
return err
}
content, err := ioutil.ReadFile(manifestPath)
if err != nil {
return err
}
manifestContent := string(content)
scm := gitutil.New(jirix, gitutil.RootDirOpt(filepath.Dir(manifestPath)))
for _, p := range m.Projects {
newRevision := ""
if rev, ok := projects[p.Name]; !ok {
continue
} else {
newRevision = rev
}
if newRevision == "" {
branch := "master"
if p.RemoteBranch != "" {
branch = p.RemoteBranch
}
out, err := scm.LsRemote(p.Remote, fmt.Sprintf("refs/heads/%s", branch))
if err != nil {
return err
}
newRevision = strings.Fields(string(out))[0]
}
if p.Revision == newRevision {
continue
}
manifestContent, err = updateRevision(manifestContent, "project", p.Revision, newRevision, p.Name)
if err != nil {
return err
}
ec.Projects = append(ec.Projects, projectChanges{
Name: p.Name,
Remote: p.Remote,
Path: p.Path,
OldRev: p.Revision,
NewRev: newRevision,
})
}
for _, i := range m.Imports {
newRevision := ""
if rev, ok := imports[i.Name]; !ok {
continue
} else {
newRevision = rev
}
if newRevision == "" {
branch := "master"
if i.RemoteBranch != "" {
branch = i.RemoteBranch
}
out, err := scm.LsRemote(i.Remote, fmt.Sprintf("refs/heads/%s", branch))
if err != nil {
return err
}
newRevision = strings.Fields(string(out))[0]
}
if i.Revision == newRevision {
continue
}
manifestContent, err = updateRevision(manifestContent, "import", i.Revision, newRevision, i.Name)
if err != nil {
return err
}
ec.Imports = append(ec.Imports, importChanges{
Name: i.Name,
Remote: i.Remote,
OldRev: i.Revision,
NewRev: newRevision,
})
}
if editFlags.jsonOutput != "" {
if err := ec.toFile(editFlags.jsonOutput); err != nil {
return err
}
}
return ioutil.WriteFile(manifestPath, []byte(manifestContent), os.ModePerm)
}