gopkg.in/zumata/godep.v14@v14.0.0-20151008182512-99082d62f381/save.go (about)

     1  package main
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"errors"
     7  	"io"
     8  	"io/ioutil"
     9  	"log"
    10  	"os"
    11  	"path/filepath"
    12  	"regexp"
    13  	"strings"
    14  
    15  	"github.com/tools/godep/Godeps/_workspace/src/github.com/kr/fs"
    16  )
    17  
    18  var cmdSave = &Command{
    19  	Usage: "save [-r] [-v] [-t] [packages]",
    20  	Short: "list and copy dependencies into Godeps",
    21  	Long: `
    22  
    23  Save writes a list of the named packages and their dependencies along
    24  with the exact source control revision of each package, and copies
    25  their source code into a subdirectory. Packages inside "." are excluded
    26  from the list to be copied.
    27  
    28  The list is written to Godeps/Godeps.json, and source code for all
    29  dependencies is copied into either Godeps/_workspace or, if the vendor
    30  experiment is turned on, vendor/.
    31  
    32  The dependency list is a JSON document with the following structure:
    33  
    34  	type Godeps struct {
    35  		ImportPath string
    36  		GoVersion  string   // Abridged output of 'go version'.
    37  		Packages   []string // Arguments to godep save, if any.
    38  		Deps       []struct {
    39  			ImportPath string
    40  			Comment    string // Tag or description of commit.
    41  			Rev        string // VCS-specific commit ID.
    42  		}
    43  	}
    44  
    45  Any packages already present in the list will be left unchanged.
    46  To update a dependency to a newer revision, use 'godep update'.
    47  
    48  If -r is given, import statements will be rewritten to refer
    49  directly to the copied source code. This is not compatible with the
    50  vendor experiment.
    51  
    52  If -v is given, verbose output is enabled.
    53  
    54  If -t is given, test files (*_test.go files + testdata directories) are
    55  also saved.
    56  
    57  For more about specifying packages, see 'go help packages'.
    58  `,
    59  	Run: runSave,
    60  }
    61  
    62  var (
    63  	saveR, saveT bool
    64  )
    65  
    66  func init() {
    67  	cmdSave.Flag.BoolVar(&verbose, "v", false, "enable verbose output")
    68  	cmdSave.Flag.BoolVar(&saveR, "r", false, "rewrite import paths")
    69  	cmdSave.Flag.BoolVar(&saveT, "t", false, "save test files")
    70  }
    71  
    72  func runSave(cmd *Command, args []string) {
    73  	if VendorExperiment && saveR {
    74  		log.Println("flag -r is incompatible with the vendoring experiment")
    75  		cmd.UsageExit()
    76  	}
    77  	err := save(args)
    78  	if err != nil {
    79  		log.Fatalln(err)
    80  	}
    81  }
    82  
    83  func dotPackage() (*Package, error) {
    84  	p, err := LoadPackages(".")
    85  	if err != nil {
    86  		return nil, err
    87  	}
    88  	if len(p) > 1 {
    89  		panic("Impossible number of packages")
    90  	}
    91  	return p[0], nil
    92  }
    93  
    94  func save(pkgs []string) error {
    95  	dot, err := dotPackage()
    96  	if err != nil {
    97  		return err
    98  	}
    99  	ver, err := goVersion()
   100  	if err != nil {
   101  		return err
   102  	}
   103  
   104  	gold, err := loadDefaultGodepsFile()
   105  	if err != nil {
   106  		if !os.IsNotExist(err) {
   107  			return err
   108  		}
   109  	}
   110  
   111  	gnew := &Godeps{
   112  		ImportPath: dot.ImportPath,
   113  		GoVersion:  ver,
   114  	}
   115  
   116  	switch len(pkgs) {
   117  	case 0:
   118  		pkgs = []string{"."}
   119  	default:
   120  		gnew.Packages = pkgs
   121  	}
   122  
   123  	a, err := LoadPackages(pkgs...)
   124  	if err != nil {
   125  		return err
   126  	}
   127  	err = gnew.fill(a, dot.ImportPath)
   128  	if err != nil {
   129  		return err
   130  	}
   131  	if gnew.Deps == nil {
   132  		gnew.Deps = make([]Dependency, 0) // produce json [], not null
   133  	}
   134  	gdisk := gnew.copy()
   135  	err = carryVersions(&gold, gnew)
   136  	if err != nil {
   137  		return err
   138  	}
   139  	if gold.isOldFile {
   140  		// If we are migrating from an old format file,
   141  		// we require that the listed version of every
   142  		// dependency must be installed in GOPATH, so it's
   143  		// available to copy.
   144  		if !eqDeps(gnew.Deps, gdisk.Deps) {
   145  			return errors.New(strings.TrimSpace(needRestore))
   146  		}
   147  		gold = Godeps{}
   148  	}
   149  	os.Remove("Godeps") // remove regular file if present; ignore error
   150  	readme := filepath.Join("Godeps", "Readme")
   151  	err = writeFile(readme, strings.TrimSpace(Readme)+"\n")
   152  	if err != nil {
   153  		log.Println(err)
   154  	}
   155  	_, err = gnew.save()
   156  	if err != nil {
   157  		return err
   158  	}
   159  	// We use a name starting with "_" so the go tool
   160  	// ignores this directory when traversing packages
   161  	// starting at the project's root. For example,
   162  	//   godep go list ./...
   163  	srcdir := filepath.FromSlash(strings.Trim(sep, "/"))
   164  	rem := subDeps(gold.Deps, gnew.Deps)
   165  	add := subDeps(gnew.Deps, gold.Deps)
   166  	err = removeSrc(srcdir, rem)
   167  	if err != nil {
   168  		return err
   169  	}
   170  	err = copySrc(srcdir, add)
   171  	if err != nil {
   172  		return err
   173  	}
   174  	if !VendorExperiment {
   175  		f, _ := filepath.Split(srcdir)
   176  		writeVCSIgnore(f)
   177  	}
   178  	var rewritePaths []string
   179  	if saveR {
   180  		for _, dep := range gnew.Deps {
   181  			rewritePaths = append(rewritePaths, dep.ImportPath)
   182  		}
   183  	}
   184  	return rewrite(a, dot.ImportPath, rewritePaths)
   185  }
   186  
   187  type revError struct {
   188  	ImportPath string
   189  	HaveRev    string
   190  	WantRev    string
   191  }
   192  
   193  func (v *revError) Error() string {
   194  	return v.ImportPath + ": revision is " + v.HaveRev + ", want " + v.WantRev
   195  }
   196  
   197  // carryVersions copies Rev and Comment from a to b for
   198  // each dependency with an identical ImportPath. For any
   199  // dependency in b that appears to be from the same repo
   200  // as one in a (for example, a parent or child directory),
   201  // the Rev must already match - otherwise it is an error.
   202  func carryVersions(a, b *Godeps) error {
   203  	for i := range b.Deps {
   204  		err := carryVersion(a, &b.Deps[i])
   205  		if err != nil {
   206  			return err
   207  		}
   208  	}
   209  	return nil
   210  }
   211  
   212  func carryVersion(a *Godeps, db *Dependency) error {
   213  	// First see if this exact package is already in the list.
   214  	for _, da := range a.Deps {
   215  		if db.ImportPath == da.ImportPath {
   216  			db.Rev = da.Rev
   217  			db.Comment = da.Comment
   218  			return nil
   219  		}
   220  	}
   221  	// No exact match, check for child or sibling package.
   222  	// We can't handle mismatched versions for packages in
   223  	// the same repo, so report that as an error.
   224  	for _, da := range a.Deps {
   225  		switch {
   226  		case strings.HasPrefix(db.ImportPath, da.ImportPath+"/"):
   227  			if da.Rev != db.Rev {
   228  				return &revError{db.ImportPath, db.Rev, da.Rev}
   229  			}
   230  		case strings.HasPrefix(da.ImportPath, db.root+"/"):
   231  			if da.Rev != db.Rev {
   232  				return &revError{db.ImportPath, db.Rev, da.Rev}
   233  			}
   234  		}
   235  	}
   236  	// No related package in the list, must be a new repo.
   237  	return nil
   238  }
   239  
   240  // subDeps returns a - b, using ImportPath for equality.
   241  func subDeps(a, b []Dependency) (diff []Dependency) {
   242  Diff:
   243  	for _, da := range a {
   244  		for _, db := range b {
   245  			if da.ImportPath == db.ImportPath {
   246  				continue Diff
   247  			}
   248  		}
   249  		diff = append(diff, da)
   250  	}
   251  	return diff
   252  }
   253  
   254  func removeSrc(srcdir string, deps []Dependency) error {
   255  	for _, dep := range deps {
   256  		path := filepath.FromSlash(dep.ImportPath)
   257  		err := os.RemoveAll(filepath.Join(srcdir, path))
   258  		if err != nil {
   259  			return err
   260  		}
   261  	}
   262  	return nil
   263  }
   264  
   265  func copySrc(dir string, deps []Dependency) error {
   266  	ok := true
   267  	for _, dep := range deps {
   268  		srcdir := filepath.Join(dep.ws, "src")
   269  		rel, err := filepath.Rel(srcdir, dep.dir)
   270  		if err != nil { // this should never happen
   271  			return err
   272  		}
   273  		dstpkgroot := filepath.Join(dir, rel)
   274  		err = os.RemoveAll(dstpkgroot)
   275  		if err != nil {
   276  			log.Println(err)
   277  			ok = false
   278  		}
   279  		vf := dep.vcs.listFiles(dep.dir)
   280  		w := fs.Walk(dep.dir)
   281  		for w.Step() {
   282  			err = copyPkgFile(vf, dir, srcdir, w)
   283  			if err != nil {
   284  				log.Println(err)
   285  				ok = false
   286  			}
   287  		}
   288  	}
   289  	if !ok {
   290  		return errorCopyingSourceCode
   291  	}
   292  	return nil
   293  }
   294  
   295  func copyPkgFile(vf vcsFiles, dstroot, srcroot string, w *fs.Walker) error {
   296  	if w.Err() != nil {
   297  		return w.Err()
   298  	}
   299  	name := w.Stat().Name()
   300  	if w.Stat().IsDir() {
   301  		if name[0] == '.' || name[0] == '_' || (!saveT && name == "testdata") {
   302  			// Skip directories starting with '.' or '_' or
   303  			// 'testdata' (last is only skipped if saveT is false)
   304  			w.SkipDir()
   305  		}
   306  		return nil
   307  	}
   308  	rel, err := filepath.Rel(srcroot, w.Path())
   309  	if err != nil { // this should never happen
   310  		return err
   311  	}
   312  	if !saveT && strings.HasSuffix(name, "_test.go") {
   313  		if verbose {
   314  			log.Printf("save: skipping test file: %s", w.Path())
   315  		}
   316  		return nil
   317  	}
   318  	if !vf.Contains(w.Path()) {
   319  		if verbose {
   320  			log.Printf("save: skipping untracked file: %s", w.Path())
   321  		}
   322  		return nil
   323  	}
   324  	return copyFile(filepath.Join(dstroot, rel), w.Path())
   325  }
   326  
   327  // copyFile copies a regular file from src to dst.
   328  // dst is opened with os.Create.
   329  // If the file name ends with .go,
   330  // copyFile strips canonical import path annotations.
   331  // These are comments of the form:
   332  //   package foo // import "bar/foo"
   333  //   package foo /* import "bar/foo" */
   334  func copyFile(dst, src string) error {
   335  	err := os.MkdirAll(filepath.Dir(dst), 0777)
   336  	if err != nil {
   337  		return err
   338  	}
   339  
   340  	linkDst, err := os.Readlink(src)
   341  	if err == nil {
   342  		return os.Symlink(linkDst, dst)
   343  	}
   344  
   345  	r, err := os.Open(src)
   346  	if err != nil {
   347  		return err
   348  	}
   349  	defer r.Close()
   350  
   351  	w, err := os.Create(dst)
   352  	if err != nil {
   353  		return err
   354  	}
   355  
   356  	if strings.HasSuffix(dst, ".go") {
   357  		err = copyWithoutImportComment(w, r)
   358  	} else {
   359  		_, err = io.Copy(w, r)
   360  	}
   361  	err1 := w.Close()
   362  	if err == nil {
   363  		err = err1
   364  	}
   365  
   366  	return err
   367  }
   368  
   369  func copyWithoutImportComment(w io.Writer, r io.Reader) error {
   370  	b := bufio.NewReader(r)
   371  	for {
   372  		l, err := b.ReadBytes('\n')
   373  		eof := err == io.EOF
   374  		if err != nil && err != io.EOF {
   375  			return err
   376  		}
   377  
   378  		// If we have data then write it out...
   379  		if len(l) > 0 {
   380  			// Strip off \n if it exists because stripImportComment
   381  			_, err := w.Write(append(stripImportComment(bytes.TrimRight(l, "\n")), '\n'))
   382  			if err != nil {
   383  				return err
   384  			}
   385  		}
   386  
   387  		if eof {
   388  			return nil
   389  		}
   390  	}
   391  }
   392  
   393  const (
   394  	importAnnotation = `import\s+(?:"[^"]*"|` + "`[^`]*`" + `)`
   395  	importComment    = `(?://\s*` + importAnnotation + `\s*$|/\*\s*` + importAnnotation + `\s*\*/)`
   396  )
   397  
   398  var (
   399  	importCommentRE = regexp.MustCompile(`^\s*(package\s+\w+)\s+` + importComment + `(.*)`)
   400  	pkgPrefix       = []byte("package ")
   401  )
   402  
   403  // stripImportComment returns line with its import comment removed.
   404  // If s is not a package statement containing an import comment,
   405  // it is returned unaltered.
   406  // FIXME: expects lines w/o a \n at the end
   407  // See also http://golang.org/s/go14customimport.
   408  func stripImportComment(line []byte) []byte {
   409  	if !bytes.HasPrefix(line, pkgPrefix) {
   410  		// Fast path; this will skip all but one line in the file.
   411  		// This assumes there is no whitespace before the keyword.
   412  		return line
   413  	}
   414  	if m := importCommentRE.FindSubmatch(line); m != nil {
   415  		return append(m[1], m[2]...)
   416  	}
   417  	return line
   418  }
   419  
   420  // Func writeVCSIgnore writes "ignore" files inside dir for known VCSs,
   421  // so that dir/pkg and dir/bin don't accidentally get committed.
   422  // It logs any errors it encounters.
   423  func writeVCSIgnore(dir string) {
   424  	// Currently git is the only VCS for which we know how to do this.
   425  	// Mercurial and Bazaar have similar mechanisms, but they apparently
   426  	// require writing files outside of dir.
   427  	const ignore = "/pkg\n/bin\n"
   428  	name := filepath.Join(dir, ".gitignore")
   429  	err := writeFile(name, ignore)
   430  	if err != nil {
   431  		log.Println(err)
   432  	}
   433  }
   434  
   435  // writeFile is like ioutil.WriteFile but it creates
   436  // intermediate directories with os.MkdirAll.
   437  func writeFile(name, body string) error {
   438  	err := os.MkdirAll(filepath.Dir(name), 0777)
   439  	if err != nil {
   440  		return err
   441  	}
   442  	return ioutil.WriteFile(name, []byte(body), 0666)
   443  }
   444  
   445  const (
   446  	// Readme contains the README text.
   447  	Readme = `
   448  This directory tree is generated automatically by godep.
   449  
   450  Please do not edit.
   451  
   452  See https://github.com/tools/godep for more information.
   453  `
   454  	needRestore = `
   455  mismatched versions while migrating
   456  
   457  It looks like you are switching from the old Godeps format
   458  (from flag -copy=false). The old format is just a file; it
   459  doesn't contain source code. For this migration, godep needs
   460  the appropriate version of each dependency to be installed in
   461  GOPATH, so that the source code is available to copy.
   462  
   463  To fix this, run 'godep restore'.
   464  `
   465  )