github.com/hashicorp/go-getter/v2@v2.2.2/get_hg.go (about) 1 package getter 2 3 import ( 4 "context" 5 "fmt" 6 "net/url" 7 "os" 8 "os/exec" 9 "path/filepath" 10 "runtime" 11 "time" 12 13 urlhelper "github.com/hashicorp/go-getter/v2/helper/url" 14 safetemp "github.com/hashicorp/go-safetemp" 15 ) 16 17 // HgGetter is a Getter implementation that will download a module from 18 // a Mercurial repository. 19 type HgGetter struct { 20 21 // Timeout sets a deadline which all hg CLI operations should 22 // complete within. Defaults to zero which means no timeout. 23 Timeout time.Duration 24 } 25 26 func (g *HgGetter) Mode(ctx context.Context, _ *url.URL) (Mode, error) { 27 return ModeDir, nil 28 } 29 30 func (g *HgGetter) Get(ctx context.Context, req *Request) error { 31 if _, err := exec.LookPath("hg"); err != nil { 32 return fmt.Errorf("hg must be available and on the PATH") 33 } 34 35 newURL, err := urlhelper.Parse(req.u.String()) 36 if err != nil { 37 return err 38 } 39 if fixWindowsDrivePath(newURL) { 40 // See valid file path form on http://www.selenic.com/hg/help/urls 41 newURL.Path = fmt.Sprintf("/%s", newURL.Path) 42 } 43 44 // Extract some query parameters we use 45 var rev string 46 q := newURL.Query() 47 if len(q) > 0 { 48 rev = q.Get("rev") 49 q.Del("rev") 50 51 newURL.RawQuery = q.Encode() 52 } 53 54 _, err = os.Stat(req.Dst) 55 if err != nil && !os.IsNotExist(err) { 56 return err 57 } 58 59 if g.Timeout > 0 { 60 var cancel context.CancelFunc 61 ctx, cancel = context.WithTimeout(ctx, g.Timeout) 62 defer cancel() 63 } 64 65 if err != nil { 66 if err := g.clone(ctx, req.Dst, newURL); err != nil { 67 return err 68 } 69 } 70 71 if err := g.pull(ctx, req.Dst, newURL); err != nil { 72 return err 73 } 74 75 return g.update(ctx, req.Dst, newURL, rev) 76 } 77 78 // GetFile for Hg doesn't support updating at this time. It will download 79 // the file every time. 80 func (g *HgGetter) GetFile(ctx context.Context, req *Request) error { 81 // Create a temporary directory to store the full source. This has to be 82 // a non-existent directory. 83 td, tdcloser, err := safetemp.Dir("", "getter") 84 if err != nil { 85 return err 86 } 87 defer tdcloser.Close() 88 89 // Get the filename, and strip the filename from the URL so we can 90 // just get the repository directly. 91 filename := filepath.Base(req.u.Path) 92 req.u.Path = filepath.Dir(req.u.Path) 93 dst := req.Dst 94 req.Dst = td 95 96 // If we're on Windows, we need to set the host to "localhost" for hg 97 if runtime.GOOS == "windows" { 98 req.u.Host = "localhost" 99 } 100 101 // Get the full repository 102 if err := g.Get(ctx, req); err != nil { 103 return err 104 } 105 106 // Copy the single file 107 req.u, err = urlhelper.Parse(fmtFileURL(filepath.Join(td, filename))) 108 if err != nil { 109 return err 110 } 111 112 fg := &FileGetter{} 113 req.Copy = true 114 req.Dst = dst 115 return fg.GetFile(ctx, req) 116 } 117 118 func (g *HgGetter) clone(ctx context.Context, dst string, u *url.URL) error { 119 cmd := exec.CommandContext(ctx, "hg", "clone", "-U", "--", u.String(), dst) 120 return getRunCommand(cmd) 121 } 122 123 func (g *HgGetter) pull(ctx context.Context, dst string, u *url.URL) error { 124 cmd := exec.CommandContext(ctx, "hg", "pull") 125 cmd.Dir = dst 126 return getRunCommand(cmd) 127 } 128 129 func (g *HgGetter) update(ctx context.Context, dst string, u *url.URL, rev string) error { 130 args := []string{"update"} 131 if rev != "" { 132 args = append(args, "--", rev) 133 } 134 135 cmd := exec.CommandContext(ctx, "hg", args...) 136 cmd.Dir = dst 137 return getRunCommand(cmd) 138 } 139 140 func (g *HgGetter) Detect(req *Request) (bool, error) { 141 src := req.Src 142 if len(src) == 0 { 143 return false, nil 144 } 145 146 if req.Forced != "" { 147 // There's a getter being Forced 148 if !g.validScheme(req.Forced) { 149 // Current getter is not the Forced one 150 // Don't use it to try to download the artifact 151 return false, nil 152 } 153 } 154 isForcedGetter := req.Forced != "" && g.validScheme(req.Forced) 155 156 u, err := url.Parse(src) 157 if err == nil && u.Scheme != "" { 158 if isForcedGetter { 159 // Is the Forced getter and source is a valid url 160 return true, nil 161 } 162 if g.validScheme(u.Scheme) { 163 return true, nil 164 } 165 // Valid url with a scheme that is not valid for current getter 166 return false, nil 167 } 168 169 src, ok, err := new(BitBucketDetector).Detect(src, req.Pwd) 170 if err != nil { 171 return ok, err 172 } 173 forced, src := getForcedGetter(src) 174 if ok && g.validScheme(forced) { 175 req.Src = src 176 return ok, nil 177 } 178 179 if isForcedGetter { 180 // Is the Forced getter and should be used to download the artifact 181 if req.Pwd != "" && !filepath.IsAbs(src) { 182 // Make sure to add pwd to relative paths 183 src = filepath.Join(req.Pwd, src) 184 } 185 // Make sure we're using "/" on Windows. URLs are "/"-based. 186 req.Src = filepath.ToSlash(src) 187 return true, nil 188 } 189 190 return false, nil 191 } 192 193 func (g *HgGetter) validScheme(scheme string) bool { 194 return scheme == "hg" 195 } 196 197 func fixWindowsDrivePath(u *url.URL) bool { 198 // hg assumes a file:/// prefix for Windows drive letter file paths. 199 // (e.g. file:///c:/foo/bar) 200 // If the URL Path does not begin with a '/' character, the resulting URL 201 // path will have a file:// prefix. (e.g. file://c:/foo/bar) 202 // See http://www.selenic.com/hg/help/urls and the examples listed in 203 // http://selenic.com/repo/hg-stable/file/1265a3a71d75/mercurial/util.py#l1936 204 return runtime.GOOS == "windows" && u.Scheme == "file" && 205 len(u.Path) > 1 && u.Path[0] != '/' && u.Path[1] == ':' 206 }