├── .travis.yml ├── LICENSE ├── README.md ├── cmd ├── check.go ├── check_test.go ├── dump.go ├── dump_test.go ├── excuse.go └── testdata │ └── output.xml ├── docs └── css │ └── style.css ├── git-ratchet.go ├── index.html ├── scripts └── build.sh └── store ├── git.go ├── reader.go ├── types.go └── writer.go /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.8 4 | env: 5 | - "PATH=/home/travis/gopath/bin:$PATH" 6 | before_install: 7 | - go get github.com/mitchellh/gox 8 | - go get github.com/tcnksm/ghr 9 | - go get github.com/inconshreveable/mousetrap 10 | script: 11 | - go test -v ./... 12 | after_success: 13 | - scripts/build.sh 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | {description} 294 | Copyright (C) {year} {fullname} 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | {signature of Ty Coon}, 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # git-ratchet 2 | git-ratchet is a tool for building ratcheted builds. Ratcheted builds are builds that go *red* when a given measure increases. 3 | 4 | [![Build Status](https://travis-ci.org/iangrunert/git-ratchet.svg?branch=master)](https://travis-ci.org/iangrunert/git-ratchet) 5 | 6 | ## What's it for? 7 | Ratcheted builds are for teams that would like to pay off technical debt or tackle larger architectural changes to a code base over a medium-to-long term time period. Let's dive into a few examples. 8 | 9 | Perhaps you are working on a large legacy Javascript codebase, which you would like to run jshint on. The number of warnings raised by jshint is large enough that you can't tackle them in a single day, but you'd like to avoid increasing the number of warnings when writing new features. You could set up a ratcheted build measuring the number of jshint warnings, and tackle them with small improvements over time. 10 | 11 | Or perhaps you are attempting to perform a library upgrade, but there are a large number of usages of a deprecated method call that need to be refactored. The refactoring isn't straight-forward and can't be easily automated. You could set up a ratcheted build measuring the number of usages of the deprecated method, and tackle them with small improvements over time. 12 | 13 | ## How do I get started? 14 | 15 | Download the [latest official release](https://github.com/iangrunert/git-ratchet/releases/latest) for your platform. 16 | 17 | Rename the binary to git-ratchet, put it on your $PATH, and make it executable. 18 | 19 | Run ```git ratchet check -w``` on a CI server, on your master branch. 20 | 21 | Feed in input that looks like this: 22 | 23 | ``` 24 | _measure_,_value_ 25 | ... 26 | ``` 27 | 28 | It then checks the measurements against previous values stored in your git repository, and returns a non-zero exit code if the measures have increased. Otherwise, it stores the measures againt the current commit hash and exits. 29 | 30 | > Note: If you're feeding measures via stdin in a terminal window (likely while testing), you'll need to send `^D` to signify the end of input. [See this StackOverflow answer for a longer explanation](http://unix.stackexchange.com/questions/16333/how-to-signal-the-end-of-stdin-input-in-bash) 31 | > 32 | > Another option would be to put measures into a file and feed that to `git-ratchet`. 33 | ``` 34 | touch measures.csv 35 | echo "test,100" > measures.csv 36 | git ratchet check -v -w < measures.csv 37 | ``` 38 | 39 | ## How do I check my changes locally? 40 | 41 | Run ```git ratchet check``` locally, feeding in the calculated input. This checks the measures against previous values but does not write the new values if they are okay. 42 | 43 | ## How do I see the trend over time? 44 | 45 | Run ```git ratchet dump``` to dump a data file containing the data. This file current is currently in CSV, and looks like this: 46 | 47 | ``` 48 | Time,Measure,Value,Baseline 49 | _timestamp_,_measure_,_value_,_baseline_ 50 | ... 51 | ``` 52 | 53 | ## It's 2am and I need to release a hotfix to PROD. How do I ignore the increase? 54 | 55 | Run ```git ratchet excuse -n "_measure_" -e "It's 2am and the servers are on fire."``` locally to write an excuse. This will allow the build to pass. 56 | 57 | ## Where is the data stored? 58 | 59 | The data is stored inside git-notes. This means this data follows around your repository, and can keep track of history, without having to pollute your working directory or commit graph. 60 | 61 | > Note: When doing a fresh clone (which is typical when using this in a CI environment), you'll need to make sure you pull down the git notes as well. A default clone *will not* do this. 62 | ``` 63 | > git fetch origin refs/notes/*:refs/notes/* 64 | ``` 65 | 66 | **For example**: 67 | 68 | Running the following will return a list of commits starting at `HEAD` which include any `git-ratchet-1-$SUFFIX_NAME` notes that have been added by `git-ratchet`. 69 | > Note: $SUFFIX_NAME is likely `master` unless you passed in a suffix (with the `-p` flag) 70 | 71 | ``` 72 | git --no-pager log --notes=git-ratchet-1-master HEAD 73 | 74 | commit c3d1bfe82a85d99f3e7ab8b00d435c4786f2eb5e 75 | Author: Your Name 76 | Date: Fri Jul 31 17:37:52 2015 -0500 77 | 78 | Add ratchet script. 79 | 80 | Notes (git-ratchet-1-master): 81 | errors,0,0 82 | warnings,0,0 83 | 84 | commit 6d4044923fbf3c35664797f56f4f54534decc872 85 | Author: Your Name 86 | Date: Wed Jul 29 15:33:27 2015 -0500 87 | 88 | Initial commit 89 | ``` 90 | -------------------------------------------------------------------------------- /cmd/check.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bytes" 5 | "github.com/iangrunert/git-ratchet/store" 6 | log "github.com/spf13/jwalterweatherman" 7 | "io" 8 | ) 9 | 10 | func Check(prefix string, slack float64, usePercents bool, write bool, inputType string, zeroOnMissing bool, input io.Reader) int { 11 | // Parse the measures from stdin 12 | log.INFO.Println("Parsing measures from stdin") 13 | passedMeasures, err := store.ParseMeasures(input, store.ParseInputType(inputType)) 14 | log.INFO.Println("Finished parsing measures from stdin") 15 | log.INFO.Println(passedMeasures) 16 | if err != nil { 17 | log.FATAL.Println(err) 18 | return 10 19 | } 20 | 21 | log.INFO.Println("Reading measures stored in git") 22 | gitlog := store.CommitMeasureCommand(prefix) 23 | var stderr bytes.Buffer 24 | gitlog.Stderr = &stderr 25 | 26 | readStoredMeasure, err := store.CommitMeasures(gitlog) 27 | if err != nil { 28 | log.FATAL.Println(err) 29 | return 20 30 | } 31 | 32 | commitmeasure, err := readStoredMeasure() 33 | 34 | // Empty state of the repository - no stored metrics. Let's store one if we can. 35 | if err == io.EOF { 36 | log.INFO.Println("No measures found.") 37 | if write { 38 | log.INFO.Println("Writing initial measure values.") 39 | err = store.PutMeasures(prefix, passedMeasures) 40 | if err != nil { 41 | log.FATAL.Println(err) 42 | return 30 43 | } 44 | log.INFO.Println("Successfully written initial measures.") 45 | } 46 | } else if err != nil { 47 | log.FATAL.Println(err) 48 | return 40 49 | } else { 50 | log.INFO.Println("Checking passed measure against stored value") 51 | finalMeasures, compareErr := store.CompareMeasures(prefix, commitmeasure.CommitHash, commitmeasure.Measures, passedMeasures, slack, usePercents, zeroOnMissing) 52 | 53 | if write { 54 | log.INFO.Println("Writing measure values.") 55 | err = store.PutMeasures(prefix, finalMeasures) 56 | if err != nil { 57 | log.FATAL.Println(err) 58 | return 30 59 | } 60 | log.INFO.Println("Successfully written measures.") 61 | } 62 | if compareErr != nil { 63 | log.FATAL.Println(compareErr) 64 | return 50 65 | } else { 66 | log.INFO.Println("Metrics passing!") 67 | } 68 | } 69 | 70 | log.INFO.Println("Finished reading measures stored in git") 71 | return 0 72 | } 73 | -------------------------------------------------------------------------------- /cmd/check_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "os/exec" 7 | "path/filepath" 8 | "strings" 9 | "testing" 10 | 11 | log "github.com/spf13/jwalterweatherman" 12 | ) 13 | 14 | var checkStyleFile *os.File 15 | var checkStyleFileErr error 16 | 17 | func TestMain(m *testing.M) { 18 | checkStyleFile, checkStyleFileErr = os.Open("./testdata/output.xml") 19 | 20 | os.Exit(m.Run()) 21 | } 22 | 23 | func TestCheck(t *testing.T) { 24 | if testing.Verbose() { 25 | log.SetLogThreshold(log.LevelInfo) 26 | log.SetStdoutThreshold(log.LevelInfo) 27 | } 28 | 29 | createEmptyGitRepo(t) 30 | 31 | runCheck(t, false, "") 32 | runCheck(t, false, "foo,5") 33 | runCheck(t, true, "foo,5") 34 | runCheck(t, false, "foo,5") 35 | 36 | t.Logf("Running check command w: %t i: %s", false, "foo,6") 37 | 38 | errCode := Check("", 0, false, true, "csv", false, strings.NewReader("foo,6")) 39 | 40 | if errCode != 50 { 41 | t.Fatalf("Check command passed unexpectedly!") 42 | } 43 | 44 | errCode = Check("", 0, false, true, "csv", false, strings.NewReader("foo,6")) 45 | 46 | if errCode != 50 { 47 | t.Fatalf("Check command passed unexpectedly!") 48 | } 49 | } 50 | 51 | func TestZeroMissing(t *testing.T) { 52 | if testing.Verbose() { 53 | log.SetLogThreshold(log.LevelInfo) 54 | log.SetStdoutThreshold(log.LevelInfo) 55 | } 56 | 57 | createEmptyGitRepo(t) 58 | 59 | runCheck(t, true, "foo,5") 60 | 61 | t.Logf("Running check command w: %t i: %s", false, "") 62 | 63 | errCode := Check("", 0, false, true, "csv", false, strings.NewReader("")) 64 | 65 | if errCode != 50 { 66 | t.Fatalf("Check command passed unexpectedly!") 67 | } 68 | 69 | t.Logf("Running check command zero on missing w: %t i: %s", false, "") 70 | 71 | errCode = Check("", 0, false, true, "csv", false, strings.NewReader("")) 72 | 73 | if errCode != 0 { 74 | t.Fatalf("Check command failed unexpectedly!") 75 | } 76 | } 77 | 78 | // Regression test for https://github.com/iangrunert/git-ratchet/issues/22 79 | func TestAddRemove(t *testing.T) { 80 | if testing.Verbose() { 81 | log.SetLogThreshold(log.LevelInfo) 82 | log.SetStdoutThreshold(log.LevelInfo) 83 | } 84 | 85 | createEmptyGitRepo(t) 86 | 87 | runCheck(t, true, "measure-A,5") 88 | 89 | t.Logf("Running check command with added measure w: %t z: %t i: %s", false, false, "measure-A,5\nmeasure-B,4") 90 | 91 | errCode := Check("", 0, false, true, "csv", false, strings.NewReader("measure-A,5\nmeasure-B,4")) 92 | 93 | if errCode != 0 { 94 | t.Fatalf("Check command failed unexpectedly!") 95 | } 96 | 97 | t.Logf("Running check command with added and removed measures w: %t z: %t i: %s", false, false, "measure-B,4\nmeasure-C,3") 98 | 99 | errCode = Check("", 0, false, true, "csv", false, strings.NewReader("measure-B,4\nmeasure-C,3")) 100 | 101 | if errCode != 50 { 102 | t.Fatalf("Check command passed unexpectedly!") 103 | } 104 | 105 | t.Logf("Running check command with added and removed measures w: %t z: %t i: %s", false, true, "measure-B,4\nmeasure-C,3") 106 | 107 | errCode = Check("", 0, false, true, "csv", true, strings.NewReader("measure-B,4\nmeasure-C,3")) 108 | 109 | if errCode != 0 { 110 | t.Fatalf("Check command failed unexpectedly!") 111 | } 112 | } 113 | 114 | func TestCheckPrefix(t *testing.T) { 115 | if testing.Verbose() { 116 | log.SetLogThreshold(log.LevelInfo) 117 | log.SetStdoutThreshold(log.LevelInfo) 118 | } 119 | 120 | createEmptyGitRepo(t) 121 | 122 | runCheckP(t, "foobar", true, "foo,5") 123 | // Running a check against a different prefix should still work 124 | runCheckP(t, "barfoo", true, "foo,6") 125 | 126 | t.Logf("Running check command p: %s w: %t i: %s", "foobar", false, "foo,6") 127 | 128 | errCode := Check("foobar", 0, false, false, "csv", false, strings.NewReader("foo,6")) 129 | 130 | if errCode != 50 { 131 | t.Fatalf("Check command passed unexpectedly!") 132 | } 133 | } 134 | 135 | func TestCheckSlackValue(t *testing.T) { 136 | if testing.Verbose() { 137 | log.SetLogThreshold(log.LevelInfo) 138 | log.SetStdoutThreshold(log.LevelInfo) 139 | } 140 | 141 | createEmptyGitRepo(t) 142 | 143 | slack := 5.0 144 | usePercents := false 145 | 146 | runCheckPS(t, "pageweight", slack, usePercents, true, "gzippedjs,10") 147 | runCheckPS(t, "pageweight", slack, usePercents, false, "gzippedjs,15") 148 | 149 | t.Logf("Running check command p: %s w: %t i: %s", "pageweight", false, "gzippedjs,16") 150 | 151 | errCode := Check("pageweight", slack, usePercents, false, "csv", false, strings.NewReader("gzippedjs,16")) 152 | 153 | if errCode != 50 { 154 | t.Fatalf("Check command passed unexpectedly!") 155 | } 156 | } 157 | 158 | func TestCheckSlackPercent(t *testing.T) { 159 | if testing.Verbose() { 160 | log.SetLogThreshold(log.LevelInfo) 161 | log.SetStdoutThreshold(log.LevelInfo) 162 | } 163 | 164 | createEmptyGitRepo(t) 165 | 166 | slack := 20.0 167 | usePercents := true 168 | 169 | runCheckPS(t, "pageweight", slack, usePercents, true, "gzippedjs,100") 170 | runCheckPS(t, "pageweight", slack, usePercents, false, "gzippedjs,101") 171 | 172 | t.Logf("Running check command p: %s w: %t i: %s", "pageweight", false, "gzippedjs,120") 173 | 174 | errCode := Check("pageweight", slack, usePercents, false, "csv", false, strings.NewReader("gzippedjs,120")) 175 | 176 | if errCode != 0 { 177 | t.Fatalf("Check command failed unexpectedly!") 178 | } 179 | 180 | t.Logf("Running check command p: %s w: %t i: %s", "pageweight", false, "gzippedjs,121") 181 | 182 | errCode = Check("pageweight", slack, usePercents, false, "csv", false, strings.NewReader("gzippedjs,121")) 183 | 184 | if errCode != 50 { 185 | t.Fatalf("Check command passed unexpectedly!") 186 | } 187 | } 188 | 189 | func TestCheckExcuse(t *testing.T) { 190 | if testing.Verbose() { 191 | log.SetLogThreshold(log.LevelInfo) 192 | log.SetStdoutThreshold(log.LevelInfo) 193 | } 194 | 195 | repo := createEmptyGitRepo(t) 196 | 197 | runCheckP(t, "foobar", true, "foo,5") 198 | // Increase on "barfoo" prefix is okay 199 | runCheckP(t, "barfoo", true, "foo,6") 200 | 201 | t.Logf("Running check command p: %s w: %t i: %s", "foobar", false, "foo,6") 202 | 203 | errCode := Check("foobar", 0, false, false, "csv", false, strings.NewReader("foo,6")) 204 | 205 | if errCode != 50 { 206 | t.Fatalf("Check command passed unexpectedly!") 207 | } 208 | 209 | writeExcuse(t, "foobar", "foo", "PROD's down right now, I'll clean foo up later") 210 | 211 | runCheckP(t, "foobar", true, "foo,6") 212 | 213 | t.Logf("Running check command p: %s w: %t i: %s", "barfoo", false, "foo,7") 214 | 215 | errCode = Check("barfoo", 0, false, false, "csv", false, strings.NewReader("foo,7")) 216 | 217 | if errCode != 50 { 218 | t.Fatalf("Check command passed unexpectedly!") 219 | } 220 | 221 | runCommand(t, repo, exec.Command("git", "add", createFile(t, repo, "test2.txt").Name())) 222 | runCommand(t, repo, exec.Command("git", "commit", "-m", "Third Commit")) 223 | 224 | runCheckP(t, "foobar", true, "foo,6") 225 | 226 | runCommand(t, repo, exec.Command("git", "add", createFile(t, repo, "test3.txt").Name())) 227 | runCommand(t, repo, exec.Command("git", "commit", "-m", "Fourth Commit")) 228 | 229 | runCheckP(t, "foobar", true, "foo,6") 230 | } 231 | 232 | func TestCheckWithCheckstyleInput(t *testing.T) { 233 | if testing.Verbose() { 234 | log.SetLogThreshold(log.LevelInfo) 235 | log.SetStdoutThreshold(log.LevelInfo) 236 | } 237 | 238 | if checkStyleFileErr != nil { 239 | t.Fatalf("Failure opening test data", checkStyleFileErr) 240 | } 241 | 242 | createEmptyGitRepo(t) 243 | 244 | t.Logf("Running check command p: %s w: %t i: %s", "jshint", true, checkStyleFile) 245 | 246 | errCode := Check("jshint", 0, false, true, "checkstyle", false, checkStyleFile) 247 | 248 | if errCode != 0 { 249 | t.Fatalf("Check command failed! Error code: %d", errCode) 250 | } 251 | 252 | t.Logf("Running check command p: %s w: %t i: %s", "jshint", false, "errors,951") 253 | 254 | errCode = Check("jshint", 0, false, false, "csv", false, strings.NewReader("errors,951")) 255 | 256 | if errCode != 50 { 257 | t.Fatalf("Check command passed unexpectedly!") 258 | } 259 | } 260 | 261 | func writeExcuse(t *testing.T, prefix string, measure string, excuse string) { 262 | t.Logf("Running excuse command p: %s m: %s, e: %s", prefix, measure, excuse) 263 | 264 | errCode := Excuse(prefix, measure, excuse) 265 | 266 | if errCode != 0 { 267 | t.Fatalf("Excuse command failed! Error code: %d", errCode) 268 | } 269 | } 270 | 271 | func runCheck(t *testing.T, write bool, input string) { 272 | runCheckP(t, "", write, input) 273 | } 274 | 275 | func runCheckP(t *testing.T, prefix string, write bool, input string) { 276 | runCheckPS(t, prefix, 0, false, write, input) 277 | } 278 | 279 | func runCheckPS(t *testing.T, prefix string, slack float64, usePercents bool, write bool, input string) { 280 | t.Logf("Running check command p: %s s: %g, sp: %t, w: %t i: %s", prefix, slack, usePercents, write, input) 281 | 282 | errCode := Check(prefix, slack, usePercents, write, "csv", false, strings.NewReader(input)) 283 | 284 | if errCode != 0 { 285 | t.Fatalf("Check command failed! Error code: %d", errCode) 286 | } 287 | } 288 | 289 | func createEmptyGitRepo(t *testing.T) string { 290 | repo, err := ioutil.TempDir(os.TempDir(), "git-ratchet-test-") 291 | 292 | if err != nil { 293 | t.Fatalf("Failed to create directory %s", repo) 294 | } 295 | 296 | err = os.Chdir(repo) 297 | 298 | if err != nil { 299 | t.Fatalf("Failed to init repository %s", repo) 300 | } 301 | 302 | runCommand(t, repo, exec.Command("git", "init", repo)) 303 | runCommand(t, repo, exec.Command("git", "config", "user.email", "test@example.com")) 304 | runCommand(t, repo, exec.Command("git", "config", "user.name", "Test Name")) 305 | 306 | runCommand(t, repo, exec.Command("git", "add", createFile(t, repo, "README").Name())) 307 | runCommand(t, repo, exec.Command("git", "commit", "-m", "First Commit")) 308 | runCommand(t, repo, exec.Command("git", "add", createFile(t, repo, "test.txt").Name())) 309 | runCommand(t, repo, exec.Command("git", "commit", "-m", "Second Commit")) 310 | 311 | t.Logf("Init repository %s", repo) 312 | 313 | return repo 314 | } 315 | 316 | func runCommand(t *testing.T, repo string, c *exec.Cmd) { 317 | t.Logf("Running command %s", strings.Join(c.Args, " ")) 318 | 319 | output, err := c.CombinedOutput() 320 | 321 | if err != nil { 322 | t.Fatalf("Failed to init repository %s, %s, %s", repo, err, output) 323 | } 324 | } 325 | 326 | func createFile(t *testing.T, repo string, filename string) *os.File { 327 | file, err := os.Create(filepath.Join(repo, filename)) 328 | 329 | if err != nil { 330 | t.Fatalf("Failed to init repository %s", repo) 331 | } 332 | 333 | return file 334 | } 335 | -------------------------------------------------------------------------------- /cmd/dump.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "encoding/csv" 5 | "github.com/iangrunert/git-ratchet/store" 6 | log "github.com/spf13/jwalterweatherman" 7 | "io" 8 | "strconv" 9 | ) 10 | 11 | func Dump(prefix string, output io.Writer) int { 12 | log.INFO.Println("Reading measures stored in git") 13 | gitlog := store.CommitMeasureCommand(prefix) 14 | 15 | readStoredMeasure, err := store.CommitMeasures(gitlog) 16 | if err != nil { 17 | log.FATAL.Println(err) 18 | return 20 19 | } 20 | 21 | for { 22 | cm, err := readStoredMeasure() 23 | 24 | // Empty state of the repository - no stored metrics. 25 | if err == io.EOF { 26 | break 27 | } else if err != nil { 28 | log.FATAL.Println(err) 29 | return 40 30 | } 31 | 32 | out := csv.NewWriter(output) 33 | 34 | for _, measure := range cm.Measures { 35 | out.Write([]string{cm.Timestamp.String(), measure.Name, strconv.Itoa(measure.Value), strconv.Itoa(measure.Baseline)}) 36 | } 37 | out.Flush() 38 | } 39 | 40 | log.INFO.Println("Finished reading measures stored in git") 41 | return 0 42 | } 43 | -------------------------------------------------------------------------------- /cmd/dump_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "os/exec" 7 | "strings" 8 | "testing" 9 | 10 | log "github.com/spf13/jwalterweatherman" 11 | ) 12 | 13 | func TestDump(t *testing.T) { 14 | if testing.Verbose() { 15 | log.SetLogThreshold(log.LevelInfo) 16 | log.SetStdoutThreshold(log.LevelInfo) 17 | } 18 | 19 | repo := createEmptyGitRepo(t) 20 | 21 | runCommand(t, repo, exec.Command("git", "notes", "add", "-m", `"Hello"`)) 22 | 23 | runCheckP(t, "foo", true, "foo,5") 24 | runCommand(t, repo, exec.Command("git", "add", createFile(t, repo, "bar.txt").Name())) 25 | runCommand(t, repo, exec.Command("git", "commit", "-m", "Third Commit")) 26 | runCheckP(t, "foo", true, "foo,4") 27 | 28 | dump := bufio.NewScanner(bytes.NewReader(runDump(t, "foo").Bytes())) 29 | 30 | dump.Scan() 31 | 32 | checkString(t, "foo,4,4", dump.Text()) 33 | 34 | dump.Scan() 35 | 36 | checkString(t, "foo,5,5", dump.Text()) 37 | 38 | if len(runDump(t, "bar").Bytes()) > 0 { 39 | t.Fatalf("Should be no data under prefix bar") 40 | } 41 | } 42 | 43 | func checkString(t *testing.T, expected string, actual string) { 44 | if !strings.HasSuffix(actual, expected) { 45 | t.Fatalf("Dump incorrect. Expected suffix %s got %s", expected, actual) 46 | } 47 | } 48 | 49 | func runDump(t *testing.T, prefix string) *bytes.Buffer { 50 | t.Logf("Running dump command") 51 | 52 | buf := new(bytes.Buffer) 53 | 54 | errCode := Dump(prefix, buf) 55 | 56 | if errCode != 0 { 57 | t.Fatalf("Dump command failed! Error code: %d", errCode) 58 | } 59 | 60 | return buf 61 | } 62 | -------------------------------------------------------------------------------- /cmd/excuse.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/iangrunert/git-ratchet/store" 5 | log "github.com/spf13/jwalterweatherman" 6 | "strings" 7 | ) 8 | 9 | func Excuse(prefix string, measure string, excuse string) int { 10 | name, err := store.GetCommitterName() 11 | 12 | if err != nil { 13 | log.FATAL.Println("Error when fetching committer name") 14 | log.DEBUG.Println(err) 15 | return 10 16 | } 17 | 18 | exclusion := store.Exclusion{Committer: name, Excuse: excuse, Measure: strings.Split(measure, ",")} 19 | 20 | err = store.WriteExclusion(prefix, exclusion) 21 | 22 | if err != nil { 23 | log.FATAL.Println("Error writing exclusion note %s", err) 24 | return 20 25 | } 26 | 27 | return 0 28 | } 29 | -------------------------------------------------------------------------------- /cmd/testdata/output.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | 527 | 528 | 529 | 530 | 531 | 532 | 533 | 534 | 535 | 536 | 537 | 538 | 539 | 540 | 541 | 542 | 543 | 544 | 545 | 546 | 547 | 548 | 549 | 550 | 551 | 552 | 553 | 554 | 555 | 556 | 557 | 558 | 559 | 560 | 561 | 562 | 563 | 564 | 565 | 566 | 567 | 568 | 569 | 570 | 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | 579 | 580 | 581 | 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | 596 | 597 | 598 | 599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 | 607 | 608 | 609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 | 619 | 620 | 621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 633 | 634 | 635 | 636 | 637 | 638 | 639 | 640 | 641 | 642 | 643 | 644 | 645 | 646 | 647 | 648 | 649 | 650 | 651 | 652 | 653 | 654 | 655 | 656 | 657 | 658 | 659 | 660 | 661 | 662 | 663 | 664 | 665 | 666 | 667 | 668 | 669 | 670 | 671 | 672 | 673 | 674 | 675 | 676 | 677 | 678 | 679 | 680 | 681 | 682 | 683 | 684 | 685 | 686 | 687 | 688 | 689 | 690 | 691 | 692 | 693 | 694 | 695 | 696 | 697 | 698 | 699 | 700 | 701 | 702 | 703 | 704 | 705 | 706 | 707 | 708 | 709 | 710 | 711 | 712 | 713 | 714 | 715 | 716 | 717 | 718 | 719 | 720 | 721 | 722 | 723 | 724 | 725 | 726 | 727 | 728 | 729 | 730 | 731 | 732 | 733 | 734 | 735 | 736 | 737 | 738 | 739 | 740 | 741 | 742 | 743 | 744 | 745 | 746 | 747 | 748 | 749 | 750 | 751 | 752 | 753 | 754 | 755 | 756 | 757 | 758 | 759 | 760 | 761 | 762 | 763 | 764 | 765 | 766 | 767 | 768 | 769 | 770 | 771 | 772 | 773 | 774 | 775 | 776 | 777 | 778 | 779 | 780 | 781 | 782 | 783 | 784 | 785 | 786 | 787 | 788 | 789 | 790 | 791 | 792 | 793 | 794 | 795 | 796 | 797 | 798 | 799 | 800 | 801 | 802 | 803 | 804 | 805 | 806 | 807 | 808 | 809 | 810 | 811 | 812 | 813 | 814 | 815 | 816 | 817 | 818 | 819 | 820 | 821 | 822 | 823 | 824 | 825 | 826 | 827 | 828 | 829 | 830 | 831 | 832 | 833 | 834 | 835 | 836 | 837 | 838 | 839 | 840 | 841 | 842 | 843 | 844 | 845 | 846 | 847 | 848 | 849 | 850 | 851 | 852 | 853 | 854 | 855 | 856 | 857 | 858 | 859 | 860 | 861 | 862 | 863 | 864 | 865 | 866 | 867 | 868 | 869 | 870 | 871 | 872 | 873 | 874 | 875 | 876 | 877 | 878 | 879 | 880 | 881 | 882 | 883 | 884 | 885 | 886 | 887 | 888 | 889 | 890 | 891 | 892 | 893 | 894 | 895 | 896 | 897 | 898 | 899 | 900 | 901 | 902 | 903 | 904 | 905 | 906 | 907 | 908 | 909 | 910 | 911 | 912 | 913 | 914 | 915 | 916 | 917 | 918 | 919 | 920 | 921 | 922 | 923 | 924 | 925 | 926 | 927 | 928 | 929 | 930 | 931 | 932 | 933 | 934 | 935 | 936 | 937 | 938 | 939 | 940 | 941 | 942 | 943 | 944 | 945 | 946 | 947 | 948 | 949 | 950 | 951 | 952 | 953 | 954 | 955 | 956 | 957 | 958 | 959 | 960 | 961 | 962 | 963 | 964 | 965 | 966 | 967 | 968 | 969 | 970 | 971 | 972 | 973 | 974 | 975 | 976 | 977 | 978 | 979 | 980 | 981 | 982 | 983 | 984 | 985 | 986 | 987 | 988 | 989 | 990 | 991 | 992 | 993 | 994 | 995 | 996 | 997 | 998 | 999 | 1000 | 1001 | 1002 | 1003 | 1004 | 1005 | 1006 | 1007 | 1008 | 1009 | 1010 | 1011 | 1012 | 1013 | 1014 | 1015 | 1016 | 1017 | 1018 | 1019 | 1020 | 1021 | 1022 | 1023 | 1024 | 1025 | 1026 | 1027 | 1028 | 1029 | 1030 | 1031 | 1032 | 1033 | 1034 | 1035 | 1036 | 1037 | 1038 | 1039 | 1040 | 1041 | 1042 | 1043 | 1044 | 1045 | 1046 | 1047 | 1048 | 1049 | 1050 | 1051 | 1052 | 1053 | 1054 | 1055 | 1056 | 1057 | 1058 | 1059 | 1060 | 1061 | 1062 | 1063 | 1064 | 1065 | 1066 | 1067 | 1068 | 1069 | 1070 | 1071 | 1072 | 1073 | 1074 | 1075 | 1076 | 1077 | 1078 | 1079 | 1080 | 1081 | 1082 | 1083 | 1084 | 1085 | 1086 | 1087 | 1088 | 1089 | 1090 | 1091 | 1092 | 1093 | 1094 | 1095 | 1096 | 1097 | 1098 | 1099 | 1100 | 1101 | 1102 | 1103 | 1104 | 1105 | 1106 | 1107 | 1108 | 1109 | 1110 | 1111 | 1112 | 1113 | 1114 | 1115 | 1116 | 1117 | 1118 | 1119 | 1120 | 1121 | 1122 | 1123 | 1124 | 1125 | 1126 | 1127 | 1128 | 1129 | 1130 | 1131 | 1132 | 1133 | 1134 | 1135 | 1136 | 1137 | 1138 | 1139 | 1140 | 1141 | 1142 | 1143 | 1144 | 1145 | 1146 | 1147 | 1148 | 1149 | 1150 | 1151 | 1152 | 1153 | 1154 | 1155 | 1156 | 1157 | 1158 | 1159 | 1160 | 1161 | 1162 | 1163 | 1164 | 1165 | 1166 | 1167 | 1168 | 1169 | 1170 | 1171 | 1172 | 1173 | 1174 | 1175 | 1176 | 1177 | 1178 | 1179 | 1180 | 1181 | 1182 | 1183 | 1184 | 1185 | 1186 | 1187 | 1188 | 1189 | 1190 | 1191 | 1192 | 1193 | 1194 | 1195 | 1196 | 1197 | 1198 | 1199 | 1200 | 1201 | 1202 | 1203 | 1204 | 1205 | 1206 | 1207 | 1208 | 1209 | 1210 | 1211 | 1212 | 1213 | 1214 | 1215 | 1216 | 1217 | 1218 | 1219 | 1220 | 1221 | 1222 | 1223 | 1224 | 1225 | 1226 | 1227 | 1228 | 1229 | 1230 | 1231 | 1232 | 1233 | 1234 | 1235 | 1236 | 1237 | 1238 | 1239 | 1240 | 1241 | 1242 | 1243 | 1244 | 1245 | 1246 | 1247 | 1248 | 1249 | 1250 | 1251 | 1252 | 1253 | 1254 | 1255 | 1256 | 1257 | 1258 | -------------------------------------------------------------------------------- /docs/css/style.css: -------------------------------------------------------------------------------- 1 | h1 { 2 | font-family: Verdana, Geneva, sans-serif; 3 | font-size: 24px; 4 | font-style: normal; 5 | font-variant: normal; 6 | font-weight: 500; 7 | line-height: 26.3999996185303px; 8 | text-align: center; 9 | } 10 | h3 { 11 | font-family: Verdana, Geneva, sans-serif; 12 | font-size: 14px; 13 | font-style: normal; 14 | font-variant: normal; 15 | font-weight: 500; 16 | line-height: 15.3999996185303px; 17 | text-align: center; 18 | padding-bottom: 0.5em; 19 | border-bottom: 1px solid #eee; 20 | } 21 | p { 22 | font-family: Verdana, Geneva, sans-serif; 23 | font-size: 14px; 24 | font-style: normal; 25 | font-variant: normal; 26 | font-weight: 400; 27 | line-height: 20px; 28 | } 29 | blockquote { 30 | font-family: Verdana, Geneva, sans-serif; 31 | font-size: 21px; 32 | font-style: normal; 33 | font-variant: normal; 34 | font-weight: 400; 35 | line-height: 30px; 36 | } 37 | pre { 38 | font-family: Verdana, Geneva, sans-serif; 39 | font-size: 13px; 40 | font-style: normal; 41 | font-variant: normal; 42 | font-weight: 400; 43 | line-height: 18.5714302062988px; 44 | } 45 | section,footer { 46 | margin-left: auto; 47 | margin-right: auto; 48 | width: 600px; 49 | } 50 | footer { 51 | text-align: center; 52 | } 53 | .right .github-fork-ribbon { 54 | background-color: gray; 55 | } -------------------------------------------------------------------------------- /git-ratchet.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | ratchet "github.com/iangrunert/git-ratchet/cmd" 6 | "github.com/spf13/cobra" 7 | log "github.com/spf13/jwalterweatherman" 8 | "os" 9 | ) 10 | 11 | var GitTag string // Will be passed to the compiler by scripts/build.sh 12 | 13 | func main() { 14 | var write bool 15 | var verbose bool 16 | var zeroOnMissing bool 17 | var prefix string 18 | var slack float64 19 | var usePercents bool 20 | var inputType string 21 | 22 | var versionCmd = &cobra.Command{ 23 | Use: "version", 24 | Short: "Print the version number", 25 | Long: `All software has versions.`, 26 | Run: func(cmd *cobra.Command, args []string) { 27 | fmt.Printf("git-ratchet version: %s\n", GitTag) 28 | }, 29 | } 30 | 31 | var checkCmd = &cobra.Command{ 32 | Use: "check", 33 | Short: "Checks the values passed in against the most recent stored values.", 34 | Long: `Checks the values passed in against the most recent stored values. 35 | The most recent stored values are found by walking up the commit graph and looking at the git-notes stored.`, 36 | Run: func(cmd *cobra.Command, args []string) { 37 | if verbose { 38 | log.SetLogThreshold(log.LevelInfo) 39 | log.SetStdoutThreshold(log.LevelInfo) 40 | } 41 | 42 | err := ratchet.Check(prefix, slack, usePercents, write, inputType, zeroOnMissing, os.Stdin) 43 | if err != 0 { 44 | os.Exit(err) 45 | } 46 | }, 47 | } 48 | 49 | checkCmd.Flags().BoolVarP(&write, "write", "w", false, "write values if no increase is detected. only use on your CI server.") 50 | checkCmd.Flags().Float64VarP(&slack, "slack", "s", 0, "slack value, increase within the range of the slack is acceptable.") 51 | checkCmd.Flags().BoolVarP(&usePercents, "usePercents", "r", false, "slack value is specified in relative percentage.") 52 | checkCmd.Flags().StringVarP(&inputType, "inputType", "i", "csv", "input type. csv and checkstyle available.") 53 | checkCmd.Flags().BoolVarP(&zeroOnMissing, "zero-on-missing", "z", false, "set measure values to zero on missing..") 54 | 55 | var measure string 56 | var excuse string 57 | 58 | var excuseCmd = &cobra.Command{ 59 | Use: "excuse", 60 | Short: "Write an excuse for a measurement increase.", 61 | Long: `Write an excuse for a measurement increase. This will allow the check command to pass.`, 62 | Run: func(cmd *cobra.Command, args []string) { 63 | if verbose { 64 | log.SetLogThreshold(log.LevelInfo) 65 | log.SetStdoutThreshold(log.LevelInfo) 66 | } 67 | 68 | os.Exit(ratchet.Excuse(prefix, measure, excuse)) 69 | }, 70 | } 71 | 72 | excuseCmd.Flags().StringVarP(&measure, "name", "n", "", "names of the measures to excuse, comma separated list.") 73 | excuseCmd.Flags().StringVarP(&excuse, "excuse", "e", "", "excuse for the measure rising.") 74 | 75 | var dumpCmd = &cobra.Command{ 76 | Use: "dump", 77 | Short: "Dump a CSV file containing the measurement data over time.", 78 | Long: `Dump a CSV file containing the measurement data over time.`, 79 | Run: func(cmd *cobra.Command, args []string) { 80 | if verbose { 81 | log.SetLogThreshold(log.LevelInfo) 82 | log.SetStdoutThreshold(log.LevelInfo) 83 | } 84 | 85 | err := ratchet.Dump(prefix, os.Stdout) 86 | 87 | if err != 0 { 88 | os.Exit(err) 89 | } 90 | }, 91 | } 92 | 93 | var rootCmd = &cobra.Command{Use: "git-ratchet"} 94 | rootCmd.AddCommand(checkCmd, excuseCmd, dumpCmd, versionCmd) 95 | rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "increase logging verbosity.") 96 | rootCmd.PersistentFlags().StringVarP(&prefix, "prefix", "p", "master", "prefix the ratchet notes. useful for storing multiple sets of values in the same repo.") 97 | 98 | rootCmd.Execute() 99 | } 100 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | git-ratchet 6 | 7 | 8 | 11 | 12 | 13 | 14 |
15 |

