PageRenderTime 52ms CodeModel.GetById 22ms RepoModel.GetById 0ms app.codeStats 0ms

/dev/devcam/hook.go

http://github.com/bradfitz/camlistore
Go | 292 lines | 214 code | 31 blank | 47 comment | 62 complexity | 4e634c3c3133984261b692d877be333c MD5 | raw file
Possible License(s): CC0-1.0, MIT, BSD-3-Clause, 0BSD, MPL-2.0-no-copyleft-exception, BSD-2-Clause, Apache-2.0
  1. /*
  2. Copyright 2015 The Perkeep Authors.
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. // This file adds the "hook" subcommand to devcam, to install and run git hooks.
  14. package main
  15. import (
  16. "bytes"
  17. "errors"
  18. "flag"
  19. "fmt"
  20. "io/ioutil"
  21. "os"
  22. "os/exec"
  23. "path/filepath"
  24. "sort"
  25. "strings"
  26. "perkeep.org/pkg/cmdmain"
  27. )
  28. var hookPath = ".git/hooks/"
  29. var hookFiles = []string{
  30. "pre-commit",
  31. "commit-msg",
  32. }
  33. func (c *hookCmd) installHook() error {
  34. root, err := repoRoot()
  35. if err != nil {
  36. return err
  37. }
  38. hookDir := filepath.Join(root, hookPath)
  39. if _, err := os.Stat(hookDir); err != nil {
  40. if !os.IsNotExist(err) {
  41. return err
  42. }
  43. if err := os.MkdirAll(hookDir, 0700); err != nil {
  44. return err
  45. }
  46. }
  47. for _, hookFile := range hookFiles {
  48. filename := filepath.Join(hookDir, hookFile)
  49. hookContent := fmt.Sprintf(hookScript, hookFile)
  50. // If hook file exists, assume it is okay.
  51. _, err := os.Stat(filename)
  52. if err == nil {
  53. if c.verbose {
  54. data, err := ioutil.ReadFile(filename)
  55. if err != nil {
  56. c.verbosef("reading hook: %v", err)
  57. } else if string(data) != hookContent {
  58. c.verbosef("unexpected hook content in %s", filename)
  59. }
  60. }
  61. continue
  62. }
  63. if !os.IsNotExist(err) {
  64. return fmt.Errorf("checking hook: %v", err)
  65. }
  66. c.verbosef("installing %s hook", hookFile)
  67. if err := ioutil.WriteFile(filename, []byte(hookContent), 0700); err != nil {
  68. return fmt.Errorf("writing hook: %v", err)
  69. }
  70. }
  71. return nil
  72. }
  73. var hookScript = `#!/bin/sh
  74. exec devcam hook %s "$@"
  75. `
  76. type hookCmd struct {
  77. verbose bool
  78. }
  79. func init() {
  80. cmdmain.RegisterMode("hook", func(flags *flag.FlagSet) cmdmain.CommandRunner {
  81. cmd := &hookCmd{}
  82. flags.BoolVar(&cmd.verbose, "verbose", false, "Be verbose.")
  83. // TODO(mpl): "-w" flag to run gofmt -w and devcam fixv -w. for now just print instruction.
  84. return cmd
  85. })
  86. }
  87. func (c *hookCmd) Usage() {
  88. printf("Usage: devcam [globalopts] hook [[hook-name] [args...]]\n")
  89. }
  90. func (c *hookCmd) Examples() []string {
  91. return []string{
  92. "# install the hooks (if needed)",
  93. "pre-commit # install the hooks (if needed), then run the pre-commit hook",
  94. }
  95. }
  96. func (c *hookCmd) Describe() string {
  97. return "Install git hooks for Perkeep, and if given, run the hook given as argument. Currently available hooks are: " + strings.TrimSuffix(strings.Join(hookFiles, ", "), ",") + "."
  98. }
  99. func (c *hookCmd) RunCommand(args []string) error {
  100. if err := c.installHook(); err != nil {
  101. return err
  102. }
  103. if len(args) == 0 {
  104. return nil
  105. }
  106. switch args[0] {
  107. case "pre-commit":
  108. if err := c.hookPreCommit(args[1:]); err != nil {
  109. if !(len(args) > 1 && args[1] == "test") {
  110. printf("You can override these checks with 'git commit --no-verify'\n")
  111. }
  112. cmdmain.ExitWithFailure = true
  113. return err
  114. }
  115. }
  116. return nil
  117. }
  118. // hookPreCommit does the following checks, in order:
  119. // gofmt, and trailing space.
  120. // If appropriate, any one of these checks prints the action
  121. // required from the user, and the following checks are not
  122. // performed.
  123. func (c *hookCmd) hookPreCommit(args []string) (err error) {
  124. if err = c.hookGofmt(); err != nil {
  125. return err
  126. }
  127. return c.hookTrailingSpace()
  128. }
  129. // hookGofmt runs a gofmt check on the local files matching the files in the
  130. // git staging area.
  131. // An error is returned if something went wrong or if some of the files need
  132. // gofmting. In the latter case, the instruction is printed.
  133. func (c *hookCmd) hookGofmt() error {
  134. if os.Getenv("GIT_GOFMT_HOOK") == "off" {
  135. printf("gofmt disabled by $GIT_GOFMT_HOOK=off\n")
  136. return nil
  137. }
  138. files, err := c.runGofmt()
  139. if err != nil {
  140. printf("gofmt hook reported errors:\n\t%v\n", strings.Replace(strings.TrimSpace(err.Error()), "\n", "\n\t", -1))
  141. return errors.New("gofmt errors")
  142. }
  143. if len(files) == 0 {
  144. return nil
  145. }
  146. printf("You need to format with gofmt:\n\tgofmt -w %s\n",
  147. strings.Join(files, " "))
  148. return errors.New("gofmt required")
  149. }
  150. func (c *hookCmd) hookTrailingSpace() error {
  151. // see 'pathspec' for the ':!' syntax to ignore a directory.
  152. out, _ := cmdOutputDirErr(".", "git", "diff-index", "--check", "--diff-filter=ACM", "--cached", "HEAD", "--", ".", ":!/vendor/")
  153. if out != "" {
  154. printf("\n%s", out)
  155. printf("Trailing whitespace detected, you need to clean it up manually.\n")
  156. return errors.New("trailing whitespace")
  157. }
  158. return nil
  159. }
  160. // runGofmt runs the external gofmt command over the local version of staged files.
  161. // It returns the files that need gofmting.
  162. func (c *hookCmd) runGofmt() (files []string, err error) {
  163. repo, err := repoRoot()
  164. if err != nil {
  165. return nil, err
  166. }
  167. if !strings.HasSuffix(repo, string(filepath.Separator)) {
  168. repo += string(filepath.Separator)
  169. }
  170. out, err := cmdOutputDirErr(".", "git", "diff-index", "--name-only", "--diff-filter=ACM", "--cached", "HEAD", "--", ":(glob)**/*.go", ":!/vendor/")
  171. if err != nil {
  172. return nil, err
  173. }
  174. indexFiles := addRoot(repo, nonBlankLines(out))
  175. if len(indexFiles) == 0 {
  176. return
  177. }
  178. args := []string{"-l"}
  179. // TODO(mpl): it would be nice to TrimPrefix the pwd from each file to get a shorter output.
  180. // However, since git sets the pwd to GIT_DIR before running the pre-commit hook, we lost
  181. // the actual pwd from when we ran `git commit`, so no dice so far.
  182. for _, file := range indexFiles {
  183. args = append(args, file)
  184. }
  185. if c.verbose {
  186. fmt.Fprintln(cmdmain.Stderr, commandString("gofmt", args))
  187. }
  188. cmd := exec.Command("gofmt", args...)
  189. var stdout, stderr bytes.Buffer
  190. cmd.Stdout = &stdout
  191. cmd.Stderr = &stderr
  192. err = cmd.Run()
  193. if err != nil {
  194. // Error but no stderr: usually can't find gofmt.
  195. if stderr.Len() == 0 {
  196. return nil, fmt.Errorf("invoking gofmt: %v", err)
  197. }
  198. return nil, fmt.Errorf("%s: %v", stderr.String(), err)
  199. }
  200. // Build file list.
  201. files = lines(stdout.String())
  202. sort.Strings(files)
  203. return files, nil
  204. }
  205. func printf(format string, args ...interface{}) {
  206. cmdmain.Errorf(format, args...)
  207. }
  208. func addRoot(root string, list []string) []string {
  209. var out []string
  210. for _, x := range list {
  211. out = append(out, filepath.Join(root, x))
  212. }
  213. return out
  214. }
  215. // nonBlankLines returns the non-blank lines in text.
  216. func nonBlankLines(text string) []string {
  217. var out []string
  218. for _, s := range lines(text) {
  219. if strings.TrimSpace(s) != "" {
  220. out = append(out, s)
  221. }
  222. }
  223. return out
  224. }
  225. func commandString(command string, args []string) string {
  226. return strings.Join(append([]string{command}, args...), " ")
  227. }
  228. func lines(text string) []string {
  229. out := strings.Split(text, "\n")
  230. // Split will include a "" after the last line. Remove it.
  231. if n := len(out) - 1; n >= 0 && out[n] == "" {
  232. out = out[:n]
  233. }
  234. return out
  235. }
  236. func (c *hookCmd) verbosef(format string, args ...interface{}) {
  237. if c.verbose {
  238. fmt.Fprintf(cmdmain.Stdout, format, args...)
  239. }
  240. }
  241. // cmdOutputDirErr runs the command line in dir, returning its output
  242. // and any error results.
  243. //
  244. // NOTE: cmdOutputDirErr must be used only to run commands that read state,
  245. // not for commands that make changes. Commands that make changes
  246. // should be run using runDirErr so that the -v and -n flags apply to them.
  247. func cmdOutputDirErr(dir, command string, args ...string) (string, error) {
  248. // NOTE: We only show these non-state-modifying commands with -v -v.
  249. // Otherwise things like 'git sync -v' show all our internal "find out about
  250. // the git repo" commands, which is confusing if you are just trying to find
  251. // out what git sync means.
  252. cmd := exec.Command(command, args...)
  253. if dir != "." {
  254. cmd.Dir = dir
  255. }
  256. b, err := cmd.CombinedOutput()
  257. return string(b), err
  258. }