Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: evilmartians/lefthook
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v1.7.5
Choose a base ref
...
head repository: evilmartians/lefthook
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: v1.7.6
Choose a head ref
  • 3 commits
  • 32 files changed
  • 1 contributor

Commits on Jul 23, 2024

  1. feat: add self-update command (#778)

    * feat: add auto updater
    
    * fix: query latest release
    
    * feat: implement the upgrader
    
    * fix: move upgrader to a specific folder
    
    * ci: adjust CI to not include upgrader in npm and rubygems packages
    
    * chore: add package description
    
    * chore: add colors
    
    * chore: simplify the look and feel of commands
    
    * chore: small cosmetic changes
    
    * chore: add backup recover on issues with file system
    
    * test: add tests
    
    * fix: special fix for windows
    mrexox authored Jul 23, 2024

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    1f055b2 View commit details

Commits on Jul 24, 2024

  1. 1.7.6: adding self-update for non-packaged binaries

    mrexox committed Jul 24, 2024

    Verified

    This commit was signed with the committer’s verified signature.
    mrexox Valentin Kiselev
    Copy the full SHA
    5646145 View commit details
  2. ci: rpm/deb without self-update

    mrexox committed Jul 24, 2024

    Verified

    This commit was signed with the committer’s verified signature.
    mrexox Valentin Kiselev
    Copy the full SHA
    efa67f5 View commit details
94 changes: 66 additions & 28 deletions .goreleaser.yml
Original file line number Diff line number Diff line change
@@ -3,29 +3,59 @@ before:
hooks:
- go generate ./...
builds:
- env:
- CGO_ENABLED=0
goos:
- linux
- darwin
- windows
- freebsd
goarch:
- amd64
- arm64
- 386
ignore:
- goos: darwin
goarch: 386
- goos: linux
goarch: 386
- goos: freebsd
goarch: 386
ldflags:
- -s -w -X github.com/evilmartians/lefthook/internal/version.commit={{.Commit}}
# Builds the binaries without `lefthook upgrade`
- id: no_self_update
tags:
- no_self_update
env:
- GCO_ENABLED=0
goos:
- linux
- darwin
- windows
- freebsd
goarch:
- amd64
- arm64
- 386
ignore:
- goos: darwin
goarch: 386
- goos: linux
goarch: 386
- goos: freebsd
goarch: 386
ldflags:
- -s -w -X github.com/evilmartians/lefthook/internal/version.commit={{.Commit}}

# Full lefthook binary
- id: lefthook
env:
- CGO_ENABLED=0
goos:
- linux
- darwin
- windows
- freebsd
goarch:
- amd64
- arm64
- 386
ignore:
- goos: darwin
goarch: 386
- goos: linux
goarch: 386
- goos: freebsd
goarch: 386
ldflags:
- -s -w -X github.com/evilmartians/lefthook/internal/version.commit={{.Commit}}

archives:
- id: lefthook
format: binary
builds:
- lefthook
files:
- none*
name_template: >-
@@ -36,8 +66,11 @@ archives:
{{- if eq .Arch "amd64" }}x86_64
{{- else if eq .Arch "386" }}i386
{{- else }}{{ .Arch }}{{ end }}
- id: lefthook-gz
format: gz
builds:
- lefthook
files:
- none*
name_template: >-
@@ -48,10 +81,13 @@ archives:
{{- if eq .Arch "amd64" }}x86_64
{{- else if eq .Arch "386" }}i386
{{- else }}{{ .Arch }}{{ end }}
checksum:
name_template: '{{ .ProjectName }}_checksums.txt'

snapshot:
name_template: "{{ .Tag }}"

changelog:
sort: asc
filters:
@@ -64,25 +100,27 @@ changelog:
- '^\d+\.\d+\.\d+:'

snapcrafts:
-
summary: Fast and powerful Git hooks manager for any type of projects.
- summary: Fast and powerful Git hooks manager for any type of projects.
description: |
Lefthook is a single dependency-free binary to manage all your git hooks. It works with any language in any environment, and in all common team workflows.
grade: stable
confinement: classic
publish: true
license: MIT
builds:
- no_self_update

nfpms:
-
file_name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}'
- file_name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}'
homepage: https://github.com/evilmartians/lefthook
description: Lefthook a single dependency-free binary to manage all your git hooks that works with any language in any environment, and in all common team workflows
maintainer: Alexander Abroskin <arkweid@evilmartians.com>
maintainer: Evil Martians <lefthook@evilmartians.com>
license: MIT
vendor: Evil Martians
builds:
- no_self_update
formats:
- deb
- rpm
- deb
- rpm
dependencies:
- git
- git
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -2,6 +2,10 @@

## master (unreleased)

## 1.7.6 (2024-07-24)

