Skip to content

Commit

Permalink
feature: Store lefthook checksum in non-hook file (#280)
Browse files Browse the repository at this point in the history
* feature: Store lefthook checksum in non-hook file

Signed-off-by: Valentin Kiselev <mrexox@evilmartians.com>

* chore: Remove redundant code

Signed-off-by: Valentin Kiselev <mrexox@evilmartians.com>

* feature: Store commit hash in a version

Signed-off-by: Valentin Kiselev <mrexox@evilmartians.com>
  • Loading branch information
mrexox authored Jul 25, 2022
1 parent 66469a5 commit 42423cd
Show file tree
Hide file tree
Showing 11 changed files with 226 additions and 72 deletions.
4 changes: 3 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
COMMIT_HASH = $(shell git rev-parse HEAD)

build:
go build -o lefthook
go build -ldflags "-X github.com/evilmartians/lefthook/internal/version.commit=$(COMMIT_HASH)" -o lefthook

test:
go test -cpu 24 -race -count=1 -timeout=30s ./...
Expand Down
9 changes: 8 additions & 1 deletion cmd/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,20 @@ import (
)

func newVersionCmd(opts *lefthook.Options) *cobra.Command {
var verbose bool

versionCmd := cobra.Command{
Use: "version",
Short: "Show lefthook version",
Run: func(cmd *cobra.Command, args []string) {
log.Println(version.Version)
log.Println(version.Version(verbose))
},
}

versionCmd.Flags().BoolVarP(
&verbose, "full", "f", false,
"full version with commit hash",
)

return &versionCmd
}
7 changes: 5 additions & 2 deletions internal/config/available_hooks.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package config

// ChecksumHookName - git hook, which is used just to store the current config checksum version.
const ChecksumHookName = "prepare-commit-msg"
// ChecksumFileName - the file, which is used just to store the current config checksum version.
const ChecksumFileName = "lefthook.checksum"

// GhostHookName - the hook which logs are not shown and which is used for synchronizing hooks.
const GhostHookName = "prepare-commit-msg"

// AvailableHooks - list of hooks taken from https://git-scm.com/docs/githooks.
var AvailableHooks = [...]string{
Expand Down
11 changes: 11 additions & 0 deletions internal/git/repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
const (
cmdRootPath = "git rev-parse --show-toplevel"
cmdHooksPath = "git rev-parse --git-path hooks"
cmdInfoPath = "git rev-parse --git-path info"
cmdGitPath = "git rev-parse --git-dir"
cmdStagedFiles = "git diff --name-only --cached"
cmdAllFiles = "git ls-files --cached"
Expand All @@ -25,6 +26,7 @@ type Repository struct {
HooksPath string
RootPath string
GitPath string
InfoPath string
}

// NewRepository returns a Repository or an error, if git repository it not initialized.
Expand All @@ -42,6 +44,14 @@ func NewRepository(fs afero.Fs) (*Repository, error) {
hooksPath = filepath.Join(rootPath, hooksPath)
}

infoPath, err := execGit(cmdInfoPath)
if err != nil {
return nil, err
}
if exists, _ := afero.DirExists(fs, filepath.Join(rootPath, infoPath)); exists {
infoPath = filepath.Join(rootPath, infoPath)
}

gitPath, err := execGit(cmdGitPath)
if err != nil {
return nil, err
Expand All @@ -55,6 +65,7 @@ func NewRepository(fs afero.Fs) (*Repository, error) {
HooksPath: hooksPath,
RootPath: rootPath,
GitPath: gitPath,
InfoPath: infoPath,
}, nil
}

Expand Down
2 changes: 1 addition & 1 deletion internal/lefthook/add.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ func (l *Lefthook) Add(args *AddArgs) error {
return err
}

err = l.addHook(args.Hook, "")
err = l.addHook(args.Hook)
if err != nil {
return err
}
Expand Down
100 changes: 78 additions & 22 deletions internal/lefthook/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"io"
"path/filepath"
"regexp"
"strconv"
"strings"

"github.com/spf13/afero"
Expand All @@ -18,11 +19,14 @@ import (

const (
configFileMode = 0o666
checksumFileMode = 0o644
configDefaultName = "lefthook.yml"
configGlob = "lefthook.y*ml"
timestampBase = 10
timestampBitsize = 64
)

var lefthookChecksumRegexp = regexp.MustCompile(`(?:#\s*lefthook_version:\s+)(\w+)`)
var lefthookChecksumRegexp = regexp.MustCompile(`(\w+)\s+(\d+)`)

type InstallArgs struct {
Force, Aggressive bool
Expand All @@ -44,7 +48,7 @@ func (l *Lefthook) Install(args *InstallArgs) error {
return err
}

return l.createHooks(cfg,
return l.createHooksIfNeeded(cfg,
args.Force || args.Aggressive || l.Options.Force || l.Options.Aggressive)
}

Expand Down Expand Up @@ -90,7 +94,7 @@ func (l *Lefthook) createConfig(path string) error {
return nil
}

func (l *Lefthook) createHooks(cfg *config.Config, force bool) error {
func (l *Lefthook) createHooksIfNeeded(cfg *config.Config, force bool) error {
if !force && l.hooksSynchronized() {
return nil
}
Expand All @@ -106,38 +110,33 @@ func (l *Lefthook) createHooks(cfg *config.Config, force bool) error {
for hook := range cfg.Hooks {
hookNames = append(hookNames, hook)

err = l.cleanHook(hook, force)
if err != nil {
if err = l.cleanHook(hook, force); err != nil {
return err
}

err = l.addHook(hook, checksum)
if err != nil {
if err = l.addHook(hook); err != nil {
return err
}
}

// Add an informational hook to use for checksum comparation.
err = l.addHook(config.ChecksumHookName, checksum)
if err != nil {
if err = l.addHook(config.GhostHookName); err != nil {
return nil
}

if err = l.addChecksumFile(checksum); err != nil {
return err
}

hookNames = append(hookNames, config.ChecksumHookName)
log.Info(log.Cyan("SERVED HOOKS:"), log.Bold(strings.Join(hookNames, ", ")))
if len(hookNames) > 0 {
log.Info(log.Cyan("SERVED HOOKS:"), log.Bold(strings.Join(hookNames, ", ")))
}

return nil
}

func (l *Lefthook) hooksSynchronized() bool {
checksum, err := l.configChecksum()
if err != nil {
return false
}

// Check checksum in a checksum file
hookFullPath := filepath.Join(l.repo.HooksPath, config.ChecksumHookName)
file, err := l.Fs.Open(hookFullPath)
file, err := l.Fs.Open(l.checksumFilePath())
if err != nil {
return false
}
Expand All @@ -146,14 +145,56 @@ func (l *Lefthook) hooksSynchronized() bool {
scanner := bufio.NewScanner(file)
scanner.Split(bufio.ScanLines)

var storedChecksum string
var storedTimestamp int64

for scanner.Scan() {
match := lefthookChecksumRegexp.FindStringSubmatch(scanner.Text())
if len(match) > 1 && match[1] == checksum {
return true
if len(match) > 1 {
storedChecksum = match[1]
storedTimestamp, err = strconv.ParseInt(match[2], timestampBase, timestampBitsize)
if err != nil {
return false
}

break
}
}

return false
if len(storedChecksum) == 0 {
return false
}

configTimestamp, err := l.configLastUpdateTimestamp()
if err != nil {
return false
}

if storedTimestamp == configTimestamp {
return true
}

configChecksum, err := l.configChecksum()
if err != nil {
return false
}

return storedChecksum == configChecksum
}

func (l *Lefthook) configLastUpdateTimestamp() (timestamp int64, err error) {
m, err := afero.Glob(l.Fs, filepath.Join(l.repo.RootPath, configGlob))
if err != nil {
return
}

info, err := l.Fs.Stat(m[0])
if err != nil {
return
}

timestamp = info.ModTime().Unix()
return
}

func (l *Lefthook) configChecksum() (checksum string, err error) {
Expand All @@ -177,3 +218,18 @@ func (l *Lefthook) configChecksum() (checksum string, err error) {
checksum = hex.EncodeToString(hash.Sum(nil)[:16])
return
}

func (l *Lefthook) addChecksumFile(checksum string) error {
timestamp, err := l.configLastUpdateTimestamp()
if err != nil {
return err
}

return afero.WriteFile(
l.Fs, l.checksumFilePath(), templates.Checksum(checksum, timestamp), checksumFileMode,
)
}

func (l *Lefthook) checksumFilePath() string {
return filepath.Join(l.repo.InfoPath, config.ChecksumFileName)
}
Loading

0 comments on commit 42423cd

Please sign in to comment.