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  }