github.com/git-lfs/git-lfs@v2.5.2+incompatible/lfs/hook.go (about) 1 package lfs 2 3 import ( 4 "fmt" 5 "io" 6 "io/ioutil" 7 "os" 8 "path/filepath" 9 "strings" 10 11 "github.com/git-lfs/git-lfs/tools" 12 "github.com/rubyist/tracerx" 13 ) 14 15 var ( 16 // The basic hook which just calls 'git lfs TYPE' 17 hookBaseContent = "#!/bin/sh\ncommand -v git-lfs >/dev/null 2>&1 || { echo >&2 \"\\nThis repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting .git/hooks/{{Command}}.\\n\"; exit 2; }\ngit lfs {{Command}} \"$@\"" 18 ) 19 20 // A Hook represents a githook as described in http://git-scm.com/docs/githooks. 21 // Hooks have a type, which is the type of hook that they are, and a body, which 22 // represents the thing they will execute when invoked by Git. 23 type Hook struct { 24 Type string 25 Contents string 26 Dir string 27 upgradeables []string 28 } 29 30 func LoadHooks(hookDir string) []*Hook { 31 return []*Hook{ 32 NewStandardHook("pre-push", hookDir, []string{ 33 "#!/bin/sh\ngit lfs push --stdin $*", 34 "#!/bin/sh\ngit lfs push --stdin \"$@\"", 35 "#!/bin/sh\ngit lfs pre-push \"$@\"", 36 "#!/bin/sh\ncommand -v git-lfs >/dev/null 2>&1 || { echo >&2 \"\\nThis repository has been set up with Git LFS but Git LFS is not installed.\\n\"; exit 0; }\ngit lfs pre-push \"$@\"", 37 "#!/bin/sh\ncommand -v git-lfs >/dev/null 2>&1 || { echo >&2 \"\\nThis repository has been set up with Git LFS but Git LFS is not installed.\\n\"; exit 2; }\ngit lfs pre-push \"$@\"", 38 }), 39 NewStandardHook("post-checkout", hookDir, []string{}), 40 NewStandardHook("post-commit", hookDir, []string{}), 41 NewStandardHook("post-merge", hookDir, []string{}), 42 } 43 } 44 45 // NewStandardHook creates a new hook using the template script calling 'git lfs theType' 46 func NewStandardHook(theType, hookDir string, upgradeables []string) *Hook { 47 return &Hook{ 48 Type: theType, 49 Contents: strings.Replace(hookBaseContent, "{{Command}}", theType, -1), 50 Dir: hookDir, 51 upgradeables: upgradeables, 52 } 53 } 54 55 func (h *Hook) Exists() bool { 56 _, err := os.Stat(h.Path()) 57 58 return !os.IsNotExist(err) 59 } 60 61 // Path returns the desired (or actual, if installed) location where this hook 62 // should be installed. It returns an absolute path in all cases. 63 func (h *Hook) Path() string { 64 return filepath.Join(h.Dir, h.Type) 65 } 66 67 // Install installs this Git hook on disk, or upgrades it if it does exist, and 68 // is upgradeable. It will create a hooks directory relative to the local Git 69 // directory. It returns and halts at any errors, and returns nil if the 70 // operation was a success. 71 func (h *Hook) Install(force bool) error { 72 msg := fmt.Sprintf("Install hook: %s, force=%t, path=%s", h.Type, force, h.Path()) 73 74 if err := os.MkdirAll(h.Dir, 0755); err != nil { 75 return err 76 } 77 78 if h.Exists() && !force { 79 tracerx.Printf(msg + ", upgrading...") 80 return h.Upgrade() 81 } 82 83 tracerx.Printf(msg) 84 return h.write() 85 } 86 87 // write writes the contents of this Hook to disk, appending a newline at the 88 // end, and sets the mode to octal 0755. It writes to disk unconditionally, and 89 // returns at any error. 90 func (h *Hook) write() error { 91 return ioutil.WriteFile(h.Path(), []byte(h.Contents+"\n"), 0755) 92 } 93 94 // Upgrade upgrades the (assumed to be) existing git hook to the current 95 // contents. A hook is considered "upgrade-able" if its contents are matched in 96 // the member variable `Upgradeables`. It halts and returns any errors as they 97 // arise. 98 func (h *Hook) Upgrade() error { 99 match, err := h.matchesCurrent() 100 if err != nil { 101 return err 102 } 103 104 if !match { 105 return nil 106 } 107 108 return h.write() 109 } 110 111 // Uninstall removes the hook on disk so long as it matches the current version, 112 // or any of the past versions of this hook. 113 func (h *Hook) Uninstall() error { 114 msg := fmt.Sprintf("Uninstall hook: %s, path=%s", h.Type, h.Path()) 115 116 match, err := h.matchesCurrent() 117 if err != nil { 118 return err 119 } 120 121 if !match { 122 tracerx.Printf(msg + ", doesn't match...") 123 return nil 124 } 125 126 tracerx.Printf(msg) 127 return os.RemoveAll(h.Path()) 128 } 129 130 // matchesCurrent returns whether or not an existing git hook is able to be 131 // written to or upgraded. A git hook matches those conditions if and only if 132 // its contents match the current contents, or any past "upgrade-able" contents 133 // of this hook. 134 func (h *Hook) matchesCurrent() (bool, error) { 135 file, err := os.Open(h.Path()) 136 if err != nil { 137 return false, err 138 } 139 140 by, err := ioutil.ReadAll(io.LimitReader(file, 1024)) 141 file.Close() 142 if err != nil { 143 return false, err 144 } 145 146 contents := strings.TrimSpace(tools.Undent(string(by))) 147 if contents == h.Contents || len(contents) == 0 { 148 return true, nil 149 } 150 151 for _, u := range h.upgradeables { 152 if u == contents { 153 return true, nil 154 } 155 } 156 157 return false, fmt.Errorf("Hook already exists: %s\n\n%s\n", string(h.Type), tools.Indent(contents)) 158 }