From d938a437a2a98370cc215da88b0195bf1d499dc6 Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Sun, 19 Aug 2018 23:28:29 +1000 Subject: [PATCH] WIP auto updates --- pkg/app/app.go | 8 +- pkg/gui/gui.go | 21 ++++- pkg/updates/updates.go | 186 +++++++++++++++++++++++++++++++++++++++-- 3 files changed, 205 insertions(+), 10 deletions(-) diff --git a/pkg/app/app.go b/pkg/app/app.go index aaa925e53..e1a26e13d 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -10,6 +10,7 @@ import ( "github.com/jesseduffield/lazygit/pkg/config" "github.com/jesseduffield/lazygit/pkg/gui" "github.com/jesseduffield/lazygit/pkg/i18n" + "github.com/jesseduffield/lazygit/pkg/updates" ) // App struct @@ -22,6 +23,7 @@ type App struct { GitCommand *commands.GitCommand Gui *gui.Gui Tr *i18n.Localizer + Updater *updates.Updater // may only need this on the Gui } func newLogger(config config.AppConfigurer) *logrus.Logger { @@ -60,7 +62,11 @@ func NewApp(config config.AppConfigurer) (*App, error) { if err != nil { return app, err } - app.Gui, err = gui.NewGui(app.Log, app.GitCommand, app.OSCommand, app.Tr, config) + app.Updater, err = updates.NewUpdater(app.Log, config, app.OSCommand) + if err != nil { + return app, err + } + app.Gui, err = gui.NewGui(app.Log, app.GitCommand, app.OSCommand, app.Tr, config, app.Updater) if err != nil { return app, err } diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index 1e7b6156b..d1b41e4d0 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -21,6 +21,7 @@ import ( "github.com/jesseduffield/lazygit/pkg/commands" "github.com/jesseduffield/lazygit/pkg/config" "github.com/jesseduffield/lazygit/pkg/i18n" + "github.com/jesseduffield/lazygit/pkg/updates" ) // OverlappingEdges determines if panel edges overlap @@ -64,6 +65,7 @@ type Gui struct { Config config.AppConfigurer Tr *i18n.Localizer Errors SentinelErrors + Updater *updates.Updater } type guiState struct { @@ -81,7 +83,7 @@ type guiState struct { } // NewGui builds a new gui handler -func NewGui(log *logrus.Logger, gitCommand *commands.GitCommand, oSCommand *commands.OSCommand, tr *i18n.Localizer, config config.AppConfigurer) (*Gui, error) { +func NewGui(log *logrus.Logger, gitCommand *commands.GitCommand, oSCommand *commands.OSCommand, tr *i18n.Localizer, config config.AppConfigurer, updater *updates.Updater) (*Gui, error) { initialState := guiState{ Files: make([]commands.File, 0), PreviousView: "files", @@ -101,6 +103,7 @@ func NewGui(log *logrus.Logger, gitCommand *commands.GitCommand, oSCommand *comm State: initialState, Config: config, Tr: tr, + Updater: updater, } gui.GenerateSentinelErrors() @@ -261,6 +264,19 @@ func (gui *Gui) layout(g *gocui.Gui) error { return err } + newVersion, err := gui.Updater.CheckForNewUpdate() + if err != nil { + return err + } + gui.Updater.NewVersion = "v0.1.75" + newVersion = "v0.1.75" + if newVersion != "" { + if err := gui.Updater.Update(); err != nil { + panic(err) + return err + } + } + // these are only called once gui.handleFileSelect(g, filesView) gui.refreshFiles(g) @@ -318,6 +334,9 @@ func (gui *Gui) Run() error { } defer g.Close() + // TODO: do this more elegantly + gui.Updater.CheckForNewUpdate() + gui.g = g // TODO: always use gui.g rather than passing g around everywhere if err := gui.SetColorScheme(); err != nil { diff --git a/pkg/updates/updates.go b/pkg/updates/updates.go index 77d491119..3599808fb 100644 --- a/pkg/updates/updates.go +++ b/pkg/updates/updates.go @@ -1,26 +1,196 @@ package updates +import ( + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net/http" + "net/url" + "os" + "path/filepath" + "runtime" + + "github.com/kardianos/osext" + + "github.com/Sirupsen/logrus" + getter "github.com/hashicorp/go-getter" + "github.com/jesseduffield/lazygit/pkg/commands" + "github.com/jesseduffield/lazygit/pkg/config" +) + // Update checks for updates and does updates -type Update struct { +type Updater struct { LastChecked string + Log *logrus.Logger + Config config.AppConfigurer + NewVersion string + OSCommand *commands.OSCommand } // Updater implements the check and update methods -type Updater interface { - Check() +type Updaterer interface { + CheckForNewUpdate() Update() } +var ( + projectUrl = "https://github.com/jesseduffield/lazygit" +) + // NewUpdater creates a new updater -func NewUpdater() *Update { +func NewUpdater(log *logrus.Logger, config config.AppConfigurer, osCommand *commands.OSCommand) (*Updater, error) { - update := &Update{ + updater := &Updater{ LastChecked: "today", + Log: log, + Config: config, + OSCommand: osCommand, } - return update + return updater, nil } -// Check checks if there is an available update -func (u *Update) Check() { +func (u *Updater) getLatestVersionNumber() (string, error) { + req, err := http.NewRequest("GET", projectUrl+"/releases/latest", nil) + if err != nil { + return "", err + } + req.Header.Set("Accept", "application/json") + resp, err := http.DefaultClient.Do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return "", err + } + + byt := []byte(body) + var dat map[string]interface{} + if err := json.Unmarshal(byt, &dat); err != nil { + return "", err + } + return dat["tag_name"].(string), nil +} + +// CheckForNewUpdate checks if there is an available update +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 + } + newVersion, err := u.getLatestVersionNumber() + if err != nil { + return "", err + } + u.NewVersion = newVersion + u.Log.Info("Current version is " + u.Config.GetVersion()) + u.Log.Info("New version is " + newVersion) + if newVersion == u.Config.GetVersion() { + return "", nil + } + // TODO: verify here that there is a binary available for this OS/arch + return newVersion, nil +} + +func (u *Updater) mappedOs(os string) string { + osMap := map[string]string{ + "darwin": "Darwin", + "linux": "Linux", + "windows": "Windows", + } + result, found := osMap[os] + if found { + return result + } + return os +} + +func (u *Updater) mappedArch(arch string) string { + archMap := map[string]string{ + "386": "32-bit", + "amd64": "x86_64", + } + result, found := archMap[arch] + if found { + return result + } + return arch +} + +// example: https://github.com/jesseduffield/lazygit/releases/download/v0.1.73/lazygit_0.1.73_Darwin_x86_64.tar.gz +func (u *Updater) getBinaryUrl() (string, error) { + if u.NewVersion == "" { + return "", errors.New("Must run CheckForUpdate() before running getBinaryUrl() to get the new version number") + } + extension := "tar.gz" + if runtime.GOOS == "windows" { + extension = "zip" + } + url := fmt.Sprintf( + "%s/releases/download/%s/lazygit_%s_%s_%s.%s", + projectUrl, + u.NewVersion, + u.NewVersion[1:], + u.mappedOs(runtime.GOOS), + u.mappedArch(runtime.GOARCH), + extension, + ) + u.Log.Info("url for latest release is " + url) + return url, nil +} + +func (u *Updater) Update() error { + rawUrl, err := u.getBinaryUrl() + if err != nil { + return err + } + return u.downloadAndInstall(rawUrl) +} + +func (u *Updater) downloadAndInstall(rawUrl string) error { + url, err := url.Parse(rawUrl) + if err != nil { + panic(err) + } + + g := new(getter.HttpGetter) + tempDir, err := ioutil.TempDir("", "lazygit") + if err != nil { + panic(err) + } + defer os.RemoveAll(tempDir) + + // Get it! + if err := g.Get(tempDir, url); err != nil { + panic(err) + } + + extension := "" + if runtime.GOOS == "windows" { + extension = ".exe" + } + + // Verify the main file exists + tempPath := filepath.Join(tempDir, "lazygit"+extension) + if _, err := os.Stat(tempPath); err != nil { + panic(err) + } + + // get the path of the current binary + execPath, err := osext.Executable() + if err != nil { + panic(err) + } + + // swap out the old binary for the new one + err = os.Rename(tempPath, execPath+"2") + if err != nil { + panic(err) + } + + return nil }