From 88a5673194ac34ec0bd347fe732b37a4a0cc2bf1 Mon Sep 17 00:00:00 2001 From: Karol Musur Date: Wed, 15 Jul 2026 19:10:34 +0200 Subject: [PATCH 1/2] fix: upgrade to go-git v6 to support worktreeConfig repos --- AGENTS.md | 7 +- command/list.go | 10 ++- command/open.go | 10 ++- go.mod | 17 ++--- go.sum | 52 ++++----------- repository/errors.go | 6 +- repository/repository.go | 138 +++++++-------------------------------- 7 files changed, 66 insertions(+), 174 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 06ad31b..a0f83a4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -24,10 +24,9 @@ The flow is: `pro.go` (CLI wiring) → `command/` (orchestration + user-facing o - `list.go` — interactive PR/MR picker using `ktr0731/go-fuzzyfinder`. - `auth.go` — prompts for and saves a personal access token. -- **`repository/`** — wraps `go-git` to find the repo and read the origin URL. Two things are done manually rather than through go-git: - - **Worktree support**: `CurrentBranchName` reads and parses the `.git/HEAD` file directly because go-git does not resolve branches correctly inside external worktrees. `makeRepository` detects the `.git/worktrees` path and tracks both the worktree git dir and the real git dir separately. - - **Parent traversal**: `FindInParents` recurses up the directory tree until it finds a git repo or hits the filesystem root. - - Typed errors live in `errors.go` (`ErrNoRemoteOrigin`, `ErrNoActiveBranch`) and are matched with `errors.Is` in `command/`. +- **`repository/`** — wraps `go-git` to find the repo, read the current branch, and read the origin URL. `FindInParents` opens the repo with `PlainOpenWithOptions{DetectDotGit: true}`, which walks up parent directories itself, so no manual traversal is needed. Linked worktrees need no special handling — go-git resolves both HEAD and the shared config from inside a worktree. + - Typed errors live in `errors.go` (`ErrNoRepository`, `ErrNoRemoteOrigin`, `ErrNoActiveBranch`) and are matched with `errors.Is` in `command/`. A detached HEAD or a branch without commits yields `ErrNoActiveBranch`. + - **go-git is pinned to a v6 alpha on purpose.** v5 cannot open any repository whose config sets the `worktreeConfig` extension (git enables it automatically for per-worktree config): v5's `extensionsValidForV0` allow-list keys are camelCase while lookups are lowercased, so `worktreeconfig` misses and `Open` rejects the repo. v5.19.1 is the last v5, and `verifyExtensions` is unconditional, so v6 is the only fix. Don't downgrade to v5. - **`provider/github/` and `provider/gitlab/`** — one file each, self-contained REST clients built on `net/http` (no SDK). Each finds the PR/MR for a branch and lists remote branches, handles pagination, and returns typed sentinel errors (e.g. `ErrNotFound`/`ErrUnauthorized`, `ErrMergeRequestNotFound`/`ErrProjectNotFound`/`ErrTokenExpired`). The two providers do not share an interface — `command/` switches on host and calls each directly. diff --git a/command/list.go b/command/list.go index e2baf9b..7f1f5fd 100644 --- a/command/list.go +++ b/command/list.go @@ -18,8 +18,12 @@ import ( func List(repoPath string, print bool, copy bool) { repo, err := repository.FindInParents(repoPath) if err != nil { - fmt.Fprintln(os.Stderr, color.RedString("Unable to find git repository in given directory or any of parent directories.")) - fmt.Fprintln(os.Stderr, "Please make sure you are in the project directory.") + if errors.Is(err, repository.ErrNoRepository) { + fmt.Fprintln(os.Stderr, color.RedString("Unable to find git repository in given directory or any of parent directories.")) + fmt.Fprintln(os.Stderr, "Please make sure you are in the project directory.") + } else { + fmt.Fprintln(os.Stderr, color.RedString("Unable to open git repository: %s", err.Error())) + } os.Exit(1) } @@ -28,10 +32,10 @@ func List(repoPath string, print bool, copy bool) { if errors.Is(err, repository.ErrNoRemoteOrigin) { fmt.Fprintln(os.Stderr, color.RedString("No remote named \"origin\" found.")) fmt.Fprintln(os.Stderr, "Please make sure you have a remote named \"origin\".") - os.Exit(1) } else { fmt.Fprintln(os.Stderr, color.RedString("Unable to get origin URL: %s", err.Error())) } + os.Exit(1) } gitURL, err := giturl.Parse(originURL) diff --git a/command/open.go b/command/open.go index 202a77f..3bb83a2 100644 --- a/command/open.go +++ b/command/open.go @@ -21,8 +21,12 @@ import ( func Open(repoPath string, print bool, copy bool) { repo, err := repository.FindInParents(repoPath) if err != nil { - fmt.Fprintln(os.Stderr, color.RedString("Unable to find git repository in given directory or any of parent directories.")) - fmt.Fprintln(os.Stderr, "Please make sure you are in the project directory.") + if errors.Is(err, repository.ErrNoRepository) { + fmt.Fprintln(os.Stderr, color.RedString("Unable to find git repository in given directory or any of parent directories.")) + fmt.Fprintln(os.Stderr, "Please make sure you are in the project directory.") + } else { + fmt.Fprintln(os.Stderr, color.RedString("Unable to open git repository: %s", err.Error())) + } os.Exit(1) } @@ -31,10 +35,10 @@ func Open(repoPath string, print bool, copy bool) { if errors.Is(err, repository.ErrNoRemoteOrigin) { fmt.Fprintln(os.Stderr, color.RedString("No remote named \"origin\" found.")) fmt.Fprintln(os.Stderr, "Please make sure you have a remote named \"origin\".") - os.Exit(1) } else { fmt.Fprintln(os.Stderr, color.RedString("Unable to get origin URL: %s", err.Error())) } + os.Exit(1) } gitURL, err := giturl.Parse(originURL) diff --git a/go.mod b/go.mod index 1eae3bf..05ab714 100644 --- a/go.mod +++ b/go.mod @@ -5,8 +5,7 @@ go 1.26.4 require ( github.com/atotto/clipboard v0.1.4 github.com/fatih/color v1.19.0 - github.com/go-git/go-billy/v5 v5.9.0 - github.com/go-git/go-git/v5 v5.19.1 + github.com/go-git/go-git/v6 v6.0.0-alpha.4 github.com/ktr0731/go-fuzzyfinder v0.9.0 github.com/mitchellh/go-homedir v1.1.0 github.com/urfave/cli/v2 v2.27.7 @@ -15,21 +14,19 @@ require ( ) require ( - dario.cat/mergo v1.0.2 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/ProtonMail/go-crypto v1.4.1 // 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/emirpasic/gods v1.18.1 // indirect github.com/gdamore/encoding v1.0.1 // indirect github.com/gdamore/tcell/v2 v2.13.10 // indirect - github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect - github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect - github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect + github.com/go-git/gcfg/v2 v2.0.2 // indirect + github.com/go-git/go-billy/v6 v6.0.0-alpha.1 // 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/ktr0731/go-ansisgr v0.1.0 // indirect github.com/lucasb-eyer/go-colorful v1.4.0 // indirect github.com/mattn/go-colorable v0.1.15 // indirect @@ -39,14 +36,14 @@ require ( github.com/pjbgf/sha1cd v0.6.0 // indirect github.com/pkg/errors v0.9.1 // 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/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 // indirect golang.org/x/crypto v0.52.0 // indirect golang.org/x/net v0.55.0 // indirect + golang.org/x/sync v0.20.0 // indirect golang.org/x/sys v0.46.0 // indirect golang.org/x/text v0.37.0 // indirect - gopkg.in/warnings.v0 v0.1.2 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect ) diff --git a/go.sum b/go.sum index 0615194..94b4f4f 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,3 @@ -dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= -dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= -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= @@ -17,13 +14,10 @@ github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg 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/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE= -github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -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/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= @@ -34,27 +28,24 @@ github.com/gdamore/tcell/v2 v2.13.10 h1:Afs3JKt83HnhuUKdZ3MnxUgOqQRWftj5JyDqv1LL github.com/gdamore/tcell/v2 v2.13.10/go.mod h1:+Wfe208WDdB7INEtCsNrAN6O2m+wsTPk1RAovjaILlo= github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= -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/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/go-git/gcfg/v2 v2.0.2 h1:MY5SIIfTGGEMhdA7d7JePuVVxtKL7Hp+ApGDJAJ7dpo= +github.com/go-git/gcfg/v2 v2.0.2/go.mod h1:/lv2NsxvhepuMrldsFilrgct6pxzpGdSRC13ydTLSLs= +github.com/go-git/go-billy/v6 v6.0.0-alpha.1 h1:xVjAR4oUvrKy7/Xuw/lLlV3gkxR3KO2H8W+MamuVVsQ= +github.com/go-git/go-billy/v6 v6.0.0-alpha.1/go.mod h1:eaCUpHbedW7//EwcYmUDfJe2N6sJC9O12AT0OTqJR1E= +github.com/go-git/go-git-fixtures/v6 v6.0.0-alpha.1 h1:gmqi2jvsreu0s8JMLylYDFq4sbjHwwlhktMw0DUg3mA= +github.com/go-git/go-git-fixtures/v6 v6.0.0-alpha.1/go.mod h1:ECf1MqJlBdYpKggBrOXjo/0EnvRZx6D++I86UYjPgAQ= +github.com/go-git/go-git/v6 v6.0.0-alpha.4 h1:aDTc2UGanmaE7FkGLSlBEB9nohMnQ+RKXcfq/D+esDQ= +github.com/go-git/go-git/v6 v6.0.0-alpha.4/go.mod h1:4ODa/G7hPWrh4Y+7lmt59Ij3zW38IEfvRoAZxLYYBhc= 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/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -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= @@ -78,49 +69,39 @@ github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/nsf/termbox-go v1.1.1 h1:nksUPLCb73Q++DwbYUBEglYBRPZyoXJdrj5L+TkjyZY= github.com/nsf/termbox-go v1.1.1/go.mod h1:T0cTdVuOwf7pHQNtfhnEbzHbcNyCEcVU4YPpouCbVxo= -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/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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.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= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -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.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU= github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4= -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/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 h1:FnBeRrxr7OU4VvAzt5X7s6266i6cSVkkFPS0TuXWbIg= github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -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= -golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/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.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= @@ -128,14 +109,12 @@ golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +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-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-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= @@ -147,7 +126,6 @@ golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc= golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= 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.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= @@ -162,8 +140,6 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 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 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/repository/errors.go b/repository/errors.go index 653d54b..9df4d6d 100644 --- a/repository/errors.go +++ b/repository/errors.go @@ -3,7 +3,7 @@ package repository import "errors" var ( - ErrNoActiveBranch = errors.New("no active branch") - ErrNoRemoteOrigin = errors.New("no remote named \"origin\" found") - ErrUnableToReadGitFile = errors.New("unable to read .git file") + ErrNoRepository = errors.New("no git repository found") + ErrNoActiveBranch = errors.New("no active branch") + ErrNoRemoteOrigin = errors.New("no remote named \"origin\" found") ) diff --git a/repository/repository.go b/repository/repository.go index eb8b9df..1df98d9 100644 --- a/repository/repository.go +++ b/repository/repository.go @@ -2,150 +2,62 @@ package repository import ( "errors" - "os" - "path/filepath" - "strings" - "github.com/go-git/go-billy/v5/helper/chroot" - "github.com/go-git/go-git/v5" - "github.com/go-git/go-git/v5/storage/filesystem" + "github.com/go-git/go-git/v6" + "github.com/go-git/go-git/v6/plumbing" ) type Repository struct { - workingDirectory string - - // Root git directory, usually workingDirectory/.git - gitDirectory string - - // Will be different from gitDirectory if the repository is an external worktree - // e.g. sample-repo/.git/worktrees/sample-repo-external - // See: https://git-scm.com/docs/git-worktree - worktreeGitDirectory string + goGitRepository *git.Repository } // Return git repository in given directory or parent directories. func FindInParents(path string) (Repository, error) { - windowsRootPath := filepath.VolumeName(path) + "\\" - - absolutePath, err := filepath.Abs(path) + goGitRepository, err := git.PlainOpenWithOptions(path, &git.PlainOpenOptions{DetectDotGit: true}) if err != nil { - return Repository{}, err - } - - _, err = git.PlainOpen(absolutePath) - // Found valid git repository - if err == nil { - return makeRepository(absolutePath) - } - - if errors.Is(err, git.ErrRepositoryNotExists) { - // Base case - we've reached the root of the filesystem - if absolutePath == "/" || absolutePath == windowsRootPath { - return Repository{}, errors.New("no git repository found") + if errors.Is(err, git.ErrRepositoryNotExists) { + return Repository{}, ErrNoRepository } - // Recurse to parent directory - return FindInParents(filepath.Dir(absolutePath)) - } - - return Repository{}, err -} - -func makeRepository(workingDirectory string) (Repository, error) { - goGitRepository, err := git.PlainOpen(workingDirectory) - if err != nil { return Repository{}, err } - // Get the resolved git directory for this working tree. For a linked - // worktree this is /.git/worktrees/; otherwise it's the - // repository's .git directory. - storage, ok := goGitRepository.Storer.(*filesystem.Storage) - if !ok { - return Repository{}, errors.New("storage is not filesystem") - } - filesystem, ok := storage.Filesystem().(*chroot.ChrootHelper) - if !ok { - return Repository{}, errors.New("filesystem is not ChrootHelper") - } - gitDirectory := filesystem.Root() - - // The per-worktree git directory is where this working tree's HEAD lives. - worktreeGitDirectory := gitDirectory - - // If a "commondir" file is present, we are inside a linked worktree and it - // points to the shared git directory holding config and remotes. - commonDir, err := readWorktreeCommonDir(gitDirectory) - if err != nil { - return Repository{}, err - } - if commonDir != "" { - gitDirectory = commonDir - } - - return Repository{ - workingDirectory: workingDirectory, - gitDirectory: gitDirectory, - worktreeGitDirectory: worktreeGitDirectory, - }, nil -} - -// readWorktreeCommonDir returns the resolved common (shared) git directory when -// gitDir belongs to a linked worktree, or "" when it does not. Presence of a -// "commondir" file in gitDir is git's own signal that this is a linked worktree. -func readWorktreeCommonDir(gitDir string) (string, error) { - contents, err := os.ReadFile(filepath.Join(gitDir, "commondir")) - if errors.Is(err, os.ErrNotExist) { - return "", nil - } - if err != nil { - return "", err - } - - commonDir := strings.TrimSpace(string(contents)) - if commonDir == "" { - return "", nil - } - - // commondir may be absolute or relative to the worktree git directory. - if !filepath.IsAbs(commonDir) { - commonDir = filepath.Join(gitDir, commonDir) - } - - return filepath.Clean(commonDir), nil + return Repository{goGitRepository: goGitRepository}, nil } func (repo *Repository) CurrentBranchName() (string, error) { - // Get HEAD file contents from git directory - // We can't use go-git to get the current branch because it doesn't support worktrees - headFile, err := os.ReadFile(filepath.Join(repo.worktreeGitDirectory, "HEAD")) + head, err := repo.goGitRepository.Head() if err != nil { - return "", errors.New("unable to read HEAD") + // HEAD points at a branch without commits yet + if errors.Is(err, plumbing.ErrReferenceNotFound) { + return "", ErrNoActiveBranch + } + + return "", err } - // Parse HEAD file to get branch name - headFileSplit := strings.Split(string(headFile), "ref: refs/heads/") - if len(headFileSplit) != 2 { - // if the HEAD file doesn't include "ref: refs/heads/", we are not on a branch + // HEAD is detached, so it doesn't point at a branch + if !head.Name().IsBranch() { return "", ErrNoActiveBranch } - branch := strings.TrimSpace(headFileSplit[1]) - return branch, nil + return head.Name().Short(), nil } func (repo *Repository) OriginUrl() (string, error) { - goGitRepo, err := git.PlainOpen(repo.gitDirectory) + origin, err := repo.goGitRepository.Remote("origin") if err != nil { + if errors.Is(err, git.ErrRemoteNotFound) { + return "", ErrNoRemoteOrigin + } + return "", err } - // check if there is a remote named origin - origin, err := goGitRepo.Remote("origin") - if err != nil { + urls := origin.Config().URLs + if len(urls) == 0 { return "", ErrNoRemoteOrigin } - originURL := origin.Config().URLs[0] - return originURL, nil + return urls[0], nil } From 773761ef7a37912a48cfd06c406e1cca2acc2d60 Mon Sep 17 00:00:00 2001 From: Karol Musur Date: Wed, 15 Jul 2026 19:20:43 +0200 Subject: [PATCH 2/2] fix: review --- repository/repository.go | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/repository/repository.go b/repository/repository.go index 1df98d9..b2f2a83 100644 --- a/repository/repository.go +++ b/repository/repository.go @@ -26,22 +26,18 @@ func FindInParents(path string) (Repository, error) { } func (repo *Repository) CurrentBranchName() (string, error) { - head, err := repo.goGitRepository.Head() + // Read HEAD without resolving it, only the branch name is needed + head, err := repo.goGitRepository.Reference(plumbing.HEAD, false) if err != nil { - // HEAD points at a branch without commits yet - if errors.Is(err, plumbing.ErrReferenceNotFound) { - return "", ErrNoActiveBranch - } - return "", err } - // HEAD is detached, so it doesn't point at a branch - if !head.Name().IsBranch() { + // HEAD holds a hash instead of pointing at a branch, so it is detached + if head.Type() != plumbing.SymbolicReference || !head.Target().IsBranch() { return "", ErrNoActiveBranch } - return head.Name().Short(), nil + return head.Target().Short(), nil } func (repo *Repository) OriginUrl() (string, error) {