├── .travis.yml ├── LICENSE ├── README.md ├── base.go ├── cmdget.go ├── cmdstatus.go ├── cmdundo.go ├── exec.go ├── go.mod ├── go.sum ├── gomodcmd.go ├── help.go ├── io.go ├── main.go ├── mod.go ├── os_1.11.go ├── os_1.12.go ├── pseudo.go ├── script_test.go ├── testdata ├── get-force.txt ├── get-no-gomod.txt ├── get-no-main-mod.txt ├── get-push-replace.txt ├── get-relative-parent.txt ├── get-relative.txt ├── get-vcs-relative-parent.txt ├── get-vcs-relative.txt ├── get-vcs.txt ├── get.txt ├── help.txt ├── mod │ ├── golang.org_x_text_v0.0.0-20170915032832-14c0d48ead0c.txt │ ├── rsc.io_quote_v1.5.2.txt │ ├── rsc.io_sampler_v1.2.1.txt │ ├── rsc.io_sampler_v1.3.0.txt │ └── rsc.io_sampler_v1.99.99.txt ├── status.txt ├── undo-all.txt ├── undo-hack-inblock.txt ├── undo-hack.txt ├── undo-not-existent.txt └── undo.txt └── vcs.go /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go_import_path: github.com/rogpeppe/gohack 3 | go: 4 | - "1.11.x" 5 | - "1.12.x" 6 | env: 7 | global: 8 | - GO111MODULE=on 9 | install: "echo no install step required" 10 | script: go test ./... 11 | 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright © 2014, Roger Peppe 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | * Neither the name of this project nor the names of its contributors 13 | may be used to endorse or promote products derived from this software 14 | without specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 17 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 18 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 19 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 20 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 21 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 22 | TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 23 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 24 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 25 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 26 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gohack: mutable checkouts of Go module dependencies 2 | 3 | The new Go module system is awesome. It ensures repeatable, deterministic 4 | builds of Go code. External module code is cached locally in a read-only 5 | directory, which is great for reproducibility. But if you're used to the 6 | global mutable namespace that is `$GOPATH`, there's an obvious question: 7 | what if I'm hacking on my program and I *want* to change one of those 8 | external modules? 9 | 10 | You might want to put a sneaky `log.Printf` statement to find out how 11 | some internal data structure works, or perhaps try out a bug fix to see 12 | if it solves your latest problem. But since all those external modules 13 | are in read-only directories, it's hard to change them. And you really 14 | don't want to change them anyway, because that will break the integrity 15 | checking that the Go tool does when building. 16 | 17 | Luckily the modules system provides a way around this: you can add a 18 | `replace` statement to the `go.mod` file which substitutes the contents 19 | of a directory holding a module for the readonly cached copy. You can of 20 | course do this manually, but gohack aims to make this process pain-free. 21 | 22 | ## Install gohack with 23 | 24 | go get github.com/rogpeppe/gohack 25 | 26 | or use [`gobin`](https://github.com/myitcv/gobin): 27 | 28 | gobin github.com/rogpeppe/gohack 29 | 30 | ## For quick edits to a module (without version control information) 31 | If the module to edit is `example.com/foo/bar`, run: 32 | 33 | gohack get example.com/foo/bar 34 | 35 | This will make a _copy_ of the module into `$HOME/gohack/example.com/foo/bar` and 36 | add replace directives to the local `go.mod` file: 37 | 38 | replace example.com/foo/bar => /home/rog/gohack/example.com/foo/bar 39 | 40 | __Note__: This copy will __not__ include version control system information so 41 | it is best for quick edits that aren't intended to land back into version control. 42 | 43 | ## To edit the module with full version control 44 | Run: 45 | 46 | gohack get -vcs example.com/foo/bar 47 | 48 | This will _clone_ the module's repository to 49 | `$HOME/gohack/example.com/foo/bar`, check out the correct version of the 50 | source code there and add the replace directive into the local `go.mod` file. 51 | 52 | ## Undoing replacements 53 | 54 | Once you are done hacking and wish to revert to the immutable version, you 55 | can remove the replace statement with: 56 | 57 | gohack undo example.com/foo/bar 58 | 59 | or you can remove all gohack replace statements with: 60 | 61 | gohack undo 62 | 63 | Note that undoing a replace does *not* remove the external module's 64 | directory - that stays around so your changes are not lost. For example, 65 | you might wish to turn that bug fix into an upstream PR. 66 | 67 | If you run gohack on a module that already has a directory, gohack will 68 | try to check out the current version without recreating the repository, 69 | but only if the directory is clean - it won't overwrite your changes 70 | until you've committed or undone them. 71 | -------------------------------------------------------------------------------- /base.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | "strings" 8 | ) 9 | 10 | // A Command is an implementation of a gohack command. 11 | type Command struct { 12 | // Run runs the command and returns its exit status. 13 | // The args are the arguments after the command name. 14 | Run func(cmd *Command, args []string) int 15 | 16 | // UsageLine is the one-line usage message. 17 | // The first word in the line is taken to be the command name. 18 | UsageLine string 19 | 20 | // Short is the short description shown in the 'gohack help' output. 21 | Short string 22 | 23 | // Long is the long message shown in the 'gohack help ' output. 24 | Long string 25 | 26 | // Flag is a set of flags specific to this command. 27 | Flag flag.FlagSet 28 | } 29 | 30 | func (c *Command) Name() string { 31 | return strings.SplitN(c.UsageLine, " ", 2)[0] 32 | } 33 | 34 | func (c *Command) Usage() { 35 | fmt.Fprintf(os.Stderr, "usage: %s\n", c.UsageLine) 36 | fmt.Fprintf(os.Stderr, "Run 'gohack help %s' for details.\n", c.Name()) 37 | } 38 | -------------------------------------------------------------------------------- /cmdget.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | 10 | "gopkg.in/errgo.v2/fmt/errors" 11 | 12 | "github.com/rogpeppe/go-internal/modfile" 13 | "github.com/rogpeppe/go-internal/module" 14 | ) 15 | 16 | var getCommand = &Command{ 17 | UsageLine: "get [-vcs] [-u] [-f] [module...]", 18 | Short: "start hacking a module", 19 | Long: ` 20 | The get command checks out Go module dependencies 21 | into a directory where they can be edited. 22 | 23 | It uses $GOHACK/ as the destination directory, 24 | or $HOME/gohack/ if $GOHACK is empty. 25 | 26 | By default it copies module source code from the existing 27 | source directory in $GOPATH/pkg/mod. If the -vcs 28 | flag is specified, it also checks out the version control information into that 29 | directory and updates it to the expected version. If the directory 30 | already exists, it will be updated in place. 31 | `[1:], 32 | } 33 | 34 | func init() { 35 | getCommand.Run = runGet // break init cycle 36 | } 37 | 38 | var ( 39 | // TODO implement getUpdate so that we can use gohack -f without 40 | // overwriting source code. 41 | // getUpdate = getCommand.Flag.Bool("u", false, "update to current version") 42 | getForce = getCommand.Flag.Bool("f", false, "force update to current version even if not clean") 43 | getVCS = getCommand.Flag.Bool("vcs", false, "get VCS information too") 44 | ) 45 | 46 | func runGet(cmd *Command, args []string) int { 47 | if err := runGet1(args); err != nil { 48 | errorf("%v", err) 49 | } 50 | return 0 51 | } 52 | 53 | func runGet1(args []string) error { 54 | if len(args) == 0 { 55 | return errors.Newf("get requires at least one module argument") 56 | } 57 | var repls []*modReplace 58 | mods, err := listModules("all") 59 | if err != nil { 60 | // TODO this happens when a replacement directory has been removed. 61 | // Perhaps we should be more resilient in that case? 62 | return errors.Notef(err, nil, "cannot get module info") 63 | } 64 | for _, mpath := range args { 65 | m := mods[mpath] 66 | if m == nil { 67 | errorf("module %q does not appear to be in use", mpath) 68 | continue 69 | } 70 | // Early check that we can replace the module, so we don't 71 | // do all the work to check it out only to find we can't 72 | // add the replace directive. 73 | if err := checkCanReplace(mainModFile, mpath); err != nil { 74 | errorf("%v", err) 75 | continue 76 | } 77 | if m.Replace != nil && m.Replace.Path == m.Replace.Dir { 78 | // TODO if -u flag specified, update to the current version instead of printing an error. 79 | errorf("%q is already replaced by %q - are you already gohacking it?", mpath, m.Replace.Dir) 80 | continue 81 | } 82 | var repl *modReplace 83 | if *getVCS { 84 | repl1, err := updateVCSDir(m) 85 | if err != nil { 86 | errorf("cannot update VCS dir for %s: %v", m.Path, err) 87 | continue 88 | } 89 | repl = repl1 90 | } else { 91 | repl1, err := updateFromLocalDir(m) 92 | if err != nil { 93 | errorf("cannot update %s from local cache: %v", m.Path, err) 94 | continue 95 | } 96 | repl = repl1 97 | } 98 | // Automatically generate a go.mod file if one doesn't already exist, 99 | // because otherwise the directory cannot be used as a module. 100 | if err := ensureGoModFile(repl.modulePath, repl.dir); err != nil { 101 | errorf("%v", err) 102 | } 103 | repls = append(repls, repl) 104 | } 105 | if len(repls) == 0 { 106 | return errors.New("all modules failed; not replacing anything") 107 | } 108 | if err := replace(mainModFile, repls); err != nil { 109 | return errors.Notef(err, nil, "cannot replace") 110 | } 111 | if err := writeModFile(mainModFile); err != nil { 112 | return errors.Wrap(err) 113 | } 114 | for _, info := range repls { 115 | fmt.Printf("%s => %s\n", info.modulePath, info.replDir) 116 | } 117 | return nil 118 | } 119 | 120 | func updateFromLocalDir(m *listModule) (*modReplace, error) { 121 | if m.Dir == "" { 122 | return nil, errors.Newf("no local source code found") 123 | } 124 | srcHash, err := hashDir(m.Dir, m.Path) 125 | if err != nil { 126 | return nil, errors.Notef(err, nil, "cannot hash %q", m.Dir) 127 | } 128 | destDir, replDir, err := moduleDir(m.Path) 129 | if err != nil { 130 | return nil, errors.Notef(err, nil, "failed to determine target directory for %v", m.Path) 131 | } 132 | _, err = os.Stat(destDir) 133 | if err != nil && !os.IsNotExist(err) { 134 | return nil, errors.Wrap(err) 135 | } 136 | repl := &modReplace{ 137 | modulePath: m.Path, 138 | dir: destDir, 139 | replDir: replDir, 140 | } 141 | if err != nil { 142 | // Destination doesn't exist. Copy the entire directory. 143 | if err := copyAll(destDir, m.Dir); err != nil { 144 | return nil, errors.Wrap(err) 145 | } 146 | } else { 147 | if !*getForce { 148 | // Destination already exists; try to update it. 149 | isEmpty, err := isEmptyDir(destDir) 150 | if err != nil { 151 | return nil, errors.Wrap(err) 152 | } 153 | if !isEmpty { 154 | // The destination directory already exists and has something in. 155 | destHash, err := checkCleanWithoutVCS(destDir, m.Path) 156 | if err != nil { 157 | return nil, errors.Wrap(err) 158 | } 159 | if destHash == srcHash { 160 | // Everything is exactly as we want it already. 161 | return repl, nil 162 | } 163 | } 164 | } 165 | // As it's empty, clean or we're forcing clean, we can safely replace its 166 | // contents with the current version. 167 | if err := updateDirWithoutVCS(destDir, m.Dir); err != nil { 168 | return nil, errors.Notef(err, nil, "cannot update %q from %q", destDir, m.Dir) 169 | } 170 | } 171 | // Write a hash file so we can tell if someone has changed the 172 | // directory later, so we avoid overwriting their changes. 173 | if err := writeHashFile(destDir, srcHash); err != nil { 174 | return nil, errors.Wrap(err) 175 | } 176 | return repl, nil 177 | } 178 | 179 | func checkCleanWithoutVCS(dir string, modulePath string) (hash string, err error) { 180 | wantHash, err := readHashFile(dir) 181 | if err != nil { 182 | if !os.IsNotExist(errors.Cause(err)) { 183 | return "", errors.Wrap(err) 184 | } 185 | return "", errors.Newf("%q already exists; not overwriting", dir) 186 | } 187 | gotHash, err := hashDir(dir, modulePath) 188 | if err != nil { 189 | return "", errors.Notef(err, nil, "cannot hash %q", dir) 190 | } 191 | if gotHash != wantHash { 192 | return "", errors.Newf("%q is not clean; not overwriting", dir) 193 | } 194 | return wantHash, nil 195 | } 196 | 197 | func updateDirWithoutVCS(destDir, srcDir string) error { 198 | if err := os.RemoveAll(destDir); err != nil { 199 | return errors.Wrap(err) 200 | } 201 | if err := copyAll(destDir, srcDir); err != nil { 202 | return errors.Wrap(err) 203 | } 204 | return nil 205 | } 206 | 207 | // TODO decide on a good name for this. 208 | const hashFile = ".gohack-modhash" 209 | 210 | func readHashFile(dir string) (string, error) { 211 | data, err := ioutil.ReadFile(filepath.Join(dir, hashFile)) 212 | if err != nil { 213 | return "", errors.Note(err, os.IsNotExist, "") 214 | } 215 | return strings.TrimSpace(string(data)), nil 216 | } 217 | 218 | func writeHashFile(dir string, hash string) error { 219 | if err := ioutil.WriteFile(filepath.Join(dir, hashFile), []byte(hash), 0666); err != nil { 220 | return errors.Wrap(err) 221 | } 222 | return nil 223 | } 224 | 225 | func updateVCSDir(m *listModule) (*modReplace, error) { 226 | info, err := getVCSInfoForModule(m) 227 | if err != nil { 228 | return nil, errors.Notef(err, nil, "cannot get info") 229 | } 230 | if err := updateModule(info); err != nil { 231 | return nil, errors.Wrap(err) 232 | } 233 | return &modReplace{ 234 | modulePath: m.Path, 235 | dir: info.dir, 236 | replDir: info.replDir, 237 | }, nil 238 | } 239 | 240 | type modReplace struct { 241 | // modulePath is the module path 242 | modulePath string 243 | // dir holds the absolute path to the replacement directory. 244 | dir string 245 | // replDir holds the path to use for the module in the go.mod replace directive. 246 | replDir string 247 | } 248 | 249 | func replace(f *modfile.File, repls []*modReplace) error { 250 | for _, repl := range repls { 251 | if err := replaceModule(f, repl.modulePath, repl.replDir); err != nil { 252 | return errors.Wrap(err) 253 | } 254 | } 255 | return nil 256 | } 257 | 258 | func updateModule(info *moduleVCSInfo) error { 259 | // Remove an auto-generated go.mod file if there is one 260 | // to avoid confusing VCS logic. 261 | if _, err := removeAutoGoMod(info); err != nil { 262 | return errors.Wrap(err) 263 | } 264 | if info.alreadyExists && !info.clean && *getForce { 265 | if err := info.vcs.Clean(info.dir); err != nil { 266 | return fmt.Errorf("cannot clean: %v", err) 267 | } 268 | } 269 | 270 | isTag := true 271 | updateTo := info.module.Version 272 | if IsPseudoVersion(updateTo) { 273 | revID, err := PseudoVersionRev(updateTo) 274 | if err != nil { 275 | return errors.Wrap(err) 276 | } 277 | isTag = false 278 | updateTo = revID 279 | } else { 280 | // Not a pseudo-version. However, this can still be in the form 281 | // of "+incompatible", so trim the suffix. 282 | updateTo = strings.TrimSuffix(updateTo, "+incompatible") 283 | } 284 | if err := info.vcs.Update(info.dir, isTag, updateTo); err == nil { 285 | fmt.Printf("updated hack version of %s to %s\n", info.module.Path, info.module.Version) 286 | return nil 287 | } 288 | if !info.alreadyExists { 289 | fmt.Printf("creating %s@%s\n", info.module.Path, info.module.Version) 290 | if err := createRepo(info); err != nil { 291 | return fmt.Errorf("cannot create repo: %v", err) 292 | } 293 | } else { 294 | fmt.Printf("fetching %s@%s\n", info.module.Path, info.module.Version) 295 | if err := info.vcs.Fetch(info.dir); err != nil { 296 | return err 297 | } 298 | } 299 | return info.vcs.Update(info.dir, isTag, updateTo) 300 | } 301 | 302 | func createRepo(info *moduleVCSInfo) error { 303 | // Some version control tools require the parent of the target to exist. 304 | parent, _ := filepath.Split(info.dir) 305 | if err := os.MkdirAll(parent, 0777); err != nil { 306 | return err 307 | } 308 | if err := info.vcs.Create(info.root.Repo, info.dir); err != nil { 309 | return errors.Wrap(err) 310 | } 311 | return nil 312 | } 313 | 314 | func ensureGoModFile(modPath, dir string) error { 315 | goModPath := filepath.Join(dir, "go.mod") 316 | if _, err := os.Stat(goModPath); err == nil { 317 | return nil 318 | } 319 | if err := ioutil.WriteFile(goModPath, []byte(autoGoMod(modPath)), 0666); err != nil { 320 | return errors.Wrap(err) 321 | } 322 | return nil 323 | } 324 | 325 | // removeAutoGoMod removes the module directory's go.mod 326 | // file if it looks like it's been autogenerated by us. 327 | // It reports whether the file was removed. 328 | func removeAutoGoMod(m *moduleVCSInfo) (bool, error) { 329 | goModPath := filepath.Join(m.dir, "go.mod") 330 | ok, err := isAutoGoMod(goModPath, m.module.Path) 331 | if err != nil || !ok { 332 | return false, err 333 | } 334 | if err := os.Remove(goModPath); err != nil { 335 | return false, errors.Wrap(err) 336 | } 337 | return true, nil 338 | } 339 | 340 | // isAutoGoMod reports whether the file at path 341 | // looks like it's a go.mod file auto-generated by gohack. 342 | func isAutoGoMod(path string, modulePath string) (bool, error) { 343 | data, err := ioutil.ReadFile(path) 344 | if err != nil { 345 | if !os.IsNotExist(err) { 346 | return false, errors.Wrap(err) 347 | } 348 | return false, nil 349 | } 350 | if string(data) != autoGoMod(modulePath) { 351 | return false, nil 352 | } 353 | return true, nil 354 | } 355 | 356 | // autoGoMod returns the contents of the go.mod file that 357 | // would be auto-generated for the module with the given 358 | // path. 359 | func autoGoMod(mpath string) string { 360 | return "// Generated by gohack; DO NOT EDIT.\nmodule " + mpath + "\n" 361 | } 362 | 363 | // checkCanReplace checks whether it may be possible to replace 364 | // the module in the given go.mod file. 365 | func checkCanReplace(f *modfile.File, mod string) error { 366 | var found *modfile.Replace 367 | for _, r := range f.Replace { 368 | if r.Old.Path != mod { 369 | continue 370 | } 371 | if found != nil { 372 | return errors.Newf("found multiple existing replacements for %q", mod) 373 | } 374 | if r.New.Version == "" { 375 | return errors.Newf("%s is already replaced by %s", mod, r.New.Path) 376 | } 377 | } 378 | return nil 379 | } 380 | 381 | // replaceModule adds or modifies a replace statement in f for mod 382 | // to be replaced with dir. 383 | func replaceModule(f *modfile.File, mod string, dir string) error { 384 | var found *modfile.Replace 385 | for _, r := range f.Replace { 386 | if r.Old.Path != mod { 387 | continue 388 | } 389 | // These checks shouldn't fail when checkCanReplace has been 390 | // called previously, but check anyway just to be sure. 391 | if found != nil || r.New.Version == "" { 392 | panic(errors.Newf("unexpected bad replace for %q (checkCanReplace not called?)", mod)) 393 | } 394 | found = r 395 | } 396 | if found == nil { 397 | // No existing replace statement. Just add a new one. 398 | if err := f.AddReplace(mod, "", dir, ""); err != nil { 399 | return errors.Wrap(err) 400 | } 401 | return nil 402 | } 403 | // There's an existing replacement for the same target, so modify it 404 | // but preserve the original replacement information around in a comment. 405 | token := fmt.Sprintf("// was %s => %s", versionPath(found.Old), versionPath(found.New)) 406 | comments := &found.Syntax.Comments 407 | if len(comments.Suffix) > 0 { 408 | // There's already a comment, so preserve it. 409 | comments.Suffix[0].Token = token + " " + comments.Suffix[0].Token 410 | } else { 411 | comments.Suffix = []modfile.Comment{{ 412 | Token: token, 413 | }} 414 | } 415 | found.Old.Version = "" 416 | found.New.Path = dir 417 | found.New.Version = "" 418 | if !found.Syntax.InBlock { 419 | found.Syntax.Token = []string{"replace"} 420 | } else { 421 | found.Syntax.Token = nil 422 | } 423 | found.Syntax.Token = append(found.Syntax.Token, []string{ 424 | mod, 425 | "=>", 426 | dir, 427 | }...) 428 | return nil 429 | } 430 | 431 | func versionPath(v module.Version) string { 432 | if v.Version == "" { 433 | return v.Path 434 | } 435 | return v.Path + " " + v.Version 436 | } 437 | -------------------------------------------------------------------------------- /cmdstatus.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | var statusCommand = &Command{ 8 | Run: cmdStatus, 9 | Short: "print the current hack status of a module", 10 | UsageLine: "status [module...]", 11 | Long: ` 12 | The status command prints the status of 13 | all modules that are currently replaced by local 14 | directories. If arguments are given, it prints information 15 | about only the specified modules. 16 | `[1:], 17 | } 18 | 19 | func cmdStatus(_ *Command, args []string) int { 20 | if len(args) > 0 { 21 | errorf("explicit module status not yet implemented") 22 | return 2 23 | } 24 | if err := printReplacementInfo(); err != nil { 25 | errorf("%v", err) 26 | } 27 | return 0 28 | } 29 | 30 | func printReplacementInfo() error { 31 | for _, r := range mainModFile.Replace { 32 | if r.Old.Version == "" && r.New.Version == "" { 33 | fmt.Printf("%s => %s\n", r.Old.Path, r.New.Path) 34 | } 35 | } 36 | return nil 37 | } 38 | -------------------------------------------------------------------------------- /cmdundo.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "sort" 7 | "strings" 8 | 9 | "github.com/rogpeppe/go-internal/modfile" 10 | "github.com/rogpeppe/go-internal/module" 11 | "github.com/rogpeppe/go-internal/semver" 12 | "gopkg.in/errgo.v2/fmt/errors" 13 | ) 14 | 15 | var undoCommand = &Command{ 16 | Run: cmdUndo, 17 | Short: "stop hacking a module", 18 | UsageLine: "undo [-rm] [-f] [module...]", 19 | Long: ` 20 | The undo command can be used to revert to the non-gohacked 21 | module versions. It only removes the relevant replace 22 | statements from the go.mod file - it does not change any 23 | of the directories referred to. With no arguments, all replace 24 | statements that refer to directories will 25 | be removed. 26 | `[1:], 27 | } 28 | 29 | var ( 30 | undoRemove = undoCommand.Flag.Bool("rm", false, "remove module directory too") 31 | undoForceClean = undoCommand.Flag.Bool("f", false, "force cleaning of modified-but-not-committed repositories. Do not use this flag unless you really need to!") 32 | ) 33 | 34 | func cmdUndo(_ *Command, args []string) int { 35 | if err := cmdUndo1(args); err != nil { 36 | errorf("%v", err) 37 | } 38 | return 0 39 | } 40 | 41 | func cmdUndo1(modules []string) error { 42 | modMap := make(map[string]bool) 43 | if len(modules) > 0 { 44 | for _, m := range modules { 45 | modMap[m] = true 46 | } 47 | } else { 48 | // With no modules specified, we un-gohack all modules 49 | // we can find with local directory info in the go.mod file. 50 | for _, r := range mainModFile.Replace { 51 | if r.Old.Version == "" && r.New.Version == "" { 52 | modMap[r.Old.Path] = true 53 | modules = append(modules, r.Old.Path) 54 | } 55 | } 56 | } 57 | drop := make(map[string]bool) 58 | for _, r := range mainModFile.Replace { 59 | if !modMap[r.Old.Path] || r.Old.Version != "" || r.New.Version != "" { 60 | continue 61 | } 62 | // Found a replacement to drop. 63 | comments := r.Syntax.Comments 64 | if len(comments.Suffix) == 0 { 65 | // No comment; we can just drop it. 66 | drop[r.Old.Path] = true 67 | delete(modMap, r.Old.Path) 68 | continue 69 | } 70 | prevReplace := splitWasComment(comments.Suffix[0].Token) 71 | if prevReplace != nil && prevReplace.Old.Path == r.Old.Path { 72 | // We're popping the old replace statement. 73 | if r.Syntax.InBlock { 74 | // When we're in a block, we don't need the "replace" token. 75 | prevReplace.Syntax.Token = prevReplace.Syntax.Token[1:] 76 | } 77 | // Preserve any before and after comments. 78 | prevReplace.Syntax.Before = r.Syntax.Before 79 | prevReplace.Syntax.After = r.Syntax.After 80 | r.Old = prevReplace.Old 81 | r.New = prevReplace.New 82 | r.Syntax.Comments.Suffix = prevReplace.Syntax.Comments.Suffix 83 | r.Syntax.Token = prevReplace.Syntax.Token 84 | } else { 85 | // It's not a "was" comment. Just remove it (after this loop so we don't 86 | // interfere with the current range statement). 87 | drop[r.Old.Path] = true 88 | } 89 | delete(modMap, r.Old.Path) 90 | } 91 | for m := range drop { 92 | if err := mainModFile.DropReplace(m, ""); err != nil { 93 | return errors.Notef(err, nil, "cannot drop replacement for %v", m) 94 | } 95 | } 96 | failed := make([]string, 0, len(modMap)) 97 | for m := range modMap { 98 | failed = append(failed, m) 99 | } 100 | sort.Strings(failed) 101 | for _, m := range failed { 102 | errorf("%s not currently replaced; cannot drop", m) 103 | } 104 | if err := writeModFile(mainModFile); err != nil { 105 | return errors.Wrap(err) 106 | } 107 | for _, m := range modules { 108 | fmt.Printf("dropped %s\n", m) 109 | } 110 | return nil 111 | } 112 | 113 | // wasCommentPat matches a comment of the form inserted by gohack get, 114 | // for example: 115 | // // was example.com v1.2.3 => foo.com v1.3.4 // original comment 116 | var wasCommentPat = regexp.MustCompile(`^// was ([^ ]+(?: [^ ]+)?) => ([^ ]+(?: [^ ]+)?)(?: (//.+))?$`) 117 | 118 | func splitWasComment(s string) *modfile.Replace { 119 | parts := wasCommentPat.FindStringSubmatch(s) 120 | if parts == nil { 121 | return nil 122 | } 123 | old, ok := splitPathVersion(parts[1]) 124 | if !ok { 125 | return nil 126 | } 127 | new, ok := splitPathVersion(parts[2]) 128 | if !ok { 129 | return nil 130 | } 131 | oldComment := parts[3] 132 | r := &modfile.Replace{ 133 | Old: old, 134 | New: new, 135 | Syntax: &modfile.Line{ 136 | Token: tokensForReplace(old, new), 137 | }, 138 | } 139 | if oldComment != "" { 140 | r.Syntax.Comments.Suffix = []modfile.Comment{{ 141 | Token: oldComment, 142 | Suffix: true, 143 | }} 144 | } 145 | return r 146 | } 147 | 148 | func tokensForReplace(old, new module.Version) []string { 149 | tokens := make([]string, 0, 6) 150 | tokens = append(tokens, "replace") 151 | tokens = append(tokens, old.Path) 152 | if old.Version != "" { 153 | tokens = append(tokens, old.Version) 154 | } 155 | tokens = append(tokens, "=>") 156 | tokens = append(tokens, new.Path) 157 | if new.Version != "" { 158 | tokens = append(tokens, new.Version) 159 | } 160 | return tokens 161 | } 162 | 163 | func splitPathVersion(s string) (module.Version, bool) { 164 | fs := strings.Fields(s) 165 | if len(fs) != 1 && len(fs) != 2 { 166 | return module.Version{}, false 167 | } 168 | v := module.Version{ 169 | Path: fs[0], 170 | } 171 | if len(fs) > 1 { 172 | if !semver.IsValid(fs[1]) { 173 | return module.Version{}, false 174 | } 175 | v.Version = fs[1] 176 | } 177 | return v, true 178 | } 179 | -------------------------------------------------------------------------------- /exec.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os" 7 | "os/exec" 8 | "strings" 9 | "sync" 10 | 11 | "gopkg.in/errgo.v2/fmt/errors" 12 | ) 13 | 14 | func runCmd(dir string, name string, args ...string) (string, error) { 15 | var outData, errData bytes.Buffer 16 | if *printCommands { 17 | printShellCommand(dir, name, args) 18 | } 19 | c := exec.Command(name, args...) 20 | c.Stdout = &outData 21 | c.Stderr = &errData 22 | c.Dir = dir 23 | err := c.Run() 24 | if err == nil { 25 | return outData.String(), nil 26 | } 27 | if _, ok := err.(*exec.ExitError); ok && errData.Len() > 0 { 28 | return "", errors.New(strings.TrimSpace(errData.String())) 29 | } 30 | return "", fmt.Errorf("cannot run %q: %v", append([]string{name}, args...), err) 31 | } 32 | 33 | var ( 34 | outputDirMutex sync.Mutex 35 | outputDir string 36 | ) 37 | 38 | func printShellCommand(dir, name string, args []string) { 39 | outputDirMutex.Lock() 40 | defer outputDirMutex.Unlock() 41 | if dir != outputDir { 42 | fmt.Fprintf(os.Stderr, "cd %s\n", shquote(dir)) 43 | outputDir = dir 44 | } 45 | var buf bytes.Buffer 46 | buf.WriteString(name) 47 | for _, arg := range args { 48 | buf.WriteString(" ") 49 | buf.WriteString(shquote(arg)) 50 | } 51 | fmt.Fprintf(os.Stderr, "%s\n", buf.Bytes()) 52 | } 53 | 54 | func shquote(s string) string { 55 | // single-quote becomes single-quote, double-quote, single-quote, double-quote, single-quote 56 | return `'` + strings.Replace(s, `'`, `'"'"'`, -1) + `'` 57 | } 58 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/rogpeppe/gohack 2 | 3 | require ( 4 | github.com/rogpeppe/go-internal v1.14.1 5 | golang.org/x/tools/go/vcs v0.1.0-deprecated 6 | gopkg.in/errgo.v2 v2.1.0 7 | ) 8 | 9 | require ( 10 | golang.org/x/mod v0.24.0 // indirect 11 | golang.org/x/sys v0.33.0 // indirect 12 | golang.org/x/tools v0.33.0 // indirect 13 | ) 14 | 15 | go 1.23.0 16 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 2 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 3 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 4 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 5 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 6 | github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= 7 | github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= 8 | golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= 9 | golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= 10 | golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= 11 | golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 12 | golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= 13 | golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= 14 | golang.org/x/tools/go/vcs v0.1.0-deprecated h1:cOIJqWBl99H1dH5LWizPa+0ImeeJq3t3cJjaeOWUAL4= 15 | golang.org/x/tools/go/vcs v0.1.0-deprecated/go.mod h1:zUrvATBAvEI9535oC0yWYsLsHIV4Z7g63sNPVMtuBy8= 16 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 17 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 18 | gopkg.in/errgo.v2 v2.1.0 h1:0vLT13EuvQ0hNvakwLuFZ/jYrLp5F3kcWHXdRggjCE8= 19 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 20 | -------------------------------------------------------------------------------- /gomodcmd.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | "io/ioutil" 7 | "path/filepath" 8 | "strings" 9 | "time" 10 | 11 | "github.com/rogpeppe/go-internal/modfile" 12 | "gopkg.in/errgo.v2/fmt/errors" 13 | ) 14 | 15 | // listModule holds information on a module as printed by go list -m. 16 | type listModule struct { 17 | Path string // module path 18 | Version string // module version 19 | Versions []string // available module versions (with -versions) 20 | Replace *listModule // replaced by this module 21 | Time *time.Time // time version was created 22 | Update *listModule // available update, if any (with -u) 23 | Main bool // is this the main module? 24 | Indirect bool // is this module only an indirect dependency of main module? 25 | Dir string // directory holding files for this module, if any 26 | GoMod string // path to go.mod file for this module, if any 27 | Error *listModuleError // error loading module 28 | } 29 | 30 | type listModuleError struct { 31 | Err string // the error itself 32 | } 33 | 34 | // listModules returns information on the given modules as used by the root module. 35 | func listModules(modules ...string) (mods map[string]*listModule, err error) { 36 | // TODO make runCmd return []byte so we don't need the []byte conversion. 37 | args := append([]string{"list", "-m", "-json"}, modules...) 38 | out, err := runCmd(cwd, "go", args...) 39 | if err != nil { 40 | return nil, errors.Wrap(err) 41 | } 42 | dec := json.NewDecoder(strings.NewReader(out)) 43 | mods = make(map[string]*listModule) 44 | for { 45 | var m listModule 46 | if err := dec.Decode(&m); err != nil { 47 | if err == io.EOF { 48 | break 49 | } 50 | return nil, errors.Wrap(err) 51 | } 52 | if mods[m.Path] != nil { 53 | return nil, errors.Newf("duplicate module %q in go list output", m.Path) 54 | } 55 | mods[m.Path] = &m 56 | } 57 | return mods, nil 58 | } 59 | 60 | // goModInfo returns the main module's root directory 61 | // and the parsed contents of the go.mod file. 62 | func goModInfo() (string, *modfile.File, error) { 63 | goModPath, err := findGoMod(cwd) 64 | if err != nil { 65 | return "", nil, errors.Notef(err, nil, "cannot find main module") 66 | } 67 | rootDir := filepath.Dir(goModPath) 68 | data, err := ioutil.ReadFile(goModPath) 69 | if err != nil { 70 | return "", nil, errors.Notef(err, nil, "cannot read main go.mod file") 71 | } 72 | modf, err := modfile.Parse(goModPath, data, nil) 73 | if err != nil { 74 | return "", nil, errors.Wrap(err) 75 | } 76 | return rootDir, modf, nil 77 | } 78 | 79 | func findGoMod(dir string) (string, error) { 80 | out, err := runCmd(dir, "go", "env", "GOMOD") 81 | if err != nil { 82 | return "", err 83 | } 84 | out = strings.TrimSpace(out) 85 | if out == "" { 86 | return "", errors.New("no go.mod file found in any parent directory") 87 | } 88 | return strings.TrimSpace(out), nil 89 | } 90 | 91 | func writeModFile(modf *modfile.File) error { 92 | data, err := modf.Format() 93 | if err != nil { 94 | return errors.Notef(err, nil, "cannot generate go.mod file") 95 | } 96 | if err := ioutil.WriteFile(modf.Syntax.Name, data, 0666); err != nil { 97 | return errors.Wrap(err) 98 | } 99 | return nil 100 | } 101 | -------------------------------------------------------------------------------- /help.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "html/template" 6 | "io" 7 | "os" 8 | "strings" 9 | ) 10 | 11 | // runHelp implements the 'help' command. 12 | func runHelp(args []string) int { 13 | if len(args) == 0 { 14 | mainUsage(os.Stdout) 15 | return 0 16 | } 17 | if len(args) != 1 { 18 | fmt.Fprintf(os.Stderr, "gohack help %s: too many arguments\n", strings.Join(args, " ")) 19 | return 2 20 | } 21 | t := template.Must(template.New("").Parse(commandHelpTemplate)) 22 | for _, c := range commands { 23 | if c.Name() == args[0] { 24 | if err := t.Execute(os.Stdout, c); err != nil { 25 | errorf("cannot write usage output: %v", err) 26 | } 27 | return 0 28 | } 29 | } 30 | fmt.Fprintf(os.Stderr, "gohack help %s: unknown command\n", args[0]) 31 | return 2 32 | } 33 | 34 | func mainUsage(f io.Writer) { 35 | t := template.Must(template.New("").Parse(mainHelpTemplate)) 36 | if err := t.Execute(f, commands); err != nil { 37 | errorf("cannot write usage output: %v", err) 38 | } 39 | } 40 | 41 | var mainHelpTemplate = ` 42 | The gohack command checks out Go module dependencies 43 | into a directory where they can be edited, and adjusts 44 | the go.mod file appropriately. 45 | 46 | Usage: 47 | 48 | gohack [arguments] 49 | 50 | The commands are: 51 | {{range .}} 52 | {{.Name | printf "%-11s"}} {{.Short}}{{end}} 53 | 54 | Use "gohack help " for more information about a command. 55 | `[1:] 56 | 57 | var commandHelpTemplate = ` 58 | usage: {{.UsageLine}} 59 | 60 | {{.Long}}`[1:] 61 | -------------------------------------------------------------------------------- /io.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | "path/filepath" 8 | 9 | "gopkg.in/errgo.v2/fmt/errors" 10 | ) 11 | 12 | func isEmptyDir(dir string) (bool, error) { 13 | f, err := os.Open(dir) 14 | if err != nil { 15 | return false, errors.Wrap(err) 16 | } 17 | defer f.Close() 18 | _, err = f.Readdir(1) 19 | if err != nil && err != io.EOF { 20 | return false, errors.Wrap(err) 21 | } 22 | return err == io.EOF, nil 23 | } 24 | 25 | func copyAll(dst, src string) error { 26 | srcInfo, srcErr := os.Lstat(src) 27 | if srcErr != nil { 28 | return errors.Wrap(srcErr) 29 | } 30 | _, dstErr := os.Lstat(dst) 31 | if dstErr == nil { 32 | return errors.Newf("will not overwrite %q", dst) 33 | } 34 | if !os.IsNotExist(dstErr) { 35 | return errors.Wrap(dstErr) 36 | } 37 | switch mode := srcInfo.Mode(); mode & os.ModeType { 38 | case os.ModeSymlink: 39 | return errors.Newf("will not copy symbolic link") 40 | case os.ModeDir: 41 | return copyDir(dst, src) 42 | case 0: 43 | return copyFile(dst, src) 44 | default: 45 | return fmt.Errorf("cannot copy file with mode %v", mode) 46 | } 47 | } 48 | 49 | func copyFile(dst, src string) error { 50 | srcf, err := os.Open(src) 51 | if err != nil { 52 | return errors.Wrap(err) 53 | } 54 | defer srcf.Close() 55 | dstf, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666) 56 | if err != nil { 57 | return errors.Wrap(err) 58 | } 59 | defer dstf.Close() 60 | if _, err := io.Copy(dstf, srcf); err != nil { 61 | return fmt.Errorf("cannot copy %q to %q: %v", src, dst, err) 62 | } 63 | return nil 64 | } 65 | 66 | func copyDir(dst, src string) error { 67 | srcf, err := os.Open(src) 68 | if err != nil { 69 | return errors.Wrap(err) 70 | } 71 | defer srcf.Close() 72 | if err := os.MkdirAll(dst, 0777); err != nil { 73 | return errors.Wrap(err) 74 | } 75 | for { 76 | names, err := srcf.Readdirnames(100) 77 | for _, name := range names { 78 | if err := copyAll(filepath.Join(dst, name), filepath.Join(src, name)); err != nil { 79 | return errors.Wrap(err) 80 | } 81 | } 82 | if err == io.EOF { 83 | break 84 | } 85 | if err != nil { 86 | return errors.Newf("error reading directory %q: %v", src, err) 87 | } 88 | } 89 | return nil 90 | } 91 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /* 2 | The gohack command automates checking out a mutable copy 3 | of a module dependency and adding the relevant replace 4 | statement to the go.mod file. 5 | 6 | See https://github.com/rogpeppe/gohack for more details. 7 | */ 8 | package main 9 | 10 | import ( 11 | "flag" 12 | "fmt" 13 | "os" 14 | 15 | "github.com/rogpeppe/go-internal/modfile" 16 | "gopkg.in/errgo.v2/fmt/errors" 17 | ) 18 | 19 | /* 20 | As the amount of functionality grows, it seems like we should consider having subcommands. 21 | 22 | A possible set of commands: 23 | 24 | gohack get [-vcs] [-u] [-f] [module...] 25 | Get gets the modules at the current version and adds replace statements to the go.mod file if they're not already replaced. 26 | If the -u flag is provided, the source code will also be updated to the current version if it's clean. 27 | If the -f flag is provided with -u, the source code will be updated even if it's not clean. 28 | If the -vcs flag is provided, it also checks out VCS information for the modules. If the modules were already gohacked in non-VCS mode, gohack switches them to VCS mode, preserving any changes made (this might result in the directory moving). 29 | 30 | With no module arguments and the -u flag, it will try to update all currently gohacked modules. 31 | 32 | gohack status 33 | Status prints a list of the replaced modules 34 | 35 | gohack rm [-f] module... 36 | Rm removes the gohack directory if it is clean and then runs gohack undo. If the -f flag is provided, the directory is removed even if it's not clean. 37 | 38 | gohack undo [module...] 39 | Undo removes the replace statements for the modules. If no modules are provided, it will undo all gohack replace statements. The gohack module directories are unaffected. 40 | 41 | gohack dir [-vcs] [module...] 42 | Dir prints the gohack module directory names for the given modules. If no modules are given, all the currently gohacked module directories are printed. If the -vcs flag is provided, the directory to be used in VCS mode is printed. Unlike the other subcommands, the modules don't need to be referenced by the current module. 43 | */ 44 | 45 | var ( 46 | printCommands = flag.Bool("x", false, "show executed commands") 47 | dryRun = flag.Bool("n", false, "print but do not execute update commands") 48 | ) 49 | 50 | var ( 51 | exitCode = 0 52 | cwd = "." 53 | 54 | mainModFile *modfile.File 55 | ) 56 | 57 | var commands = []*Command{ 58 | getCommand, 59 | undoCommand, 60 | statusCommand, 61 | } 62 | 63 | func main() { 64 | os.Exit(main1()) 65 | } 66 | 67 | func main1() int { 68 | if dir, err := os.Getwd(); err == nil { 69 | cwd = dir 70 | } else { 71 | return errorf("cannot get current working directory: %v", err) 72 | } 73 | flag.Usage = func() { 74 | mainUsage(os.Stderr) 75 | } 76 | flag.Parse() 77 | if flag.NArg() == 0 { 78 | mainUsage(os.Stderr) 79 | return 2 80 | } 81 | cmdName := flag.Arg(0) 82 | args := flag.Args()[1:] 83 | if cmdName == "help" { 84 | return runHelp(args) 85 | } 86 | var cmd *Command 87 | for _, c := range commands { 88 | if c.Name() == cmdName { 89 | cmd = c 90 | break 91 | } 92 | } 93 | if cmd == nil { 94 | errorf("gohack %s: unknown command\nRun 'gohack help' for usage\n", cmdName) 95 | return 2 96 | } 97 | 98 | cmd.Flag.Usage = func() { cmd.Usage() } 99 | 100 | if err := cmd.Flag.Parse(args); err != nil { 101 | if err != flag.ErrHelp { 102 | errorf(err.Error()) 103 | } 104 | return 2 105 | } 106 | 107 | if _, mf, err := goModInfo(); err == nil { 108 | mainModFile = mf 109 | } else { 110 | return errorf("cannot determine main module: %v", err) 111 | } 112 | 113 | rcode := cmd.Run(cmd, cmd.Flag.Args()) 114 | return max(exitCode, rcode) 115 | } 116 | 117 | const debug = false 118 | 119 | func errorf(f string, a ...interface{}) int { 120 | fmt.Fprintln(os.Stderr, fmt.Sprintf(f, a...)) 121 | if debug { 122 | for _, arg := range a { 123 | if err, ok := arg.(error); ok { 124 | fmt.Fprintf(os.Stderr, "error: %s\n", errors.Details(err)) 125 | } 126 | } 127 | } 128 | exitCode = 1 129 | return exitCode 130 | } 131 | 132 | func max(a, b int) int { 133 | if a > b { 134 | return a 135 | } 136 | return b 137 | } 138 | -------------------------------------------------------------------------------- /mod.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | 9 | "github.com/rogpeppe/go-internal/dirhash" 10 | "golang.org/x/tools/go/vcs" 11 | "gopkg.in/errgo.v2/fmt/errors" 12 | ) 13 | 14 | // hashDir is like dirhash.HashDir except that it ignores the 15 | // gohack hash file in the top level directory, and auto-generated 16 | // go.mod files. 17 | func hashDir(dir string, modulePath string) (string, error) { 18 | files, err := dirhash.DirFiles(dir, "") 19 | if err != nil { 20 | return "", err 21 | } 22 | j := 0 23 | for _, f := range files { 24 | if f == hashFile { 25 | continue 26 | } else if f == "go.mod" { 27 | ok, err := isAutoGoMod(filepath.Join(dir, f), modulePath) 28 | if err != nil { 29 | return "", errors.Wrap(err) 30 | } 31 | if ok { 32 | continue 33 | } 34 | } 35 | files[j] = f 36 | j++ 37 | } 38 | files = files[:j] 39 | return dirhash.Hash1(files, func(name string) (io.ReadCloser, error) { 40 | return os.Open(filepath.Join(dir, name)) 41 | }) 42 | } 43 | 44 | type moduleVCSInfo struct { 45 | // module holds the module information as printed by go list. 46 | module *listModule 47 | // alreadyExists holds whether the replacement directory already exists. 48 | alreadyExists bool 49 | // dir holds the absolute path to the replacement directory. 50 | dir string 51 | // replDir holds the path to use for the module in the go.mod replace directive. 52 | replDir string 53 | // root holds information on the VCS root of the module. 54 | root *vcs.RepoRoot 55 | // vcs holds the implementation of the VCS used by the module. 56 | vcs VCS 57 | // VCSInfo holds information on the VCS tree in the replacement 58 | // directory. It is only filled in when alreadyExists is true. 59 | VCSInfo 60 | } 61 | 62 | // getVCSInfoForModule returns VCS information about the module 63 | // by inspecting the module path and the module's checked out 64 | // directory. 65 | func getVCSInfoForModule(m *listModule) (*moduleVCSInfo, error) { 66 | // TODO if module directory already exists, could look in it to see if there's 67 | // a single VCS directory and use that if so, to avoid hitting the network 68 | // for vanity imports. 69 | root, err := vcs.RepoRootForImportPath(m.Path, *printCommands) 70 | if err != nil { 71 | return nil, errors.Note(err, nil, "cannot find module root") 72 | } 73 | v, ok := kindToVCS[root.VCS.Cmd] 74 | if !ok { 75 | return nil, errors.Newf("unknown VCS kind %q", root.VCS.Cmd) 76 | } 77 | dir, replDir, err := moduleDir(m.Path) 78 | if err != nil { 79 | return nil, errors.Notef(err, nil, "failed to determine target directory for %v", m.Path) 80 | } 81 | dirInfo, err := os.Stat(dir) 82 | if err != nil && !os.IsNotExist(err) { 83 | return nil, errors.Wrap(err) 84 | } 85 | if err == nil && !dirInfo.IsDir() { 86 | return nil, errors.Newf("%q is not a directory", dir) 87 | } 88 | info := &moduleVCSInfo{ 89 | module: m, 90 | root: root, 91 | alreadyExists: err == nil, 92 | dir: dir, 93 | replDir: replDir, 94 | vcs: v, 95 | } 96 | if !info.alreadyExists { 97 | return info, nil 98 | } 99 | // Remove the go.mod file if it was autogenerated so that the 100 | // normal VCS cleanliness detection works OK. 101 | removedGoMod, err := removeAutoGoMod(info) 102 | if err != nil { 103 | return nil, errors.Wrap(err) 104 | } 105 | info.VCSInfo, err = info.vcs.Info(dir) 106 | if err != nil { 107 | return nil, errors.Notef(err, nil, "cannot get VCS info from %q", dir) 108 | } 109 | if removedGoMod { 110 | // We removed the autogenerated go.mod file so add it back again. 111 | if err := ensureGoModFile(info.module.Path, info.dir); err != nil { 112 | return nil, errors.Wrap(err) 113 | } 114 | } 115 | return info, nil 116 | } 117 | 118 | // moduleDir returns the path to the directory to be used for storing the 119 | // module with the given path, as well as the filepath to be used in a replace 120 | // directive. If $GOHACK is set then it will be used. A relative $GOHACK will 121 | // be interpreted relative to main module directory. 122 | func moduleDir(module string) (path string, replPath string, err error) { 123 | modfp := filepath.FromSlash(module) 124 | d := os.Getenv("GOHACK") 125 | if d == "" { 126 | uhd, err := UserHomeDir() 127 | if err != nil { 128 | return "", "", errors.Notef(err, nil, "failed to determine user home dir") 129 | } 130 | path = filepath.Join(uhd, "gohack", modfp) 131 | return path, path, nil 132 | } 133 | 134 | if filepath.IsAbs(d) { 135 | path = filepath.Join(d, modfp) 136 | return path, path, nil 137 | } 138 | 139 | replPath = filepath.Join(d, modfp) 140 | if !strings.HasPrefix(replPath, ".."+string(os.PathSeparator)) { 141 | // We know replPath is relative, but filepath.Join strips any leading 142 | // "./" prefix, and we need that in the replace directive because 143 | // otherwise the path will be treated as a module path rather than a 144 | // relative file path, so add it back. 145 | replPath = "." + string(os.PathSeparator) + replPath 146 | } 147 | 148 | mainModDir := filepath.Dir(mainModFile.Syntax.Name) 149 | path = filepath.Join(mainModDir, replPath) 150 | 151 | return path, replPath, err 152 | } 153 | -------------------------------------------------------------------------------- /os_1.11.go: -------------------------------------------------------------------------------- 1 | // +build !go1.12 2 | 3 | package main 4 | 5 | import ( 6 | "errors" 7 | "os" 8 | "runtime" 9 | ) 10 | 11 | // UserHomeDir was introduced in Go 1.12. When we drop support for Go 1.11, we can 12 | // lose this file. 13 | 14 | // UserHomeDir returns the current user's home directory. 15 | // 16 | // On Unix, including macOS, it returns the $HOME environment variable. 17 | // On Windows, it returns %USERPROFILE%. 18 | // On Plan 9, it returns the $home environment variable. 19 | func UserHomeDir() (string, error) { 20 | env, enverr := "HOME", "$HOME" 21 | switch runtime.GOOS { 22 | case "windows": 23 | env, enverr = "USERPROFILE", "%userprofile%" 24 | case "plan9": 25 | env, enverr = "home", "$home" 26 | case "nacl", "android": 27 | return "/", nil 28 | case "darwin": 29 | if runtime.GOARCH == "arm" || runtime.GOARCH == "arm64" { 30 | return "/", nil 31 | } 32 | } 33 | if v := os.Getenv(env); v != "" { 34 | return v, nil 35 | } 36 | return "", errors.New(enverr + " is not defined") 37 | } 38 | -------------------------------------------------------------------------------- /os_1.12.go: -------------------------------------------------------------------------------- 1 | // +build go1.12 2 | 3 | package main 4 | 5 | import "os" 6 | 7 | // UserHomeDir was introduced in Go 1.12. When we drop support for Go 1.11, we can 8 | // lose this file. 9 | 10 | func UserHomeDir() (string, error) { 11 | return os.UserHomeDir() 12 | } 13 | -------------------------------------------------------------------------------- /pseudo.go: -------------------------------------------------------------------------------- 1 | // from $GOROOT/src/cmd/go/internal/modfetch/pseudo.go 2 | 3 | // Copyright 2018 The Go Authors. All rights reserved. 4 | // Use of this source code is governed by a BSD-style 5 | // license that can be found in the LICENSE file. 6 | 7 | // Pseudo-versions 8 | // 9 | // Code authors are expected to tag the revisions they want users to use, 10 | // including prereleases. However, not all authors tag versions at all, 11 | // and not all commits a user might want to try will have tags. 12 | // A pseudo-version is a version with a special form that allows us to 13 | // address an untagged commit and order that version with respect to 14 | // other versions we might encounter. 15 | // 16 | // A pseudo-version takes one of the general forms: 17 | // 18 | // (1) vX.0.0-yyyymmddhhmmss-abcdef123456 19 | // (2) vX.Y.(Z+1)-0.yyyymmddhhmmss-abcdef123456 20 | // (3) vX.Y.(Z+1)-0.yyyymmddhhmmss-abcdef123456+incompatible 21 | // (4) vX.Y.Z-pre.0.yyyymmddhhmmss-abcdef123456 22 | // (5) vX.Y.Z-pre.0.yyyymmddhhmmss-abcdef123456+incompatible 23 | // 24 | // If there is no recently tagged version with the right major version vX, 25 | // then form (1) is used, creating a space of pseudo-versions at the bottom 26 | // of the vX version range, less than any tagged version, including the unlikely v0.0.0. 27 | // 28 | // If the most recent tagged version before the target commit is vX.Y.Z or vX.Y.Z+incompatible, 29 | // then the pseudo-version uses form (2) or (3), making it a prerelease for the next 30 | // possible semantic version after vX.Y.Z. The leading 0 segment in the prerelease string 31 | // ensures that the pseudo-version compares less than possible future explicit prereleases 32 | // like vX.Y.(Z+1)-rc1 or vX.Y.(Z+1)-1. 33 | // 34 | // If the most recent tagged version before the target commit is vX.Y.Z-pre or vX.Y.Z-pre+incompatible, 35 | // then the pseudo-version uses form (4) or (5), making it a slightly later prerelease. 36 | 37 | package main 38 | 39 | import ( 40 | "fmt" 41 | "regexp" 42 | "strings" 43 | "time" 44 | 45 | "github.com/rogpeppe/go-internal/semver" 46 | ) 47 | 48 | // PseudoVersion returns a pseudo-version for the given major version ("v1") 49 | // preexisting older tagged version ("" or "v1.2.3" or "v1.2.3-pre"), revision time, 50 | // and revision identifier (usually a 12-byte commit hash prefix). 51 | func PseudoVersion(major, older string, t time.Time, rev string) string { 52 | if major == "" { 53 | major = "v0" 54 | } 55 | segment := fmt.Sprintf("%s-%s", t.UTC().Format("20060102150405"), rev) 56 | build := semver.Build(older) 57 | older = semver.Canonical(older) 58 | if older == "" { 59 | return major + ".0.0-" + segment // form (1) 60 | } 61 | if semver.Prerelease(older) != "" { 62 | return older + ".0." + segment + build // form (4), (5) 63 | } 64 | 65 | // Form (2), (3). 66 | // Extract patch from vMAJOR.MINOR.PATCH 67 | v := older[:len(older)] 68 | i := strings.LastIndex(v, ".") + 1 69 | v, patch := v[:i], v[i:] 70 | 71 | // Increment PATCH by adding 1 to decimal: 72 | // scan right to left turning 9s to 0s until you find a digit to increment. 73 | // (Number might exceed int64, but math/big is overkill.) 74 | digits := []byte(patch) 75 | for i = len(digits) - 1; i >= 0 && digits[i] == '9'; i-- { 76 | digits[i] = '0' 77 | } 78 | if i >= 0 { 79 | digits[i]++ 80 | } else { 81 | // digits is all zeros 82 | digits[0] = '1' 83 | digits = append(digits, '0') 84 | } 85 | patch = string(digits) 86 | 87 | // Reassemble. 88 | return v + patch + "-0." + segment + build 89 | } 90 | 91 | var pseudoVersionRE = regexp.MustCompile(`^v[0-9]+\.(0\.0-|\d+\.\d+-([^+]*\.)?0\.)\d{14}-[A-Za-z0-9]+(\+incompatible)?$`) 92 | 93 | // IsPseudoVersion reports whether v is a pseudo-version. 94 | func IsPseudoVersion(v string) bool { 95 | return strings.Count(v, "-") >= 2 && semver.IsValid(v) && pseudoVersionRE.MatchString(v) 96 | } 97 | 98 | // PseudoVersionTime returns the time stamp of the pseudo-version v. 99 | // It returns an error if v is not a pseudo-version or if the time stamp 100 | // embedded in the pseudo-version is not a valid time. 101 | func PseudoVersionTime(v string) (time.Time, error) { 102 | timestamp, _, err := parsePseudoVersion(v) 103 | t, err := time.Parse("20060102150405", timestamp) 104 | if err != nil { 105 | return time.Time{}, fmt.Errorf("pseudo-version with malformed time %s: %q", timestamp, v) 106 | } 107 | return t, nil 108 | } 109 | 110 | // PseudoVersionRev returns the revision identifier of the pseudo-version v. 111 | // It returns an error if v is not a pseudo-version. 112 | func PseudoVersionRev(v string) (rev string, err error) { 113 | _, rev, err = parsePseudoVersion(v) 114 | return 115 | } 116 | 117 | func parsePseudoVersion(v string) (timestamp, rev string, err error) { 118 | if !IsPseudoVersion(v) { 119 | return "", "", fmt.Errorf("malformed pseudo-version %q", v) 120 | } 121 | v = strings.TrimSuffix(v, "+incompatible") 122 | j := strings.LastIndex(v, "-") 123 | v, rev = v[:j], v[j+1:] 124 | i := strings.LastIndex(v, "-") 125 | if j := strings.LastIndex(v, "."); j > i { 126 | timestamp = v[j+1:] 127 | } else { 128 | timestamp = v[i+1:] 129 | } 130 | return timestamp, rev, nil 131 | } 132 | -------------------------------------------------------------------------------- /script_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "testing" 7 | 8 | "github.com/rogpeppe/go-internal/goproxytest" 9 | "github.com/rogpeppe/go-internal/gotooltest" 10 | "github.com/rogpeppe/go-internal/testscript" 11 | ) 12 | 13 | var proxyURL string 14 | 15 | func TestMain(m *testing.M) { 16 | os.Exit(testscript.RunMain(gohackMain{m}, map[string]func() int{ 17 | "gohack": main1, 18 | })) 19 | } 20 | 21 | type gohackMain struct { 22 | m *testing.M 23 | } 24 | 25 | func (m gohackMain) Run() int { 26 | if os.Getenv("GO_GCFLAGS") != "" { 27 | fmt.Fprintf(os.Stderr, "testing: warning: no tests to run\n") // magic string for cmd/go 28 | fmt.Printf("cmd/go test is not compatible with $GO_GCFLAGS being set\n") 29 | fmt.Printf("SKIP\n") 30 | return 0 31 | } 32 | os.Unsetenv("GOROOT_FINAL") 33 | 34 | // Start the Go proxy server running for all tests. 35 | srv, err := goproxytest.NewServer("testdata/mod", "") 36 | if err != nil { 37 | errorf("cannot start proxy: %v", err) 38 | return 1 39 | } 40 | proxyURL = srv.URL 41 | 42 | return m.m.Run() 43 | } 44 | 45 | func TestScripts(t *testing.T) { 46 | p := testscript.Params{ 47 | Dir: "testdata", 48 | Setup: func(e *testscript.Env) error { 49 | e.Vars = append(e.Vars, 50 | "GOPROXY="+proxyURL, 51 | "GONOSUMDB=*", 52 | ) 53 | return nil 54 | }, 55 | } 56 | if err := gotooltest.Setup(&p); err != nil { 57 | t.Fatal(err) 58 | } 59 | testscript.Run(t, p) 60 | } 61 | -------------------------------------------------------------------------------- /testdata/get-force.txt: -------------------------------------------------------------------------------- 1 | cd repo 2 | go get rsc.io/quote@v1.5.2 3 | env GOHACK=$WORK/gohack 4 | gohack get rsc.io/quote 5 | stdout '^rsc.io/quote => .*/gohack/rsc.io/quote$' 6 | ! stderr .+ 7 | 8 | # Undo the gohack, leaving the source files in place. 9 | gohack undo rsc.io/quote 10 | ! stderr .+ 11 | stdout '^dropped rsc.io/quote$' 12 | 13 | # Modify the hacked source. 14 | cp ../bogus.go $WORK/gohack/rsc.io/quote/bogus.go 15 | 16 | # When we get again, we can't get it because it's dirty 17 | ! gohack get rsc.io/quote 18 | ! stdout .+ 19 | stderr '^cannot update rsc.io/quote from local cache: ".*/gohack/rsc.io/quote" is not clean; not overwriting$' 20 | stderr '^all modules failed; not replacing anything$' 21 | 22 | # With the force flag, we can update it; the source directory will 23 | # be returned to its pristine state. 24 | gohack get -f rsc.io/quote 25 | stdout '^rsc.io/quote => .*/gohack/rsc.io/quote$' 26 | grep -count=1 '^replace rsc\.io/quote => .*/gohack/rsc.io/quote$' go.mod 27 | ! exists $WORK/gohack/rsc.io/quote/bogus.go 28 | go install example.com/repo 29 | 30 | -- repo/main.go -- 31 | package main 32 | import ( 33 | "fmt" 34 | "rsc.io/quote" 35 | ) 36 | 37 | func main() { 38 | fmt.Println(quote.Glass()) 39 | } 40 | 41 | -- repo/go.mod -- 42 | module example.com/repo 43 | 44 | -- bogus.go -- 45 | 46 | package wrong 47 | -------------------------------------------------------------------------------- /testdata/get-no-gomod.txt: -------------------------------------------------------------------------------- 1 | # Get a module that has no go.mod file. Gohack should add the 2 | # go.mod file for us. 3 | cd repo 4 | go get rsc.io/sampler@v1.2.1 5 | env GOHACK=$WORK/gohack 6 | gohack get rsc.io/sampler 7 | 8 | exists $GOHACK/rsc.io/sampler/go.mod 9 | gohack undo rsc.io/sampler 10 | exists $GOHACK/rsc.io/sampler/go.mod 11 | 12 | # Run gohack get a second time. 13 | # This should reuse the same directory, which it should 14 | # consider clean even though it contains the auto-generated 15 | # go.mod file. 16 | gohack get rsc.io/sampler 17 | stdout '^rsc.io/sampler => .*/gohack/rsc.io/sampler$' 18 | grep -count=1 '^replace rsc\.io/sampler => .*/gohack/rsc.io/sampler$' go.mod 19 | 20 | -- repo/main.go -- 21 | package main 22 | import ( 23 | _ "rsc.io/sampler" 24 | ) 25 | 26 | func main() {} 27 | 28 | -- repo/go.mod -- 29 | module example.com/repo 30 | -------------------------------------------------------------------------------- /testdata/get-no-main-mod.txt: -------------------------------------------------------------------------------- 1 | ! gohack get nothing 2 | stderr 'cannot get module info: go: cannot match "all": go.mod file not found in current directory or any parent directory' 3 | -------------------------------------------------------------------------------- /testdata/get-push-replace.txt: -------------------------------------------------------------------------------- 1 | cd repo 2 | go get rsc.io/sampler@v1.3.0 3 | env GOHACK=$WORK/gohack 4 | gohack get rsc.io/sampler 5 | 6 | # We should have checked out the replaced 7 | # version, not the original version. 8 | grep '99 bottles of beer' $GOHACK/rsc.io/sampler/hello.go 9 | 10 | # The go.mod file should have included the original replace 11 | # version as a comment. 12 | grep 'rsc\.io/sampler => .*/gohack/rsc\.io/sampler *// was rsc.io/sampler v1.3.0 => rsc.io/sampler v1.99.99 // old comment$' go.mod 13 | 14 | # Undoing the gohack should restore the original replace statement. 15 | gohack undo 16 | grep 'rsc.io/sampler v1.3.0 => rsc.io/sampler v1.99.99 // old comment$' go.mod 17 | 18 | -- repo/main.go -- 19 | package main 20 | import ( 21 | "fmt" 22 | "rsc.io/sampler" 23 | ) 24 | 25 | func main() { 26 | sampler.DefaultUserPrefs() 27 | } 28 | 29 | -- repo/go.mod -- 30 | module example.com/repo 31 | 32 | replace ( 33 | rsc.io/sampler v1.3.0 => rsc.io/sampler v1.99.99 // old comment 34 | ) 35 | -------------------------------------------------------------------------------- /testdata/get-relative-parent.txt: -------------------------------------------------------------------------------- 1 | cd repo/dummy 2 | go get rsc.io/quote@v1.5.2 3 | env GOHACK=../gohack 4 | gohack get rsc.io/quote 5 | stdout '^rsc.io/quote => \.\./gohack/rsc.io/quote$' 6 | ! stderr .+ 7 | 8 | # move back to the module root 9 | cd .. 10 | 11 | # Check that the replace statement is there. 12 | grep -count=1 '^replace rsc\.io/quote => \.\./gohack/rsc.io/quote$' go.mod 13 | 14 | # Check that the source files have been copied. 15 | exists ../gohack/rsc.io/quote/quote.go 16 | 17 | # Check that we can compile the command OK. 18 | go install example.com/repo 19 | 20 | -- repo/main.go -- 21 | package main 22 | import ( 23 | "fmt" 24 | "rsc.io/quote" 25 | ) 26 | 27 | func main() { 28 | fmt.Println(quote.Glass()) 29 | } 30 | 31 | -- repo/dummy/file -- 32 | 33 | // This dummy file exists in order to create the dummy 34 | // directory so that the gohack command can be run from 35 | // within the dummy directory 36 | 37 | 38 | -- repo/go.mod -- 39 | module example.com/repo 40 | -------------------------------------------------------------------------------- /testdata/get-relative.txt: -------------------------------------------------------------------------------- 1 | cd repo/dummy 2 | go get rsc.io/quote@v1.5.2 3 | env GOHACK=. 4 | gohack get rsc.io/quote 5 | stdout '^rsc.io/quote => \./rsc.io/quote$' 6 | ! stderr .+ 7 | 8 | # move back to the module root 9 | cd .. 10 | 11 | # Check that the replace statement is there. 12 | grep -count=1 '^replace rsc\.io/quote => \./rsc.io/quote$' go.mod 13 | 14 | # Check that the source files have been copied. 15 | exists ./rsc.io/quote/quote.go 16 | 17 | # It should not be a git repository. 18 | ! exists ./rsc.io/quote/.git 19 | 20 | # Check that we can compile the command OK. 21 | go install example.com/repo 22 | 23 | -- repo/main.go -- 24 | package main 25 | import ( 26 | "fmt" 27 | "rsc.io/quote" 28 | ) 29 | 30 | func main() { 31 | fmt.Println(quote.Glass()) 32 | } 33 | 34 | -- repo/dummy/file -- 35 | 36 | // This dummy file exists in order to create the dummy 37 | // directory so that the gohack command can be run from 38 | // within the dummy directory 39 | 40 | -- repo/go.mod -- 41 | module example.com/repo 42 | -------------------------------------------------------------------------------- /testdata/get-vcs-relative-parent.txt: -------------------------------------------------------------------------------- 1 | # This is same as get.txt except that we use the -vcs flag on gohack get with a 2 | # parent-relative GOHACK set. Until we can figure out better, this requires 3 | # access to the network. 4 | 5 | [!net] skip 6 | [!exec:git] skip 7 | env GOPROXY= 8 | 9 | cd repo/dummy 10 | go get rsc.io/quote@v1.5.2 11 | env GOHACK=../gohack 12 | gohack get -vcs rsc.io/quote 13 | stdout '^rsc.io/quote => \.\./gohack/rsc.io/quote$' 14 | ! stderr .+ 15 | 16 | # move back to the module root 17 | cd .. 18 | 19 | # Check that the replace statement is there. 20 | grep -count=1 '^replace rsc\.io/quote => \.\./gohack/rsc.io/quote$' go.mod 21 | 22 | # Check that it's a git repository 23 | exists ../gohack/rsc.io/quote/.git 24 | 25 | # Check that the source files have been copied. 26 | exists ../gohack/rsc.io/quote/quote.go 27 | 28 | # Check that we can compile the command OK. 29 | go install example.com/repo 30 | 31 | -- repo/main.go -- 32 | 33 | package main 34 | import ( 35 | "fmt" 36 | "rsc.io/quote" 37 | ) 38 | 39 | func main() { 40 | fmt.Println(quote.Glass()) 41 | } 42 | 43 | -- repo/dummy/file -- 44 | 45 | // This dummy file exists in order to create the dummy 46 | // directory so that the gohack command can be run from 47 | // within the dummy directory 48 | 49 | -- repo/go.mod -- 50 | module example.com/repo 51 | 52 | -- bogus.go -- 53 | 54 | package wrong 55 | -------------------------------------------------------------------------------- /testdata/get-vcs-relative.txt: -------------------------------------------------------------------------------- 1 | # This is same as get.txt except that we use the -vcs flag on gohack get with a 2 | # relative GOHACK set. Until we can figure out better, this requires access to 3 | # the network. 4 | 5 | [!net] skip 6 | [!exec:git] skip 7 | env GOPROXY= 8 | 9 | cd repo/dummy 10 | go get rsc.io/quote@v1.5.2 11 | env GOHACK=. 12 | gohack get -vcs rsc.io/quote 13 | stdout '^rsc.io/quote => \./rsc.io/quote$' 14 | ! stderr .+ 15 | 16 | # move back to the module root 17 | cd .. 18 | 19 | # Check that the replace statement is there. 20 | grep -count=1 '^replace rsc\.io/quote => \./rsc.io/quote$' go.mod 21 | 22 | # Check that it's a git repository 23 | exists ./rsc.io/quote/.git 24 | 25 | # Check that the source files have been copied. 26 | exists ./rsc.io/quote/quote.go 27 | 28 | # Check that we can compile the command OK. 29 | go install example.com/repo 30 | 31 | -- repo/main.go -- 32 | 33 | package main 34 | import ( 35 | "fmt" 36 | "rsc.io/quote" 37 | ) 38 | 39 | func main() { 40 | fmt.Println(quote.Glass()) 41 | } 42 | 43 | -- repo/dummy/file -- 44 | 45 | // This dummy file exists in order to create the dummy 46 | // directory so that the gohack command can be run from 47 | // within the dummy directory 48 | 49 | -- repo/go.mod -- 50 | module example.com/repo 51 | 52 | -- bogus.go -- 53 | 54 | package wrong 55 | -------------------------------------------------------------------------------- /testdata/get-vcs.txt: -------------------------------------------------------------------------------- 1 | # This is same as get.txt except that we use the -vcs flag on gohack get. Until 2 | # we can figure out better, this requires access to the network. 3 | 4 | [!net] skip 5 | [!exec:git] skip 6 | env GOPROXY= 7 | 8 | cd repo 9 | go get rsc.io/quote@v1.5.2 10 | env GOHACK=$WORK/gohack 11 | gohack get -vcs rsc.io/quote 12 | stdout '^rsc.io/quote => .*/gohack/rsc.io/quote$' 13 | ! stderr .+ 14 | 15 | # Check that the replace statement is there. 16 | grep -count=1 '^replace rsc\.io/quote => .*/gohack/rsc.io/quote$' go.mod 17 | 18 | # Check that it's a git repository 19 | exists $WORK/gohack/rsc.io/quote/.git 20 | 21 | # Check that the source files have been copied. 22 | grep '^' $WORK/gohack/rsc.io/quote/quote.go 23 | 24 | # Check that we can compile the command OK. 25 | go install example.com/repo 26 | 27 | # Hack the package a bit and check that it doesn't compile 28 | # any more. 29 | cp ../bogus.go $WORK/gohack/rsc.io/quote/bogus.go 30 | ! go install example.com/repo 31 | stderr 'found packages wrong \(bogus\.go\) and quote \(quote\.go\)' 32 | 33 | # Use git to reset the package. 34 | cd $WORK/gohack/rsc.io/quote 35 | exec git clean -xf 36 | 37 | # We should be able to compile it again. 38 | cd $WORK/repo 39 | go install 40 | 41 | -- repo/main.go -- 42 | 43 | package main 44 | import ( 45 | "fmt" 46 | "rsc.io/quote" 47 | ) 48 | 49 | func main() { 50 | fmt.Println(quote.Glass()) 51 | } 52 | 53 | -- repo/go.mod -- 54 | module example.com/repo 55 | 56 | -- bogus.go -- 57 | 58 | package wrong 59 | -------------------------------------------------------------------------------- /testdata/get.txt: -------------------------------------------------------------------------------- 1 | cd repo 2 | go get rsc.io/quote@v1.5.2 3 | env GOHACK=$WORK/gohack 4 | gohack get rsc.io/quote 5 | stdout '^rsc.io/quote => .*/gohack/rsc.io/quote$' 6 | ! stderr .+ 7 | 8 | # Check that the replace statement is there. 9 | grep -count=1 '^replace rsc\.io/quote => .*/gohack/rsc.io/quote$' go.mod 10 | 11 | # Check that the source files have been copied. 12 | grep '^' $WORK/gohack/rsc.io/quote/quote.go 13 | 14 | # It should not be a git repository. 15 | ! exists $WORK/gohack/rsc.io/quote/.git 16 | 17 | # Check that we can compile the command OK. 18 | go install example.com/repo 19 | 20 | # Hack the package a bit and check that it doesn't compile 21 | # any more. 22 | cp ../bogus.go $WORK/gohack/rsc.io/quote/bogus.go 23 | ! go install example.com/repo 24 | stderr 'found packages wrong \(bogus\.go\) and quote \(quote\.go\)' 25 | 26 | -- repo/main.go -- 27 | package main 28 | import ( 29 | "fmt" 30 | "rsc.io/quote" 31 | ) 32 | 33 | func main() { 34 | fmt.Println(quote.Glass()) 35 | } 36 | 37 | -- repo/go.mod -- 38 | module example.com/repo 39 | 40 | -- bogus.go -- 41 | 42 | package wrong 43 | -------------------------------------------------------------------------------- /testdata/help.txt: -------------------------------------------------------------------------------- 1 | # --help flag produces output to stderr and fails 2 | ! gohack get --help 3 | stderr '^usage: get \[-vcs] \[-u] \[-f] \[module...]\nRun ''gohack help get'' for details.\n' 4 | ! stdout .+ 5 | 6 | gohack help get 7 | stdout '^usage: get \[-vcs] \[-u] \[-f] \[module...]$' 8 | ! stderr .+ 9 | -------------------------------------------------------------------------------- /testdata/mod/golang.org_x_text_v0.0.0-20170915032832-14c0d48ead0c.txt: -------------------------------------------------------------------------------- 1 | written by hand - just enough to compile rsc.io/sampler, rsc.io/quote 2 | 3 | -- .mod -- 4 | module golang.org/x/text 5 | -- .info -- 6 | {"Version":"v0.0.0-20170915032832-14c0d48ead0c","Name":"v0.0.0-20170915032832-14c0d48ead0c","Short":"14c0d48ead0c","Time":"2017-09-15T03:28:32Z"} 7 | -- go.mod -- 8 | module golang.org/x/text 9 | -- unused/unused.go -- 10 | package unused 11 | -- language/lang.go -- 12 | // Copyright 2018 The Go Authors. All rights reserved. 13 | // Use of this source code is governed by a BSD-style 14 | // license that can be found in the LICENSE file. 15 | 16 | // This is a tiny version of golang.org/x/text. 17 | 18 | package language 19 | 20 | import "strings" 21 | 22 | type Tag string 23 | 24 | func Make(s string) Tag { return Tag(s) } 25 | 26 | func (t Tag) String() string { return string(t) } 27 | 28 | func NewMatcher(tags []Tag) Matcher { return &matcher{tags} } 29 | 30 | type Matcher interface { 31 | Match(...Tag) (Tag, int, int) 32 | } 33 | 34 | type matcher struct { 35 | tags []Tag 36 | } 37 | 38 | func (m *matcher) Match(prefs ...Tag) (Tag, int, int) { 39 | for _, pref := range prefs { 40 | for _, tag := range m.tags { 41 | if tag == pref || strings.HasPrefix(string(pref), string(tag+"-")) || strings.HasPrefix(string(tag), string(pref+"-")) { 42 | return tag, 0, 0 43 | } 44 | } 45 | } 46 | return m.tags[0], 0, 0 47 | } 48 | -------------------------------------------------------------------------------- /testdata/mod/rsc.io_quote_v1.5.2.txt: -------------------------------------------------------------------------------- 1 | module rsc.io/quote@v1.5.2 2 | 3 | -- .mod -- 4 | module "rsc.io/quote" 5 | 6 | require "rsc.io/sampler" v1.3.0 7 | -- .info -- 8 | {"Version":"v1.5.2","Time":"2018-02-14T15:44:20Z"} 9 | -- buggy/buggy_test.go -- 10 | // Copyright 2018 The Go Authors. All rights reserved. 11 | // Use of this source code is governed by a BSD-style 12 | // license that can be found in the LICENSE file. 13 | 14 | package buggy 15 | 16 | import "testing" 17 | 18 | func Test(t *testing.T) { 19 | t.Fatal("buggy!") 20 | } 21 | -- go.mod -- 22 | module "rsc.io/quote" 23 | 24 | require "rsc.io/sampler" v1.3.0 25 | -- quote.go -- 26 | // Copyright 2018 The Go Authors. All rights reserved. 27 | // Use of this source code is governed by a BSD-style 28 | // license that can be found in the LICENSE file. 29 | 30 | // Package quote collects pithy sayings. 31 | package quote // import "rsc.io/quote" 32 | 33 | import "rsc.io/sampler" 34 | 35 | // Hello returns a greeting. 36 | func Hello() string { 37 | return sampler.Hello() 38 | } 39 | 40 | // Glass returns a useful phrase for world travelers. 41 | func Glass() string { 42 | // See http://www.oocities.org/nodotus/hbglass.html. 43 | return "I can eat glass and it doesn't hurt me." 44 | } 45 | 46 | // Go returns a Go proverb. 47 | func Go() string { 48 | return "Don't communicate by sharing memory, share memory by communicating." 49 | } 50 | 51 | // Opt returns an optimization truth. 52 | func Opt() string { 53 | // Wisdom from ken. 54 | return "If a program is too slow, it must have a loop." 55 | } 56 | -- quote_test.go -- 57 | // Copyright 2018 The Go Authors. All rights reserved. 58 | // Use of this source code is governed by a BSD-style 59 | // license that can be found in the LICENSE file. 60 | 61 | package quote 62 | 63 | import ( 64 | "os" 65 | "testing" 66 | ) 67 | 68 | func init() { 69 | os.Setenv("LC_ALL", "en") 70 | } 71 | 72 | func TestHello(t *testing.T) { 73 | hello := "Hello, world." 74 | if out := Hello(); out != hello { 75 | t.Errorf("Hello() = %q, want %q", out, hello) 76 | } 77 | } 78 | 79 | func TestGlass(t *testing.T) { 80 | glass := "I can eat glass and it doesn't hurt me." 81 | if out := Glass(); out != glass { 82 | t.Errorf("Glass() = %q, want %q", out, glass) 83 | } 84 | } 85 | 86 | func TestGo(t *testing.T) { 87 | go1 := "Don't communicate by sharing memory, share memory by communicating." 88 | if out := Go(); out != go1 { 89 | t.Errorf("Go() = %q, want %q", out, go1) 90 | } 91 | } 92 | 93 | func TestOpt(t *testing.T) { 94 | opt := "If a program is too slow, it must have a loop." 95 | if out := Opt(); out != opt { 96 | t.Errorf("Opt() = %q, want %q", out, opt) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /testdata/mod/rsc.io_sampler_v1.2.1.txt: -------------------------------------------------------------------------------- 1 | generated by ./addmod.bash rsc.io/sampler@v1.2.1 2 | 3 | -- .mod -- 4 | module "rsc.io/sampler" 5 | 6 | require "golang.org/x/text" v0.0.0-20170915032832-14c0d48ead0c 7 | -- .info -- 8 | {"Version":"v1.2.1","Name":"cac3af4f8a0ab40054fa6f8d423108a63a1255bb","Short":"cac3af4f8a0a","Time":"2018-02-13T18:16:22Z"} 9 | -- hello.go -- 10 | // Copyright 2018 The Go Authors. All rights reserved. 11 | // Use of this source code is governed by a BSD-style 12 | // license that can be found in the LICENSE file. 13 | 14 | // Translations by Google Translate. 15 | 16 | package sampler 17 | 18 | var hello = newText(` 19 | 20 | English: en: Hello, world. 21 | French: fr: Bonjour le monde. 22 | Spanish: es: Hola Mundo. 23 | 24 | `) 25 | -- hello_test.go -- 26 | // Copyright 2018 The Go Authors. All rights reserved. 27 | // Use of this source code is governed by a BSD-style 28 | // license that can be found in the LICENSE file. 29 | 30 | package sampler 31 | 32 | import ( 33 | "testing" 34 | 35 | "golang.org/x/text/language" 36 | ) 37 | 38 | var helloTests = []struct { 39 | prefs []language.Tag 40 | text string 41 | }{ 42 | { 43 | []language.Tag{language.Make("en-US"), language.Make("fr")}, 44 | "Hello, world.", 45 | }, 46 | { 47 | []language.Tag{language.Make("fr"), language.Make("en-US")}, 48 | "Bonjour le monde.", 49 | }, 50 | } 51 | 52 | func TestHello(t *testing.T) { 53 | for _, tt := range helloTests { 54 | text := Hello(tt.prefs...) 55 | if text != tt.text { 56 | t.Errorf("Hello(%v) = %q, want %q", tt.prefs, text, tt.text) 57 | } 58 | } 59 | } 60 | -- sampler.go -- 61 | // Copyright 2018 The Go Authors. All rights reserved. 62 | // Use of this source code is governed by a BSD-style 63 | // license that can be found in the LICENSE file. 64 | 65 | // Package sampler shows simple texts. 66 | package sampler // import "rsc.io/sampler" 67 | 68 | import ( 69 | "os" 70 | "strings" 71 | 72 | "golang.org/x/text/language" 73 | ) 74 | 75 | // DefaultUserPrefs returns the default user language preferences. 76 | // It consults the $LC_ALL, $LC_MESSAGES, and $LANG environment 77 | // variables, in that order. 78 | func DefaultUserPrefs() []language.Tag { 79 | var prefs []language.Tag 80 | for _, k := range []string{"LC_ALL", "LC_MESSAGES", "LANG"} { 81 | if env := os.Getenv(k); env != "" { 82 | prefs = append(prefs, language.Make(env)) 83 | } 84 | } 85 | return prefs 86 | } 87 | 88 | // Hello returns a localized greeting. 89 | // If no prefs are given, Hello uses DefaultUserPrefs. 90 | func Hello(prefs ...language.Tag) string { 91 | if len(prefs) == 0 { 92 | prefs = DefaultUserPrefs() 93 | } 94 | return hello.find(prefs) 95 | } 96 | 97 | // A text is a localized text. 98 | type text struct { 99 | byTag map[string]string 100 | matcher language.Matcher 101 | } 102 | 103 | // newText creates a new localized text, given a list of translations. 104 | func newText(s string) *text { 105 | t := &text{ 106 | byTag: make(map[string]string), 107 | } 108 | var tags []language.Tag 109 | for _, line := range strings.Split(s, "\n") { 110 | line = strings.TrimSpace(line) 111 | if line == "" { 112 | continue 113 | } 114 | f := strings.Split(line, ": ") 115 | if len(f) != 3 { 116 | continue 117 | } 118 | tag := language.Make(f[1]) 119 | tags = append(tags, tag) 120 | t.byTag[tag.String()] = f[2] 121 | } 122 | t.matcher = language.NewMatcher(tags) 123 | return t 124 | } 125 | 126 | // find finds the text to use for the given language tag preferences. 127 | func (t *text) find(prefs []language.Tag) string { 128 | tag, _, _ := t.matcher.Match(prefs...) 129 | s := t.byTag[tag.String()] 130 | if strings.HasPrefix(s, "RTL ") { 131 | s = "\u200F" + strings.TrimPrefix(s, "RTL ") + "\u200E" 132 | } 133 | return s 134 | } 135 | -------------------------------------------------------------------------------- /testdata/mod/rsc.io_sampler_v1.3.0.txt: -------------------------------------------------------------------------------- 1 | rsc.io/sampler@v1.3.0 2 | 3 | -- .mod -- 4 | module "rsc.io/sampler" 5 | 6 | require "golang.org/x/text" v0.0.0-20170915032832-14c0d48ead0c 7 | -- .info -- 8 | {"Version":"v1.3.0","Name":"0cc034b51e57ed7832d4c67d526f75a900996e5c","Short":"0cc034b51e57","Time":"2018-02-13T19:05:03Z"} 9 | -- glass.go -- 10 | // Copyright 2018 The Go Authors. All rights reserved. 11 | // Use of this source code is governed by a BSD-style 12 | // license that can be found in the LICENSE file. 13 | 14 | // Translations from Frank da Cruz, Ethan Mollick, and many others. 15 | // See http://kermitproject.org/utf8.html. 16 | // http://www.oocities.org/nodotus/hbglass.html 17 | // https://en.wikipedia.org/wiki/I_Can_Eat_Glass 18 | 19 | package sampler 20 | 21 | var glass = newText(` 22 | 23 | English: en: I can eat glass and it doesn't hurt me. 24 | French: fr: Je peux manger du verre, ça ne me fait pas mal. 25 | Spanish: es: Puedo comer vidrio, no me hace daño. 26 | 27 | `) 28 | -- glass_test.go -- 29 | // Copyright 2018 The Go Authors. All rights reserved. 30 | // Use of this source code is governed by a BSD-style 31 | // license that can be found in the LICENSE file. 32 | 33 | package sampler 34 | 35 | import ( 36 | "testing" 37 | 38 | "golang.org/x/text/language" 39 | _ "rsc.io/testonly" 40 | ) 41 | 42 | var glassTests = []struct { 43 | prefs []language.Tag 44 | text string 45 | }{ 46 | { 47 | []language.Tag{language.Make("en-US"), language.Make("fr")}, 48 | "I can eat glass and it doesn't hurt me.", 49 | }, 50 | { 51 | []language.Tag{language.Make("fr"), language.Make("en-US")}, 52 | "Je peux manger du verre, ça ne me fait pas mal.", 53 | }, 54 | } 55 | 56 | func TestGlass(t *testing.T) { 57 | for _, tt := range glassTests { 58 | text := Glass(tt.prefs...) 59 | if text != tt.text { 60 | t.Errorf("Glass(%v) = %q, want %q", tt.prefs, text, tt.text) 61 | } 62 | } 63 | } 64 | -- go.mod -- 65 | module "rsc.io/sampler" 66 | 67 | require "golang.org/x/text" v0.0.0-20170915032832-14c0d48ead0c 68 | -- hello.go -- 69 | // Copyright 2018 The Go Authors. All rights reserved. 70 | // Use of this source code is governed by a BSD-style 71 | // license that can be found in the LICENSE file. 72 | 73 | // Translations by Google Translate. 74 | 75 | package sampler 76 | 77 | var hello = newText(` 78 | 79 | English: en: Hello, world. 80 | French: fr: Bonjour le monde. 81 | Spanish: es: Hola Mundo. 82 | 83 | `) 84 | -- hello_test.go -- 85 | // Copyright 2018 The Go Authors. All rights reserved. 86 | // Use of this source code is governed by a BSD-style 87 | // license that can be found in the LICENSE file. 88 | 89 | package sampler 90 | 91 | import ( 92 | "testing" 93 | 94 | "golang.org/x/text/language" 95 | ) 96 | 97 | var helloTests = []struct { 98 | prefs []language.Tag 99 | text string 100 | }{ 101 | { 102 | []language.Tag{language.Make("en-US"), language.Make("fr")}, 103 | "Hello, world.", 104 | }, 105 | { 106 | []language.Tag{language.Make("fr"), language.Make("en-US")}, 107 | "Bonjour le monde.", 108 | }, 109 | } 110 | 111 | func TestHello(t *testing.T) { 112 | for _, tt := range helloTests { 113 | text := Hello(tt.prefs...) 114 | if text != tt.text { 115 | t.Errorf("Hello(%v) = %q, want %q", tt.prefs, text, tt.text) 116 | } 117 | } 118 | } 119 | -- sampler.go -- 120 | // Copyright 2018 The Go Authors. All rights reserved. 121 | // Use of this source code is governed by a BSD-style 122 | // license that can be found in the LICENSE file. 123 | 124 | // Package sampler shows simple texts. 125 | package sampler // import "rsc.io/sampler" 126 | 127 | import ( 128 | "os" 129 | "strings" 130 | 131 | "golang.org/x/text/language" 132 | ) 133 | 134 | // DefaultUserPrefs returns the default user language preferences. 135 | // It consults the $LC_ALL, $LC_MESSAGES, and $LANG environment 136 | // variables, in that order. 137 | func DefaultUserPrefs() []language.Tag { 138 | var prefs []language.Tag 139 | for _, k := range []string{"LC_ALL", "LC_MESSAGES", "LANG"} { 140 | if env := os.Getenv(k); env != "" { 141 | prefs = append(prefs, language.Make(env)) 142 | } 143 | } 144 | return prefs 145 | } 146 | 147 | // Hello returns a localized greeting. 148 | // If no prefs are given, Hello uses DefaultUserPrefs. 149 | func Hello(prefs ...language.Tag) string { 150 | if len(prefs) == 0 { 151 | prefs = DefaultUserPrefs() 152 | } 153 | return hello.find(prefs) 154 | } 155 | 156 | // Glass returns a localized silly phrase. 157 | // If no prefs are given, Glass uses DefaultUserPrefs. 158 | func Glass(prefs ...language.Tag) string { 159 | if len(prefs) == 0 { 160 | prefs = DefaultUserPrefs() 161 | } 162 | return glass.find(prefs) 163 | } 164 | 165 | // A text is a localized text. 166 | type text struct { 167 | byTag map[string]string 168 | matcher language.Matcher 169 | } 170 | 171 | // newText creates a new localized text, given a list of translations. 172 | func newText(s string) *text { 173 | t := &text{ 174 | byTag: make(map[string]string), 175 | } 176 | var tags []language.Tag 177 | for _, line := range strings.Split(s, "\n") { 178 | line = strings.TrimSpace(line) 179 | if line == "" { 180 | continue 181 | } 182 | f := strings.Split(line, ": ") 183 | if len(f) != 3 { 184 | continue 185 | } 186 | tag := language.Make(f[1]) 187 | tags = append(tags, tag) 188 | t.byTag[tag.String()] = f[2] 189 | } 190 | t.matcher = language.NewMatcher(tags) 191 | return t 192 | } 193 | 194 | // find finds the text to use for the given language tag preferences. 195 | func (t *text) find(prefs []language.Tag) string { 196 | tag, _, _ := t.matcher.Match(prefs...) 197 | s := t.byTag[tag.String()] 198 | if strings.HasPrefix(s, "RTL ") { 199 | s = "\u200F" + strings.TrimPrefix(s, "RTL ") + "\u200E" 200 | } 201 | return s 202 | } 203 | -------------------------------------------------------------------------------- /testdata/mod/rsc.io_sampler_v1.99.99.txt: -------------------------------------------------------------------------------- 1 | rsc.io/sampler@v1.99.99 2 | 3 | -- .mod -- 4 | module "rsc.io/sampler" 5 | 6 | require "golang.org/x/text" v0.0.0-20170915032832-14c0d48ead0c 7 | -- .info -- 8 | {"Version":"v1.99.99","Time":"2018-02-13T22:20:19Z"} 9 | -- go.mod -- 10 | module "rsc.io/sampler" 11 | 12 | require "golang.org/x/text" v0.0.0-20170915032832-14c0d48ead0c 13 | -- hello.go -- 14 | // Copyright 2018 The Go Authors. All rights reserved. 15 | // Use of this source code is governed by a BSD-style 16 | // license that can be found in the LICENSE file. 17 | 18 | // Translations by Google Translate. 19 | 20 | package sampler 21 | 22 | var hello = newText(` 23 | 24 | English: en: 99 bottles of beer on the wall, 99 bottles of beer, ... 25 | 26 | `) 27 | -- hello_test.go -- 28 | // Copyright 2018 The Go Authors. All rights reserved. 29 | // Use of this source code is governed by a BSD-style 30 | // license that can be found in the LICENSE file. 31 | 32 | package sampler 33 | 34 | import ( 35 | "testing" 36 | 37 | "golang.org/x/text/language" 38 | ) 39 | 40 | var helloTests = []struct { 41 | prefs []language.Tag 42 | text string 43 | }{ 44 | { 45 | []language.Tag{language.Make("en-US"), language.Make("fr")}, 46 | "Hello, world.", 47 | }, 48 | { 49 | []language.Tag{language.Make("fr"), language.Make("en-US")}, 50 | "Bonjour le monde.", 51 | }, 52 | } 53 | 54 | func TestHello(t *testing.T) { 55 | for _, tt := range helloTests { 56 | text := Hello(tt.prefs...) 57 | if text != tt.text { 58 | t.Errorf("Hello(%v) = %q, want %q", tt.prefs, text, tt.text) 59 | } 60 | } 61 | } 62 | -- sampler.go -- 63 | // Copyright 2018 The Go Authors. All rights reserved. 64 | // Use of this source code is governed by a BSD-style 65 | // license that can be found in the LICENSE file. 66 | 67 | // Package sampler shows simple texts. 68 | package sampler // import "rsc.io/sampler" 69 | 70 | import ( 71 | "os" 72 | "strings" 73 | 74 | "golang.org/x/text/language" 75 | ) 76 | 77 | // DefaultUserPrefs returns the default user language preferences. 78 | // It consults the $LC_ALL, $LC_MESSAGES, and $LANG environment 79 | // variables, in that order. 80 | func DefaultUserPrefs() []language.Tag { 81 | var prefs []language.Tag 82 | for _, k := range []string{"LC_ALL", "LC_MESSAGES", "LANG"} { 83 | if env := os.Getenv(k); env != "" { 84 | prefs = append(prefs, language.Make(env)) 85 | } 86 | } 87 | return prefs 88 | } 89 | 90 | // Hello returns a localized greeting. 91 | // If no prefs are given, Hello uses DefaultUserPrefs. 92 | func Hello(prefs ...language.Tag) string { 93 | if len(prefs) == 0 { 94 | prefs = DefaultUserPrefs() 95 | } 96 | return hello.find(prefs) 97 | } 98 | 99 | func Glass() string { 100 | return "I can eat glass and it doesn't hurt me." 101 | } 102 | 103 | // A text is a localized text. 104 | type text struct { 105 | byTag map[string]string 106 | matcher language.Matcher 107 | } 108 | 109 | // newText creates a new localized text, given a list of translations. 110 | func newText(s string) *text { 111 | t := &text{ 112 | byTag: make(map[string]string), 113 | } 114 | var tags []language.Tag 115 | for _, line := range strings.Split(s, "\n") { 116 | line = strings.TrimSpace(line) 117 | if line == "" { 118 | continue 119 | } 120 | f := strings.Split(line, ": ") 121 | if len(f) != 3 { 122 | continue 123 | } 124 | tag := language.Make(f[1]) 125 | tags = append(tags, tag) 126 | t.byTag[tag.String()] = f[2] 127 | } 128 | t.matcher = language.NewMatcher(tags) 129 | return t 130 | } 131 | 132 | // find finds the text to use for the given language tag preferences. 133 | func (t *text) find(prefs []language.Tag) string { 134 | tag, _, _ := t.matcher.Match(prefs...) 135 | s := t.byTag[tag.String()] 136 | if strings.HasPrefix(s, "RTL ") { 137 | s = "\u200F" + strings.TrimPrefix(s, "RTL ") + "\u200E" 138 | } 139 | return s 140 | } 141 | -------------------------------------------------------------------------------- /testdata/status.txt: -------------------------------------------------------------------------------- 1 | cd repo 2 | go get rsc.io/quote@v1.5.2 3 | env GOHACK=$WORK/gohack 4 | gohack get rsc.io/quote 5 | 6 | gohack status 7 | ! stderr .+ 8 | stdout '^rsc.io/quote => .*/rsc\.io/quote$' 9 | 10 | -- repo/main.go -- 11 | package main 12 | import ( 13 | "fmt" 14 | "rsc.io/quote" 15 | "rsc.io/sampler" 16 | ) 17 | 18 | func main() { 19 | sampler.DefaultUserPrefs() 20 | fmt.Println(quote.Glass()) 21 | } 22 | 23 | -- repo/go.mod -- 24 | module example.com/repo 25 | 26 | replace ( 27 | rsc.io/sampler v1.3.0 => rsc.io/sampler v1.99.99 28 | ) 29 | -------------------------------------------------------------------------------- /testdata/undo-all.txt: -------------------------------------------------------------------------------- 1 | cd repo 2 | go get rsc.io/quote@v1.5.2 3 | go get rsc.io/sampler@v1.3.0 4 | env GOHACK=$WORK/gohack 5 | gohack get rsc.io/quote rsc.io/sampler 6 | stdout '^rsc.io/quote => .*/gohack/rsc.io/quote$' 7 | stdout '^rsc.io/sampler => .*/gohack/rsc.io/sampler$' 8 | ! stderr .+ 9 | 10 | gohack undo 11 | stdout '^dropped rsc\.io/quote$' 12 | stdout '^dropped rsc\.io/sampler$' 13 | ! stderr .+ 14 | 15 | ! grep replace go.mod 16 | 17 | -- repo/main.go -- 18 | package main 19 | import ( 20 | "fmt" 21 | "rsc.io/quote" 22 | "rsc.io/sampler" 23 | ) 24 | 25 | func main() { 26 | sampler.DefaultUserPrefs() 27 | fmt.Println(quote.Glass()) 28 | } 29 | 30 | -- repo/go.mod -- 31 | module example.com/repo 32 | -------------------------------------------------------------------------------- /testdata/undo-hack-inblock.txt: -------------------------------------------------------------------------------- 1 | cd repo 2 | gohack undo 3 | stdout 'dropped gopkg.in/errgo.v2' 4 | 5 | grep '^\tgopkg.in/errgo.v2 => github.com/rogpeppe/test2/arble v0.0.0-20181008213029-f6022c873160$' go.mod 6 | 7 | -- repo/main.go -- 8 | package main 9 | import _ "gopkg.in/errgo.v2" 10 | 11 | -- repo/go.mod -- 12 | module example.com/repo 13 | 14 | require ( 15 | gopkg.in/errgo.v2 v2.1.0 16 | rsc.io/sampler v1.3.0 17 | ) 18 | 19 | replace ( 20 | rsc.io/sampler v1.3.0 => rsc.io/sampler v1.2.1 21 | gopkg.in/errgo.v2 => ../foo // was gopkg.in/errgo.v2 => github.com/rogpeppe/test2/arble v0.0.0-20181008213029-f6022c873160 22 | ) 23 | 24 | -- foo/foo.go -- 25 | package foo 26 | 27 | -- foo/go.mod -- 28 | module example.com/foo 29 | 30 | -------------------------------------------------------------------------------- /testdata/undo-hack.txt: -------------------------------------------------------------------------------- 1 | cd repo 2 | gohack undo 3 | stdout 'dropped gopkg.in/errgo.v2' 4 | 5 | grep '^replace gopkg.in/errgo.v2 => github.com/rogpeppe/test2/arble v0.0.0-20181008213029-f6022c873160$' go.mod 6 | 7 | -- repo/main.go -- 8 | package main 9 | import _ "gopkg.in/errgo.v2" 10 | 11 | -- repo/go.mod -- 12 | module example.com/repo 13 | 14 | require gopkg.in/errgo.v2 v2.1.0 15 | 16 | replace gopkg.in/errgo.v2 => ../foo // was gopkg.in/errgo.v2 => github.com/rogpeppe/test2/arble v0.0.0-20181008213029-f6022c873160 17 | 18 | -- foo/foo.go -- 19 | package foo 20 | 21 | -- foo/go.mod -- 22 | module example.com/foo 23 | -------------------------------------------------------------------------------- /testdata/undo-not-existent.txt: -------------------------------------------------------------------------------- 1 | cd repo 2 | go get rsc.io/quote@v1.5.2 3 | env GOHACK=$WORK/gohack 4 | gohack get rsc.io/quote 5 | stdout '^rsc.io/quote => .*/gohack/rsc.io/quote$' 6 | ! stderr .+ 7 | 8 | exists $GOHACK/rsc.io/quote 9 | rm $GOHACK/rsc.io/quote 10 | 11 | gohack undo 12 | stdout '^dropped rsc\.io/quote$' 13 | ! stderr .+ 14 | 15 | -- repo/main.go -- 16 | package main 17 | import ( 18 | "fmt" 19 | "rsc.io/quote" 20 | "rsc.io/sampler" 21 | ) 22 | 23 | func main() { 24 | sampler.DefaultUserPrefs() 25 | fmt.Println(quote.Glass()) 26 | } 27 | 28 | -- repo/go.mod -- 29 | module example.com/repo 30 | -------------------------------------------------------------------------------- /testdata/undo.txt: -------------------------------------------------------------------------------- 1 | cd repo 2 | go get rsc.io/quote@v1.5.2 3 | env GOHACK=$WORK/gohack 4 | gohack get rsc.io/quote 5 | stdout '^rsc.io/quote => .*/gohack/rsc.io/quote$' 6 | ! stderr .+ 7 | 8 | # Check that the replace statement is there. 9 | grep -count=1 '^replace rsc\.io/quote => .*/gohack/rsc.io/quote$' go.mod 10 | 11 | # undo the gohack and the replace statement should be removed. 12 | gohack undo rsc.io/quote 13 | ! grep '^replace rsc\.io/quote => .*/gohack/rsc.io/quote$' go.mod 14 | 15 | # The source files should still be there though. 16 | grep '^' $WORK/gohack/rsc.io/quote/quote.go 17 | 18 | # If we modify the hacked version, the module should still 19 | # work OK (it's not using that version any more) 20 | cp ../bogus.go $WORK/gohack/rsc.io/quote/bogus.go 21 | go install example.com/repo 22 | 23 | -- repo/main.go -- 24 | package main 25 | import ( 26 | "fmt" 27 | "rsc.io/quote" 28 | ) 29 | 30 | func main() { 31 | fmt.Println(quote.Glass()) 32 | } 33 | 34 | -- repo/go.mod -- 35 | module example.com/repo 36 | 37 | -- bogus.go -- 38 | 39 | package wrong 40 | -------------------------------------------------------------------------------- /vcs.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/hex" 5 | "fmt" 6 | "regexp" 7 | "strconv" 8 | "strings" 9 | "time" 10 | ) 11 | 12 | var kindToVCS = map[string]VCS{ 13 | "bzr": bzrVCS{}, 14 | "hg": hgVCS{}, 15 | "git": gitVCS{}, 16 | } 17 | 18 | type VCS interface { 19 | Kind() string 20 | Info(dir string) (VCSInfo, error) 21 | Update(dir string, isTag bool, revid string) error 22 | Clean(dir string) error 23 | Create(repo, rootDir string) error 24 | Fetch(dir string) error 25 | } 26 | 27 | type VCSInfo struct { 28 | revid string 29 | revno string // optional 30 | clean bool 31 | } 32 | 33 | type gitVCS struct{} 34 | 35 | func (gitVCS) Kind() string { 36 | return "git" 37 | } 38 | 39 | func (gitVCS) Info(dir string) (VCSInfo, error) { 40 | out, err := runCmd(dir, "git", "log", "-n", "1", "--pretty=format:%H %ct", "HEAD") 41 | if err != nil { 42 | return VCSInfo{}, err 43 | } 44 | fields := strings.Fields(out) 45 | if len(fields) != 2 { 46 | return VCSInfo{}, fmt.Errorf("unexpected git log output %q", out) 47 | } 48 | revid := fields[0] 49 | // validate the revision hash 50 | revhash, err := hex.DecodeString(revid) 51 | if err != nil || len(revhash) == 0 { 52 | return VCSInfo{}, 53 | fmt.Errorf("git rev-parse provided invalid revision %q", revid) 54 | } 55 | unixTime, err := strconv.ParseInt(fields[1], 10, 64) 56 | if err != nil { 57 | return VCSInfo{}, 58 | fmt.Errorf("git rev-parse provided invalid time %q", fields[1]) 59 | } 60 | 61 | // `git status --porcelain` outputs one line per changed or untracked file. 62 | out, err = runCmd(dir, "git", "status", "--porcelain") 63 | if err != nil { 64 | return VCSInfo{}, err 65 | } 66 | return VCSInfo{ 67 | revid: revid, 68 | // Empty output (with rc=0) indicates no changes in working copy. 69 | clean: out == "", 70 | revno: time.Unix(unixTime, 0).UTC().Format(time.RFC3339), 71 | }, nil 72 | } 73 | 74 | func (gitVCS) Create(repo, rootDir string) error { 75 | _, err := runUpdateCmd("", "git", "clone", repo, rootDir) 76 | return err 77 | } 78 | 79 | func (gitVCS) Update(dir string, isTag bool, revid string) error { 80 | _, err := runUpdateCmd(dir, "git", "checkout", revid) 81 | return err 82 | } 83 | 84 | func (gitVCS) Clean(dir string) error { 85 | _, err := runUpdateCmd(dir, "git", "reset", "--hard", "HEAD") 86 | return err 87 | } 88 | 89 | func (gitVCS) Fetch(dir string) error { 90 | _, err := runCmd(dir, "git", "fetch") 91 | return err 92 | } 93 | 94 | type bzrVCS struct{} 95 | 96 | func (bzrVCS) Kind() string { 97 | return "bzr" 98 | } 99 | 100 | var validBzrInfo = regexp.MustCompile(`^([0-9.]+) ([^ \t]+)$`) 101 | var shelveLine = regexp.MustCompile(`^[0-9]+ (shelves exist|shelf exists)\.`) 102 | 103 | func (bzrVCS) Info(dir string) (VCSInfo, error) { 104 | out, err := runCmd(dir, "bzr", "revision-info", "--tree") 105 | if err != nil { 106 | return VCSInfo{}, err 107 | } 108 | m := validBzrInfo.FindStringSubmatch(strings.TrimSpace(out)) 109 | if m == nil { 110 | return VCSInfo{}, fmt.Errorf("bzr revision-info has unexpected result %q", out) 111 | } 112 | 113 | out, err = runCmd(dir, "bzr", "status", "-S") 114 | if err != nil { 115 | return VCSInfo{}, err 116 | } 117 | clean := true 118 | statusLines := strings.Split(out, "\n") 119 | for _, line := range statusLines { 120 | if line == "" || shelveLine.MatchString(line) { 121 | continue 122 | } 123 | clean = false 124 | break 125 | } 126 | return VCSInfo{ 127 | revid: m[2], 128 | revno: m[1], 129 | clean: clean, 130 | }, nil 131 | } 132 | 133 | func (bzrVCS) Create(repo, rootDir string) error { 134 | _, err := runUpdateCmd("", "bzr", "branch", repo, rootDir) 135 | return err 136 | } 137 | 138 | func (bzrVCS) Clean(dir string) error { 139 | _, err := runUpdateCmd(dir, "bzr", "revert") 140 | return err 141 | } 142 | 143 | func (bzrVCS) Update(dir string, isTag bool, to string) error { 144 | if isTag { 145 | to = "tag:" + to 146 | } else { 147 | to = "revid:" + to 148 | } 149 | _, err := runUpdateCmd(dir, "bzr", "update", "-r", to) 150 | return err 151 | } 152 | 153 | func (bzrVCS) Fetch(dir string) error { 154 | _, err := runCmd(dir, "bzr", "pull") 155 | return err 156 | } 157 | 158 | var validHgInfo = regexp.MustCompile(`^([a-f0-9]+) ([0-9]+)$`) 159 | 160 | type hgVCS struct{} 161 | 162 | func (hgVCS) Info(dir string) (VCSInfo, error) { 163 | out, err := runCmd(dir, "hg", "log", "-l", "1", "-r", ".", "--template", "{node} {rev}") 164 | if err != nil { 165 | return VCSInfo{}, err 166 | } 167 | m := validHgInfo.FindStringSubmatch(strings.TrimSpace(out)) 168 | if m == nil { 169 | return VCSInfo{}, fmt.Errorf("hg identify has unexpected result %q", out) 170 | } 171 | out, err = runCmd(dir, "hg", "status") 172 | if err != nil { 173 | return VCSInfo{}, err 174 | } 175 | // TODO(rog) check that tree is clean 176 | return VCSInfo{ 177 | revid: m[1], 178 | revno: m[2], 179 | clean: out == "", 180 | }, nil 181 | } 182 | 183 | func (hgVCS) Kind() string { 184 | return "hg" 185 | } 186 | 187 | func (hgVCS) Create(repo, rootDir string) error { 188 | _, err := runUpdateCmd("", "hg", "clone", "-U", repo, rootDir) 189 | return err 190 | } 191 | 192 | func (hgVCS) Clean(dir string) error { 193 | _, err := runUpdateCmd(dir, "hg", "revert", "--all") 194 | return err 195 | } 196 | 197 | func (hgVCS) Update(dir string, isTag bool, revid string) error { 198 | _, err := runUpdateCmd(dir, "hg", "update", revid) 199 | return err 200 | } 201 | 202 | func (hgVCS) Fetch(dir string) error { 203 | _, err := runCmd(dir, "hg", "pull") 204 | return err 205 | } 206 | 207 | func runUpdateCmd(dir string, name string, args ...string) (string, error) { 208 | if *dryRun { 209 | printShellCommand(dir, name, args) 210 | return "", nil 211 | } 212 | return runCmd(dir, name, args...) 213 | } 214 | --------------------------------------------------------------------------------