github.com/golang/dep@v0.5.4/gps/vcs_repo.go (about) 1 // Copyright 2017 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package gps 6 7 import ( 8 "bytes" 9 "context" 10 "encoding/xml" 11 "os" 12 "path/filepath" 13 "runtime" 14 "strings" 15 "time" 16 17 "github.com/Masterminds/vcs" 18 "github.com/pkg/errors" 19 ) 20 21 type ctxRepo interface { 22 vcs.Repo 23 get(context.Context) error 24 fetch(context.Context) error 25 updateVersion(context.Context, string) error 26 //ping(context.Context) (bool, error) 27 } 28 29 // ensureCleaner is an optional extension of ctxRepo. 30 type ensureCleaner interface { 31 // ensureClean ensures a repository is clean and in working order, 32 // or returns an error if the adaptive recovery attempts fail. 33 ensureClean(context.Context) error 34 } 35 36 // original implementation of these methods come from 37 // https://github.com/Masterminds/vcs 38 39 type gitRepo struct { 40 *vcs.GitRepo 41 } 42 43 func newVcsRemoteErrorOr(err error, args []string, out, msg string) error { 44 if err == context.Canceled || err == context.DeadlineExceeded { 45 return err 46 } 47 return vcs.NewRemoteError(msg, errors.Wrapf(err, "command failed: %v", args), out) 48 } 49 50 func newVcsLocalErrorOr(err error, args []string, out, msg string) error { 51 if err == context.Canceled || err == context.DeadlineExceeded { 52 return err 53 } 54 return vcs.NewLocalError(msg, errors.Wrapf(err, "command failed: %v", args), out) 55 } 56 57 func (r *gitRepo) get(ctx context.Context) error { 58 cmd := commandContext( 59 ctx, 60 "git", 61 "clone", 62 "--recursive", 63 "-v", 64 "--progress", 65 r.Remote(), 66 r.LocalPath(), 67 ) 68 // Ensure no prompting for PWs 69 cmd.SetEnv(append([]string{"GIT_ASKPASS=", "GIT_TERMINAL_PROMPT=0"}, os.Environ()...)) 70 if out, err := cmd.CombinedOutput(); err != nil { 71 return newVcsRemoteErrorOr(err, cmd.Args(), string(out), 72 "unable to get repository") 73 } 74 75 return nil 76 } 77 78 func (r *gitRepo) fetch(ctx context.Context) error { 79 cmd := commandContext( 80 ctx, 81 "git", 82 "fetch", 83 "--tags", 84 "--prune", 85 r.RemoteLocation, 86 ) 87 cmd.SetDir(r.LocalPath()) 88 // Ensure no prompting for PWs 89 cmd.SetEnv(append([]string{"GIT_ASKPASS=", "GIT_TERMINAL_PROMPT=0"}, os.Environ()...)) 90 if out, err := cmd.CombinedOutput(); err != nil { 91 return newVcsRemoteErrorOr(err, cmd.Args(), string(out), 92 "unable to update repository") 93 } 94 return nil 95 } 96 97 func (r *gitRepo) updateVersion(ctx context.Context, v string) error { 98 cmd := commandContext(ctx, "git", "checkout", v) 99 cmd.SetDir(r.LocalPath()) 100 if out, err := cmd.CombinedOutput(); err != nil { 101 return newVcsLocalErrorOr(err, cmd.Args(), string(out), 102 "unable to update checked out version") 103 } 104 105 return r.defendAgainstSubmodules(ctx) 106 } 107 108 // defendAgainstSubmodules tries to keep repo state sane in the event of 109 // submodules. Or nested submodules. What a great idea, submodules. 110 func (r *gitRepo) defendAgainstSubmodules(ctx context.Context) error { 111 // First, update them to whatever they should be, if there should happen to be any. 112 { 113 cmd := commandContext( 114 ctx, 115 "git", 116 "submodule", 117 "update", 118 "--init", 119 "--recursive", 120 ) 121 cmd.SetDir(r.LocalPath()) 122 // Ensure no prompting for PWs 123 cmd.SetEnv(append([]string{"GIT_ASKPASS=", "GIT_TERMINAL_PROMPT=0"}, os.Environ()...)) 124 if out, err := cmd.CombinedOutput(); err != nil { 125 return newVcsLocalErrorOr(err, cmd.Args(), string(out), 126 "unexpected error while defensively updating submodules") 127 } 128 } 129 130 // Now, do a special extra-aggressive clean in case changing versions caused 131 // one or more submodules to go away. 132 { 133 cmd := commandContext(ctx, "git", "clean", "-x", "-d", "-f", "-f") 134 cmd.SetDir(r.LocalPath()) 135 if out, err := cmd.CombinedOutput(); err != nil { 136 return newVcsLocalErrorOr(err, cmd.Args(), string(out), 137 "unexpected error while defensively cleaning up after possible derelict submodule directories") 138 } 139 } 140 141 // Then, repeat just in case there are any nested submodules that went away. 142 { 143 cmd := commandContext( 144 ctx, 145 "git", 146 "submodule", 147 "foreach", 148 "--recursive", 149 "git clean -x -d -f -f", 150 ) 151 cmd.SetDir(r.LocalPath()) 152 if out, err := cmd.CombinedOutput(); err != nil { 153 return newVcsLocalErrorOr(err, cmd.Args(), string(out), 154 "unexpected error while defensively cleaning up after possible derelict nested submodule directories") 155 } 156 } 157 158 return nil 159 } 160 161 func (r *gitRepo) ensureClean(ctx context.Context) error { 162 cmd := commandContext( 163 ctx, 164 "git", 165 "status", 166 "--porcelain", 167 ) 168 cmd.SetDir(r.LocalPath()) 169 170 out, err := cmd.CombinedOutput() 171 if err != nil { 172 // An error on simple git status indicates some aggressive repository 173 // corruption, outside of the purview that we can deal with here. 174 return err 175 } 176 177 if len(bytes.TrimSpace(out)) == 0 { 178 // No output from status indicates a clean tree, without any modified or 179 // untracked files - we're in good shape. 180 return nil 181 } 182 183 // We could be more parsimonious about this, but it's probably not worth it 184 // - it's a rare case to have to do any cleanup anyway, so when we do, we 185 // might as well just throw the kitchen sink at it. 186 cmd = commandContext( 187 ctx, 188 "git", 189 "reset", 190 "--hard", 191 ) 192 cmd.SetDir(r.LocalPath()) 193 _, err = cmd.CombinedOutput() 194 if err != nil { 195 return err 196 } 197 198 // We also need to git clean -df; just reuse defendAgainstSubmodules here, 199 // even though it's a bit layer-breaky. 200 err = r.defendAgainstSubmodules(ctx) 201 if err != nil { 202 return err 203 } 204 205 // Check status one last time. If it's still not clean, give up. 206 cmd = commandContext( 207 ctx, 208 "git", 209 "status", 210 "--porcelain", 211 ) 212 cmd.SetDir(r.LocalPath()) 213 214 out, err = cmd.CombinedOutput() 215 if err != nil { 216 return err 217 } 218 219 if len(bytes.TrimSpace(out)) != 0 { 220 return errors.Errorf("failed to clean up git repository at %s - dirty? corrupted? status output: \n%s", r.LocalPath(), string(out)) 221 } 222 223 return nil 224 } 225 226 type bzrRepo struct { 227 *vcs.BzrRepo 228 } 229 230 func (r *bzrRepo) get(ctx context.Context) error { 231 basePath := filepath.Dir(filepath.FromSlash(r.LocalPath())) 232 if _, err := os.Stat(basePath); os.IsNotExist(err) { 233 err = os.MkdirAll(basePath, 0755) 234 if err != nil { 235 return newVcsLocalErrorOr(err, nil, "", "unable to create directory") 236 } 237 } 238 239 cmd := commandContext(ctx, "bzr", "branch", r.Remote(), r.LocalPath()) 240 if out, err := cmd.CombinedOutput(); err != nil { 241 return newVcsRemoteErrorOr(err, cmd.Args(), string(out), 242 "unable to get repository") 243 } 244 245 return nil 246 } 247 248 func (r *bzrRepo) fetch(ctx context.Context) error { 249 cmd := commandContext(ctx, "bzr", "pull") 250 cmd.SetDir(r.LocalPath()) 251 if out, err := cmd.CombinedOutput(); err != nil { 252 return newVcsRemoteErrorOr(err, cmd.Args(), string(out), 253 "unable to update repository") 254 } 255 return nil 256 } 257 258 func (r *bzrRepo) updateVersion(ctx context.Context, version string) error { 259 cmd := commandContext(ctx, "bzr", "update", "-r", version) 260 cmd.SetDir(r.LocalPath()) 261 if out, err := cmd.CombinedOutput(); err != nil { 262 return newVcsLocalErrorOr(err, cmd.Args(), string(out), 263 "unable to update checked out version") 264 } 265 return nil 266 } 267 268 type hgRepo struct { 269 *vcs.HgRepo 270 } 271 272 func (r *hgRepo) get(ctx context.Context) error { 273 cmd := commandContext(ctx, "hg", "clone", r.Remote(), r.LocalPath()) 274 if out, err := cmd.CombinedOutput(); err != nil { 275 return newVcsRemoteErrorOr(err, cmd.Args(), string(out), 276 "unable to get repository") 277 } 278 279 return nil 280 } 281 282 func (r *hgRepo) fetch(ctx context.Context) error { 283 cmd := commandContext(ctx, "hg", "pull") 284 cmd.SetDir(r.LocalPath()) 285 if out, err := cmd.CombinedOutput(); err != nil { 286 return newVcsRemoteErrorOr(err, cmd.Args(), string(out), 287 "unable to fetch latest changes") 288 } 289 return nil 290 } 291 292 func (r *hgRepo) updateVersion(ctx context.Context, version string) error { 293 cmd := commandContext(ctx, "hg", "update", version) 294 cmd.SetDir(r.LocalPath()) 295 if out, err := cmd.CombinedOutput(); err != nil { 296 return newVcsRemoteErrorOr(err, cmd.Args(), string(out), 297 "unable to update checked out version") 298 } 299 300 return nil 301 } 302 303 type svnRepo struct { 304 *vcs.SvnRepo 305 } 306 307 func (r *svnRepo) get(ctx context.Context) error { 308 remote := r.Remote() 309 if strings.HasPrefix(remote, "/") { 310 remote = "file://" + remote 311 } else if runtime.GOOS == "windows" && filepath.VolumeName(remote) != "" { 312 remote = "file:///" + remote 313 } 314 315 cmd := commandContext(ctx, "svn", "checkout", remote, r.LocalPath()) 316 if out, err := cmd.CombinedOutput(); err != nil { 317 return newVcsRemoteErrorOr(err, cmd.Args(), string(out), 318 "unable to get repository") 319 } 320 321 return nil 322 } 323 324 func (r *svnRepo) fetch(ctx context.Context) error { 325 cmd := commandContext(ctx, "svn", "update") 326 cmd.SetDir(r.LocalPath()) 327 if out, err := cmd.CombinedOutput(); err != nil { 328 return newVcsRemoteErrorOr(err, cmd.Args(), string(out), 329 "unable to update repository") 330 } 331 332 return nil 333 } 334 335 func (r *svnRepo) updateVersion(ctx context.Context, version string) error { 336 cmd := commandContext(ctx, "svn", "update", "-r", version) 337 cmd.SetDir(r.LocalPath()) 338 if out, err := cmd.CombinedOutput(); err != nil { 339 return newVcsRemoteErrorOr(err, cmd.Args(), string(out), 340 "unable to update checked out version") 341 } 342 343 return nil 344 } 345 346 func (r *svnRepo) CommitInfo(id string) (*vcs.CommitInfo, error) { 347 ctx := context.TODO() 348 // There are cases where Svn log doesn't return anything for HEAD or BASE. 349 // svn info does provide details for these but does not have elements like 350 // the commit message. 351 if id == "HEAD" || id == "BASE" { 352 type commit struct { 353 Revision string `xml:"revision,attr"` 354 } 355 356 type info struct { 357 Commit commit `xml:"entry>commit"` 358 } 359 360 cmd := commandContext(ctx, "svn", "info", "-r", id, "--xml") 361 cmd.SetDir(r.LocalPath()) 362 out, err := cmd.CombinedOutput() 363 if err != nil { 364 return nil, newVcsLocalErrorOr(err, cmd.Args(), string(out), 365 "unable to retrieve commit information") 366 } 367 368 infos := new(info) 369 if err := xml.Unmarshal(out, &infos); err != nil { 370 return nil, newVcsLocalErrorOr(err, cmd.Args(), string(out), 371 "unable to retrieve commit information") 372 } 373 374 id = infos.Commit.Revision 375 if id == "" { 376 return nil, vcs.ErrRevisionUnavailable 377 } 378 } 379 380 cmd := commandContext(ctx, "svn", "log", "-r", id, "--xml") 381 cmd.SetDir(r.LocalPath()) 382 out, err := cmd.CombinedOutput() 383 if err != nil { 384 return nil, newVcsRemoteErrorOr(err, cmd.Args(), string(out), 385 "unable to retrieve commit information") 386 } 387 388 type logentry struct { 389 Author string `xml:"author"` 390 Date string `xml:"date"` 391 Msg string `xml:"msg"` 392 } 393 394 type log struct { 395 XMLName xml.Name `xml:"log"` 396 Logs []logentry `xml:"logentry"` 397 } 398 399 logs := new(log) 400 if err := xml.Unmarshal(out, &logs); err != nil { 401 return nil, newVcsLocalErrorOr(err, cmd.Args(), string(out), 402 "unable to retrieve commit information") 403 } 404 405 if len(logs.Logs) == 0 { 406 return nil, vcs.ErrRevisionUnavailable 407 } 408 409 ci := &vcs.CommitInfo{ 410 Commit: id, 411 Author: logs.Logs[0].Author, 412 Message: logs.Logs[0].Msg, 413 } 414 415 if len(logs.Logs[0].Date) > 0 { 416 ci.Date, err = time.Parse(time.RFC3339Nano, logs.Logs[0].Date) 417 if err != nil { 418 return nil, newVcsLocalErrorOr(err, cmd.Args(), string(out), 419 "unable to retrieve commit information") 420 } 421 } 422 423 return ci, nil 424 }