├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── lib ├── lib.go └── lib_test.go ├── main.go └── main_test.go /.travis.yml: -------------------------------------------------------------------------------- 1 | go_import_path: github.com/Shyp/bump_version 2 | language: go 3 | 4 | go: 5 | - "1.7.x" 6 | - "1.8.x" 7 | - "1.9.x" 8 | - "1.10.x" 9 | - master 10 | 11 | script: make test 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Shyp, Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test 2 | 3 | STATICCHECK := $(shell command -v staticcheck) 4 | 5 | install: 6 | go install ./... 7 | 8 | lint: 9 | ifndef STATICCHECK 10 | go get -u honnef.co/go/tools/cmd/staticcheck 11 | endif 12 | go vet ./... 13 | staticcheck ./... 14 | 15 | test: lint 16 | go test -race ./... -timeout 1s 17 | 18 | release: install test 19 | bump_version minor main.go 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bump_version 2 | 3 | This is a tool for bumping version numbers in Go files. 4 | 5 | ## Installation 6 | 7 | For the moment, you'll need a working Go installation. 8 | 9 | ``` 10 | go get github.com/Shyp/bump_version 11 | ``` 12 | 13 | That will install the `bump_version` binary to your `$GOPATH`. 14 | 15 | ## Usage 16 | 17 | ``` 18 | bump_version 19 | ``` 20 | 21 | This will: 22 | 23 | 1. Look for a `const` named `version`, `VERSION`, or `Version` in that file. 24 | Here's an example: 25 | 26 | ```go 27 | package main 28 | 29 | const VERSION = "0.2.1" 30 | ``` 31 | 32 | 2. Apply the version bump - `bump_version major` will increment the major 33 | version number, `bump_version minor` will increment the middle version number, 34 | `bump_version patch` will increment the last version number. If your version is 35 | "0.3" and you ask for `bump_version minor`, the new version will be "0.4". 36 | 37 | 3. Write the new file to disk, with the bumped version. 38 | 39 | 4. Add the file with `git add `. 40 | 41 | 5. Add a commit with the message "x.y.z" (`git commit -m ""`) 42 | 43 | 6. Tag the new version. 44 | 45 | If any of these steps fail, `bump_version` will abort. 46 | 47 | ## Notes 48 | 49 | The VERSION should be a string in one of these formats: "3", "0.3", 50 | "0.3.4". Any prefixes like "v" or suffixes like "0.3.3-beta" will be 51 | stripped or generate an error. 52 | 53 | - `"v0.1"` - parse error, no prefixes allowed. 54 | - `bump_version("0.1", "minor")` -> "0.2" 55 | - `bump_version("0.1", "patch")` -> "0.1.1" 56 | - `bump_version("0.1", "major")` -> "1.1" 57 | - `bump_version("0.1-beta", "major")` -> "1.1" 58 | - `bump_version("devel", "major")` -> parse error. 59 | 60 | We use the VERSION in code exclusively - any existing git tags are ignored. 61 | 62 | Alan Shreve would like to note that you probably shouldn't store version 63 | numbers in code - instead, check in `const VERSION = "devel"`, then build your 64 | project via: 65 | 66 | ``` 67 | go build -ldflags="-X main.VERSION=0.2" 68 | ``` 69 | 70 | Which you are welcome to do! 71 | -------------------------------------------------------------------------------- /lib/lib.go: -------------------------------------------------------------------------------- 1 | package bump_version 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "go/ast" 7 | "go/parser" 8 | "go/printer" 9 | "go/token" 10 | "os" 11 | "strconv" 12 | "strings" 13 | ) 14 | 15 | type VersionType string 16 | 17 | const Major = VersionType("major") 18 | const Minor = VersionType("minor") 19 | const Patch = VersionType("patch") 20 | 21 | type Version struct { 22 | Major int64 23 | Minor int64 24 | Patch int64 25 | } 26 | 27 | func (v *Version) String() string { 28 | if v.Major >= 0 && v.Minor >= 0 && v.Patch >= 0 { 29 | return fmt.Sprintf("%d.%d.%d", v.Major, v.Minor, v.Patch) 30 | } else if v.Major >= 0 && v.Minor >= 0 { 31 | return fmt.Sprintf("%d.%d", v.Major, v.Minor) 32 | } else if v.Major >= 0 { 33 | return fmt.Sprintf("%d", v.Major) 34 | } else { 35 | return "%!s(INVALID_VERSION)" 36 | } 37 | } 38 | 39 | // ParseVersion parses a version string of the forms "2", "2.3", or "0.10.11". 40 | // Any information after the third number ("2.0.0-beta") is discarded. Very 41 | // little effort is taken to validate the input. 42 | // 43 | // If a field is omitted from the string version (e.g. "0.2"), it's stored in 44 | // the Version string as the integer -1. 45 | func Parse(version string) (*Version, error) { 46 | if len(version) == 0 { 47 | return nil, errors.New("Empty version string") 48 | } 49 | 50 | parts := strings.SplitN(version, ".", 3) 51 | if len(parts) == 1 { 52 | major, err := strconv.ParseInt(parts[0], 10, 64) 53 | if err != nil { 54 | return nil, err 55 | } 56 | return &Version{ 57 | Major: major, 58 | Minor: -1, 59 | Patch: -1, 60 | }, nil 61 | } 62 | if len(parts) == 2 { 63 | major, err := strconv.ParseInt(parts[0], 10, 64) 64 | if err != nil { 65 | return nil, err 66 | } 67 | minor, err := strconv.ParseInt(parts[1], 10, 64) 68 | if err != nil { 69 | return nil, err 70 | } 71 | return &Version{ 72 | Major: major, 73 | Minor: minor, 74 | Patch: -1, 75 | }, nil 76 | } 77 | if len(parts) == 3 { 78 | major, err := strconv.ParseInt(parts[0], 10, 64) 79 | if err != nil { 80 | return nil, err 81 | } 82 | minor, err := strconv.ParseInt(parts[1], 10, 64) 83 | if err != nil { 84 | return nil, err 85 | } 86 | patchParts := strings.SplitN(parts[2], "-", 2) 87 | patch, err := strconv.ParseInt(patchParts[0], 10, 64) 88 | if err != nil { 89 | return nil, err 90 | } 91 | return &Version{ 92 | Major: major, 93 | Minor: minor, 94 | Patch: patch, 95 | }, nil 96 | } 97 | return nil, fmt.Errorf("Invalid version string: %s", version) 98 | } 99 | 100 | // changeVersion takes a basic literal representing a string version, and 101 | // increments the version number per the given VersionType. 102 | func changeVersion(vtype VersionType, value string) (*Version, error) { 103 | versionNoQuotes := strings.Replace(value, "\"", "", -1) 104 | version, err := Parse(versionNoQuotes) 105 | if err != nil { 106 | return nil, err 107 | } 108 | if vtype == Major { 109 | version.Major++ 110 | if version.Minor != -1 { 111 | version.Minor = 0 112 | } 113 | if version.Patch != -1 { 114 | version.Patch = 0 115 | } 116 | } else if vtype == Minor { 117 | if version.Minor == -1 { 118 | version.Minor = 0 119 | } 120 | if version.Patch != -1 { 121 | version.Patch = 0 122 | } 123 | version.Minor++ 124 | } else if vtype == Patch { 125 | if version.Patch == -1 { 126 | version.Patch = 0 127 | } 128 | version.Patch++ 129 | } else { 130 | return nil, fmt.Errorf("Invalid version type: %s", vtype) 131 | } 132 | return version, nil 133 | } 134 | 135 | func findBasicLit(file *ast.File) (*ast.BasicLit, error) { 136 | for _, decl := range file.Decls { 137 | switch gd := decl.(type) { 138 | case *ast.GenDecl: 139 | if gd.Tok != token.CONST { 140 | continue 141 | } 142 | spec, _ := gd.Specs[0].(*ast.ValueSpec) 143 | if strings.ToUpper(spec.Names[0].Name) == "VERSION" { 144 | value, ok := spec.Values[0].(*ast.BasicLit) 145 | if !ok || value.Kind != token.STRING { 146 | return nil, fmt.Errorf("VERSION is not a string, was %#v\n", value.Value) 147 | } 148 | return value, nil 149 | } 150 | default: 151 | continue 152 | } 153 | } 154 | return nil, errors.New("bump_version: No version const found") 155 | } 156 | 157 | func writeFile(filename string, fset *token.FileSet, file *ast.File) error { 158 | f, err := os.Create(filename) 159 | if err != nil { 160 | return err 161 | } 162 | defer f.Close() 163 | cfg := printer.Config{Mode: printer.UseSpaces | printer.TabIndent, Tabwidth: 8} 164 | return cfg.Fprint(f, fset, file) 165 | } 166 | 167 | func changeInFile(filename string, f func(*ast.BasicLit) error) error { 168 | fset := token.NewFileSet() 169 | parsedFile, err := parser.ParseFile(fset, filename, nil, parser.ParseComments) 170 | if err != nil { 171 | return err 172 | } 173 | lit, err := findBasicLit(parsedFile) 174 | if err != nil { 175 | return fmt.Errorf("No Version const found in %s", filename) 176 | } 177 | if err := f(lit); err != nil { 178 | return err 179 | } 180 | writeErr := writeFile(filename, fset, parsedFile) 181 | return writeErr 182 | } 183 | 184 | // SetInFile sets the version in filename to newVersion. 185 | func SetInFile(newVersion *Version, filename string) error { 186 | return changeInFile(filename, func(lit *ast.BasicLit) error { 187 | lit.Value = fmt.Sprintf("\"%s\"", newVersion.String()) 188 | return nil 189 | }) 190 | } 191 | 192 | // BumpInFile finds a constant named VERSION, version, or Version in the file 193 | // with the given filename, increments the version per the given VersionType, 194 | // and writes the file back to disk. Returns the incremented Version object. 195 | func BumpInFile(vtype VersionType, filename string) (*Version, error) { 196 | var version *Version 197 | err := changeInFile(filename, func(lit *ast.BasicLit) error { 198 | var err error 199 | version, err = changeVersion(vtype, lit.Value) 200 | if err != nil { 201 | return err 202 | } 203 | lit.Value = fmt.Sprintf("\"%s\"", version.String()) 204 | return nil 205 | }) 206 | return version, err 207 | } 208 | -------------------------------------------------------------------------------- /lib/lib_test.go: -------------------------------------------------------------------------------- 1 | package bump_version 2 | 3 | import "testing" 4 | 5 | func TestChangeVersion(t *testing.T) { 6 | testCases := []struct { 7 | in string 8 | vtype VersionType 9 | out string 10 | }{ 11 | {"0.4", Major, "1.0"}, 12 | {"0.4.0", Major, "1.0.0"}, 13 | {"1.0", Major, "2.0"}, 14 | {"1", Major, "2"}, 15 | {"1.0.1", Minor, "1.1.0"}, 16 | } 17 | for _, tt := range testCases { 18 | v, err := changeVersion(tt.vtype, tt.in) 19 | if err != nil { 20 | t.Fatal(err) 21 | } 22 | if v.String() != tt.out { 23 | t.Errorf("changeVersion(%s, %s): got %s, want %s", tt.vtype, tt.in, v.String(), tt.out) 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | "os/exec" 8 | 9 | "github.com/Shyp/bump_version/lib" 10 | ) 11 | 12 | const VERSION = "1.3" 13 | 14 | func usage() { 15 | fmt.Fprintf(os.Stderr, "Usage: bump_version [--version=] [] \n") 16 | flag.PrintDefaults() 17 | } 18 | 19 | // runCommand execs the given command and exits if it fails. 20 | func runCommand(binary string, args ...string) { 21 | out, err := exec.Command(binary, args...).CombinedOutput() 22 | if err != nil { 23 | fmt.Fprintf(os.Stderr, "Error when running command: %s.\nOutput was:\n%s", err.Error(), string(out)) 24 | os.Exit(2) 25 | } 26 | } 27 | 28 | var vsn = flag.String("version", "", "Set this version in the file (don't increment whatever version is present)") 29 | 30 | func main() { 31 | flag.Usage = usage 32 | flag.Parse() 33 | args := flag.Args() 34 | var filename string 35 | var version *bump_version.Version 36 | if *vsn != "" { 37 | // no "minor" 38 | if len(args) != 1 { 39 | flag.Usage() 40 | return 41 | } 42 | var err error 43 | version, err = bump_version.Parse(*vsn) 44 | if err != nil { 45 | os.Stderr.WriteString(err.Error()) 46 | os.Exit(2) 47 | } 48 | filename = args[0] 49 | setErr := bump_version.SetInFile(version, filename) 50 | if setErr != nil { 51 | os.Stderr.WriteString(setErr.Error() + "\n") 52 | os.Exit(2) 53 | } 54 | } else { 55 | if len(args) != 2 { 56 | flag.Usage() 57 | return 58 | } 59 | versionTypeStr := args[0] 60 | filename = args[1] 61 | 62 | var err error 63 | version, err = bump_version.BumpInFile(bump_version.VersionType(versionTypeStr), filename) 64 | if err != nil { 65 | os.Stderr.WriteString(err.Error() + "\n") 66 | os.Exit(2) 67 | } 68 | } 69 | runCommand("git", "add", filename) 70 | runCommand("git", "commit", "-m", version.String()) 71 | runCommand("git", "tag", version.String(), "--annotate", "--message", version.String()) 72 | os.Stdout.WriteString(version.String() + "\n") 73 | } 74 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestVersionString(t *testing.T) { 9 | typ := reflect.TypeOf(VERSION) 10 | if typ.String() != "string" { 11 | t.Errorf("expected VERSION to be a string, got %#v (type %#v)", VERSION, typ.String()) 12 | } 13 | } 14 | --------------------------------------------------------------------------------