git-ratchet

16 |

Iteratively improve software with git-ratchet

17 |
18 |
19 |

20 | A tool for creating ratcheted builds - Make CI fail when a given measure increases. 21 |

22 |

23 | Introduce static analysis tools to your project without fixing all the warnings in one sitting. Ensure your team works with you towards your technical direction. Have automatically updated baselines for measures which you are improving, such as performance baselines. 24 |

25 |

26 | The data is stored inside git-notes which means it's right next to your code. You don't have to set up and maintain an additional server to store the data. 27 |

28 |

29 | Take a peek at the README to get started. 30 |

31 |
32 |
33 |
34 | Fork me on GitHub 35 |
36 |
37 | 38 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # This script builds the application from source for multiple platforms. 4 | 5 | # Get the parent directory of where this script is. 6 | 7 | SOURCE="${BASH_SOURCE[0]}" 8 | while [ -h "$SOURCE" ] ; do SOURCE="$(readlink "$SOURCE")"; done 9 | DIR="$( cd -P "$( dirname "$SOURCE" )/.." && pwd )" 10 | 11 | # Change into that directory 12 | cd "$DIR" 13 | 14 | # Get latest tag 15 | VERSION=$(git describe --abbrev=0 --tags --match="v*") 16 | 17 | # Determine the arch/os combos we're building for 18 | XC_ARCH=${XC_ARCH:-"386 amd64 arm"} 19 | XC_OS=${XC_OS:-linux darwin windows} 20 | 21 | gox \ 22 | -os="${XC_OS}" \ 23 | -arch="${XC_ARCH}" \ 24 | -ldflags "-X main.GitTag=${VERSION}" \ 25 | -output "dist/{{.OS}}_{{.Arch}}_{{.Dir}}" \ 26 | ./... 27 | 28 | # Done! 29 | echo 30 | echo "==> Results:" 31 | ls -hl dist/* 32 | 33 | ghr --username iangrunert --token $GITHUB_TOKEN --replace --prerelease --debug $VERSION dist/ 34 | -------------------------------------------------------------------------------- /store/git.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "fmt" 5 | log "github.com/spf13/jwalterweatherman" 6 | "io" 7 | "os" 8 | "os/exec" 9 | "strings" 10 | ) 11 | 12 | func GitLog(ref string, commitrange string, format string) *exec.Cmd { 13 | gitlog := exec.Command("git", "--no-pager", "log", "--notes="+ref, `--pretty=format:'`+format+`'`, commitrange) 14 | log.INFO.Println(strings.Join(gitlog.Args, " ")) 15 | return gitlog 16 | } 17 | 18 | func GetCommitterName() (string, error) { 19 | getname := exec.Command("git", "config", "--get", "user.name") 20 | 21 | name, err := getname.CombinedOutput() 22 | 23 | if err != nil { 24 | log.ERROR.Printf("Get committer name failed %s : %s", err, name) 25 | return "", err 26 | } 27 | 28 | return strings.Trim(string(name), "\n"), nil 29 | } 30 | 31 | func WriteNotes(writef func(io.Writer) error, ref string) error { 32 | // Create a temporary file to store the note data 33 | notepath := ".git-ratchet-note" 34 | 35 | tempfile, err := os.Create(notepath) 36 | if err != nil { 37 | return fmt.Errorf("Error creating file .git-ratchet-note %s", err) 38 | } 39 | defer os.Remove(notepath) 40 | 41 | err = writef(tempfile) 42 | if err != nil { 43 | return fmt.Errorf("Error writing notes to .git-ratchet-note %s", err) 44 | } 45 | 46 | err = tempfile.Close() 47 | if err != nil { 48 | return fmt.Errorf("Error closing .git-ratchet-note %s", err) 49 | } 50 | 51 | writenotes := exec.Command("git", "notes", "--ref="+ref, "add", "-f", "-F", notepath) 52 | 53 | log.INFO.Println(strings.Join(writenotes.Args, " ")) 54 | 55 | bytes, err := writenotes.CombinedOutput() 56 | 57 | if err != nil { 58 | return fmt.Errorf("Error writing notes %s, %s", err, string(bytes)) 59 | } 60 | 61 | return err 62 | } 63 | 64 | func PushNotes(ref string) error { 65 | pushnotes := exec.Command("git", "push", "origin", "refs/notes/"+ref) 66 | log.INFO.Println(strings.Join(pushnotes.Args, " ")) 67 | 68 | bytes, err := pushnotes.CombinedOutput() 69 | 70 | if err != nil { 71 | return fmt.Errorf("Error pushing notes %s, %s", err, bytes) 72 | } 73 | 74 | return err 75 | } 76 | -------------------------------------------------------------------------------- /store/reader.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "bufio" 5 | "encoding/csv" 6 | "encoding/json" 7 | "encoding/xml" 8 | "errors" 9 | log "github.com/spf13/jwalterweatherman" 10 | "io" 11 | "os/exec" 12 | "sort" 13 | "strconv" 14 | "strings" 15 | "syscall" 16 | "time" 17 | ) 18 | 19 | func ParseInputType(input string) InputType { 20 | switch input { 21 | case "csv": 22 | return CSV 23 | case "checkstyle": 24 | return Checkstyle 25 | default: 26 | return Unknown 27 | } 28 | } 29 | 30 | func CommitMeasureCommand(prefix string) *exec.Cmd { 31 | return GitLog("git-ratchet-1-"+prefix, "HEAD", `%H,%ae,%at,"%N",`) 32 | } 33 | 34 | func CommitMeasures(gitlog *exec.Cmd) (func() (CommitMeasure, error), error) { 35 | stdout, err := gitlog.StdoutPipe() 36 | if err != nil { 37 | return nil, err 38 | } 39 | 40 | output := csv.NewReader(stdout) 41 | output.TrailingComma = true 42 | 43 | err = gitlog.Start() 44 | if err != nil { 45 | return nil, err 46 | } 47 | 48 | return func() (CommitMeasure, error) { 49 | for { 50 | // The log is of the form commithash,committer,timestamp,note 51 | // If note is empty, there's no set of Measures 52 | record, err := output.Read() 53 | if err != nil { 54 | return CommitMeasure{}, err 55 | } 56 | 57 | // The note needs to be non-empty to contain measures. 58 | if len(record[len(record)-1]) == 0 { 59 | continue 60 | } 61 | 62 | timestamp, err := strconv.Atoi(strings.Trim(record[2], "\\\"")) 63 | if err != nil { 64 | return CommitMeasure{}, err 65 | } 66 | 67 | measures, err := ParseMeasures(strings.NewReader(strings.Trim(record[3], "\\\"")), CSV) 68 | if err != nil { 69 | return CommitMeasure{}, err 70 | } 71 | 72 | if len(measures) > 0 { 73 | return CommitMeasure{CommitHash: strings.Trim(record[0], "'"), 74 | Committer: record[1], 75 | Timestamp: time.Unix(int64(timestamp), 0), 76 | Measures: measures}, nil 77 | } 78 | } 79 | }, nil 80 | } 81 | 82 | func ParseMeasures(r io.Reader, t InputType) ([]Measure, error) { 83 | switch t { 84 | case CSV: 85 | return ParseMeasuresCSV(r) 86 | case Checkstyle: 87 | return ParseMeasuresCheckstyle(r) 88 | default: 89 | return nil, errors.New("Unknown input type") 90 | } 91 | } 92 | 93 | func ParseMeasuresCSV(r io.Reader) ([]Measure, error) { 94 | data := csv.NewReader(r) 95 | data.FieldsPerRecord = -1 // Variable number of fields per record 96 | 97 | measures := make([]Measure, 0) 98 | 99 | for { 100 | var baseline int 101 | 102 | arr, err := data.Read() 103 | if err == io.EOF { 104 | break 105 | } 106 | 107 | if err != nil { 108 | return nil, err 109 | } 110 | 111 | if len(arr) < 2 { 112 | return nil, errors.New("Badly formatted measures") 113 | } 114 | 115 | value, err := strconv.Atoi(arr[1]) 116 | if err != nil { 117 | return nil, err 118 | } 119 | 120 | if len(arr) > 2 { 121 | baseline, err = strconv.Atoi(arr[2]) 122 | if err != nil { 123 | return nil, err 124 | } 125 | } else { 126 | baseline = value 127 | } 128 | 129 | measure := Measure{Name: arr[0], Value: value, Baseline: baseline} 130 | measures = append(measures, measure) 131 | } 132 | 133 | sort.Sort(ByName(measures)) 134 | 135 | return measures, nil 136 | } 137 | 138 | func ParseMeasuresCheckstyle(r io.Reader) ([]Measure, error) { 139 | decoder := xml.NewDecoder(r) 140 | errors := 0 141 | 142 | for { 143 | t, _ := decoder.Token() 144 | if t == nil { 145 | break 146 | } 147 | switch se := t.(type) { 148 | case xml.StartElement: 149 | if se.Name.Local == "error" { 150 | errors++ 151 | } 152 | } 153 | } 154 | 155 | return []Measure{{Name: "errors", Value: errors, Baseline: errors}}, nil 156 | } 157 | 158 | func CompareMeasures(prefix string, hash string, storedm []Measure, computedm []Measure, slack float64, usePercents bool, zeroOnMissing bool) ([]Measure, error) { 159 | if len(storedm) == 0 { 160 | return computedm, errors.New("No stored measures to compare against.") 161 | } 162 | 163 | excuses, err := GetExclusions(prefix, hash) 164 | 165 | if err != nil { 166 | return computedm, err 167 | } 168 | 169 | log.INFO.Printf("Total excuses %s", excuses) 170 | 171 | failing := make([]*Measure, 0) 172 | zeroMes := make([]Measure, 0) 173 | 174 | i := 0 175 | j := 0 176 | 177 | exc := 0 178 | 179 | for i < len(storedm) && j < len(computedm) { 180 | stored := storedm[i] 181 | computed := computedm[j] 182 | 183 | log.INFO.Printf("Checking measures: %s %s", stored.Name, computed.Name) 184 | if stored.Name < computed.Name { 185 | log.ERROR.Printf("Missing computed value for stored measure: %s", stored.Name) 186 | if zeroOnMissing { 187 | zeroMes = append(zeroMes, Measure{Name: stored.Name, Value: 0, Baseline: 0}) 188 | } else { 189 | failing = append(failing, &stored) 190 | } 191 | i++ 192 | } else if computed.Name < stored.Name { 193 | log.WARN.Printf("New measure found: %s", computed.Name) 194 | j++ 195 | } else { 196 | if computed.Baseline > stored.Baseline { 197 | computed.Baseline = stored.Baseline 198 | computedm[j].Baseline = stored.Baseline 199 | } 200 | 201 | delta := computed.Value - stored.Baseline 202 | deltaPercent := 100.0 203 | if stored.Baseline > 0 { 204 | deltaPercent = float64(delta) * 100.0 / float64(stored.Baseline) 205 | } 206 | 207 | // Compare the value 208 | if deltaIsUnacceptable(delta, deltaPercent, slack, usePercents) { 209 | log.ERROR.Printf("Measure rising: %s, delta %d (%g percents)", computed.Name, delta, deltaPercent) 210 | 211 | if exc < len(excuses) { 212 | ex := excuses[exc] 213 | 214 | log.INFO.Printf("Checking excuses: %s %s", ex, computed) 215 | if ex < computed.Name { 216 | log.WARN.Printf("Exclusion found for not failing measure: %s", ex) 217 | exc++ 218 | failing = append(failing, &computed) 219 | } else if computed.Name < ex { 220 | log.ERROR.Printf("No exclusion for failing measure: %s", computed.Name) 221 | failing = append(failing, &computed) 222 | } else { 223 | log.WARN.Printf("Exclusion found for failing measure: %s", computed.Name) 224 | computed.Baseline = computed.Value 225 | computedm[j].Baseline = computed.Value 226 | exc++ 227 | } 228 | } else { 229 | failing = append(failing, &computed) 230 | } 231 | 232 | } 233 | i++ 234 | j++ 235 | } 236 | } 237 | 238 | for i < len(storedm) { 239 | stored := storedm[i] 240 | log.ERROR.Printf("Missing computed value for stored measure: %s", stored.Name) 241 | if zeroOnMissing { 242 | zeroMes = append(zeroMes, Measure{Name: stored.Name, Value: 0, Baseline: 0}) 243 | } else { 244 | failing = append(failing, &stored) 245 | } 246 | i++ 247 | } 248 | 249 | for j < len(computedm) { 250 | computed := computedm[j] 251 | log.WARN.Printf("New measure found: %s", computed.Name) 252 | j++ 253 | } 254 | 255 | if len(failing) > 0 { 256 | return computedm, errors.New("One or more metrics currently failing.") 257 | } 258 | 259 | computedm = append(computedm, zeroMes...) 260 | sort.Sort(ByName(computedm)) 261 | 262 | return computedm, nil 263 | } 264 | 265 | func deltaIsUnacceptable(delta int, deltaPercent float64, slack float64, usePercents bool) bool { 266 | if usePercents { 267 | return deltaPercent > slack 268 | } else { 269 | return float64(delta) > slack 270 | } 271 | } 272 | 273 | func GetExclusions(prefix string, hash string) ([]string, error) { 274 | ref := "git-ratchet-excuse-1-" + prefix 275 | 276 | gitlog := GitLog(ref, hash+"^1..HEAD", "%N") 277 | 278 | stdout, err := gitlog.StdoutPipe() 279 | if err != nil { 280 | return []string{}, err 281 | } 282 | 283 | scanner := bufio.NewScanner(stdout) 284 | 285 | err = gitlog.Start() 286 | if err != nil { 287 | return []string{}, err 288 | } 289 | 290 | exclusions := make([]string, 0) 291 | 292 | for scanner.Scan() { 293 | record := strings.Trim(scanner.Text(), "'") 294 | 295 | if len(record) == 0 { 296 | continue 297 | } 298 | 299 | measures, err := ParseExclusion(record) 300 | 301 | if err != nil && err != io.EOF { 302 | return []string{}, err 303 | } 304 | 305 | exclusions = append(exclusions, measures...) 306 | } 307 | 308 | if err = scanner.Err(); err != nil { 309 | return []string{}, err 310 | } 311 | 312 | stdout.Close() 313 | 314 | err = gitlog.Wait() 315 | 316 | if err != nil && err != syscall.EPIPE { 317 | return []string{}, err 318 | } 319 | 320 | sort.Strings(exclusions) 321 | 322 | return exclusions, nil 323 | } 324 | 325 | func ParseExclusion(ex string) ([]string, error) { 326 | log.INFO.Printf("Exclusion %s", ex) 327 | 328 | var m Exclusion 329 | err := json.Unmarshal([]byte(strings.Trim(ex, "'")), &m) 330 | 331 | if err != nil { 332 | return []string{}, err 333 | } 334 | 335 | return m.Measure, nil 336 | } 337 | -------------------------------------------------------------------------------- /store/types.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type InputType int 8 | 9 | const ( 10 | CSV = iota 11 | Checkstyle 12 | Unknown 13 | ) 14 | 15 | type Measure struct { 16 | Name string 17 | Value int 18 | Baseline int 19 | } 20 | 21 | type CommitMeasure struct { 22 | CommitHash string 23 | Timestamp time.Time 24 | Committer string 25 | Measures []Measure 26 | } 27 | 28 | type Exclusion struct { 29 | Committer string 30 | Excuse string 31 | Measure []string 32 | } 33 | 34 | type ByName []Measure 35 | 36 | func (a ByName) Len() int { return len(a) } 37 | func (a ByName) Swap(i, j int) { a[i], a[j] = a[j], a[i] } 38 | func (a ByName) Less(i, j int) bool { return a[i].Name < a[j].Name } 39 | 40 | func (cm *CommitMeasure) String() string { 41 | return cm.CommitHash 42 | } 43 | -------------------------------------------------------------------------------- /store/writer.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "encoding/csv" 5 | "encoding/json" 6 | "io" 7 | "sort" 8 | "strconv" 9 | 10 | log "github.com/spf13/jwalterweatherman" 11 | ) 12 | 13 | func PutMeasures(prefix string, m []Measure) error { 14 | writef := func(tempfile io.Writer) error { 15 | err := WriteMeasures(m, tempfile) 16 | if err != nil { 17 | return err 18 | } 19 | return nil 20 | } 21 | 22 | return WriteNotes(writef, "git-ratchet-1-"+prefix) 23 | } 24 | 25 | func WriteMeasures(measures []Measure, w io.Writer) error { 26 | out := csv.NewWriter(w) 27 | sort.Sort(ByName(measures)) 28 | for _, m := range measures { 29 | err := out.Write([]string{m.Name, strconv.Itoa(m.Value), strconv.Itoa(m.Baseline)}) 30 | if err != nil { 31 | return err 32 | } 33 | } 34 | out.Flush() 35 | return nil 36 | } 37 | 38 | func WriteExclusion(prefix string, ex Exclusion) error { 39 | ref := "git-ratchet-excuse-1-" + prefix 40 | 41 | writef := func(tempfile io.Writer) error { 42 | b, err := json.Marshal(ex) 43 | 44 | if err != nil { 45 | return err 46 | } 47 | 48 | tempfile.Write(b) 49 | return nil 50 | } 51 | 52 | err := WriteNotes(writef, ref) 53 | 54 | if err != nil { 55 | return err 56 | } 57 | 58 | err = PushNotes(ref) 59 | 60 | if err != nil { 61 | log.ERROR.Printf("Error while pushing notes: %s", err) 62 | } 63 | 64 | return nil 65 | } 66 | --------------------------------------------------------------------------------