Skip to content

Commit

Permalink
feat: Add --every flag to update command
Browse files Browse the repository at this point in the history
  • Loading branch information
twpayne committed Jan 25, 2025
1 parent 2461322 commit b189d38
Show file tree
Hide file tree
Showing 3 changed files with 88 additions and 0 deletions.
4 changes: 4 additions & 0 deletions assets/chezmoi.io/docs/reference/commands/update.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ If `update.command` is set then chezmoi will run `update.command` with

Apply changes after pulling, `true` by default. Can be disabled with `--apply=false`.

### `--every` *duration*

Only update if the last update was more than *duration* ago.

### `--recurse-submodules`

Update submodules recursively. This defaults to `true`. Can be disabled with `--recurse-submodules=false`.
Expand Down
33 changes: 33 additions & 0 deletions internal/cmd/testdata/scripts/updateevery.txtar
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
[windows] skip 'UNIX only'
[!exec:git] skip 'git not found in $PATH'

mkgitconfig

# create a git repo in home/user/repo
exec git -C $HOME/repo init
exec git -C $HOME/repo add .
exec git -C $HOME/repo commit -m 'Initial commit'

exec chezmoi init file://$HOME/repo

# test that chezmoi update runs every time
exec chezmoi update
stdout 'ran update'
exec chezmoi update
stdout 'ran update'

# test that chezmoi update --every rate limits updates
exec chezmoi update --every=24h
! stdout 'ran update'
exec chezmoi update --every=1ns
stdout 'ran update'
exec chezmoi update --every=24h
! stdout 'ran update'
exec chezmoi update
stdout 'ran update'

-- home/user/repo/run_after_update.sh --
#!/bin/sh

echo "ran update"

51 changes: 51 additions & 0 deletions internal/cmd/updatecmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package cmd

import (
"errors"
"time"

"github.com/go-git/go-git/v5"
"github.com/spf13/cobra"
Expand All @@ -14,12 +15,18 @@ type updateCmdConfig struct {
Args []string `json:"args" mapstructure:"args" yaml:"args"`
Apply bool `json:"apply" mapstructure:"apply" yaml:"apply"`
RecurseSubmodules bool `json:"recurseSubmodules" mapstructure:"recurseSubmodules" yaml:"recurseSubmodules"`
every time.Duration
filter *chezmoi.EntryTypeFilter
init bool
parentDirs bool
recursive bool
}

var (
updateStateBucket = []byte("updateState")
lastUpdateKey = []byte("lastUpdate")
)

func (c *Config) newUpdateCmd() *cobra.Command {
updateCmd := &cobra.Command{
Use: "update",
Expand All @@ -38,6 +45,7 @@ func (c *Config) newUpdateCmd() *cobra.Command {
}

updateCmd.Flags().BoolVarP(&c.Update.Apply, "apply", "a", c.Update.Apply, "Apply after pulling")
updateCmd.Flags().DurationVar(&c.Update.every, "every", c.Update.every, "Rate limit updates")
updateCmd.Flags().VarP(c.Update.filter.Exclude, "exclude", "x", "Exclude entry types")
updateCmd.Flags().VarP(c.Update.filter.Include, "include", "i", "Include entry types")
updateCmd.Flags().BoolVar(&c.Update.init, "init", c.Update.init, "Recreate config file from template")
Expand All @@ -50,6 +58,12 @@ func (c *Config) newUpdateCmd() *cobra.Command {
}

func (c *Config) runUpdateCmd(cmd *cobra.Command, args []string) error {
// Determine whether to update.
shouldUpdate, markUpdateDone := c.shouldUpdate()
if !shouldUpdate {
return nil
}

switch {
case c.Update.Command != "":
if err := c.run(c.WorkingTreeAbsPath, c.Update.Command, c.Update.Args); err != nil {
Expand Down Expand Up @@ -103,5 +117,42 @@ func (c *Config) runUpdateCmd(cmd *cobra.Command, args []string) error {
}
}

if markUpdateDone != nil {
markUpdateDone()
}

return nil
}

// shouldUpdate returns whether to update and a function to be run
// after the update is successful.
func (c *Config) shouldUpdate() (bool, func()) {
now := time.Now()
markUpdateDone := func() {
_ = c.persistentState.Set(updateStateBucket, lastUpdateKey, []byte(now.Format(time.RFC3339)))
}

// If the user has not rate-limited updates, then always run the update.
if c.Update.every == 0 {
return true, markUpdateDone
}

// Otherwise, determine when the last update was run. In case of any error,
// ignore the error and run the update.
lastUpdateValue, err := c.persistentState.Get(updateStateBucket, lastUpdateKey)
if err != nil {
return true, markUpdateDone
}
lastUpdate, err := time.Parse(time.RFC3339, string(lastUpdateValue))
if err != nil {
return true, markUpdateDone
}

// If the last update was run sufficiently recently then don't update.
if lastUpdate.Add(c.Update.every).After(now) {
return false, nil
}

// Otherwise, update.
return true, markUpdateDone
}

0 comments on commit b189d38

Please sign in to comment.