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 }