- feat: add self-update command ([#778](https://github.com/evilmartians/lefthook/pull/778)) by @mrexox

## 1.7.5 (2024-07-22)

- feat: use glob in exclude array ([#777](https://github.com/evilmartians/lefthook/pull/777)) by @mrexox
4 changes: 3 additions & 1 deletion cmd/add.go
Original file line number Diff line number Diff line change
@@ -12,7 +12,9 @@ import (
//go:embed add-doc.txt
var addDoc string

func newAddCmd(opts *lefthook.Options) *cobra.Command {
type add struct{}

func (add) New(opts *lefthook.Options) *cobra.Command {
args := lefthook.AddArgs{}

addHookCompletions := func(cmd *cobra.Command, args []string, toComplete string) (ret []string, compDir cobra.ShellCompDirective) {
23 changes: 23 additions & 0 deletions cmd/commands.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//go:build !no_self_update

package cmd

import (
"github.com/spf13/cobra"

"github.com/evilmartians/lefthook/internal/lefthook"
)

type command interface {
New(*lefthook.Options) *cobra.Command
}

var commands = [...]command{
version{},
add{},
install{},
uninstall{},
run{},
dump{},
selfUpdate{},
}
22 changes: 22 additions & 0 deletions cmd/commands_no_self_update.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//go:build no_self_update

package cmd

import (
"github.com/spf13/cobra"

"github.com/evilmartians/lefthook/internal/lefthook"
)

type command interface {
New(*lefthook.Options) *cobra.Command
}

var commands = [...]command{
version{},
add{},
install{},
uninstall{},
run{},
dump{},
}
4 changes: 3 additions & 1 deletion cmd/dump.go
Original file line number Diff line number Diff line change
@@ -6,7 +6,9 @@ import (
"github.com/evilmartians/lefthook/internal/lefthook"
)

func newDumpCmd(opts *lefthook.Options) *cobra.Command {
type dump struct{}

func (dump) New(opts *lefthook.Options) *cobra.Command {
dumpArgs := lefthook.DumpArgs{}
dumpCmd := cobra.Command{
Use: "dump",
4 changes: 3 additions & 1 deletion cmd/install.go
Original file line number Diff line number Diff line change
@@ -7,7 +7,9 @@ import (
"github.com/evilmartians/lefthook/internal/log"
)

func newInstallCmd(opts *lefthook.Options) *cobra.Command {
type install struct{}

func (install) New(opts *lefthook.Options) *cobra.Command {
var a, force bool

installCmd := cobra.Command{
11 changes: 1 addition & 10 deletions cmd/root.go
Original file line number Diff line number Diff line change
@@ -9,15 +9,6 @@ import (
"github.com/evilmartians/lefthook/internal/log"
)

var commands = [...]func(*lefthook.Options) *cobra.Command{
newVersionCmd,
newAddCmd,
newInstallCmd,
newUninstallCmd,
newRunCmd,
newDumpCmd,
}

func newRootCmd() *cobra.Command {
options := lefthook.Options{
Fs: afero.NewOsFs(),
@@ -61,7 +52,7 @@ func newRootCmd() *cobra.Command {
}

for _, subcommand := range commands {
rootCmd.AddCommand(subcommand(&options))
rootCmd.AddCommand(subcommand.New(&options))
}

return rootCmd
4 changes: 3 additions & 1 deletion cmd/run.go
Original file line number Diff line number Diff line change
@@ -8,7 +8,9 @@ import (
"github.com/evilmartians/lefthook/internal/log"
)

func newRunCmd(opts *lefthook.Options) *cobra.Command {
type run struct{}

func (run) New(opts *lefthook.Options) *cobra.Command {
runArgs := lefthook.RunArgs{}

runHookCompletions := func(cmd *cobra.Command, args []string, toComplete string) (ret []string, compDir cobra.ShellCompDirective) {
73 changes: 73 additions & 0 deletions cmd/self_update.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package cmd

import (
"context"
"fmt"
"os"
"os/signal"
"syscall"

"github.com/spf13/cobra"

"github.com/evilmartians/lefthook/internal/lefthook"
"github.com/evilmartians/lefthook/internal/log"
"github.com/evilmartians/lefthook/internal/updater"
)

type selfUpdate struct{}

func (selfUpdate) New(opts *lefthook.Options) *cobra.Command {
var yes bool
upgradeCmd := cobra.Command{
Use: "self-update",
Short: "Update lefthook executable",
Example: "lefthook self-update",
ValidArgsFunction: cobra.NoFileCompletions,
Args: cobra.NoArgs,
RunE: func(_cmd *cobra.Command, _args []string) error {
return update(opts, yes)
},
}

upgradeCmd.Flags().BoolVarP(&yes, "yes", "y", false, "no prompt")
upgradeCmd.Flags().BoolVarP(&opts.Force, "force", "f", false, "force upgrade")
upgradeCmd.Flags().BoolVarP(&opts.Verbose, "verbose", "v", false, "show verbose logs")

return &upgradeCmd
}

func update(opts *lefthook.Options, yes bool) error {
if os.Getenv(lefthook.EnvVerbose) == "1" || os.Getenv(lefthook.EnvVerbose) == "true" {
opts.Verbose = true
}
if opts.Verbose {
log.SetLevel(log.DebugLevel)
log.Debug("Verbose mode enabled")
}

ctx, cancel := context.WithCancel(context.Background())
defer cancel()

// Handle interrupts
signalChan := make(chan os.Signal, 1)
signal.Notify(
signalChan,
syscall.SIGINT,
syscall.SIGTERM,
)
go func() {
<-signalChan
cancel()
}()

exePath, err := os.Executable()
if err != nil {
return fmt.Errorf("failed to determine the binary path: %w", err)
}

return updater.New().SelfUpdate(ctx, updater.Options{
Yes: yes,
Force: opts.Force,
ExePath: exePath,
})
}
4 changes: 3 additions & 1 deletion cmd/uninstall.go
Original file line number Diff line number Diff line change
@@ -6,7 +6,9 @@ import (
"github.com/evilmartians/lefthook/internal/lefthook"
)

func newUninstallCmd(opts *lefthook.Options) *cobra.Command {
type uninstall struct{}

func (uninstall) New(opts *lefthook.Options) *cobra.Command {
args := lefthook.UninstallArgs{}

uninstallCmd := cobra.Command{
8 changes: 5 additions & 3 deletions cmd/version.go
Original file line number Diff line number Diff line change
@@ -5,10 +5,12 @@ import (

"github.com/evilmartians/lefthook/internal/lefthook"
"github.com/evilmartians/lefthook/internal/log"
"github.com/evilmartians/lefthook/internal/version"
ver "github.com/evilmartians/lefthook/internal/version"
)

func newVersionCmd(_opts *lefthook.Options) *cobra.Command {
type version struct{}

func (version) New(_opts *lefthook.Options) *cobra.Command {
var verbose bool

versionCmd := cobra.Command{
@@ -17,7 +19,7 @@ func newVersionCmd(_opts *lefthook.Options) *cobra.Command {
ValidArgsFunction: cobra.NoFileCompletions,
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) {
log.Println(version.Version(verbose))
log.Println(ver.Version(verbose))
},
}

2 changes: 1 addition & 1 deletion docs/install.md
Original file line number Diff line number Diff line change
@@ -81,7 +81,7 @@ You can find the Swift wrapper plugin [here](https://github.com/csjones/lefthook
Utilize lefthook in your Swift project using Swift Package Manager:

```swift
.package(url: "https://github.com/csjones/lefthook-plugin.git", exact: "1.7.5"),
.package(url: "https://github.com/csjones/lefthook-plugin.git", exact: "1.7.6"),
```

Or, with [mint](https://github.com/yonaskolb/Mint):
9 changes: 7 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
@@ -14,15 +14,20 @@ require (
github.com/mattn/go-tty v0.0.5
github.com/mitchellh/mapstructure v1.5.0
github.com/rogpeppe/go-internal v1.12.0
github.com/schollz/progressbar/v3 v3.14.4
github.com/spf13/afero v1.11.0
github.com/spf13/cobra v1.8.0
github.com/spf13/viper v1.19.0
github.com/stretchr/testify v1.9.0
gopkg.in/alessio/shellescape.v1 v1.0.0-20170105083845-52074bc9df61
)

require (
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/x/ansi v0.1.1 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
@@ -49,8 +54,8 @@ require (
github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
golang.org/x/sys v0.19.0 // indirect
golang.org/x/term v0.15.0 // indirect
golang.org/x/sys v0.22.0 // indirect
golang.org/x/term v0.22.0 // indirect
golang.org/x/text v0.14.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1
15 changes: 11 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
@@ -31,6 +31,7 @@ github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -52,6 +53,8 @@ github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZ
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-tty v0.0.5 h1:s09uXI7yDbXzzTTfw3zonKFzwGkyYlgU3OMjqA0ddz4=
github.com/mattn/go-tty v0.0.5/go.mod h1:u5GGXBtZU6RQoKV8gY5W6UhMudbR5vXnUe7j3pxse28=
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ=
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
@@ -71,6 +74,8 @@ github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6ke
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
github.com/schollz/progressbar/v3 v3.14.4 h1:W9ZrDSJk7eqmQhd3uxFNNcTr0QL+xuGNI9dEMrw0r74=
github.com/schollz/progressbar/v3 v3.14.4/go.mod h1:aT3UQ7yGm+2ZjeXPqsjTenwL3ddUiuZ0kfQ/2tHlyNI=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
@@ -106,10 +111,12 @@ golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk=
golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ=
4 changes: 2 additions & 2 deletions internal/lefthook/lefthook.go
Original file line number Diff line number Diff line change
@@ -16,8 +16,8 @@ import (
)

const (
EnvVerbose = "LEFTHOOK_VERBOSE" // keep all output
hookFileMode = 0o755
envVerbose = "LEFTHOOK_VERBOSE" // keep all output
oldHookPostfix = ".old"
)

@@ -41,7 +41,7 @@ type Lefthook struct {

// New returns an instance of Lefthook.
func initialize(opts *Options) (*Lefthook, error) {
if os.Getenv(envVerbose) == "1" || os.Getenv(envVerbose) == "true" {
if os.Getenv(EnvVerbose) == "1" || os.Getenv(EnvVerbose) == "true" {
opts.Verbose = true
}

278 changes: 278 additions & 0 deletions internal/updater/updater.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,278 @@
// Package updater contains the self-update implementation for the lefthook executable.
package updater

import (
"bufio"
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"runtime"
"strings"
"time"

"github.com/schollz/progressbar/v3"

"github.com/evilmartians/lefthook/internal/log"
"github.com/evilmartians/lefthook/internal/version"
)

const (
timeout = 10 * time.Second
latestReleaseURL = "https://api.github.com/repos/evilmartians/lefthook/releases/latest"
checksumsFilename = "lefthook_checksums.txt"
checksumFields = 2
modExecutable os.FileMode = 0o755
)

var (
errNoAsset = errors.New("Couldn't find an asset to download. Please submit an issue to https://github.com/evilmartians/lefthook")
errInvalidHashsum = errors.New("SHA256 sums differ, it's not safe to use the downloaded binary.\nIf you have problems upgrading lefthook please submit an issue to https://github.com/evilmartians/lefthook")
errUpdateFailed = errors.New("Update failed")

osNames = map[string]string{
"windows": "Windows",
"darwin": "MacOS",
"linux": "Linux",
"freebsd": "Freebsd",
}

archNames = map[string]string{
"amd64": "x86_64",
"arm64": "arm64",
"386": "i386",
}
)

type release struct {
TagName string `json:"tag_name"`
Assets []asset
}

type asset struct {
Name string `json:"name"`
DownloadURL string `json:"browser_download_url"`
}

type Options struct {
Yes bool
Force bool
ExePath string
}

type Updater struct {
client *http.Client
releaseURL string
}

func New() *Updater {
return &Updater{
client: &http.Client{Timeout: timeout},
releaseURL: latestReleaseURL,
}
}

func (u *Updater) SelfUpdate(ctx context.Context, opts Options) error {
rel, ferr := u.fetchLatestRelease(ctx)
if ferr != nil {
return fmt.Errorf("latest release fetch failed: %w", ferr)
}

latestVersion := strings.TrimPrefix(rel.TagName, "v")

if latestVersion == version.Version(false) && !opts.Force {
log.Infof("Up to date: %s\n", latestVersion)
return nil
}

wantedAsset := fmt.Sprintf("lefthook_%s_%s_%s", latestVersion, osNames[runtime.GOOS], archNames[runtime.GOARCH])
if runtime.GOOS == "windows" {
wantedAsset += ".exe"
}

log.Debugf("Searching assets for %s", wantedAsset)

var downloadURL string
var checksumURL string
for i := range rel.Assets {
asset := rel.Assets[i]
if len(downloadURL) == 0 && asset.Name == wantedAsset {
downloadURL = asset.DownloadURL
if len(checksumURL) > 0 {
break
}
}

if len(checksumURL) == 0 && asset.Name == checksumsFilename {
checksumURL = asset.DownloadURL
if len(downloadURL) > 0 {
break
}
}
}

if len(downloadURL) == 0 {
log.Warnf("Couldn't find the right asset to download. Wanted: %s\n", wantedAsset)
return errNoAsset
}

if len(checksumURL) == 0 {
log.Warn("Couldn't find checksums")
}

if !opts.Yes {
log.Infof("Update %s to %s? %s ", log.Cyan("lefthook"), log.Yellow(latestVersion), log.Gray("[Y/n]"))
scanner := bufio.NewScanner(os.Stdin)
scanner.Scan()
ans := scanner.Text()

if len(ans) > 0 && ans[0] != 'y' && ans[0] != 'Y' {
log.Debug("Update rejected")
return nil
}
}

lefthookExePath := opts.ExePath
if realPath, serr := filepath.EvalSymlinks(lefthookExePath); serr == nil {
lefthookExePath = realPath
}

destPath := lefthookExePath + "." + latestVersion
defer os.Remove(destPath)

ok, err := u.download(ctx, wantedAsset, downloadURL, checksumURL, destPath)
if err != nil {
return err
}
if !ok {
return errInvalidHashsum
}

backupPath := lefthookExePath + ".bak"
defer os.Remove(backupPath)

log.Debugf("mv %s %s", lefthookExePath, backupPath)
if err = os.Rename(lefthookExePath, backupPath); err != nil {
return fmt.Errorf("failed to backup lefthook executable: %w", err)
}

log.Debugf("mv %s %s", destPath, lefthookExePath)
err = os.Rename(destPath, lefthookExePath)
if err != nil {
log.Errorf("Failed to replace the lefthook executable: %s", err)
if err = os.Rename(backupPath, lefthookExePath); err != nil {
return fmt.Errorf("failed to recover from backup: %w", err)
}

return errUpdateFailed
}

log.Debugf("chmod +x %s", lefthookExePath)
if err = os.Chmod(lefthookExePath, modExecutable); err != nil {
log.Errorf("Failed to set executable file mode: %s", err)
if err = os.Rename(backupPath, lefthookExePath); err != nil {
return fmt.Errorf("failed to recover from backup: %w", err)
}

return errUpdateFailed
}

return nil
}

func (u *Updater) fetchLatestRelease(ctx context.Context) (*release, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.releaseURL, nil)
if err != nil {
return nil, fmt.Errorf("failed to initialize a request: %w", err)
}
req.Header.Set("Accept", "application/vnd.github+json")
req.Header.Set("X-GitHub-Api-Version", "2022-11-28")

resp, err := u.client.Do(req)
if err != nil {
return nil, fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()

var rel release
if err = json.NewDecoder(resp.Body).Decode(&rel); err != nil {
return nil, fmt.Errorf("failed to parse the Github response: %w", err)
}

return &rel, nil
}

func (u *Updater) download(ctx context.Context, name, fileURL, checksumURL, path string) (bool, error) {
log.Debugf("Downloading %s to %s", fileURL, path)

filereq, err := http.NewRequestWithContext(ctx, http.MethodGet, fileURL, nil)
if err != nil {
return false, fmt.Errorf("failed to build download request: %w", err)
}

sumreq, err := http.NewRequestWithContext(ctx, http.MethodGet, checksumURL, nil)
if err != nil {
return false, fmt.Errorf("failed to build checksum download request: %w", err)
}

file, err := os.Create(path)
if err != nil {
return false, fmt.Errorf("failed to create destination path (%s): %w", path, err)
}
defer file.Close()

resp, err := u.client.Do(filereq)
if err != nil {
return false, fmt.Errorf("download request failed: %w", err)
}
defer resp.Body.Close()

checksumResp, err := u.client.Do(sumreq)
if err != nil {
return false, fmt.Errorf("checksum download request failed: %w", err)
}
defer checksumResp.Body.Close()

bar := progressbar.DefaultBytes(resp.ContentLength+checksumResp.ContentLength, name)

fileHasher := sha256.New()
if _, err = io.Copy(io.MultiWriter(file, fileHasher, bar), resp.Body); err != nil {
return false, fmt.Errorf("failed to download the file: %w", err)
}
log.Debug()

hashsum := hex.EncodeToString(fileHasher.Sum(nil))

scanner := bufio.NewScanner(checksumResp.Body)
for scanner.Scan() {
sums := strings.Fields(scanner.Text())
if len(sums) < checksumFields {
continue
}

log.Debugf("Checking %s %s", sums[0], sums[1])
if sums[1] == name {
if sums[0] == hashsum {
if err = bar.Finish(); err != nil {
log.Debugf("Progressbar error: %s", err)
}

log.Debugf("Match %s %s", sums[0], sums[1])

return true, nil
} else {
return false, nil
}
}
}

log.Debugf("No matches found for %s %s", name, hashsum)

return false, nil
}
162 changes: 162 additions & 0 deletions internal/updater/updater_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
package updater

import (
"context"
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"runtime"
"testing"

"github.com/stretchr/testify/assert"

"github.com/evilmartians/lefthook/internal/version"
)

func TestUpdater_SelfUpdate(t *testing.T) {
var extension string
if runtime.GOOS == "windows" {
extension = ".exe"
}
exePath := filepath.Join(os.TempDir(), "lefthook")
for name, tt := range map[string]struct {
latestRelease string
assetName string
checksums string
opts Options
asset []byte
err error
}{
"asset not found": {
latestRelease: "v1.0.0",
assetName: "lefthook_1.0.0_darwin_arm64",
opts: Options{
Yes: true,
Force: false,
ExePath: exePath,
},
err: errNoAsset,
},
"no need to update": {
latestRelease: "v" + version.Version(false),
assetName: "lefthook_1.0.0_darwin_arm64",
opts: Options{
Yes: true,
Force: false,
ExePath: exePath,
},
err: nil,
},
"forced update but asset not found": {
latestRelease: "v" + version.Version(false),
assetName: "lefthook_1.0.0_darwin_arm64",
opts: Options{
Yes: true,
Force: true,
ExePath: exePath,
},
err: errNoAsset,
},
"invalid hashsum": {
latestRelease: "v1.0.0",
assetName: "lefthook_1.0.0_" + osNames[runtime.GOOS] + "_" + archNames[runtime.GOARCH] + extension,
opts: Options{
Yes: true,
Force: true,
ExePath: exePath,
},
asset: []byte{65, 54, 24, 32, 43, 67, 21},
checksums: `
67a5740c6c66d986c5708cddd6bd0bc240db29451646fc4c1398b988dcf7cdfe lefthook_1.0.0_MacOS_arm64
67a5740c6c66d986c5708cddd6bd0bc240db29451646fc4c1398b988dcf7cdfe lefthook_1.0.0_MacOS_x86_64
67a5740c6c66d986c5708cddd6bd0bc240db29451646fc4c1398b988dcf7cdfe lefthook_1.0.0_Linux_x86_64
67a5740c6c66d986c5708cddd6bd0bc240db29451646fc4c1398b988dcf7cdfe lefthook_1.0.0_Linux_arm64
67a5740c6c66d986c5708cddd6bd0bc240db29451646fc4c1398b988dcf7cdfe lefthook_1.0.0_Windows_x86_64.exe
`,
err: errInvalidHashsum,
},
"success": {
latestRelease: "v1.0.0",
assetName: "lefthook_1.0.0_" + osNames[runtime.GOOS] + "_" + archNames[runtime.GOARCH] + extension,
opts: Options{
Yes: true,
Force: true,
ExePath: exePath,
},
asset: []byte{65, 54, 24, 32, 43, 67, 21},
checksums: `
0e1c97246ba1bc8bde78355ae986589545d3c69bf1264d2d3c1835ec072006f6 lefthook_1.0.0_MacOS_arm64
0e1c97246ba1bc8bde78355ae986589545d3c69bf1264d2d3c1835ec072006f6 lefthook_1.0.0_MacOS_x86_64
0e1c97246ba1bc8bde78355ae986589545d3c69bf1264d2d3c1835ec072006f6 lefthook_1.0.0_Linux_x86_64
0e1c97246ba1bc8bde78355ae986589545d3c69bf1264d2d3c1835ec072006f6 lefthook_1.0.0_Linux_arm64
0e1c97246ba1bc8bde78355ae986589545d3c69bf1264d2d3c1835ec072006f6 lefthook_1.0.0_Windows_x86_64.exe
`,
err: nil,
},
} {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
file, err := os.Create(tt.opts.ExePath)
assert.NoError(err)
file.Close()

checksumServer := httptest.NewServer(
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
n, werr := w.Write([]byte(tt.checksums))
assert.Equal(n, len(tt.checksums))
assert.NoError(werr)
}))
defer checksumServer.Close()
assetServer := httptest.NewServer(
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
n, werr := w.Write(tt.asset)
assert.Equal(n, len(tt.asset))
assert.NoError(werr)
}))
defer assetServer.Close()

releaseServer := httptest.NewServer(
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.NoError(json.NewEncoder(w).Encode(map[string]interface{}{
"tag_name": tt.latestRelease,
"assets": []map[string]string{
{
"name": tt.assetName,
"browser_download_url": assetServer.URL,
},
{
"name": "lefthook_checksums.txt",
"browser_download_url": checksumServer.URL,
},
},
}))
}))
defer releaseServer.Close()

upd := Updater{
client: releaseServer.Client(),
releaseURL: releaseServer.URL,
}

err = upd.SelfUpdate(context.Background(), tt.opts)

if tt.err != nil {
if !errors.Is(err, tt.err) {
t.Error(err)
}
} else {
assert.NoError(err)

if tt.asset != nil {
content, err := os.ReadFile(tt.opts.ExePath)
assert.NoError(err)

assert.Equal(content, tt.asset)
}
}
})
}
}
2 changes: 1 addition & 1 deletion internal/version/version.go
Original file line number Diff line number Diff line change
@@ -8,7 +8,7 @@ import (
"strconv"
)

const version = "1.7.5"
const version = "1.7.6"

var (
// Is set via -X github.com/evilmartians/lefthook/internal/version.commit={commit}.
2 changes: 1 addition & 1 deletion packaging/npm-bundled/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@evilmartians/lefthook",
"version": "1.7.5",
"version": "1.7.6",
"description": "Simple git hooks manager",
"main": "bin/index.js",
"bin": {
2 changes: 1 addition & 1 deletion packaging/npm-installer/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@evilmartians/lefthook-installer",
"version": "1.7.5",
"version": "1.7.6",
"description": "Simple git hooks manager",
"main": "bin/index.js",
"bin": {
2 changes: 1 addition & 1 deletion packaging/npm/lefthook-darwin-arm64/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "lefthook-darwin-arm64",
"version": "1.7.5",
"version": "1.7.6",
"description": "The macOS ARM 64-bit binary for lefthook, git hooks manager.",
"preferUnplugged": false,
"repository": "https://github.com/evilmartians/lefthook",
2 changes: 1 addition & 1 deletion packaging/npm/lefthook-darwin-x64/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "lefthook-darwin-x64",
"version": "1.7.5",
"version": "1.7.6",
"description": "The macOS 64-bit binary for lefthook, git hooks manager.",
"preferUnplugged": false,
"repository": "https://github.com/evilmartians/lefthook",
2 changes: 1 addition & 1 deletion packaging/npm/lefthook-freebsd-arm64/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "lefthook-freebsd-arm64",
"version": "1.7.5",
"version": "1.7.6",
"description": "The FreeBSD ARM 64-bit binary for lefthook, git hooks manager.",
"preferUnplugged": false,
"repository": "https://github.com/evilmartians/lefthook",
2 changes: 1 addition & 1 deletion packaging/npm/lefthook-freebsd-x64/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "lefthook-freebsd-x64",
"version": "1.7.5",
"version": "1.7.6",
"description": "The FreeBSD 64-bit binary for lefthook, git hooks manager.",
"preferUnplugged": false,
"repository": "https://github.com/evilmartians/lefthook",
2 changes: 1 addition & 1 deletion packaging/npm/lefthook-linux-arm64/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "lefthook-linux-arm64",
"version": "1.7.5",
"version": "1.7.6",
"description": "The Linux ARM 64-bit binary for lefthook, git hooks manager.",
"preferUnplugged": false,
"repository": "https://github.com/evilmartians/lefthook",
2 changes: 1 addition & 1 deletion packaging/npm/lefthook-linux-x64/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "lefthook-linux-x64",
"version": "1.7.5",
"version": "1.7.6",
"description": "The Linux 64-bit binary for lefthook, git hooks manager.",
"preferUnplugged": false,
"repository": "https://github.com/evilmartians/lefthook",
2 changes: 1 addition & 1 deletion packaging/npm/lefthook-windows-arm64/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "lefthook-windows-arm64",
"version": "1.7.5",
"version": "1.7.6",
"description": "The Windows ARM 64-bit binary for lefthook, git hooks manager.",
"preferUnplugged": false,
"repository": "https://github.com/evilmartians/lefthook",
2 changes: 1 addition & 1 deletion packaging/npm/lefthook-windows-x64/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "lefthook-windows-x64",
"version": "1.7.5",
"version": "1.7.6",
"description": "The Windows 64-bit binary for lefthook, git hooks manager.",
"preferUnplugged": false,
"repository": "https://github.com/evilmartians/lefthook",
18 changes: 9 additions & 9 deletions packaging/npm/lefthook/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "lefthook",
"version": "1.7.5",
"version": "1.7.6",
"description": "Simple git hooks manager",
"repository": "https://github.com/evilmartians/lefthook",
"main": "bin/index.js",
@@ -20,14 +20,14 @@
},
"homepage": "https://github.com/evilmartians/lefthook#readme",
"optionalDependencies": {
"lefthook-darwin-arm64": "1.7.5",
"lefthook-darwin-x64": "1.7.5",
"lefthook-linux-arm64": "1.7.5",
"lefthook-linux-x64": "1.7.5",
"lefthook-freebsd-arm64": "1.7.5",
"lefthook-freebsd-x64": "1.7.5",
"lefthook-windows-arm64": "1.7.5",
"lefthook-windows-x64": "1.7.5"
"lefthook-darwin-arm64": "1.7.6",
"lefthook-darwin-x64": "1.7.6",
"lefthook-linux-arm64": "1.7.6",
"lefthook-linux-x64": "1.7.6",
"lefthook-freebsd-arm64": "1.7.6",
"lefthook-freebsd-x64": "1.7.6",
"lefthook-windows-arm64": "1.7.6",
"lefthook-windows-x64": "1.7.6"
},
"scripts": {
"postinstall": "node postinstall.js"
50 changes: 25 additions & 25 deletions packaging/pack.rb
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@

require "fileutils"

VERSION = "1.7.5"
VERSION = "1.7.6"

ROOT = File.join(__dir__, "..")
DIST = File.join(ROOT, "dist")
@@ -54,42 +54,42 @@ def put_binaries
cd(__dir__)
puts "Putting binaries to packages..."
{
"#{DIST}/lefthook_linux_amd64_v1/lefthook" => "npm/lefthook-linux-x64/bin/lefthook",
"#{DIST}/lefthook_linux_arm64/lefthook" => "npm/lefthook-linux-arm64/bin/lefthook",
"#{DIST}/lefthook_freebsd_amd64_v1/lefthook" => "npm/lefthook-freebsd-x64/bin/lefthook",
"#{DIST}/lefthook_freebsd_arm64/lefthook" => "npm/lefthook-freebsd-arm64/bin/lefthook",
"#{DIST}/lefthook_windows_amd64_v1/lefthook.exe" => "npm/lefthook-windows-x64/bin/lefthook.exe",
"#{DIST}/lefthook_windows_arm64/lefthook.exe" => "npm/lefthook-windows-arm64/bin/lefthook.exe",
"#{DIST}/lefthook_darwin_amd64_v1/lefthook" => "npm/lefthook-darwin-x64/bin/lefthook",
"#{DIST}/lefthook_darwin_arm64/lefthook" => "npm/lefthook-darwin-arm64/bin/lefthook",
"#{DIST}/no_self_update_linux_amd64_v1/lefthook" => "npm/lefthook-linux-x64/bin/lefthook",
"#{DIST}/no_self_update_linux_arm64/lefthook" => "npm/lefthook-linux-arm64/bin/lefthook",
"#{DIST}/no_self_update_freebsd_amd64_v1/lefthook" => "npm/lefthook-freebsd-x64/bin/lefthook",
"#{DIST}/no_self_update_freebsd_arm64/lefthook" => "npm/lefthook-freebsd-arm64/bin/lefthook",
"#{DIST}/no_self_update_windows_amd64_v1/lefthook.exe" => "npm/lefthook-windows-x64/bin/lefthook.exe",
"#{DIST}/no_self_update_windows_arm64/lefthook.exe" => "npm/lefthook-windows-arm64/bin/lefthook.exe",
"#{DIST}/no_self_update_darwin_amd64_v1/lefthook" => "npm/lefthook-darwin-x64/bin/lefthook",
"#{DIST}/no_self_update_darwin_arm64/lefthook" => "npm/lefthook-darwin-arm64/bin/lefthook",
}.each do |(source, dest)|
mkdir_p(File.dirname(dest))
cp(source, dest, verbose: true)
end

{
"#{DIST}/lefthook_linux_amd64_v1/lefthook" => "npm-bundled/bin/lefthook-linux-x64/lefthook",
"#{DIST}/lefthook_linux_arm64/lefthook" => "npm-bundled/bin/lefthook-linux-arm64/lefthook",
"#{DIST}/lefthook_freebsd_amd64_v1/lefthook" => "npm-bundled/bin/lefthook-freebsd-x64/lefthook",
"#{DIST}/lefthook_freebsd_arm64/lefthook" => "npm-bundled/bin/lefthook-freebsd-arm64/lefthook",
"#{DIST}/lefthook_windows_amd64_v1/lefthook.exe" => "npm-bundled/bin/lefthook-windows-x64/lefthook.exe",
"#{DIST}/lefthook_windows_arm64/lefthook.exe" => "npm-bundled/bin/lefthook-windows-arm64/lefthook.exe",
"#{DIST}/lefthook_darwin_amd64_v1/lefthook" => "npm-bundled/bin/lefthook-darwin-x64/lefthook",
"#{DIST}/lefthook_darwin_arm64/lefthook" => "npm-bundled/bin/lefthook-darwin-arm64/lefthook",
"#{DIST}/no_self_update_linux_amd64_v1/lefthook" => "npm-bundled/bin/lefthook-linux-x64/lefthook",
"#{DIST}/no_self_update_linux_arm64/lefthook" => "npm-bundled/bin/lefthook-linux-arm64/lefthook",
"#{DIST}/no_self_update_freebsd_amd64_v1/lefthook" => "npm-bundled/bin/lefthook-freebsd-x64/lefthook",
"#{DIST}/no_self_update_freebsd_arm64/lefthook" => "npm-bundled/bin/lefthook-freebsd-arm64/lefthook",
"#{DIST}/no_self_update_windows_amd64_v1/lefthook.exe" => "npm-bundled/bin/lefthook-windows-x64/lefthook.exe",
"#{DIST}/no_self_update_windows_arm64/lefthook.exe" => "npm-bundled/bin/lefthook-windows-arm64/lefthook.exe",
"#{DIST}/no_self_update_darwin_amd64_v1/lefthook" => "npm-bundled/bin/lefthook-darwin-x64/lefthook",
"#{DIST}/no_self_update_darwin_arm64/lefthook" => "npm-bundled/bin/lefthook-darwin-arm64/lefthook",
}.each do |(source, dest)|
mkdir_p(File.dirname(dest))
cp(source, dest, verbose: true)
end

{
"#{DIST}/lefthook_linux_amd64_v1/lefthook" => "rubygems/libexec/lefthook-linux-x64/lefthook",
"#{DIST}/lefthook_linux_arm64/lefthook" => "rubygems/libexec/lefthook-linux-arm64/lefthook",
"#{DIST}/lefthook_freebsd_amd64_v1/lefthook" => "rubygems/libexec/lefthook-freebsd-x64/lefthook",
"#{DIST}/lefthook_freebsd_arm64/lefthook" => "rubygems/libexec/lefthook-freebsd-arm64/lefthook",
"#{DIST}/lefthook_windows_amd64_v1/lefthook.exe" => "rubygems/libexec/lefthook-windows-x64/lefthook.exe",
"#{DIST}/lefthook_windows_arm64/lefthook.exe" => "rubygems/libexec/lefthook-windows-arm64/lefthook.exe",
"#{DIST}/lefthook_darwin_amd64_v1/lefthook" => "rubygems/libexec/lefthook-darwin-x64/lefthook",
"#{DIST}/lefthook_darwin_arm64/lefthook" => "rubygems/libexec/lefthook-darwin-arm64/lefthook",
"#{DIST}/no_self_update_linux_amd64_v1/lefthook" => "rubygems/libexec/lefthook-linux-x64/lefthook",
"#{DIST}/no_self_update_linux_arm64/lefthook" => "rubygems/libexec/lefthook-linux-arm64/lefthook",
"#{DIST}/no_self_update_freebsd_amd64_v1/lefthook" => "rubygems/libexec/lefthook-freebsd-x64/lefthook",
"#{DIST}/no_self_update_freebsd_arm64/lefthook" => "rubygems/libexec/lefthook-freebsd-arm64/lefthook",
"#{DIST}/no_self_update_windows_amd64_v1/lefthook.exe" => "rubygems/libexec/lefthook-windows-x64/lefthook.exe",
"#{DIST}/no_self_update_windows_arm64/lefthook.exe" => "rubygems/libexec/lefthook-windows-arm64/lefthook.exe",
"#{DIST}/no_self_update_darwin_amd64_v1/lefthook" => "rubygems/libexec/lefthook-darwin-x64/lefthook",
"#{DIST}/no_self_update_darwin_arm64/lefthook" => "rubygems/libexec/lefthook-darwin-arm64/lefthook",
}.each do |(source, dest)|
mkdir_p(File.dirname(dest))
cp(source, dest, verbose: true)
2 changes: 1 addition & 1 deletion packaging/rubygems/lefthook.gemspec
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Gem::Specification.new do |spec|
spec.name = "lefthook"
spec.version = "1.7.5"
spec.version = "1.7.6"
spec.authors = ["A.A.Abroskin", "Evil Martians"]
spec.email = ["lefthook@evilmartians.com"]