Add jiri edit command

This command has been ported from scripts/update-manifest.go in fuchsia

Change-Id: I906224a58e1a7cf43a85ddcacc82d7df6ebdecb4
diff --git a/cmd/jiri/cmd.go b/cmd/jiri/cmd.go
index 0ca9601..7222385 100644
--- a/cmd/jiri/cmd.go
+++ b/cmd/jiri/cmd.go
@@ -56,6 +56,7 @@
 		Children: []*cmdline.Command{
 			cmdBranch,
 			cmdDiff,
+			cmdEdit,
 			cmdGrep,
 			cmdImport,
 			cmdInit,
diff --git a/cmd/jiri/edit.go b/cmd/jiri/edit.go
new file mode 100644
index 0000000..13b9228
--- /dev/null
+++ b/cmd/jiri/edit.go
@@ -0,0 +1,97 @@
+// 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 (
+	"fmt"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"strings"
+
+	"fuchsia.googlesource.com/jiri"
+	"fuchsia.googlesource.com/jiri/cmdline"
+	"fuchsia.googlesource.com/jiri/gitutil"
+	"fuchsia.googlesource.com/jiri/project"
+)
+
+type stringsValue []string
+
+func (i *stringsValue) String() string {
+	return strings.Join(*i, ",")
+}
+
+func (i *stringsValue) Set(value string) error {
+	*i = strings.Split(value, ",")
+	return nil
+}
+
+var editFlags struct {
+	projects stringsValue
+}
+
+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, "projects", "List of projects to update")
+}
+
+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
+	}
+	projects := make(map[string]struct{})
+	for _, p := range editFlags.projects {
+		projects[p] = struct{}{}
+	}
+
+	return updateManifest(jirix, manifestPath, projects)
+}
+
+func updateManifest(jirix *jiri.X, manifestPath string, projects map[string]struct{}) error {
+	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 {
+
+		if _, ok := projects[p.Name]; !ok {
+			continue
+		}
+
+		if p.Revision != "" {
+			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
+			}
+			latestRevision := strings.Fields(string(out))[0]
+			manifestContent = strings.Replace(manifestContent, p.Revision, latestRevision, 1)
+		}
+	}
+
+	return ioutil.WriteFile(manifestPath, []byte(manifestContent), os.ModePerm)
+}
diff --git a/gitutil/git.go b/gitutil/git.go
index ad51338..a3759bd 100644
--- a/gitutil/git.go
+++ b/gitutil/git.go
@@ -326,6 +326,19 @@
 	return g.run("branch", "-u", upstream, branch)
 }
 
+func (g *Git) LsRemote(args ...string) (string, error) {
+	a := []string{"ls-remote"}
+	a = append(a, args...)
+	out, err := g.runOutput(a...)
+	if err != nil {
+		return "", err
+	}
+	if got, want := len(out), 1; got != want {
+		return "", fmt.Errorf("git ls-remote %s: unexpected length of %s: got %s, want %s", strings.Join(args, " "), out, got, want)
+	}
+	return out[0], nil
+}
+
 // CreateBranchWithUpstream creates a new branch and sets the upstream
 // repository to the given upstream.
 func (g *Git) CreateBranchWithUpstream(branch, upstream string) error {