github.com/psexton/git-lfs@v2.1.1-0.20170517224304-289a18b2bc53+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/config"
    12  	"github.com/git-lfs/git-lfs/errors"
    13  	"github.com/git-lfs/git-lfs/git"
    14  )
    15  
    16  var (
    17  	// The basic hook which just calls 'git lfs TYPE'
    18  	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}} \"$@\""
    19  )
    20  
    21  // A Hook represents a githook as described in http://git-scm.com/docs/githooks.
    22  // Hooks have a type, which is the type of hook that they are, and a body, which
    23  // represents the thing they will execute when invoked by Git.
    24  type Hook struct {
    25  	Type         string
    26  	Contents     string
    27  	Upgradeables []string
    28  }
    29  
    30  // NewStandardHook creates a new hook using the template script calling 'git lfs theType'
    31  func NewStandardHook(theType string, upgradeables []string) *Hook {
    32  	return &Hook{
    33  		Type:         theType,
    34  		Contents:     strings.Replace(hookBaseContent, "{{Command}}", theType, -1),
    35  		Upgradeables: upgradeables,
    36  	}
    37  }
    38  
    39  func (h *Hook) Exists() bool {
    40  	_, err := os.Stat(h.Path())
    41  	return err == nil
    42  }
    43  
    44  // Path returns the desired (or actual, if installed) location where this hook
    45  // should be installed. It returns an absolute path in all cases.
    46  func (h *Hook) Path() string {
    47  	return filepath.Join(h.Dir(), h.Type)
    48  }
    49  
    50  // Dir returns the directory used by LFS for storing Git hooks. By default, it
    51  // will return the hooks/ sub-directory of the local repository's .git
    52  // directory. If `core.hooksPath` is configured and supported (Git verison is
    53  // greater than "2.9.0"), it will return that instead.
    54  func (h *Hook) Dir() string {
    55  	customHooksSupported := git.Config.IsGitVersionAtLeast("2.9.0")
    56  	if hp, ok := config.Config.Git.Get("core.hooksPath"); ok && customHooksSupported {
    57  		return hp
    58  	}
    59  
    60  	return filepath.Join(config.LocalGitDir, "hooks")
    61  }
    62  
    63  // Install installs this Git hook on disk, or upgrades it if it does exist, and
    64  // is upgradeable. It will create a hooks directory relative to the local Git
    65  // directory. It returns and halts at any errors, and returns nil if the
    66  // operation was a success.
    67  func (h *Hook) Install(force bool) error {
    68  	if err := os.MkdirAll(h.Dir(), 0755); err != nil {
    69  		return err
    70  	}
    71  
    72  	if h.Exists() && !force {
    73  		return h.Upgrade()
    74  	}
    75  
    76  	return h.write()
    77  }
    78  
    79  // write writes the contents of this Hook to disk, appending a newline at the
    80  // end, and sets the mode to octal 0755. It writes to disk unconditionally, and
    81  // returns at any error.
    82  func (h *Hook) write() error {
    83  	return ioutil.WriteFile(h.Path(), []byte(h.Contents+"\n"), 0755)
    84  }
    85  
    86  // Upgrade upgrades the (assumed to be) existing git hook to the current
    87  // contents. A hook is considered "upgrade-able" if its contents are matched in
    88  // the member variable `Upgradeables`. It halts and returns any errors as they
    89  // arise.
    90  func (h *Hook) Upgrade() error {
    91  	match, err := h.matchesCurrent()
    92  	if err != nil {
    93  		return err
    94  	}
    95  
    96  	if !match {
    97  		return nil
    98  	}
    99  
   100  	return h.write()
   101  }
   102  
   103  // Uninstall removes the hook on disk so long as it matches the current version,
   104  // or any of the past versions of this hook.
   105  func (h *Hook) Uninstall() error {
   106  	if !InRepo() {
   107  		return errors.New("Not in a git repository")
   108  	}
   109  
   110  	match, err := h.matchesCurrent()
   111  	if err != nil {
   112  		return err
   113  	}
   114  
   115  	if !match {
   116  		return nil
   117  	}
   118  
   119  	return os.RemoveAll(h.Path())
   120  }
   121  
   122  // matchesCurrent returns whether or not an existing git hook is able to be
   123  // written to or upgraded. A git hook matches those conditions if and only if
   124  // its contents match the current contents, or any past "upgrade-able" contents
   125  // of this hook.
   126  func (h *Hook) matchesCurrent() (bool, error) {
   127  	file, err := os.Open(h.Path())
   128  	if err != nil {
   129  		return false, err
   130  	}
   131  
   132  	by, err := ioutil.ReadAll(io.LimitReader(file, 1024))
   133  	file.Close()
   134  	if err != nil {
   135  		return false, err
   136  	}
   137  
   138  	contents := strings.TrimSpace(string(by))
   139  	if contents == h.Contents || len(contents) == 0 {
   140  		return true, nil
   141  	}
   142  
   143  	for _, u := range h.Upgradeables {
   144  		if u == contents {
   145  			return true, nil
   146  		}
   147  	}
   148  
   149  	return false, fmt.Errorf("Hook already exists: %s\n\n%s\n", string(h.Type), contents)
   150  }