github.com/kazu/ghq@v0.8.1-0.20180818162325-dedd532b4440/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/urfave/cli" 15 "github.com/kazu/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 directory. 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 }