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 }