github.com/vanadium-archive/go.jiri@v0.0.0-20160715023856-abfb8b131290/cmd/jiri/snapshot.go (about) 1 // Copyright 2015 The Vanadium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package main 6 7 import ( 8 "fmt" 9 "io/ioutil" 10 "os" 11 "path/filepath" 12 "sort" 13 "strings" 14 "time" 15 16 "v.io/jiri" 17 "v.io/jiri/collect" 18 "v.io/jiri/gitutil" 19 "v.io/jiri/project" 20 "v.io/jiri/runutil" 21 "v.io/x/lib/cmdline" 22 ) 23 24 const ( 25 defaultSnapshotDir = ".snapshot" 26 ) 27 28 var ( 29 pushRemoteFlag bool 30 snapshotDirFlag string 31 snapshotGcFlag bool 32 timeFormatFlag string 33 ) 34 35 func init() { 36 cmdSnapshot.Flags.StringVar(&snapshotDirFlag, "dir", "", "Directory where snapshot are stored. Defaults to $JIRI_ROOT/.snapshot.") 37 cmdSnapshotCheckout.Flags.BoolVar(&snapshotGcFlag, "gc", false, "Garbage collect obsolete repositories.") 38 cmdSnapshotCreate.Flags.BoolVar(&pushRemoteFlag, "push-remote", false, "Commit and push snapshot upstream.") 39 cmdSnapshotCreate.Flags.StringVar(&timeFormatFlag, "time-format", time.RFC3339, "Time format for snapshot file name.") 40 } 41 42 var cmdSnapshot = &cmdline.Command{ 43 Name: "snapshot", 44 Short: "Manage project snapshots", 45 Long: ` 46 The "jiri snapshot" command can be used to manage project snapshots. 47 In particular, it can be used to create new snapshots and to list 48 existing snapshots. 49 `, 50 Children: []*cmdline.Command{cmdSnapshotCheckout, cmdSnapshotCreate, cmdSnapshotList}, 51 } 52 53 // cmdSnapshotCreate represents the "jiri snapshot create" command. 54 var cmdSnapshotCreate = &cmdline.Command{ 55 Runner: jiri.RunnerFunc(runSnapshotCreate), 56 Name: "create", 57 Short: "Create a new project snapshot", 58 Long: ` 59 The "jiri snapshot create <label>" command captures the current project state 60 in a manifest. If the -push-remote flag is provided, the snapshot is committed 61 and pushed upstream. 62 63 Internally, snapshots are organized as follows: 64 65 <snapshot-dir>/ 66 labels/ 67 <label1>/ 68 <label1-snapshot1> 69 <label1-snapshot2> 70 ... 71 <label2>/ 72 <label2-snapshot1> 73 <label2-snapshot2> 74 ... 75 <label3>/ 76 ... 77 <label1> # a symlink to the latest <label1-snapshot*> 78 <label2> # a symlink to the latest <label2-snapshot*> 79 ... 80 81 NOTE: Unlike the jiri tool commands, the above internal organization 82 is not an API. It is an implementation and can change without notice. 83 `, 84 ArgsName: "<label>", 85 ArgsLong: "<label> is the snapshot label.", 86 } 87 88 func runSnapshotCreate(jirix *jiri.X, args []string) error { 89 if len(args) != 1 { 90 return jirix.UsageErrorf("unexpected number of arguments") 91 } 92 label := args[0] 93 snapshotDir, err := getSnapshotDir(jirix) 94 if err != nil { 95 return err 96 } 97 snapshotFile := filepath.Join(snapshotDir, "labels", label, time.Now().Format(timeFormatFlag)) 98 99 if !pushRemoteFlag { 100 // No git operations necessary. Just create the snapshot file. 101 return createSnapshot(jirix, snapshotDir, snapshotFile, label) 102 } 103 104 // Attempt to create a snapshot on a clean master branch. If snapshot 105 // creation fails, return to the state we were in before. 106 createFn := func() error { 107 git := gitutil.New(jirix.NewSeq()) 108 revision, err := git.CurrentRevision() 109 if err != nil { 110 return err 111 } 112 if err := createSnapshot(jirix, snapshotDir, snapshotFile, label); err != nil { 113 git.Reset(revision) 114 git.RemoveUntrackedFiles() 115 return err 116 } 117 return commitAndPushChanges(jirix, snapshotDir, snapshotFile, label) 118 } 119 120 // Execute the above function in the snapshot directory on a clean master branch. 121 p := project.Project{ 122 Path: snapshotDir, 123 Protocol: "git", 124 RemoteBranch: "master", 125 Revision: "HEAD", 126 } 127 return project.ApplyToLocalMaster(jirix, project.Projects{p.Key(): p}, createFn) 128 } 129 130 // getSnapshotDir returns the path to the snapshot directory, creating it if 131 // necessary. 132 func getSnapshotDir(jirix *jiri.X) (string, error) { 133 dir := snapshotDirFlag 134 if dir == "" { 135 dir = filepath.Join(jirix.Root, defaultSnapshotDir) 136 } 137 138 if !filepath.IsAbs(dir) { 139 cwd, err := os.Getwd() 140 if err != nil { 141 return "", err 142 } 143 dir = filepath.Join(cwd, dir) 144 } 145 146 // Make sure directory exists. 147 if err := jirix.NewSeq().MkdirAll(dir, 0755).Done(); err != nil { 148 return "", err 149 } 150 return dir, nil 151 } 152 153 func createSnapshot(jirix *jiri.X, snapshotDir, snapshotFile, label string) error { 154 // Create a snapshot that encodes the current state of master 155 // branches for all local projects. 156 if err := project.CreateSnapshot(jirix, snapshotFile, ""); err != nil { 157 return err 158 } 159 160 s := jirix.NewSeq() 161 // Update the symlink for this snapshot label to point to the 162 // latest snapshot. 163 symlink := filepath.Join(snapshotDir, label) 164 newSymlink := symlink + ".new" 165 relativeSnapshotPath := strings.TrimPrefix(snapshotFile, snapshotDir+string(os.PathSeparator)) 166 return s.RemoveAll(newSymlink). 167 Symlink(relativeSnapshotPath, newSymlink). 168 Rename(newSymlink, symlink).Done() 169 } 170 171 // commitAndPushChanges commits changes identified by the given manifest file 172 // and label to the containing repository and pushes these changes to the 173 // remote repository. 174 func commitAndPushChanges(jirix *jiri.X, snapshotDir, snapshotFile, label string) (e error) { 175 cwd, err := os.Getwd() 176 if err != nil { 177 return err 178 } 179 defer collect.Error(func() error { return jirix.NewSeq().Chdir(cwd).Done() }, &e) 180 if err := jirix.NewSeq().Chdir(snapshotDir).Done(); err != nil { 181 return err 182 } 183 relativeSnapshotPath := strings.TrimPrefix(snapshotFile, snapshotDir+string(os.PathSeparator)) 184 git := gitutil.New(jirix.NewSeq()) 185 // Pull from master so we are up-to-date. 186 if err := git.Pull("origin", "master"); err != nil { 187 return err 188 } 189 if err := git.Add(relativeSnapshotPath); err != nil { 190 return err 191 } 192 if err := git.Add(label); err != nil { 193 return err 194 } 195 name := strings.TrimPrefix(snapshotFile, snapshotDir) 196 if err := git.CommitNoVerify(fmt.Sprintf("adding snapshot %q for label %q", name, label)); err != nil { 197 return err 198 } 199 if err := git.Push("origin", "master", gitutil.VerifyOpt(false)); err != nil { 200 return err 201 } 202 return nil 203 } 204 205 // cmdSnapshotCheckout represents the "jiri snapshot checkout" command. 206 var cmdSnapshotCheckout = &cmdline.Command{ 207 Runner: jiri.RunnerFunc(runSnapshotCheckout), 208 Name: "checkout", 209 Short: "Checkout a project snapshot", 210 Long: ` 211 The "jiri snapshot checkout <snapshot>" command restores local project state to 212 the state in the given snapshot manifest. 213 `, 214 ArgsName: "<snapshot>", 215 ArgsLong: "<snapshot> is the snapshot manifest file.", 216 } 217 218 func runSnapshotCheckout(jirix *jiri.X, args []string) error { 219 if len(args) != 1 { 220 return jirix.UsageErrorf("unexpected number of arguments") 221 } 222 return project.CheckoutSnapshot(jirix, args[0], snapshotGcFlag) 223 } 224 225 // cmdSnapshotList represents the "jiri snapshot list" command. 226 var cmdSnapshotList = &cmdline.Command{ 227 Runner: jiri.RunnerFunc(runSnapshotList), 228 Name: "list", 229 Short: "List existing project snapshots", 230 Long: ` 231 The "snapshot list" command lists existing snapshots of the labels 232 specified as command-line arguments. If no arguments are provided, the 233 command lists snapshots for all known labels. 234 `, 235 ArgsName: "<label ...>", 236 ArgsLong: "<label ...> is a list of snapshot labels.", 237 } 238 239 func runSnapshotList(jirix *jiri.X, args []string) error { 240 snapshotDir, err := getSnapshotDir(jirix) 241 if err != nil { 242 return err 243 } 244 if len(args) == 0 { 245 // Identify all known snapshot labels, using a 246 // heuristic that looks for all symbolic links <foo> 247 // in the snapshot directory that point to a file in 248 // the "labels/<foo>" subdirectory of the snapshot 249 // directory. 250 fileInfoList, err := ioutil.ReadDir(snapshotDir) 251 if err != nil { 252 return fmt.Errorf("ReadDir(%v) failed: %v", snapshotDir, err) 253 } 254 for _, fileInfo := range fileInfoList { 255 if fileInfo.Mode()&os.ModeSymlink != 0 { 256 path := filepath.Join(snapshotDir, fileInfo.Name()) 257 dst, err := filepath.EvalSymlinks(path) 258 if err != nil { 259 return fmt.Errorf("EvalSymlinks(%v) failed: %v", path, err) 260 } 261 if strings.HasSuffix(filepath.Dir(dst), filepath.Join("labels", fileInfo.Name())) { 262 args = append(args, fileInfo.Name()) 263 } 264 } 265 } 266 } 267 268 // Check that all labels exist. 269 var notexist []string 270 for _, label := range args { 271 labelDir := filepath.Join(snapshotDir, "labels", label) 272 switch _, err := jirix.NewSeq().Stat(labelDir); { 273 case runutil.IsNotExist(err): 274 notexist = append(notexist, label) 275 case err != nil: 276 return err 277 } 278 } 279 if len(notexist) > 0 { 280 return fmt.Errorf("snapshot labels %v not found", notexist) 281 } 282 283 // Print snapshots for all labels. 284 sort.Strings(args) 285 for _, label := range args { 286 // Scan the snapshot directory "labels/<label>" printing 287 // all snapshots. 288 labelDir := filepath.Join(snapshotDir, "labels", label) 289 fileInfoList, err := ioutil.ReadDir(labelDir) 290 if err != nil { 291 return fmt.Errorf("ReadDir(%v) failed: %v", labelDir, err) 292 } 293 fmt.Fprintf(jirix.Stdout(), "snapshots of label %q:\n", label) 294 for _, fileInfo := range fileInfoList { 295 fmt.Fprintf(jirix.Stdout(), " %v\n", fileInfo.Name()) 296 } 297 } 298 return nil 299 }