| // 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" |
| "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 |
| packages arrayFlag |
| jsonOutput string |
| editMode string |
| } |
| |
| const ( |
| manifest = "manifest" |
| lockfile = "lockfile" |
| both = "both" |
| ) |
| |
| 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 packageChanges struct { |
| Name string `json:"name"` |
| OldVer string `json:"old_version"` |
| NewVer string `json:"new_version"` |
| } |
| |
| type editChanges struct { |
| Projects []projectChanges `json:"projects"` |
| Imports []importChanges `json:"imports"` |
| Packages []packageChanges `json:"packages"` |
| } |
| |
| 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 |
| } |
| |
| // TODO(IN-361): Make this a subcommand of 'manifest' |
| 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, imports or packages`, |
| 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>=<revision> where revision is optional. It can be specified multiple times.") |
| flags.Var(&editFlags.imports, "import", "List of imports to update. It is of form <import-name>=<revision> where revision is optional. It can be specified multiple times.") |
| flags.Var(&editFlags.packages, "package", "List of packages to update. It is of form <package-name>=<version>. It can be specified multiple times.") |
| flags.StringVar(&editFlags.jsonOutput, "json-output", "", "File to print changes to, in json format.") |
| flags.StringVar(&editFlags.editMode, "edit-mode", "both", "Edit mode. It can be 'manifest' for updating project revisions in manifest only, 'lockfile' for updating project revisions in lockfile only or 'both' for updating project revisions in both files.") |
| } |
| |
| func runEdit(jirix *jiri.X, args []string) error { |
| if len(args) != 1 { |
| return jirix.UsageErrorf("Wrong number of args") |
| } |
| |
| editFlags.editMode = strings.ToLower(editFlags.editMode) |
| if editFlags.editMode != manifest && editFlags.editMode != lockfile && editFlags.editMode != both { |
| return fmt.Errorf("unsupported edit-mode: %q", editFlags.editMode) |
| } |
| |
| manifestPath, err := filepath.Abs(args[0]) |
| if err != nil { |
| return err |
| } |
| if len(editFlags.projects) == 0 && len(editFlags.imports) == 0 && len(editFlags.packages) == 0 { |
| return jirix.UsageErrorf("Please provide -project, -import and/or -package flag") |
| } |
| projects := make(map[string]string) |
| imports := make(map[string]string) |
| packages := 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] |
| } |
| } |
| for _, p := range editFlags.packages { |
| s := strings.SplitN(p, "=", 2) |
| if len(s) == 1 { |
| return jirix.UsageErrorf("Please provide the -package flag in the form <package-name>=<version>") |
| } else { |
| packages[s[0]] = s[1] |
| } |
| } |
| |
| return updateManifest(jirix, manifestPath, projects, imports, packages) |
| } |
| |
| func writeManifest(jirix *jiri.X, manifestPath, manifestContent string, projects map[string]string) error { |
| // Create a temp dir to save backedup lockfiles |
| tempDir, err := ioutil.TempDir("", "jiri_lockfile") |
| if err != nil { |
| return err |
| } |
| defer os.RemoveAll(tempDir) |
| |
| // map "backup" stores the mapping between updated lockfile with backups |
| backup := make(map[string]string) |
| rewind := func() { |
| for k, v := range backup { |
| if err := os.Rename(v, k); err != nil { |
| jirix.Logger.Errorf("failed to revert changes to lockfile %q", k) |
| } else { |
| jirix.Logger.Debugf("reverted lockfile %q", k) |
| } |
| } |
| } |
| |
| isLockfileDir := func(jirix *jiri.X, s string) bool { |
| switch s { |
| case "", ".", jirix.Root, string(filepath.Separator): |
| return false |
| } |
| return true |
| } |
| |
| if len(projects) != 0 && (editFlags.editMode == lockfile || editFlags.editMode == both) { |
| // Search lockfiles and update |
| dir := manifestPath |
| for ; isLockfileDir(jirix, dir); dir = path.Dir(dir) { |
| lockfile := path.Join(path.Dir(dir), jirix.LockfileName) |
| |
| if _, err := os.Stat(lockfile); err != nil { |
| jirix.Logger.Debugf("lockfile could not be accessed at %q due to error %v", lockfile, err) |
| continue |
| } |
| if err := updateLocks(jirix, tempDir, lockfile, backup, projects); err != nil { |
| rewind() |
| return err |
| } |
| } |
| } |
| |
| if err := ioutil.WriteFile(manifestPath, []byte(manifestContent), os.ModePerm); err != nil { |
| rewind() |
| return err |
| } |
| return nil |
| } |
| |
| func updateLocks(jirix *jiri.X, tempDir, lockfile string, backup, projects map[string]string) error { |
| jirix.Logger.Debugf("try updating lockfile %q", lockfile) |
| bin, err := ioutil.ReadFile(lockfile) |
| if err != nil { |
| return err |
| } |
| |
| projectLocks, packageLocks, err := project.UnmarshalLockEntries(bin) |
| if err != nil { |
| return err |
| } |
| |
| found := false |
| for k, v := range projectLocks { |
| if newRev, ok := projects[string(k)]; ok { |
| v.Revision = newRev |
| projectLocks[k] = v |
| found = true |
| } |
| } |
| |
| if found { |
| // backup original lockfile |
| info, err := os.Stat(lockfile) |
| if err != nil { |
| return err |
| } |
| backupName := path.Join(tempDir, path.Base(lockfile)) |
| if err := ioutil.WriteFile(backupName, bin, info.Mode()); err != nil { |
| return err |
| } |
| backup[lockfile] = backupName |
| ebin, err := project.MarshalLockEntries(projectLocks, packageLocks) |
| if err != nil { |
| return err |
| } |
| jirix.Logger.Debugf("updated lockfile %q", lockfile) |
| return ioutil.WriteFile(lockfile, ebin, info.Mode()) |
| } |
| jirix.Logger.Debugf("skipped lockfile %q, no matching projects", lockfile) |
| return nil |
| } |
| |
| func updateRevision(manifestContent, tag, currentRevision, newRevision, name string) (string, error) { |
| if currentRevision != "" && currentRevision != "HEAD" { |
| return strings.Replace(manifestContent, currentRevision, newRevision, 1), nil |
| } |
| return updateRevisionOrVersionAttr(manifestContent, tag, newRevision, name, "revision") |
| } |
| |
| func updateVersion(manifestContent, tag string, pc packageChanges) (string, error) { |
| // There are chances multiple packages share the same version tag, |
| // therefore, we cannot simple replace version string globally. |
| // Unlike project declaration, the version attribute of a package is not |
| // allowed to be empty. |
| name := regexp.QuoteMeta(pc.Name) |
| oldVal := regexp.QuoteMeta(pc.OldVer) |
| // Avoid using %q in regex, it behaves differently from regex.QuoteMeta. |
| r, err := regexp.Compile(fmt.Sprintf("( *?)<%s[\\s\\n]+[^<]*?name=\"%s\"(.|\\n)*?version=\"%s\"(.|\\n)*?\\/>", tag, name, oldVal)) |
| if err != nil { |
| return "", err |
| } |
| t := r.FindStringSubmatch(manifestContent) |
| if t == nil { |
| return "", fmt.Errorf("Not able to match %s \"%s\"", tag, name) |
| } |
| s := t[0] |
| us := strings.Replace(s, fmt.Sprintf("version=\"%s\"", pc.OldVer), fmt.Sprintf("version=\"%s\"", pc.NewVer), 1) |
| return strings.Replace(manifestContent, s, us, 1), nil |
| } |
| |
| func updateRevisionOrVersionAttr(manifestContent, tag, newAttrValue, name, attr string) (string, error) { |
| name = regexp.QuoteMeta(name) |
| // Avoid using %q in regex, it behaves differently from regex.QuoteMeta. |
| r, err := regexp.Compile(fmt.Sprintf("( *?)<%s[\\s\\n]+[^<]*?name=\"%s\"(.|\\n)*?\\/>", tag, name)) |
| if err != nil { |
| return "", err |
| } |
| t := r.FindStringSubmatch(manifestContent) |
| if t == nil { |
| return "", fmt.Errorf("Not able to match %s \"%s\"", 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 %s=%q/>", spaces, attr, newAttrValue), 1) |
| return strings.Replace(manifestContent, s, us, 1), nil |
| } |
| |
| func updateManifest(jirix *jiri.X, manifestPath string, projects, imports, packages map[string]string) error { |
| ec := &editChanges{ |
| Projects: []projectChanges{}, |
| Imports: []importChanges{}, |
| Packages: []packageChanges{}, |
| } |
| |
| m, err := project.ManifestFromFile(jirix, manifestPath) |
| if err != nil { |
| return err |
| } |
| content, err := ioutil.ReadFile(manifestPath) |
| if err != nil { |
| return err |
| } |
| manifestContent := string(content) |
| editedProjects := make(map[string]string) |
| 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 |
| } |
| if editFlags.editMode == manifest || editFlags.editMode == both { |
| manifestContent, err = updateRevision(manifestContent, "project", p.Revision, newRevision, p.Name) |
| if err != nil { |
| return err |
| } |
| } |
| editedProjects[string(p.Key())] = newRevision |
| 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, |
| }) |
| } |
| |
| for _, p := range m.Packages { |
| newVersion := "" |
| if ver, ok := packages[p.Name]; !ok { |
| continue |
| } else { |
| newVersion = ver |
| } |
| if newVersion == "" || p.Version == newVersion { |
| continue |
| } |
| pc := packageChanges{ |
| Name: p.Name, |
| OldVer: p.Version, |
| NewVer: newVersion, |
| } |
| manifestContent, err = updateVersion(manifestContent, "package", pc) |
| if err != nil { |
| return err |
| } |
| ec.Packages = append(ec.Packages, pc) |
| } |
| if editFlags.jsonOutput != "" { |
| if err := ec.toFile(editFlags.jsonOutput); err != nil { |
| return err |
| } |
| } |
| |
| return writeManifest(jirix, manifestPath, manifestContent, editedProjects) |
| } |