blob: 437cd81692481d8e66ae3ade3182fcffa5b43804 [file] [log] [blame]
// Copyright 2015 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package main
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"sort"
"strings"
"time"
"v.io/jiri/collect"
"v.io/jiri/gitutil"
"v.io/jiri/jiri"
"v.io/jiri/project"
"v.io/jiri/runutil"
"v.io/x/lib/cmdline"
)
const (
defaultSnapshotDir = ".snapshot"
)
var (
pushRemoteFlag bool
snapshotDirFlag string
snapshotGcFlag bool
timeFormatFlag string
)
func init() {
cmdSnapshot.Flags.StringVar(&snapshotDirFlag, "dir", "", "Directory where snapshot are stored. Defaults to $JIRI_ROOT/.snapshot.")
cmdSnapshotCheckout.Flags.BoolVar(&snapshotGcFlag, "gc", false, "Garbage collect obsolete repositories.")
cmdSnapshotCreate.Flags.BoolVar(&pushRemoteFlag, "push-remote", false, "Commit and push snapshot upstream.")
cmdSnapshotCreate.Flags.StringVar(&timeFormatFlag, "time-format", time.RFC3339, "Time format for snapshot file name.")
}
var cmdSnapshot = &cmdline.Command{
Name: "snapshot",
Short: "Manage project snapshots",
Long: `
The "jiri snapshot" command can be used to manage project snapshots.
In particular, it can be used to create new snapshots and to list
existing snapshots.
`,
Children: []*cmdline.Command{cmdSnapshotCheckout, cmdSnapshotCreate, cmdSnapshotList},
}
// cmdSnapshotCreate represents the "jiri snapshot create" command.
var cmdSnapshotCreate = &cmdline.Command{
Runner: jiri.RunnerFunc(runSnapshotCreate),
Name: "create",
Short: "Create a new project snapshot",
Long: `
The "jiri snapshot create <label>" command captures the current project state
in a manifest. If the -push-remote flag is provided, the snapshot is committed
and pushed upstream.
Internally, snapshots are organized as follows:
<snapshot-dir>/
labels/
<label1>/
<label1-snapshot1>
<label1-snapshot2>
...
<label2>/
<label2-snapshot1>
<label2-snapshot2>
...
<label3>/
...
<label1> # a symlink to the latest <label1-snapshot*>
<label2> # a symlink to the latest <label2-snapshot*>
...
NOTE: Unlike the jiri tool commands, the above internal organization
is not an API. It is an implementation and can change without notice.
`,
ArgsName: "<label>",
ArgsLong: "<label> is the snapshot label.",
}
func runSnapshotCreate(jirix *jiri.X, args []string) error {
if len(args) != 1 {
return jirix.UsageErrorf("unexpected number of arguments")
}
label := args[0]
snapshotDir, err := getSnapshotDir(jirix)
if err != nil {
return err
}
snapshotFile := filepath.Join(snapshotDir, "labels", label, time.Now().Format(timeFormatFlag))
if !pushRemoteFlag {
// No git operations necessary. Just create the snapshot file.
return createSnapshot(jirix, snapshotDir, snapshotFile, label)
}
// Attempt to create a snapshot on a clean master branch. If snapshot
// creation fails, return to the state we were in before.
createFn := func() error {
git := gitutil.New(jirix.NewSeq())
revision, err := git.CurrentRevision()
if err != nil {
return err
}
if err := createSnapshot(jirix, snapshotDir, snapshotFile, label); err != nil {
git.Reset(revision)
git.RemoveUntrackedFiles()
return err
}
return commitAndPushChanges(jirix, snapshotDir, snapshotFile, label)
}
// Execute the above function in the snapshot directory on a clean master branch.
p := project.Project{
Path: snapshotDir,
Protocol: "git",
RemoteBranch: "master",
Revision: "HEAD",
}
return project.ApplyToLocalMaster(jirix, project.Projects{p.Key(): p}, createFn)
}
// getSnapshotDir returns the path to the snapshot directory, creating it if
// necessary.
func getSnapshotDir(jirix *jiri.X) (string, error) {
dir := snapshotDirFlag
if dir == "" {
dir = filepath.Join(jirix.Root, defaultSnapshotDir)
}
if !filepath.IsAbs(dir) {
cwd, err := os.Getwd()
if err != nil {
return "", err
}
dir = filepath.Join(cwd, dir)
}
// Make sure directory exists.
if err := jirix.NewSeq().MkdirAll(dir, 0755).Done(); err != nil {
return "", err
}
return dir, nil
}
func createSnapshot(jirix *jiri.X, snapshotDir, snapshotFile, label string) error {
// Create a snapshot that encodes the current state of master
// branches for all local projects.
if err := project.CreateSnapshot(jirix, snapshotFile, ""); err != nil {
return err
}
s := jirix.NewSeq()
// Update the symlink for this snapshot label to point to the
// latest snapshot.
symlink := filepath.Join(snapshotDir, label)
newSymlink := symlink + ".new"
relativeSnapshotPath := strings.TrimPrefix(snapshotFile, snapshotDir+string(os.PathSeparator))
return s.RemoveAll(newSymlink).
Symlink(relativeSnapshotPath, newSymlink).
Rename(newSymlink, symlink).Done()
}
// commitAndPushChanges commits changes identified by the given manifest file
// and label to the containing repository and pushes these changes to the
// remote repository.
func commitAndPushChanges(jirix *jiri.X, snapshotDir, snapshotFile, label string) (e error) {
cwd, err := os.Getwd()
if err != nil {
return err
}
defer collect.Error(func() error { return jirix.NewSeq().Chdir(cwd).Done() }, &e)
if err := jirix.NewSeq().Chdir(snapshotDir).Done(); err != nil {
return err
}
relativeSnapshotPath := strings.TrimPrefix(snapshotFile, snapshotDir+string(os.PathSeparator))
git := gitutil.New(jirix.NewSeq())
// Pull from master so we are up-to-date.
if err := git.Pull("origin", "master"); err != nil {
return err
}
if err := git.Add(relativeSnapshotPath); err != nil {
return err
}
if err := git.Add(label); err != nil {
return err
}
name := strings.TrimPrefix(snapshotFile, snapshotDir)
if err := git.CommitNoVerify(fmt.Sprintf("adding snapshot %q for label %q", name, label)); err != nil {
return err
}
if err := git.Push("origin", "master", gitutil.VerifyOpt(false)); err != nil {
return err
}
return nil
}
// cmdSnapshotCheckout represents the "jiri snapshot checkout" command.
var cmdSnapshotCheckout = &cmdline.Command{
Runner: jiri.RunnerFunc(runSnapshotCheckout),
Name: "checkout",
Short: "Checkout a project snapshot",
Long: `
The "jiri snapshot checkout <snapshot>" command restores local project state to
the state in the given snapshot manifest.
`,
ArgsName: "<snapshot>",
ArgsLong: "<snapshot> is the snapshot manifest file.",
}
func runSnapshotCheckout(jirix *jiri.X, args []string) error {
if len(args) != 1 {
return jirix.UsageErrorf("unexpected number of arguments")
}
return project.CheckoutSnapshot(jirix, args[0], snapshotGcFlag)
}
// cmdSnapshotList represents the "jiri snapshot list" command.
var cmdSnapshotList = &cmdline.Command{
Runner: jiri.RunnerFunc(runSnapshotList),
Name: "list",
Short: "List existing project snapshots",
Long: `
The "snapshot list" command lists existing snapshots of the labels
specified as command-line arguments. If no arguments are provided, the
command lists snapshots for all known labels.
`,
ArgsName: "<label ...>",
ArgsLong: "<label ...> is a list of snapshot labels.",
}
func runSnapshotList(jirix *jiri.X, args []string) error {
snapshotDir, err := getSnapshotDir(jirix)
if err != nil {
return err
}
if len(args) == 0 {
// Identify all known snapshot labels, using a
// heuristic that looks for all symbolic links <foo>
// in the snapshot directory that point to a file in
// the "labels/<foo>" subdirectory of the snapshot
// directory.
fileInfoList, err := ioutil.ReadDir(snapshotDir)
if err != nil {
return fmt.Errorf("ReadDir(%v) failed: %v", snapshotDir, err)
}
for _, fileInfo := range fileInfoList {
if fileInfo.Mode()&os.ModeSymlink != 0 {
path := filepath.Join(snapshotDir, fileInfo.Name())
dst, err := filepath.EvalSymlinks(path)
if err != nil {
return fmt.Errorf("EvalSymlinks(%v) failed: %v", path, err)
}
if strings.HasSuffix(filepath.Dir(dst), filepath.Join("labels", fileInfo.Name())) {
args = append(args, fileInfo.Name())
}
}
}
}
// Check that all labels exist.
failed := false
for _, label := range args {
labelDir := filepath.Join(snapshotDir, "labels", label)
if _, err := jirix.NewSeq().Stat(labelDir); err != nil {
if !runutil.IsNotExist(err) {
return err
}
failed = true
fmt.Fprintf(jirix.Stderr(), "snapshot label %q not found", label)
}
}
if failed {
return cmdline.ErrExitCode(2)
}
// Print snapshots for all labels.
sort.Strings(args)
for _, label := range args {
// Scan the snapshot directory "labels/<label>" printing
// all snapshots.
labelDir := filepath.Join(snapshotDir, "labels", label)
fileInfoList, err := ioutil.ReadDir(labelDir)
if err != nil {
return fmt.Errorf("ReadDir(%v) failed: %v", labelDir, err)
}
fmt.Fprintf(jirix.Stdout(), "snapshots of label %q:\n", label)
for _, fileInfo := range fileInfoList {
fmt.Fprintf(jirix.Stdout(), " %v\n", fileInfo.Name())
}
}
return nil
}