git-lfs/lfs/hook.go
brian m. carlson 86a8d7369c
hooks: mention core.hookspath instead of just .git/hooks
In some cases, users may use `core.hooksPath` to locate their hooks in a
different location.  However, right now, our hook and installation
instructions mention only `.git/hooks`.

We can update the instructions without a problem, but we don't want to
hard-code the hook path into the hooks themselves because of things like
symlinks and moved repository, plus the fact that we can't update a hook
automatically unless it's identical minus some whitespace changes.  To
avoid spuriously failing to update a hook, let's print the right
location with the instructions, and just mention `core.hookspath` and
`.git/hooks` in the message, leaving it to the user to discover.

Note that technically, core.hookspath was not implemented until Git
2.9.0.  However, while we still support older versions of Git, no
presently supported version of Ubuntu or Debian offers a version older
than 2.11, so the risk of practical confusion by mentioning this is low.
Nevertheless, make our tests pass by checking for an old Git explicitly.
2023-01-05 17:08:25 +00:00

174 lines
6.1 KiB
Go

package lfs
import (
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/git-lfs/git-lfs/v3/config"
"github.com/git-lfs/git-lfs/v3/errors"
"github.com/git-lfs/git-lfs/v3/tools"
"github.com/git-lfs/git-lfs/v3/tr"
"github.com/rubyist/tracerx"
)
var (
// The basic hook which just calls 'git lfs TYPE'
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 the '{{Command}}' file in the hooks directory (set by 'core.hookspath'; usually '.git/hooks').\\n\"; exit 2; }\ngit lfs {{Command}} \"$@\""
hookOldContent = "#!/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}} \"$@\""
hookOldContent2 = "#!/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}} \"$@\""
)
// 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 {
Type string
Contents string
Dir string
upgradeables []string
cfg *config.Configuration
}
func LoadHooks(hookDir string, cfg *config.Configuration) []*Hook {
return []*Hook{
NewStandardHook("pre-push", hookDir, []string{
"#!/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 \"$@\"",
hookOldContent,
hookOldContent2,
}, cfg),
NewStandardHook("post-checkout", hookDir, []string{hookOldContent, hookOldContent2}, cfg),
NewStandardHook("post-commit", hookDir, []string{hookOldContent, hookOldContent2}, cfg),
NewStandardHook("post-merge", hookDir, []string{hookOldContent, hookOldContent2}, cfg),
}
}
// NewStandardHook creates a new hook using the template script calling 'git lfs theType'
func NewStandardHook(theType, hookDir string, upgradeables []string, cfg *config.Configuration) *Hook {
formattedUpgradeables := make([]string, 0, len(upgradeables))
for _, s := range upgradeables {
formattedUpgradeables = append(formattedUpgradeables, strings.Replace(s, "{{Command}}", theType, -1))
}
return &Hook{
Type: theType,
Contents: strings.Replace(hookBaseContent, "{{Command}}", theType, -1),
Dir: hookDir,
upgradeables: formattedUpgradeables,
cfg: cfg,
}
}
func (h *Hook) Exists() bool {
_, err := os.Stat(h.Path())
return !os.IsNotExist(err)
}
// Path returns the desired (or actual, if installed) location where this hook
// should be installed. It returns an absolute path in all cases.
func (h *Hook) Path() string {
return filepath.Join(h.Dir, h.Type)
}
// 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 {
msg := fmt.Sprintf("Install hook: %s, force=%t, path=%s", h.Type, force, h.Path())
if err := tools.MkdirAll(h.Dir, h.cfg); err != nil {
return err
}
if h.Exists() && !force {
tracerx.Printf(msg + ", upgrading...")
return h.Upgrade()
}
tracerx.Printf(msg)
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 {
upgradable, match, err := h.matchesCurrent()
if err != nil {
return err
}
if !upgradable || 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 {
msg := fmt.Sprintf("Uninstall hook: %s, path=%s", h.Type, h.Path())
upgradable, _, err := h.matchesCurrent()
if err != nil {
return err
}
if !upgradable {
tracerx.Printf(msg + ", doesn't match...")
return nil
}
tracerx.Printf(msg)
return os.RemoveAll(h.Path())
}
// matchesCurrent returns whether or not an existing git hook is able to be
// written to or upgraded and additionally whether it is identical to the
// current hook. 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, bool, error) {
file, err := os.Open(h.Path())
if err != nil {
return false, false, err
}
by, err := ioutil.ReadAll(io.LimitReader(file, 1024))
file.Close()
if err != nil {
return false, false, err
}
contents := strings.TrimSpace(tools.Undent(string(by)))
if contents == h.Contents {
return true, true, nil
} else if len(contents) == 0 {
return true, false, nil
}
for _, u := range h.upgradeables {
if u == contents {
return true, false, nil
}
}
return false, false, errors.New(fmt.Sprintf("%s\n\n%s\n", tr.Tr.Get("Hook already exists: %s", string(h.Type)), tools.Indent(contents)))
}