diff --git a/Makefile b/Makefile index 9f662665..afa5d16f 100644 --- a/Makefile +++ b/Makefile @@ -34,6 +34,7 @@ PACKAGES ?= $(shell $(GO) list ./... | grep -v '^gitea.dev/tea/tests') UNIT_PACKAGES ?= $(PACKAGES) INTEGRATION_PACKAGES ?= $(shell $(GO) list ./tests/... 2>/dev/null) INTEGRATION_TEST_TAGS ?= testtools +INTEGRATION_TEST_GOFLAGS ?= -v SOURCES ?= $(shell find . -name "*.go" -type f) # OS specific vars. @@ -103,7 +104,7 @@ unit-test: .PHONY: integration-test integration-test: @if [ -n "$(INTEGRATION_PACKAGES)" ]; then \ - $(GO) test -tags='$(INTEGRATION_TEST_TAGS)' $(INTEGRATION_PACKAGES); \ + $(GO) test $(INTEGRATION_TEST_GOFLAGS) -tags='$(INTEGRATION_TEST_TAGS)' $(INTEGRATION_PACKAGES); \ else \ echo "No integration test packages found"; \ fi diff --git a/cmd/api_test.go b/cmd/api_test.go index c355e339..44d03006 100644 --- a/cmd/api_test.go +++ b/cmd/api_test.go @@ -8,6 +8,7 @@ import ( "encoding/json" "io" "os" + "os/exec" "path/filepath" "testing" @@ -15,9 +16,6 @@ import ( "gitea.dev/tea/modules/context" tea_git "gitea.dev/tea/modules/git" - gogit "github.com/go-git/go-git/v5" - "github.com/go-git/go-git/v5/plumbing" - "github.com/go-git/go-git/v5/plumbing/object" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/urfave/cli/v3" @@ -572,33 +570,32 @@ func TestExpandPlaceholders(t *testing.T) { t.Run("replaces branch from local repo HEAD", func(t *testing.T) { tmpDir := t.TempDir() - repo, err := gogit.PlainInit(tmpDir, false) - require.NoError(t, err) + runGit := func(args ...string) { + cmd := exec.Command("git", args...) + cmd.Dir = tmpDir + require.NoError(t, cmd.Run()) + } + + runGit("init") + runGit("config", "user.email", "test@test.com") + runGit("config", "user.name", "test") // Create an initial commit so HEAD points to a branch. - wt, err := repo.Worktree() - require.NoError(t, err) tmpFile := filepath.Join(tmpDir, "init.txt") require.NoError(t, os.WriteFile(tmpFile, []byte("init"), 0o644)) - _, err = wt.Add("init.txt") - require.NoError(t, err) - _, err = wt.Commit("initial commit", &gogit.CommitOptions{ - Author: &object.Signature{Name: "test", Email: "test@test.com"}, - }) - require.NoError(t, err) + runGit("add", "init.txt") + runGit("commit", "-m", "initial commit") // Create and checkout a feature branch. - headRef, err := repo.Head() + runGit("checkout", "-b", "feature/my-branch") + + repo, err := tea_git.RepoFromPath(tmpDir) require.NoError(t, err) - branchRef := plumbing.NewBranchReferenceName("feature/my-branch") - ref := plumbing.NewHashReference(branchRef, headRef.Hash()) - require.NoError(t, repo.Storer.SetReference(ref)) - require.NoError(t, wt.Checkout(&gogit.CheckoutOptions{Branch: branchRef})) ctx := &context.TeaContext{ Owner: "alice", Repo: "proj", - LocalRepo: &tea_git.TeaRepo{Repository: repo}, + LocalRepo: repo, } result := expandPlaceholders("/repos/{owner}/{repo}/branches/{branch}", ctx) assert.Equal(t, "/repos/alice/proj/branches/feature/my-branch", result) diff --git a/go.mod b/go.mod index ffb866f8..29bdbd50 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,6 @@ require ( github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de github.com/enescakir/emoji v1.0.0 github.com/go-authgate/sdk-go v0.11.0 - github.com/go-git/go-git/v5 v5.19.1 github.com/muesli/termenv v0.16.0 github.com/olekukonko/tablewriter v1.1.4 github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 @@ -30,10 +29,7 @@ require ( require ( charm.land/bubbles/v2 v2.1.0 // indirect charm.land/bubbletea/v2 v2.0.6 // indirect - dario.cat/mergo v1.0.2 // indirect github.com/42wim/httpsig v1.2.4 // indirect - github.com/Microsoft/go-winio v0.6.2 // indirect - github.com/ProtonMail/go-crypto v1.4.1 // indirect github.com/alecthomas/chroma/v2 v2.24.1 // indirect github.com/atotto/clipboard v0.1.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect @@ -51,27 +47,20 @@ require ( github.com/charmbracelet/x/windows v0.2.2 // indirect github.com/clipperhouse/displaywidth v0.11.0 // indirect github.com/clipperhouse/uax29/v2 v2.7.0 // indirect - github.com/cloudflare/circl v1.6.3 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect - github.com/cyphar/filepath-securejoin v0.6.1 // indirect github.com/danieljoos/wincred v1.2.3 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/davidmz/go-pageant v1.0.2 // indirect github.com/dlclark/regexp2 v1.12.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect - github.com/emirpasic/gods v1.18.1 // indirect github.com/fatih/color v1.19.0 // indirect github.com/go-fed/httpsig v1.1.0 // indirect - github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect - github.com/go-git/go-billy/v5 v5.9.0 // indirect github.com/goccy/go-json v0.10.6 // indirect github.com/godbus/dbus/v5 v5.2.2 // indirect - github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect + github.com/google/go-cmp v0.7.0 // indirect github.com/gorilla/css v1.0.1 // indirect github.com/hashicorp/go-version v1.9.0 // indirect - github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect - github.com/kevinburke/ssh_config v1.6.0 // indirect - github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/kr/pretty v0.3.1 // indirect github.com/lucasb-eyer/go-colorful v1.4.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.22 // indirect @@ -82,22 +71,20 @@ require ( github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect github.com/olekukonko/errors v1.3.0 // indirect github.com/olekukonko/ll v0.1.8 // indirect - github.com/pjbgf/sha1cd v0.6.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect + github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect - github.com/sergi/go-diff v1.4.0 // indirect - github.com/skeema/knownhosts v1.3.2 // indirect - github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/yuin/goldmark v1.8.2 // indirect github.com/yuin/goldmark-emoji v1.0.6 // indirect github.com/zalando/go-keyring v0.2.8 // indirect + golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect golang.org/x/net v0.55.0 // indirect golang.org/x/sync v0.20.0 // indirect golang.org/x/text v0.37.0 // indirect golang.org/x/tools v0.45.0 // indirect - gopkg.in/warnings.v0 v0.1.2 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect ) retract v1.3.3 // accidental release, tag deleted diff --git a/go.sum b/go.sum index 89a92575..81687a2a 100644 --- a/go.sum +++ b/go.sum @@ -12,19 +12,12 @@ code.gitea.io/gitea-vet v0.2.3 h1:gdFmm6WOTM65rE8FUBTRzeQZYzXePKSSB1+r574hWwI= code.gitea.io/gitea-vet v0.2.3/go.mod h1:zcNbT/aJEmivCAhfmkHOlT645KNOf9W2KnkLgFjGGfE= code.gitea.io/sdk/gitea v0.25.1 h1:yywxWwoV+SdjHtbC6unBiXojWdZOtoHuGhEazEXeWuE= code.gitea.io/sdk/gitea v0.25.1/go.mod h1:uDFWYBU8dgZsgOHwe6C/6olxvf8FHguNB3wW1i83fgg= -dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= -dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= gitea.com/noerw/unidiff-comments v0.0.0-20220822113322-50f4daa0e35c h1:8fTkq2UaVkLHZCF+iB4wTxINmVAToe2geZGayk9LMbA= gitea.com/noerw/unidiff-comments v0.0.0-20220822113322-50f4daa0e35c/go.mod h1:Fc8iyPm4NINRWujeIk2bTfcbGc4ZYY29/oMAAGcr4qI= github.com/42wim/httpsig v1.2.4 h1:mI5bH0nm4xn7K18fo1K3okNDRq8CCJ0KbBYWyA6r8lU= github.com/42wim/httpsig v1.2.4/go.mod h1:yKsYfSyTBEohkPik224QPFylmzEBtda/kjyIAJjh3ps= github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= -github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= -github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= -github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= -github.com/ProtonMail/go-crypto v1.4.1 h1:9RfcZHqEQUvP8RzecWEUafnZVtEvrBVL9BiF67IQOfM= -github.com/ProtonMail/go-crypto v1.4.1/go.mod h1:e1OaTyu5SYVrO9gKOEhTc+5UcXtTUa+P3uLudwcgPqo= github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78= github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ= github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= @@ -33,12 +26,8 @@ github.com/alecthomas/chroma/v2 v2.24.1 h1:m5ffpfZbIb++k8AqFEKy9uVgY12xIQtBsQlc6 github.com/alecthomas/chroma/v2 v2.24.1/go.mod h1:l+ohZ9xRXIbGe7cIW+YZgOGbvuVLjMps/FYN/CwuabI= github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs= github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= -github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= -github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de h1:FxWPpzIjnTlhPwqqXc4/vE0f7GvRjuAsbW+HOIe8KnA= github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de/go.mod h1:DCaWoUhZrYW9p1lxo/cm8EmUOOzAPSEZNGF2DK1dJgw= -github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= -github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= @@ -81,14 +70,11 @@ github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSE github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0= github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= -github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= -github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= -github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE= -github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc= github.com/danieljoos/wincred v1.2.3 h1:v7dZC2x32Ut3nEfRH+vhoZGvN72+dQ/snVXo/vMFLdQ= github.com/danieljoos/wincred v1.2.3/go.mod h1:6qqX0WNrS4RzPZ1tnroDzq9kY3fu1KwE7MRLQK4X0bs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -100,34 +86,18 @@ github.com/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz github.com/dlclark/regexp2 v1.12.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= -github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= -github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= -github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/enescakir/emoji v1.0.0 h1:W+HsNql8swfCQFtioDGDHCHri8nudlK1n5p2rHCJoog= github.com/enescakir/emoji v1.0.0/go.mod h1:Bt1EKuLnKDTYpLALApstIkAjdDrS/8IAgTkKp+WKFD0= github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= -github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= -github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= github.com/go-authgate/sdk-go v0.11.0 h1:ZTfJ0rzeDn4QBqAmF9VKS3CqlKhE8+0tJxg8OGNtIzo= github.com/go-authgate/sdk-go v0.11.0/go.mod h1:sa0ige5wtayj2WcnXlxa8wGuyi5z/c/chc0mXPJTl/Q= github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI= github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM= -github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= -github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= -github.com/go-git/go-billy/v5 v5.9.0 h1:jItGXszUDRtR/AlferWPTMN4j38BQ88XnXKbilmmBPA= -github.com/go-git/go-billy/v5 v5.9.0/go.mod h1:jCnQMLj9eUgGU7+ludSTYoZL/GGmii14RxKFj7ROgHw= -github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= -github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= -github.com/go-git/go-git/v5 v5.19.1 h1:nX27AnaU43/K5bKktKwgBmR9lawoYVe1Ckg0rgzzN00= -github.com/go-git/go-git/v5 v5.19.1/go.mod h1:Pb1v0c7/g8aGQJwx9Us09W85yGoyvSwuhEGMH7zjDKQ= github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU= github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ= github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c= -github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= -github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= @@ -136,13 +106,7 @@ github.com/hashicorp/go-version v1.9.0 h1:CeOIz6k+LoN3qX9Z0tyQrPtiB1DFYRPfCIBtaX github.com/hashicorp/go-version v1.9.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= -github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= -github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= -github.com/kevinburke/ssh_config v1.6.0 h1:J1FBfmuVosPHf5GRdltRLhPJtJpTlMdKTBjRgTaQBFY= -github.com/kevinburke/ssh_config v1.6.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M= -github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= -github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -174,17 +138,13 @@ github.com/olekukonko/ll v0.1.8 h1:ysHCJRGHYKzmBSdz9w5AySztx7lG8SQY+naTGYUbsz8= github.com/olekukonko/ll v0.1.8/go.mod h1:RPRC6UcscfFZgjo1nulkfMH5IM0QAYim0LfnMvUuozw= github.com/olekukonko/tablewriter v1.1.4 h1:ORUMI3dXbMnRlRggJX3+q7OzQFDdvgbN9nVWj1drm6I= github.com/olekukonko/tablewriter v1.1.4/go.mod h1:+kedxuyTtgoZLwif3P1Em4hARJs+mVnzKxmsCL/C5RY= -github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= -github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= -github.com/pjbgf/sha1cd v0.6.0 h1:3WJ8Wz8gvDz29quX1OcEmkAlUg9diU4GxJHqs0/XiwU= -github.com/pjbgf/sha1cd v0.6.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= @@ -192,18 +152,11 @@ github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD github.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4/go.mod h1:C1a7PQSMz9NShzorzCiG2fk9+xuCgLkPeCvMHYR2OWg= github.com/seletskiy/tplutil v0.0.0-20200921103632-f880f6245597 h1:nZY1S2jo+VtDrUfjO9XYI137O41hhRkxZNV5Fb5ixCA= github.com/seletskiy/tplutil v0.0.0-20200921103632-f880f6245597/go.mod h1:F8CBHSOjnzjx9EeXyWJTAzJyVxN+Y8JH2WjLMn4utiw= -github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= -github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= -github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/skeema/knownhosts v1.3.2 h1:EDL9mgf4NzwMXCTfaxSD/o/a5fxDw/xL9nkU28JjdBg= -github.com/skeema/knownhosts v1.3.2/go.mod h1:bEg3iQAuw+jyiw+484wwFJoKSLwcfd7fqRy+N0QTiow= github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA= github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= @@ -211,8 +164,6 @@ github.com/urfave/cli-docs/v3 v3.1.0 h1:Sa5xm19IpE5gpm6tZzXdfjdFxn67PnEsE4dpXF7v github.com/urfave/cli-docs/v3 v3.1.0/go.mod h1:59d+5Hz1h6GSGJ10cvcEkbIe3j233t4XDqI72UIx7to= github.com/urfave/cli/v3 v3.9.0 h1:AV9lIiPv3ukYnxunaCUsHnEozptYmDN2F0+yWqLMn/c= github.com/urfave/cli/v3 v3.9.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso= -github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= -github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -226,7 +177,6 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= -golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM= @@ -238,7 +188,6 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= @@ -249,12 +198,7 @@ golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -262,7 +206,6 @@ golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -274,13 +217,8 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= -gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/modules/config/testtools.go b/modules/config/testtools.go index 384cfe24..2bec228d 100644 --- a/modules/config/testtools.go +++ b/modules/config/testtools.go @@ -1,8 +1,6 @@ // Copyright 2026 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -//go:build testtools - package config import "time" diff --git a/modules/context/context.go b/modules/context/context.go index 989bb7c1..87e0f7e2 100644 --- a/modules/context/context.go +++ b/modules/context/context.go @@ -15,7 +15,6 @@ import ( "gitea.dev/tea/modules/utils" "charm.land/huh/v2" - gogit "github.com/go-git/go-git/v5" "github.com/urfave/cli/v3" "golang.org/x/term" ) @@ -112,7 +111,7 @@ func InitCommand(cmd *cli.Command) (*TeaContext, error) { } if c.LocalRepo, c.Login, c.RepoSlug, err = contextFromLocalRepo(repoPath, remoteFlag, extraLogins); err != nil { - if err == errNotAGiteaRepo || err == gogit.ErrRepositoryNotExists { + if err == errNotAGiteaRepo || err == git.ErrRepositoryNotExists { // we can deal with that, commands needing the optional values use ctx.Ensure() } else { return nil, err diff --git a/modules/git/auth.go b/modules/git/auth.go index 5509ab7a..9d7c0bfd 100644 --- a/modules/git/auth.go +++ b/modules/git/auth.go @@ -7,69 +7,64 @@ import ( "fmt" "net/url" "os" + "strings" "gitea.dev/tea/modules/utils" - - git_transport "github.com/go-git/go-git/v5/plumbing/transport" - gogit_http "github.com/go-git/go-git/v5/plumbing/transport/http" - gogit_ssh "github.com/go-git/go-git/v5/plumbing/transport/ssh" "golang.org/x/crypto/ssh" ) type pwCallback = func(string) (string, error) -// GetAuthForURL returns the appropriate AuthMethod to be used in Push() / Pull() -// operations depending on the protocol, and prompts the user for credentials if -// necessary. -func GetAuthForURL(remoteURL *url.URL, authToken, keyFile string, passwordCallback pwCallback) (git_transport.AuthMethod, error) { +// GetAuthForURL returns backend-agnostic auth settings for git network operations. +func GetAuthForURL(remoteURL *url.URL, authToken, keyFile string, passwordCallback pwCallback) (*AuthMethod, error) { switch remoteURL.Scheme { case "http", "https": - // gitea supports push/pull via app token as username. - return &gogit_http.BasicAuth{Password: "", Username: authToken}, nil - + return &AuthMethod{Scheme: remoteURL.Scheme, Username: authToken}, nil case "ssh": - // try to select right key via ssh-agent. if it fails, try to read a key manually - user := remoteURL.User.Username() - auth, err := gogit_ssh.DefaultAuthBuilder(user) - if err != nil { - signer, err2 := readSSHPrivKey(keyFile, passwordCallback) - if err2 != nil { - return nil, err2 - } - auth = &gogit_ssh.PublicKeys{User: user, Signer: signer} + if keyFile == "" { + return &AuthMethod{Scheme: remoteURL.Scheme, Username: remoteURL.User.Username()}, nil + } + expandedKeyFile, err := utils.AbsPathWithExpansion(keyFile) + if err != nil { + return nil, err + } + sshKey, err := os.ReadFile(expandedKeyFile) + if err != nil { + return nil, fmt.Errorf("can not read ssh key '%s'", expandedKeyFile) } - return auth, nil - } - return nil, fmt.Errorf("don't know how to handle url scheme %v", remoteURL.Scheme) -} -func readSSHPrivKey(keyFile string, passwordCallback pwCallback) (sig ssh.Signer, err error) { - if keyFile != "" { - keyFile, err = utils.AbsPathWithExpansion(keyFile) - } else { - keyFile, err = utils.AbsPathWithExpansion("~/.ssh/id_rsa") - } - if err != nil { - return nil, err - } - sshKey, err := os.ReadFile(keyFile) - if err != nil { - return nil, fmt.Errorf("can not read ssh key '%s'", keyFile) - } - sig, err = ssh.ParsePrivateKey(sshKey) - if _, ok := err.(*ssh.PassphraseMissingError); ok && passwordCallback != nil { - // allow for up to 3 password attempts - for i := 0; i < 3; i++ { - var pass string - pass, err = passwordCallback(keyFile) - if err != nil { + auth := &AuthMethod{ + Scheme: remoteURL.Scheme, + Username: remoteURL.User.Username(), + KeyFile: expandedKeyFile, + } + if _, err := ssh.ParsePrivateKey(sshKey); err == nil { + return auth, nil + } + + if _, ok := err.(*ssh.PassphraseMissingError); ok { + if passwordCallback == nil { return nil, err } - sig, err = ssh.ParsePrivateKeyWithPassphrase(sshKey, []byte(pass)) - if err == nil { - break + for i := 0; i < 3; i++ { + pass, err := passwordCallback(expandedKeyFile) + if err != nil { + return nil, err + } + if _, err := ssh.ParsePrivateKeyWithPassphrase(sshKey, []byte(pass)); err == nil { + auth.KeyPassphrase = pass + return auth, nil + } } + return nil, err } + + return nil, err + default: + return nil, fmt.Errorf("don't know how to handle url scheme %v", remoteURL.Scheme) } - return sig, err +} + +func shellQuote(s string) string { + return "'" + strings.ReplaceAll(s, "'", "'\\''") + "'" } diff --git a/modules/git/backend.go b/modules/git/backend.go new file mode 100644 index 00000000..49e62897 --- /dev/null +++ b/modules/git/backend.go @@ -0,0 +1,103 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package git + +import ( + "fmt" + "sort" + "sync" +) + +var backendRegistry = struct { + sync.RWMutex + current string + backends map[string]Backend +}{ + backends: make(map[string]Backend), +} + +func init() { + mustRegisterBackend(cliBackend{}) + mustUseBackend("cli") +} + +// RegisterBackend makes a git backend available for later switching. +func RegisterBackend(backend Backend) error { + if backend == nil { + return fmt.Errorf("git backend is nil") + } + name := backend.Name() + if name == "" { + return fmt.Errorf("git backend name is empty") + } + + backendRegistry.Lock() + defer backendRegistry.Unlock() + backendRegistry.backends[name] = backend + if backendRegistry.current == "" { + backendRegistry.current = name + } + return nil +} + +func mustRegisterBackend(backend Backend) { + if err := RegisterBackend(backend); err != nil { + panic(err) + } +} + +// UseBackend switches the active git backend implementation. +func UseBackend(name string) error { + backendRegistry.Lock() + defer backendRegistry.Unlock() + if _, ok := backendRegistry.backends[name]; !ok { + return fmt.Errorf("git backend %q is not registered", name) + } + backendRegistry.current = name + return nil +} + +func mustUseBackend(name string) { + if err := UseBackend(name); err != nil { + panic(err) + } +} + +// CurrentBackendName returns the active backend name. +func CurrentBackendName() string { + backendRegistry.RLock() + defer backendRegistry.RUnlock() + return backendRegistry.current +} + +// RegisteredBackends returns all registered backend names. +func RegisteredBackends() []string { + backendRegistry.RLock() + defer backendRegistry.RUnlock() + out := make([]string, 0, len(backendRegistry.backends)) + for name := range backendRegistry.backends { + out = append(out, name) + } + sort.Strings(out) + return out +} + +func currentBackend() Backend { + backendRegistry.RLock() + defer backendRegistry.RUnlock() + return backendRegistry.backends[backendRegistry.current] +} + +func setBackendForTesting(t testingT, backend Backend) { + t.Helper() + prev := CurrentBackendName() + mustRegisterBackend(backend) + mustUseBackend(backend.Name()) + t.Cleanup(func() { mustUseBackend(prev) }) +} + +type testingT interface { + Cleanup(func()) + Helper() +} diff --git a/modules/git/backend_test.go b/modules/git/backend_test.go new file mode 100644 index 00000000..b514c5f0 --- /dev/null +++ b/modules/git/backend_test.go @@ -0,0 +1,74 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package git + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/require" +) + +type fakeBackend struct{} + +type fakeRepoBackend struct { + workTree string +} + +func (fakeBackend) Name() string { return "fake" } + +func (fakeBackend) Open(path string) (RepositoryBackend, error) { + return &fakeRepoBackend{workTree: "open:" + path}, nil +} + +func (fakeBackend) Clone(path, remoteURL string, auth *AuthMethod, opts CloneOptions) (RepositoryBackend, error) { + return &fakeRepoBackend{workTree: fmt.Sprintf("clone:%s:%s", path, remoteURL)}, nil +} + +func (r *fakeRepoBackend) WorkTree() string { return r.workTree } +func (r *fakeRepoBackend) Config() (*Config, error) { + return &Config{Remotes: map[string]*RemoteConfig{}, Branches: map[string]*Branch{}}, nil +} + +func (r *fakeRepoBackend) Head() (*Reference, error) { + return &Reference{name: NewBranchReferenceName("main"), hash: Hash("deadbeef")}, nil +} +func (r *fakeRepoBackend) AddRemote(name, remoteURL string) error { return nil } +func (r *fakeRepoBackend) SetBranchUpstream(branchName, remoteName, remoteBranch string) error { + return nil +} + +func (r *fakeRepoBackend) Fetch(remoteName string, refspecs []string, auth *AuthMethod) error { + return nil +} + +func (r *fakeRepoBackend) CreateTrackingBranch(localBranchName, remoteBranchName, remoteName string) error { + return nil +} +func (r *fakeRepoBackend) Checkout(ref ReferenceName) error { return nil } +func (r *fakeRepoBackend) DeleteLocalBranch(branchName string) error { return nil } +func (r *fakeRepoBackend) DeleteRemoteBranch(remoteName, remoteBranch string, auth *AuthMethod) error { + return nil +} +func (r *fakeRepoBackend) ListReferences(prefixes ...string) ([]*Reference, error) { return nil, nil } +func (r *fakeRepoBackend) PushToAgitFlowPR(remoteName, head, base, topic string, pushOptions map[string]string, auth *AuthMethod) error { + return nil +} + +func TestCanSwitchBackends(t *testing.T) { + setBackendForTesting(t, fakeBackend{}) + + repo, err := RepoFromPath("demo") + require.NoError(t, err) + require.Equal(t, "open:demo", repo.WorkTree()) + require.Equal(t, "fake", CurrentBackendName()) + + cloned, err := Clone("target", "https://example.com/repo.git", nil, 1, false) + require.NoError(t, err) + require.Equal(t, "clone:target:https://example.com/repo.git", cloned.WorkTree()) +} + +func TestRegisteredBackendsContainsCLI(t *testing.T) { + require.Contains(t, RegisteredBackends(), "cli") +} diff --git a/modules/git/branch.go b/modules/git/branch.go index 6c709ad8..db531539 100644 --- a/modules/git/branch.go +++ b/modules/git/branch.go @@ -8,73 +8,31 @@ import ( "fmt" "strings" "unicode" - - "github.com/go-git/go-git/v5" - git_config "github.com/go-git/go-git/v5/config" - git_plumbing "github.com/go-git/go-git/v5/plumbing" - git_transport "github.com/go-git/go-git/v5/plumbing/transport" ) // TeaCreateBranch creates a new branch in the repo, tracking from another branch. func (r TeaRepo) TeaCreateBranch(localBranchName, remoteBranchName, remoteName string) error { - // save in .git/config to assign remote for future pulls - localBranchRefName := git_plumbing.NewBranchReferenceName(localBranchName) - err := r.CreateBranch(&git_config.Branch{ - Name: localBranchName, - Merge: git_plumbing.NewBranchReferenceName(remoteBranchName), - Remote: remoteName, - }) - if err != nil { - return err - } - - // serialize the branch to .git/refs/heads - remoteBranchRefName := git_plumbing.NewRemoteReferenceName(remoteName, remoteBranchName) - remoteBranchRef, err := r.Storer.Reference(remoteBranchRefName) - if err != nil { - return err - } - localHashRef := git_plumbing.NewHashReference(localBranchRefName, remoteBranchRef.Hash()) - return r.Storer.SetReference(localHashRef) + return r.backend.CreateTrackingBranch(localBranchName, remoteBranchName, remoteName) } // TeaCheckout checks out the given branch in the worktree. -func (r TeaRepo) TeaCheckout(ref git_plumbing.ReferenceName) error { - tree, err := r.Worktree() - if err != nil { - return err - } - return tree.Checkout(&git.CheckoutOptions{Branch: ref}) +func (r TeaRepo) TeaCheckout(ref ReferenceName) error { + return r.backend.Checkout(ref) } -// TeaDeleteLocalBranch removes the given branch locally -func (r TeaRepo) TeaDeleteLocalBranch(branch *git_config.Branch) error { - err := r.DeleteBranch(branch.Name) - // if the branch is not found that's ok, as .git/config may have no entry if - // no remote tracking branch is configured for it (eg push without -u flag) - if err != nil && err.Error() != "branch not found" { - return err - } - return r.Storer.RemoveReference(git_plumbing.NewBranchReferenceName(branch.Name)) +// TeaDeleteLocalBranch removes the given branch locally. +func (r TeaRepo) TeaDeleteLocalBranch(branch *Branch) error { + return r.backend.DeleteLocalBranch(branch.Name) } -// TeaDeleteRemoteBranch removes the given branch on the given remote via git protocol -func (r TeaRepo) TeaDeleteRemoteBranch(remoteName, remoteBranch string, auth git_transport.AuthMethod) error { - // delete remote branch via git protocol: - // an empty source in the refspec means remote deletion to git 🙃 - refspec := fmt.Sprintf(":%s", git_plumbing.NewBranchReferenceName(remoteBranch)) - return r.Push(&git.PushOptions{ - RemoteName: remoteName, - RefSpecs: []git_config.RefSpec{git_config.RefSpec(refspec)}, - Prune: true, - Auth: auth, - }) +// TeaDeleteRemoteBranch removes the given branch on the given remote via git protocol. +func (r TeaRepo) TeaDeleteRemoteBranch(remoteName, remoteBranch string, auth *AuthMethod) error { + return r.backend.DeleteRemoteBranch(remoteName, remoteBranch, auth) } // TeaFindBranchBySha returns a branch that is at the the given SHA and syncs to the // given remote repo. -func (r TeaRepo) TeaFindBranchBySha(sha, repoURL string) (b *git_config.Branch, err error) { - // find remote matching our repoURL +func (r TeaRepo) TeaFindBranchBySha(sha, repoURL string) (b *Branch, err error) { remote, err := r.GetRemote(repoURL) if err != nil { return nil, err @@ -84,41 +42,26 @@ func (r TeaRepo) TeaFindBranchBySha(sha, repoURL string) (b *git_config.Branch, } remoteName := remote.Config().Name - // check if the given remote has our branch (.git/refs/remotes//*) - iter, err := r.References() + refs, err := r.backend.ListReferences("refs/heads", "refs/remotes/"+remoteName) if err != nil { return nil, err } - defer iter.Close() - var remoteRefName git_plumbing.ReferenceName - var localRefName git_plumbing.ReferenceName - err = iter.ForEach(func(ref *git_plumbing.Reference) error { - if ref.Name().IsRemote() { - name := ref.Name().Short() - if ref.Hash().String() == sha && strings.HasPrefix(name, remoteName) { - remoteRefName = ref.Name() - } - } + var remoteRefName ReferenceName + var localRefName ReferenceName + for _, ref := range refs { + if ref.Name().IsRemote() && ref.Hash().String() == sha { + remoteRefName = ref.Name() + } if ref.Name().IsBranch() && ref.Hash().String() == sha { localRefName = ref.Name() } - return nil - }) - if err != nil { - return nil, err } if remoteRefName == "" || localRefName == "" { - // no remote tracking branch found, so a potential local branch - // can't be a match either return nil, nil } - b = &git_config.Branch{ - Remote: remoteName, - Name: localRefName.Short(), - Merge: localRefName, - } + b = &Branch{Remote: remoteName, Name: localRefName.Short(), Merge: localRefName} return b, b.Validate() } @@ -126,8 +69,7 @@ func (r TeaRepo) TeaFindBranchBySha(sha, repoURL string) (b *git_config.Branch, // remote names and syncs to the given remote repo. This method is less precise // than TeaFindBranchBySha(), but may be desirable if local and remote branch // have diverged. -func (r TeaRepo) TeaFindBranchByName(branchName, repoURL string) (b *git_config.Branch, err error) { - // find remote matching our repoURL +func (r TeaRepo) TeaFindBranchByName(branchName, repoURL string) (b *Branch, err error) { remote, err := r.GetRemote(repoURL) if err != nil { return nil, err @@ -137,45 +79,35 @@ func (r TeaRepo) TeaFindBranchByName(branchName, repoURL string) (b *git_config. } remoteName := remote.Config().Name - // check if the given remote has our branch (.git/refs/remotes//*) - iter, err := r.References() + refs, err := r.backend.ListReferences("refs/heads", "refs/remotes/"+remoteName) if err != nil { return nil, err } - defer iter.Close() - var remoteRefName git_plumbing.ReferenceName - var localRefName git_plumbing.ReferenceName + + var remoteRefName ReferenceName + var localRefName ReferenceName remoteSearchingName := fmt.Sprintf("%s/%s", remoteName, branchName) - err = iter.ForEach(func(ref *git_plumbing.Reference) error { + for _, ref := range refs { if ref.Name().IsRemote() && ref.Name().Short() == remoteSearchingName { remoteRefName = ref.Name() } - n := ref.Name() - if n.IsBranch() && n.Short() == branchName { - localRefName = n + if ref.Name().IsBranch() && ref.Name().Short() == branchName { + localRefName = ref.Name() } - return nil - }) - if err != nil { - return nil, err } if remoteRefName == "" || localRefName == "" { return nil, nil } - b = &git_config.Branch{ - Remote: remoteName, - Name: localRefName.Short(), - Merge: localRefName, - } + b = &Branch{Remote: remoteName, Name: localRefName.Short(), Merge: localRefName} return b, b.Validate() } // TeaFindBranchRemote gives the first remote that has a branch with the same name or sha, // depending on what is passed in. // This function is needed, as git does not always define branches in .git/config with remote entries. -// Priority order is: first match of sha and branch -> first match of branch -> first match of sha -func (r TeaRepo) TeaFindBranchRemote(branchName, hash string) (*git.Remote, error) { +// Priority order is: first match of sha and branch -> first match of branch -> first match of sha. +func (r TeaRepo) TeaFindBranchRemote(branchName, hash string) (*Remote, error) { remotes, err := r.Remotes() if err != nil { return nil, err @@ -188,55 +120,53 @@ func (r TeaRepo) TeaFindBranchRemote(branchName, hash string) (*git.Remote, erro return remotes[0], nil } - // check if the given remote has our branch (.git/refs/remotes//*) - iter, err := r.References() + refs, err := r.backend.ListReferences("refs/remotes") if err != nil { return nil, err } - defer iter.Close() - var shaMatch *git.Remote - var branchMatch *git.Remote - var fullMatch *git.Remote - if err := iter.ForEach(func(ref *git_plumbing.Reference) error { - if ref.Name().IsRemote() { - names := strings.SplitN(ref.Name().Short(), "/", 2) - remote := names[0] - branch := names[1] - if branchMatch == nil && branchName != "" && branchName == branch { - if branchMatch, err = r.Remote(remote); err != nil { - return err - } - } - if shaMatch == nil && hash != "" && hash == ref.Hash().String() { - if shaMatch, err = r.Remote(remote); err != nil { - return err - } - } - if fullMatch == nil && branchName != "" && branchName == branch && hash != "" && hash == ref.Hash().String() { - if fullMatch, err = r.Remote(remote); err != nil { - return err - } - // stop asap you have a full match - return nil - } + remoteByName := make(map[string]*Remote, len(remotes)) + for _, remote := range remotes { + remoteByName[remote.Config().Name] = remote + } + + var shaMatch *Remote + var branchMatch *Remote + var fullMatch *Remote + for _, ref := range refs { + remoteName, remoteBranch, ok := splitRemoteRef(ref.Name()) + if !ok { + continue + } + remote := remoteByName[remoteName] + if remote == nil { + continue + } + if branchMatch == nil && branchName != "" && branchName == remoteBranch { + branchMatch = remote + } + if shaMatch == nil && hash != "" && hash == ref.Hash().String() { + shaMatch = remote + } + if fullMatch == nil && branchName != "" && branchName == remoteBranch && hash != "" && hash == ref.Hash().String() { + fullMatch = remote + break } - return nil - }); err != nil { - return nil, err } - if fullMatch != nil { + switch { + case fullMatch != nil: return fullMatch, nil - } else if branchMatch != nil { + case branchMatch != nil: return branchMatch, nil - } else if shaMatch != nil { + case shaMatch != nil: return shaMatch, nil + default: + return nil, nil } - return nil, nil } -// TeaGetCurrentBranchNameAndSHA return the name and sha of the branch witch is currently active +// TeaGetCurrentBranchNameAndSHA return the name and sha of the branch witch is currently active. func (r TeaRepo) TeaGetCurrentBranchNameAndSHA() (string, string, error) { localHead, err := r.Head() if err != nil { @@ -251,13 +181,11 @@ func (r TeaRepo) TeaGetCurrentBranchNameAndSHA() (string, string, error) { } // PushToCreatAgitFlowPR pushes the given head to the refs/for// ref on the remote to create an agit flow PR. -func (r TeaRepo) PushToCreatAgitFlowPR(remoteName, head, base, topic, title, description string, auth git_transport.AuthMethod) error { +func (r TeaRepo) PushToCreatAgitFlowPR(remoteName, head, base, topic, title, description string, auth *AuthMethod) error { if !strings.HasPrefix(head, "refs/") { head = "refs/heads/" + head } - ref := fmt.Sprintf("%s:refs/for/%s/%s", head, base, topic) - pushOptions := make(map[string]string) if len(title) > 0 { pushOptions["title"] = b64Encode(title) @@ -265,15 +193,18 @@ func (r TeaRepo) PushToCreatAgitFlowPR(remoteName, head, base, topic, title, des if len(description) > 0 { pushOptions["description"] = b64Encode(description) } + return r.backend.PushToAgitFlowPR(remoteName, head, base, topic, pushOptions, auth) +} - opts := &git.PushOptions{ - RemoteName: remoteName, - RefSpecs: []git_config.RefSpec{git_config.RefSpec(ref)}, - Options: pushOptions, - Auth: auth, +func splitRemoteRef(ref ReferenceName) (remoteName, branchName string, ok bool) { + if !ref.IsRemote() { + return "", "", false } - - return r.Push(opts) + parts := strings.SplitN(ref.Short(), "/", 2) + if len(parts) != 2 { + return "", "", false + } + return parts[0], parts[1], true } // b64Encode implements base64 encode for string if necessary. diff --git a/modules/git/cli_backend.go b/modules/git/cli_backend.go new file mode 100644 index 00000000..99989e5a --- /dev/null +++ b/modules/git/cli_backend.go @@ -0,0 +1,374 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package git + +import ( + "bytes" + "encoding/base64" + "errors" + "fmt" + "os" + "os/exec" + "sort" + "strconv" + "strings" +) + +type cliBackend struct{} + +type cliRepository struct { + workTree string +} + +func (cliBackend) Name() string { + return "cli" +} + +func (b cliBackend) Open(path string) (RepositoryBackend, error) { + if path == "" { + path = "." + } + + out, err := runGitCommand(path, nil, nil, "rev-parse", "--show-toplevel") + if err != nil { + return nil, classifyRepoError(err) + } + + return &cliRepository{workTree: strings.TrimSpace(out)}, nil +} + +func (b cliBackend) Clone(path, remoteURL string, auth *AuthMethod, opts CloneOptions) (RepositoryBackend, error) { + extraConfigs := make([]string, 0, 1) + if opts.Insecure { + extraConfigs = append(extraConfigs, "http.sslVerify=false") + } + + args := []string{"clone"} + if opts.Depth > 0 { + args = append(args, "--depth", strconv.Itoa(opts.Depth)) + } + args = append(args, remoteURL, path) + + if _, err := runGitCommand("", auth, extraConfigs, args...); err != nil { + return nil, err + } + return b.Open(path) +} + +func (r *cliRepository) WorkTree() string { + return r.workTree +} + +func (r *cliRepository) Config() (*Config, error) { + cfg := &Config{ + Remotes: map[string]*RemoteConfig{}, + Branches: map[string]*Branch{}, + } + + remoteOut, err := r.git(nil, nil, "remote") + if err != nil { + return nil, err + } + for _, remoteName := range strings.Fields(remoteOut) { + urlOut, err := r.git(nil, nil, "config", "--get-all", "remote."+remoteName+".url") + if err != nil { + return nil, err + } + cfg.Remotes[remoteName] = &RemoteConfig{Name: remoteName, URLs: splitNonEmptyLines(urlOut)} + } + + branchOut, err := r.git(nil, nil, "config", "--get-regexp", `^branch\..*\.(remote|merge)$`) + if err != nil { + var gitErr *gitCommandError + if !errors.As(err, &gitErr) { + return nil, err + } + if strings.TrimSpace(gitErr.stderr) != "" && !strings.Contains(gitErr.stderr, "No such section or key") { + return nil, err + } + return cfg, nil + } + + for _, line := range splitNonEmptyLines(branchOut) { + parts := strings.SplitN(line, " ", 2) + if len(parts) != 2 { + continue + } + branchName, field, ok := parseBranchConfigKey(parts[0]) + if !ok { + continue + } + branch := cfg.Branches[branchName] + if branch == nil { + branch = &Branch{Name: branchName} + cfg.Branches[branchName] = branch + } + switch field { + case "remote": + branch.Remote = parts[1] + case "merge": + branch.Merge = ReferenceName(parts[1]) + } + } + + return cfg, nil +} + +func (r *cliRepository) Head() (*Reference, error) { + hashOut, err := r.git(nil, nil, "rev-parse", "HEAD") + if err != nil { + return nil, err + } + hash := Hash(strings.TrimSpace(hashOut)) + + if refOut, err := r.git(nil, nil, "symbolic-ref", "-q", "HEAD"); err == nil { + return &Reference{name: ReferenceName(strings.TrimSpace(refOut)), hash: hash}, nil + } + if tagOut, err := r.git(nil, nil, "describe", "--exact-match", "--tags", "HEAD"); err == nil { + return &Reference{name: ReferenceName("refs/tags/" + strings.TrimSpace(tagOut)), hash: hash}, nil + } + return &Reference{name: ReferenceName("HEAD"), hash: hash}, nil +} + +func (r *cliRepository) AddRemote(name, remoteURL string) error { + _, err := r.git(nil, nil, "remote", "add", name, remoteURL) + return err +} + +func (r *cliRepository) SetBranchUpstream(branchName, remoteName, remoteBranch string) error { + mergeRef := NewBranchReferenceName(remoteBranch).String() + if _, err := r.git(nil, nil, "config", "branch."+branchName+".remote", remoteName); err != nil { + return err + } + _, err := r.git(nil, nil, "config", "branch."+branchName+".merge", mergeRef) + return err +} + +func (r *cliRepository) Fetch(remoteName string, refspecs []string, auth *AuthMethod) error { + args := []string{"fetch", remoteName} + args = append(args, refspecs...) + _, err := r.git(auth, nil, args...) + return err +} + +func (r *cliRepository) CreateTrackingBranch(localBranchName, remoteBranchName, remoteName string) error { + _, err := r.git(nil, nil, "branch", "--track", localBranchName, fmt.Sprintf("%s/%s", remoteName, remoteBranchName)) + if err == nil { + return nil + } + if gitErr, ok := err.(*gitCommandError); ok && strings.Contains(gitErr.stderr, "already exists") { + return ErrBranchExists + } + return err +} + +func (r *cliRepository) Checkout(ref ReferenceName) error { + _, err := r.git(nil, nil, "checkout", ref.String()) + return err +} + +func (r *cliRepository) DeleteLocalBranch(branchName string) error { + _, err := r.git(nil, nil, "branch", "-D", branchName) + if err == nil { + return nil + } + if gitErr, ok := err.(*gitCommandError); ok { + stderr := strings.ToLower(gitErr.stderr) + if strings.Contains(stderr, "not found") || strings.Contains(stderr, "not exist") { + return nil + } + } + return err +} + +func (r *cliRepository) DeleteRemoteBranch(remoteName, remoteBranch string, auth *AuthMethod) error { + _, err := r.git(auth, nil, "push", "--delete", remoteName, remoteBranch) + return err +} + +func (r *cliRepository) ListReferences(prefixes ...string) ([]*Reference, error) { + args := []string{"for-each-ref", "--format=%(objectname)%09%(refname)"} + args = append(args, prefixes...) + out, err := r.git(nil, nil, args...) + if err != nil { + return nil, err + } + + refs := make([]*Reference, 0) + for _, line := range splitNonEmptyLines(out) { + parts := strings.SplitN(line, " ", 2) + if len(parts) != 2 { + continue + } + refs = append(refs, &Reference{name: ReferenceName(parts[1]), hash: Hash(parts[0])}) + } + return refs, nil +} + +func (r *cliRepository) PushToAgitFlowPR(remoteName, head, base, topic string, pushOptions map[string]string, auth *AuthMethod) error { + ref := fmt.Sprintf("%s:refs/for/%s/%s", head, base, topic) + args := []string{"push", remoteName, ref} + if len(pushOptions) > 0 { + keys := make([]string, 0, len(pushOptions)) + for key := range pushOptions { + keys = append(keys, key) + } + sort.Strings(keys) + for _, key := range keys { + args = append(args, "-o", key+"="+pushOptions[key]) + } + } + _, err := r.git(auth, nil, args...) + return err +} + +func (r *cliRepository) git(auth *AuthMethod, extraConfigs []string, args ...string) (string, error) { + return runGitCommand(r.workTree, auth, extraConfigs, args...) +} + +type gitCommandError struct { + args []string + stderr string + err error +} + +func (e *gitCommandError) Error() string { + stderr := strings.TrimSpace(e.stderr) + if stderr == "" { + return fmt.Sprintf("git %s: %v", strings.Join(e.args, " "), e.err) + } + return fmt.Sprintf("git %s: %s", strings.Join(e.args, " "), stderr) +} + +func (e *gitCommandError) Unwrap() error { + return e.err +} + +func runGitCommand(dir string, auth *AuthMethod, extraConfigs []string, args ...string) (string, error) { + authConfigs, authEnv, cleanup, err := prepareCLIAuth(auth) + if err != nil { + return "", err + } + defer cleanup() + + fullArgs := make([]string, 0, (len(extraConfigs)+len(authConfigs))*2+len(args)) + for _, cfg := range extraConfigs { + fullArgs = append(fullArgs, "-c", cfg) + } + for _, cfg := range authConfigs { + fullArgs = append(fullArgs, "-c", cfg) + } + fullArgs = append(fullArgs, args...) + + cmd := exec.Command("git", fullArgs...) + if dir != "" { + cmd.Dir = dir + } + cmd.Env = append(os.Environ(), authEnv...) + + var stdout bytes.Buffer + var stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + if err := cmd.Run(); err != nil { + return "", &gitCommandError{args: fullArgs, stderr: stderr.String(), err: err} + } + return stdout.String(), nil +} + +func prepareCLIAuth(auth *AuthMethod) ([]string, []string, func(), error) { + if auth == nil { + return nil, nil, func() {}, nil + } + + configs := make([]string, 0, 1) + env := make([]string, 0, 4) + cleanup := func() {} + + switch auth.Scheme { + case "http", "https": + if auth.Username != "" || auth.Password != "" { + header := "Authorization: Basic " + base64.StdEncoding.EncodeToString([]byte(auth.Username+":"+auth.Password)) + configs = append(configs, "http.extraHeader="+header) + } + case "ssh": + sshCommand := "ssh" + if auth.KeyFile != "" { + sshCommand += " -i " + shellQuote(auth.KeyFile) + " -o IdentitiesOnly=yes" + } + env = append(env, "GIT_SSH_COMMAND="+sshCommand) + if auth.KeyPassphrase != "" { + askPassPath, err := writeAskPassScript(auth.KeyPassphrase) + if err != nil { + return nil, nil, cleanup, err + } + cleanup = func() { _ = os.Remove(askPassPath) } + env = append(env, + "SSH_ASKPASS="+askPassPath, + "SSH_ASKPASS_REQUIRE=force", + "DISPLAY=tea", + ) + } + } + + return configs, env, cleanup, nil +} + +func writeAskPassScript(passphrase string) (string, error) { + f, err := os.CreateTemp("", "tea-ssh-askpass-*") + if err != nil { + return "", err + } + defer f.Close() + + content := "#!/bin/sh\nprintf '%s\\n' " + shellQuote(passphrase) + "\n" + if _, err := f.WriteString(content); err != nil { + _ = os.Remove(f.Name()) + return "", err + } + if err := f.Chmod(0o700); err != nil { + _ = os.Remove(f.Name()) + return "", err + } + return f.Name(), nil +} + +func classifyRepoError(err error) error { + var gitErr *gitCommandError + if errors.As(err, &gitErr) { + msg := strings.ToLower(gitErr.stderr) + if strings.Contains(msg, "not a git repository") || strings.Contains(msg, "cannot change to") { + return ErrRepositoryNotExists + } + } + return err +} + +func parseBranchConfigKey(key string) (branchName, field string, ok bool) { + const prefix = "branch." + if !strings.HasPrefix(key, prefix) { + return "", "", false + } + trimmed := strings.TrimPrefix(key, prefix) + switch { + case strings.HasSuffix(trimmed, ".remote"): + return strings.TrimSuffix(trimmed, ".remote"), "remote", true + case strings.HasSuffix(trimmed, ".merge"): + return strings.TrimSuffix(trimmed, ".merge"), "merge", true + default: + return "", "", false + } +} + +func splitNonEmptyLines(s string) []string { + lines := strings.Split(strings.TrimSpace(s), "\n") + out := make([]string, 0, len(lines)) + for _, line := range lines { + line = strings.TrimSpace(line) + if line != "" { + out = append(out, line) + } + } + return out +} diff --git a/modules/git/network.go b/modules/git/network.go new file mode 100644 index 00000000..aac21ced --- /dev/null +++ b/modules/git/network.go @@ -0,0 +1,28 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package git + +// Clone clones a repository using the active backend. +func Clone(path, remoteURL string, auth *AuthMethod, depth int, insecure bool) (*TeaRepo, error) { + backend, err := currentBackend().Clone(path, remoteURL, auth, CloneOptions{Depth: depth, Insecure: insecure}) + if err != nil { + return nil, err + } + return newTeaRepo(backend), nil +} + +// AddRemote adds a new remote to the repository. +func (r TeaRepo) AddRemote(name, remoteURL string) error { + return r.backend.AddRemote(name, remoteURL) +} + +// SetBranchUpstream configures the branch's upstream remote. +func (r TeaRepo) SetBranchUpstream(branchName, remoteName, remoteBranch string) error { + return r.backend.SetBranchUpstream(branchName, remoteName, remoteBranch) +} + +// Fetch fetches updates from the named remote. +func (r TeaRepo) Fetch(remoteName string, refspecs []string, auth *AuthMethod) error { + return r.backend.Fetch(remoteName, refspecs, auth) +} diff --git a/modules/git/remote.go b/modules/git/remote.go index c15606d9..8de93e45 100644 --- a/modules/git/remote.go +++ b/modules/git/remote.go @@ -6,14 +6,11 @@ package git import ( "fmt" "net/url" - - "github.com/go-git/go-git/v5" - git_config "github.com/go-git/go-git/v5/config" ) // GetRemote tries to match a Remote of the repo via the given URL. // Matching is based on the normalized URL, accepting different protocols. -func (r TeaRepo) GetRemote(remoteURL string) (*git.Remote, error) { +func (r TeaRepo) GetRemote(remoteURL string) (*Remote, error) { repoURL, err := ParseURL(remoteURL) if err != nil { return nil, err @@ -23,14 +20,14 @@ func (r TeaRepo) GetRemote(remoteURL string) (*git.Remote, error) { if err != nil { return nil, err } - for _, r := range remotes { - for _, u := range r.Config().URLs { - remoteURL, err := ParseURL(u) + for _, remote := range remotes { + for _, u := range remote.Config().URLs { + parsedRemoteURL, err := ParseURL(u) if err != nil { return nil, err } - if remoteURL.Host == repoURL.Host && remoteURL.Path == repoURL.Path { - return r, nil + if parsedRemoteURL.Host == repoURL.Host && parsedRemoteURL.Path == repoURL.Path { + return remote, nil } } } @@ -41,27 +38,23 @@ func (r TeaRepo) GetRemote(remoteURL string) (*git.Remote, error) { // GetOrCreateRemote tries to match a Remote of the repo via the given URL. // If no match is found, a new Remote with `newRemoteName` is created. // Matching is based on the normalized URL, accepting different protocols. -func (r TeaRepo) GetOrCreateRemote(remoteURL, newRemoteName string) (*git.Remote, error) { +func (r TeaRepo) GetOrCreateRemote(remoteURL, newRemoteName string) (*Remote, error) { localRemote, err := r.GetRemote(remoteURL) if err != nil { return nil, err } - // if no match found, create a new remote if localRemote == nil { - localRemote, err = r.CreateRemote(&git_config.RemoteConfig{ - Name: newRemoteName, - URLs: []string{remoteURL}, - }) - if err != nil { + if err := r.AddRemote(newRemoteName, remoteURL); err != nil { return nil, err } + return r.Remote(newRemoteName) } return localRemote, nil } -// TeaRemoteURL returns the first url entry for the given remote name +// TeaRemoteURL returns the first url entry for the given remote name. func (r TeaRepo) TeaRemoteURL(name string) (auth *url.URL, err error) { remote, err := r.Remote(name) if err != nil { @@ -71,5 +64,5 @@ func (r TeaRepo) TeaRemoteURL(name string) (auth *url.URL, err error) { if len(urls) == 0 { return nil, fmt.Errorf("remote %s has no URL configured", name) } - return ParseURL(remote.Config().URLs[0]) + return ParseURL(urls[0]) } diff --git a/modules/git/repo.go b/modules/git/repo.go index 938e4e60..32e5e156 100644 --- a/modules/git/repo.go +++ b/modules/git/repo.go @@ -4,14 +4,18 @@ package git import ( + "fmt" "net/url" - - "github.com/go-git/go-git/v5" + "sort" ) -// TeaRepo is a go-git Repository, with an extended high level interface. +// TeaRepo wraps a local git repository behind a swappable backend. type TeaRepo struct { - *git.Repository + backend RepositoryBackend +} + +func newTeaRepo(backend RepositoryBackend) *TeaRepo { + return &TeaRepo{backend: backend} } // RepoForWorkdir tries to open the git repository in the local directory @@ -20,28 +24,71 @@ func RepoForWorkdir() (*TeaRepo, error) { return RepoFromPath("") } -// RepoFromPath tries to open the git repository by path +// RepoFromPath tries to open the git repository by path. func RepoFromPath(path string) (*TeaRepo, error) { - if len(path) == 0 { - path = "./" + backend, err := currentBackend().Open(path) + if err != nil { + return nil, err } - repo, err := git.PlainOpenWithOptions(path, &git.PlainOpenOptions{ - DetectDotGit: true, - EnableDotGitCommonDir: true, // Enable commondir support for worktrees - }) + return newTeaRepo(backend), nil +} + +// WorkTree returns the repository work tree path. +func (r TeaRepo) WorkTree() string { + return r.backend.WorkTree() +} + +// Config returns the repository config values tea needs. +func (r TeaRepo) Config() (*Config, error) { + return r.backend.Config() +} + +// Remote returns the configured remote by name. +func (r TeaRepo) Remote(remoteName string) (*Remote, error) { + cfg, err := r.Config() + if err != nil { + return nil, err + } + remoteCfg, ok := cfg.Remotes[remoteName] + if !ok { + return nil, fmt.Errorf("remote %s not found", remoteName) + } + return &Remote{repo: &r, config: remoteCfg}, nil +} + +// Remotes returns all configured remotes sorted by name. +func (r TeaRepo) Remotes() ([]*Remote, error) { + cfg, err := r.Config() if err != nil { return nil, err } - return &TeaRepo{repo}, nil + remoteNames := make([]string, 0, len(cfg.Remotes)) + for name := range cfg.Remotes { + remoteNames = append(remoteNames, name) + } + sort.Strings(remoteNames) + + remotes := make([]*Remote, 0, len(remoteNames)) + for _, name := range remoteNames { + remotes = append(remotes, &Remote{repo: &r, config: cfg.Remotes[name]}) + } + return remotes, nil } -// RemoteURL returns the URL of the given remote +// Head returns the currently checked out ref. +func (r TeaRepo) Head() (*Reference, error) { + return r.backend.Head() +} + +// RemoteURL returns the URL of the given remote. func (r TeaRepo) RemoteURL(remoteName string) (*url.URL, error) { remote, err := r.Remote(remoteName) if err != nil { return nil, err } - - return url.Parse(remote.Config().URLs[0]) + if len(remote.Config().URLs) == 0 { + return nil, fmt.Errorf("remote %s has no URL configured", remoteName) + } + return ParseURL(remote.Config().URLs[0]) } diff --git a/modules/git/repo_cli_test.go b/modules/git/repo_cli_test.go new file mode 100644 index 00000000..053b8512 --- /dev/null +++ b/modules/git/repo_cli_test.go @@ -0,0 +1,136 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package git + +import ( + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +func runGit(t *testing.T, dir string, args ...string) string { + t.Helper() + cmd := exec.Command("git", args...) + if dir != "" { + cmd.Dir = dir + } + out, err := cmd.CombinedOutput() + require.NoErrorf(t, err, "git %v failed: %s", args, out) + return string(out) +} + +func tryGit(dir string, args ...string) error { + cmd := exec.Command("git", args...) + if dir != "" { + cmd.Dir = dir + } + _, err := cmd.CombinedOutput() + return err +} + +func TestRepoFromPathSupportsWorktrees(t *testing.T) { + tmpDir := t.TempDir() + mainRepoPath := filepath.Join(tmpDir, "main-repo") + worktreePath := filepath.Join(tmpDir, "worktree") + + runGit(t, "", "init", mainRepoPath) + runGit(t, mainRepoPath, "config", "user.email", "test@example.com") + runGit(t, mainRepoPath, "config", "user.name", "Test User") + runGit(t, mainRepoPath, "remote", "add", "origin", "https://gitea.com/owner/repo.git") + + readmePath := filepath.Join(mainRepoPath, "README.md") + require.NoError(t, os.WriteFile(readmePath, []byte("# Test Repo\n"), 0o644)) + runGit(t, mainRepoPath, "add", "README.md") + runGit(t, mainRepoPath, "commit", "-m", "Initial commit") + runGit(t, mainRepoPath, "worktree", "add", worktreePath, "-b", "test-branch") + + repo, err := RepoFromPath(worktreePath) + require.NoError(t, err) + + config, err := repo.Config() + require.NoError(t, err) + require.Contains(t, config.Remotes, "origin") + require.Equal(t, "https://gitea.com/owner/repo.git", config.Remotes["origin"].URLs[0]) + + head, err := repo.Head() + require.NoError(t, err) + require.Equal(t, "test-branch", head.Name().Short()) +} + +func TestRepoFromPathSupportsSHA256Repos(t *testing.T) { + tmpDir := t.TempDir() + repoPath := filepath.Join(tmpDir, "sha256-repo") + + if err := tryGit("", "init", "--object-format=sha256", repoPath); err != nil { + t.Skip("git does not support sha256 object format in this environment") + } + + runGit(t, repoPath, "config", "user.email", "test@example.com") + runGit(t, repoPath, "config", "user.name", "Test User") + runGit(t, repoPath, "remote", "add", "origin", "https://gitea.com/owner/repo.git") + + readmePath := filepath.Join(repoPath, "README.md") + require.NoError(t, os.WriteFile(readmePath, []byte("sha256\n"), 0o644)) + runGit(t, repoPath, "add", "README.md") + runGit(t, repoPath, "commit", "-m", "Initial commit") + + repo, err := RepoFromPath(repoPath) + require.NoError(t, err) + + branch, sha, err := repo.TeaGetCurrentBranchNameAndSHA() + require.NoError(t, err) + require.NotEmpty(t, branch) + require.Len(t, sha, 64) + + config, err := repo.Config() + require.NoError(t, err) + require.Contains(t, config.Remotes, "origin") +} + +func TestTeaFindBranchByShaAndName(t *testing.T) { + tmpDir := t.TempDir() + remotePath := filepath.Join(tmpDir, "remote.git") + localPath := filepath.Join(tmpDir, "local") + + runGit(t, "", "init", "--bare", remotePath) + runGit(t, "", "clone", remotePath, localPath) + runGit(t, localPath, "config", "user.email", "test@example.com") + runGit(t, localPath, "config", "user.name", "Test User") + + filePath := filepath.Join(localPath, "README.md") + require.NoError(t, os.WriteFile(filePath, []byte("main\n"), 0o644)) + runGit(t, localPath, "add", "README.md") + runGit(t, localPath, "commit", "-m", "Initial commit") + runGit(t, localPath, "branch", "-M", "main") + runGit(t, localPath, "push", "-u", "origin", "main") + runGit(t, localPath, "checkout", "-b", "feature/demo") + + require.NoError(t, os.WriteFile(filePath, []byte("feature\n"), 0o644)) + runGit(t, localPath, "commit", "-am", "Feature commit") + runGit(t, localPath, "push", "-u", "origin", "feature/demo") + + repo, err := RepoFromPath(localPath) + require.NoError(t, err) + + sha := strings.TrimSpace(runGit(t, localPath, "rev-parse", "HEAD")) + branchBySha, err := repo.TeaFindBranchBySha(sha, remotePath) + require.NoError(t, err) + require.NotNil(t, branchBySha) + require.Equal(t, "feature/demo", branchBySha.Name) + require.Equal(t, "origin", branchBySha.Remote) + + branchByName, err := repo.TeaFindBranchByName("feature/demo", remotePath) + require.NoError(t, err) + require.NotNil(t, branchByName) + require.Equal(t, "feature/demo", branchByName.Name) + + remote, err := repo.TeaFindBranchRemote("feature/demo", sha) + require.NoError(t, err) + require.NotNil(t, remote) + require.Equal(t, "origin", remote.Config().Name) +} diff --git a/modules/git/types.go b/modules/git/types.go new file mode 100644 index 00000000..41fe7f36 --- /dev/null +++ b/modules/git/types.go @@ -0,0 +1,163 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package git + +import ( + "errors" + "strings" +) + +var ( + // ErrRepositoryNotExists indicates the requested path is not inside a git repository. + ErrRepositoryNotExists = errors.New("repository does not exist") + // ErrBranchExists indicates the requested branch already exists locally. + ErrBranchExists = errors.New("branch already exists") +) + +// AuthMethod carries backend-agnostic authentication information for git operations. +type AuthMethod struct { + Scheme string + Username string + Password string + KeyFile string + KeyPassphrase string +} + +// CloneOptions describes repository clone behavior. +type CloneOptions struct { + Depth int + Insecure bool +} + +// RepositoryBackend is the backend abstraction used by TeaRepo. +type RepositoryBackend interface { + WorkTree() string + Config() (*Config, error) + Head() (*Reference, error) + AddRemote(name, remoteURL string) error + SetBranchUpstream(branchName, remoteName, remoteBranch string) error + Fetch(remoteName string, refspecs []string, auth *AuthMethod) error + CreateTrackingBranch(localBranchName, remoteBranchName, remoteName string) error + Checkout(ref ReferenceName) error + DeleteLocalBranch(branchName string) error + DeleteRemoteBranch(remoteName, remoteBranch string, auth *AuthMethod) error + ListReferences(prefixes ...string) ([]*Reference, error) + PushToAgitFlowPR(remoteName, head, base, topic string, pushOptions map[string]string, auth *AuthMethod) error +} + +// Backend opens and clones repositories using a concrete git implementation. +type Backend interface { + Name() string + Open(path string) (RepositoryBackend, error) + Clone(path, remoteURL string, auth *AuthMethod, opts CloneOptions) (RepositoryBackend, error) +} + +// Config mirrors the repository config fields tea needs. +type Config struct { + Remotes map[string]*RemoteConfig + Branches map[string]*Branch +} + +// RemoteConfig stores remote configuration. +type RemoteConfig struct { + Name string + URLs []string +} + +// Branch stores branch configuration. +type Branch struct { + Name string + Remote string + Merge ReferenceName +} + +// Validate checks whether the branch contains the fields tea needs. +func (b *Branch) Validate() error { + if b == nil || b.Name == "" { + return errors.New("branch name is required") + } + return nil +} + +// Remote wraps a configured git remote. +type Remote struct { + repo *TeaRepo + config *RemoteConfig +} + +// Config returns the remote configuration. +func (r *Remote) Config() *RemoteConfig { + if r == nil { + return nil + } + return r.config +} + +// ReferenceName identifies a git reference. +type ReferenceName string + +func (r ReferenceName) String() string { return string(r) } + +// Short returns the short display name for the reference. +func (r ReferenceName) Short() string { + s := string(r) + switch { + case strings.HasPrefix(s, "refs/heads/"): + return strings.TrimPrefix(s, "refs/heads/") + case strings.HasPrefix(s, "refs/remotes/"): + return strings.TrimPrefix(s, "refs/remotes/") + case strings.HasPrefix(s, "refs/tags/"): + return strings.TrimPrefix(s, "refs/tags/") + default: + return s + } +} + +// IsBranch reports whether the reference points to a local branch. +func (r ReferenceName) IsBranch() bool { + return strings.HasPrefix(string(r), "refs/heads/") +} + +// IsRemote reports whether the reference points to a remote-tracking branch. +func (r ReferenceName) IsRemote() bool { + return strings.HasPrefix(string(r), "refs/remotes/") +} + +// IsTag reports whether the reference points to a tag. +func (r ReferenceName) IsTag() bool { + return strings.HasPrefix(string(r), "refs/tags/") +} + +// Hash wraps a git object id. +type Hash string + +func (h Hash) String() string { return string(h) } + +// Reference stores a resolved git ref and its hash. +type Reference struct { + name ReferenceName + hash Hash +} + +// Name returns the reference name. +func (r *Reference) Name() ReferenceName { return r.name } + +// Hash returns the reference hash. +func (r *Reference) Hash() Hash { return r.hash } + +// NewBranchReferenceName constructs a local branch ref name. +func NewBranchReferenceName(name string) ReferenceName { + if strings.HasPrefix(name, "refs/") { + return ReferenceName(name) + } + return ReferenceName("refs/heads/" + name) +} + +// NewRemoteReferenceName constructs a remote-tracking ref name. +func NewRemoteReferenceName(remote, name string) ReferenceName { + if strings.HasPrefix(name, "refs/") { + return ReferenceName(name) + } + return ReferenceName("refs/remotes/" + remote + "/" + name) +} diff --git a/modules/task/pull_checkout.go b/modules/task/pull_checkout.go index 00353a2e..cd954368 100644 --- a/modules/task/pull_checkout.go +++ b/modules/task/pull_checkout.go @@ -10,10 +10,6 @@ import ( "gitea.dev/tea/modules/config" local_git "gitea.dev/tea/modules/git" - - "github.com/go-git/go-git/v5" - git_config "github.com/go-git/go-git/v5/config" - git_plumbing "github.com/go-git/go-git/v5/plumbing" ) // PullCheckout checkout current workdir to the head branch of specified pull request @@ -76,7 +72,7 @@ func doPRFetch( login *config.Login, pr *gitea.PullRequest, localRepo *local_git.TeaRepo, - localRemote *git.Remote, + localRemote *local_git.Remote, callback func(string) (string, error), ) (string, error) { localRemoteName := localRemote.Config().Name @@ -90,25 +86,23 @@ func doPRFetch( if err != nil { return "", err } - fetchOpts := &git.FetchOptions{Auth: auth} + refspecs := []string{} if isRemoteDeleted(pr) { // When the head branch is already deleted, pr.Head.Ref points to // `refs/pull//head`, where the commits stay available. // This ref must be fetched explicitly, and does not allow pushing, so we use it // only in this case as fallback. localBranchName = fmt.Sprintf("pulls/%d", pr.Index) - fetchOpts.RefSpecs = []git_config.RefSpec{git_config.RefSpec(fmt.Sprintf("%s:refs/remotes/%s/%s", + refspecs = []string{fmt.Sprintf("%s:refs/remotes/%s/%s", pr.Head.Ref, localRemoteName, localBranchName, - ))} + )} } fmt.Printf("Fetching PR %v (head %s:%s) from remote '%s'\n", pr.Index, url, pr.Head.Ref, localRemoteName) - err = localRemote.Fetch(fetchOpts) - if err == git.NoErrAlreadyUpToDate { - fmt.Println(err) - } else if err != nil { + err = localRepo.Fetch(localRemoteName, refspecs, auth) + if err != nil { return "", err } return localBranchName, nil @@ -124,12 +118,12 @@ func doPRCheckout( ) error { // determine the ref to checkout, depending on existence of a matching commit on a local branch var info string - var checkoutRef git_plumbing.ReferenceName + var checkoutRef local_git.ReferenceName if b, _ := localRepo.TeaFindBranchBySha(pr.Head.Sha, remoteURL); b != nil { // if a matching branch exists, use that - checkoutRef = git_plumbing.NewBranchReferenceName(b.Name) + checkoutRef = local_git.NewBranchReferenceName(b.Name) info = fmt.Sprintf("Found matching local branch %s, checking it out", checkoutRef.Short()) } else if forceCreateBranch { @@ -139,10 +133,10 @@ func doPRCheckout( if isRemoteDeleted(pr) { localBranchName += "-" + pr.Head.Ref } - checkoutRef = git_plumbing.NewBranchReferenceName(localBranchName) + checkoutRef = local_git.NewBranchReferenceName(localBranchName) if err := localRepo.TeaCreateBranch(localBranchName, localRemoteBranchName, localRemoteName); err == nil { info = fmt.Sprintf("Created branch '%s'\n", localBranchName) - } else if err == git.ErrBranchExists { + } else if err == local_git.ErrBranchExists { info = "There may be changes since you last checked out, run `git pull` to get them." } else { return err @@ -151,7 +145,7 @@ func doPRCheckout( } else { // use the remote tracking branch - checkoutRef = git_plumbing.NewRemoteReferenceName(localRemoteName, localRemoteBranchName) + checkoutRef = local_git.NewRemoteReferenceName(localRemoteName, localRemoteBranchName) info = fmt.Sprintf( "Checking out remote tracking branch %s. To make changes, create a new branch:\n git checkout %s", checkoutRef.String(), localRemoteBranchName) diff --git a/modules/task/pull_clean.go b/modules/task/pull_clean.go index 62f9a6a7..7561f355 100644 --- a/modules/task/pull_clean.go +++ b/modules/task/pull_clean.go @@ -10,8 +10,6 @@ import ( "gitea.dev/tea/modules/config" local_git "gitea.dev/tea/modules/git" - git_config "github.com/go-git/go-git/v5/config" - git_plumbing "github.com/go-git/go-git/v5/plumbing" ) // PullClean deletes local & remote feature-branches for a closed pull @@ -51,7 +49,7 @@ func PullClean(login *config.Login, repoOwner, repoName string, index int64, ign } // find a branch with matching sha or name, that has a remote matching the repo url - var branch *git_config.Branch + var branch *local_git.Branch if ignoreSHA { branch, err = r.TeaFindBranchByName(remoteBranch, pr.Head.Repository.CloneURL) } else { @@ -77,7 +75,7 @@ call me again with the --ignore-sha flag`, remoteBranch) } if headRef.Name().Short() == branch.Name { fmt.Printf("Checking out '%s' to delete local branch '%s'\n", defaultBranch, branch.Name) - ref := git_plumbing.NewBranchReferenceName(defaultBranch) + ref := local_git.NewBranchReferenceName(defaultBranch) if err = r.TeaCheckout(ref); err != nil { return err } diff --git a/modules/task/repo_clone.go b/modules/task/repo_clone.go index 0e688d88..c940446f 100644 --- a/modules/task/repo_clone.go +++ b/modules/task/repo_clone.go @@ -4,17 +4,12 @@ package task import ( - "fmt" "net/url" "code.gitea.io/sdk/gitea" "gitea.dev/tea/modules/config" local_git "gitea.dev/tea/modules/git" - - "github.com/go-git/go-git/v5" - git_config "github.com/go-git/go-git/v5/config" - "github.com/go-git/go-git/v5/plumbing" ) // RepoClone creates a local git clone in the given path, and sets up upstream remote @@ -46,12 +41,7 @@ func RepoClone( path = repoName } - repo, err := git.PlainClone(path, false, &git.CloneOptions{ - URL: originURL.String(), - Auth: auth, - Depth: depth, - InsecureSkipTLS: login.Insecure, - }) + repo, err := local_git.Clone(path, originURL.String(), auth, depth, login.Insecure) if err != nil { return nil, err } @@ -63,28 +53,15 @@ func RepoClone( return nil, err } upstreamBranch := repoMeta.Parent.DefaultBranch - _, err = repo.CreateRemote(&git_config.RemoteConfig{ - Name: "upstream", - URLs: []string{upstreamURL.String()}, - }) - if err != nil { + if err = repo.AddRemote("upstream", upstreamURL.String()); err != nil { return nil, err } - - repoConf, err := repo.Config() - if err != nil { - return nil, err - } - if b, ok := repoConf.Branches[upstreamBranch]; ok { - b.Remote = "upstream" - b.Merge = plumbing.ReferenceName(fmt.Sprintf("refs/heads/%s", upstreamBranch)) - } - if err = repo.SetConfig(repoConf); err != nil { + if err = repo.SetBranchUpstream(upstreamBranch, "upstream", upstreamBranch); err != nil { return nil, err } } - return &local_git.TeaRepo{Repository: repo}, nil + return repo, nil } func cloneURL(repo *gitea.Repository, login *config.Login) (*url.URL, error) {