launchpad.net/~rogpeppe/juju-core/500-errgo-fix@v0.0.0-20140213181702-000000002356/bzr/bzr.go (about)

     1  // Copyright 2013 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  // Package bzr offers an interface to manage branches of the Bazaar VCS.
     5  package bzr
     6  
     7  import (
     8  	"bytes"
     9  	"fmt"
    10  	"os"
    11  	"os/exec"
    12  	"path"
    13  	"strings"
    14  )
    15  
    16  // Branch represents a Bazaar branch.
    17  type Branch struct {
    18  	location string
    19  	env      []string
    20  }
    21  
    22  // New returns a new Branch for the Bazaar branch at location.
    23  func New(location string) *Branch {
    24  	b := &Branch{location, cenv()}
    25  	if _, err := os.Stat(location); err == nil {
    26  		stdout, _, err := b.bzr("root")
    27  		if err == nil {
    28  			b.location = strings.TrimRight(string(stdout), "\n")
    29  		}
    30  	}
    31  	return b
    32  }
    33  
    34  // cenv returns a copy of the current process environment with LC_ALL=C.
    35  func cenv() []string {
    36  	env := os.Environ()
    37  	for i, pair := range env {
    38  		if strings.HasPrefix(pair, "LC_ALL=") {
    39  			env[i] = "LC_ALL=C"
    40  			return env
    41  		}
    42  	}
    43  	return append(env, "LC_ALL=C")
    44  }
    45  
    46  // Location returns the location of branch b.
    47  func (b *Branch) Location() string {
    48  	return b.location
    49  }
    50  
    51  // Join returns b's location with parts appended as path components.
    52  // In other words, if b's location is "lp:foo", and parts is {"bar, baz"},
    53  // Join returns "lp:foo/bar/baz".
    54  func (b *Branch) Join(parts ...string) string {
    55  	return path.Join(append([]string{b.location}, parts...)...)
    56  }
    57  
    58  func (b *Branch) bzr(subcommand string, args ...string) (stdout, stderr []byte, err error) {
    59  	cmd := exec.Command("bzr", append([]string{subcommand}, args...)...)
    60  	if _, err := os.Stat(b.location); err == nil {
    61  		cmd.Dir = b.location
    62  	}
    63  	errbuf := &bytes.Buffer{}
    64  	cmd.Stderr = errbuf
    65  	cmd.Env = b.env
    66  	stdout, err = cmd.Output()
    67  	// Some commands fail with exit status 0 (e.g. bzr root). :-(
    68  	if err != nil || bytes.Contains(errbuf.Bytes(), []byte("ERROR")) {
    69  		var errmsg string
    70  		if err != nil {
    71  			errmsg = err.Error()
    72  		}
    73  		return nil, nil, fmt.Errorf(`error running "bzr %s": %s%s%s`, subcommand, stdout, errbuf.Bytes(), errmsg)
    74  	}
    75  	return stdout, errbuf.Bytes(), err
    76  }
    77  
    78  // Init intializes a new branch at b's location.
    79  func (b *Branch) Init() error {
    80  	_, _, err := b.bzr("init", b.location)
    81  	return err
    82  }
    83  
    84  // Add adds to b the path resultant from calling b.Join(parts...).
    85  func (b *Branch) Add(parts ...string) error {
    86  	_, _, err := b.bzr("add", b.Join(parts...))
    87  	return err
    88  }
    89  
    90  // Commit commits pending changes into b.
    91  func (b *Branch) Commit(message string) error {
    92  	_, _, err := b.bzr("commit", "-q", "-m", message)
    93  	return err
    94  }
    95  
    96  // RevisionId returns the Bazaar revision id for the tip of b.
    97  func (b *Branch) RevisionId() (string, error) {
    98  	stdout, stderr, err := b.bzr("revision-info", "-d", b.location)
    99  	if err != nil {
   100  		return "", err
   101  	}
   102  	pair := bytes.Fields(stdout)
   103  	if len(pair) != 2 {
   104  		return "", fmt.Errorf(`invalid output from "bzr revision-info": %s%s`, stdout, stderr)
   105  	}
   106  	id := string(pair[1])
   107  	if id == "null:" {
   108  		return "", fmt.Errorf("branch has no content")
   109  	}
   110  	return id, nil
   111  }
   112  
   113  // PushLocation returns the default push location for b.
   114  func (b *Branch) PushLocation() (string, error) {
   115  	stdout, _, err := b.bzr("info", b.location)
   116  	if err != nil {
   117  		return "", err
   118  	}
   119  	if i := bytes.Index(stdout, []byte("push branch:")); i >= 0 {
   120  		return string(stdout[i+13 : i+bytes.IndexAny(stdout[i:], "\r\n")]), nil
   121  	}
   122  	return "", fmt.Errorf("no push branch location defined")
   123  }
   124  
   125  // PushAttr holds options for the Branch.Push method.
   126  type PushAttr struct {
   127  	Location string // Location to push to. Use the default push location if empty.
   128  	Remember bool   // Whether to remember the location being pushed to as the default.
   129  }
   130  
   131  // Push pushes any new revisions in b to attr.Location if that's
   132  // provided, or to the default push location otherwise.
   133  // See PushAttr for other options.
   134  func (b *Branch) Push(attr *PushAttr) error {
   135  	var args []string
   136  	if attr != nil {
   137  		if attr.Remember {
   138  			args = append(args, "--remember")
   139  		}
   140  		if attr.Location != "" {
   141  			args = append(args, attr.Location)
   142  		}
   143  	}
   144  	_, _, err := b.bzr("push", args...)
   145  	return err
   146  }
   147  
   148  // CheckClean returns an error if 'bzr status' is not clean.
   149  func (b *Branch) CheckClean() error {
   150  	stdout, _, err := b.bzr("status", b.location)
   151  	if err != nil {
   152  		return err
   153  	}
   154  	if bytes.Count(stdout, []byte{'\n'}) == 1 && bytes.Contains(stdout, []byte(`See "bzr shelve --list" for details.`)) {
   155  		return nil // Shelves are fine.
   156  	}
   157  	if len(stdout) > 0 {
   158  		return fmt.Errorf("branch is not clean (bzr status)")
   159  	}
   160  	return nil
   161  }