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  }