github.com/sdboyer/gps@v0.16.3/vcs_repo.go (about)

     1  package gps
     2  
     3  import (
     4  	"context"
     5  	"encoding/xml"
     6  	"os"
     7  	"path/filepath"
     8  	"runtime"
     9  	"strings"
    10  	"time"
    11  
    12  	"github.com/Masterminds/vcs"
    13  )
    14  
    15  type ctxRepo interface {
    16  	vcs.Repo
    17  	get(context.Context) error
    18  	fetch(context.Context) error
    19  	updateVersion(context.Context, string) error
    20  	//ping(context.Context) (bool, error)
    21  }
    22  
    23  // original implementation of these methods come from
    24  // https://github.com/Masterminds/vcs
    25  
    26  type gitRepo struct {
    27  	*vcs.GitRepo
    28  }
    29  
    30  func newVcsRemoteErrorOr(msg string, err error, out string) error {
    31  	if err == context.Canceled || err == context.DeadlineExceeded {
    32  		return err
    33  	}
    34  	return vcs.NewRemoteError(msg, err, out)
    35  }
    36  
    37  func newVcsLocalErrorOr(msg string, err error, out string) error {
    38  	if err == context.Canceled || err == context.DeadlineExceeded {
    39  		return err
    40  	}
    41  	return vcs.NewLocalError(msg, err, out)
    42  }
    43  
    44  func (r *gitRepo) get(ctx context.Context) error {
    45  	out, err := runFromCwd(ctx, "git", "clone", "--recursive", r.Remote(), r.LocalPath())
    46  	if err != nil {
    47  		return newVcsRemoteErrorOr("unable to get repository", err, string(out))
    48  	}
    49  
    50  	return nil
    51  }
    52  
    53  func (r *gitRepo) fetch(ctx context.Context) error {
    54  	// Perform a fetch to make sure everything is up to date.
    55  	out, err := runFromRepoDir(ctx, r, "git", "fetch", "--tags", "--prune", r.RemoteLocation)
    56  	if err != nil {
    57  		return newVcsRemoteErrorOr("unable to update repository", err, string(out))
    58  	}
    59  	return nil
    60  }
    61  
    62  func (r *gitRepo) updateVersion(ctx context.Context, v string) error {
    63  	out, err := runFromRepoDir(ctx, r, "git", "checkout", v)
    64  	if err != nil {
    65  		return newVcsLocalErrorOr("Unable to update checked out version", err, string(out))
    66  	}
    67  
    68  	return r.defendAgainstSubmodules(ctx)
    69  }
    70  
    71  // defendAgainstSubmodules tries to keep repo state sane in the event of
    72  // submodules. Or nested submodules. What a great idea, submodules.
    73  func (r *gitRepo) defendAgainstSubmodules(ctx context.Context) error {
    74  	// First, update them to whatever they should be, if there should happen to be any.
    75  	out, err := runFromRepoDir(ctx, r, "git", "submodule", "update", "--init", "--recursive")
    76  	if err != nil {
    77  		return newVcsLocalErrorOr("unexpected error while defensively updating submodules", err, string(out))
    78  	}
    79  
    80  	// Now, do a special extra-aggressive clean in case changing versions caused
    81  	// one or more submodules to go away.
    82  	out, err = runFromRepoDir(ctx, r, "git", "clean", "-x", "-d", "-f", "-f")
    83  	if err != nil {
    84  		return newVcsLocalErrorOr("unexpected error while defensively cleaning up after possible derelict submodule directories", err, string(out))
    85  	}
    86  
    87  	// Then, repeat just in case there are any nested submodules that went away.
    88  	out, err = runFromRepoDir(ctx, r, "git", "submodule", "foreach", "--recursive", "git", "clean", "-x", "-d", "-f", "-f")
    89  	if err != nil {
    90  		return newVcsLocalErrorOr("unexpected error while defensively cleaning up after possible derelict nested submodule directories", err, string(out))
    91  	}
    92  
    93  	return nil
    94  }
    95  
    96  type bzrRepo struct {
    97  	*vcs.BzrRepo
    98  }
    99  
   100  func (r *bzrRepo) get(ctx context.Context) error {
   101  	basePath := filepath.Dir(filepath.FromSlash(r.LocalPath()))
   102  	if _, err := os.Stat(basePath); os.IsNotExist(err) {
   103  		err = os.MkdirAll(basePath, 0755)
   104  		if err != nil {
   105  			return newVcsLocalErrorOr("unable to create directory", err, "")
   106  		}
   107  	}
   108  
   109  	out, err := runFromCwd(ctx, "bzr", "branch", r.Remote(), r.LocalPath())
   110  	if err != nil {
   111  		return newVcsRemoteErrorOr("unable to get repository", err, string(out))
   112  	}
   113  
   114  	return nil
   115  }
   116  
   117  func (r *bzrRepo) fetch(ctx context.Context) error {
   118  	out, err := runFromRepoDir(ctx, r, "bzr", "pull")
   119  	if err != nil {
   120  		return newVcsRemoteErrorOr("unable to update repository", err, string(out))
   121  	}
   122  	return nil
   123  }
   124  
   125  func (r *bzrRepo) updateVersion(ctx context.Context, version string) error {
   126  	out, err := runFromRepoDir(ctx, r, "bzr", "update", "-r", version)
   127  	if err != nil {
   128  		return newVcsLocalErrorOr("unable to update checked out version", err, string(out))
   129  	}
   130  	return nil
   131  }
   132  
   133  type hgRepo struct {
   134  	*vcs.HgRepo
   135  }
   136  
   137  func (r *hgRepo) get(ctx context.Context) error {
   138  	out, err := runFromCwd(ctx, "hg", "clone", r.Remote(), r.LocalPath())
   139  	if err != nil {
   140  		return newVcsRemoteErrorOr("unable to get repository", err, string(out))
   141  	}
   142  
   143  	return nil
   144  }
   145  
   146  func (r *hgRepo) fetch(ctx context.Context) error {
   147  	out, err := runFromRepoDir(ctx, r, "hg", "pull")
   148  	if err != nil {
   149  		return newVcsRemoteErrorOr("unable to fetch latest changes", err, string(out))
   150  	}
   151  	return nil
   152  }
   153  
   154  func (r *hgRepo) updateVersion(ctx context.Context, version string) error {
   155  	out, err := runFromRepoDir(ctx, r, "hg", "update", version)
   156  	if err != nil {
   157  		return newVcsRemoteErrorOr("unable to update checked out version", err, string(out))
   158  	}
   159  
   160  	return nil
   161  }
   162  
   163  type svnRepo struct {
   164  	*vcs.SvnRepo
   165  }
   166  
   167  func (r *svnRepo) get(ctx context.Context) error {
   168  	remote := r.Remote()
   169  	if strings.HasPrefix(remote, "/") {
   170  		remote = "file://" + remote
   171  	} else if runtime.GOOS == "windows" && filepath.VolumeName(remote) != "" {
   172  		remote = "file:///" + remote
   173  	}
   174  
   175  	out, err := runFromCwd(ctx, "svn", "checkout", remote, r.LocalPath())
   176  	if err != nil {
   177  		return newVcsRemoteErrorOr("unable to get repository", err, string(out))
   178  	}
   179  
   180  	return nil
   181  }
   182  
   183  func (r *svnRepo) update(ctx context.Context) error {
   184  	out, err := runFromRepoDir(ctx, r, "svn", "update")
   185  	if err != nil {
   186  		return newVcsRemoteErrorOr("unable to update repository", err, string(out))
   187  	}
   188  
   189  	return err
   190  }
   191  
   192  func (r *svnRepo) updateVersion(ctx context.Context, version string) error {
   193  	out, err := runFromRepoDir(ctx, r, "svn", "update", "-r", version)
   194  	if err != nil {
   195  		return newVcsRemoteErrorOr("unable to update checked out version", err, string(out))
   196  	}
   197  
   198  	return nil
   199  }
   200  
   201  func (r *svnRepo) CommitInfo(id string) (*vcs.CommitInfo, error) {
   202  	ctx := context.TODO()
   203  	// There are cases where Svn log doesn't return anything for HEAD or BASE.
   204  	// svn info does provide details for these but does not have elements like
   205  	// the commit message.
   206  	if id == "HEAD" || id == "BASE" {
   207  		type commit struct {
   208  			Revision string `xml:"revision,attr"`
   209  		}
   210  
   211  		type info struct {
   212  			Commit commit `xml:"entry>commit"`
   213  		}
   214  
   215  		out, err := runFromRepoDir(ctx, r, "svn", "info", "-r", id, "--xml")
   216  		if err != nil {
   217  			return nil, newVcsLocalErrorOr("unable to retrieve commit information", err, string(out))
   218  		}
   219  
   220  		infos := new(info)
   221  		err = xml.Unmarshal(out, &infos)
   222  		if err != nil {
   223  			return nil, newVcsLocalErrorOr("unable to retrieve commit information", err, string(out))
   224  		}
   225  
   226  		id = infos.Commit.Revision
   227  		if id == "" {
   228  			return nil, vcs.ErrRevisionUnavailable
   229  		}
   230  	}
   231  
   232  	out, err := runFromRepoDir(ctx, r, "svn", "log", "-r", id, "--xml")
   233  	if err != nil {
   234  		return nil, newVcsRemoteErrorOr("unable to retrieve commit information", err, string(out))
   235  	}
   236  
   237  	type logentry struct {
   238  		Author string `xml:"author"`
   239  		Date   string `xml:"date"`
   240  		Msg    string `xml:"msg"`
   241  	}
   242  
   243  	type log struct {
   244  		XMLName xml.Name   `xml:"log"`
   245  		Logs    []logentry `xml:"logentry"`
   246  	}
   247  
   248  	logs := new(log)
   249  	err = xml.Unmarshal(out, &logs)
   250  	if err != nil {
   251  		return nil, newVcsLocalErrorOr("unable to retrieve commit information", err, string(out))
   252  	}
   253  
   254  	if len(logs.Logs) == 0 {
   255  		return nil, vcs.ErrRevisionUnavailable
   256  	}
   257  
   258  	ci := &vcs.CommitInfo{
   259  		Commit:  id,
   260  		Author:  logs.Logs[0].Author,
   261  		Message: logs.Logs[0].Msg,
   262  	}
   263  
   264  	if len(logs.Logs[0].Date) > 0 {
   265  		ci.Date, err = time.Parse(time.RFC3339Nano, logs.Logs[0].Date)
   266  		if err != nil {
   267  			return nil, newVcsLocalErrorOr("unable to retrieve commit information", err, string(out))
   268  		}
   269  	}
   270  
   271  	return ci, nil
   272  }