// Copyright 2013 The Go 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 (
"bytes"
"encoding/xml"
"fmt"
"os"
"path/filepath"
"strings"
"sync"
"code.google.com/p/go.tools/go/vcs"
)
// Repo represents a mercurial repository.
type Repo struct {
Path string
Master *vcs.RepoRoot
sync.Mutex
}
// RemoteRepo constructs a *Repo representing a remote repository.
func RemoteRepo(url, path string) (*Repo, error) {
rr, err := vcs.RepoRootForImportPath(url, *verbose)
if err != nil {
return nil, err
}
return &Repo{
Path: path,
Master: rr,
}, nil
}
// Clone clones the current Repo to a new destination
// returning a new *Repo if successful.
func (r *Repo) Clone(path, rev string) (*Repo, error) {
r.Lock()
defer r.Unlock()
err := timeout(*cmdTimeout, func() error {
downloadPath := r.Path
if !r.Exists() {
downloadPath = r.Master.Repo
}
err := r.Master.VCS.CreateAtRev(path, downloadPath, rev)
if err != nil {
return err
}
return r.Master.VCS.TagSync(path, "")
})
if err != nil {
return nil, err
}
return &Repo{
Path: path,
Master: r.Master,
}, nil
}
// Export exports the current Repo at revision rev to a new destination.
func (r *Repo) Export(path, rev string) error {
r.Lock()
defer r.Unlock()
downloadPath := r.Path
if !r.Exists() {
_, err := r.Clone(path, rev)
return err
}
args := []string{r.Master.VCS.Cmd, "archive", "-t", "files", "-r", rev, path}
if err := run(*cmdTimeout, nil, downloadPath, args...); err != nil {
return fmt.Errorf("executing %s: %v", strings.Join(args, " "), err)
}
return nil
}
// UpdateTo updates the working copy of this Repo to the
// supplied revision.
func (r *Repo) UpdateTo(hash string) error {
r.Lock()
defer r.Unlock()
return timeout(*cmdTimeout, func() error {
return r.Master.VCS.TagSync(r.Path, hash)
})
}
// Exists reports whether this Repo represents a valid Mecurial repository.
func (r *Repo) Exists() bool {
fi, err := os.Stat(filepath.Join(r.Path, "."+r.Master.VCS.Cmd))
if err != nil {
return false
}
return fi.IsDir()
}
// Pull pulls changes from the default path, that is, the path
// this Repo was cloned from.
func (r *Repo) Pull() error {
r.Lock()
defer r.Unlock()
return timeout(*cmdTimeout, func() error {
return r.Master.VCS.Download(r.Path)
})
}
// Log returns the changelog for this repository.
func (r *Repo) Log() ([]HgLog, error) {
if err := r.Pull(); err != nil {
return nil, err
}
r.Lock()
defer r.Unlock()
var logStruct struct {
Log []HgLog
}
err := timeout(*cmdTimeout, func() error {
data, err := r.Master.VCS.Log(r.Path, xmlLogTemplate)
if err != nil {
return err
}
// We have a commit with description that contains 0x1b byte.
// Mercurial does not escape it, but xml.Unmarshal does not accept it.
data = bytes.Replace(data, []byte{0x1b}, []byte{'?'}, -1)
err = xml.Unmarshal([]byte(""+string(data)+""), &logStruct)
if err != nil {
return fmt.Errorf("unmarshal %s log: %v", r.Master.VCS, err)
}
return nil
})
if err != nil {
return nil, err
}
for i, log := range logStruct.Log {
// Let's pretend there can be only one parent.
if log.Parent != "" && strings.Contains(log.Parent, " ") {
logStruct.Log[i].Parent = strings.Split(log.Parent, " ")[0]
}
}
return logStruct.Log, nil
}
// FullHash returns the full hash for the given Mercurial revision.
func (r *Repo) FullHash(rev string) (string, error) {
r.Lock()
defer r.Unlock()
var hash string
err := timeout(*cmdTimeout, func() error {
data, err := r.Master.VCS.LogAtRev(r.Path, rev, "{node}")
if err != nil {
return err
}
s := strings.TrimSpace(string(data))
if s == "" {
return fmt.Errorf("cannot find revision")
}
if len(s) != 40 {
return fmt.Errorf("%s returned invalid hash: %s", r.Master.VCS, s)
}
hash = s
return nil
})
if err != nil {
return "", err
}
return hash, nil
}
// HgLog represents a single Mercurial revision.
type HgLog struct {
Hash string
Author string
Date string
Desc string
Parent string
Branch string
Files string
// Internal metadata
added bool
bench bool // needs to be benchmarked?
}
// xmlLogTemplate is a template to pass to Mercurial to make
// hg log print the log in valid XML for parsing with xml.Unmarshal.
// Can not escape branches and files, because it crashes python with:
// AttributeError: 'NoneType' object has no attribute 'replace'
const xmlLogTemplate = `
{node|escape}
{parents}
{author|escape}
{date|rfc3339date}
{desc|escape}
{branches}
{files}
`