summaryrefslogtreecommitdiff
path: root/src/cmd/hgpatch
diff options
context:
space:
mode:
Diffstat (limited to 'src/cmd/hgpatch')
-rw-r--r--src/cmd/hgpatch/Makefile11
-rw-r--r--src/cmd/hgpatch/doc.go18
-rw-r--r--src/cmd/hgpatch/main.go358
3 files changed, 387 insertions, 0 deletions
diff --git a/src/cmd/hgpatch/Makefile b/src/cmd/hgpatch/Makefile
new file mode 100644
index 000000000..1ef98d7f9
--- /dev/null
+++ b/src/cmd/hgpatch/Makefile
@@ -0,0 +1,11 @@
+# Copyright 2009 The Go Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file.
+
+include ../../Make.inc
+
+TARG=hgpatch
+GOFILES=\
+ main.go\
+
+include ../../Make.cmd
diff --git a/src/cmd/hgpatch/doc.go b/src/cmd/hgpatch/doc.go
new file mode 100644
index 000000000..1e0f1da38
--- /dev/null
+++ b/src/cmd/hgpatch/doc.go
@@ -0,0 +1,18 @@
+// Copyright 2009 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+/*
+
+Hgpatch applies a patch to the local Mercurial repository.
+The patch should have been been generated by
+a version control system like CVS, Git, Mercurial, or Subversion.
+If successful, hgpatch writes a list of affected files to standard output.
+
+Hgpatch is meant to be used by the Mercurial codereview extension.
+
+Usage:
+ hgpatch [patchfile]
+
+*/
+package documentation
diff --git a/src/cmd/hgpatch/main.go b/src/cmd/hgpatch/main.go
new file mode 100644
index 000000000..9e338abcb
--- /dev/null
+++ b/src/cmd/hgpatch/main.go
@@ -0,0 +1,358 @@
+// Copyright 2009 The Go 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 (
+ "bytes"
+ "container/vector"
+ "exec"
+ "flag"
+ "fmt"
+ "io/ioutil"
+ "os"
+ "patch"
+ "path/filepath"
+ "sort"
+ "strings"
+)
+
+var checkSync = flag.Bool("checksync", true, "check whether repository is out of sync")
+
+func usage() {
+ fmt.Fprintf(os.Stderr, "usage: hgpatch [options] [patchfile]\n")
+ flag.PrintDefaults()
+ os.Exit(2)
+}
+
+func main() {
+ flag.Usage = usage
+ flag.Parse()
+
+ args := flag.Args()
+ var data []byte
+ var err os.Error
+ switch len(args) {
+ case 0:
+ data, err = ioutil.ReadAll(os.Stdin)
+ case 1:
+ data, err = ioutil.ReadFile(args[0])
+ default:
+ usage()
+ }
+ chk(err)
+
+ pset, err := patch.Parse(data)
+ chk(err)
+
+ // Change to hg root directory, because
+ // patch paths are relative to root.
+ root, err := hgRoot()
+ chk(err)
+ chk(os.Chdir(root))
+
+ // Make sure there are no pending changes on the server.
+ if *checkSync && hgIncoming() {
+ fmt.Fprintf(os.Stderr, "incoming changes waiting; run hg sync first\n")
+ os.Exit(2)
+ }
+
+ // Make sure we won't be editing files with local pending changes.
+ dirtylist, err := hgModified()
+ chk(err)
+ dirty := make(map[string]bool)
+ for _, f := range dirtylist {
+ dirty[f] = true
+ }
+ conflict := make(map[string]bool)
+ for _, f := range pset.File {
+ if f.Verb == patch.Delete || f.Verb == patch.Rename {
+ if dirty[f.Src] {
+ conflict[f.Src] = true
+ }
+ }
+ if f.Verb != patch.Delete {
+ if dirty[f.Dst] {
+ conflict[f.Dst] = true
+ }
+ }
+ }
+ if len(conflict) > 0 {
+ fmt.Fprintf(os.Stderr, "cannot apply patch to locally modified files:\n")
+ for name := range conflict {
+ fmt.Fprintf(os.Stderr, "\t%s\n", name)
+ }
+ os.Exit(2)
+ }
+
+ // Apply changes in memory.
+ op, err := pset.Apply(ioutil.ReadFile)
+ chk(err)
+
+ // Write changes to disk copy: order of commands matters.
+ // Accumulate undo log as we go, in case there is an error.
+ // Also accumulate list of modified files to print at end.
+ changed := make(map[string]int)
+
+ // Copy, Rename create the destination file, so they
+ // must happen before we write the data out.
+ // A single patch may have a Copy and a Rename
+ // with the same source, so we have to run all the
+ // Copy in one pass, then all the Rename.
+ for i := range op {
+ o := &op[i]
+ if o.Verb == patch.Copy {
+ makeParent(o.Dst)
+ chk(hgCopy(o.Dst, o.Src))
+ undoRevert(o.Dst)
+ changed[o.Dst] = 1
+ }
+ }
+ for i := range op {
+ o := &op[i]
+ if o.Verb == patch.Rename {
+ makeParent(o.Dst)
+ chk(hgRename(o.Dst, o.Src))
+ undoRevert(o.Dst)
+ undoRevert(o.Src)
+ changed[o.Src] = 1
+ changed[o.Dst] = 1
+ }
+ }
+
+ // Run Delete before writing to files in case one of the
+ // deleted paths is becoming a directory.
+ for i := range op {
+ o := &op[i]
+ if o.Verb == patch.Delete {
+ chk(hgRemove(o.Src))
+ undoRevert(o.Src)
+ changed[o.Src] = 1
+ }
+ }
+
+ // Write files.
+ for i := range op {
+ o := &op[i]
+ if o.Verb == patch.Delete {
+ continue
+ }
+ if o.Verb == patch.Add {
+ makeParent(o.Dst)
+ changed[o.Dst] = 1
+ }
+ if o.Data != nil {
+ chk(ioutil.WriteFile(o.Dst, o.Data, 0644))
+ if o.Verb == patch.Add {
+ undoRm(o.Dst)
+ } else {
+ undoRevert(o.Dst)
+ }
+ changed[o.Dst] = 1
+ }
+ if o.Mode != 0 {
+ chk(os.Chmod(o.Dst, uint32(o.Mode&0755)))
+ undoRevert(o.Dst)
+ changed[o.Dst] = 1
+ }
+ }
+
+ // hg add looks at the destination file, so it must happen
+ // after we write the data out.
+ for i := range op {
+ o := &op[i]
+ if o.Verb == patch.Add {
+ chk(hgAdd(o.Dst))
+ undoRevert(o.Dst)
+ changed[o.Dst] = 1
+ }
+ }
+
+ // Finished editing files. Write the list of changed files to stdout.
+ list := make([]string, len(changed))
+ i := 0
+ for f := range changed {
+ list[i] = f
+ i++
+ }
+ sort.Strings(list)
+ for _, f := range list {
+ fmt.Printf("%s\n", f)
+ }
+}
+
+// make parent directory for name, if necessary
+func makeParent(name string) {
+ parent, _ := filepath.Split(name)
+ chk(mkdirAll(parent, 0755))
+}
+
+// Copy of os.MkdirAll but adds to undo log after
+// creating a directory.
+func mkdirAll(path string, perm uint32) os.Error {
+ dir, err := os.Lstat(path)
+ if err == nil {
+ if dir.IsDirectory() {
+ return nil
+ }
+ return &os.PathError{"mkdir", path, os.ENOTDIR}
+ }
+
+ i := len(path)
+ for i > 0 && path[i-1] == '/' { // Skip trailing slashes.
+ i--
+ }
+
+ j := i
+ for j > 0 && path[j-1] != '/' { // Scan backward over element.
+ j--
+ }
+
+ if j > 0 {
+ err = mkdirAll(path[0:j-1], perm)
+ if err != nil {
+ return err
+ }
+ }
+
+ err = os.Mkdir(path, perm)
+ if err != nil {
+ // Handle arguments like "foo/." by
+ // double-checking that directory doesn't exist.
+ dir, err1 := os.Lstat(path)
+ if err1 == nil && dir.IsDirectory() {
+ return nil
+ }
+ return err
+ }
+ undoRm(path)
+ return nil
+}
+
+// If err != nil, process the undo log and exit.
+func chk(err os.Error) {
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "%s\n", err)
+ runUndo()
+ os.Exit(2)
+ }
+}
+
+// Undo log
+type undo func() os.Error
+
+var undoLog vector.Vector // vector of undo
+
+func undoRevert(name string) { undoLog.Push(undo(func() os.Error { return hgRevert(name) })) }
+
+func undoRm(name string) { undoLog.Push(undo(func() os.Error { return os.Remove(name) })) }
+
+func runUndo() {
+ for i := undoLog.Len() - 1; i >= 0; i-- {
+ if err := undoLog.At(i).(undo)(); err != nil {
+ fmt.Fprintf(os.Stderr, "%s\n", err)
+ }
+ }
+}
+
+// hgRoot returns the root directory of the repository.
+func hgRoot() (string, os.Error) {
+ out, err := run([]string{"hg", "root"}, nil)
+ if err != nil {
+ return "", err
+ }
+ return strings.TrimSpace(out), nil
+}
+
+// hgIncoming returns true if hg sync will pull in changes.
+func hgIncoming() bool {
+ // hg -q incoming exits 0 when there is nothing incoming, 1 otherwise.
+ _, err := run([]string{"hg", "-q", "incoming"}, nil)
+ return err == nil
+}
+
+// hgModified returns a list of the modified files in the
+// repository.
+func hgModified() ([]string, os.Error) {
+ out, err := run([]string{"hg", "status", "-n"}, nil)
+ if err != nil {
+ return nil, err
+ }
+ return strings.Split(strings.TrimSpace(out), "\n"), nil
+}
+
+// hgAdd adds name to the repository.
+func hgAdd(name string) os.Error {
+ _, err := run([]string{"hg", "add", name}, nil)
+ return err
+}
+
+// hgRemove removes name from the repository.
+func hgRemove(name string) os.Error {
+ _, err := run([]string{"hg", "rm", name}, nil)
+ return err
+}
+
+// hgRevert reverts name.
+func hgRevert(name string) os.Error {
+ _, err := run([]string{"hg", "revert", name}, nil)
+ return err
+}
+
+// hgCopy copies src to dst in the repository.
+// Note that the argument order matches io.Copy, not "hg cp".
+func hgCopy(dst, src string) os.Error {
+ _, err := run([]string{"hg", "cp", src, dst}, nil)
+ return err
+}
+
+// hgRename renames src to dst in the repository.
+// Note that the argument order matches io.Copy, not "hg mv".
+func hgRename(dst, src string) os.Error {
+ _, err := run([]string{"hg", "mv", src, dst}, nil)
+ return err
+}
+
+func dup(a []string) []string {
+ b := make([]string, len(a))
+ copy(b, a)
+ return b
+}
+
+var lookPathCache = make(map[string]string)
+
+// run runs the command argv, resolving argv[0] if necessary by searching $PATH.
+// It provides input on standard input to the command.
+func run(argv []string, input []byte) (out string, err os.Error) {
+ if len(argv) < 1 {
+ return "", &runError{dup(argv), os.EINVAL}
+ }
+
+ prog, ok := lookPathCache[argv[0]]
+ if !ok {
+ prog, err = exec.LookPath(argv[0])
+ if err != nil {
+ return "", &runError{dup(argv), err}
+ }
+ lookPathCache[argv[0]] = prog
+ }
+
+ cmd := exec.Command(prog, argv[1:]...)
+ if len(input) > 0 {
+ cmd.Stdin = bytes.NewBuffer(input)
+ }
+ bs, err := cmd.CombinedOutput()
+ if err != nil {
+ return "", &runError{dup(argv), err}
+ }
+ return string(bs), nil
+}
+
+// A runError represents an error that occurred while running a command.
+type runError struct {
+ cmd []string
+ err os.Error
+}
+
+func (e *runError) String() string { return strings.Join(e.cmd, " ") + ": " + e.err.String() }