From 21f6e9ba87e3513973a62b81e67de253dc8afcde Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Sat, 25 Aug 2018 15:55:49 +1000 Subject: [PATCH] auto-updates --- .goreleaser.yml | 3 + main.go | 11 ++- pkg/app/app.go | 2 +- pkg/commands/git.go | 2 +- pkg/config/app_config.go | 170 +++++++++++++++++++++++++--------- pkg/gui/app_status_manager.go | 44 +++++++++ pkg/gui/confirmation_panel.go | 53 ++++++----- pkg/gui/gui.go | 135 ++++++++++++++------------- pkg/gui/keybindings.go | 1 + pkg/gui/status_panel.go | 6 ++ pkg/gui/view_helpers.go | 12 --- pkg/i18n/english.go | 3 + pkg/updates/updates.go | 74 +++++++++++---- pkg/utils/utils.go | 9 ++ 14 files changed, 359 insertions(+), 166 deletions(-) create mode 100644 pkg/gui/app_status_manager.go diff --git a/.goreleaser.yml b/.goreleaser.yml index 8f8da01ee..c02fb0c67 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -13,6 +13,9 @@ builds: - arm - arm64 - 386 + # Default is `-s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}`. + ldflags: + - -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}} -X main.buildSource=binaryRelease archive: replacements: diff --git a/main.go b/main.go index e8c092da2..2e0ad8f77 100644 --- a/main.go +++ b/main.go @@ -12,9 +12,10 @@ import ( ) var ( - commit string - version = "unversioned" - date string + commit string + version = "unversioned" + date string + buildSource = "unknown" debuggingFlag = flag.Bool("debug", false, "a boolean") versionFlag = flag.Bool("v", false, "Print the current version") @@ -28,10 +29,10 @@ func projectPath(path string) string { func main() { flag.Parse() if *versionFlag { - fmt.Printf("commit=%s, build date=%s, version=%s, os=%s, arch=%s\n", commit, date, version, runtime.GOOS, runtime.GOARCH) + fmt.Printf("commit=%s, build date=%s, build source=%s, version=%s, os=%s, arch=%s\n", commit, date, buildSource, version, runtime.GOOS, runtime.GOARCH) os.Exit(0) } - appConfig, err := config.NewAppConfig("lazygit", version, commit, date, debuggingFlag) + appConfig, err := config.NewAppConfig("lazygit", version, commit, date, buildSource, debuggingFlag) if err != nil { panic(err) } diff --git a/pkg/app/app.go b/pkg/app/app.go index d4219289e..a00eb8c06 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -5,12 +5,12 @@ import ( "io/ioutil" "os" - "github.com/sirupsen/logrus" "github.com/jesseduffield/lazygit/pkg/commands" "github.com/jesseduffield/lazygit/pkg/config" "github.com/jesseduffield/lazygit/pkg/gui" "github.com/jesseduffield/lazygit/pkg/i18n" "github.com/jesseduffield/lazygit/pkg/updates" + "github.com/sirupsen/logrus" ) // App struct diff --git a/pkg/commands/git.go b/pkg/commands/git.go index 14f3a433a..7ec63ee7e 100644 --- a/pkg/commands/git.go +++ b/pkg/commands/git.go @@ -7,9 +7,9 @@ import ( "os/exec" "strings" - "github.com/sirupsen/logrus" "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/utils" + "github.com/sirupsen/logrus" gitconfig "github.com/tcnksm/go-gitconfig" gogit "gopkg.in/src-d/go-git.v4" ) diff --git a/pkg/config/app_config.go b/pkg/config/app_config.go index aa56365e3..4038e65fd 100644 --- a/pkg/config/app_config.go +++ b/pkg/config/app_config.go @@ -2,19 +2,24 @@ package config import ( "bytes" + "io/ioutil" + "path/filepath" "github.com/shibukawa/configdir" "github.com/spf13/viper" + yaml "gopkg.in/yaml.v2" ) // AppConfig contains the base configuration fields required for lazygit. type AppConfig struct { - Debug bool `long:"debug" env:"DEBUG" default:"false"` - Version string `long:"version" env:"VERSION" default:"unversioned"` - Commit string `long:"commit" env:"COMMIT"` - BuildDate string `long:"build-date" env:"BUILD_DATE"` - Name string `long:"name" env:"NAME" default:"lazygit"` - UserConfig *viper.Viper + Debug bool `long:"debug" env:"DEBUG" default:"false"` + Version string `long:"version" env:"VERSION" default:"unversioned"` + Commit string `long:"commit" env:"COMMIT"` + BuildDate string `long:"build-date" env:"BUILD_DATE"` + Name string `long:"name" env:"NAME" default:"lazygit"` + BuildSource string `long:"build-source" env:"BUILD_SOURCE" default:""` + UserConfig *viper.Viper + AppState *AppState } // AppConfigurer interface allows individual app config structs to inherit Fields @@ -25,25 +30,37 @@ type AppConfigurer interface { GetCommit() string GetBuildDate() string GetName() string + GetBuildSource() string GetUserConfig() *viper.Viper - InsertToUserConfig(string, string) error + GetAppState() *AppState + WriteToUserConfig(string, string) error + SaveAppState() error + LoadAppState() error } // NewAppConfig makes a new app config -func NewAppConfig(name, version, commit, date string, debuggingFlag *bool) (*AppConfig, error) { - userConfig, err := LoadUserConfig() +func NewAppConfig(name, version, commit, date string, buildSource string, debuggingFlag *bool) (*AppConfig, error) { + defaultConfig := getDefaultConfig() + userConfig, err := LoadConfig("config", defaultConfig) if err != nil { - panic(err) + return nil, err } appConfig := &AppConfig{ - Name: "lazygit", - Version: version, - Commit: commit, - BuildDate: date, - Debug: *debuggingFlag, - UserConfig: userConfig, + Name: "lazygit", + Version: version, + Commit: commit, + BuildDate: date, + Debug: *debuggingFlag, + BuildSource: buildSource, + UserConfig: userConfig, + AppState: &AppState{}, } + + if err := appConfig.LoadAppState(); err != nil { + return nil, err + } + return appConfig, nil } @@ -72,73 +89,122 @@ func (c *AppConfig) GetName() string { return c.Name } +// GetBuildSource returns the source of the build. For builds from goreleaser +// this will be binaryBuild +func (c *AppConfig) GetBuildSource() string { + return c.BuildSource +} + // GetUserConfig returns the user config func (c *AppConfig) GetUserConfig() *viper.Viper { return c.UserConfig } -func newViper() (*viper.Viper, error) { +// GetAppState returns the app state +func (c *AppConfig) GetAppState() *AppState { + return c.AppState +} + +func newViper(filename string) (*viper.Viper, error) { v := viper.New() v.SetConfigType("yaml") - v.SetConfigName("config") + v.SetConfigName(filename) return v, nil } -// LoadUserConfig gets the user's config -func LoadUserConfig() (*viper.Viper, error) { - v, err := newViper() +// LoadConfig gets the user's config +func LoadConfig(filename string, defaults []byte) (*viper.Viper, error) { + v, err := newViper(filename) if err != nil { - panic(err) - } - if err = LoadDefaultConfig(v); err != nil { return nil, err } - if err = LoadUserConfigFromFile(v); err != nil { + if defaults != nil { + if err = LoadDefaults(v, defaults); err != nil { + return nil, err + } + } + if err = LoadAndMergeFile(v, filename+".yml"); err != nil { return nil, err } return v, nil } -// LoadDefaultConfig loads in the defaults defined in this file -func LoadDefaultConfig(v *viper.Viper) error { - defaults := getDefaultConfig() +// LoadDefaults loads in the defaults defined in this file +func LoadDefaults(v *viper.Viper, defaults []byte) error { return v.ReadConfig(bytes.NewBuffer(defaults)) } -// LoadUserConfigFromFile Loads the user config from their config file, creating -// the file as an empty config if it does not exist -func LoadUserConfigFromFile(v *viper.Viper) error { +func prepareConfigFile(filename string) (string, error) { // chucking my name there is not for vanity purposes, the xdg spec (and that // function) requires a vendor name. May as well line up with github configDirs := configdir.New("jesseduffield", "lazygit") - folder := configDirs.QueryFolderContainsFile("config.yml") + folder := configDirs.QueryFolderContainsFile(filename) if folder == nil { - // create the file as an empty config and load it + // create the file as empty folders := configDirs.QueryFolders(configdir.Global) - if err := folders[0].WriteFile("config.yml", []byte{}); err != nil { - return err + if err := folders[0].WriteFile(filename, []byte{}); err != nil { + return "", err } - folder = configDirs.QueryFolderContainsFile("config.yml") + folder = configDirs.QueryFolderContainsFile(filename) } - v.AddConfigPath(folder.Path) - return v.MergeInConfig() + return filepath.Join(folder.Path, filename), nil } -// InsertToUserConfig adds a key/value pair to the user's config and saves it -func (c *AppConfig) InsertToUserConfig(key, value string) error { - // making a new viper object so that we're not writing any defaults back - // to the user's config file - v, err := newViper() +// LoadAndMergeFile Loads the config/state file, creating +// the file as an empty one if it does not exist +func LoadAndMergeFile(v *viper.Viper, filename string) error { + configPath, err := prepareConfigFile(filename) if err != nil { return err } - if err = LoadUserConfigFromFile(v); err != nil { + + v.AddConfigPath(filepath.Dir(configPath)) + return v.MergeInConfig() +} + +// WriteToUserConfig adds a key/value pair to the user's config and saves it +func (c *AppConfig) WriteToUserConfig(key, value string) error { + // reloading the user config directly (without defaults) so that we're not + // writing any defaults back to the user's config + v, err := LoadConfig("config", nil) + if err != nil { return err } + v.Set(key, value) return v.WriteConfig() } +// SaveAppState marhsalls the AppState struct and writes it to the disk +func (c *AppConfig) SaveAppState() error { + marshalledAppState, err := yaml.Marshal(c.AppState) + if err != nil { + return err + } + + filepath, err := prepareConfigFile("state.yml") + if err != nil { + return err + } + + return ioutil.WriteFile(filepath, marshalledAppState, 0644) +} + +func (c *AppConfig) LoadAppState() error { + filepath, err := prepareConfigFile("state.yml") + if err != nil { + return err + } + appStateBytes, err := ioutil.ReadFile(filepath) + if err != nil { + return err + } + if len(appStateBytes) == 0 { + return yaml.Unmarshal(getDefaultAppState(), c.AppState) + } + return yaml.Unmarshal(appStateBytes, c.AppState) +} + func getDefaultConfig() []byte { return []byte(` gui: @@ -156,7 +222,23 @@ func getDefaultConfig() []byte { # stuff relating to git os: # stuff relating to the OS + update: + method: prompt # can be prompt | background | never + days: 7 # only applies for prompt/background update methods +`) +} +// AppState stores data between runs of the app like when the last update check +// was performed and which other repos have been checked out +type AppState struct { + LastUpdateCheck int64 + RecentRepos []string +} + +func getDefaultAppState() []byte { + return []byte(` + lastUpdateCheck: 0 + recentRepos: [] `) } diff --git a/pkg/gui/app_status_manager.go b/pkg/gui/app_status_manager.go new file mode 100644 index 000000000..2d479e42f --- /dev/null +++ b/pkg/gui/app_status_manager.go @@ -0,0 +1,44 @@ +package gui + +import "github.com/jesseduffield/lazygit/pkg/utils" + +type appStatus struct { + name string + statusType string + duration int +} + +type statusManager struct { + statuses []appStatus +} + +func (m *statusManager) removeStatus(name string) { + newStatuses := []appStatus{} + for _, status := range m.statuses { + if status.name != name { + newStatuses = append(newStatuses, status) + } + } + m.statuses = newStatuses +} + +func (m *statusManager) addWaitingStatus(name string) { + m.removeStatus(name) + newStatus := appStatus{ + name: name, + statusType: "waiting", + duration: 0, + } + m.statuses = append([]appStatus{newStatus}, m.statuses...) +} + +func (m *statusManager) getStatusString() string { + if len(m.statuses) == 0 { + return "" + } + topStatus := m.statuses[0] + if topStatus.statusType == "waiting" { + return topStatus.name + " " + utils.Loader() + } + return topStatus.name +} diff --git a/pkg/gui/confirmation_panel.go b/pkg/gui/confirmation_panel.go index 9d91b53bc..17ca62b01 100644 --- a/pkg/gui/confirmation_panel.go +++ b/pkg/gui/confirmation_panel.go @@ -58,20 +58,30 @@ func (gui *Gui) getConfirmationPanelDimensions(g *gocui.Gui, prompt string) (int func (gui *Gui) createPromptPanel(g *gocui.Gui, currentView *gocui.View, title string, handleConfirm func(*gocui.Gui, *gocui.View) error) error { gui.onNewPopupPanel() - // only need to fit one line - x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(g, "") - if confirmationView, err := g.SetView("confirmation", x0, y0, x1, y1, 0); err != nil { - if err != gocui.ErrUnknownView { - return err - } + confirmationView, err := gui.prepareConfirmationPanel(currentView, title, "") + if err != nil { + return err + } + confirmationView.Editable = true + return gui.setKeyBindings(g, handleConfirm, nil) +} - confirmationView.Editable = true +func (gui *Gui) prepareConfirmationPanel(currentView *gocui.View, title, prompt string) (*gocui.View, error) { + x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(gui.g, prompt) + confirmationView, err := gui.g.SetView("confirmation", x0, y0, x1, y1, 0) + if err != nil { + if err != gocui.ErrUnknownView { + return nil, err + } confirmationView.Title = title confirmationView.FgColor = gocui.ColorWhite - gui.switchFocus(g, currentView, confirmationView) - return gui.setKeyBindings(g, handleConfirm, nil) } - return nil + confirmationView.Clear() + + if err := gui.switchFocus(gui.g, currentView, confirmationView); err != nil { + return nil, err + } + return confirmationView, nil } func (gui *Gui) onNewPopupPanel() { @@ -93,18 +103,15 @@ func (gui *Gui) createConfirmationPanel(g *gocui.Gui, currentView *gocui.View, t gui.Log.Error(errMessage) } } - x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(g, prompt) - if confirmationView, err := g.SetView("confirmation", x0, y0, x1, y1, 0); err != nil { - if err != gocui.ErrUnknownView { - return err - } - confirmationView.Title = title - confirmationView.FgColor = gocui.ColorWhite - gui.renderString(g, "confirmation", prompt) - gui.switchFocus(g, currentView, confirmationView) - return gui.setKeyBindings(g, handleConfirm, handleClose) + confirmationView, err := gui.prepareConfirmationPanel(currentView, title, prompt) + if err != nil { + return err } - return nil + confirmationView.Editable = false + if err := gui.renderString(g, "confirmation", prompt); err != nil { + return err + } + return gui.setKeyBindings(g, handleConfirm, handleClose) }) return nil } @@ -131,7 +138,9 @@ func (gui *Gui) setKeyBindings(g *gocui.Gui, handleConfirm, handleClose func(*go "keyBindConfirm": "enter", }, ) - gui.renderString(g, "options", actions) + if err := gui.renderString(g, "options", actions); err != nil { + return err + } if err := g.SetKeybinding("confirmation", gocui.KeyEnter, gocui.ModNone, gui.wrappedConfirmationFunction(handleConfirm)); err != nil { return err } diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index c00b26b78..c8ebbfa72 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -21,6 +21,7 @@ import ( "github.com/jesseduffield/lazygit/pkg/config" "github.com/jesseduffield/lazygit/pkg/i18n" "github.com/jesseduffield/lazygit/pkg/updates" + "github.com/jesseduffield/lazygit/pkg/utils" "github.com/sirupsen/logrus" ) @@ -56,16 +57,17 @@ type Teml i18n.Teml // Gui wraps the gocui Gui object which handles rendering and events type Gui struct { - g *gocui.Gui - Log *logrus.Logger - GitCommand *commands.GitCommand - OSCommand *commands.OSCommand - SubProcess *exec.Cmd - State guiState - Config config.AppConfigurer - Tr *i18n.Localizer - Errors SentinelErrors - Updater *updates.Updater + g *gocui.Gui + Log *logrus.Logger + GitCommand *commands.GitCommand + OSCommand *commands.OSCommand + SubProcess *exec.Cmd + State guiState + Config config.AppConfigurer + Tr *i18n.Localizer + Errors SentinelErrors + Updater *updates.Updater + statusManager *statusManager } type guiState struct { @@ -98,13 +100,14 @@ func NewGui(log *logrus.Logger, gitCommand *commands.GitCommand, oSCommand *comm } gui := &Gui{ - Log: log, - GitCommand: gitCommand, - OSCommand: oSCommand, - State: initialState, - Config: config, - Tr: tr, - Updater: updater, + Log: log, + GitCommand: gitCommand, + OSCommand: oSCommand, + State: initialState, + Config: config, + Tr: tr, + Updater: updater, + statusManager: &statusManager{}, } gui.GenerateSentinelErrors() @@ -141,13 +144,6 @@ func max(a, b int) int { return b } -func (gui *Gui) setAppStatus(status string) error { - if err := gui.renderString(gui.g, "appStatus", status); err != nil { - return err - } - return nil -} - // layout is called for every screen re-render e.g. when the screen is resized func (gui *Gui) layout(g *gocui.Gui) error { g.Highlight = true @@ -162,10 +158,10 @@ func (gui *Gui) layout(g *gocui.Gui) error { minimumHeight := 16 minimumWidth := 10 - appStatusView, _ := g.View("appStatus") - appStatusOptionsBoundary := -2 - if appStatusView != nil && len(appStatusView.Buffer()) > 2 { - appStatusOptionsBoundary = len(appStatusView.Buffer()) + 2 + appStatus := gui.statusManager.getStatusString() + appStatusOptionsBoundary := 0 + if appStatus != "" { + appStatusOptionsBoundary = len(appStatus) + 2 } panelSpacing := 1 @@ -257,7 +253,7 @@ func (gui *Gui) layout(g *gocui.Gui) error { if gui.getCommitMessageView(g) == nil { // doesn't matter where this view starts because it will be hidden - if commitMessageView, err := g.SetView("commitMessage", 0, 0, width, height, 0); err != nil { + if commitMessageView, err := g.SetView("commitMessage", 0, 0, width/2, height/2, 0); err != nil { if err != gocui.ErrUnknownView { return err } @@ -268,18 +264,14 @@ func (gui *Gui) layout(g *gocui.Gui) error { } } - appStatusLeft := -1 - if appStatusOptionsBoundary < 2 { - appStatusLeft = -5 - } - - if v, err := g.SetView("appStatus", appStatusLeft, optionsTop, appStatusOptionsBoundary, optionsTop+2, 0); err != nil { + if appStatusView, err := g.SetView("appStatus", -1, optionsTop, width, optionsTop+2, 0); err != nil { if err != gocui.ErrUnknownView { return err } - v.BgColor = gocui.ColorDefault - v.FgColor = gocui.ColorCyan - v.Frame = false + appStatusView.BgColor = gocui.ColorDefault + appStatusView.FgColor = gocui.ColorCyan + appStatusView.Frame = false + g.SetViewOnBottom("appStatus") } if v, err := g.SetView("version", optionsVersionBoundary-1, optionsTop, width, optionsTop+2, 0); err != nil { @@ -293,9 +285,9 @@ func (gui *Gui) layout(g *gocui.Gui) error { return err } - // gui.Updater.CheckForNewUpdate(gui.onUpdateCheckFinish) - - // these are only called once + // these are only called once (it's a place to put all the things you want + // to happen on startup after the screen is first rendered) + gui.Updater.CheckForNewUpdate(gui.onUpdateCheckFinish, false) gui.handleFileSelect(g, filesView) gui.refreshFiles(g) gui.refreshBranches(g) @@ -313,32 +305,42 @@ func (gui *Gui) layout(g *gocui.Gui) error { func (gui *Gui) onUpdateFinish(err error) error { gui.State.Updating = false - gui.setAppStatus("") - if err != nil { - gui.createErrorPanel(gui.g, "Update failed: "+err.Error()) + gui.statusManager.removeStatus("updating") + if err := gui.renderString(gui.g, "appStatus", ""); err != nil { + return err + } + if err != nil { + return gui.createErrorPanel(gui.g, "Update failed: "+err.Error()) } - // TODO: on attempted quit, if downloading is true, ask if sure the user wants to quit return nil } func (gui *Gui) onUpdateCheckFinish(newVersion string, err error) error { - newVersion = "v0.1.72" if err != nil { - // ignoring the error for now + // ignoring the error for now so that I'm not annoying users + gui.Log.Error(err.Error()) return nil } if newVersion == "" { return nil } + if gui.Config.GetUserConfig().Get("update.method") == "background" { + gui.startUpdating(newVersion) + return nil + } title := "New version available!" message := "Download latest version? (enter/esc)" - // TODO: support nil view in createConfirmationPanel or always go back to filesPanel when there is no previous view - return gui.createConfirmationPanel(gui.g, nil, title, message, func(g *gocui.Gui, v *gocui.View) error { - gui.State.Updating = true - gui.setAppStatus("updating...") - gui.Updater.Update(newVersion, gui.onUpdateFinish) + currentView := gui.g.CurrentView() + return gui.createConfirmationPanel(gui.g, currentView, title, message, func(g *gocui.Gui, v *gocui.View) error { + gui.startUpdating(newVersion) return nil - }, nil) // TODO: set config value saying not to check for another while if user hits escape + }, nil) +} + +func (gui *Gui) startUpdating(newVersion string) { + gui.State.Updating = true + gui.statusManager.addWaitingStatus("updating") + gui.Updater.Update(newVersion, gui.onUpdateFinish) } func (gui *Gui) fetch(g *gocui.Gui) error { @@ -348,22 +350,26 @@ func (gui *Gui) fetch(g *gocui.Gui) error { } func (gui *Gui) updateLoader(g *gocui.Gui) error { - viewNames := []string{"confirmation", "appStatus"} - for _, viewName := range viewNames { - if view, _ := g.View(viewName); view != nil { - content := gui.trimmedContent(view) - if strings.Contains(content, "...") { - staticContent := strings.Split(content, "...")[0] + "..." - if viewName == "appStatus" { - // panic(staticContent + " " + gui.loader()) - } - gui.renderString(g, viewName, staticContent+" "+gui.loader()) + if view, _ := g.View("confirmation"); view != nil { + content := gui.trimmedContent(view) + if strings.Contains(content, "...") { + staticContent := strings.Split(content, "...")[0] + "..." + if err := gui.renderString(g, "confirmation", staticContent+" "+utils.Loader()); err != nil { + return err } } } return nil } +func (gui *Gui) renderAppStatus(g *gocui.Gui) error { + appStatus := gui.statusManager.getStatusString() + if appStatus != "" { + return gui.renderString(gui.g, "appStatus", appStatus) + } + return nil +} + func (gui *Gui) goEvery(g *gocui.Gui, interval time.Duration, function func(*gocui.Gui) error) { go func() { for range time.Tick(interval) { @@ -396,7 +402,8 @@ func (gui *Gui) Run() error { gui.goEvery(g, time.Second*60, gui.fetch) gui.goEvery(g, time.Second*10, gui.refreshFiles) - gui.goEvery(g, time.Millisecond*10, gui.updateLoader) + gui.goEvery(g, time.Millisecond*50, gui.updateLoader) + gui.goEvery(g, time.Millisecond*50, gui.renderAppStatus) g.SetManagerFunc(gui.layout) diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go index 8041d14ff..68bb295e5 100644 --- a/pkg/gui/keybindings.go +++ b/pkg/gui/keybindings.go @@ -26,6 +26,7 @@ func (gui *Gui) keybindings(g *gocui.Gui) error { {ViewName: "", Key: 'R', Modifier: gocui.ModNone, Handler: gui.handleRefresh}, {ViewName: "status", Key: 'e', Modifier: gocui.ModNone, Handler: gui.handleEditConfig}, {ViewName: "status", Key: 'o', Modifier: gocui.ModNone, Handler: gui.handleOpenConfig}, + {ViewName: "status", Key: 'u', Modifier: gocui.ModNone, Handler: gui.handleCheckForUpdate}, {ViewName: "files", Key: 'c', Modifier: gocui.ModNone, Handler: gui.handleCommitPress}, {ViewName: "files", Key: 'C', Modifier: gocui.ModNone, Handler: gui.handleCommitEditorPress}, {ViewName: "files", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: gui.handleFilePress}, diff --git a/pkg/gui/status_panel.go b/pkg/gui/status_panel.go index 88134096e..1f0eac1ad 100644 --- a/pkg/gui/status_panel.go +++ b/pkg/gui/status_panel.go @@ -45,9 +45,15 @@ func (gui *Gui) renderStatusOptions(g *gocui.Gui) error { return gui.renderOptionsMap(g, map[string]string{ "o": gui.Tr.SLocalize("OpenConfig"), "e": gui.Tr.SLocalize("EditConfig"), + "u": gui.Tr.SLocalize("CheckForUpdate"), }) } +func (gui *Gui) handleCheckForUpdate(g *gocui.Gui, v *gocui.View) error { + gui.Updater.CheckForNewUpdate(gui.onUpdateCheckFinish, true) + return gui.createMessagePanel(gui.g, v, "Checking for updates", "Checking for updates...") +} + func (gui *Gui) handleStatusSelect(g *gocui.Gui, v *gocui.View) error { dashboardString := fmt.Sprintf( "%s\n\n%s\n\n%s\n\n%s\n\n%s", diff --git a/pkg/gui/view_helpers.go b/pkg/gui/view_helpers.go index 91d81b55e..fd810220b 100644 --- a/pkg/gui/view_helpers.go +++ b/pkg/gui/view_helpers.go @@ -4,7 +4,6 @@ import ( "fmt" "sort" "strings" - "time" "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/utils" @@ -222,9 +221,6 @@ func (gui *Gui) renderString(g *gocui.Gui, viewName, s string) error { if err != nil { return nil } - if viewName == "appStatus" { - gui.Log.Info(s) - } v.Clear() output := string(bom.Clean([]byte(s))) output = utils.NormalizeLinefeeds(output) @@ -248,14 +244,6 @@ func (gui *Gui) renderOptionsMap(g *gocui.Gui, optionsMap map[string]string) err return gui.renderString(g, "options", gui.optionsMapToString(optionsMap)) } -func (gui *Gui) loader() string { - characters := "|/-\\" - now := time.Now() - nanos := now.UnixNano() - index := nanos / 50000000 % int64(len(characters)) - return characters[index : index+1] -} - // TODO: refactor properly func (gui *Gui) getFilesView(g *gocui.Gui) *gocui.View { v, _ := g.View("files") diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index 38fbac4cb..286d4f7e0 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -312,6 +312,9 @@ func addEnglish(i18nObject *i18n.Bundle) error { }, &i18n.Message{ ID: "ForcePushPrompt", Other: "Your branch has diverged from the remote branch. Press 'esc' to cancel, or 'enter' to force push.", + }, &i18n.Message{ + ID: "CheckForUpdate", + Other: "Check for update", }, ) } diff --git a/pkg/updates/updates.go b/pkg/updates/updates.go index 7e745635c..ae2265c4e 100644 --- a/pkg/updates/updates.go +++ b/pkg/updates/updates.go @@ -9,6 +9,7 @@ import ( "os" "path/filepath" "runtime" + "strings" "time" "github.com/kardianos/osext" @@ -21,10 +22,9 @@ import ( // Update checks for updates and does updates type Updater struct { - LastChecked string - Log *logrus.Entry - Config config.AppConfigurer - OSCommand *commands.OSCommand + Log *logrus.Entry + Config config.AppConfigurer + OSCommand *commands.OSCommand } // Updater implements the check and update methods @@ -42,16 +42,14 @@ func NewUpdater(log *logrus.Logger, config config.AppConfigurer, osCommand *comm contextLogger := log.WithField("context", "updates") updater := &Updater{ - LastChecked: "today", - Log: contextLogger, - Config: config, - OSCommand: osCommand, + Log: contextLogger, + Config: config, + OSCommand: osCommand, } return updater, nil } func (u *Updater) getLatestVersionNumber() (string, error) { - time.Sleep(5) req, err := http.NewRequest("GET", projectUrl+"/releases/latest", nil) if err != nil { return "", err @@ -76,21 +74,41 @@ func (u *Updater) getLatestVersionNumber() (string, error) { return dat["tag_name"].(string), nil } +func (u *Updater) RecordLastUpdateCheck() error { + u.Config.GetAppState().LastUpdateCheck = time.Now().Unix() + return u.Config.SaveAppState() +} + +// expecting version to be of the form `v12.34.56` +func (u *Updater) majorVersionDiffers(oldVersion, newVersion string) bool { + return strings.Split(oldVersion, ".")[0] != strings.Split(newVersion, ".")[0] +} + func (u *Updater) checkForNewUpdate() (string, error) { u.Log.Info("Checking for an updated version") - // if u.Config.GetVersion() == "unversioned" { - // u.Log.Info("Current version is not built from an official release so we won't check for an update") - // return "", nil - // } + if u.Config.GetVersion() == "unversioned" { + u.Log.Info("Current version is not built from an official release so we won't check for an update") + return "", nil + } newVersion, err := u.getLatestVersionNumber() if err != nil { return "", err } u.Log.Info("Current version is " + u.Config.GetVersion()) u.Log.Info("New version is " + newVersion) - // if newVersion == u.Config.GetVersion() { - // return "", nil - // } + + if err := u.RecordLastUpdateCheck(); err != nil { + return "", err + } + + if newVersion == u.Config.GetVersion() { + return "", nil + } + + if u.majorVersionDiffers(u.Config.GetVersion(), newVersion) { + u.Log.Info("New version has non-backwards compatible changes.") + return "", nil + } rawUrl, err := u.getBinaryUrl(newVersion) if err != nil { @@ -102,11 +120,16 @@ func (u *Updater) checkForNewUpdate() (string, error) { return "", nil } u.Log.Info("Verified resource is available, ready to update") + return newVersion, nil } // CheckForNewUpdate checks if there is an available update -func (u *Updater) CheckForNewUpdate(onFinish func(string, error) error) { +func (u *Updater) CheckForNewUpdate(onFinish func(string, error) error, userRequested bool) { + if !userRequested && u.skipUpdateCheck() { + return + } + go func() { newVersion, err := u.checkForNewUpdate() if err = onFinish(newVersion, err); err != nil { @@ -115,6 +138,23 @@ func (u *Updater) CheckForNewUpdate(onFinish func(string, error) error) { }() } +func (u *Updater) skipUpdateCheck() bool { + if u.Config.GetBuildSource() != "buildBinary" { + return true + } + + userConfig := u.Config.GetUserConfig() + if userConfig.Get("update.method") == "never" { + return true + } + + currentTimestamp := time.Now().Unix() + lastUpdateCheck := u.Config.GetAppState().LastUpdateCheck + days := userConfig.GetInt64("update.days") + + return (currentTimestamp-lastUpdateCheck)/(60*60*24) < days +} + func (u *Updater) mappedOs(os string) string { osMap := map[string]string{ "darwin": "Darwin", diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index 511de1af1..62706559e 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -6,6 +6,7 @@ import ( "os" "path/filepath" "strings" + "time" "github.com/fatih/color" ) @@ -81,3 +82,11 @@ func GetProjectRoot() string { } return strings.Split(dir, "lazygit")[0] + "lazygit" } + +func Loader() string { + characters := "|/-\\" + now := time.Now() + nanos := now.UnixNano() + index := nanos / 50000000 % int64(len(characters)) + return characters[index : index+1] +}