Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add --every flag to update command #4227

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
}
Loading