1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2024-12-04 10:34:55 +02:00
lazygit/pkg/updates/updates.go
Jesse Duffield 63dc07fded Construct arg vector manually rather than parse string
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
2023-05-23 19:49:19 +10:00

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
}