package vcs import ( "bytes" "encoding/xml" "errors" "os" "os/exec" "regexp" "strings" "time" ) var hgDetectURL = regexp.MustCompile("default = (?P.+)\n") // NewHgRepo creates a new instance of HgRepo. The remote and local directories // need to be passed in. func NewHgRepo(remote, local string) (*HgRepo, error) { ins := depInstalled("hg") if !ins { return nil, NewLocalError("hg is not installed", nil, "") } ltype, err := DetectVcsFromFS(local) // Found a VCS other than Hg. Need to report an error. if err == nil && ltype != Hg { return nil, ErrWrongVCS } r := &HgRepo{} r.setRemote(remote) r.setLocalPath(local) r.Logger = Logger // Make sure the local Hg repo is configured the same as the remote when // A remote value was passed in. if err == nil && r.CheckLocal() { // An Hg repo was found so test that the URL there matches // the repo passed in here. c := exec.Command("hg", "paths") 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 := hgDetectURL.FindStringSubmatch(string(out)) if m[1] != "" && m[1] != remote { return nil, ErrWrongRemote } // If no remote was passed in but one is configured for the locally // checked out Hg repo use that one. if remote == "" && m[1] != "" { r.setRemote(m[1]) } } return r, nil } // HgRepo implements the Repo interface for the Mercurial source control. type HgRepo struct { base } // Vcs retrieves the underlying VCS being implemented. func (s HgRepo) Vcs() Type { return Hg } // Get is used to perform an initial clone of a repository. func (s *HgRepo) Get() error { out, err := s.run("hg", "clone", "--", s.Remote(), s.LocalPath()) if err != nil { return NewRemoteError("Unable to get repository", err, string(out)) } return nil } // Init will initialize a mercurial repository at local location. func (s *HgRepo) Init() error { out, err := s.run("hg", "init", "--", s.LocalPath()) if err != nil { return NewLocalError("Unable to initialize repository", err, string(out)) } return nil } // Update performs a Mercurial pull to an existing checkout. func (s *HgRepo) Update() error { return s.UpdateVersion(``) } // UpdateVersion sets the version of a package currently checked out via Hg. func (s *HgRepo) UpdateVersion(version string) error { out, err := s.RunFromDir("hg", "pull") if err != nil { return NewLocalError("Unable to update checked out version", err, string(out)) } if len(strings.TrimSpace(version)) > 0 { out, err = s.RunFromDir("hg", "update", "--", version) } else { out, err = s.RunFromDir("hg", "update") } if err != nil { return NewLocalError("Unable to update checked out version", err, string(out)) } return nil } // Version retrieves the current version. func (s *HgRepo) Version() (string, error) { c := s.CmdFromDir("hg", "--debug", "identify") stdout, stderr := new(bytes.Buffer), new(bytes.Buffer) c.Stdout = stdout c.Stderr = stderr if err := c.Run(); err != nil { return "", NewLocalError("Unable to retrieve checked out version", err, stderr.String()) } if stderr.Len() > 0 { // "hg --debug identify" can print out errors before it actually prints // the version. // https://github.com/Masterminds/vcs/issues/90 return "", NewLocalError("Unable to retrieve checked out version", errors.New("Error output printed before identify"), stderr.String()) } parts := strings.SplitN(stdout.String(), " ", 2) sha := parts[0] return strings.TrimSpace(sha), nil } // Current returns the current version-ish. This means: // * Branch name if on the tip of the branch // * Tag if on a tag // * Otherwise a revision id func (s *HgRepo) Current() (string, error) { out, err := s.RunFromDir("hg", "branch") if err != nil { return "", err } branch := strings.TrimSpace(string(out)) tip, err := s.CommitInfo("max(branch(" + branch + "))") if err != nil { return "", err } curr, err := s.Version() if err != nil { return "", err } if tip.Commit == curr { return branch, 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 *HgRepo) Date() (time.Time, error) { version, err := s.Version() if err != nil { return time.Time{}, NewLocalError("Unable to retrieve revision date", err, "") } out, err := s.RunFromDir("hg", "log", "-r", version, "--template", "{date|isodatesec}") 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 Git repo. func (s *HgRepo) CheckLocal() bool { if _, err := os.Stat(s.LocalPath() + "/.hg"); err == nil { return true } return false } // Branches returns a list of available branches func (s *HgRepo) Branches() ([]string, error) { out, err := s.RunFromDir("hg", "branches") if err != nil { return []string{}, NewLocalError("Unable to retrieve branches", err, string(out)) } branches := s.referenceList(string(out), `(?m-s)^(\S+)`) return branches, nil } // Tags returns a list of available tags func (s *HgRepo) Tags() ([]string, error) { out, err := s.RunFromDir("hg", "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, branch, or tag. func (s *HgRepo) IsReference(r string) bool { _, err := s.RunFromDir("hg", "log", "-r", r) return err == nil } // IsDirty returns if the checkout has been modified from the checked // out reference. func (s *HgRepo) IsDirty() bool { out, err := s.RunFromDir("hg", "diff") return err != nil || len(out) != 0 } // CommitInfo retrieves metadata about a commit. func (s *HgRepo) CommitInfo(id string) (*CommitInfo, error) { out, err := s.RunFromDir("hg", "log", "-r", id, "--style=xml") if err != nil { return nil, ErrRevisionUnavailable } type Author struct { Name string `xml:",chardata"` Email string `xml:"email,attr"` } type Logentry struct { Node string `xml:"node,attr"` Author Author `xml:"author"` Date string `xml:"date"` Msg string `xml:"msg"` } type Log struct { XMLName xml.Name `xml:"log"` Logs []Logentry `xml:"logentry"` } logs := &Log{} err = xml.Unmarshal(out, &logs) if err != nil { return nil, NewLocalError("Unable to retrieve commit information", err, string(out)) } if len(logs.Logs) == 0 { return nil, ErrRevisionUnavailable } ci := &CommitInfo{ Commit: logs.Logs[0].Node, Author: logs.Logs[0].Author.Name + " <" + logs.Logs[0].Author.Email + ">", Message: logs.Logs[0].Msg, } if logs.Logs[0].Date != "" { ci.Date, err = time.Parse(time.RFC3339, logs.Logs[0].Date) if err != nil { return nil, NewLocalError("Unable to retrieve commit information", err, string(out)) } } return ci, nil } // TagsFromCommit retrieves tags from a commit id. func (s *HgRepo) TagsFromCommit(id string) ([]string, error) { // Hg has a single tag per commit. If a second tag is added to a commit a // new commit is created and the tag is attached to that new commit. out, err := s.RunFromDir("hg", "log", "-r", id, "--style=xml") if err != nil { return []string{}, NewLocalError("Unable to retrieve tags", err, string(out)) } type Logentry struct { Node string `xml:"node,attr"` Tag string `xml:"tag"` } type Log struct { XMLName xml.Name `xml:"log"` Logs []Logentry `xml:"logentry"` } logs := &Log{} err = xml.Unmarshal(out, &logs) if err != nil { return []string{}, NewLocalError("Unable to retrieve tags", err, string(out)) } if len(logs.Logs) == 0 { return []string{}, NewLocalError("Unable to retrieve tags", err, string(out)) } t := strings.TrimSpace(logs.Logs[0].Tag) if t != "" { return []string{t}, nil } return []string{}, nil } // Ping returns if remote location is accessible. func (s *HgRepo) Ping() bool { _, err := s.run("hg", "identify", "--", s.Remote()) return err == nil } // ExportDir exports the current revision to the passed in directory. func (s *HgRepo) ExportDir(dir string) error { out, err := s.RunFromDir("hg", "archive", "--", dir) s.log(out) if err != nil { return NewLocalError("Unable to export source", err, string(out)) } return nil }