lfs: promote Hooks and Filters to types
This commit introduces two new types into the API: Hook, and Filter. Both `Hook` and `Filter` are abstractions built on Git hooks and filters respectively. Each type knows how to install and uninstall itself. These processes are utilized by the setup method in the `lfs` package, and the appropriate calls have been updated in the init and uninit commands. These abstractions were introduced to make adding/removing required filters and hooks easier for future projects, including the migration away from the smudge filter. Eventually it seems appropriate to move both new types into the `git` package, as opposed to the `lfs` package. At the time of writing this commit, there is some coupling against state defined in the `lfs` package (i.e., whether or not we're currently in a git repo, the local working directory, etc). At somepoint it would be nice to remove that coupling and move these new types into the `git` package.
This commit is contained in:
parent
d7aa084533
commit
c7cb8e0303
@ -22,7 +22,7 @@ var (
|
||||
)
|
||||
|
||||
func initCommand(cmd *cobra.Command, args []string) {
|
||||
if err := lfs.InstallFilters(forceInit); err != nil {
|
||||
if err := lfs.SetupFilters(forceInit); err != nil {
|
||||
Error(err.Error())
|
||||
Exit("Run `git lfs init --force` to reset git config.")
|
||||
}
|
||||
|
@ -6,12 +6,14 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
// uninitCmd removes any configuration and hooks set by Git LFS.
|
||||
uninitCmd = &cobra.Command{
|
||||
Use: "uninit",
|
||||
Short: "Clear the Git LFS configuration",
|
||||
Run: uninitCommand,
|
||||
}
|
||||
|
||||
// uninitHooksCmd removes any hooks created by Git LFS.
|
||||
uninitHooksCmd = &cobra.Command{
|
||||
Use: "hooks",
|
||||
Short: "Clear only the Git hooks for the current repository",
|
||||
@ -20,7 +22,7 @@ var (
|
||||
)
|
||||
|
||||
func uninitCommand(cmd *cobra.Command, args []string) {
|
||||
if err := lfs.UninstallFilters(); err != nil {
|
||||
if err := lfs.TeardownFilters(); err != nil {
|
||||
Error(err.Error())
|
||||
}
|
||||
|
||||
|
76
lfs/filters.go
Normal file
76
lfs/filters.go
Normal file
@ -0,0 +1,76 @@
|
||||
package lfs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
|
||||
"github.com/github/git-lfs/git"
|
||||
)
|
||||
|
||||
var (
|
||||
valueRegexp = regexp.MustCompile("\\Agit[\\-\\s]media")
|
||||
)
|
||||
|
||||
// A Filter represents a git-configurable attribute of the type "filter". It
|
||||
// holds a name and a value.
|
||||
type Filter struct {
|
||||
Name string
|
||||
Value string
|
||||
}
|
||||
|
||||
// Install installs the filter in context. It returns any errors it encounters
|
||||
// along the way. If the value for this filter is set, but not equal to the
|
||||
// value that is given, then an exception will be returned. If `force` is
|
||||
// passed as true in that same situtation, the value will be overridden. All
|
||||
// other cases will pass.
|
||||
func (f *Filter) Install(force bool) error {
|
||||
key := fmt.Sprintf("filter.lfs.%s", f.Name)
|
||||
|
||||
currentValue := git.Config.Find(key)
|
||||
if force || f.shouldReset(currentValue) {
|
||||
git.Config.UnsetGlobal(key)
|
||||
git.Config.SetGlobal(key, f.Value)
|
||||
|
||||
return nil
|
||||
} else if currentValue != f.Value {
|
||||
return fmt.Errorf("The %s filter should be \"%s\" but is \"%s\"",
|
||||
f.Name, f.Value, currentValue)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// shouldReset determines whether or not a value is resettable given its current
|
||||
// value on the system. If the value is empty (length = 0), then it will pass.
|
||||
// Otherwise, it will pass if the below regex matches.
|
||||
func (f *Filter) shouldReset(value string) bool {
|
||||
if len(value) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
return valueRegexp.MatchString(value)
|
||||
}
|
||||
|
||||
// The Filters type represents a set of filters to install on the system. It
|
||||
// exists purely for the convenience of being able to stick the Teardown method
|
||||
// somewhere where it makes sense.
|
||||
type Filters []*Filter
|
||||
|
||||
// Setup installs all filters in range of the current Filters instance. It
|
||||
// passes the force argument directly to each of them. If any filter returns an
|
||||
// error, Setup will halt and return that error.
|
||||
func (fs *Filters) Setup(force bool) error {
|
||||
for _, f := range *fs {
|
||||
if err := f.Install(force); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
// Teardown unsets all filters that were installed.
|
||||
func (fs *Filters) Teardown() {
|
||||
git.Config.UnsetGlobalSection("filters.lfs")
|
||||
}
|
147
lfs/hooks.go
Normal file
147
lfs/hooks.go
Normal file
@ -0,0 +1,147 @@
|
||||
package lfs
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// A HookType represents a type of Git hook as defined in
|
||||
// http://git-scm.com/docs/githooks.
|
||||
type HookType string
|
||||
|
||||
const (
|
||||
ApplyPatchMessageHook HookType = "applypatch-msg"
|
||||
PreApplyPatchHook = "pre-applypatch"
|
||||
PostApplyPatchHook = "post-applypatch"
|
||||
PreCommitHook = "pre-commit"
|
||||
PrepareCommitMessageHook = "prepare-commit-msg"
|
||||
CommitMessageHook = "commit-msg"
|
||||
PostCommitHook = "post-commit"
|
||||
PreRebaseHook = "pre-rebase"
|
||||
PostCheckoutHook = "post-checkout"
|
||||
PostMergeHook = "post-merge"
|
||||
PrePushHook = "pre-push"
|
||||
PreReceiveHook = "pre-receive"
|
||||
UpdateHook = "update"
|
||||
PostReceiveHook = "post-receive"
|
||||
PostUpdateHook = "post-update"
|
||||
PushToCheckoutHook = "push-to-checkout"
|
||||
PostRewriteHook = "post-rewrite"
|
||||
RebaseHook = "rebase"
|
||||
)
|
||||
|
||||
// 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 HookType
|
||||
Contents string
|
||||
Upgradeables []string
|
||||
}
|
||||
|
||||
func (h *Hook) Exists() bool {
|
||||
_, err := os.Stat(h.Path())
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// Path returns the desired (or actual, if installed) location where this hook
|
||||
// should be installed, relative to the local Git directory.
|
||||
func (h *Hook) Path() string {
|
||||
return filepath.Join(LocalGitDir, "hooks", string(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 {
|
||||
if !InRepo() {
|
||||
return newInvalidRepoError(nil)
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(filepath.Join(LocalGitDir, "hooks"), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if h.Exists() && !force {
|
||||
return h.Upgrade()
|
||||
}
|
||||
|
||||
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 {
|
||||
if !InRepo() {
|
||||
return newInvalidRepoError(nil)
|
||||
}
|
||||
|
||||
match, err := h.matchesCurrent()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !match {
|
||||
return nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
contents := strings.TrimSpace(string(by))
|
||||
if contents == h.Contents {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
for _, u := range h.Upgradeables {
|
||||
if u == contents {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
193
lfs/setup.go
193
lfs/setup.go
@ -1,159 +1,80 @@
|
||||
package lfs
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/github/git-lfs/git"
|
||||
)
|
||||
|
||||
var (
|
||||
valueRegexp = regexp.MustCompile("\\Agit[\\-\\s]media")
|
||||
// prePushHook invokes `git lfs push` at the pre-push phase.
|
||||
prePushHook = &Hook{
|
||||
Type: PrePushHook,
|
||||
Contents: "#!/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/pre-push.\\n\"; exit 2; }\ngit lfs pre-push \"$@\"",
|
||||
Upgradeables: []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 \"$@\"",
|
||||
},
|
||||
}
|
||||
|
||||
prePushHook = "#!/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/pre-push.\\n\"; exit 2; }\ngit lfs pre-push \"$@\""
|
||||
prePushUpgrades = map[string]bool{
|
||||
"#!/bin/sh\ngit lfs push --stdin $*": true,
|
||||
"#!/bin/sh\ngit lfs push --stdin \"$@\"": true,
|
||||
"#!/bin/sh\ngit lfs pre-push \"$@\"": true,
|
||||
"#!/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 \"$@\"": true,
|
||||
"#!/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 \"$@\"": true,
|
||||
// hooks is a collection of all hooks to be installed by Git LFS.
|
||||
hooks = []*Hook{
|
||||
prePushHook,
|
||||
}
|
||||
|
||||
// cleanFilter invokes `git lfs clean` as the clean filter.
|
||||
cleanFilter = &Filter{Name: "clean", Value: "git lfs clean %%f"}
|
||||
|
||||
// smudgeFilter invokes `git lfs smudge` as the smudge filter.
|
||||
smudgeFilter = &Filter{Name: "smudge", Value: "git lfs smudge %%f"}
|
||||
|
||||
// XXX(@ttaylorr) not sure if this makes sense as a filter? Perhaps a
|
||||
// Settable or Attribute type may be more appropriate.
|
||||
|
||||
requireFilters = &Filter{Name: "required", Value: "true"}
|
||||
|
||||
// filters is a collection of all filters to be installed by Git LFS.
|
||||
filters = Filters{
|
||||
cleanFilter,
|
||||
smudgeFilter,
|
||||
requireFilters,
|
||||
}
|
||||
)
|
||||
|
||||
type HookExists struct {
|
||||
Name string
|
||||
Path string
|
||||
Contents string
|
||||
}
|
||||
|
||||
func (e *HookExists) Error() string {
|
||||
return fmt.Sprintf("Hook already exists: %s\n\n%s\n", e.Name, e.Contents)
|
||||
}
|
||||
|
||||
// InstallHooks installs all hooks in the `hooks` var.
|
||||
func InstallHooks(force bool) error {
|
||||
if !InRepo() {
|
||||
return newInvalidRepoError(nil)
|
||||
for _, h := range hooks {
|
||||
if err := h.Install(force); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(filepath.Join(LocalGitDir, "hooks"), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
hookPath := filepath.Join(LocalGitDir, "hooks", "pre-push")
|
||||
if _, err := os.Stat(hookPath); err == nil && !force {
|
||||
return upgradeHookOrError(hookPath, "pre-push", prePushHook, prePushUpgrades)
|
||||
}
|
||||
|
||||
return ioutil.WriteFile(hookPath, []byte(prePushHook+"\n"), 0755)
|
||||
return nil
|
||||
}
|
||||
|
||||
// UninstallHooks resmoves all hooks in range of the `hooks` var.
|
||||
func UninstallHooks() error {
|
||||
if !InRepo() {
|
||||
return newInvalidRepoError(nil)
|
||||
}
|
||||
|
||||
prePushHookPath := filepath.Join(LocalGitDir, "hooks", "pre-push")
|
||||
file, err := os.Open(prePushHookPath)
|
||||
if err != nil {
|
||||
// hook doesn't exist, our work here is done
|
||||
return nil
|
||||
}
|
||||
|
||||
by, err := ioutil.ReadAll(io.LimitReader(file, 1024))
|
||||
file.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
contents := strings.TrimSpace(string(by))
|
||||
if contents == prePushHook || prePushUpgrades[contents] {
|
||||
return os.RemoveAll(prePushHookPath)
|
||||
for _, h := range hooks {
|
||||
if err := h.Uninstall(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func upgradeHookOrError(hookPath, hookName, hook string, upgrades map[string]bool) error {
|
||||
file, err := os.Open(hookPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
by, err := ioutil.ReadAll(io.LimitReader(file, 1024))
|
||||
file.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
contents := strings.TrimSpace(string(by))
|
||||
if contents == hook {
|
||||
return nil
|
||||
}
|
||||
|
||||
if upgrades[contents] {
|
||||
return ioutil.WriteFile(hookPath, []byte(hook+"\n"), 0755)
|
||||
}
|
||||
|
||||
return &HookExists{hookName, hookPath, contents}
|
||||
}
|
||||
|
||||
func InstallFilters(force bool) error {
|
||||
if err := setFilter("clean", force); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := setFilter("smudge", force); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := requireFilters(force); err != nil {
|
||||
return err
|
||||
}
|
||||
// SetupFilters installs filters necessary for git-lfs to process normal git
|
||||
// operations. Currently, that list includes:
|
||||
// - smudge filter
|
||||
// - clean filter
|
||||
//
|
||||
// An error will be returned if a filter is unable to be set, or if the required
|
||||
// filters were not present.
|
||||
func SetupFilters(force bool) error {
|
||||
filters.Setup(force)
|
||||
return nil
|
||||
}
|
||||
|
||||
func UninstallFilters() error {
|
||||
git.Config.UnsetGlobalSection("filter.lfs")
|
||||
// TeardownFilters proxies into the Teardown method on the Filters type to
|
||||
// remove all installed filters.
|
||||
func TeardownFilters() error {
|
||||
filters.Teardown()
|
||||
return nil
|
||||
}
|
||||
|
||||
func setFilter(filterName string, force bool) error {
|
||||
key := fmt.Sprintf("filter.lfs.%s", filterName)
|
||||
value := fmt.Sprintf("git-lfs %s %%f", filterName)
|
||||
|
||||
existing := git.Config.Find(key)
|
||||
if force || shouldReset(existing) {
|
||||
git.Config.UnsetGlobal(key)
|
||||
git.Config.SetGlobal(key, value)
|
||||
} else if existing != value {
|
||||
return fmt.Errorf("The %s filter should be \"%s\" but is \"%s\"", filterName, value, existing)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func requireFilters(force bool) error {
|
||||
key := "filter.lfs.required"
|
||||
value := "true"
|
||||
|
||||
existing := git.Config.Find(key)
|
||||
if force || shouldReset(existing) {
|
||||
git.Config.UnsetGlobal(key)
|
||||
git.Config.SetGlobal(key, value)
|
||||
} else if existing != value {
|
||||
return errors.New("Git LFS filters should be required but are not.")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func shouldReset(value string) bool {
|
||||
if len(value) == 0 {
|
||||
return true
|
||||
}
|
||||
return valueRegexp.MatchString(value)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user