latest cdbcd03

_update.go RawHistoryFolder

1// the update command is used to update gordon's executable to the latest version
2
3// Heavily inspired by Pocketbase's update command
4// See: https://github.com/pocketbase/pocketbase/blob/master/plugins/ghupdate/ghupdate.go
5package cmd
6
7// import (
8// 	"context"
9// 	"encoding/json"
10// 	"errors"
11// 	"fmt"
12// 	"io"
13// 	"log"
14// 	"net/http"
15// 	"os"
16// 	"path/filepath"
17// 	"runtime"
18// 	"strconv"
19// 	"strings"
20
21// 	"github.com/AlecAivazis/survey/v2"
22// 	"github.com/fatih/color"
23// 	"github.com/pocketbase/pocketbase/core"
24// 	"github.com/pocketbase/pocketbase/tools/archive"
25// 	"github.com/spf13/cobra"
26// )
27
28// // HttpClient is a base HTTP client interface (usually used for test purposes).
29// type HttpClient interface {
30// 	Do(req *http.Request) (*http.Response, error)
31// }
32
33// // Config defines the config options of the ghupdate plugin.
34// //
35// // NB! This plugin is considered experimental and its config options may change in the future.
36// type Config struct {
37// 	// Owner specifies the account owner of the repository (default to "pocketbase").
38// 	Owner string
39
40// 	// Repo specifies the name of the repository (default to "pocketbase").
41// 	Repo string
42
43// 	// ArchiveExecutable specifies the name of the executable file in the release archive
44// 	// (default to "pocketbase"; an additional ".exe" check is also performed as a fallback).
45// 	ArchiveExecutable string
46
47// 	// Optional context to use when fetching and downloading the latest release.
48// 	Context context.Context
49
50// 	// The HTTP client to use when fetching and downloading the latest release.
51// 	// Defaults to `http.DefaultClient`.
52// 	HttpClient HttpClient
53// }
54
55// // MustRegister registers the ghupdate plugin to the provided app instance
56// // and panic if it fails.
57// func MustRegister(app core.App, rootCmd *cobra.Command, config Config) {
58// 	if err := Register(app, rootCmd, config); err != nil {
59// 		panic(err)
60// 	}
61// }
62
63// // Register registers the ghupdate plugin to the provided app instance.
64// func Register(app core.App, rootCmd *cobra.Command, config Config) error {
65// 	p := &plugin{
66// 		app:            app,
67// 		currentVersion: rootCmd.Version,
68// 		config:         config,
69// 	}
70
71// 	if p.config.Owner == "" {
72// 		p.config.Owner = "pocketbase"
73// 	}
74
75// 	if p.config.Repo == "" {
76// 		p.config.Repo = "pocketbase"
77// 	}
78
79// 	if p.config.ArchiveExecutable == "" {
80// 		p.config.ArchiveExecutable = "pocketbase"
81// 	}
82
83// 	if p.config.HttpClient == nil {
84// 		p.config.HttpClient = http.DefaultClient
85// 	}
86
87// 	if p.config.Context == nil {
88// 		p.config.Context = context.Background()
89// 	}
90
91// 	rootCmd.AddCommand(p.updateCmd())
92
93// 	return nil
94// }
95
96// type plugin struct {
97// 	app            core.App
98// 	currentVersion string
99// 	config         Config
100// }
101
102// func (p *plugin) updateCmd() *cobra.Command {
103// 	var withBackup bool
104
105// 	command := &cobra.Command{
106// 		Use:   "update",
107// 		Short: "Automatically updates the current PocketBase executable with the latest available version",
108// 		// @todo remove after logs generalization
109// 		// prevents printing the error log twice
110// 		SilenceErrors: true,
111// 		SilenceUsage:  true,
112// 		RunE: func(command *cobra.Command, args []string) error {
113// 			var needConfirm bool
114// 			if isMaybeRunningInDocker() {
115// 				needConfirm = true
116// 				color.Yellow("NB! It seems that you are in a Docker container.")
117// 				color.Yellow("The update command may not work as expected in this context because usually the version of the app is managed by the container image itself.")
118// 			} else if isMaybeRunningInNixOS() {
119// 				needConfirm = true
120// 				color.Yellow("NB! It seems that you are in a NixOS.")
121// 				color.Yellow("Due to the non-standard filesystem implementation of the environment, the update command may not work as expected.")
122// 			}
123
124// 			if needConfirm {
125// 				confirm := false
126// 				prompt := &survey.Confirm{
127// 					Message: "Do you want to proceed with the update?",
128// 				}
129// 				survey.AskOne(prompt, &confirm)
130// 				if !confirm {
131// 					fmt.Println("The command has been cancelled.")
132// 					return nil
133// 				}
134// 			}
135
136// 			return p.update(withBackup)
137// 		},
138// 	}
139
140// 	command.PersistentFlags().BoolVar(
141// 		&withBackup,
142// 		"backup",
143// 		true,
144// 		"Creates a pb_data backup at the end of the update process",
145// 	)
146
147// 	return command
148// }
149
150// func (p *plugin) update(withBackup bool) error {
151// 	color.Yellow("Fetching release information...")
152
153// 	latest, err := fetchLatestRelease(
154// 		p.config.Context,
155// 		p.config.HttpClient,
156// 		p.config.Owner,
157// 		p.config.Repo,
158// 	)
159// 	if err != nil {
160// 		return err
161// 	}
162
163// 	if compareVersions(strings.TrimPrefix(p.currentVersion, "v"), strings.TrimPrefix(latest.Tag, "v")) <= 0 {
164// 		color.Green("You already have the latest PocketBase %s.", p.currentVersion)
165// 		return nil
166// 	}
167
168// 	suffix := archiveSuffix(runtime.GOOS, runtime.GOARCH)
169// 	if suffix == "" {
170// 		return errors.New("unsupported platform")
171// 	}
172
173// 	asset, err := latest.findAssetBySuffix(suffix)
174// 	if err != nil {
175// 		return err
176// 	}
177
178// 	releaseDir := filepath.Join(p.app.DataDir(), core.LocalTempDirName)
179// 	defer os.RemoveAll(releaseDir)
180
181// 	color.Yellow("Downloading %s...", asset.Name)
182
183// 	// download the release asset
184// 	assetZip := filepath.Join(releaseDir, asset.Name)
185// 	if err := downloadFile(p.config.Context, p.config.HttpClient, asset.DownloadUrl, assetZip); err != nil {
186// 		return err
187// 	}
188
189// 	color.Yellow("Extracting %s...", asset.Name)
190
191// 	extractDir := filepath.Join(releaseDir, "extracted_"+asset.Name)
192// 	defer os.RemoveAll(extractDir)
193
194// 	if err := archive.Extract(assetZip, extractDir); err != nil {
195// 		return err
196// 	}
197
198// 	color.Yellow("Replacing the executable...")
199
200// 	oldExec, err := os.Executable()
201// 	if err != nil {
202// 		return err
203// 	}
204// 	renamedOldExec := oldExec + ".old"
205// 	defer os.Remove(renamedOldExec)
206
207// 	newExec := filepath.Join(extractDir, p.config.ArchiveExecutable)
208// 	if _, err := os.Stat(newExec); err != nil {
209// 		// try again with an .exe extension
210// 		newExec = newExec + ".exe"
211// 		if _, fallbackErr := os.Stat(newExec); fallbackErr != nil {
212// 			return fmt.Errorf("The executable in the extracted path is missing or it is inaccessible: %v, %v", err, fallbackErr)
213// 		}
214// 	}
215
216// 	// rename the current executable
217// 	if err := os.Rename(oldExec, renamedOldExec); err != nil {
218// 		return fmt.Errorf("Failed to rename the current executable: %w", err)
219// 	}
220
221// 	tryToRevertExecChanges := func() {
222// 		if revertErr := os.Rename(renamedOldExec, oldExec); revertErr != nil && p.app.IsDebug() {
223// 			log.Println(revertErr)
224// 		}
225// 	}
226
227// 	// replace with the extracted binary
228// 	if err := os.Rename(newExec, oldExec); err != nil {
229// 		tryToRevertExecChanges()
230// 		return fmt.Errorf("Failed replacing the executable: %w", err)
231// 	}
232
233// 	if withBackup {
234// 		color.Yellow("Creating pb_data backup...")
235
236// 		backupName := fmt.Sprintf("@update_%s.zip", latest.Tag)
237// 		if err := p.app.CreateBackup(p.config.Context, backupName); err != nil {
238// 			tryToRevertExecChanges()
239// 			return err
240// 		}
241// 	}
242
243// 	color.HiBlack("---")
244// 	color.Green("Update completed successfully! You can start the executable as usual.")
245
246// 	return nil
247// }
248
249// func fetchLatestRelease(
250// 	ctx context.Context,
251// 	client HttpClient,
252// 	owner string,
253// 	repo string,
254// ) (*release, error) {
255// 	url := fmt.Sprintf("https://api.github.com/repos/%s/%s/releases/latest", owner, repo)
256
257// 	req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
258// 	if err != nil {
259// 		return nil, err
260// 	}
261
262// 	res, err := client.Do(req)
263// 	if err != nil {
264// 		return nil, err
265// 	}
266// 	defer res.Body.Close()
267
268// 	rawBody, err := io.ReadAll(res.Body)
269// 	if err != nil {
270// 		return nil, err
271// 	}
272
273// 	// http.Client doesn't treat non 2xx responses as error
274// 	if res.StatusCode >= 400 {
275// 		return nil, fmt.Errorf(
276// 			"(%d) failed to fetch latest releases:\n%s",
277// 			res.StatusCode,
278// 			string(rawBody),
279// 		)
280// 	}
281
282// 	result := &release{}
283// 	if err := json.Unmarshal(rawBody, result); err != nil {
284// 		return nil, err
285// 	}
286
287// 	return result, nil
288// }
289
290// func downloadFile(
291// 	ctx context.Context,
292// 	client HttpClient,
293// 	url string,
294// 	destPath string,
295// ) error {
296// 	req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
297// 	if err != nil {
298// 		return err
299// 	}
300
301// 	res, err := client.Do(req)
302// 	if err != nil {
303// 		return err
304// 	}
305// 	defer res.Body.Close()
306
307// 	// http.Client doesn't treat non 2xx responses as error
308// 	if res.StatusCode >= 400 {
309// 		return fmt.Errorf("(%d) failed to send download file request", res.StatusCode)
310// 	}
311
312// 	// ensure that the dest parent dir(s) exist
313// 	if err := os.MkdirAll(filepath.Dir(destPath), os.ModePerm); err != nil {
314// 		return err
315// 	}
316
317// 	dest, err := os.Create(destPath)
318// 	if err != nil {
319// 		return err
320// 	}
321// 	defer dest.Close()
322
323// 	if _, err := io.Copy(dest, res.Body); err != nil {
324// 		return err
325// 	}
326
327// 	return nil
328// }
329
330// func archiveSuffix(goos, goarch string) string {
331// 	switch goos {
332// 	case "linux":
333// 		switch goarch {
334// 		case "amd64":
335// 			return "_linux_amd64.zip"
336// 		case "arm64":
337// 			return "_linux_arm64.zip"
338// 		case "arm":
339// 			return "_linux_armv7.zip"
340// 		}
341// 	case "darwin":
342// 		switch goarch {
343// 		case "amd64":
344// 			return "_darwin_amd64.zip"
345// 		case "arm64":
346// 			return "_darwin_arm64.zip"
347// 		}
348// 	case "windows":
349// 		switch goarch {
350// 		case "amd64":
351// 			return "_windows_amd64.zip"
352// 		case "arm64":
353// 			return "_windows_arm64.zip"
354// 		}
355// 	}
356
357// 	return ""
358// }
359
360// func compareVersions(a, b string) int {
361// 	aSplit := strings.Split(a, ".")
362// 	aTotal := len(aSplit)
363
364// 	bSplit := strings.Split(b, ".")
365// 	bTotal := len(bSplit)
366
367// 	limit := aTotal
368// 	if bTotal > aTotal {
369// 		limit = bTotal
370// 	}
371
372// 	for i := 0; i < limit; i++ {
373// 		var x, y int
374
375// 		if i < aTotal {
376// 			x, _ = strconv.Atoi(aSplit[i])
377// 		}
378
379// 		if i < bTotal {
380// 			y, _ = strconv.Atoi(bSplit[i])
381// 		}
382
383// 		if x < y {
384// 			return 1 // b is newer
385// 		}
386
387// 		if x > y {
388// 			return -1 // a is newer
389// 		}
390// 	}
391
392// 	return 0 // equal
393// }
394
395// // note: not completely reliable as it may not work on all platforms
396// // but should at least provide a warning for the most common use cases
397// func isMaybeRunningInDocker() bool {
398// 	_, err := os.Stat("/.dockerenv")
399// 	return err == nil
400// }
401
402// // note: untested
403// func isMaybeRunningInNixOS() bool {
404// 	_, err := os.Stat("/etc/NIXOS")
405// 	return err == nil
406// }

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close