github.com/motemen/ghq@v1.0.3/vcs.go (about)

     1  package main
     2  
     3  import (
     4  	"bytes"
     5  	"errors"
     6  	"fmt"
     7  	"io/ioutil"
     8  	"net/url"
     9  	"os"
    10  	"os/exec"
    11  	"path/filepath"
    12  	"regexp"
    13  	"strings"
    14  
    15  	"github.com/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 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  		args = append(args, vg.url.String(), vg.dir)
    71  
    72  		return run(vg.silent)("git", args...)
    73  	},
    74  	Update: func(vg *vcsGetOption) error {
    75  		if _, err := os.Stat(filepath.Join(vg.dir, ".git/svn")); err == nil {
    76  			return GitsvnBackend.Update(vg)
    77  		}
    78  		err := runInDir(vg.silent)(vg.dir, "git", "pull", "--ff-only")
    79  		if err != nil {
    80  			return err
    81  		}
    82  		if vg.recursive {
    83  			return runInDir(vg.silent)(vg.dir, "git", "submodule", "update", "--init", "--recursive")
    84  		}
    85  		return nil
    86  	},
    87  	Init: func(dir string) error {
    88  		return cmdutil.RunInDir(dir, "git", "init")
    89  	},
    90  	Contents: []string{".git"},
    91  }
    92  
    93  /*
    94  If the svn target is under standard svn directory structure, "ghq" canonicalizes the checkout path.
    95  For example, all following targets are checked-out into `$(ghq root)/svn.example.com/proj/repo`.
    96  
    97  - svn.example.com/proj/repo
    98  - svn.example.com/proj/repo/trunk
    99  - svn.example.com/proj/repo/branches/featureN
   100  - svn.example.com/proj/repo/tags/v1.0.1
   101  
   102  Addition, when the svn target may be project root, "ghq" tries to checkout "/trunk".
   103  
   104  This checkout rule is also applied when using "git-svn".
   105  */
   106  
   107  const trunk = "/trunk"
   108  
   109  var svnReg = regexp.MustCompile(`/(?:tags|branches)/[^/]+$`)
   110  
   111  func replaceOnce(reg *regexp.Regexp, str, replace string) string {
   112  	replaced := false
   113  	return reg.ReplaceAllStringFunc(str, func(match string) string {
   114  		if replaced {
   115  			return match
   116  		}
   117  		replaced = true
   118  		return reg.ReplaceAllString(match, replace)
   119  	})
   120  }
   121  
   122  func svnBase(p string) string {
   123  	if strings.HasSuffix(p, trunk) {
   124  		return strings.TrimSuffix(p, trunk)
   125  	}
   126  	return replaceOnce(svnReg, p, "")
   127  }
   128  
   129  // SubversionBackend is the VCSBackend for subversion
   130  var SubversionBackend = &VCSBackend{
   131  	Clone: func(vg *vcsGetOption) error {
   132  		vg.dir = svnBase(vg.dir)
   133  		dir, _ := filepath.Split(vg.dir)
   134  		err := os.MkdirAll(dir, 0755)
   135  		if err != nil {
   136  			return err
   137  		}
   138  
   139  		args := []string{"checkout"}
   140  		if vg.shallow {
   141  			args = append(args, "--depth", "immediates")
   142  		}
   143  		remote := vg.url
   144  		if vg.branch != "" {
   145  			copied := *vg.url
   146  			remote = &copied
   147  			remote.Path = svnBase(remote.Path)
   148  			remote.Path += "/branches/" + url.PathEscape(vg.branch)
   149  		} else if !strings.HasSuffix(remote.Path, trunk) {
   150  			copied := *vg.url
   151  			copied.Path += trunk
   152  			if err := cmdutil.RunSilently("svn", "info", copied.String()); err == nil {
   153  				remote = &copied
   154  			}
   155  		}
   156  		args = append(args, remote.String(), vg.dir)
   157  
   158  		return run(vg.silent)("svn", args...)
   159  	},
   160  	Update: func(vg *vcsGetOption) error {
   161  		return runInDir(vg.silent)(vg.dir, "svn", "update")
   162  	},
   163  	Contents: []string{".svn"},
   164  }
   165  
   166  var svnLastRevReg = regexp.MustCompile(`(?m)^Last Changed Rev: (\d+)$`)
   167  
   168  // GitsvnBackend is the VCSBackend for git-svn
   169  var GitsvnBackend = &VCSBackend{
   170  	Clone: func(vg *vcsGetOption) error {
   171  		orig := vg.dir
   172  		vg.dir = svnBase(vg.dir)
   173  		standard := orig == vg.dir
   174  
   175  		dir, _ := filepath.Split(vg.dir)
   176  		err := os.MkdirAll(dir, 0755)
   177  		if err != nil {
   178  			return err
   179  		}
   180  
   181  		var getSvnInfo = func(u string) (string, error) {
   182  			buf := &bytes.Buffer{}
   183  			cmd := exec.Command("svn", "info", u)
   184  			cmd.Stdout = buf
   185  			cmd.Stderr = ioutil.Discard
   186  			err := cmdutil.RunCommand(cmd, true)
   187  			return buf.String(), err
   188  		}
   189  		var svnInfo string
   190  		args := []string{"svn", "clone"}
   191  		remote := vg.url
   192  		if vg.branch != "" {
   193  			copied := *remote
   194  			remote = &copied
   195  			remote.Path = svnBase(remote.Path)
   196  			remote.Path += "/branches/" + url.PathEscape(vg.branch)
   197  			standard = false
   198  		} else if standard {
   199  			copied := *remote
   200  			copied.Path += trunk
   201  			info, err := getSvnInfo(copied.String())
   202  			if err == nil {
   203  				args = append(args, "-s")
   204  				svnInfo = info
   205  			} else {
   206  				standard = false
   207  			}
   208  		}
   209  
   210  		if vg.shallow {
   211  			if svnInfo == "" {
   212  				info, err := getSvnInfo(remote.String())
   213  				if err != nil {
   214  					return err
   215  				}
   216  				svnInfo = info
   217  			}
   218  			m := svnLastRevReg.FindStringSubmatch(svnInfo)
   219  			if len(m) < 2 {
   220  				return fmt.Errorf("no revisions are taken from svn info output: %s", svnInfo)
   221  			}
   222  			args = append(args, fmt.Sprintf("-r%s:HEAD", m[1]))
   223  		}
   224  		args = append(args, remote.String(), vg.dir)
   225  		return run(vg.silent)("git", args...)
   226  	},
   227  	Update: func(vg *vcsGetOption) error {
   228  		return runInDir(vg.silent)(vg.dir, "git", "svn", "rebase")
   229  	},
   230  	Contents: []string{".git/svn"},
   231  }
   232  
   233  // MercurialBackend is the VCSBackend for mercurial
   234  var MercurialBackend = &VCSBackend{
   235  	// Mercurial seems not supporting shallow clone currently.
   236  	Clone: func(vg *vcsGetOption) error {
   237  		dir, _ := filepath.Split(vg.dir)
   238  		err := os.MkdirAll(dir, 0755)
   239  		if err != nil {
   240  			return err
   241  		}
   242  		args := []string{"clone"}
   243  		if vg.branch != "" {
   244  			args = append(args, "--branch", vg.branch)
   245  		}
   246  		args = append(args, vg.url.String(), vg.dir)
   247  
   248  		return run(vg.silent)("hg", args...)
   249  	},
   250  	Update: func(vg *vcsGetOption) error {
   251  		return runInDir(vg.silent)(vg.dir, "hg", "pull", "--update")
   252  	},
   253  	Init: func(dir string) error {
   254  		return cmdutil.RunInDir(dir, "hg", "init")
   255  	},
   256  	Contents: []string{".hg"},
   257  }
   258  
   259  // DarcsBackend is the VCSBackend for darcs
   260  var DarcsBackend = &VCSBackend{
   261  	Clone: func(vg *vcsGetOption) error {
   262  		if vg.branch != "" {
   263  			return errors.New("Darcs does not support branch")
   264  		}
   265  
   266  		dir, _ := filepath.Split(vg.dir)
   267  		err := os.MkdirAll(dir, 0755)
   268  		if err != nil {
   269  			return err
   270  		}
   271  
   272  		args := []string{"get"}
   273  		if vg.shallow {
   274  			args = append(args, "--lazy")
   275  		}
   276  		args = append(args, vg.url.String(), vg.dir)
   277  
   278  		return run(vg.silent)("darcs", args...)
   279  	},
   280  	Update: func(vg *vcsGetOption) error {
   281  		return runInDir(vg.silent)(vg.dir, "darcs", "pull")
   282  	},
   283  	Init: func(dir string) error {
   284  		return cmdutil.RunInDir(dir, "darcs", "init")
   285  	},
   286  	Contents: []string{"_darcs"},
   287  }
   288  
   289  var cvsDummyBackend = &VCSBackend{
   290  	Clone: func(vg *vcsGetOption) error {
   291  		return errors.New("CVS clone is not supported")
   292  	},
   293  	Update: func(vg *vcsGetOption) error {
   294  		return errors.New("CVS update is not supported")
   295  	},
   296  	Contents: []string{"CVS/Repository"},
   297  }
   298  
   299  const fossilRepoName = ".fossil" // same as Go
   300  
   301  // FossilBackend is the VCSBackend for fossil
   302  var FossilBackend = &VCSBackend{
   303  	Clone: func(vg *vcsGetOption) error {
   304  		if vg.branch != "" {
   305  			return errors.New("Fossil does not support cloning specific branch")
   306  		}
   307  		if err := os.MkdirAll(vg.dir, 0755); err != nil {
   308  			return err
   309  		}
   310  
   311  		if err := run(vg.silent)("fossil", "clone", vg.url.String(), filepath.Join(vg.dir, fossilRepoName)); err != nil {
   312  			return err
   313  		}
   314  		return runInDir(vg.silent)(vg.dir, "fossil", "open", fossilRepoName)
   315  	},
   316  	Update: func(vg *vcsGetOption) error {
   317  		return runInDir(vg.silent)(vg.dir, "fossil", "update")
   318  	},
   319  	Init: func(dir string) error {
   320  		if err := cmdutil.RunInDir(dir, "fossil", "init", fossilRepoName); err != nil {
   321  			return err
   322  		}
   323  		return cmdutil.RunInDir(dir, "fossil", "open", fossilRepoName)
   324  	},
   325  	Contents: []string{".fslckout", "_FOSSIL_"},
   326  }
   327  
   328  // BazaarBackend is the VCSBackend for bazaar
   329  var BazaarBackend = &VCSBackend{
   330  	// bazaar seems not supporting shallow clone currently.
   331  	Clone: func(vg *vcsGetOption) error {
   332  		if vg.branch != "" {
   333  			return errors.New("--branch option is unavailable for Bazaar since branch is included in remote URL")
   334  		}
   335  		dir, _ := filepath.Split(vg.dir)
   336  		err := os.MkdirAll(dir, 0755)
   337  		if err != nil {
   338  			return err
   339  		}
   340  		return run(vg.silent)("bzr", "branch", vg.url.String(), vg.dir)
   341  	},
   342  	Update: func(vg *vcsGetOption) error {
   343  		// Without --overwrite bzr will not pull tags that changed.
   344  		return runInDir(vg.silent)(vg.dir, "bzr", "pull", "--overwrite")
   345  	},
   346  	Init: func(dir string) error {
   347  		return cmdutil.RunInDir(dir, "bzr", "init")
   348  	},
   349  	Contents: []string{".bzr"},
   350  }
   351  
   352  var vcsRegistry = map[string]*VCSBackend{
   353  	"git":        GitBackend,
   354  	"github":     GitBackend,
   355  	"svn":        SubversionBackend,
   356  	"subversion": SubversionBackend,
   357  	"git-svn":    GitsvnBackend,
   358  	"hg":         MercurialBackend,
   359  	"mercurial":  MercurialBackend,
   360  	"darcs":      DarcsBackend,
   361  	"fossil":     FossilBackend,
   362  	"bzr":        BazaarBackend,
   363  	"bazaar":     BazaarBackend,
   364  }