1package getter
2
3import (
4	"context"
5	"fmt"
6	"net/url"
7	"os"
8	"os/exec"
9	"path/filepath"
10	"runtime"
11
12	urlhelper "github.com/hashicorp/go-getter/helper/url"
13	safetemp "github.com/hashicorp/go-safetemp"
14)
15
16// HgGetter is a Getter implementation that will download a module from
17// a Mercurial repository.
18type HgGetter struct {
19	getter
20}
21
22func (g *HgGetter) ClientMode(_ *url.URL) (ClientMode, error) {
23	return ClientModeDir, nil
24}
25
26func (g *HgGetter) Get(dst string, u *url.URL) error {
27	ctx := g.Context()
28	if _, err := exec.LookPath("hg"); err != nil {
29		return fmt.Errorf("hg must be available and on the PATH")
30	}
31
32	newURL, err := urlhelper.Parse(u.String())
33	if err != nil {
34		return err
35	}
36	if fixWindowsDrivePath(newURL) {
37		// See valid file path form on http://www.selenic.com/hg/help/urls
38		newURL.Path = fmt.Sprintf("/%s", newURL.Path)
39	}
40
41	// Extract some query parameters we use
42	var rev string
43	q := newURL.Query()
44	if len(q) > 0 {
45		rev = q.Get("rev")
46		q.Del("rev")
47
48		newURL.RawQuery = q.Encode()
49	}
50
51	_, err = os.Stat(dst)
52	if err != nil && !os.IsNotExist(err) {
53		return err
54	}
55	if err != nil {
56		if err := g.clone(dst, newURL); err != nil {
57			return err
58		}
59	}
60
61	if err := g.pull(dst, newURL); err != nil {
62		return err
63	}
64
65	return g.update(ctx, dst, newURL, rev)
66}
67
68// GetFile for Hg doesn't support updating at this time. It will download
69// the file every time.
70func (g *HgGetter) GetFile(dst string, u *url.URL) error {
71	// Create a temporary directory to store the full source. This has to be
72	// a non-existent directory.
73	td, tdcloser, err := safetemp.Dir("", "getter")
74	if err != nil {
75		return err
76	}
77	defer tdcloser.Close()
78
79	// Get the filename, and strip the filename from the URL so we can
80	// just get the repository directly.
81	filename := filepath.Base(u.Path)
82	u.Path = filepath.ToSlash(filepath.Dir(u.Path))
83
84	// If we're on Windows, we need to set the host to "localhost" for hg
85	if runtime.GOOS == "windows" {
86		u.Host = "localhost"
87	}
88
89	// Get the full repository
90	if err := g.Get(td, u); err != nil {
91		return err
92	}
93
94	// Copy the single file
95	u, err = urlhelper.Parse(fmtFileURL(filepath.Join(td, filename)))
96	if err != nil {
97		return err
98	}
99
100	fg := &FileGetter{Copy: true, getter: g.getter}
101	return fg.GetFile(dst, u)
102}
103
104func (g *HgGetter) clone(dst string, u *url.URL) error {
105	cmd := exec.Command("hg", "clone", "-U", u.String(), dst)
106	return getRunCommand(cmd)
107}
108
109func (g *HgGetter) pull(dst string, u *url.URL) error {
110	cmd := exec.Command("hg", "pull")
111	cmd.Dir = dst
112	return getRunCommand(cmd)
113}
114
115func (g *HgGetter) update(ctx context.Context, dst string, u *url.URL, rev string) error {
116	args := []string{"update"}
117	if rev != "" {
118		args = append(args, rev)
119	}
120
121	cmd := exec.CommandContext(ctx, "hg", args...)
122	cmd.Dir = dst
123	return getRunCommand(cmd)
124}
125
126func fixWindowsDrivePath(u *url.URL) bool {
127	// hg assumes a file:/// prefix for Windows drive letter file paths.
128	// (e.g. file:///c:/foo/bar)
129	// If the URL Path does not begin with a '/' character, the resulting URL
130	// path will have a file:// prefix. (e.g. file://c:/foo/bar)
131	// See http://www.selenic.com/hg/help/urls and the examples listed in
132	// http://selenic.com/repo/hg-stable/file/1265a3a71d75/mercurial/util.py#l1936
133	return runtime.GOOS == "windows" && u.Scheme == "file" &&
134		len(u.Path) > 1 && u.Path[0] != '/' && u.Path[1] == ':'
135}
136