mirror of
https://github.com/jesseduffield/lazygit.git
synced 2024-12-14 11:23:09 +02:00
63dc07fded
By constructing an arg vector manually, we no longer need to quote arguments Mandate that args must be passed when building a command Now you need to provide an args array when building a command. There are a handful of places where we need to deal with a string, such as with user-defined custom commands, and for those we now require that at the callsite they use str.ToArgv to do that. I don't want to provide a method out of the box for it because I want to discourage its use. For some reason we were invoking a command through a shell when amending a commit, and I don't believe we needed to do that as there was nothing user- supplied about the command. So I've switched to using a regular command out- side the shell there
330 lines
8.0 KiB
Go
330 lines
8.0 KiB
Go
package updates
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/go-errors/errors"
|
|
|
|
"github.com/kardianos/osext"
|
|
|
|
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
|
"github.com/jesseduffield/lazygit/pkg/common"
|
|
"github.com/jesseduffield/lazygit/pkg/config"
|
|
"github.com/jesseduffield/lazygit/pkg/constants"
|
|
"github.com/jesseduffield/lazygit/pkg/utils"
|
|
)
|
|
|
|
// Updater checks for updates and does updates
|
|
type Updater struct {
|
|
*common.Common
|
|
Config config.AppConfigurer
|
|
OSCommand *oscommands.OSCommand
|
|
}
|
|
|
|
// Updaterer implements the check and update methods
|
|
type Updaterer interface {
|
|
CheckForNewUpdate()
|
|
Update()
|
|
}
|
|
|
|
// NewUpdater creates a new updater
|
|
func NewUpdater(cmn *common.Common, config config.AppConfigurer, osCommand *oscommands.OSCommand) (*Updater, error) {
|
|
return &Updater{
|
|
Common: cmn,
|
|
Config: config,
|
|
OSCommand: osCommand,
|
|
}, nil
|
|
}
|
|
|
|
func (u *Updater) getLatestVersionNumber() (string, error) {
|
|
req, err := http.NewRequest("GET", constants.Links.RepoUrl+"/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()
|
|
|
|
dec := json.NewDecoder(resp.Body)
|
|
data := struct {
|
|
TagName string `json:"tag_name"`
|
|
}{}
|
|
if err := dec.Decode(&data); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return data.TagName, nil
|
|
}
|
|
|
|
// RecordLastUpdateCheck records last time an update check was performed
|
|
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 {
|
|
if oldVersion == "unversioned" {
|
|
return false
|
|
}
|
|
oldVersion = strings.TrimPrefix(oldVersion, "v")
|
|
newVersion = strings.TrimPrefix(newVersion, "v")
|
|
return strings.Split(oldVersion, ".")[0] != strings.Split(newVersion, ".")[0]
|
|
}
|
|
|
|
func (u *Updater) currentVersion() string {
|
|
versionNumber := u.Config.GetVersion()
|
|
if versionNumber == "unversioned" {
|
|
return versionNumber
|
|
}
|
|
|
|
return fmt.Sprintf("v%s", u.Config.GetVersion())
|
|
}
|
|
|
|
func (u *Updater) checkForNewUpdate() (string, error) {
|
|
u.Log.Info("Checking for an updated version")
|
|
currentVersion := u.currentVersion()
|
|
if err := u.RecordLastUpdateCheck(); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
newVersion, err := u.getLatestVersionNumber()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
u.Log.Info("Current version is " + currentVersion)
|
|
u.Log.Info("New version is " + newVersion)
|
|
|
|
if newVersion == currentVersion {
|
|
return "", errors.New(u.Tr.OnLatestVersionErr)
|
|
}
|
|
|
|
if u.majorVersionDiffers(currentVersion, newVersion) {
|
|
errMessage := utils.ResolvePlaceholderString(
|
|
u.Tr.MajorVersionErr, map[string]string{
|
|
"newVersion": newVersion,
|
|
"currentVersion": currentVersion,
|
|
},
|
|
)
|
|
return "", errors.New(errMessage)
|
|
}
|
|
|
|
rawUrl := u.getBinaryUrl(newVersion)
|
|
|
|
u.Log.Info("Checking for resource at url " + rawUrl)
|
|
if !u.verifyResourceFound(rawUrl) {
|
|
errMessage := utils.ResolvePlaceholderString(
|
|
u.Tr.CouldNotFindBinaryErr, map[string]string{
|
|
"url": rawUrl,
|
|
},
|
|
)
|
|
|
|
return "", errors.New(errMessage)
|
|
}
|
|
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, userRequested bool) {
|
|
if !userRequested && u.skipUpdateCheck() {
|
|
return
|
|
}
|
|
|
|
newVersion, err := u.checkForNewUpdate()
|
|
if err = onFinish(newVersion, err); err != nil {
|
|
u.Log.Error(err)
|
|
}
|
|
}
|
|
|
|
func (u *Updater) skipUpdateCheck() bool {
|
|
// will remove the check for windows after adding a manifest file asking for
|
|
// the required permissions
|
|
if runtime.GOOS == "windows" {
|
|
u.Log.Info("Updating is currently not supported for windows until we can fix permission issues")
|
|
return true
|
|
}
|
|
|
|
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 true
|
|
}
|
|
|
|
if u.Config.GetBuildSource() != "buildBinary" {
|
|
u.Log.Info("Binary is not built with the buildBinary flag so we won't check for an update")
|
|
return true
|
|
}
|
|
|
|
userConfig := u.UserConfig
|
|
if userConfig.Update.Method == "never" {
|
|
u.Log.Info("Update method is set to never so we won't check for an update")
|
|
return true
|
|
}
|
|
|
|
currentTimestamp := time.Now().Unix()
|
|
lastUpdateCheck := u.Config.GetAppState().LastUpdateCheck
|
|
days := userConfig.Update.Days
|
|
|
|
if (currentTimestamp-lastUpdateCheck)/(60*60*24) < days {
|
|
u.Log.Info("Last update was too recent so we won't check for an update")
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
func (u *Updater) zipExtension() string {
|
|
if runtime.GOOS == "windows" {
|
|
return "zip"
|
|
}
|
|
|
|
return "tar.gz"
|
|
}
|
|
|
|
// example: https://github.com/jesseduffield/lazygit/releases/download/v0.1.73/lazygit_0.1.73_Darwin_x86_64.tar.gz
|
|
func (u *Updater) getBinaryUrl(newVersion string) string {
|
|
url := fmt.Sprintf(
|
|
"%s/releases/download/%s/lazygit_%s_%s_%s.%s",
|
|
constants.Links.RepoUrl,
|
|
newVersion,
|
|
newVersion[1:],
|
|
u.mappedOs(runtime.GOOS),
|
|
u.mappedArch(runtime.GOARCH),
|
|
u.zipExtension(),
|
|
)
|
|
u.Log.Info("Url for latest release is " + url)
|
|
return url
|
|
}
|
|
|
|
// Update downloads the latest binary and replaces the current binary with it
|
|
func (u *Updater) Update(newVersion string) error {
|
|
return u.update(newVersion)
|
|
}
|
|
|
|
func (u *Updater) update(newVersion string) error {
|
|
rawUrl := u.getBinaryUrl(newVersion)
|
|
u.Log.Info("Updating with url " + rawUrl)
|
|
return u.downloadAndInstall(rawUrl)
|
|
}
|
|
|
|
func (u *Updater) downloadAndInstall(rawUrl string) error {
|
|
configDir := u.Config.GetUserConfigDir()
|
|
u.Log.Info("Download directory is " + configDir)
|
|
|
|
zipPath := filepath.Join(configDir, "temp_lazygit."+u.zipExtension())
|
|
u.Log.Info("Temp path to tarball/zip file is " + zipPath)
|
|
|
|
// remove existing zip file
|
|
if err := os.RemoveAll(zipPath); err != nil && !os.IsNotExist(err) {
|
|
return err
|
|
}
|
|
|
|
// Create the zip file
|
|
out, err := os.Create(zipPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer out.Close()
|
|
|
|
// Get the data
|
|
resp, err := http.Get(rawUrl)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
// Check server response
|
|
if resp.StatusCode != http.StatusOK {
|
|
return fmt.Errorf("error while trying to download latest lazygit: %s", resp.Status)
|
|
}
|
|
|
|
// Write the body to file
|
|
_, err = io.Copy(out, resp.Body)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
u.Log.Info("untarring tarball/unzipping zip file")
|
|
err = u.OSCommand.Cmd.New([]string{"tar", "-zxf", zipPath, "lazygit"}).Run()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// the `tar` terminal cannot store things in a new location without permission
|
|
// so it creates it in the current directory. As such our path is fairly simple.
|
|
// You won't see it because it's gitignored.
|
|
tempLazygitFilePath := "lazygit"
|
|
|
|
u.Log.Infof("Path to temp binary is %s", tempLazygitFilePath)
|
|
|
|
// get the path of the current binary
|
|
binaryPath, err := osext.Executable()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
u.Log.Info("Binary path is " + binaryPath)
|
|
|
|
// Verify the main file exists
|
|
if _, err := os.Stat(zipPath); err != nil {
|
|
return err
|
|
}
|
|
|
|
// swap out the old binary for the new one
|
|
err = os.Rename(tempLazygitFilePath, binaryPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
u.Log.Info("Update complete!")
|
|
|
|
return nil
|
|
}
|
|
|
|
func (u *Updater) verifyResourceFound(rawUrl string) bool {
|
|
resp, err := http.Head(rawUrl)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
defer resp.Body.Close()
|
|
u.Log.Info("Received status code ", resp.StatusCode)
|
|
// OK (200) indicates that the resource is present.
|
|
return resp.StatusCode == http.StatusOK
|
|
}
|