github.com/iwataka/ghq@v0.7.5-0.20160611155400-0aa07ac077a9/commands.go (about)

     1  package main
     2  
     3  import (
     4  	"bufio"
     5  	"fmt"
     6  	"io"
     7  	"os"
     8  	"os/exec"
     9  	"path/filepath"
    10  	"runtime"
    11  	"strings"
    12  	"syscall"
    13  
    14  	"github.com/codegangsta/cli"
    15  	"github.com/motemen/ghq/utils"
    16  )
    17  
    18  var Commands = []cli.Command{
    19  	commandGet,
    20  	commandList,
    21  	commandLook,
    22  	commandImport,
    23  	commandRoot,
    24  }
    25  
    26  var cloneFlags = []cli.Flag{
    27  	cli.BoolFlag{Name: "update, u", Usage: "Update local repository if cloned already"},
    28  	cli.BoolFlag{Name: "p", Usage: "Clone with SSH"},
    29  	cli.BoolFlag{Name: "shallow", Usage: "Do a shallow clone"},
    30  }
    31  
    32  var commandGet = cli.Command{
    33  	Name:  "get",
    34  	Usage: "Clone/sync with a remote repository",
    35  	Description: `
    36      Clone a GitHub repository under ghq root direcotry. If the repository is
    37      already cloned to local, nothing will happen unless '-u' ('--update')
    38      flag is supplied, in which case 'git remote update' is executed.
    39      When you use '-p' option, the repository is cloned via SSH.
    40  `,
    41  	Action: doGet,
    42  	Flags:  cloneFlags,
    43  }
    44  
    45  var commandList = cli.Command{
    46  	Name:  "list",
    47  	Usage: "List local repositories",
    48  	Description: `
    49      List locally cloned repositories. If a query argument is given, only
    50      repositories whose names contain that query text are listed. '-e'
    51      ('--exact') forces the match to be an exact one (i.e. the query equals to
    52      _project_ or _user_/_project_) If '-p' ('--full-path') is given, the full paths
    53      to the repository root are printed instead of relative ones.
    54  `,
    55  	Action: doList,
    56  	Flags: []cli.Flag{
    57  		cli.BoolFlag{Name: "exact, e", Usage: "Perform an exact match"},
    58  		cli.BoolFlag{Name: "full-path, p", Usage: "Print full paths"},
    59  		cli.BoolFlag{Name: "unique", Usage: "Print unique subpaths"},
    60  	},
    61  }
    62  
    63  var commandLook = cli.Command{
    64  	Name:  "look",
    65  	Usage: "Look into a local repository",
    66  	Description: `
    67      Look into a locally cloned repository with the shell.
    68  `,
    69  	Action: doLook,
    70  }
    71  
    72  var commandImport = cli.Command{
    73  	Name:   "import",
    74  	Usage:  "Bulk get repositories from stdin",
    75  	Action: doImport,
    76  	Flags:  cloneFlags,
    77  }
    78  
    79  var commandRoot = cli.Command{
    80  	Name:   "root",
    81  	Usage:  "Show repositories' root",
    82  	Action: doRoot,
    83  	Flags: []cli.Flag{
    84  		cli.BoolFlag{Name: "all", Usage: "Show all roots"},
    85  	},
    86  }
    87  
    88  type commandDoc struct {
    89  	Parent    string
    90  	Arguments string
    91  }
    92  
    93  var commandDocs = map[string]commandDoc{
    94  	"get":    {"", "[-u] <repository URL> | [-u] [-p] <user>/<project>"},
    95  	"list":   {"", "[-p] [-e] [<query>]"},
    96  	"look":   {"", "<project> | <user>/<project> | <host>/<user>/<project>"},
    97  	"import": {"", "< file"},
    98  	"root":   {"", ""},
    99  }
   100  
   101  // Makes template conditionals to generate per-command documents.
   102  func mkCommandsTemplate(genTemplate func(commandDoc) string) string {
   103  	template := "{{if false}}"
   104  	for _, command := range append(Commands) {
   105  		template = template + fmt.Sprintf("{{else if (eq .Name %q)}}%s", command.Name, genTemplate(commandDocs[command.Name]))
   106  	}
   107  	return template + "{{end}}"
   108  }
   109  
   110  func init() {
   111  	argsTemplate := mkCommandsTemplate(func(doc commandDoc) string { return doc.Arguments })
   112  	parentTemplate := mkCommandsTemplate(func(doc commandDoc) string { return string(strings.TrimLeft(doc.Parent+" ", " ")) })
   113  
   114  	cli.CommandHelpTemplate = `NAME:
   115      {{.Name}} - {{.Usage}}
   116  
   117  USAGE:
   118      ghq ` + parentTemplate + `{{.Name}} ` + argsTemplate + `
   119  {{if (len .Description)}}
   120  DESCRIPTION: {{.Description}}
   121  {{end}}{{if (len .Flags)}}
   122  OPTIONS:
   123      {{range .Flags}}{{.}}
   124      {{end}}
   125  {{end}}`
   126  }
   127  
   128  func doGet(c *cli.Context) error {
   129  	argURL := c.Args().Get(0)
   130  	doUpdate := c.Bool("update")
   131  	isShallow := c.Bool("shallow")
   132  
   133  	if argURL == "" {
   134  		cli.ShowCommandHelp(c, "get")
   135  		os.Exit(1)
   136  	}
   137  
   138  	// If argURL is a "./foo" or "../bar" form,
   139  	// find repository name trailing after github.com/USER/.
   140  	parts := strings.Split(argURL, string(filepath.Separator))
   141  	if parts[0] == "." || parts[0] == ".." {
   142  		if wd, err := os.Getwd(); err == nil {
   143  			path := filepath.Clean(filepath.Join(wd, filepath.Join(parts...)))
   144  
   145  			var repoPath string
   146  			for _, r := range localRepositoryRoots() {
   147  				p := strings.TrimPrefix(path, r+string(filepath.Separator))
   148  				if p != path && (repoPath == "" || len(p) < len(repoPath)) {
   149  					repoPath = p
   150  				}
   151  			}
   152  
   153  			if repoPath != "" {
   154  				// Guess it
   155  				utils.Log("resolved", fmt.Sprintf("relative %q to %q", argURL, "https://"+repoPath))
   156  				argURL = "https://" + repoPath
   157  			}
   158  		}
   159  	}
   160  
   161  	url, err := NewURL(argURL)
   162  	utils.DieIf(err)
   163  
   164  	isSSH := c.Bool("p")
   165  	if isSSH {
   166  		// Assume Git repository if `-p` is given.
   167  		url, err = ConvertGitURLHTTPToSSH(url)
   168  		utils.DieIf(err)
   169  	}
   170  
   171  	remote, err := NewRemoteRepository(url)
   172  	utils.DieIf(err)
   173  
   174  	if remote.IsValid() == false {
   175  		utils.Log("error", fmt.Sprintf("Not a valid repository: %s", url))
   176  		os.Exit(1)
   177  	}
   178  
   179  	getRemoteRepository(remote, doUpdate, isShallow)
   180  	return nil
   181  }
   182  
   183  // getRemoteRepository clones or updates a remote repository remote.
   184  // If doUpdate is true, updates the locally cloned repository. Otherwise does nothing.
   185  // If isShallow is true, does shallow cloning. (no effect if already cloned or the VCS is Mercurial and git-svn)
   186  func getRemoteRepository(remote RemoteRepository, doUpdate bool, isShallow bool) {
   187  	remoteURL := remote.URL()
   188  	local := LocalRepositoryFromURL(remoteURL)
   189  
   190  	path := local.FullPath
   191  	newPath := false
   192  
   193  	_, err := os.Stat(path)
   194  	if err != nil {
   195  		if os.IsNotExist(err) {
   196  			newPath = true
   197  			err = nil
   198  		}
   199  		utils.PanicIf(err)
   200  	}
   201  
   202  	if newPath {
   203  		utils.Log("clone", fmt.Sprintf("%s -> %s", remoteURL, path))
   204  
   205  		vcs := remote.VCS()
   206  		if vcs == nil {
   207  			utils.Log("error", fmt.Sprintf("Could not find version control system: %s", remoteURL))
   208  			os.Exit(1)
   209  		}
   210  
   211  		err := vcs.Clone(remoteURL, path, isShallow)
   212  		if err != nil {
   213  			utils.Log("error", err.Error())
   214  			os.Exit(1)
   215  		}
   216  	} else {
   217  		if doUpdate {
   218  			utils.Log("update", path)
   219  			local.VCS().Update(path)
   220  		} else {
   221  			utils.Log("exists", path)
   222  		}
   223  	}
   224  }
   225  
   226  func doList(c *cli.Context) error {
   227  	query := c.Args().First()
   228  	exact := c.Bool("exact")
   229  	printFullPaths := c.Bool("full-path")
   230  	printUniquePaths := c.Bool("unique")
   231  
   232  	var filterFn func(*LocalRepository) bool
   233  	if query == "" {
   234  		filterFn = func(_ *LocalRepository) bool {
   235  			return true
   236  		}
   237  	} else if exact {
   238  		filterFn = func(repo *LocalRepository) bool {
   239  			return repo.Matches(query)
   240  		}
   241  	} else {
   242  		filterFn = func(repo *LocalRepository) bool {
   243  			return strings.Contains(repo.NonHostPath(), query)
   244  		}
   245  	}
   246  
   247  	repos := []*LocalRepository{}
   248  
   249  	walkLocalRepositories(func(repo *LocalRepository) {
   250  		if filterFn(repo) == false {
   251  			return
   252  		}
   253  
   254  		repos = append(repos, repo)
   255  	})
   256  
   257  	if printUniquePaths {
   258  		subpathCount := map[string]int{} // Count duplicated subpaths (ex. foo/dotfiles and bar/dotfiles)
   259  		reposCount := map[string]int{}   // Check duplicated repositories among roots
   260  
   261  		// Primary first
   262  		for _, repo := range repos {
   263  			if reposCount[repo.RelPath] == 0 {
   264  				for _, p := range repo.Subpaths() {
   265  					subpathCount[p] = subpathCount[p] + 1
   266  				}
   267  			}
   268  
   269  			reposCount[repo.RelPath] = reposCount[repo.RelPath] + 1
   270  		}
   271  
   272  		for _, repo := range repos {
   273  			if reposCount[repo.RelPath] > 1 && repo.IsUnderPrimaryRoot() == false {
   274  				continue
   275  			}
   276  
   277  			for _, p := range repo.Subpaths() {
   278  				if subpathCount[p] == 1 {
   279  					fmt.Println(p)
   280  					break
   281  				}
   282  			}
   283  		}
   284  	} else {
   285  		for _, repo := range repos {
   286  			if printFullPaths {
   287  				fmt.Println(repo.FullPath)
   288  			} else {
   289  				fmt.Println(repo.RelPath)
   290  			}
   291  		}
   292  	}
   293  	return nil
   294  }
   295  
   296  func doLook(c *cli.Context) error {
   297  	name := c.Args().First()
   298  
   299  	if name == "" {
   300  		cli.ShowCommandHelp(c, "look")
   301  		os.Exit(1)
   302  	}
   303  
   304  	reposFound := []*LocalRepository{}
   305  	walkLocalRepositories(func(repo *LocalRepository) {
   306  		if repo.Matches(name) {
   307  			reposFound = append(reposFound, repo)
   308  		}
   309  	})
   310  
   311  	if len(reposFound) == 0 {
   312  		url, err := NewURL(name)
   313  
   314  		if err == nil {
   315  			repo := LocalRepositoryFromURL(url)
   316  			_, err := os.Stat(repo.FullPath)
   317  
   318  			// if the directory exists
   319  			if err == nil {
   320  				reposFound = append(reposFound, repo)
   321  			}
   322  		}
   323  	}
   324  
   325  	switch len(reposFound) {
   326  	case 0:
   327  		utils.Log("error", "No repository found")
   328  		os.Exit(1)
   329  
   330  	case 1:
   331  		if runtime.GOOS == "windows" {
   332  			cmd := exec.Command(os.Getenv("COMSPEC"))
   333  			cmd.Stdin = os.Stdin
   334  			cmd.Stdout = os.Stdout
   335  			cmd.Stderr = os.Stderr
   336  			cmd.Dir = reposFound[0].FullPath
   337  			err := cmd.Start()
   338  			if err == nil {
   339  				cmd.Wait()
   340  				os.Exit(0)
   341  			}
   342  		} else {
   343  			shell := os.Getenv("SHELL")
   344  			if shell == "" {
   345  				shell = "/bin/sh"
   346  			}
   347  
   348  			utils.Log("cd", reposFound[0].FullPath)
   349  			err := os.Chdir(reposFound[0].FullPath)
   350  			utils.PanicIf(err)
   351  
   352  			env := append(syscall.Environ(), "GHQ_LOOK="+reposFound[0].RelPath)
   353  			syscall.Exec(shell, []string{shell}, env)
   354  		}
   355  
   356  	default:
   357  		utils.Log("error", "More than one repositories are found; Try more precise name")
   358  		for _, repo := range reposFound {
   359  			utils.Log("error", "- "+strings.Join(repo.PathParts, "/"))
   360  		}
   361  	}
   362  	return nil
   363  }
   364  
   365  func doImport(c *cli.Context) error {
   366  	var (
   367  		doUpdate  = c.Bool("update")
   368  		isSSH     = c.Bool("p")
   369  		isShallow = c.Bool("shallow")
   370  	)
   371  
   372  	var (
   373  		in       io.Reader
   374  		finalize func() error
   375  	)
   376  
   377  	if len(c.Args()) == 0 {
   378  		// `ghq import` reads URLs from stdin
   379  		in = os.Stdin
   380  		finalize = func() error { return nil }
   381  	} else {
   382  		// Handle `ghq import starred motemen` case
   383  		// with `git config --global ghq.import.starred "!github-list-starred"`
   384  		subCommand := c.Args().First()
   385  		command, err := GitConfigSingle("ghq.import." + subCommand)
   386  		if err == nil && command == "" {
   387  			err = fmt.Errorf("ghq.import.%s configuration not found", subCommand)
   388  		}
   389  		utils.DieIf(err)
   390  
   391  		// execute `sh -c 'COMMAND "$@"' -- ARG...`
   392  		// TODO: Windows
   393  		command = strings.TrimLeft(command, "!")
   394  		shellCommand := append([]string{"sh", "-c", command + ` "$@"`, "--"}, c.Args().Tail()...)
   395  
   396  		utils.Log("run", strings.Join(append([]string{command}, c.Args().Tail()...), " "))
   397  
   398  		cmd := exec.Command(shellCommand[0], shellCommand[1:]...)
   399  		cmd.Stderr = os.Stderr
   400  
   401  		in, err = cmd.StdoutPipe()
   402  		utils.DieIf(err)
   403  
   404  		err = cmd.Start()
   405  		utils.DieIf(err)
   406  
   407  		finalize = cmd.Wait
   408  	}
   409  
   410  	scanner := bufio.NewScanner(in)
   411  	for scanner.Scan() {
   412  		line := scanner.Text()
   413  		url, err := NewURL(line)
   414  		if err != nil {
   415  			utils.Log("error", fmt.Sprintf("Could not parse URL <%s>: %s", line, err))
   416  			continue
   417  		}
   418  		if isSSH {
   419  			url, err = ConvertGitURLHTTPToSSH(url)
   420  			if err != nil {
   421  				utils.Log("error", fmt.Sprintf("Could not convert URL <%s>: %s", url, err))
   422  				continue
   423  			}
   424  		}
   425  
   426  		remote, err := NewRemoteRepository(url)
   427  		if utils.ErrorIf(err) {
   428  			continue
   429  		}
   430  		if remote.IsValid() == false {
   431  			utils.Log("error", fmt.Sprintf("Not a valid repository: %s", url))
   432  			continue
   433  		}
   434  
   435  		getRemoteRepository(remote, doUpdate, isShallow)
   436  	}
   437  	if err := scanner.Err(); err != nil {
   438  		utils.Log("error", fmt.Sprintf("While reading input: %s", err))
   439  		os.Exit(1)
   440  	}
   441  
   442  	utils.DieIf(finalize())
   443  	return nil
   444  }
   445  
   446  func doRoot(c *cli.Context) error {
   447  	all := c.Bool("all")
   448  	if all {
   449  		for _, root := range localRepositoryRoots() {
   450  			fmt.Println(root)
   451  		}
   452  	} else {
   453  		fmt.Println(primaryLocalRepositoryRoot())
   454  	}
   455  	return nil
   456  }