--- /dev/null
+// Copyright 2015 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.
+
+// This file contains the implementation of the 'gomvpkg' command
+// whose main function is in golang.org/x/tools/cmd/gomvpkg.
+
+package rename
+
+// TODO(matloob):
+// - think about what happens if the package is moving across version control systems.
+// - dot imports are not supported. Make sure it's clearly documented.
+
+import (
+ "bytes"
+ "fmt"
+ "go/ast"
+ "go/build"
+ "go/format"
+ "go/token"
+ exec "golang.org/x/sys/execabs"
+ "log"
+ "os"
+ "path"
+ "path/filepath"
+ "regexp"
+ "runtime"
+ "strconv"
+ "strings"
+ "text/template"
+
+ "golang.org/x/tools/go/buildutil"
+ "golang.org/x/tools/go/loader"
+ "golang.org/x/tools/refactor/importgraph"
+)
+
+// Move, given a package path and a destination package path, will try
+// to move the given package to the new path. The Move function will
+// first check for any conflicts preventing the move, such as a
+// package already existing at the destination package path. If the
+// move can proceed, it builds an import graph to find all imports of
+// the packages whose paths need to be renamed. This includes uses of
+// the subpackages of the package to be moved as those packages will
+// also need to be moved. It then renames all imports to point to the
+// new paths, and then moves the packages to their new paths.
+func Move(ctxt *build.Context, from, to, moveTmpl string) error {
+ srcDir, err := srcDir(ctxt, from)
+ if err != nil {
+ return err
+ }
+
+ // This should be the only place in the program that constructs
+ // file paths.
+ fromDir := buildutil.JoinPath(ctxt, srcDir, filepath.FromSlash(from))
+ toDir := buildutil.JoinPath(ctxt, srcDir, filepath.FromSlash(to))
+ toParent := filepath.Dir(toDir)
+ if !buildutil.IsDir(ctxt, toParent) {
+ return fmt.Errorf("parent directory does not exist for path %s", toDir)
+ }
+
+ // Build the import graph and figure out which packages to update.
+ _, rev, errors := importgraph.Build(ctxt)
+ if len(errors) > 0 {
+ // With a large GOPATH tree, errors are inevitable.
+ // Report them but proceed.
+ fmt.Fprintf(os.Stderr, "While scanning Go workspace:\n")
+ for path, err := range errors {
+ fmt.Fprintf(os.Stderr, "Package %q: %s.\n", path, err)
+ }
+ }
+
+ // Determine the affected packages---the set of packages whose import
+ // statements need updating.
+ affectedPackages := map[string]bool{from: true}
+ destinations := make(map[string]string) // maps old import path to new import path
+ for pkg := range subpackages(ctxt, srcDir, from) {
+ for r := range rev[pkg] {
+ affectedPackages[r] = true
+ }
+ destinations[pkg] = strings.Replace(pkg, from, to, 1)
+ }
+
+ // Load all the affected packages.
+ iprog, err := loadProgram(ctxt, affectedPackages)
+ if err != nil {
+ return err
+ }
+
+ // Prepare the move command, if one was supplied.
+ var cmd string
+ if moveTmpl != "" {
+ if cmd, err = moveCmd(moveTmpl, fromDir, toDir); err != nil {
+ return err
+ }
+ }
+
+ m := mover{
+ ctxt: ctxt,
+ rev: rev,
+ iprog: iprog,
+ from: from,
+ to: to,
+ fromDir: fromDir,
+ toDir: toDir,
+ affectedPackages: affectedPackages,
+ destinations: destinations,
+ cmd: cmd,
+ }
+
+ if err := m.checkValid(); err != nil {
+ return err
+ }
+
+ m.move()
+
+ return nil
+}
+
+// srcDir returns the absolute path of the srcdir containing pkg.
+func srcDir(ctxt *build.Context, pkg string) (string, error) {
+ for _, srcDir := range ctxt.SrcDirs() {
+ path := buildutil.JoinPath(ctxt, srcDir, pkg)
+ if buildutil.IsDir(ctxt, path) {
+ return srcDir, nil
+ }
+ }
+ return "", fmt.Errorf("src dir not found for package: %s", pkg)
+}
+
+// subpackages returns the set of packages in the given srcDir whose
+// import path equals to root, or has "root/" as the prefix.
+func subpackages(ctxt *build.Context, srcDir string, root string) map[string]bool {
+ var subs = make(map[string]bool)
+ buildutil.ForEachPackage(ctxt, func(pkg string, err error) {
+ if err != nil {
+ log.Fatalf("unexpected error in ForEachPackage: %v", err)
+ }
+
+ // Only process the package root, or a sub-package of it.
+ if !(strings.HasPrefix(pkg, root) &&
+ (len(pkg) == len(root) || pkg[len(root)] == '/')) {
+ return
+ }
+
+ p, err := ctxt.Import(pkg, "", build.FindOnly)
+ if err != nil {
+ log.Fatalf("unexpected: package %s can not be located by build context: %s", pkg, err)
+ }
+ if p.SrcRoot == "" {
+ log.Fatalf("unexpected: could not determine srcDir for package %s: %s", pkg, err)
+ }
+ if p.SrcRoot != srcDir {
+ return
+ }
+
+ subs[pkg] = true
+ })
+ return subs
+}
+
+type mover struct {
+ // iprog contains all packages whose contents need to be updated
+ // with new package names or import paths.
+ iprog *loader.Program
+ ctxt *build.Context
+ // rev is the reverse import graph.
+ rev importgraph.Graph
+ // from and to are the source and destination import
+ // paths. fromDir and toDir are the source and destination
+ // absolute paths that package source files will be moved between.
+ from, to, fromDir, toDir string
+ // affectedPackages is the set of all packages whose contents need
+ // to be updated to reflect new package names or import paths.
+ affectedPackages map[string]bool
+ // destinations maps each subpackage to be moved to its
+ // destination path.
+ destinations map[string]string
+ // cmd, if not empty, will be executed to move fromDir to toDir.
+ cmd string
+}
+
+func (m *mover) checkValid() error {
+ const prefix = "invalid move destination"
+
+ match, err := regexp.MatchString("^[_\\pL][_\\pL\\p{Nd}]*$", path.Base(m.to))
+ if err != nil {
+ panic("regexp.MatchString failed")
+ }
+ if !match {
+ return fmt.Errorf("%s: %s; gomvpkg does not support move destinations "+
+ "whose base names are not valid go identifiers", prefix, m.to)
+ }
+
+ if buildutil.FileExists(m.ctxt, m.toDir) {
+ return fmt.Errorf("%s: %s conflicts with file %s", prefix, m.to, m.toDir)
+ }
+ if buildutil.IsDir(m.ctxt, m.toDir) {
+ return fmt.Errorf("%s: %s conflicts with directory %s", prefix, m.to, m.toDir)
+ }
+
+ for _, toSubPkg := range m.destinations {
+ if _, err := m.ctxt.Import(toSubPkg, "", build.FindOnly); err == nil {
+ return fmt.Errorf("%s: %s; package or subpackage %s already exists",
+ prefix, m.to, toSubPkg)
+ }
+ }
+
+ return nil
+}
+
+// moveCmd produces the version control move command used to move fromDir to toDir by
+// executing the given template.
+func moveCmd(moveTmpl, fromDir, toDir string) (string, error) {
+ tmpl, err := template.New("movecmd").Parse(moveTmpl)
+ if err != nil {
+ return "", err
+ }
+
+ var buf bytes.Buffer
+ err = tmpl.Execute(&buf, struct {
+ Src string
+ Dst string
+ }{fromDir, toDir})
+ return buf.String(), err
+}
+
+func (m *mover) move() error {
+ filesToUpdate := make(map[*ast.File]bool)
+
+ // Change the moved package's "package" declaration to its new base name.
+ pkg, ok := m.iprog.Imported[m.from]
+ if !ok {
+ log.Fatalf("unexpected: package %s is not in import map", m.from)
+ }
+ newName := filepath.Base(m.to)
+ for _, f := range pkg.Files {
+ // Update all import comments.
+ for _, cg := range f.Comments {
+ c := cg.List[0]
+ if c.Slash >= f.Name.End() &&
+ sameLine(m.iprog.Fset, c.Slash, f.Name.End()) &&
+ (f.Decls == nil || c.Slash < f.Decls[0].Pos()) {
+ if strings.HasPrefix(c.Text, `// import "`) {
+ c.Text = `// import "` + m.to + `"`
+ break
+ }
+ if strings.HasPrefix(c.Text, `/* import "`) {
+ c.Text = `/* import "` + m.to + `" */`
+ break
+ }
+ }
+ }
+ f.Name.Name = newName // change package decl
+ filesToUpdate[f] = true
+ }
+
+ // Look through the external test packages (m.iprog.Created contains the external test packages).
+ for _, info := range m.iprog.Created {
+ // Change the "package" declaration of the external test package.
+ if info.Pkg.Path() == m.from+"_test" {
+ for _, f := range info.Files {
+ f.Name.Name = newName + "_test" // change package decl
+ filesToUpdate[f] = true
+ }
+ }
+
+ // Mark all the loaded external test packages, which import the "from" package,
+ // as affected packages and update the imports.
+ for _, imp := range info.Pkg.Imports() {
+ if imp.Path() == m.from {
+ m.affectedPackages[info.Pkg.Path()] = true
+ m.iprog.Imported[info.Pkg.Path()] = info
+ if err := importName(m.iprog, info, m.from, path.Base(m.from), newName); err != nil {
+ return err
+ }
+ }
+ }
+ }
+
+ // Update imports of that package to use the new import name.
+ // None of the subpackages will change their name---only the from package
+ // itself will.
+ for p := range m.rev[m.from] {
+ if err := importName(m.iprog, m.iprog.Imported[p], m.from, path.Base(m.from), newName); err != nil {
+ return err
+ }
+ }
+
+ // Update import paths for all imports by affected packages.
+ for ap := range m.affectedPackages {
+ info, ok := m.iprog.Imported[ap]
+ if !ok {
+ log.Fatalf("unexpected: package %s is not in import map", ap)
+ }
+ for _, f := range info.Files {
+ for _, imp := range f.Imports {
+ importPath, _ := strconv.Unquote(imp.Path.Value)
+ if newPath, ok := m.destinations[importPath]; ok {
+ imp.Path.Value = strconv.Quote(newPath)
+
+ oldName := path.Base(importPath)
+ if imp.Name != nil {
+ oldName = imp.Name.Name
+ }
+
+ newName := path.Base(newPath)
+ if imp.Name == nil && oldName != newName {
+ imp.Name = ast.NewIdent(oldName)
+ } else if imp.Name == nil || imp.Name.Name == newName {
+ imp.Name = nil
+ }
+ filesToUpdate[f] = true
+ }
+ }
+ }
+ }
+
+ for f := range filesToUpdate {
+ var buf bytes.Buffer
+ if err := format.Node(&buf, m.iprog.Fset, f); err != nil {
+ log.Printf("failed to pretty-print syntax tree: %v", err)
+ continue
+ }
+ tokenFile := m.iprog.Fset.File(f.Pos())
+ writeFile(tokenFile.Name(), buf.Bytes())
+ }
+
+ // Move the directories.
+ // If either the fromDir or toDir are contained under version control it is
+ // the user's responsibility to provide a custom move command that updates
+ // version control to reflect the move.
+ // TODO(matloob): If the parent directory of toDir does not exist, create it.
+ // For now, it's required that it does exist.
+
+ if m.cmd != "" {
+ // TODO(matloob): Verify that the windows and plan9 cases are correct.
+ var cmd *exec.Cmd
+ switch runtime.GOOS {
+ case "windows":
+ cmd = exec.Command("cmd", "/c", m.cmd)
+ case "plan9":
+ cmd = exec.Command("rc", "-c", m.cmd)
+ default:
+ cmd = exec.Command("sh", "-c", m.cmd)
+ }
+ cmd.Stderr = os.Stderr
+ cmd.Stdout = os.Stdout
+ if err := cmd.Run(); err != nil {
+ return fmt.Errorf("version control system's move command failed: %v", err)
+ }
+
+ return nil
+ }
+
+ return moveDirectory(m.fromDir, m.toDir)
+}
+
+// sameLine reports whether two positions in the same file are on the same line.
+func sameLine(fset *token.FileSet, x, y token.Pos) bool {
+ return fset.Position(x).Line == fset.Position(y).Line
+}
+
+var moveDirectory = func(from, to string) error {
+ return os.Rename(from, to)
+}