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  }