2015-08-29 04:14:06 +00:00
package lfs
import (
2015-08-31 00:09:28 +00:00
"fmt"
2015-08-29 04:14:06 +00:00
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
2016-05-13 16:38:06 +00:00
2017-07-06 17:43:48 +00:00
"github.com/git-lfs/git-lfs/tools"
2017-07-07 17:34:43 +00:00
"github.com/rubyist/tracerx"
2015-08-29 04:14:06 +00:00
)
2017-01-02 13:02:00 +00:00
var (
// The basic hook which just calls 'git lfs TYPE'
2017-01-11 11:12:31 +00:00
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}} \"$@\""
2017-01-02 13:02:00 +00:00
)
2015-08-29 04:14:06 +00:00
// A Hook represents a githook as described in http://git-scm.com/docs/githooks.
// Hooks have a type, which is the type of hook that they are, and a body, which
// represents the thing they will execute when invoked by Git.
type Hook struct {
2015-09-02 18:57:09 +00:00
Type string
2015-08-29 04:14:06 +00:00
Contents string
2017-10-18 21:51:48 +00:00
Dir string
2017-10-18 21:42:00 +00:00
upgradeables [ ] string
}
2017-10-18 21:51:48 +00:00
func LoadHooks ( hookDir string ) [ ] * Hook {
2017-10-18 21:42:00 +00:00
return [ ] * Hook {
2017-10-18 21:51:48 +00:00
NewStandardHook ( "pre-push" , hookDir , [ ] string {
2017-10-18 21:42:00 +00:00
"#!/bin/sh\ngit lfs push --stdin $*" ,
"#!/bin/sh\ngit lfs push --stdin \"$@\"" ,
"#!/bin/sh\ngit lfs pre-push \"$@\"" ,
"#!/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 \"$@\"" ,
"#!/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 \"$@\"" ,
} ) ,
2017-10-18 21:51:48 +00:00
NewStandardHook ( "post-checkout" , hookDir , [ ] string { } ) ,
NewStandardHook ( "post-commit" , hookDir , [ ] string { } ) ,
NewStandardHook ( "post-merge" , hookDir , [ ] string { } ) ,
2017-10-18 21:42:00 +00:00
}
2015-08-29 04:14:06 +00:00
}
2017-01-02 13:02:00 +00:00
// NewStandardHook creates a new hook using the template script calling 'git lfs theType'
2017-10-18 21:51:48 +00:00
func NewStandardHook ( theType , hookDir string , upgradeables [ ] string ) * Hook {
2017-01-02 13:02:00 +00:00
return & Hook {
Type : theType ,
2017-01-11 11:12:31 +00:00
Contents : strings . Replace ( hookBaseContent , "{{Command}}" , theType , - 1 ) ,
2017-10-18 21:51:48 +00:00
Dir : hookDir ,
2017-10-18 21:42:00 +00:00
upgradeables : upgradeables ,
2017-01-02 13:02:00 +00:00
}
}
2015-08-29 04:14:06 +00:00
func ( h * Hook ) Exists ( ) bool {
_ , err := os . Stat ( h . Path ( ) )
2017-07-07 17:34:43 +00:00
return ! os . IsNotExist ( err )
2015-08-29 04:14:06 +00:00
}
// Path returns the desired (or actual, if installed) location where this hook
lfs/hook: teach `lfs.Hook` about `core.hooksPath`
This commit gives the `lfs.Hook` knowledge of the new `core.hooksPath`
configuration value that was introduce in Git 2.9.0.
When `core.HooksPath` is found in the Git configuration AND is supported (i.e.,
installed Git binary has a version greater than or equal to "2.9.0"), `git-lfs
install` will place new hooks in that directory. If the `core.hooksPath` is
specified, but the installed verison of Git does NOT support it, then it will
be ignored and installed in `.git/hooks` as per usual.
To test this behavior, two new shell tests were added:
- One that runs on Git >= 2.9.0, and tests that `core.hooksPath` is respected
- One that runs on Git < 2.9.0, and tests that `core.hooksPath` is ignored
Unfortunately, our current testing framework does not support skipping
individual tests, only skipping entire files, so two new shell test files were
added.
Resolves github/git-lfs#1407.
2016-08-01 21:47:54 +00:00
// should be installed. It returns an absolute path in all cases.
2015-08-29 04:14:06 +00:00
func ( h * Hook ) Path ( ) string {
2017-10-18 21:51:48 +00:00
return filepath . Join ( h . Dir , h . Type )
2015-08-29 04:14:06 +00:00
}
// Install installs this Git hook on disk, or upgrades it if it does exist, and
// is upgradeable. It will create a hooks directory relative to the local Git
// directory. It returns and halts at any errors, and returns nil if the
// operation was a success.
func ( h * Hook ) Install ( force bool ) error {
2017-07-10 17:24:44 +00:00
msg := fmt . Sprintf ( "Install hook: %s, force=%t, path=%s" , h . Type , force , h . Path ( ) )
2017-07-10 16:52:16 +00:00
2017-10-18 21:51:48 +00:00
if err := os . MkdirAll ( h . Dir , 0755 ) ; err != nil {
2015-08-29 04:14:06 +00:00
return err
}
if h . Exists ( ) && ! force {
2017-07-10 16:52:16 +00:00
tracerx . Printf ( msg + ", upgrading..." )
2015-08-29 04:14:06 +00:00
return h . Upgrade ( )
}
2017-07-10 16:52:16 +00:00
tracerx . Printf ( msg )
2015-08-29 04:14:06 +00:00
return h . write ( )
}
// write writes the contents of this Hook to disk, appending a newline at the
// end, and sets the mode to octal 0755. It writes to disk unconditionally, and
// returns at any error.
func ( h * Hook ) write ( ) error {
return ioutil . WriteFile ( h . Path ( ) , [ ] byte ( h . Contents + "\n" ) , 0755 )
}
// Upgrade upgrades the (assumed to be) existing git hook to the current
// contents. A hook is considered "upgrade-able" if its contents are matched in
// the member variable `Upgradeables`. It halts and returns any errors as they
// arise.
func ( h * Hook ) Upgrade ( ) error {
match , err := h . matchesCurrent ( )
if err != nil {
return err
}
if ! match {
return nil
}
return h . write ( )
}
// Uninstall removes the hook on disk so long as it matches the current version,
// or any of the past versions of this hook.
func ( h * Hook ) Uninstall ( ) error {
2017-07-10 16:52:16 +00:00
msg := fmt . Sprintf ( "Uninstall hook: %s, path=%s" , h . Type , h . Path ( ) )
2015-08-29 04:14:06 +00:00
match , err := h . matchesCurrent ( )
if err != nil {
return err
}
if ! match {
2017-07-10 16:52:16 +00:00
tracerx . Printf ( msg + ", doesn't match..." )
2015-08-29 04:14:06 +00:00
return nil
}
2017-07-10 16:52:16 +00:00
tracerx . Printf ( msg )
2015-08-29 04:14:06 +00:00
return os . RemoveAll ( h . Path ( ) )
}
// matchesCurrent returns whether or not an existing git hook is able to be
// written to or upgraded. A git hook matches those conditions if and only if
// its contents match the current contents, or any past "upgrade-able" contents
// of this hook.
func ( h * Hook ) matchesCurrent ( ) ( bool , error ) {
file , err := os . Open ( h . Path ( ) )
if err != nil {
return false , err
}
by , err := ioutil . ReadAll ( io . LimitReader ( file , 1024 ) )
file . Close ( )
if err != nil {
return false , err
}
2017-07-06 18:59:20 +00:00
contents := strings . TrimSpace ( tools . Undent ( string ( by ) ) )
2016-04-25 13:52:45 +00:00
if contents == h . Contents || len ( contents ) == 0 {
2015-08-29 04:14:06 +00:00
return true , nil
}
2017-10-18 21:42:00 +00:00
for _ , u := range h . upgradeables {
2015-08-29 04:14:06 +00:00
if u == contents {
return true , nil
}
}
2017-07-06 17:43:48 +00:00
return false , fmt . Errorf ( "Hook already exists: %s\n\n%s\n" , string ( h . Type ) , tools . Indent ( contents ) )
2015-08-29 04:14:06 +00:00
}