package vcs import ( "fmt" "net/url" "os" "os/exec" "path/filepath" "regexp" "strings" "time" ) var bzrDetectURL = regexp.MustCompile("parent branch: (?P.+)\n") // NewBzrRepo creates a new instance of BzrRepo. The remote and local directories // need to be passed in. func NewBzrRepo(remote, local string) (*BzrRepo, error) { ins := depInstalled("bzr") if !ins { return nil, NewLocalError("bzr is not installed", nil, "") } ltype, err := DetectVcsFromFS(local) // Found a VCS other than Bzr. Need to report an error. if err == nil && ltype != Bzr { return nil, ErrWrongVCS } r := &BzrRepo{} r.setRemote(remote) r.setLocalPath(local) r.Logger = Logger // With the other VCS we can check if the endpoint locally is different // from the one configured internally. But, with Bzr you can't. For example, // if you do `bzr branch https://launchpad.net/govcstestbzrrepo` and then // use `bzr info` to get the parent branch you'll find it set to // http://bazaar.launchpad.net/~mattfarina/govcstestbzrrepo/trunk/. Notice // the change from https to http and the path chance. // Here we set the remote to be the local one if none is passed in. if err == nil && r.CheckLocal() && remote == "" { c := exec.Command("bzr", "info") c.Dir = local c.Env = envForDir(c.Dir) out, err := c.CombinedOutput() if err != nil { return nil, NewLocalError("Unable to retrieve local repo information", err, string(out)) } m := bzrDetectURL.FindStringSubmatch(string(out)) // If no remote was passed in but one is configured for the locally // checked out Bzr repo use that one. if m[1] != "" { r.setRemote(m[1]) } } return r, nil } // BzrRepo implements the Repo interface for the Bzr source control. type BzrRepo struct { base } // Vcs retrieves the underlying VCS being implemented. func (s BzrRepo) Vcs() Type { return Bzr } // Get is used to perform an initial clone of a repository. func (s *BzrRepo) Get() error { basePath := filepath.Dir(filepath.FromSlash(s.LocalPath())) if _, err := os.Stat(basePath); os.IsNotExist(err) { err = os.MkdirAll(basePath, 0755) if err != nil { return NewLocalError("Unable to create directory", err, "") } } out, err := s.run("bzr", "branch", "--", s.Remote(), s.LocalPath()) if err != nil { return NewRemoteError("Unable to get repository", err, string(out)) } return nil } // Init initializes a bazaar repository at local location. func (s *BzrRepo) Init() error { out, err := s.run("bzr", "init", "--", s.LocalPath()) // There are some windows cases where bazaar cannot create the parent // directory if it does not already exist, to the location it's trying // to create the repo. Catch that error and try to handle it. if err != nil && s.isUnableToCreateDir(err) { basePath := filepath.Dir(filepath.FromSlash(s.LocalPath())) if _, err := os.Stat(basePath); os.IsNotExist(err) { err = os.MkdirAll(basePath, 0755) if err != nil { return NewLocalError("Unable to initialize repository", err, "") } out, err = s.run("bzr", "init", "--", s.LocalPath()) if err != nil { return NewLocalError("Unable to initialize repository", err, string(out)) } return nil } } else if err != nil { return NewLocalError("Unable to initialize repository", err, string(out)) } return nil } // Update performs a Bzr pull and update to an existing checkout. func (s *BzrRepo) Update() error { out, err := s.RunFromDir("bzr", "pull") if err != nil { return NewRemoteError("Unable to update repository", err, string(out)) } out, err = s.RunFromDir("bzr", "update") if err != nil { return NewRemoteError("Unable to update repository", err, string(out)) } return nil } // UpdateVersion sets the version of a package currently checked out via Bzr. func (s *BzrRepo) UpdateVersion(version string) error { out, err := s.RunFromDir("bzr", "update", "-r", version) if err != nil { return NewLocalError("Unable to update checked out version", err, string(out)) } return nil } // Version retrieves the current version. func (s *BzrRepo) Version() (string, error) { out, err := s.RunFromDir("bzr", "revno", "--tree") if err != nil { return "", NewLocalError("Unable to retrieve checked out version", err, string(out)) } return strings.TrimSpace(string(out)), nil } // Current returns the current version-ish. This means: // * -1 if on the tip of the branch (this is the Bzr value for HEAD) // * A tag if on a tag // * Otherwise a revision func (s *BzrRepo) Current() (string, error) { tip, err := s.CommitInfo("-1") if err != nil { return "", err } curr, err := s.Version() if err != nil { return "", err } if tip.Commit == curr { return "-1", nil } ts, err := s.TagsFromCommit(curr) if err != nil { return "", err } if len(ts) > 0 { return ts[0], nil } return curr, nil } // Date retrieves the date on the latest commit. func (s *BzrRepo) Date() (time.Time, error) { out, err := s.RunFromDir("bzr", "version-info", "--custom", "--template={date}") if err != nil { return time.Time{}, NewLocalError("Unable to retrieve revision date", err, string(out)) } t, err := time.Parse(longForm, string(out)) if err != nil { return time.Time{}, NewLocalError("Unable to retrieve revision date", err, string(out)) } return t, nil } // CheckLocal verifies the local location is a Bzr repo. func (s *BzrRepo) CheckLocal() bool { if _, err := os.Stat(s.LocalPath() + "/.bzr"); err == nil { return true } return false } // Branches returns a list of available branches on the repository. // In Bazaar (Bzr) clones and branches are the same. A different branch will // have a different URL location which we cannot detect from the repo. This // is a little different from other VCS. func (s *BzrRepo) Branches() ([]string, error) { var branches []string return branches, nil } // Tags returns a list of available tags on the repository. func (s *BzrRepo) Tags() ([]string, error) { out, err := s.RunFromDir("bzr", "tags") if err != nil { return []string{}, NewLocalError("Unable to retrieve tags", err, string(out)) } tags := s.referenceList(string(out), `(?m-s)^(\S+)`) return tags, nil } // IsReference returns if a string is a reference. A reference can be a // commit id or tag. func (s *BzrRepo) IsReference(r string) bool { _, err := s.RunFromDir("bzr", "revno", "-r", r) return err == nil } // IsDirty returns if the checkout has been modified from the checked // out reference. func (s *BzrRepo) IsDirty() bool { out, err := s.RunFromDir("bzr", "diff") return err != nil || len(out) != 0 } // CommitInfo retrieves metadata about a commit. func (s *BzrRepo) CommitInfo(id string) (*CommitInfo, error) { r := "-r" + id out, err := s.RunFromDir("bzr", "log", r, "--log-format=long") if err != nil { return nil, ErrRevisionUnavailable } ci := &CommitInfo{} lines := strings.Split(string(out), "\n") const format = "Mon 2006-01-02 15:04:05 -0700" var track int var trackOn bool // Note, bzr does not appear to use i18m. for i, l := range lines { if strings.HasPrefix(l, "revno:") { ci.Commit = strings.TrimSpace(strings.TrimPrefix(l, "revno:")) } else if strings.HasPrefix(l, "committer:") { ci.Author = strings.TrimSpace(strings.TrimPrefix(l, "committer:")) } else if strings.HasPrefix(l, "timestamp:") { ts := strings.TrimSpace(strings.TrimPrefix(l, "timestamp:")) ci.Date, err = time.Parse(format, ts) if err != nil { return nil, NewLocalError("Unable to retrieve commit information", err, string(out)) } } else if strings.TrimSpace(l) == "message:" { track = i trackOn = true } else if trackOn && i > track { ci.Message = ci.Message + l } } ci.Message = strings.TrimSpace(ci.Message) // Didn't find the revision if ci.Author == "" { return nil, ErrRevisionUnavailable } return ci, nil } // TagsFromCommit retrieves tags from a commit id. func (s *BzrRepo) TagsFromCommit(id string) ([]string, error) { out, err := s.RunFromDir("bzr", "tags", "-r", id) if err != nil { return []string{}, NewLocalError("Unable to retrieve tags", err, string(out)) } tags := s.referenceList(string(out), `(?m-s)^(\S+)`) return tags, nil } // Ping returns if remote location is accessible. func (s *BzrRepo) Ping() bool { // Running bzr info is slow. Many of the projects are on launchpad which // has a public 1.0 API we can use. u, err := url.Parse(s.Remote()) if err == nil { if u.Host == "launchpad.net" { try := strings.TrimPrefix(u.Path, "/") // get returns the body and an err. If the status code is not a 200 // an error is returned. Launchpad returns a 404 for a codebase that // does not exist. Otherwise it returns a JSON object describing it. _, er := get("https://api.launchpad.net/1.0/" + try) return er == nil } } // This is the same command that Go itself uses but it's not fast (or fast // enough by my standards). A faster method would be useful. _, err = s.run("bzr", "info", "--", s.Remote()) return err == nil } // ExportDir exports the current revision to the passed in directory. func (s *BzrRepo) ExportDir(dir string) error { out, err := s.RunFromDir("bzr", "export", "--", dir) s.log(out) if err != nil { return NewLocalError("Unable to export source", err, string(out)) } return nil } // Multi-lingual manner check for the VCS error that it couldn't create directory. // https://bazaar.launchpad.net/~bzr-pqm/bzr/bzr.dev/files/head:/po/ func (s *BzrRepo) isUnableToCreateDir(err error) bool { msg := err.Error() if strings.HasPrefix(msg, fmt.Sprintf("Parent directory of %s does not exist.", s.LocalPath())) || strings.HasPrefix(msg, fmt.Sprintf("Nadřazený adresář %s neexistuje.", s.LocalPath())) || strings.HasPrefix(msg, fmt.Sprintf("El directorio padre de %s no existe.", s.LocalPath())) || strings.HasPrefix(msg, fmt.Sprintf("%s の親ディレクトリがありません。", s.LocalPath())) || strings.HasPrefix(msg, fmt.Sprintf("Родительская директория для %s не существует.", s.LocalPath())) { return true } return false }