diff --git a/v3/UNRELEASED_CHANGELOG.md b/v3/UNRELEASED_CHANGELOG.md index 2ebe4eb0b50..eaf430cba7f 100644 --- a/v3/UNRELEASED_CHANGELOG.md +++ b/v3/UNRELEASED_CHANGELOG.md @@ -20,6 +20,8 @@ After processing, the content will be moved to the main changelog and this file ## Changed +- Replace `github.com/go-git/go-git/v5` direct dependency with calls to the system `git` CLI (`internal/git` package). **Note: `git` must be installed on the system.** Graceful errors are returned when `git` is not found in `PATH`. + ## Fixed diff --git a/v3/go.mod b/v3/go.mod index 2c8297c1996..08fbe485009 100644 --- a/v3/go.mod +++ b/v3/go.mod @@ -13,7 +13,6 @@ require ( github.com/charmbracelet/huh v0.8.0 github.com/coder/websocket v1.8.14 github.com/ebitengine/purego v0.9.1 - github.com/go-git/go-git/v5 v5.19.1 github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e github.com/go-ole/go-ole v1.3.0 github.com/godbus/dbus/v5 v5.2.2 @@ -55,6 +54,7 @@ require ( github.com/clipperhouse/uax29/v2 v2.4.0 // indirect github.com/danieljoos/wincred v1.2.3 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect + github.com/go-git/go-git/v5 v5.19.1 // indirect github.com/google/uuid v1.6.0 // indirect github.com/konoui/go-qsort v0.1.0 // indirect github.com/lmittmann/tint v1.0.3 // indirect diff --git a/v3/internal/commands/init.go b/v3/internal/commands/init.go index 3f827d1b731..f95dd9d3891 100644 --- a/v3/internal/commands/init.go +++ b/v3/internal/commands/init.go @@ -9,14 +9,12 @@ import ( "regexp" "strings" - "github.com/go-git/go-git/v5/config" - "github.com/wailsapp/wails/v3/internal/defaults" - "github.com/wailsapp/wails/v3/internal/term" - - "github.com/go-git/go-git/v5" "github.com/pterm/pterm" + "github.com/wailsapp/wails/v3/internal/defaults" "github.com/wailsapp/wails/v3/internal/flags" + "github.com/wailsapp/wails/v3/internal/git" "github.com/wailsapp/wails/v3/internal/templates" + "github.com/wailsapp/wails/v3/internal/term" ) var DisableFooter bool @@ -70,32 +68,15 @@ func gitURLToModulePath(gitURL string) string { } func initGitRepository(projectDir string, gitURL string) error { - // Initialize repository - repo, err := git.PlainInit(projectDir, false) - if err != nil { + if err := git.Init(projectDir); err != nil { return fmt.Errorf("failed to initialize git repository: %w", err) } - - // Create remote - _, err = repo.CreateRemote(&config.RemoteConfig{ - Name: "origin", - URLs: []string{gitURL}, - }) - if err != nil { + if err := git.RemoteAdd(projectDir, "origin", gitURL); err != nil { return fmt.Errorf("failed to create git remote: %w", err) } - - // Stage all files - worktree, err := repo.Worktree() - if err != nil { - return fmt.Errorf("failed to get git worktree: %w", err) - } - - _, err = worktree.Add(".") - if err != nil { + if err := git.AddAll(projectDir); err != nil { return fmt.Errorf("failed to stage files: %w", err) } - return nil } diff --git a/v3/internal/doctor/doctor.go b/v3/internal/doctor/doctor.go index 9d4d43415ee..88d7f4f7a30 100644 --- a/v3/internal/doctor/doctor.go +++ b/v3/internal/doctor/doctor.go @@ -16,8 +16,8 @@ import ( "github.com/wailsapp/wails/v3/internal/buildinfo" - "github.com/go-git/go-git/v5" "github.com/jaypipes/ghw" + "github.com/wailsapp/wails/v3/internal/git" "github.com/pterm/pterm" "github.com/wailsapp/wails/v3/internal/lo" "github.com/wailsapp/wails/v3/internal/operatingsystem" @@ -78,12 +78,8 @@ func Run() (err error) { if wailsPackage != nil && wailsPackage.Replace != nil { wailsVersion = "(local) => " + filepath.ToSlash(wailsPackage.Replace.Path) // Get the latest commit hash - repo, err := git.PlainOpen(filepath.Join(wailsPackage.Replace.Path, "..")) - if err == nil { - head, err := repo.Head() - if err == nil { - wailsVersion += " (" + head.Hash().String()[:8] + ")" - } + if hash, err := git.HeadHash(filepath.Join(wailsPackage.Replace.Path, "..")); err == nil { + wailsVersion += " (" + hash + ")" } } diff --git a/v3/internal/git/git.go b/v3/internal/git/git.go new file mode 100644 index 00000000000..e2b2b0b62ee --- /dev/null +++ b/v3/internal/git/git.go @@ -0,0 +1,73 @@ +package git + +import ( + "bytes" + "errors" + "fmt" + "os/exec" + "strings" +) + +// ErrNotInstalled is returned when git is not found in PATH. +var ErrNotInstalled = errors.New("git is not installed; please install git from https://git-scm.com") + +func isNotFound(err error) bool { + var execErr *exec.Error + return errors.As(err, &execErr) && errors.Is(execErr.Err, exec.ErrNotFound) +} + +func run(args ...string) error { + out, err := exec.Command("git", args...).CombinedOutput() + if err != nil { + if isNotFound(err) { + return ErrNotInstalled + } + return fmt.Errorf("git %s: %w\n%s", strings.Join(args, " "), err, bytes.TrimSpace(out)) + } + return nil +} + +func output(args ...string) (string, error) { + out, err := exec.Command("git", args...).CombinedOutput() + if err != nil { + if isNotFound(err) { + return "", ErrNotInstalled + } + return "", fmt.Errorf("git %s: %w\n%s", strings.Join(args, " "), err, bytes.TrimSpace(out)) + } + return strings.TrimSpace(string(out)), nil +} + +// HeadHash returns the short (8-character) commit hash of HEAD in dir. +func HeadHash(dir string) (string, error) { + hash, err := output("-C", dir, "rev-parse", "HEAD") + if err != nil { + return "", err + } + return hash[:8], nil +} + +// Clone clones url into dir. If tag is non-empty, checks out that tag or branch. +func Clone(url, dir, tag string) error { + args := []string{"clone", "--quiet"} + if tag != "" { + args = append(args, "--branch", tag) + } + args = append(args, url, dir) + return run(args...) +} + +// Init initializes a new git repository at dir. +func Init(dir string) error { + return run("-C", dir, "init", "--quiet") +} + +// RemoteAdd adds a named remote to the repository at dir. +func RemoteAdd(dir, name, url string) error { + return run("-C", dir, "remote", "add", name, url) +} + +// AddAll stages all files in the repository at dir. +func AddAll(dir string) error { + return run("-C", dir, "add", ".") +} diff --git a/v3/internal/git/git_test.go b/v3/internal/git/git_test.go new file mode 100644 index 00000000000..0917d3772f9 --- /dev/null +++ b/v3/internal/git/git_test.go @@ -0,0 +1,112 @@ +package git + +import ( + "errors" + "os" + "os/exec" + "path/filepath" + "testing" +) + +// makeRepo creates a temp git repository with one commit and a v1.0.0 tag. +func makeRepo(t *testing.T) string { + t.Helper() + dir := t.TempDir() + cmds := [][]string{ + {"-C", dir, "init", "--quiet"}, + {"-C", dir, "-c", "user.email=t@t.com", "-c", "user.name=T", "commit", "--allow-empty", "--quiet", "-m", "init"}, + {"-C", dir, "tag", "v1.0.0"}, + } + for _, args := range cmds { + if out, err := exec.Command("git", args...).CombinedOutput(); err != nil { + t.Fatalf("setup git %v: %v\n%s", args, err, out) + } + } + return dir +} + +func TestInit_Success(t *testing.T) { + if err := Init(t.TempDir()); err != nil { + t.Fatal(err) + } +} + +func TestInit_Error(t *testing.T) { + // git -C on a non-existent path fails + if err := Init("/nonexistent_wails_test_path_xyz"); err == nil { + t.Fatal("expected error for nonexistent path") + } +} + +func TestRemoteAdd_Success(t *testing.T) { + dir := t.TempDir() + if err := Init(dir); err != nil { + t.Fatal(err) + } + if err := RemoteAdd(dir, "origin", "https://example.com/repo.git"); err != nil { + t.Fatal(err) + } +} + +func TestAddAll_Success(t *testing.T) { + dir := t.TempDir() + if err := Init(dir); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(dir, "a.txt"), []byte("a"), 0644); err != nil { + t.Fatal(err) + } + if err := AddAll(dir); err != nil { + t.Fatal(err) + } +} + +func TestHeadHash_Success(t *testing.T) { + src := makeRepo(t) + hash, err := HeadHash(src) + if err != nil { + t.Fatal(err) + } + if len(hash) != 8 { + t.Errorf("expected 8-char hash, got %q (len %d)", hash, len(hash)) + } +} + +func TestHeadHash_Error(t *testing.T) { + // not a git repository + if _, err := HeadHash(t.TempDir()); err == nil { + t.Fatal("expected error for non-repo dir") + } +} + +func TestClone_WithoutTag(t *testing.T) { + src := makeRepo(t) + dst := filepath.Join(t.TempDir(), "clone") + if err := Clone(src, dst, ""); err != nil { + t.Fatal(err) + } +} + +func TestClone_WithTag(t *testing.T) { + src := makeRepo(t) + dst := filepath.Join(t.TempDir(), "clone") + if err := Clone(src, dst, "v1.0.0"); err != nil { + t.Fatal(err) + } +} + +func TestRun_NotInstalled(t *testing.T) { + t.Setenv("PATH", "/nonexistent_path_that_does_not_exist") + err := Init(t.TempDir()) + if !errors.Is(err, ErrNotInstalled) { + t.Fatalf("expected ErrNotInstalled, got %v", err) + } +} + +func TestOutput_NotInstalled(t *testing.T) { + t.Setenv("PATH", "/nonexistent_path_that_does_not_exist") + _, err := HeadHash(t.TempDir()) + if !errors.Is(err, ErrNotInstalled) { + t.Fatalf("expected ErrNotInstalled, got %v", err) + } +} diff --git a/v3/internal/templates/templates.go b/v3/internal/templates/templates.go index 9b007c10ee2..60091dc80e6 100644 --- a/v3/internal/templates/templates.go +++ b/v3/internal/templates/templates.go @@ -17,9 +17,8 @@ import ( "errors" - "github.com/go-git/go-git/v5" - "github.com/go-git/go-git/v5/plumbing" "github.com/pterm/pterm" + "github.com/wailsapp/wails/v3/internal/git" "github.com/wailsapp/wails/v3/internal/debug" "github.com/wailsapp/wails/v3/internal/flags" @@ -220,27 +219,20 @@ func parseTemplate(templateFS fs.FS, templateName string) (Template, error) { return result, nil } -// Clones the given uri and returns the temporary cloned directory +// gitclone clones uri into a temporary directory and returns its path. func gitclone(uri string) (string, error) { - // Create temporary directory dirname, err := os.MkdirTemp("", "wails-template-*") if err != nil { return "", err } - // Parse remote template url and version number - templateInfo := strings.Split(uri, "@") - cloneOption := &git.CloneOptions{ - URL: templateInfo[0], + parts := strings.SplitN(uri, "@", 2) + url, tag := parts[0], "" + if len(parts) > 1 { + tag = parts[1] } - if len(templateInfo) > 1 { - cloneOption.ReferenceName = plumbing.NewTagReferenceName(templateInfo[1]) - } - - _, err = git.PlainClone(dirname, false, cloneOption) - - return dirname, err + return dirname, git.Clone(url, dirname, tag) } func getRemoteTemplate(uri string) (*Template, error) { diff --git a/v3/pkg/application/application_debug.go b/v3/pkg/application/application_debug.go index 744ccaefba5..80868d76d08 100644 --- a/v3/pkg/application/application_debug.go +++ b/v3/pkg/application/application_debug.go @@ -3,7 +3,7 @@ package application import ( - "github.com/go-git/go-git/v5" + "github.com/wailsapp/wails/v3/internal/git" "github.com/wailsapp/wails/v3/internal/lo" "github.com/wailsapp/wails/v3/internal/version" "path/filepath" @@ -53,12 +53,8 @@ func (a *App) logStartup() { if wailsPackage != nil && wailsPackage.Replace != nil { wailsVersion = "(local) => " + filepath.ToSlash(wailsPackage.Replace.Path) // Get the latest commit hash - repo, err := git.PlainOpen(filepath.Join(wailsPackage.Replace.Path, "..")) - if err == nil { - head, err := repo.Head() - if err == nil { - wailsVersion += " (" + head.Hash().String()[:8] + ")" - } + if hash, err := git.HeadHash(filepath.Join(wailsPackage.Replace.Path, "..")); err == nil { + wailsVersion += " (" + hash + ")" } } args = append(args, "Wails", wailsVersion) diff --git a/v3/pkg/doctor-ng/doctor.go b/v3/pkg/doctor-ng/doctor.go index 26009914c9c..94ab3dd2147 100644 --- a/v3/pkg/doctor-ng/doctor.go +++ b/v3/pkg/doctor-ng/doctor.go @@ -6,7 +6,7 @@ import ( "runtime/debug" "strings" - "github.com/go-git/go-git/v5" + "github.com/wailsapp/wails/v3/internal/git" "github.com/wailsapp/wails/v3/internal/lo" "github.com/wailsapp/wails/v3/internal/operatingsystem" "github.com/wailsapp/wails/v3/internal/version" @@ -84,12 +84,8 @@ func (d *Doctor) collectBuildInfo() error { if found && wailsPackage != nil && wailsPackage.Replace != nil { wailsVersion = "(local) => " + filepath.ToSlash(wailsPackage.Replace.Path) - repo, err := git.PlainOpen(filepath.Join(wailsPackage.Replace.Path, "..")) - if err == nil { - head, err := repo.Head() - if err == nil { - wailsVersion += " (" + head.Hash().String()[:8] + ")" - } + if hash, err := git.HeadHash(filepath.Join(wailsPackage.Replace.Path, "..")); err == nil { + wailsVersion += " (" + hash + ")" } }