auto-updates

This commit is contained in:
Jesse Duffield 2018-08-25 15:55:49 +10:00
parent f24c95aede
commit 21f6e9ba87
14 changed files with 359 additions and 166 deletions

View file

@ -13,6 +13,9 @@ builds:
- arm - arm
- arm64 - arm64
- 386 - 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: archive:
replacements: replacements:

11
main.go
View file

@ -12,9 +12,10 @@ import (
) )
var ( var (
commit string commit string
version = "unversioned" version = "unversioned"
date string date string
buildSource = "unknown"
debuggingFlag = flag.Bool("debug", false, "a boolean") debuggingFlag = flag.Bool("debug", false, "a boolean")
versionFlag = flag.Bool("v", false, "Print the current version") versionFlag = flag.Bool("v", false, "Print the current version")
@ -28,10 +29,10 @@ func projectPath(path string) string {
func main() { func main() {
flag.Parse() flag.Parse()
if *versionFlag { 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) 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 { if err != nil {
panic(err) panic(err)
} }

View file

@ -5,12 +5,12 @@ import (
"io/ioutil" "io/ioutil"
"os" "os"
"github.com/sirupsen/logrus"
"github.com/jesseduffield/lazygit/pkg/commands" "github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/config" "github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/gui" "github.com/jesseduffield/lazygit/pkg/gui"
"github.com/jesseduffield/lazygit/pkg/i18n" "github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/jesseduffield/lazygit/pkg/updates" "github.com/jesseduffield/lazygit/pkg/updates"
"github.com/sirupsen/logrus"
) )
// App struct // App struct

View file

@ -7,9 +7,9 @@ import (
"os/exec" "os/exec"
"strings" "strings"
"github.com/sirupsen/logrus"
"github.com/jesseduffield/gocui" "github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/utils" "github.com/jesseduffield/lazygit/pkg/utils"
"github.com/sirupsen/logrus"
gitconfig "github.com/tcnksm/go-gitconfig" gitconfig "github.com/tcnksm/go-gitconfig"
gogit "gopkg.in/src-d/go-git.v4" gogit "gopkg.in/src-d/go-git.v4"
) )

View file

@ -2,19 +2,24 @@ package config
import ( import (
"bytes" "bytes"
"io/ioutil"
"path/filepath"
"github.com/shibukawa/configdir" "github.com/shibukawa/configdir"
"github.com/spf13/viper" "github.com/spf13/viper"
yaml "gopkg.in/yaml.v2"
) )
// AppConfig contains the base configuration fields required for lazygit. // AppConfig contains the base configuration fields required for lazygit.
type AppConfig struct { type AppConfig struct {
Debug bool `long:"debug" env:"DEBUG" default:"false"` Debug bool `long:"debug" env:"DEBUG" default:"false"`
Version string `long:"version" env:"VERSION" default:"unversioned"` Version string `long:"version" env:"VERSION" default:"unversioned"`
Commit string `long:"commit" env:"COMMIT"` Commit string `long:"commit" env:"COMMIT"`
BuildDate string `long:"build-date" env:"BUILD_DATE"` BuildDate string `long:"build-date" env:"BUILD_DATE"`
Name string `long:"name" env:"NAME" default:"lazygit"` Name string `long:"name" env:"NAME" default:"lazygit"`
UserConfig *viper.Viper BuildSource string `long:"build-source" env:"BUILD_SOURCE" default:""`
UserConfig *viper.Viper
AppState *AppState
} }
// AppConfigurer interface allows individual app config structs to inherit Fields // AppConfigurer interface allows individual app config structs to inherit Fields
@ -25,25 +30,37 @@ type AppConfigurer interface {
GetCommit() string GetCommit() string
GetBuildDate() string GetBuildDate() string
GetName() string GetName() string
GetBuildSource() string
GetUserConfig() *viper.Viper GetUserConfig() *viper.Viper
InsertToUserConfig(string, string) error GetAppState() *AppState
WriteToUserConfig(string, string) error
SaveAppState() error
LoadAppState() error
} }
// NewAppConfig makes a new app config // NewAppConfig makes a new app config
func NewAppConfig(name, version, commit, date string, debuggingFlag *bool) (*AppConfig, error) { func NewAppConfig(name, version, commit, date string, buildSource string, debuggingFlag *bool) (*AppConfig, error) {
userConfig, err := LoadUserConfig() defaultConfig := getDefaultConfig()
userConfig, err := LoadConfig("config", defaultConfig)
if err != nil { if err != nil {
panic(err) return nil, err
} }
appConfig := &AppConfig{ appConfig := &AppConfig{
Name: "lazygit", Name: "lazygit",
Version: version, Version: version,
Commit: commit, Commit: commit,
BuildDate: date, BuildDate: date,
Debug: *debuggingFlag, Debug: *debuggingFlag,
UserConfig: userConfig, BuildSource: buildSource,
UserConfig: userConfig,
AppState: &AppState{},
} }
if err := appConfig.LoadAppState(); err != nil {
return nil, err
}
return appConfig, nil return appConfig, nil
} }
@ -72,73 +89,122 @@ func (c *AppConfig) GetName() string {
return c.Name 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 // GetUserConfig returns the user config
func (c *AppConfig) GetUserConfig() *viper.Viper { func (c *AppConfig) GetUserConfig() *viper.Viper {
return c.UserConfig 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 := viper.New()
v.SetConfigType("yaml") v.SetConfigType("yaml")
v.SetConfigName("config") v.SetConfigName(filename)
return v, nil return v, nil
} }
// LoadUserConfig gets the user's config // LoadConfig gets the user's config
func LoadUserConfig() (*viper.Viper, error) { func LoadConfig(filename string, defaults []byte) (*viper.Viper, error) {
v, err := newViper() v, err := newViper(filename)
if err != nil { if err != nil {
panic(err)
}
if err = LoadDefaultConfig(v); err != nil {
return nil, err 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 nil, err
} }
return v, nil return v, nil
} }
// LoadDefaultConfig loads in the defaults defined in this file // LoadDefaults loads in the defaults defined in this file
func LoadDefaultConfig(v *viper.Viper) error { func LoadDefaults(v *viper.Viper, defaults []byte) error {
defaults := getDefaultConfig()
return v.ReadConfig(bytes.NewBuffer(defaults)) return v.ReadConfig(bytes.NewBuffer(defaults))
} }
// LoadUserConfigFromFile Loads the user config from their config file, creating func prepareConfigFile(filename string) (string, error) {
// the file as an empty config if it does not exist
func LoadUserConfigFromFile(v *viper.Viper) error {
// chucking my name there is not for vanity purposes, the xdg spec (and that // 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 // function) requires a vendor name. May as well line up with github
configDirs := configdir.New("jesseduffield", "lazygit") configDirs := configdir.New("jesseduffield", "lazygit")
folder := configDirs.QueryFolderContainsFile("config.yml") folder := configDirs.QueryFolderContainsFile(filename)
if folder == nil { if folder == nil {
// create the file as an empty config and load it // create the file as empty
folders := configDirs.QueryFolders(configdir.Global) folders := configDirs.QueryFolders(configdir.Global)
if err := folders[0].WriteFile("config.yml", []byte{}); err != nil { if err := folders[0].WriteFile(filename, []byte{}); err != nil {
return err return "", err
} }
folder = configDirs.QueryFolderContainsFile("config.yml") folder = configDirs.QueryFolderContainsFile(filename)
} }
v.AddConfigPath(folder.Path) return filepath.Join(folder.Path, filename), nil
return v.MergeInConfig()
} }
// InsertToUserConfig adds a key/value pair to the user's config and saves it // LoadAndMergeFile Loads the config/state file, creating
func (c *AppConfig) InsertToUserConfig(key, value string) error { // the file as an empty one if it does not exist
// making a new viper object so that we're not writing any defaults back func LoadAndMergeFile(v *viper.Viper, filename string) error {
// to the user's config file configPath, err := prepareConfigFile(filename)
v, err := newViper()
if err != nil { if err != nil {
return err 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 return err
} }
v.Set(key, value) v.Set(key, value)
return v.WriteConfig() 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 { func getDefaultConfig() []byte {
return []byte(` return []byte(`
gui: gui:
@ -156,7 +222,23 @@ func getDefaultConfig() []byte {
# stuff relating to git # stuff relating to git
os: os:
# stuff relating to the 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: []
`) `)
} }

View file

@ -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
}

View file

@ -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 { func (gui *Gui) createPromptPanel(g *gocui.Gui, currentView *gocui.View, title string, handleConfirm func(*gocui.Gui, *gocui.View) error) error {
gui.onNewPopupPanel() gui.onNewPopupPanel()
// only need to fit one line confirmationView, err := gui.prepareConfirmationPanel(currentView, title, "")
x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(g, "") if err != nil {
if confirmationView, err := g.SetView("confirmation", x0, y0, x1, y1, 0); err != nil { return err
if err != gocui.ErrUnknownView { }
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.Title = title
confirmationView.FgColor = gocui.ColorWhite 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() { func (gui *Gui) onNewPopupPanel() {
@ -93,18 +103,15 @@ func (gui *Gui) createConfirmationPanel(g *gocui.Gui, currentView *gocui.View, t
gui.Log.Error(errMessage) gui.Log.Error(errMessage)
} }
} }
x0, y0, x1, y1 := gui.getConfirmationPanelDimensions(g, prompt) confirmationView, err := gui.prepareConfirmationPanel(currentView, title, prompt)
if confirmationView, err := g.SetView("confirmation", x0, y0, x1, y1, 0); err != nil { if err != nil {
if err != gocui.ErrUnknownView { return err
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)
} }
return nil confirmationView.Editable = false
if err := gui.renderString(g, "confirmation", prompt); err != nil {
return err
}
return gui.setKeyBindings(g, handleConfirm, handleClose)
}) })
return nil return nil
} }
@ -131,7 +138,9 @@ func (gui *Gui) setKeyBindings(g *gocui.Gui, handleConfirm, handleClose func(*go
"keyBindConfirm": "enter", "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 { if err := g.SetKeybinding("confirmation", gocui.KeyEnter, gocui.ModNone, gui.wrappedConfirmationFunction(handleConfirm)); err != nil {
return err return err
} }

