github.com/x-motemen/ghq@v1.6.1/vcs.go (about)

     1  package main
     2  
     3  import (
     4  	"bytes"
     5  	"errors"
     6  	"fmt"
     7  	"io"
     8  	"net/url"
     9  	"os"
    10  	"os/exec"
    11  	"path/filepath"
    12  	"regexp"
    13  	"strings"
    14  
    15  	"github.com/x-motemen/ghq/cmdutil"
    16  )
    17  
    18  func run(silent bool) func(command string, args ...string) error {
    19  	if silent {
    20  		return cmdutil.RunSilently
    21  	}
    22  	return cmdutil.Run
    23  }
    24  
    25  func runInDir(silent bool) func(dir, command string, args ...string) error {
    26  	if silent {
    27  		return cmdutil.RunInDirSilently
    28  	}
    29  	return cmdutil.RunInDir
    30  }
    31  
    32  // A VCSBackend represents a VCS backend.
    33  type VCSBackend struct {
    34  	// Clones a remote repository to local path.
    35  	Clone func(*vcsGetOption) error
    36  	// Updates a cloned local repository.
    37  	Update func(*vcsGetOption) error
    38  	Init   func(dir string) error
    39  	// Returns VCS specific files
    40  	Contents []string
    41  }
    42  
    43  type vcsGetOption struct {
    44  	url                              *url.URL
    45  	dir                              string
    46  	recursive, shallow, silent, bare bool
    47  	branch                           string
    48  }
    49  
    50  // GitBackend is the VCSBackend of git
    51  var GitBackend = &VCSBackend{
    52  	// support submodules?
    53  	Clone: func(vg *vcsGetOption) error {
    54  		dir, _ := filepath.Split(vg.dir)
    55  		err := os.MkdirAll(dir, 0755)
    56  		if err != nil {
    57  			return err
    58  		}
    59  
    60  		args := []string{"clone"}
    61  		if vg.shallow {
    62  			args = append(args, "--depth", "1")
    63  		}
    64  		if vg.branch != "" {
    65  			args = append(args, "--branch", vg.branch, "--single-branch")
    66  		}
    67  		if vg.recursive {
    68  			args = append(args, "--recursive")
    69  		}
    70  		if vg.bare {
    71  			args = append(args, "--bare")
    72  		}
    73  		args = append(args, vg.url.String(), vg.dir)
    74  
    75  		return run(vg.silent)("git", args...)
    76  	},
    77  	Update: func(vg *vcsGetOption) error {
    78  		if _, err := os.Stat(filepath.Join(vg.dir, ".git/svn")); err == nil {
    79  			return GitsvnBackend.Update(vg)
    80  		}
    81  		if vg.bare {
    82  			return runInDir(true)(vg.dir, "git", "fetch", vg.url.String(), "*:*")
    83  		}
    84  		err := runInDir(true)(vg.dir, "git", "rev-parse", "@{upstream}")
    85  		if err != nil {
    86  			err := runInDir(vg.silent)(vg.dir, "git", "fetch")
    87  			if err != nil {
    88  				return err
    89  			}
    90  			return nil
    91  		}
    92  		err = runInDir(vg.silent)(vg.dir, "git", "pull", "--ff-only")
    93  		if err != nil {
    94  			return err
    95  		}
    96  		if vg.recursive {
    97  			return runInDir(vg.silent)(vg.dir, "git", "submodule", "update", "--init", "--recursive")
    98  		}
    99  		return nil
   100  	},
   101  	Init: func(dir string) error {
   102  		args := []string{"init"}
   103  		if strings.HasSuffix(dir, ".git") {
   104  			args = append(args, "--bare")
   105  		}
   106  		return cmdutil.RunInDir(dir, "git", args...)
   107  	},
   108  	Contents: []string{".git"},
   109  }
   110  
   111  /*
   112  If the svn target is under standard svn directory structure, "ghq" canonicalizes the checkout path.
   113  For example, all following targets are checked-out into `$(ghq root)/svn.example.com/proj/repo`.
   114  
   115  - svn.example.com/proj/repo
   116  - svn.example.com/proj/repo/trunk
   117  - svn.example.com/proj/repo/branches/featureN
   118  - svn.example.com/proj/repo/tags/v1.0.1
   119  
   120  Addition, when the svn target may be project root, "ghq" tries to checkout "/trunk".
   121  
   122  This checkout rule is also applied when using "git-svn".
   123  */
   124  
   125  const trunk = "/trunk"
   126  
   127  var svnReg = regexp.MustCompile(`/(?:tags|branches)/[^/]+$`)
   128  
   129  func replaceOnce(reg *regexp.Regexp, str, replace string) string {
   130  	replaced := false
   131  	return reg.ReplaceAllStringFunc(str, func(match string) string {
   132  		if replaced {
   133  			return match
   134  		}
   135  		replaced = true
   136  		return reg.ReplaceAllString(match, replace)
   137  	})
   138  }
   139  
   140  func svnBase(p string) string {
   141  	if strings.HasSuffix(p, trunk) {
   142  		return strings.TrimSuffix(p, trunk)
   143  	}
   144  	return replaceOnce(svnReg, p, "")
   145  }
   146  
   147  // SubversionBackend is the VCSBackend for subversion
   148  var SubversionBackend = &VCSBackend{
   149  	Clone: func(vg *vcsGetOption) error {
   150  		vg.dir = svnBase(vg.dir)
   151  		dir, _ := filepath.Split(vg.dir)
   152  		err := os.MkdirAll(dir, 0755)
   153  		if err != nil {
   154  			return err
   155  		}
   156  
   157  		args := []string{"checkout"}
   158  		if vg.shallow {
   159  			args = append(args, "--depth", "immediates")
   160  		}
   161  		remote := vg.url
   162  		if vg.branch != "" {
   163  			copied := *vg.url
   164  			remote = &copied
   165  			remote.Path = svnBase(remote.Path)
   166  			remote.Path += "/branches/" + url.PathEscape(vg.branch)
   167  		} else if !strings.HasSuffix(remote.Path, trunk) {
   168  			copied := *vg.url
   169  			copied.Path += trunk
   170  			if err := cmdutil.RunSilently("svn", "info", copied.String()); err == nil {
   171  				remote = &copied
   172  			}
   173  		}
   174  		args = append(args, remote.String(), vg.dir)
   175  
   176  		return run(vg.silent)("svn", args...)
   177  	},
   178  	Update: func(vg *vcsGetOption) error {
   179  		return runInDir(vg.silent)(vg.dir, "svn", "update")
   180  	},
   181  	Contents: []string{".svn"},
   182  }
   183  
   184  var svnLastRevReg = regexp.MustCompile(`(?m)^Last Changed Rev: (\d+)$`)
   185  
   186  // GitsvnBackend is the VCSBackend for git-svn
   187  var GitsvnBackend = &VCSBackend{
   188  	Clone: func(vg *vcsGetOption) error {
   189  		orig := vg.dir
   190  		vg.dir = svnBase(vg.dir)
   191  		standard := orig == vg.dir
   192  
   193  		dir, _ := filepath.Split(vg.dir)
   194  		err := os.MkdirAll(dir, 0755)
   195  		if err != nil {
   196  			return err
   197  		}
   198  
   199  		var getSvnInfo = func(u string) (string, error) {
   200  			buf := &bytes.Buffer{}
   201  			cmd := exec.Command("svn", "info", u)
   202  			cmd.Stdout = buf
   203  			cmd.Stderr = io.Discard
   204  			err := cmdutil.RunCommand(cmd, true)
   205  			return buf.String(), err
   206  		}
   207  		var svnInfo string
   208  		args := []string{"svn", "clone"}
   209  		remote := vg.url
   210  		if vg.branch != "" {
   211  			copied := *remote
   212  			remote = &copied
   213  			remote.Path = svnBase(remote.Path)
   214  			remote.Path += "/branches/" + url.PathEscape(vg.branch)
   215  			standard = false
   216  		} else if standard {
   217  			copied := *remote
   218  			copied.Path += trunk
   219  			info, err := getSvnInfo(copied.String())
   220  			if err == nil {
   221  				args = append(args, "-s")
   222  				svnInfo = info
   223  			} else {
   224  				standard = false
   225  			}
   226  		}
   227  
   228  		if vg.shallow {
   229  			if svnInfo == "" {
   230  				info, err := getSvnInfo(remote.String())
   231  				if err != nil {
   232  					return err
   233  				}
   234  				svnInfo = info
   235  			}
   236  			m := svnLastRevReg.FindStringSubmatch(svnInfo)
   237  			if len(m) < 2 {
   238  				return fmt.Errorf("no revisions are taken from svn info output: %s", svnInfo)
   239  			}
   240  			args = append(args, fmt.Sprintf("-r%s:HEAD", m[1]))
   241  		}
   242  		args = append(args, remote.String(), vg.dir)
   243  		return run(vg.silent)("git", args...)
   244  	},
   245  	Update: func(vg *vcsGetOption) error {
   246  		return runInDir(vg.silent)(vg.dir, "git", "svn", "rebase")
   247  	},
   248  	Contents: []string{".git/svn"},
   249  }
   250  
   251  // MercurialBackend is the VCSBackend for mercurial
   252  var MercurialBackend = &VCSBackend{
   253  	// Mercurial seems not supporting shallow clone currently.
   254  	Clone: func(vg *vcsGetOption) error {
   255  		dir, _ := filepath.Split(vg.dir)
   256  		err := os.MkdirAll(dir, 0755)
   257  		if err != nil {
   258  			return err
   259  		}
   260  		args := []string{"clone"}
   261  		if vg.branch != "" {
   262  			args = append(args, "--branch", vg.branch)
   263  		}
   264  		args = append(args, vg.url.String(), vg.dir)
   265  
   266  		return run(vg.silent)("hg", args...)
   267  	},
   268  	Update: func(vg *vcsGetOption) error {
   269  		return runInDir(vg.silent)(vg.dir, "hg", "pull", "--update")
   270  	},
   271  	Init: func(dir string) error {
   272  		return cmdutil.RunInDir(dir, "hg", "init")
   273  	},
   274  	Contents: []string{".hg"},
   275  }
   276  
   277  // DarcsBackend is the VCSBackend for darcs
   278  var DarcsBackend = &VCSBackend{
   279  	Clone: func(vg *vcsGetOption) error {
   280  		if vg.branch != "" {
   281  			return errors.New("darcs does not support branch")
   282  		}
   283  
   284  		dir, _ := filepath.Split(vg.dir)
   285  		err := os.MkdirAll(dir, 0755)
   286  		if err != nil {
   287  			return err
   288  		}
   289  
   290  		args := []string{"get"}
   291  		if vg.shallow {
   292  			args = append(args, "--lazy")
   293  		}
   294  		args = append(args, vg.url.String(), vg.dir)
   295  
   296  		return run(vg.silent)("darcs", args...)
   297  	},
   298  	Update: func(vg *vcsGetOption) error {
   299  		return runInDir(vg.silent)(vg.dir, "darcs", "pull")
   300  	},
   301  	Init: func(dir string) error {
   302  		return cmdutil.RunInDir(dir, "darcs", "init")
   303  	},
   304  	Contents: []string{"_darcs"},
   305  }
   306  
   307  // PijulBackend is the VCSBackend for pijul
   308  var PijulBackend = &VCSBackend{
   309  	Clone: func(vg *vcsGetOption) error {
   310  		dir, _ := filepath.Split(vg.dir)
   311  		err := os.MkdirAll(dir, 0755)
   312  		if err != nil {
   313  			return err
   314  		}
   315  
   316  		args := []string{"clone"}
   317  		if vg.branch != "" {
   318  			args = append(args, "--channel", vg.branch)
   319  		}
   320  		args = append(args, vg.url.String(), vg.dir)
   321  
   322  		return run(vg.silent)("pijul", args...)
   323  	},
   324  	Update: func(vg *vcsGetOption) error {
   325  		return runInDir(vg.silent)(vg.dir, "pijul", "pull")
   326  	},
   327  	Init: func(dir string) error {
   328  		return cmdutil.RunInDir(dir, "pijul", "init")
   329  	},
   330  	Contents: []string{".pijul"},
   331  }
   332  
   333  var cvsDummyBackend = &VCSBackend{
   334  	Clone: func(vg *vcsGetOption) error {
   335  		return errors.New("CVS clone is not supported")
   336  	},
   337  	Update: func(vg *vcsGetOption) error {
   338  		return errors.New("CVS update is not supported")
   339  	},
   340  	Contents: []string{"CVS/Repository"},
   341  }
   342  
   343  const fossilRepoName = ".fossil" // same as Go
   344  
   345  // FossilBackend is the VCSBackend for fossil
   346  var FossilBackend = &VCSBackend{
   347  	Clone: func(vg *vcsGetOption) error {
   348  		if vg.branch != "" {
   349  			return errors.New("fossil does not support cloning specific branch")
   350  		}
   351  		if err := os.MkdirAll(vg.dir, 0755); err != nil {
   352  			return err
   353  		}
   354  
   355  		if err := run(vg.silent)("fossil", "clone", vg.url.String(), filepath.Join(vg.dir, fossilRepoName)); err != nil {
   356  			return err
   357  		}
   358  		return runInDir(vg.silent)(vg.dir, "fossil", "open", fossilRepoName)
   359  	},
   360  	Update: func(vg *vcsGetOption) error {
   361  		return runInDir(vg.silent)(vg.dir, "fossil", "update")
   362  	},
   363  	Init: func(dir string) error {
   364  		if err := cmdutil.RunInDir(dir, "fossil", "init", fossilRepoName); err != nil {
   365  			return err
   366  		}
   367  		return cmdutil.RunInDir(dir, "fossil", "open", fossilRepoName)
   368  	},
   369  	Contents: []string{".fslckout", "_FOSSIL_"},
   370  }
   371  
   372  // BazaarBackend is the VCSBackend for bazaar
   373  var BazaarBackend = &VCSBackend{
   374  	// bazaar seems not supporting shallow clone currently.
   375  	Clone: func(vg *vcsGetOption) error {
   376  		if vg.branch != "" {
   377  			return errors.New("--branch option is unavailable for Bazaar since branch is included in remote URL")
   378  		}
   379  		dir, _ := filepath.Split(vg.dir)
   380  		err := os.MkdirAll(dir, 0755)
   381  		if err != nil {
   382  			return err
   383  		}
   384  		return run(vg.silent)("bzr", "branch", vg.url.String(), vg.dir)
   385  	},
   386  	Update: func(vg *vcsGetOption) error {
   387  		// Without --overwrite bzr will not pull tags that changed.
   388  		return runInDir(vg.silent)(vg.dir, "bzr", "pull", "--overwrite")
   389  	},
   390  	Init: func(dir string) error {
   391  		return cmdutil.RunInDir(dir, "bzr", "init")
   392  	},
   393  	Contents: []string{".bzr"},
   394  }
   395  
   396  var vcsRegistry = map[string]*VCSBackend{
   397  	"git":        GitBackend,
   398  	"github":     GitBackend,
   399  	"codecommit": GitBackend,
   400  	"svn":        SubversionBackend,
   401  	"subversion": SubversionBackend,
   402  	"git-svn":    GitsvnBackend,
   403  	"hg":         MercurialBackend,
   404  	"mercurial":  MercurialBackend,
   405  	"darcs":      DarcsBackend,
   406  	"pijul":      PijulBackend,
   407  	"fossil":     FossilBackend,
   408  	"bzr":        BazaarBackend,
   409  	"bazaar":     BazaarBackend,
   410  }