gopkg.in/tools/godep.v19@v19.0.0-20151103222550-d423d08236e8/save.go (about)

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