View file

@ -21,6 +21,7 @@ import (
"github.com/jesseduffield/lazygit/pkg/config" "github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/i18n" "github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/jesseduffield/lazygit/pkg/updates" "github.com/jesseduffield/lazygit/pkg/updates"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
@ -56,16 +57,17 @@ type Teml i18n.Teml
// Gui wraps the gocui Gui object which handles rendering and events // Gui wraps the gocui Gui object which handles rendering and events
type Gui struct { type Gui struct {
g *gocui.Gui g *gocui.Gui
Log *logrus.Logger Log *logrus.Logger
GitCommand *commands.GitCommand GitCommand *commands.GitCommand
OSCommand *commands.OSCommand OSCommand *commands.OSCommand
SubProcess *exec.Cmd SubProcess *exec.Cmd
State guiState State guiState
Config config.AppConfigurer Config config.AppConfigurer
Tr *i18n.Localizer Tr *i18n.Localizer
Errors SentinelErrors Errors SentinelErrors
Updater *updates.Updater Updater *updates.Updater
statusManager *statusManager
} }
type guiState struct { type guiState struct {
@ -98,13 +100,14 @@ func NewGui(log *logrus.Logger, gitCommand *commands.GitCommand, oSCommand *comm
} }
gui := &Gui{ gui := &Gui{
Log: log, Log: log,
GitCommand: gitCommand, GitCommand: gitCommand,
OSCommand: oSCommand, OSCommand: oSCommand,
State: initialState, State: initialState,
Config: config, Config: config,
Tr: tr, Tr: tr,
Updater: updater, Updater: updater,
statusManager: &statusManager{},
} }
gui.GenerateSentinelErrors() gui.GenerateSentinelErrors()
@ -141,13 +144,6 @@ func max(a, b int) int {
return b 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 // layout is called for every screen re-render e.g. when the screen is resized
func (gui *Gui) layout(g *gocui.Gui) error { func (gui *Gui) layout(g *gocui.Gui) error {
g.Highlight = true g.Highlight = true
@ -162,10 +158,10 @@ func (gui *Gui) layout(g *gocui.Gui) error {
minimumHeight := 16 minimumHeight := 16
minimumWidth := 10 minimumWidth := 10
appStatusView, _ := g.View("appStatus") appStatus := gui.statusManager.getStatusString()
appStatusOptionsBoundary := -2 appStatusOptionsBoundary := 0
if appStatusView != nil && len(appStatusView.Buffer()) > 2 { if appStatus != "" {
appStatusOptionsBoundary = len(appStatusView.Buffer()) + 2 appStatusOptionsBoundary = len(appStatus) + 2
} }
panelSpacing := 1 panelSpacing := 1
@ -257,7 +253,7 @@ func (gui *Gui) layout(g *gocui.Gui) error {
if gui.getCommitMessageView(g) == nil { if gui.getCommitMessageView(g) == nil {
// doesn't matter where this view starts because it will be hidden // 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 { if err != gocui.ErrUnknownView {
return err return err
} }
@ -268,18 +264,14 @@ func (gui *Gui) layout(g *gocui.Gui) error {
} }
} }
appStatusLeft := -1 if appStatusView, err := g.SetView("appStatus", -1, optionsTop, width, optionsTop+2, 0); err != nil {
if appStatusOptionsBoundary < 2 {
appStatusLeft = -5
}
if v, err := g.SetView("appStatus", appStatusLeft, optionsTop, appStatusOptionsBoundary, optionsTop+2, 0); err != nil {
if err != gocui.ErrUnknownView { if err != gocui.ErrUnknownView {
return err return err
} }
v.BgColor = gocui.ColorDefault appStatusView.BgColor = gocui.ColorDefault
v.FgColor = gocui.ColorCyan appStatusView.FgColor = gocui.ColorCyan
v.Frame = false appStatusView.Frame = false
g.SetViewOnBottom("appStatus")
} }
if v, err := g.SetView("version", optionsVersionBoundary-1, optionsTop, width, optionsTop+2, 0); err != nil { 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 return err
} }
// gui.Updater.CheckForNewUpdate(gui.onUpdateCheckFinish) // 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)
// these are only called once gui.Updater.CheckForNewUpdate(gui.onUpdateCheckFinish, false)
gui.handleFileSelect(g, filesView) gui.handleFileSelect(g, filesView)
gui.refreshFiles(g) gui.refreshFiles(g)
gui.refreshBranches(g) gui.refreshBranches(g)
@ -313,32 +305,42 @@ func (gui *Gui) layout(g *gocui.Gui) error {
func (gui *Gui) onUpdateFinish(err error) error { func (gui *Gui) onUpdateFinish(err error) error {
gui.State.Updating = false gui.State.Updating = false
gui.setAppStatus("") gui.statusManager.removeStatus("updating")
if err != nil { if err := gui.renderString(gui.g, "appStatus", ""); err != nil {
gui.createErrorPanel(gui.g, "Update failed: "+err.Error()) 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 return nil
} }
func (gui *Gui) onUpdateCheckFinish(newVersion string, err error) error { func (gui *Gui) onUpdateCheckFinish(newVersion string, err error) error {
newVersion = "v0.1.72"
if err != nil { 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 return nil
} }
if newVersion == "" { if newVersion == "" {
return nil return nil
} }
if gui.Config.GetUserConfig().Get("update.method") == "background" {
gui.startUpdating(newVersion)
return nil
}
title := "New version available!" title := "New version available!"
message := "Download latest version? (enter/esc)" message := "Download latest version? (enter/esc)"
// TODO: support nil view in createConfirmationPanel or always go back to filesPanel when there is no previous view currentView := gui.g.CurrentView()
return gui.createConfirmationPanel(gui.g, nil, title, message, func(g *gocui.Gui, v *gocui.View) error { return gui.createConfirmationPanel(gui.g, currentView, title, message, func(g *gocui.Gui, v *gocui.View) error {
gui.State.Updating = true gui.startUpdating(newVersion)
gui.setAppStatus("updating...")
gui.Updater.Update(newVersion, gui.onUpdateFinish)
return nil 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 { 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 { func (gui *Gui) updateLoader(g *gocui.Gui) error {
viewNames := []string{"confirmation", "appStatus"} if view, _ := g.View("confirmation"); view != nil {
for _, viewName := range viewNames { content := gui.trimmedContent(view)
if view, _ := g.View(viewName); view != nil { if strings.Contains(content, "...") {
content := gui.trimmedContent(view) staticContent := strings.Split(content, "...")[0] + "..."
if strings.Contains(content, "...") { if err := gui.renderString(g, "confirmation", staticContent+" "+utils.Loader()); err != nil {
staticContent := strings.Split(content, "...")[0] + "..." return err
if viewName == "appStatus" {
// panic(staticContent + " " + gui.loader())
}
gui.renderString(g, viewName, staticContent+" "+gui.loader())
} }
} }
} }
return nil 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) { func (gui *Gui) goEvery(g *gocui.Gui, interval time.Duration, function func(*gocui.Gui) error) {
go func() { go func() {
for range time.Tick(interval) { 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*60, gui.fetch)
gui.goEvery(g, time.Second*10, gui.refreshFiles) 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) g.SetManagerFunc(gui.layout)

View file

@ -26,6 +26,7 @@ func (gui *Gui) keybindings(g *gocui.Gui) error {
{ViewName: "", Key: 'R', Modifier: gocui.ModNone, Handler: gui.handleRefresh}, {ViewName: "", Key: 'R', Modifier: gocui.ModNone, Handler: gui.handleRefresh},
{ViewName: "status", Key: 'e', Modifier: gocui.ModNone, Handler: gui.handleEditConfig}, {ViewName: "status", Key: 'e', Modifier: gocui.ModNone, Handler: gui.handleEditConfig},
{ViewName: "status", Key: 'o', Modifier: gocui.ModNone, Handler: gui.handleOpenConfig}, {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.handleCommitPress},
{ViewName: "files", Key: 'C', Modifier: gocui.ModNone, Handler: gui.handleCommitEditorPress}, {ViewName: "files", Key: 'C', Modifier: gocui.ModNone, Handler: gui.handleCommitEditorPress},
{ViewName: "files", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: gui.handleFilePress}, {ViewName: "files", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: gui.handleFilePress},

View file

@ -45,9 +45,15 @@ func (gui *Gui) renderStatusOptions(g *gocui.Gui) error {
return gui.renderOptionsMap(g, map[string]string{ return gui.renderOptionsMap(g, map[string]string{
"o": gui.Tr.SLocalize("OpenConfig"), "o": gui.Tr.SLocalize("OpenConfig"),
"e": gui.Tr.SLocalize("EditConfig"), "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 { func (gui *Gui) handleStatusSelect(g *gocui.Gui, v *gocui.View) error {
dashboardString := fmt.Sprintf( dashboardString := fmt.Sprintf(
"%s\n\n%s\n\n%s\n\n%s\n\n%s", "%s\n\n%s\n\n%s\n\n%s\n\n%s",

View file

@ -4,7 +4,6 @@ import (
"fmt" "fmt"
"sort" "sort"
"strings" "strings"
"time"
"github.com/jesseduffield/gocui" "github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/utils" "github.com/jesseduffield/lazygit/pkg/utils"
@ -222,9 +221,6 @@ func (gui *Gui) renderString(g *gocui.Gui, viewName, s string) error {
if err != nil { if err != nil {
return nil return nil
} }
if viewName == "appStatus" {
gui.Log.Info(s)
}
v.Clear() v.Clear()
output := string(bom.Clean([]byte(s))) output := string(bom.Clean([]byte(s)))
output = utils.NormalizeLinefeeds(output) 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)) 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 // TODO: refactor properly
func (gui *Gui) getFilesView(g *gocui.Gui) *gocui.View { func (gui *Gui) getFilesView(g *gocui.Gui) *gocui.View {
v, _ := g.View("files") v, _ := g.View("files")

View file

@ -312,6 +312,9 @@ func addEnglish(i18nObject *i18n.Bundle) error {
}, &i18n.Message{ }, &i18n.Message{
ID: "ForcePushPrompt", ID: "ForcePushPrompt",
Other: "Your branch has diverged from the remote branch. Press 'esc' to cancel, or 'enter' to force push.", 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",
}, },
) )
} }

View file

@ -9,6 +9,7 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"runtime" "runtime"
"strings"
"time" "time"
"github.com/kardianos/osext" "github.com/kardianos/osext"
@ -21,10 +22,9 @@ import (
// Update checks for updates and does updates // Update checks for updates and does updates
type Updater struct { type Updater struct {
LastChecked string Log *logrus.Entry
Log *logrus.Entry Config config.AppConfigurer
Config config.AppConfigurer OSCommand *commands.OSCommand
OSCommand *commands.OSCommand
} }
// Updater implements the check and update methods // 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") contextLogger := log.WithField("context", "updates")
updater := &Updater{ updater := &Updater{
LastChecked: "today", Log: contextLogger,
Log: contextLogger, Config: config,
Config: config, OSCommand: osCommand,
OSCommand: osCommand,
} }
return updater, nil return updater, nil
} }
func (u *Updater) getLatestVersionNumber() (string, error) { func (u *Updater) getLatestVersionNumber() (string, error) {
time.Sleep(5)
req, err := http.NewRequest("GET", projectUrl+"/releases/latest", nil) req, err := http.NewRequest("GET", projectUrl+"/releases/latest", nil)
if err != nil { if err != nil {
return "", err return "", err
@ -76,21 +74,41 @@ func (u *Updater) getLatestVersionNumber() (string, error) {
return dat["tag_name"].(string), nil 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) { func (u *Updater) checkForNewUpdate() (string, error) {
u.Log.Info("Checking for an updated version") u.Log.Info("Checking for an updated version")
// if u.Config.GetVersion() == "unversioned" { 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") u.Log.Info("Current version is not built from an official release so we won't check for an update")
// return "", nil return "", nil
// } }
newVersion, err := u.getLatestVersionNumber() newVersion, err := u.getLatestVersionNumber()
if err != nil { if err != nil {
return "", err return "", err
} }
u.Log.Info("Current version is " + u.Config.GetVersion()) u.Log.Info("Current version is " + u.Config.GetVersion())
u.Log.Info("New version is " + newVersion) 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) rawUrl, err := u.getBinaryUrl(newVersion)
if err != nil { if err != nil {
@ -102,11 +120,16 @@ func (u *Updater) checkForNewUpdate() (string, error) {
return "", nil return "", nil
} }
u.Log.Info("Verified resource is available, ready to update") u.Log.Info("Verified resource is available, ready to update")
return newVersion, nil return newVersion, nil
} }
// CheckForNewUpdate checks if there is an available update // 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() { go func() {
newVersion, err := u.checkForNewUpdate() newVersion, err := u.checkForNewUpdate()
if err = onFinish(newVersion, err); err != nil { 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 { func (u *Updater) mappedOs(os string) string {
osMap := map[string]string{ osMap := map[string]string{
"darwin": "Darwin", "darwin": "Darwin",

View file

@ -6,6 +6,7 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"time"
"github.com/fatih/color" "github.com/fatih/color"
) )
@ -81,3 +82,11 @@ func GetProjectRoot() string {
} }
return strings.Split(dir, "lazygit")[0] + "lazygit" 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]
}