| // 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" |
| "os/exec" |
| "path/filepath" |
| "strings" |
| "sync" |
| |
| "golang.org/x/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 |
| } |
| if rev == "" { |
| return r.Master.VCS.Create(path, downloadPath) |
| } |
| return r.Master.VCS.CreateAtRev(path, downloadPath, rev) |
| }) |
| 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 { |
| // TODO(adg,cmang): implement Export in go/vcs |
| _, err := r.Clone(path, rev) |
| return err |
| } |
| |
| // UpdateTo updates the working copy of this Repo to the |
| // supplied revision. |
| func (r *Repo) UpdateTo(hash string) error { |
| r.Lock() |
| defer r.Unlock() |
| |
| if r.Master.VCS.Cmd == "git" { |
| cmd := exec.Command("git", "reset", "--hard", hash) |
| var log bytes.Buffer |
| err := run(cmd, runTimeout(*cmdTimeout), runDir(r.Path), allOutput(&log)) |
| if err != nil { |
| return fmt.Errorf("Error running git update -C %v: %v ; output=%s", hash, err, log.Bytes()) |
| } |
| return nil |
| } |
| |
| // Else go down three more levels of abstractions, at |
| // least two of which are broken for git. |
| 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("<Top>"+string(data)+"</Top>"), &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 Git or Mercurial revision. |
| func (r *Repo) FullHash(rev string) (string, error) { |
| r.Lock() |
| defer r.Unlock() |
| |
| var hash string |
| err := timeout(*cmdTimeout, func() error { |
| var data []byte |
| // Avoid the vcs package for git, since it's broken |
| // for git, and and we're trying to remove levels of |
| // abstraction which are increasingly getting |
| // difficult to navigate. |
| if r.Master.VCS.Cmd == "git" { |
| cmd := exec.Command("git", "rev-parse", rev) |
| var out bytes.Buffer |
| err := run(cmd, runTimeout(*cmdTimeout), runDir(r.Path), allOutput(&out)) |
| data = out.Bytes() |
| if err != nil { |
| return fmt.Errorf("Failed to find FullHash of %q; git rev-parse: %v, %s", rev, err, data) |
| } |
| } else { |
| var err 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 { // correct for both hg and git |
| 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 = ` |
| <Log> |
| <Hash>{node|escape}</Hash> |
| <Parent>{p1node}</Parent> |
| <Author>{author|escape}</Author> |
| <Date>{date|rfc3339date}</Date> |
| <Desc>{desc|escape}</Desc> |
| <Branch>{branches}</Branch> |
| <Files>{files}</Files> |
| </Log> |
| ` |