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 )