Compare commits

...

207 commits

Author SHA1 Message Date
Masayuki Matsuki 2474201bcc
Merge pull request #540 from umekikazuya/document-clone-config
Some checks failed
integration / test (push) Has been cancelled
tagpr / tagpr (push) Has been cancelled
test / test (macOS-latest) (push) Has been cancelled
test / test (ubuntu-latest) (push) Has been cancelled
test / test (windows-latest) (push) Has been cancelled
docs: mention clone.defaultRemoteName
2026-08-22 13:58:52 +09:00
Kazuya Umeki b221561f45
docs: mention clone.defaultRemoteName 2026-08-16 03:00:38 +09:00
Masayuki Matsuki 9d181bfa4b
Merge pull request #531 from x-motemen/dependabot/github_actions/crate-ci/typos-1.48.0
Some checks failed
integration / test (push) Has been cancelled
tagpr / tagpr (push) Has been cancelled
test / test (macOS-latest) (push) Has been cancelled
test / test (ubuntu-latest) (push) Has been cancelled
test / test (windows-latest) (push) Has been cancelled
2026-07-11 02:45:52 +09:00
Masayuki Matsuki daef1aadff
Merge pull request #530 from x-motemen/dependabot/github_actions/actions-9b7bde0b82 2026-07-11 02:45:31 +09:00
Masayuki Matsuki 58ae404952
Merge pull request #529 from x-motemen/dependabot/go_modules/golang-x-8f304dcb3e 2026-07-11 02:45:13 +09:00
dependabot[bot] f5e8c9f2d6
chore(deps): bump crate-ci/typos from 1.47.2 to 1.48.0
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.47.2 to 1.48.0.
- [Release notes](https://github.com/crate-ci/typos/releases)
- [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md)
- [Commits](37bb98842b...bee27e3a4f)

---
updated-dependencies:
- dependency-name: crate-ci/typos
  dependency-version: 1.48.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-10 12:42:15 +00:00
dependabot[bot] b6861f0e51
chore(deps): bump actions/setup-go in the actions group
Bumps the actions group with 1 update: [actions/setup-go](https://github.com/actions/setup-go).


Updates `actions/setup-go` from 6.4.0 to 6.5.0
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](4a3601121d...924ae3a1cd)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-version: 6.5.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-10 12:42:00 +00:00
dependabot[bot] 274da4a759
chore(deps): bump the golang-x group with 3 updates
Bumps the golang-x group with 3 updates: [golang.org/x/net](https://github.com/golang/net), [golang.org/x/sync](https://github.com/golang/sync) and [golang.org/x/text](https://github.com/golang/text).


Updates `golang.org/x/net` from 0.54.0 to 0.57.0
- [Commits](https://github.com/golang/net/compare/v0.54.0...v0.57.0)

Updates `golang.org/x/sync` from 0.20.0 to 0.22.0
- [Commits](https://github.com/golang/sync/compare/v0.20.0...v0.22.0)

Updates `golang.org/x/text` from 0.37.0 to 0.40.0
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.37.0...v0.40.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.57.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: golang-x
- dependency-name: golang.org/x/sync
  dependency-version: 0.22.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: golang-x
- dependency-name: golang.org/x/text
  dependency-version: 0.40.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: golang-x
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-10 12:41:53 +00:00
Masayuki Matsuki 5e4f3de9fc
Merge pull request #528 from x-motemen/group-dependabot-updates
Some checks are pending
integration / test (push) Waiting to run
tagpr / tagpr (push) Waiting to run
test / test (macOS-latest) (push) Waiting to run
test / test (ubuntu-latest) (push) Waiting to run
test / test (windows-latest) (push) Waiting to run
Group Dependabot updates for golang.org/x and actions
2026-07-10 21:41:18 +09:00
Songmu d5bad01dff
Group Dependabot updates for golang.org/x and actions
Group golang.org/x/* Go module updates and actions/* GitHub Actions
updates into single grouped pull requests respectively.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 7be4cd47-9ad4-4e96-8ef4-af53bd5fad6c
2026-07-10 21:24:02 +09:00
Masayuki Matsuki 743f5e5a5f
Merge pull request #511 from x-motemen/dependabot/github_actions/crate-ci/typos-1.47.2
chore(deps): bump crate-ci/typos from 1.46.1 to 1.47.2
2026-07-10 21:18:44 +09:00
Masayuki Matsuki a24083e6ca
Merge pull request #509 from x-motemen/dependabot/github_actions/Songmu/tagpr-1.20.0
chore(deps): bump Songmu/tagpr from 1.19.0 to 1.20.0
2026-07-10 21:18:25 +09:00
Masayuki Matsuki d092abdbc7
Merge pull request #512 from x-motemen/dependabot/github_actions/codecov/codecov-action-7.0.0
chore(deps): bump codecov/codecov-action from 6.0.0 to 7.0.0
2026-07-10 21:18:06 +09:00
Masayuki Matsuki 4b6891a472
Merge pull request #520 from x-motemen/dependabot/github_actions/actions/checkout-7.0.0
chore(deps): bump actions/checkout from 6.0.2 to 7.0.0
2026-07-10 21:17:38 +09:00
Masayuki Matsuki 468bfe6cdb
Merge pull request #523 from x-motemen/dependabot/github_actions/reviewdog/action-staticcheck-1.29.0
chore(deps): bump reviewdog/action-staticcheck from 1.28.0 to 1.29.0
2026-07-10 21:17:23 +09:00
Masayuki Matsuki 27f9bd6efb
Merge pull request #524 from x-motemen/dependabot/go_modules/github.com/urfave/cli/v3-3.10.1
chore(deps): bump github.com/urfave/cli/v3 from 3.8.0 to 3.10.1
2026-07-10 21:17:05 +09:00
Masayuki Matsuki e89f3a5e8b
Merge pull request #526 from hezhizhen/update-completion
feat(completion): update bash/zsh/fish completions
2026-07-10 21:16:35 +09:00
Masayuki Matsuki df13d57bd2
Merge pull request #514 from hezhizhen/fix-push-branch
fix: correct branch name
2026-07-10 21:16:05 +09:00
Zhizhen He b75675d37c feat(completion): update bash/zsh/fish completions 2026-07-08 17:25:23 +08:00
Masayuki Matsuki 54bdb6d41f
Merge pull request #525 from ryoppippi/ryoppippi/nix
Some checks failed
integration / test (push) Has been cancelled
tagpr / tagpr (push) Has been cancelled
docs: add nix
2026-07-06 00:21:24 +09:00
ryoppippi 26231fbe6e
docs: add nix 2026-07-05 16:09:00 +01:00
dependabot[bot] 5d175304f6
chore(deps): bump github.com/urfave/cli/v3 from 3.8.0 to 3.10.1
Bumps [github.com/urfave/cli/v3](https://github.com/urfave/cli) from 3.8.0 to 3.10.1.
- [Release notes](https://github.com/urfave/cli/releases)
- [Changelog](https://github.com/urfave/cli/blob/main/docs/CHANGELOG.md)
- [Commits](https://github.com/urfave/cli/compare/v3.8.0...v3.10.1)

---
updated-dependencies:
- dependency-name: github.com/urfave/cli/v3
  dependency-version: 3.10.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-29 09:12:47 +00:00
dependabot[bot] de2f1d0c1a
chore(deps): bump reviewdog/action-staticcheck from 1.28.0 to 1.29.0
Bumps [reviewdog/action-staticcheck](https://github.com/reviewdog/action-staticcheck) from 1.28.0 to 1.29.0.
- [Release notes](https://github.com/reviewdog/action-staticcheck/releases)
- [Commits](50f9bfcf37...564f18c629)

---
updated-dependencies:
- dependency-name: reviewdog/action-staticcheck
  dependency-version: 1.29.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-29 09:12:46 +00:00
dependabot[bot] 2c78efff02
chore(deps): bump actions/checkout from 6.0.2 to 7.0.0
Bumps [actions/checkout](https://github.com/actions/checkout) from 6.0.2 to 7.0.0.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](de0fac2e45...9c091bb21b)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-22 09:13:05 +00:00
Masayuki Matsuki d014ab3965
Merge pull request #508 from mcha-forks/master
Some checks are pending
integration / test (push) Waiting to run
tagpr / tagpr (push) Waiting to run
feat(completion): update fish completions
2026-06-11 00:32:45 +09:00
Zhizhen He ac1694b66c fix: correct branch name 2026-06-09 15:17:37 +08:00
dependabot[bot] 927ff49ab7
chore(deps): bump codecov/codecov-action from 6.0.0 to 7.0.0
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 6.0.0 to 7.0.0.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](57e3a136b7...fb8b3582c8)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-08 09:15:10 +00:00
dependabot[bot] cadcad0ab9
chore(deps): bump crate-ci/typos from 1.46.1 to 1.47.2
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.46.1 to 1.47.2.
- [Release notes](https://github.com/crate-ci/typos/releases)
- [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md)
- [Commits](5374cbf686...37bb98842b)

---
updated-dependencies:
- dependency-name: crate-ci/typos
  dependency-version: 1.47.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-08 09:15:04 +00:00
dependabot[bot] 9be4e0f352
chore(deps): bump Songmu/tagpr from 1.19.0 to 1.20.0
Bumps [Songmu/tagpr](https://github.com/songmu/tagpr) from 1.19.0 to 1.20.0.
- [Release notes](https://github.com/songmu/tagpr/releases)
- [Changelog](https://github.com/Songmu/tagpr/blob/main/CHANGELOG.md)
- [Commits](555e72cee6...e84001bd5d)

---
updated-dependencies:
- dependency-name: Songmu/tagpr
  dependency-version: 1.20.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-02 03:36:50 +00:00
Zephyr Lykos d968bd3e48
feat(completion): update fish completions 2026-05-28 10:25:37 +08:00
Masayuki Matsuki f0ea1c9648
Merge pull request #499 from x-motemen/dependabot/go_modules/golang.org/x/net-0.54.0
chore(deps): bump golang.org/x/net from 0.53.0 to 0.54.0
2026-05-12 11:21:52 +09:00
dependabot[bot] 39912f3fe5
chore(deps): bump golang.org/x/net from 0.53.0 to 0.54.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.53.0 to 0.54.0.
- [Commits](https://github.com/golang/net/compare/v0.53.0...v0.54.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.54.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-12 02:11:37 +00:00
Masayuki Matsuki 687e7ae85c
Merge pull request #500 from x-motemen/dependabot/go_modules/golang.org/x/text-0.37.0
chore(deps): bump golang.org/x/text from 0.36.0 to 0.37.0
2026-05-12 11:10:26 +09:00
Masayuki Matsuki 182ca13043
Merge pull request #501 from x-motemen/dependabot/github_actions/crate-ci/typos-1.46.1
chore(deps): bump crate-ci/typos from 1.45.0 to 1.46.1
2026-05-12 11:10:13 +09:00
Masayuki Matsuki 83b68bdfbb
Merge pull request #498 from x-motemen/dependabot/github_actions/Songmu/tagpr-1.19.0
chore(deps): bump Songmu/tagpr from 1.18.2 to 1.19.0
2026-05-12 11:09:53 +09:00
Masayuki Matsuki e1bf2f7a47
Merge pull request #495 from x-motemen/dependabot/go_modules/github.com/mattn/go-isatty-0.0.22
chore(deps): bump github.com/mattn/go-isatty from 0.0.21 to 0.0.22
2026-05-12 11:09:39 +09:00
dependabot[bot] d602bc06b3
chore(deps): bump crate-ci/typos from 1.45.0 to 1.46.1
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.45.0 to 1.46.1.
- [Release notes](https://github.com/crate-ci/typos/releases)
- [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md)
- [Commits](02ea592e44...5374cbf686)

---
updated-dependencies:
- dependency-name: crate-ci/typos
  dependency-version: 1.46.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-11 12:55:36 +00:00
dependabot[bot] 1307087804
chore(deps): bump golang.org/x/text from 0.36.0 to 0.37.0
Bumps [golang.org/x/text](https://github.com/golang/text) from 0.36.0 to 0.37.0.
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.36.0...v0.37.0)

---
updated-dependencies:
- dependency-name: golang.org/x/text
  dependency-version: 0.37.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-11 12:55:33 +00:00
dependabot[bot] 5f27c38fab
chore(deps): bump Songmu/tagpr from 1.18.2 to 1.19.0
Bumps [Songmu/tagpr](https://github.com/songmu/tagpr) from 1.18.2 to 1.19.0.
- [Release notes](https://github.com/songmu/tagpr/releases)
- [Changelog](https://github.com/Songmu/tagpr/blob/main/CHANGELOG.md)
- [Commits](84850af451...555e72cee6)

---
updated-dependencies:
- dependency-name: Songmu/tagpr
  dependency-version: 1.19.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-11 12:55:24 +00:00
dependabot[bot] d91aa22c47
chore(deps): bump github.com/mattn/go-isatty from 0.0.21 to 0.0.22
Bumps [github.com/mattn/go-isatty](https://github.com/mattn/go-isatty) from 0.0.21 to 0.0.22.
- [Commits](https://github.com/mattn/go-isatty/compare/v0.0.21...v0.0.22)

---
updated-dependencies:
- dependency-name: github.com/mattn/go-isatty
  dependency-version: 0.0.22
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-27 10:37:38 +00:00
Masayuki Matsuki 4f1c46dec6
Merge pull request #487 from x-motemen/dependabot/go_modules/github.com/urfave/cli/v3-3.8.0
chore(deps): bump github.com/urfave/cli/v3 from 3.7.0 to 3.8.0
2026-04-19 21:18:06 +09:00
Masayuki Matsuki 9e24f0b0c2
Merge pull request #484 from x-motemen/dependabot/github_actions/Songmu/tagpr-1.18.2
chore(deps): bump Songmu/tagpr from 1.18.1 to 1.18.2
2026-04-19 21:17:49 +09:00
Masayuki Matsuki 7c228ec3ce
Merge pull request #486 from x-motemen/dependabot/go_modules/github.com/mattn/go-isatty-0.0.21
chore(deps): bump github.com/mattn/go-isatty from 0.0.20 to 0.0.21
2026-04-19 21:17:30 +09:00
dependabot[bot] 88811c2645
chore(deps): bump github.com/mattn/go-isatty from 0.0.20 to 0.0.21
Bumps [github.com/mattn/go-isatty](https://github.com/mattn/go-isatty) from 0.0.20 to 0.0.21.
- [Commits](https://github.com/mattn/go-isatty/compare/v0.0.20...v0.0.21)

---
updated-dependencies:
- dependency-name: github.com/mattn/go-isatty
  dependency-version: 0.0.21
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-19 08:37:19 +00:00
dependabot[bot] 336b7790d3
chore(deps): bump github.com/urfave/cli/v3 from 3.7.0 to 3.8.0
Bumps [github.com/urfave/cli/v3](https://github.com/urfave/cli) from 3.7.0 to 3.8.0.
- [Release notes](https://github.com/urfave/cli/releases)
- [Changelog](https://github.com/urfave/cli/blob/main/docs/CHANGELOG.md)
- [Commits](https://github.com/urfave/cli/compare/v3.7.0...v3.8.0)

---
updated-dependencies:
- dependency-name: github.com/urfave/cli/v3
  dependency-version: 3.8.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-19 08:37:16 +00:00
Masayuki Matsuki 112f9b2a9d
Merge pull request #485 from x-motemen/dependabot/go_modules/golang.org/x/net-0.53.0
chore(deps): bump golang.org/x/net from 0.52.0 to 0.53.0
2026-04-19 17:36:16 +09:00
Masayuki Matsuki bbe6528a13
Merge pull request #489 from nurazon59/test/fix-rm-command-noop
fix(test): exercise `ghq rm` in command tests
2026-04-19 17:35:25 +09:00
Masayuki Matsuki 5c17f32b34
Merge pull request #490 from sciencesakura/improve-bash-completions
feat(completion): improve bash completions.
2026-04-19 17:33:59 +09:00
sciencesakura 4e51d4d660 fix(completion): does not suggest repositories after the --branch option 2026-04-19 14:50:12 +09:00
nurazon59 b9fd98e162 fix(test): run ghq rm dry-run test cases 2026-04-18 22:10:56 +09:00
sciencesakura e3f7e3d035 feat(completion): improve bash completions.
Now, all commands and options are available.
2026-04-18 17:45:28 +09:00
sciencesakura 41e6d08a74 style(completion): format misc/bash/_ghq. 2026-04-18 16:36:06 +09:00
nurazon59 9201500760 fix(test): add testcase and fix testcase 2026-04-16 01:13:35 +09:00
nurazon59 f6b96648d8 fix(test): Fix rm command tests to exercise command execution 2026-04-16 01:04:35 +09:00
dependabot[bot] 0a809d8b51
chore(deps): bump golang.org/x/net from 0.52.0 to 0.53.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.52.0 to 0.53.0.
- [Commits](https://github.com/golang/net/compare/v0.52.0...v0.53.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.53.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-13 09:51:19 +00:00
dependabot[bot] 439da3bd1d
chore(deps): bump Songmu/tagpr from 1.18.1 to 1.18.2
Bumps [Songmu/tagpr](https://github.com/songmu/tagpr) from 1.18.1 to 1.18.2.
- [Release notes](https://github.com/songmu/tagpr/releases)
- [Changelog](https://github.com/Songmu/tagpr/blob/main/CHANGELOG.md)
- [Commits](9d0f650553...84850af451)

---
updated-dependencies:
- dependency-name: Songmu/tagpr
  dependency-version: 1.18.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-13 09:51:15 +00:00
Masayuki Matsuki f600896542
Merge pull request #483 from x-motemen/tagpr-from-v1.10.0 2026-04-11 23:37:22 +09:00
github-actions[bot] 500a99a5e9
[tagpr] update CHANGELOG.md 2026-04-11 14:14:09 +00:00
github-actions[bot] f51d964aa9
[tagpr] prepare for the next release 2026-04-11 14:14:08 +00:00
Masayuki Matsuki 3b1f4ad5a1
Merge pull request #481 from chris-monardo/chrism/rm-worktree
feat(rm): make ghq rm worktree-aware
2026-04-11 23:13:46 +09:00
Chris Monardo 1ce2e915fb address comments 2026-04-11 07:45:04 -04:00
Masayuki Matsuki 8506f1f526
Return an error instead of logging it when failing to delete the worktree.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-11 16:59:05 +09:00
Masayuki Matsuki ec14d34d39
Merge pull request #455 from x-motemen/tagpr-from-v1.9.4
Release for v1.10.0
2026-04-09 17:36:23 +09:00
github-actions[bot] b6ab1e9a45
[tagpr] update CHANGELOG.md 2026-04-09 07:41:23 +00:00
github-actions[bot] 12ffcb2d6c
[tagpr] prepare for the next release 2026-04-09 07:41:22 +00:00
Masayuki Matsuki 385a9940bb
Merge pull request #457 from KeitaShimura/feature/get-print-flag
feat(get): output local repository path to stdout
2026-04-09 16:40:57 +09:00
Chris Monardo 8a99bf1670 Make ghq rm worktree-aware
ghq rm previously used os.RemoveAll with no worktree awareness, which
caused two problems: removing a linked worktree left a dangling entry
in the parent repo's .git/worktrees/, and removing a repo that had
linked worktrees orphaned all of them.

Now ghq rm detects both scenarios:
- If the target is a linked worktree, use git worktree remove to
  properly unregister it from the parent repo.
- If the target repo has linked worktrees, prune each one before
  removing the main repo.

Extract shared worktree helpers into worktree.go so both cmd_rm.go
and cmd_migrate.go can reuse them.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 02:57:37 -04:00
KeitaShimura 435ed0ec87 feat: remove --print flag and print local path by default 2026-04-09 15:24:09 +09:00
Masayuki Matsuki 723bf49fd6
Merge branch 'master' into feature/get-print-flag 2026-04-08 14:10:13 +09:00
Masayuki Matsuki c33ed436cf
Merge pull request #479 from x-motemen/dependabot/github_actions/crate-ci/typos-1.45.0
chore(deps): bump crate-ci/typos from 1.44.0 to 1.45.0
2026-04-08 14:03:31 +09:00
Masayuki Matsuki c1ee89a04f
Merge pull request #476 from x-motemen/dependabot/github_actions/codecov/codecov-action-6.0.0
chore(deps): bump codecov/codecov-action from 5.5.2 to 6.0.0
2026-04-08 14:03:18 +09:00
Masayuki Matsuki 43600f28a6
Merge pull request #477 from x-motemen/dependabot/github_actions/reviewdog/action-actionlint-1.72.0
chore(deps): bump reviewdog/action-actionlint from 1.71.0 to 1.72.0
2026-04-08 14:02:43 +09:00
Masayuki Matsuki d25a363d0a
Merge pull request #475 from x-motemen/dependabot/github_actions/actions/setup-go-6.4.0
chore(deps): bump actions/setup-go from 6.3.0 to 6.4.0
2026-04-08 14:02:27 +09:00
Masayuki Matsuki 6a0021954f
Merge pull request #473 from hezhizhen/upgrade-cli
Update urfave/cli/v2 to v3
2026-04-08 13:59:45 +09:00
Songmu d45b3d0e95
fix test.yaml 2026-04-08 13:55:32 +09:00
Masayuki Matsuki bdace9708d
Merge pull request #472 from sciencesakura/support-default-host
feat(get): add `ghq.defaultHost` config to resolve the default host.
2026-04-08 13:53:58 +09:00
Masayuki Matsuki d284552480
Merge pull request #478 from x-motemen/dependabot/github_actions/Songmu/tagpr-1.18.1
chore(deps): bump Songmu/tagpr from 1.17.1 to 1.18.1
2026-04-08 13:50:35 +09:00
Masayuki Matsuki a8c8f050e9
Merge pull request #469 from nurazon59/feat/completion-ghq-rm-list
feat(completion): add ghq list completion to ghq rm in fish/zsh
2026-04-08 13:50:08 +09:00
dependabot[bot] bdf5b4c80c
chore(deps): bump crate-ci/typos from 1.44.0 to 1.45.0
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.44.0 to 1.45.0.
- [Release notes](https://github.com/crate-ci/typos/releases)
- [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md)
- [Commits](631208b7aa...02ea592e44)

---
updated-dependencies:
- dependency-name: crate-ci/typos
  dependency-version: 1.45.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-06 09:31:29 +00:00
dependabot[bot] 54915f57a6
chore(deps): bump Songmu/tagpr from 1.17.1 to 1.18.1
Bumps [Songmu/tagpr](https://github.com/songmu/tagpr) from 1.17.1 to 1.18.1.
- [Release notes](https://github.com/songmu/tagpr/releases)
- [Changelog](https://github.com/Songmu/tagpr/blob/main/CHANGELOG.md)
- [Commits](b3fb894246...9d0f650553)

---
updated-dependencies:
- dependency-name: Songmu/tagpr
  dependency-version: 1.18.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-06 09:31:20 +00:00
dependabot[bot] 9c3659fffe
chore(deps): bump reviewdog/action-actionlint from 1.71.0 to 1.72.0
Bumps [reviewdog/action-actionlint](https://github.com/reviewdog/action-actionlint) from 1.71.0 to 1.72.0.
- [Release notes](https://github.com/reviewdog/action-actionlint/releases)
- [Commits](0d952c597e...6fb7acc99f)

---
updated-dependencies:
- dependency-name: reviewdog/action-actionlint
  dependency-version: 1.72.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-06 09:31:15 +00:00
dependabot[bot] 03b8efaa81
chore(deps): bump codecov/codecov-action from 5.5.2 to 6.0.0
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 5.5.2 to 6.0.0.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](671740ac38...57e3a136b7)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-version: 6.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-30 09:45:57 +00:00
dependabot[bot] bd68bf70aa
chore(deps): bump actions/setup-go from 6.3.0 to 6.4.0
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 6.3.0 to 6.4.0.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](4b73464bb3...4a3601121d)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-version: 6.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-30 09:45:51 +00:00
Zhizhen He 3425b00fbc Enable suggest 2026-03-19 14:54:00 +08:00
Zhizhen He 7dff4b0aa8 Update urfave/cli/v2 to v3 2026-03-19 14:16:12 +08:00
sciencesakura 8b888f0737 docs: add a description of the ghq.defaultHost config. 2026-03-19 00:41:52 +09:00
sciencesakura c042fa4d99 feat(get): add ghq.defaultHost config to resolve the default host. 2026-03-19 00:16:13 +09:00
nurazon59 f0d52d2307 feat(completion): add ghq list completion to ghq rm in fish/zsh
The fish and zsh completions for `ghq rm` were missing repository
name suggestions from `ghq list`, making it inconvenient to use.
2026-03-18 23:27:43 +09:00
Masayuki Matsuki c71ae6ae4b
Merge pull request #467 from x-motemen/dependabot/go_modules/golang.org/x/net-0.52.0
chore(deps): bump golang.org/x/net from 0.50.0 to 0.52.0
2026-03-17 10:34:37 +09:00
Masayuki Matsuki e3decba1c0
Merge pull request #458 from x-motemen/dependabot/github_actions/reviewdog/action-actionlint-1.71.0
chore(deps): bump reviewdog/action-actionlint from 1.70.0 to 1.71.0
2026-03-17 10:34:13 +09:00
Masayuki Matsuki 719b543464
Merge pull request #462 from x-motemen/dependabot/github_actions/crate-ci/typos-1.44.0
chore(deps): bump crate-ci/typos from 1.43.4 to 1.44.0
2026-03-17 10:33:53 +09:00
dependabot[bot] 8edf244e22
chore(deps): bump golang.org/x/net from 0.50.0 to 0.52.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.50.0 to 0.52.0.
- [Commits](https://github.com/golang/net/compare/v0.50.0...v0.52.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.52.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-17 01:32:46 +00:00
Masayuki Matsuki fdc03b5080
Merge pull request #463 from x-motemen/dependabot/github_actions/actions/setup-go-6.3.0
chore(deps): bump actions/setup-go from 6.2.0 to 6.3.0
2026-03-17 10:32:20 +09:00
Masayuki Matsuki 54c751514b
Merge pull request #468 from x-motemen/dependabot/go_modules/golang.org/x/text-0.35.0
chore(deps): bump golang.org/x/text from 0.34.0 to 0.35.0
2026-03-17 10:31:06 +09:00
Masayuki Matsuki ed40ead1db
Merge pull request #461 from x-motemen/dependabot/github_actions/Songmu/tagpr-1.17.1
chore(deps): bump Songmu/tagpr from 1.17.0 to 1.17.1
2026-03-17 10:30:49 +09:00
dependabot[bot] c25ed1ad70
chore(deps): bump golang.org/x/text from 0.34.0 to 0.35.0
Bumps [golang.org/x/text](https://github.com/golang/text) from 0.34.0 to 0.35.0.
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.34.0...v0.35.0)

---
updated-dependencies:
- dependency-name: golang.org/x/text
  dependency-version: 0.35.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-16 09:39:50 +00:00
Masayuki Matsuki ed918b5315
Merge pull request #465 from upft-kengotate/fix/zsh-completion
Update zsh completion to match current commands and flags
2026-03-04 11:01:27 +09:00
CC 舘 謙吾 c9d0ca0ad1 Update zsh completion to match current commands and flags
- Add ghq-dev to #compdef for development build completion
- Add rm and migrate to command list
- Add missing --bare flag to list, rm, and create subcommands
- Add migrate subcommand completion with directory completion

Co-Authored-By: Claude <noreply@anthropic.com>
2026-03-03 16:24:46 +09:00
dependabot[bot] ffd505d1a5
chore(deps): bump actions/setup-go from 6.2.0 to 6.3.0
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 6.2.0 to 6.3.0.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](7a3fe6cf4c...4b73464bb3)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-version: 6.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-02 09:55:53 +00:00
dependabot[bot] 57f0bc66e7
chore(deps): bump crate-ci/typos from 1.43.4 to 1.44.0
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.43.4 to 1.44.0.
- [Release notes](https://github.com/crate-ci/typos/releases)
- [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md)
- [Commits](78bc6fb2c0...631208b7aa)

---
updated-dependencies:
- dependency-name: crate-ci/typos
  dependency-version: 1.44.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-02 09:55:47 +00:00
dependabot[bot] be2ccb10b3
chore(deps): bump Songmu/tagpr from 1.17.0 to 1.17.1
Bumps [Songmu/tagpr](https://github.com/songmu/tagpr) from 1.17.0 to 1.17.1.
- [Release notes](https://github.com/songmu/tagpr/releases)
- [Changelog](https://github.com/Songmu/tagpr/blob/main/CHANGELOG.md)
- [Commits](da82ed6743...b3fb894246)

---
updated-dependencies:
- dependency-name: Songmu/tagpr
  dependency-version: 1.17.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-02 09:55:36 +00:00
dependabot[bot] c5d8347d7b
chore(deps): bump reviewdog/action-actionlint from 1.70.0 to 1.71.0
Bumps [reviewdog/action-actionlint](https://github.com/reviewdog/action-actionlint) from 1.70.0 to 1.71.0.
- [Release notes](https://github.com/reviewdog/action-actionlint/releases)
- [Commits](e58ee9d111...0d952c597e)

---
updated-dependencies:
- dependency-name: reviewdog/action-actionlint
  dependency-version: 1.71.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-23 09:59:54 +00:00
KeitaShimura e7882d7169 feat(get): add --print flag to output local repository path 2026-02-23 00:11:12 +09:00
Songmu 531d4f1a5f
update 2026-02-17 17:24:34 +09:00
Masayuki Matsuki 39ead659b5
Merge pull request #453 from x-motemen/tagpr-from-v1.9.3
Release for v1.9.4
2026-02-17 16:47:37 +09:00
github-actions[bot] 2cf21aeacb
[tagpr] update CHANGELOG.md 2026-02-17 07:40:37 +00:00
github-actions[bot] 216dc5de57
[tagpr] prepare for the next release 2026-02-17 07:40:36 +00:00
Masayuki Matsuki 98d4faa146
Merge pull request #454 from x-motemen/copilot/fix-github-actions-workflow-test
Fix Windows worktree migration failure: normalize Git path separators
2026-02-17 16:40:12 +09:00
copilot-swe-agent[bot] 46be3fa40c Fix worktree migration path handling on Windows
The repairWorktreeBackPointers function was failing to detect internal
worktrees on Windows because Git writes paths with forward slashes in
gitdir files, but the code was comparing them with backslash paths.

- Normalize oldDir to forward slashes for comparison
- Normalize wtPath read from gitdir file for comparison
- Write updated gitdir with forward slashes (Git's format)
- Fix wtDir trimming to use forward slash separator

This fixes both the back-pointer repair and the paths passed to
'git worktree repair' on Windows.

Co-authored-by: Songmu <177122+Songmu@users.noreply.github.com>
2026-02-17 07:30:11 +00:00
copilot-swe-agent[bot] a12656da90 Fix Windows worktree test by normalizing path separators
Git on Windows uses forward slashes in .git files, even though Windows
uses backslashes. Use filepath.ToSlash() to normalize paths for comparison
in the worktree migration tests.

Co-authored-by: Songmu <177122+Songmu@users.noreply.github.com>
2026-02-17 06:54:43 +00:00
copilot-swe-agent[bot] d82dd80f86 Initial plan 2026-02-17 06:51:24 +00:00
Masayuki Matsuki 73d8a79b05
Merge pull request #452 from x-motemen/dependabot/github_actions/Songmu/tagpr-1.17.0
chore(deps): bump Songmu/tagpr from 1.15.0 to 1.17.0
2026-02-17 15:43:48 +09:00
Masayuki Matsuki 0c5582e422
Merge pull request #451 from x-motemen/dependabot/github_actions/crate-ci/typos-1.43.4
chore(deps): bump crate-ci/typos from 1.43.3 to 1.43.4
2026-02-17 15:43:28 +09:00
Masayuki Matsuki 8f58aeffe0
Merge pull request #449 from atusy/feat-git-migrate-be-aware-of-worktree-and-submodule
feat(migrate): make worktree and submodule aware
2026-02-17 15:43:11 +09:00
Masayuki Matsuki ea2820c4a6
Merge pull request #450 from yulog/apply-go-fix
apply go fix modernizations
2026-02-17 15:24:13 +09:00
dependabot[bot] d2c9c36798
chore(deps): bump Songmu/tagpr from 1.15.0 to 1.17.0
Bumps [Songmu/tagpr](https://github.com/songmu/tagpr) from 1.15.0 to 1.17.0.
- [Release notes](https://github.com/songmu/tagpr/releases)
- [Changelog](https://github.com/Songmu/tagpr/blob/main/CHANGELOG.md)
- [Commits](78a926ba83...da82ed6743)

---
updated-dependencies:
- dependency-name: Songmu/tagpr
  dependency-version: 1.17.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-16 09:58:25 +00:00
dependabot[bot] f4db0e785e
chore(deps): bump crate-ci/typos from 1.43.3 to 1.43.4
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.43.3 to 1.43.4.
- [Release notes](https://github.com/crate-ci/typos/releases)
- [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md)
- [Commits](9066e9940a...78bc6fb2c0)

---
updated-dependencies:
- dependency-name: crate-ci/typos
  dependency-version: 1.43.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-16 09:58:15 +00:00
yulog cefa808653 chore: exec go fix ./... 2026-02-15 19:45:03 +09:00
atusy 32914e6738 chore(deps): go mod tidy 2026-02-15 18:54:55 +09:00
atusy 17a1d6216e feat(migrate): make worktree and submodule aware
The migrate command had no awareness of Git worktrees or submodules,
which are linked checkouts with a .git file referencing a parent repo.

This adds three capabilities:

- Detects linked checkouts (.git file with gitdir: reference) and
  refuses migration with a descriptive error, since moving them
  independently always breaks the link.

- After migrating a repo with linked worktrees, runs
  'git worktree repair' to update forward references (worktree .git
  files) back to the new main repo location.

- Handles internal worktrees (inside the repo directory) by rewriting
  stale back-pointers (.git/worktrees/*/gitdir) before repair, since
  both directions become stale when the worktree moves with the repo.
2026-02-15 18:54:31 +09:00
Masayuki Matsuki 7caa68bcd0
Merge pull request #448 from x-motemen/tagpr-from-v1.9.2
Release for v1.9.3
2026-02-14 16:58:42 +09:00
github-actions[bot] 936f79524f
[tagpr] update CHANGELOG.md 2026-02-14 07:56:05 +00:00
github-actions[bot] 1b4f893fad
[tagpr] prepare for the next release 2026-02-14 07:56:04 +00:00
Masayuki Matsuki d50c58368e
Merge pull request #446 from x-motemen/copilot/handle-cross-device-move-fallback
migrate: Handle cross-device move (EXDEV fallback)
2026-02-14 16:55:43 +09:00
copilot-swe-agent[bot] 483e6fd0b4 Replace custom copy implementation with otiai10/copy library
Co-authored-by: Songmu <177122+Songmu@users.noreply.github.com>
2026-02-14 07:43:12 +00:00
Masayuki Matsuki c16d46e8bb
Update cmd_migrate_test.go
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-14 16:30:19 +09:00
Masayuki Matsuki 5bb07f7c74
Update cmd_migrate.go
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-14 16:30:02 +09:00
Masayuki Matsuki af48fe25ac
Update cmd_migrate.go
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-14 16:29:22 +09:00
copilot-swe-agent[bot] 92e49a0463 Add migrate command documentation to README.adoc
Co-authored-by: Songmu <177122+Songmu@users.noreply.github.com>
2026-02-14 07:13:26 +00:00
copilot-swe-agent[bot] 344f570f5e Run goimports to fix formatting
Co-authored-by: Songmu <177122+Songmu@users.noreply.github.com>
2026-02-14 07:10:26 +00:00
copilot-swe-agent[bot] 6a0a293e64
Add coverage.out to .gitignore and remove from tracking
Co-authored-by: Songmu <177122+Songmu@users.noreply.github.com>
2026-02-14 16:05:38 +09:00
copilot-swe-agent[bot] ca5bd93797
Skip permission checks on Windows in cmd_migrate_test.go
Co-authored-by: Songmu <177122+Songmu@users.noreply.github.com>
2026-02-14 16:05:38 +09:00
copilot-swe-agent[bot] e3f393527d
Initial plan 2026-02-14 16:05:38 +09:00
copilot-swe-agent[bot] d17d6c2257
Fix permission bits and add test error handling
Co-authored-by: Songmu <177122+Songmu@users.noreply.github.com>
2026-02-14 16:05:38 +09:00
copilot-swe-agent[bot] 351082d1ed
Address code review feedback: improve error handling and permission bits
Co-authored-by: Songmu <177122+Songmu@users.noreply.github.com>
2026-02-14 16:05:38 +09:00
copilot-swe-agent[bot] 875224e621
Add cross-device move support with EXDEV fallback
Co-authored-by: Songmu <177122+Songmu@users.noreply.github.com>
2026-02-14 16:05:38 +09:00
copilot-swe-agent[bot] c695205e97
Initial plan 2026-02-14 16:05:38 +09:00
Songmu 0661bf9429
Add Copilot instructions for Go development
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-02-14 16:04:43 +09:00
Masayuki Matsuki deaa4ad5f0
Merge pull request #444 from x-motemen/tagpr-from-v1.9.1
Release for v1.9.2
2026-02-14 00:51:27 +07:00
github-actions[bot] cdbf4a59e0
[tagpr] update CHANGELOG.md 2026-02-13 17:51:04 +00:00
github-actions[bot] b88484299f
[tagpr] prepare for the next release 2026-02-13 17:51:03 +00:00
Masayuki Matsuki d99f5303a1
Merge pull request #443 from x-motemen/go1.26
Go 1.26
2026-02-14 00:50:42 +07:00
Songmu 9d0eae2507
Go 1.26 2026-02-14 02:47:55 +09:00
Masayuki Matsuki 63d3a6a3f2
Merge pull request #440 from x-motemen/tagpr-from-v1.9.0
Release for v1.9.1
2026-02-14 00:43:39 +07:00
github-actions[bot] 863f3cce19
[tagpr] update CHANGELOG.md 2026-02-13 17:42:12 +00:00
github-actions[bot] 8ecdc6603d
[tagpr] prepare for the next release 2026-02-13 17:42:11 +00:00
Masayuki Matsuki af2759f3fa
Merge pull request #442 from x-motemen/copilot/add-multi-vcs-support
migrate: Add multi-VCS remote URL support
2026-02-14 00:41:42 +07:00
copilot-swe-agent[bot] 640fb3f223 Address code review feedback: use strings.Contains and remove extra blank line
Co-authored-by: Songmu <177122+Songmu@users.noreply.github.com>
2026-02-13 17:37:19 +00:00
copilot-swe-agent[bot] 9515744ee5 Add RemoteURL field to VCSBackend and implement for all backends
Co-authored-by: Songmu <177122+Songmu@users.noreply.github.com>
2026-02-13 17:35:14 +00:00
copilot-swe-agent[bot] 12f129c2f0 Initial plan 2026-02-13 17:31:13 +00:00
Masayuki Matsuki 7557b26547
Merge pull request #439 from x-motemen/deps
Go 1.26
2026-02-14 00:28:33 +07:00
Songmu c94a21c4ec
Go 1.26 2026-02-14 02:26:37 +09:00
Masayuki Matsuki 78f8afc82c
Merge pull request #435 from x-motemen/tagpr-from-v1.8.1
Release for v1.9.0
2026-02-14 00:14:54 +07:00
github-actions[bot] 641e2d9679
[tagpr] update CHANGELOG.md 2026-02-13 17:11:05 +00:00
github-actions[bot] 4c104d74ad
[tagpr] prepare for the next release 2026-02-13 17:11:03 +00:00
Masayuki Matsuki 7e57039d92
Merge pull request #438 from x-motemen/copilot/add-migrate-subcommand
Add migrate subcommand to move existing repos into ghq structure
2026-02-14 00:10:39 +07:00
copilot-swe-agent[bot] 4010026f51 Format code with goimports and go fmt
Co-authored-by: Songmu <177122+Songmu@users.noreply.github.com>
2026-02-13 17:01:03 +00:00
copilot-swe-agent[bot] 929852152e Improve error message for user-aborted migration
Co-authored-by: Songmu <177122+Songmu@users.noreply.github.com>
2026-02-13 16:50:54 +00:00
copilot-swe-agent[bot] 0ffd7f26a4 Add comprehensive test coverage for migrate command
Co-authored-by: Songmu <177122+Songmu@users.noreply.github.com>
2026-02-13 16:49:10 +00:00
copilot-swe-agent[bot] 9503ad3620 Add migrate subcommand implementation
Co-authored-by: Songmu <177122+Songmu@users.noreply.github.com>
2026-02-13 16:48:07 +00:00
copilot-swe-agent[bot] 9ba67d64a7 Initial plan 2026-02-13 16:43:09 +00:00
Masayuki Matsuki f28beb574d
Merge pull request #436 from x-motemen/dependabot/github_actions/crate-ci/typos-1.43.3
chore(deps): bump crate-ci/typos from 1.42.3 to 1.43.3
2026-02-13 23:21:21 +07:00
dependabot[bot] f7ca71c498
chore(deps): bump crate-ci/typos from 1.42.3 to 1.43.3
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.42.3 to 1.43.3.
- [Release notes](https://github.com/crate-ci/typos/releases)
- [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md)
- [Commits](06d010dfe4...9066e9940a)

---
updated-dependencies:
- dependency-name: crate-ci/typos
  dependency-version: 1.43.3
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-09 10:16:00 +00:00
Masayuki Matsuki 7a5333a16e
Merge pull request #434 from x-motemen/dependabot/go_modules/github.com/Songmu/gitconfig-0.2.2
chore(deps): bump github.com/Songmu/gitconfig from 0.2.1 to 0.2.2
2026-02-02 22:43:30 +09:00
dependabot[bot] 2eee0eee71
chore(deps): bump github.com/Songmu/gitconfig from 0.2.1 to 0.2.2
Bumps [github.com/Songmu/gitconfig](https://github.com/Songmu/gitconfig) from 0.2.1 to 0.2.2.
- [Release notes](https://github.com/Songmu/gitconfig/releases)
- [Changelog](https://github.com/Songmu/gitconfig/blob/main/CHANGELOG.md)
- [Commits](https://github.com/Songmu/gitconfig/compare/v0.2.1...v0.2.2)

---
updated-dependencies:
- dependency-name: github.com/Songmu/gitconfig
  dependency-version: 0.2.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-02 10:19:35 +00:00
Masayuki Matsuki 21fb2efbea
Merge pull request #422 from x-motemen/tagpr-from-v1.8.0
Release for v1.8.1
2026-02-01 19:16:17 +09:00
github-actions[bot] d422d4c612
[tagpr] update CHANGELOG.md 2026-02-01 10:06:23 +00:00
github-actions[bot] d8981c15a3
[tagpr] prepare for the next release 2026-02-01 10:06:21 +00:00
Masayuki Matsuki 0c5ad41e30
Merge pull request #433 from x-motemen/dependabot/github_actions/actions/checkout-6.0.2
chore(deps): bump actions/checkout from 6.0.1 to 6.0.2
2026-02-01 19:06:05 +09:00
Masayuki Matsuki 7a1ec399ff
Merge pull request #432 from x-motemen/dependabot/github_actions/crate-ci/typos-1.42.3
chore(deps): bump crate-ci/typos from 1.13.10 to 1.42.3
2026-02-01 19:05:52 +09:00
dependabot[bot] e1e2b654d1
chore(deps): bump actions/checkout from 6.0.1 to 6.0.2
Bumps [actions/checkout](https://github.com/actions/checkout) from 6.0.1 to 6.0.2.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](8e8c483db8...de0fac2e45)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 6.0.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-01 10:04:11 +00:00
dependabot[bot] 4d03bbc189
chore(deps): bump crate-ci/typos from 1.13.10 to 1.42.3
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.13.10 to 1.42.3.
- [Release notes](https://github.com/crate-ci/typos/releases)
- [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md)
- [Commits](6e14cefb85...06d010dfe4)

---
updated-dependencies:
- dependency-name: crate-ci/typos
  dependency-version: 1.42.3
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-01 10:04:01 +00:00
Masayuki Matsuki 56ad4bca21
Merge pull request #431 from x-motemen/maint
update dependencies and CI/CD
2026-02-01 19:03:22 +09:00
Songmu 382d74f827
make ghalint pass 2026-02-01 19:00:57 +09:00
Songmu 3c04f664ff
persist-credentials set fales 2026-02-01 18:59:23 +09:00
Songmu fd8b62aee1
set permissions to github actions 2026-02-01 18:17:49 +09:00
Songmu 61a751b92a
ghatm set -t 10 2026-02-01 18:12:10 +09:00
Songmu 73682d3064
update deps 2026-02-01 18:07:43 +09:00
Songmu 7c7882b83a
introduce pinact 2026-02-01 18:04:12 +09:00
Songmu 691e4ec805
add dependabot.yml 2026-02-01 18:00:01 +09:00
Masayuki Matsuki 650c877e62
Merge pull request #430 from mitsu-yuki/master
docs: add description for `ghq.user` and `ghq. completeUser`
2025-12-21 14:57:39 +09:00
Mitsu 66a60071be
docs: add description for ghq.completeUser 2025-12-21 12:09:13 +09:00
Mitsu 287b1bbe3e
docs: add description for ghq.user 2025-12-21 12:08:55 +09:00
Masayuki Matsuki 4f4abb143d
Merge pull request #426 from hezhizhen/rm-bare
fix: accept bare flag to remove bare repositories
2025-11-12 15:37:35 +09:00
Zhizhen He 4b56f98e01
fix: accept bare flag to remove bare repositories 2025-10-14 22:38:19 +08:00
Masayuki Matsuki 511c23c8b7
Merge pull request #423 from x-motemen/dependabot/go_modules/golang.org/x/net-0.38.0
chore(deps): bump golang.org/x/net from 0.37.0 to 0.38.0
2025-06-17 02:44:28 +09:00
dependabot[bot] d4f29e7ede
chore(deps): bump golang.org/x/net from 0.37.0 to 0.38.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.37.0 to 0.38.0.
- [Commits](https://github.com/golang/net/compare/v0.37.0...v0.38.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.38.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-16 17:02:13 +00:00
Masayuki Matsuki baff4ac333
Merge pull request #418 from Okabe-Junya/bump-go-124
Maintain: Bump dependencies
2025-05-22 03:13:25 +09:00
Junya Okabe 82927c2aad
chore: exec go fmt ./... 2025-03-26 17:18:06 +09:00
Junya Okabe a09becab6a
bump go 1.24.1 and go get -u ./... 2025-03-26 17:17:50 +09:00
Masayuki Matsuki 406c7dc8d6
Merge pull request #416 from x-motemen/tagpr-from-v1.7.1
Release for v1.8.0
2025-03-26 00:37:49 +09:00
github-actions[bot] a38442787a [tagpr] update CHANGELOG.md 2025-03-25 15:34:29 +00:00
github-actions[bot] 1143ba4b21 [tagpr] prepare for the next release 2025-03-25 15:34:28 +00:00
Masayuki Matsuki e0e71a3449
Merge pull request #412 from RShirohara/add-partial-clone-support
feat: support partial clone on Git repository
2025-03-26 00:34:04 +09:00
Ray Shirohara 39c5d070de
docs: add hard break before --partial option description 2025-03-26 00:25:04 +09:00
Ray Shirohara 80c4db33fd
feat: add partial flag support to fish and zsh completions 2025-03-26 00:25:04 +09:00
Ray Shirohara 8631279352
docs: add partial option document to README 2025-03-26 00:25:03 +09:00
Ray Shirohara 63e148f646
test: add blobless and treeless testcases to integration test 2025-03-26 00:25:03 +09:00
Ray Shirohara a02175071d
feat: add partial option support to ghq get 2025-03-26 00:25:03 +09:00
Ray Shirohara a655d1d81f
feat: add partial option support to GitBackend 2025-03-26 00:24:42 +09:00
Ray Shirohara ed4b406086
feat: add partial flag definition to ghq get 2025-03-26 00:23:48 +09:00
Masayuki Matsuki e69c7b189a
Merge pull request #414 from Sixeight/sixeight/improve-silent-flag
improve --silent flag in get command
2025-03-25 23:37:12 +09:00
Masayuki Matsuki ea5ed54d7c
Merge pull request #417 from x-motemen/no-color
Make the NO_COLOR environment variable accept strings other than the “true” strings
2025-03-25 23:19:45 +09:00
Songmu a8f99be06c Make the NO_COLOR environment variable accept strings other than the “true” sings 2025-03-25 23:16:39 +09:00
Masayuki Matsuki e50e60bbc2
Merge pull request #411 from hezhizhen/logger-support-no-color
feat: support NO_COLOR environment variable
2025-03-25 23:11:27 +09:00
Sixeight 2d66830943
improve --silent flag in get command 2025-03-20 18:40:58 +09:00
Zhizhen He cf15ec26ef feat: support NO_COLOR environment variable 2024-12-19 15:53:24 +08:00
43 changed files with 2226 additions and 685 deletions

View file

@ -11,7 +11,7 @@ runs:
using: composite
steps:
- name: setup go
uses: actions/setup-go@v3
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0
with:
go-version-file: go.mod
- name: release

20
.github/copilot-instructions.md vendored Normal file
View file

@ -0,0 +1,20 @@
# Copilot Instructions for ghq
## Language & Build
- This is a Go project. Use `go build` to build and `go test ./...` to run tests.
## Code Quality Checks
Before committing, always run the following in order:
1. `goimports -w .` — format code and organize imports
2. `go vet ./...` — check for common errors
3. `staticcheck ./...` — run static analysis
All three must pass with no errors before pushing.
## Testing
- Run `go test ./...` to execute all tests.
- Ensure all existing tests pass after making changes.

18
.github/dependabot.yml vendored Normal file
View file

@ -0,0 +1,18 @@
version: 2
updates:
- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: "weekly"
groups:
golang-x:
patterns:
- "golang.org/x/*"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
groups:
actions:
patterns:
- "actions/*"

View file

@ -3,14 +3,19 @@ on:
push:
branches:
- master
permissions:
contents: read
jobs:
test:
timeout-minutes: 20
runs-on: macos-latest
steps:
- name: checkout
uses: actions/checkout@v3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: setup go
uses: actions/setup-go@v3
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
with:
go-version-file: go.mod
- name: install

View file

@ -3,12 +3,18 @@ on:
push:
tags:
- "v[0-9]+.[0-9]+.[0-9]+"
permissions:
contents: write
packages: write
jobs:
release:
timeout-minutes: 20
runs-on: ubuntu-latest
steps:
- name: checkout
uses: actions/checkout@v3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- uses: ./.github/actions/release
with:
token: ${{ secrets.GITHUB_TOKEN }}

View file

@ -1,49 +1,70 @@
name: reviewdog
on: [pull_request]
permissions:
contents: read
pull-requests: write
jobs:
typos:
permissions:
contents: read
pull-requests: write
timeout-minutes: 10
name: Spell Check with Typos
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: crate-ci/typos@v1.13.10
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- uses: crate-ci/typos@bee27e3a4fd1ea2111cf90ab89cd076c870fce14 # v1.48.0
staticcheck:
permissions:
contents: read
pull-requests: write
timeout-minutes: 10
name: staticcheck
runs-on: ubuntu-latest
steps:
- name: Check out code into the Go module directory
uses: actions/checkout@v3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: staticcheck
uses: reviewdog/action-staticcheck@v1
uses: reviewdog/action-staticcheck@564f18c6297af36f7b10b6c7a72e814341bd813a # v1.29.0
with:
reporter: github-pr-review
level: warning
misspell:
permissions:
contents: read
pull-requests: write
timeout-minutes: 10
name: misspell
runs-on: ubuntu-latest
steps:
- name: Check out code into the Go module directory
uses: actions/checkout@v3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: misspell
uses: reviewdog/action-misspell@v1
uses: reviewdog/action-misspell@d6429416b12b09b4e2768307d53bef58d172e962 # v1.27.0
with:
reporter: github-pr-review
level: warning
locale: "US"
actionlint:
permissions:
contents: read
pull-requests: write
timeout-minutes: 10
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- uses: reviewdog/action-actionlint@v1
- uses: reviewdog/action-actionlint@6fb7acc99f4a1008869fa8a0f09cfca740837d9d # v1.72.0
with:
reporter: github-pr-review

View file

@ -4,19 +4,26 @@ on:
branches:
- "master"
workflow_dispatch:
permissions:
contents: write
pull-requests: write
issues: read
jobs:
tagpr:
timeout-minutes: 20
runs-on: ubuntu-latest
steps:
- name: checkout
uses: actions/checkout@v3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: setup go
uses: actions/setup-go@v3
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
with:
go-version-file: go.mod
- name: tagpr
id: tagpr
uses: Songmu/tagpr@v1
uses: Songmu/tagpr@e84001bd5dac8defa0c75b3913937d284a3b3660 # v1.20.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- uses: ./.github/actions/release

View file

@ -1,10 +1,17 @@
name: test
on:
push:
pull_request:
branches:
- "**"
push:
branches:
- master
permissions:
contents: read
pull-requests: read
jobs:
test:
timeout-minutes: 10
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
@ -20,12 +27,14 @@ jobs:
git config --global core.eol lf
if: "matrix.os == 'windows-latest'"
- name: checkout
uses: actions/checkout@v3
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: setup go
uses: actions/setup-go@v3
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0
with:
go-version-file: go.mod
- name: test
run: go test -coverprofile coverage.out -covermode atomic ./...
- name: Send coverage
uses: codecov/codecov-action@v1
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0

1
.gitignore vendored
View file

@ -3,3 +3,4 @@
/dist
.vscode
.idea
coverage.out

View file

@ -1,5 +1,63 @@
# Changelog
## [v1.10.1](https://github.com/x-motemen/ghq/compare/v1.10.0...v1.10.1) - 2026-04-11
- feat(rm): make ghq rm worktree-aware by @chris-monardo in https://github.com/x-motemen/ghq/pull/481
## [v1.10.0](https://github.com/x-motemen/ghq/compare/v1.9.4...v1.10.0) - 2026-04-09
- Update zsh completion to match current commands and flags by @upft-kengotate in https://github.com/x-motemen/ghq/pull/465
- chore(deps): bump Songmu/tagpr from 1.17.0 to 1.17.1 by @dependabot[bot] in https://github.com/x-motemen/ghq/pull/461
- chore(deps): bump golang.org/x/text from 0.34.0 to 0.35.0 by @dependabot[bot] in https://github.com/x-motemen/ghq/pull/468
- chore(deps): bump actions/setup-go from 6.2.0 to 6.3.0 by @dependabot[bot] in https://github.com/x-motemen/ghq/pull/463
- chore(deps): bump crate-ci/typos from 1.43.4 to 1.44.0 by @dependabot[bot] in https://github.com/x-motemen/ghq/pull/462
- chore(deps): bump reviewdog/action-actionlint from 1.70.0 to 1.71.0 by @dependabot[bot] in https://github.com/x-motemen/ghq/pull/458
- chore(deps): bump golang.org/x/net from 0.50.0 to 0.52.0 by @dependabot[bot] in https://github.com/x-motemen/ghq/pull/467
- feat(completion): add ghq list completion to ghq rm in fish/zsh by @nurazon59 in https://github.com/x-motemen/ghq/pull/469
- chore(deps): bump Songmu/tagpr from 1.17.1 to 1.18.1 by @dependabot[bot] in https://github.com/x-motemen/ghq/pull/478
- feat(get): add `ghq.defaultHost` config to resolve the default host. by @sciencesakura in https://github.com/x-motemen/ghq/pull/472
- Update urfave/cli/v2 to v3 by @hezhizhen in https://github.com/x-motemen/ghq/pull/473
- chore(deps): bump actions/setup-go from 6.3.0 to 6.4.0 by @dependabot[bot] in https://github.com/x-motemen/ghq/pull/475
- chore(deps): bump reviewdog/action-actionlint from 1.71.0 to 1.72.0 by @dependabot[bot] in https://github.com/x-motemen/ghq/pull/477
- chore(deps): bump codecov/codecov-action from 5.5.2 to 6.0.0 by @dependabot[bot] in https://github.com/x-motemen/ghq/pull/476
- chore(deps): bump crate-ci/typos from 1.44.0 to 1.45.0 by @dependabot[bot] in https://github.com/x-motemen/ghq/pull/479
- feat(get): output local repository path to stdout by @KeitaShimura in https://github.com/x-motemen/ghq/pull/457
## [v1.9.4](https://github.com/x-motemen/ghq/compare/v1.9.3...v1.9.4) - 2026-02-17
- apply go fix modernizations by @yulog in https://github.com/x-motemen/ghq/pull/450
- feat(migrate): make worktree and submodule aware by @atusy in https://github.com/x-motemen/ghq/pull/449
- chore(deps): bump crate-ci/typos from 1.43.3 to 1.43.4 by @dependabot[bot] in https://github.com/x-motemen/ghq/pull/451
- chore(deps): bump Songmu/tagpr from 1.15.0 to 1.17.0 by @dependabot[bot] in https://github.com/x-motemen/ghq/pull/452
- Fix Windows worktree migration failure: normalize Git path separators by @Copilot in https://github.com/x-motemen/ghq/pull/454
## [v1.9.3](https://github.com/x-motemen/ghq/compare/v1.9.2...v1.9.3) - 2026-02-14
- migrate: Handle cross-device move (EXDEV fallback) by @Copilot in https://github.com/x-motemen/ghq/pull/446
## [v1.9.2](https://github.com/x-motemen/ghq/compare/v1.9.1...v1.9.2) - 2026-02-13
- Go 1.26 by @Songmu in https://github.com/x-motemen/ghq/pull/443
## [v1.9.1](https://github.com/x-motemen/ghq/compare/v1.9.0...v1.9.1) - 2026-02-13
- Go 1.26 by @Songmu in https://github.com/x-motemen/ghq/pull/439
- migrate: Add multi-VCS remote URL support by @Copilot in https://github.com/x-motemen/ghq/pull/442
## [v1.9.0](https://github.com/x-motemen/ghq/compare/v1.8.1...v1.9.0) - 2026-02-13
- chore(deps): bump github.com/Songmu/gitconfig from 0.2.1 to 0.2.2 by @dependabot[bot] in https://github.com/x-motemen/ghq/pull/434
- chore(deps): bump crate-ci/typos from 1.42.3 to 1.43.3 by @dependabot[bot] in https://github.com/x-motemen/ghq/pull/436
- Add migrate subcommand to move existing repos into ghq structure by @Copilot in https://github.com/x-motemen/ghq/pull/438
## [v1.8.1](https://github.com/x-motemen/ghq/compare/v1.8.0...v1.8.1) - 2026-02-01
- Maintain: Bump dependencies by @Okabe-Junya in https://github.com/x-motemen/ghq/pull/418
- chore(deps): bump golang.org/x/net from 0.37.0 to 0.38.0 by @dependabot[bot] in https://github.com/x-motemen/ghq/pull/423
- fix: accept bare flag to remove bare repositories by @hezhizhen in https://github.com/x-motemen/ghq/pull/426
- docs: add description for `ghq.user` and `ghq. completeUser` by @mitsu-yuki in https://github.com/x-motemen/ghq/pull/430
- update dependencies and CI/CD by @Songmu in https://github.com/x-motemen/ghq/pull/431
- chore(deps): bump crate-ci/typos from 1.13.10 to 1.42.3 by @dependabot[bot] in https://github.com/x-motemen/ghq/pull/432
- chore(deps): bump actions/checkout from 6.0.1 to 6.0.2 by @dependabot[bot] in https://github.com/x-motemen/ghq/pull/433
## [v1.8.0](https://github.com/x-motemen/ghq/compare/v1.7.1...v1.8.0) - 2025-03-25
- feat: support NO_COLOR environment variable by @hezhizhen in https://github.com/x-motemen/ghq/pull/411
- Make the NO_COLOR environment variable accept strings other than the “true” strings by @Songmu in https://github.com/x-motemen/ghq/pull/417
- improve --silent flag in get command by @Sixeight in https://github.com/x-motemen/ghq/pull/414
- feat: support partial clone on Git repository by @RShirohara in https://github.com/x-motemen/ghq/pull/412
## [v1.7.1](https://github.com/x-motemen/ghq/compare/v1.7.0...v1.7.1) - 2024-11-09
- refine git vcs backend detection by @Songmu in https://github.com/x-motemen/ghq/pull/409

390
CREDITS
View file

@ -1,7 +1,7 @@
Go (the standard library)
https://golang.org/
----------------------------------------------------------------
Copyright (c) 2009 The Go Authors. All rights reserved.
Copyright 2009 The Go Authors.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
@ -13,7 +13,7 @@ notice, this list of conditions and the following disclaimer.
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
* Neither the name of Google LLC nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
@ -59,8 +59,8 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
================================================================
github.com/cli/go-gh
https://github.com/cli/go-gh
github.com/cli/go-gh/v2
https://github.com/cli/go-gh/v2
----------------------------------------------------------------
MIT License
@ -117,33 +117,6 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
================================================================
github.com/cpuguy83/go-md2man/v2
https://github.com/cpuguy83/go-md2man/v2
----------------------------------------------------------------
The MIT License (MIT)
Copyright (c) 2014 Brian Goff
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================================
github.com/davecgh/go-spew
https://github.com/davecgh/go-spew
----------------------------------------------------------------
@ -223,113 +196,6 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================================
github.com/fatih/color
https://github.com/fatih/color
----------------------------------------------------------------
The MIT License (MIT)
Copyright (c) 2013 Fatih Arslan
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
================================================================
github.com/go-playground/locales
https://github.com/go-playground/locales
----------------------------------------------------------------
The MIT License (MIT)
Copyright (c) 2016 Go Playground
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================================
github.com/go-playground/universal-translator
https://github.com/go-playground/universal-translator
----------------------------------------------------------------
The MIT License (MIT)
Copyright (c) 2016 Go Playground
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================================
github.com/go-playground/validator/v10
https://github.com/go-playground/validator/v10
----------------------------------------------------------------
The MIT License (MIT)
Copyright (c) 2015 Dean Karn
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================================
github.com/goccy/go-yaml
@ -391,39 +257,6 @@ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
================================================================
github.com/google/go-cmp
https://github.com/google/go-cmp
----------------------------------------------------------------
Copyright (c) 2017 The Go Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
================================================================
github.com/kr/pretty
@ -478,60 +311,6 @@ THE SOFTWARE.
================================================================
github.com/leodido/go-urn
https://github.com/leodido/go-urn
----------------------------------------------------------------
MIT License
Copyright (c) 2018 Leonardo Di Donato
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================================
github.com/mattn/go-colorable
https://github.com/mattn/go-colorable
----------------------------------------------------------------
The MIT License (MIT)
Copyright (c) 2016 Yasuhiro Matsumoto
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================================
github.com/mattn/go-isatty
https://github.com/mattn/go-isatty
----------------------------------------------------------------
@ -573,6 +352,46 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================================
github.com/otiai10/copy
https://github.com/otiai10/copy
----------------------------------------------------------------
The MIT License (MIT)
Copyright (c) 2018 otiai10
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
================================================================
github.com/otiai10/mint
https://github.com/otiai10/mint
----------------------------------------------------------------
Copyright 2017 otiai10 (Hiromu OCHIAI)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
================================================================
github.com/pmezard/go-difflib
https://github.com/pmezard/go-difflib
----------------------------------------------------------------
@ -606,41 +425,6 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
================================================================
github.com/russross/blackfriday/v2
https://github.com/russross/blackfriday/v2
----------------------------------------------------------------
Blackfriday is distributed under the Simplified BSD License:
> Copyright © 2011 Russ Ross
> All rights reserved.
>
> Redistribution and use in source and binary forms, with or without
> modification, are permitted provided that the following conditions
> are met:
>
> 1. Redistributions of source code must retain the above copyright
> notice, this list of conditions and the following disclaimer.
>
> 2. Redistributions in binary form must reproduce the above
> copyright notice, this list of conditions and the following
> disclaimer in the documentation and/or other materials provided with
> the distribution.
>
> THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
> "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
> LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
> FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
> COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
> INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
> BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
> LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
> CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
> LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
> ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
> POSSIBILITY OF SUCH DAMAGE.
================================================================
github.com/saracen/walker
https://github.com/saracen/walker
----------------------------------------------------------------
@ -694,12 +478,12 @@ SOFTWARE.
================================================================
github.com/urfave/cli/v2
https://github.com/urfave/cli/v2
github.com/urfave/cli/v3
https://github.com/urfave/cli/v3
----------------------------------------------------------------
MIT License
Copyright (c) 2022 urfave/cli maintainers
Copyright (c) 2023 urfave/cli maintainers
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@ -721,70 +505,10 @@ SOFTWARE.
================================================================
github.com/xrash/smetrics
https://github.com/xrash/smetrics
----------------------------------------------------------------
Copyright (C) 2016 Felipe da Cunha Gonçalves
All Rights Reserved.
MIT LICENSE
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
================================================================
golang.org/x/crypto
https://golang.org/x/crypto
----------------------------------------------------------------
Copyright (c) 2009 The Go Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
================================================================
golang.org/x/net
https://golang.org/x/net
----------------------------------------------------------------
Copyright (c) 2009 The Go Authors. All rights reserved.
Copyright 2009 The Go Authors.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
@ -796,7 +520,7 @@ notice, this list of conditions and the following disclaimer.
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
* Neither the name of Google LLC nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
@ -817,7 +541,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
golang.org/x/sync
https://golang.org/x/sync
----------------------------------------------------------------
Copyright (c) 2009 The Go Authors. All rights reserved.
Copyright 2009 The Go Authors.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
@ -829,7 +553,7 @@ notice, this list of conditions and the following disclaimer.
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
* Neither the name of Google LLC nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
@ -850,7 +574,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
golang.org/x/sys
https://golang.org/x/sys
----------------------------------------------------------------
Copyright (c) 2009 The Go Authors. All rights reserved.
Copyright 2009 The Go Authors.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
@ -862,7 +586,7 @@ notice, this list of conditions and the following disclaimer.
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
* Neither the name of Google LLC nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
@ -880,10 +604,10 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
================================================================
golang.org/x/xerrors
https://golang.org/x/xerrors
golang.org/x/text
https://golang.org/x/text
----------------------------------------------------------------
Copyright (c) 2019 The Go Authors. All rights reserved.
Copyright 2009 The Go Authors.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
@ -895,7 +619,7 @@ notice, this list of conditions and the following disclaimer.
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
* Neither the name of Google LLC nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.

View file

@ -16,10 +16,11 @@ You can also list local repositories (+ghq list+).
== SYNOPSIS
[verse]
ghq get [-u] [-p] [--shallow] [--vcs <vcs>] [--look] [--silent] [--branch] [--no-recursive] [--bare] <repository URL>|<host>/<user>/<project>|<user>/<project>|<project>
ghq get [-u] [-p] [--shallow] [--vcs <vcs>] [--look] [--silent] [--branch] [--no-recursive] [--bare] [--partial blobless|treeless] <repository URL>|<host>/<user>/<project>|<user>/<project>|<project>
ghq list [-p] [-e] [<query>]
ghq create [--vcs <vcs>] <repository URL>|<host>/<user>/<project>|<user>/<project>|<project>
ghq rm [--dry-run] <repository URL>|<host>/<user>/<project>|<user>/<project>|<project>
ghq migrate [-y] [--dry-run] <local repository path>
ghq root [--all]
== COMMANDS
@ -44,7 +45,10 @@ get::
The 'ghq' gets the git repository recursively by default. +
We can prevent it with '--no-recursive' option.
With '--bare' option, a "bare clone" will be performed (for Git
repositories only, 'git clone --bare ...' eg.).
repositories only, 'git clone --bare ...' eg.). +
With '--partial' option, a "partial clone" will be performed (for Git
repositories only, in 'blobless' mode, 'git clone --filter=blob:none ...',
in 'treeless' mode, 'git clone --filter=tree:0 ...' eg.).
list::
List locally cloned repositories. If a query argument is given, only
@ -64,6 +68,11 @@ rm::
create::
Creates new repository.
migrate::
Migrate an existing repository directory to the ghq-managed directory structure.
The command detects the VCS backend, retrieves the remote URL, and moves
the repository to the appropriate location under ghq root.
== CONFIGURATION
Configuration uses 'git-config' variables.
@ -76,6 +85,24 @@ ghq.root::
want to specify "$GOPATH/src" as a secondary root (environment variables
should be expanded.)
ghq.user::
In ghq, when specifying only the repository name without slashes as in `ghq get {{Project}}`,
ghq attempts to auto-complete the repository owner.
By default, the owner used is the value of the environment variable `USER` (or `USERNAME` on Windows).
Setting this option allows you to explicitly specify the owner.
ghq.completeUser::
Rather than always using your own username for owner completion,
you may want to complete the owner with the same name as the repository.
For example, fetch `ruby` as `github.com/ruby/ruby`,
`vim` as `github.com/vim/vim`, and `peco` as `github.com/peco/peco`.
If you prefer this behavior, set this option to `false` to switch the owner completion method.
ghq.defaultHost::
The default host used when the repository specification omits the host.
For example, `ghq get owner/project` normally resolves to `github.com/owner/project`.
If this option is set, the specified host will be used instead.
ghq.<url>.vcs::
ghq tries to detect the remote repository's VCS backend for non-"github.com"
repositories. With this option you can explicitly specify the VCS for the
@ -90,6 +117,10 @@ ghq.<url>.root::
you can specify a repository-specific root directory instead of the common ghq root directory. +
The URL is matched against '<url>' using 'git config --get-urlmatch'.
Since `ghq get` runs `git clone` for Git repositories, git's own configuration applies as
well. For example, `clone.defaultRemoteName` changes the name of the created remote, which
is `origin` by default. +
To get this configuration variable effective, you will need Git 2.30 or higher.
=== Example configuration (.gitconfig):
@ -177,6 +208,13 @@ mise install ghq
mise use ghq
----
=== https://github.com/nixos/nixpkgs[nixpkgs]
----
nix run nixpkgs#ghq # run ghq once
nix profile install nixpkgs#ghq # install ghq to your profile
----
=== build
----

View file

@ -1,19 +1,20 @@
package main
import (
"context"
"fmt"
"io"
"os"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v3"
)
func doCreate(c *cli.Context) error {
func doCreate(ctx context.Context, cmd *cli.Command) error {
var (
name = c.Args().First()
vcs = c.String("vcs")
w = c.App.Writer
bare = c.Bool("bare")
name = cmd.Args().First()
vcs = cmd.String("vcs")
w = cmd.Root().Writer
bare = cmd.Bool("bare")
)
if name == "" {

View file

@ -1,6 +1,7 @@
package main
import (
"context"
"errors"
"os"
"os/exec"
@ -128,7 +129,7 @@ func TestDoCreate(t *testing.T) {
var err error
out, _, _ := capture(func() {
err = newApp().Run(append([]string{""}, tc.input...))
err = newApp().Run(context.Background(), append([]string{""}, tc.input...))
})
out = strings.TrimSpace(out)

View file

@ -2,8 +2,10 @@ package main
import (
"bufio"
"context"
"errors"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
@ -12,32 +14,37 @@ import (
"sync"
"github.com/mattn/go-isatty"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v3"
"github.com/x-motemen/ghq/cmdutil"
"github.com/x-motemen/ghq/logger"
"golang.org/x/sync/errgroup"
)
func doGet(c *cli.Context) error {
func doGet(ctx context.Context, cmd *cli.Command) error {
var (
args = c.Args().Slice()
andLook = c.Bool("look")
parallel = c.Bool("parallel")
args = cmd.Args().Slice()
andLook = cmd.Bool("look")
parallel = cmd.Bool("parallel")
silent = cmd.Bool("silent")
)
g := &getter{
update: c.Bool("update"),
shallow: c.Bool("shallow"),
ssh: c.Bool("p"),
vcs: c.String("vcs"),
silent: c.Bool("silent"),
branch: c.String("branch"),
recursive: !c.Bool("no-recursive"),
bare: c.Bool("bare"),
update: cmd.Bool("update"),
shallow: cmd.Bool("shallow"),
ssh: cmd.Bool("p"),
vcs: cmd.String("vcs"),
silent: silent,
branch: cmd.String("branch"),
recursive: !cmd.Bool("no-recursive"),
bare: cmd.Bool("bare"),
partial: cmd.String("partial"),
}
if parallel {
// force silent in parallel import
g.silent = true
}
if silent {
logger.SetOutput(io.Discard)
}
var (
firstArg string // Look at the first repo only, if there are more than one
@ -68,15 +75,25 @@ func doGet(c *cli.Context) error {
sem <- struct{}{}
eg.Go(func() error {
defer func() { <-sem }()
if getInfo, err = g.get(target); err != nil {
logger.Logf("error", "failed to get %q: %s", target, err)
info, getErr := g.get(ctx, target)
getInfo, err = info, getErr
if getErr != nil {
logger.Logf("error", "failed to get %q: %s", target, getErr)
} else if info.localRepository != nil {
fmt.Println(info.localRepository.FullPath)
}
return nil
})
} else {
if getInfo, err = g.get(target); err != nil {
if getInfo, err = g.get(ctx, target); err != nil {
return fmt.Errorf("failed to get %q: %w", target, err)
}
if getInfo.localRepository != nil {
if !silent {
fmt.Fprintln(os.Stderr, "Got the repo to the following:")
}
fmt.Println(getInfo.localRepository.FullPath)
}
}
}
if err = scr.Err(); err != nil {

View file

@ -2,6 +2,7 @@ package main
import (
"bytes"
"context"
"fmt"
"os"
"os/exec"
@ -26,7 +27,7 @@ func TestCommandGet(t *testing.T) {
scenario: func(t *testing.T, tmpRoot string, cloneArgs *_cloneArgs, updateArgs *_updateArgs) {
localDir := filepath.Join(tmpRoot, "github.com", "motemen", "ghq-test-repo")
app.Run([]string{"", "get", "motemen/ghq-test-repo"})
app.Run(context.Background(), []string{"", "get", "motemen/ghq-test-repo"})
expect := "https://github.com/motemen/ghq-test-repo"
if cloneArgs.remote.String() != expect {
@ -53,7 +54,7 @@ func TestCommandGet(t *testing.T) {
scenario: func(t *testing.T, tmpRoot string, cloneArgs *_cloneArgs, updateArgs *_updateArgs) {
localDir := filepath.Join(tmpRoot, "github.com", "motemen", "ghq-test-repo")
app.Run([]string{"", "get", "-p", "motemen/ghq-test-repo"})
app.Run(context.Background(), []string{"", "get", "-p", "motemen/ghq-test-repo"})
expect := "ssh://git@github.com/motemen/ghq-test-repo"
if cloneArgs.remote.String() != expect {
@ -73,7 +74,7 @@ func TestCommandGet(t *testing.T) {
// mark as "already cloned", the condition may change later
os.MkdirAll(filepath.Join(localDir, ".git"), 0755)
app.Run([]string{"", "get", "-update", "motemen/ghq-test-repo"})
app.Run(context.Background(), []string{"", "get", "-update", "motemen/ghq-test-repo"})
if updateArgs.local != localDir {
t.Errorf("got: %s, expect: %s", updateArgs.local, localDir)
@ -84,7 +85,7 @@ func TestCommandGet(t *testing.T) {
scenario: func(t *testing.T, tmpRoot string, cloneArgs *_cloneArgs, updateArgs *_updateArgs) {
localDir := filepath.Join(tmpRoot, "github.com", "motemen", "ghq-test-repo")
app.Run([]string{"", "get", "-shallow", "motemen/ghq-test-repo"})
app.Run(context.Background(), []string{"", "get", "-shallow", "motemen/ghq-test-repo"})
expect := "https://github.com/motemen/ghq-test-repo"
if cloneArgs.remote.String() != expect {
@ -106,7 +107,7 @@ func TestCommandGet(t *testing.T) {
os.Chdir(localDir)
defer os.Chdir(wd)
app.Run([]string{"", "get", "-update", "." + string(filepath.Separator) + "ghq-test-repo"})
app.Run(context.Background(), []string{"", "get", "-update", "." + string(filepath.Separator) + "ghq-test-repo"})
expect := "https://github.com/motemen/ghq-test-repo"
if cloneArgs.remote.String() != expect {
@ -126,7 +127,7 @@ func TestCommandGet(t *testing.T) {
os.Chdir(localDir)
defer os.Chdir(wd)
app.Run([]string{"", "get", "-update", ".." + string(filepath.Separator) + "ghq-another-test-repo"})
app.Run(context.Background(), []string{"", "get", "-update", ".." + string(filepath.Separator) + "ghq-another-test-repo"})
expect := "https://github.com/motemen/ghq-another-test-repo"
if cloneArgs.remote.String() != expect {
@ -143,7 +144,7 @@ func TestCommandGet(t *testing.T) {
localDir := filepath.Join(tmpRoot, "github.com", "motemen", "ghq-test-repo")
expectBranch := "hello"
app.Run([]string{"", "get", "-shallow", "-branch", expectBranch, "motemen/ghq-test-repo"})
app.Run(context.Background(), []string{"", "get", "-shallow", "-branch", expectBranch, "motemen/ghq-test-repo"})
expect := "https://github.com/motemen/ghq-test-repo"
if cloneArgs.remote.String() != expect {
@ -162,7 +163,7 @@ func TestCommandGet(t *testing.T) {
localDir := filepath.Join(tmpRoot, "github.com", "motemen", "ghq-test-repo")
expectBranch := "hello"
app.Run([]string{"", "get", "-shallow", "motemen/ghq-test-repo@" + expectBranch})
app.Run(context.Background(), []string{"", "get", "-shallow", "motemen/ghq-test-repo@" + expectBranch})
expect := "https://github.com/motemen/ghq-test-repo"
if cloneArgs.remote.String() != expect {
@ -178,7 +179,7 @@ func TestCommandGet(t *testing.T) {
}, {
name: "with --no-recursive option",
scenario: func(t *testing.T, tmpRoot string, cloneArgs *_cloneArgs, updateArgs *_updateArgs) {
app.Run([]string{"", "get", "--no-recursive", "motemen/ghq-test-repo"})
app.Run(context.Background(), []string{"", "get", "--no-recursive", "motemen/ghq-test-repo"})
localDir := filepath.Join(tmpRoot, "github.com", "motemen", "ghq-test-repo")
if filepath.ToSlash(cloneArgs.local) != filepath.ToSlash(localDir) {
@ -196,7 +197,7 @@ func TestCommandGet(t *testing.T) {
[ghq "https://github.com/motemen"]
root = "%s"
`, filepath.ToSlash(tmpd))))
app.Run([]string{"", "get", "motemen/ghq-test-repo"})
app.Run(context.Background(), []string{"", "get", "motemen/ghq-test-repo"})
localDir := filepath.Join(tmpd, "github.com", "motemen", "ghq-test-repo")
if filepath.ToSlash(cloneArgs.local) != filepath.ToSlash(localDir) {
@ -208,7 +209,7 @@ func TestCommandGet(t *testing.T) {
scenario: func(t *testing.T, tmpRoot string, cloneArgs *_cloneArgs, updateArgs *_updateArgs) {
localDir := filepath.Join(tmpRoot, "github.com", "motemen", "ghq-test-repo.git")
app.Run([]string{"", "get", "--bare", "motemen/ghq-test-repo"})
app.Run(context.Background(), []string{"", "get", "--bare", "motemen/ghq-test-repo"})
expect := "https://github.com/motemen/ghq-test-repo"
if cloneArgs.remote.String() != expect {
@ -221,6 +222,79 @@ func TestCommandGet(t *testing.T) {
t.Errorf("cloneArgs.bare should be true")
}
},
}, {
name: "silent mode",
scenario: func(t *testing.T, tmpRoot string, cloneArgs *_cloneArgs, updateArgs *_updateArgs) {
localDir := filepath.Join(tmpRoot, "github.com", "motemen", "ghq-test-repo")
out, _, err := captureWithInput([]string{}, func() {
app.Run(context.Background(), []string{"", "get", "--silent", "motemen/ghq-test-repo"})
})
if err != nil {
t.Errorf("error should be nil, but: %s", err)
}
expect := "https://github.com/motemen/ghq-test-repo"
if cloneArgs.remote.String() != expect {
t.Errorf("got: %s, expect: %s", cloneArgs.remote, expect)
}
if filepath.ToSlash(cloneArgs.local) != filepath.ToSlash(localDir) {
t.Errorf("got: %s, expect: %s", filepath.ToSlash(cloneArgs.local), filepath.ToSlash(localDir))
}
if !cloneArgs.silent {
t.Errorf("cloneArgs.silent should be true")
}
if !strings.Contains(out, localDir) {
t.Errorf("silent mode should still print local path to stdout, but got: %q", out)
}
},
}, {
name: "[partial] blobless",
scenario: func(t *testing.T, tmpRoot string, cloneArgs *_cloneArgs, updateArgs *_updateArgs) {
localDir := filepath.Join(tmpRoot, "github.com", "motemen", "ghq-test-repo")
app.Run(context.Background(), []string{"", "get", "--partial", "blobless", "motemen/ghq-test-repo"})
expect := "https://github.com/motemen/ghq-test-repo"
if cloneArgs.remote.String() != expect {
t.Errorf("got: %s, expect: %s", cloneArgs.remote, expect)
}
if filepath.ToSlash(cloneArgs.local) != filepath.ToSlash(localDir) {
t.Errorf("got: %s, expect: %s", filepath.ToSlash(cloneArgs.local), filepath.ToSlash(localDir))
}
if cloneArgs.partial != "blobless" {
t.Errorf("cloneArgs.partial should be \"blobless\"")
}
},
}, {
name: "[partial] treeless",
scenario: func(t *testing.T, tmpRoot string, cloneArgs *_cloneArgs, updateArgs *_updateArgs) {
localDir := filepath.Join(tmpRoot, "github.com", "motemen", "ghq-test-repo")
app.Run(context.Background(), []string{"", "get", "--partial", "treeless", "motemen/ghq-test-repo"})
expect := "https://github.com/motemen/ghq-test-repo"
if cloneArgs.remote.String() != expect {
t.Errorf("got: %s, expect: %s", cloneArgs.remote, expect)
}
if filepath.ToSlash(cloneArgs.local) != filepath.ToSlash(localDir) {
t.Errorf("got: %s, expect: %s", filepath.ToSlash(cloneArgs.local), filepath.ToSlash(localDir))
}
if cloneArgs.partial != "treeless" {
t.Errorf("cloneArgs.partial should be \"treeless\"")
}
},
}, {
name: "[partial] unacceptable value",
scenario: func(t *testing.T, tmpRoot string, cloneArgs *_cloneArgs, updateArgs *_updateArgs) {
err := app.Run(context.Background(), []string{"", "get", "--partial", "unacceptable", "motemen/ghq-test-repo"})
expect := "flag partial value \"unacceptable\" is not allowed"
if err.Error() != expect {
t.Errorf("got: %s, expect: %s", err.Error(), expect)
}
},
}}
for _, tc := range testCases {
@ -230,6 +304,106 @@ func TestCommandGet(t *testing.T) {
}
}
func TestCommandGet_printPath(t *testing.T) {
testCases := []struct {
name string
args []string
inputRepos []string
}{{
name: "single repo",
args: []string{"", "get", "motemen/ghq-test-repo"},
inputRepos: nil,
}, {
name: "bulk from stdin",
args: []string{"", "get"},
inputRepos: []string{"github.com/x-motemen/ghq", "github.com/motemen/gore"},
}, {
name: "bulk parallel",
args: []string{"", "get", "--parallel"},
inputRepos: []string{"github.com/x-motemen/ghq", "github.com/motemen/gore"},
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
withFakeGitBackend(t, func(t *testing.T, tmpRoot string, _ *_cloneArgs, _ *_updateArgs) {
// pre-create dirs for bulk cases
for _, r := range tc.inputRepos {
os.MkdirAll(filepath.Join(tmpRoot, r, ".git"), 0755)
}
var out string
var err error
if len(tc.inputRepos) > 0 {
out, _, err = captureWithInput(tc.inputRepos, func() {
newApp().Run(context.Background(), tc.args)
})
} else {
out, _, err = capture(func() {
newApp().Run(context.Background(), tc.args)
})
}
if err != nil {
t.Fatalf("capture error: %s", err)
}
lines := strings.Split(strings.TrimRight(out, "\n"), "\n")
if len(tc.inputRepos) == 0 {
// single repo: output should contain the local path
if len(lines) != 1 || lines[0] == "" {
t.Errorf("expected one path in output, got: %q", out)
}
if !filepath.IsAbs(lines[0]) {
t.Errorf("expected absolute path, got: %q", lines[0])
}
} else {
// bulk: one path per repo
if len(lines) != len(tc.inputRepos) {
t.Errorf("expected %d paths, got %d: %q", len(tc.inputRepos), len(lines), out)
}
for _, line := range lines {
if !filepath.IsAbs(line) {
t.Errorf("expected absolute path, got: %q", line)
}
}
}
})
})
}
}
func TestCommandGet_gotMessage(t *testing.T) {
t.Run("prints 'Got the repo to the following:' to stderr", func(t *testing.T) {
withFakeGitBackend(t, func(t *testing.T, tmpRoot string, _ *_cloneArgs, _ *_updateArgs) {
_, errOut, err := capture(func() {
newApp().Run(context.Background(), []string{"", "get", "motemen/ghq-test-repo"})
})
if err != nil {
t.Fatalf("capture error: %s", err)
}
if !strings.Contains(errOut, "Got the repo to the following:") {
t.Errorf("expected stderr to contain 'Got the repo to the following:', got: %q", errOut)
}
})
})
t.Run("suppresses message with --silent but still prints path to stdout", func(t *testing.T) {
withFakeGitBackend(t, func(t *testing.T, tmpRoot string, _ *_cloneArgs, _ *_updateArgs) {
out, errOut, err := capture(func() {
newApp().Run(context.Background(), []string{"", "get", "--silent", "motemen/ghq-test-repo"})
})
if err != nil {
t.Fatalf("capture error: %s", err)
}
if strings.Contains(errOut, "Got the repo to the following:") {
t.Errorf("expected stderr not to contain 'Got the repo to the following:' with --silent, got: %q", errOut)
}
if !filepath.IsAbs(strings.TrimRight(out, "\n")) {
t.Errorf("expected absolute path in stdout, got: %q", out)
}
})
})
}
func TestLook(t *testing.T) {
withFakeGitBackend(t, func(t *testing.T, tmproot string, _ *_cloneArgs, _ *_updateArgs) {
os.MkdirAll(filepath.Join(tmproot, "github.com", "motemen", "ghq", ".git"), 0755)
@ -245,7 +419,7 @@ func TestLook(t *testing.T) {
}
sh := detectShell()
err := newApp().Run([]string{"", "get", "--look", "https://github.com/motemen/ghq"})
err := newApp().Run(context.Background(), []string{"", "get", "--look", "https://github.com/motemen/ghq"})
if err != nil {
t.Errorf("error should be nil, but: %s", err)
}
@ -291,7 +465,7 @@ func TestBareLook(t *testing.T) {
}
sh := detectShell()
err := newApp().Run([]string{"", "get", "--bare", "--look", "https://github.com/motemen/ghq.git"})
err := newApp().Run(context.Background(), []string{"", "get", "--bare", "--look", "https://github.com/motemen/ghq.git"})
if err != nil {
t.Errorf("error should be nil, but: %s", err)
}
@ -353,15 +527,18 @@ func TestDoGet_bulk(t *testing.T) {
buf.Reset()
out, _, err := captureWithInput(in, func() {
args := append([]string{"", "get"}, tc.args...)
if err := newApp().Run(args); err != nil {
if err := newApp().Run(context.Background(), args); err != nil {
t.Errorf("error should be nil but: %s", err)
}
})
if err != nil {
t.Errorf("error should be nil, but: %s", err)
}
if out != "" {
t.Errorf("out should be empty, but: %s", out)
for _, r := range in {
expectedPath := filepath.Join(tmproot, r)
if !strings.Contains(out, expectedPath) {
t.Errorf("out should contain %q, but got: %s", expectedPath, out)
}
}
log := filepath.ToSlash(buf.String())
for _, r := range in {

View file

@ -1,24 +1,25 @@
package main
import (
"context"
"fmt"
"path/filepath"
"sort"
"strings"
"sync"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v3"
)
func doList(c *cli.Context) error {
func doList(ctx context.Context, cmd *cli.Command) error {
var (
w = c.App.Writer
query = c.Args().First()
exact = c.Bool("exact")
vcsBackend = c.String("vcs")
printFullPaths = c.Bool("full-path")
printUniquePaths = c.Bool("unique")
bare = c.Bool("bare")
w = cmd.Root().Writer
query = cmd.Args().First()
exact = cmd.Bool("exact")
vcsBackend = cmd.String("vcs")
printFullPaths = cmd.Bool("full-path")
printUniquePaths = cmd.Bool("unique")
bare = cmd.Bool("bare")
)
filterByQuery := func(_ *LocalRepository) bool {

View file

@ -1,7 +1,7 @@
package main
import (
"flag"
"context"
"os"
"path/filepath"
"runtime"
@ -9,26 +9,11 @@ import (
"strings"
"sync"
"testing"
"github.com/urfave/cli/v2"
)
func flagSet(name string, flags []cli.Flag) *flag.FlagSet {
set := flag.NewFlagSet(name, flag.ContinueOnError)
for _, f := range flags {
f.Apply(set)
}
return set
}
func TestCommandList(t *testing.T) {
_, _, err := capture(func() {
app := cli.NewApp()
flagSet := flagSet("list", commandList.Flags)
c := cli.NewContext(app, flagSet, nil)
doList(c)
newApp().Run(context.Background(), []string{"ghq", "list"})
})
if err != nil {
@ -38,12 +23,7 @@ func TestCommandList(t *testing.T) {
func TestCommandListUnique(t *testing.T) {
_, _, err := capture(func() {
app := cli.NewApp()
flagSet := flagSet("list", commandList.Flags)
flagSet.Parse([]string{"--unique"})
c := cli.NewContext(app, flagSet, nil)
doList(c)
newApp().Run(context.Background(), []string{"ghq", "list", "--unique"})
})
if err != nil {
@ -53,12 +33,7 @@ func TestCommandListUnique(t *testing.T) {
func TestCommandListUnknown(t *testing.T) {
_, _, err := capture(func() {
app := cli.NewApp()
flagSet := flagSet("list", commandList.Flags)
flagSet.Parse([]string{"--unknown-flag"})
c := cli.NewContext(app, flagSet, nil)
doList(c)
newApp().Run(context.Background(), []string{"ghq", "list", "--unknown-flag"})
})
if err != nil {
@ -158,7 +133,7 @@ func TestDoList_query(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
args := append([]string{"ghq", "list"}, tc.args...)
out, _, _ := capture(func() {
newApp().Run(args)
newApp().Run(context.Background(), args)
})
if !equalPathLines(out, tc.expect) {
t.Errorf("got:\n%s\nexpect:\n%s", out, tc.expect)
@ -177,7 +152,7 @@ func TestDoList_query(t *testing.T) {
fullExpect += "\n"
}
out, _, _ = capture(func() {
newApp().Run(argsFull)
newApp().Run(context.Background(), argsFull)
})
if !equalPathLines(out, fullExpect) {
t.Errorf("got:\n%s\nexpect:\n%s", out, fullExpect)
@ -201,7 +176,7 @@ func TestDoList_unique(t *testing.T) {
os.MkdirAll(filepath.Join(rootPath, "github.com/motemen/ghq/.git"), 0755)
}
out, _, _ := capture(func() {
newApp().Run([]string{"ghq", "list", "--unique"})
newApp().Run(context.Background(), []string{"ghq", "list", "--unique"})
})
if out != "ghq\n" {
t.Errorf("got: %s, expect: ghq\n", out)
@ -214,7 +189,7 @@ func TestDoList_unknownRoot(t *testing.T) {
_localRepositoryRoots = nil
localRepoOnce = &sync.Once{}
err := newApp().Run([]string{"ghq", "list"})
err := newApp().Run(context.Background(), []string{"ghq", "list"})
if err != nil {
t.Errorf("error should be nil, but: %v", err)
}
@ -233,7 +208,7 @@ func TestDoList_notPermittedRoot(t *testing.T) {
localRepoOnce = &sync.Once{}
os.Chmod(tmpdir, 0000)
err := newApp().Run([]string{"ghq", "list"})
err := newApp().Run(context.Background(), []string{"ghq", "list"})
if err != nil {
t.Errorf("error should be nil, but: %+v", err)
}
@ -253,7 +228,7 @@ func TestDoList_withSystemHiddenDir(t *testing.T) {
_localRepositoryRoots = nil
localRepoOnce = &sync.Once{}
err := newApp().Run([]string{"ghq", "list"})
err := newApp().Run(context.Background(), []string{"ghq", "list"})
if err != nil {
t.Errorf("error should be nil, but: %v", err)
}

205
cmd_migrate.go Normal file
View file

@ -0,0 +1,205 @@
package main
import (
"context"
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"syscall"
"github.com/otiai10/copy"
"github.com/urfave/cli/v3"
"github.com/x-motemen/ghq/logger"
)
func doMigrate(ctx context.Context, cmd *cli.Command) error {
var (
repoDir = cmd.Args().First()
dry = cmd.Bool("dry-run")
skipConfirm = cmd.Bool("y")
w = cmd.Root().Writer
)
if repoDir == "" {
return fmt.Errorf("repository directory is required")
}
// Resolve directory (supports both absolute and relative paths)
absDir, err := filepath.Abs(repoDir)
if err != nil {
return fmt.Errorf("failed to resolve directory path: %w", err)
}
// Check if the directory exists
if _, err := os.Stat(absDir); os.IsNotExist(err) {
return fmt.Errorf("directory %q does not exist", absDir)
} else if err != nil {
return fmt.Errorf("failed to access directory %q: %w", absDir, err)
}
// Detect VCS backend
vcsBackend := findVCSBackend(absDir, "")
if vcsBackend == nil {
return fmt.Errorf("failed to detect VCS backend in %q", absDir)
}
// Refuse to migrate a linked Git checkout (worktree or submodule).
// These have a .git file referencing a parent repo; moving them alone
// breaks the link.
if vcsBackend == GitBackend {
if linked, target, err := isLinkedGitDir(absDir); err != nil {
return fmt.Errorf("failed to check .git link status: %w", err)
} else if linked {
return fmt.Errorf("directory %q has a .git file linking to %q; it is a worktree or submodule and cannot be migrated independently", absDir, target)
}
}
// Get remote URL
if vcsBackend.RemoteURL == nil {
return fmt.Errorf("migrate is not supported for this VCS backend")
}
remoteURL, err := vcsBackend.RemoteURL(absDir)
if err != nil {
return fmt.Errorf("failed to get remote URL: %w", err)
}
// Parse the remote URL
u, err := newURL(remoteURL, false, false)
if err != nil {
return fmt.Errorf("failed to parse remote URL %q: %w", remoteURL, err)
}
// Derive destination path
localRepo, err := LocalRepositoryFromURL(u, false)
if err != nil {
return fmt.Errorf("failed to derive destination path: %w", err)
}
destPath := localRepo.FullPath
// Check if source and destination are the same
if absDir == destPath {
return fmt.Errorf("repository is already at the correct location: %s", destPath)
}
// Check if destination already exists
if _, err := os.Stat(destPath); err == nil {
return fmt.Errorf("destination directory %q already exists", destPath)
} else if !os.IsNotExist(err) {
return fmt.Errorf("failed to check destination directory: %w", err)
}
// Check for linked worktrees before dry-run return so we can report them
var hasWorktrees bool
if vcsBackend == GitBackend {
hasWorktrees, err = hasLinkedWorktrees(absDir)
if err != nil {
return fmt.Errorf("failed to check for linked worktrees: %w", err)
}
}
// Dry-run mode
if dry {
fmt.Fprintf(w, "Would migrate %s to %s\n", absDir, destPath)
if hasWorktrees {
fmt.Fprintf(w, "Would run 'git worktree repair' to update linked worktrees\n")
}
return nil
}
// Confirmation prompt (skip if -y flag is set)
if !skipConfirm {
ok, err := confirm(fmt.Sprintf("Migrate %s to %s?", absDir, destPath))
if err != nil {
return err
}
if !ok {
return fmt.Errorf("migration aborted by user")
}
}
// Create parent directories
destDir := filepath.Dir(destPath)
if err := os.MkdirAll(destDir, 0755); err != nil {
return fmt.Errorf("failed to create parent directories: %w", err)
}
// Move the repository
if err := moveDir(absDir, destPath); err != nil {
return fmt.Errorf("failed to move repository: %w", err)
}
// Repair linked worktrees so their .git files reference the new location.
//
// For each worktree, two pointers exist:
// back-pointer: .git/worktrees/<name>/gitdir → worktree working dir
// forward ref: <worktree>/.git → main repo's .git/worktrees/<name>
//
// External worktrees (outside the repo) didn't move, so only the forward
// ref is stale. Internal worktrees (inside the repo) moved along with
// the repo, so BOTH pointers are stale. We fix the back-pointers first
// so that "git worktree repair" can match entries to update the forward refs.
if hasWorktrees {
wtPaths, wtErr := repairWorktreeBackPointers(absDir, destPath)
if wtErr != nil {
logger.Log("warning", fmt.Sprintf("failed to discover linked worktree paths: %v", wtErr))
} else if len(wtPaths) > 0 {
args := append([]string{"worktree", "repair"}, wtPaths...)
cmd := exec.Command("git", args...)
cmd.Dir = destPath
if out, err := cmd.CombinedOutput(); err != nil {
logger.Log("warning", fmt.Sprintf("git worktree repair failed: %v\n%s", err, out))
}
}
}
fmt.Fprintln(w, destPath)
return nil
}
// moveDir attempts to move directory from src to dst, with fallback for cross-device moves
func moveDir(src, dst string) error {
// Try atomic rename first
renameErr := os.Rename(src, dst)
if renameErr == nil {
return nil
}
// Check for cross-device error
var linkError *os.LinkError
isCrossDevice := errors.As(renameErr, &linkError) && errors.Is(linkError.Err, syscall.EXDEV)
if !isCrossDevice {
return renameErr
}
// Fallback: copy directory tree using otiai10/copy, then remove source
opt := copy.Options{
// Preserve symlinks as-is
OnSymlink: func(src string) copy.SymlinkAction {
return copy.Shallow
},
// Skip special files (pipes, sockets, devices) as they're uncommon in repos
Skip: func(srcinfo os.FileInfo, src, dest string) (bool, error) {
mode := srcinfo.Mode()
// Skip if not regular file, directory, or symlink
if !mode.IsRegular() && !mode.IsDir() && mode&os.ModeSymlink == 0 {
return true, nil
}
return false, nil
},
}
copyErr := copy.Copy(src, dst, opt)
if copyErr != nil {
// Attempt to cleanup partial copy
if cleanupErr := os.RemoveAll(dst); cleanupErr != nil {
return fmt.Errorf("copy failed: %w (cleanup also failed: %v)", copyErr, cleanupErr)
}
return copyErr
}
return os.RemoveAll(src)
}

459
cmd_migrate_test.go Normal file
View file

@ -0,0 +1,459 @@
package main
import (
"context"
"os"
"os/exec"
"path/filepath"
"strings"
"sync"
"testing"
)
// initGitRepo creates a git repo at dir with the given remote URL and an
// initial empty commit. It returns dir for convenience.
func initGitRepo(t *testing.T, dir, remoteURL string) string {
t.Helper()
os.MkdirAll(dir, 0755)
for _, args := range [][]string{
{"init"},
{"remote", "add", "origin", remoteURL},
{"-c", "user.name=test", "-c", "user.email=test@test.com",
"commit", "--allow-empty", "-m", "init"},
} {
c := exec.Command("git", args...)
c.Dir = dir
if out, err := c.CombinedOutput(); err != nil {
t.Fatalf("git %s: %v\n%s", args[0], err, out)
}
}
return dir
}
// addWorktree creates a git worktree at wtDir branching from the repo at repoDir.
func addWorktree(t *testing.T, repoDir, wtDir, branch string) {
t.Helper()
c := exec.Command("git", "worktree", "add", "-b", branch, wtDir)
c.Dir = repoDir
if out, err := c.CombinedOutput(); err != nil {
t.Fatalf("git worktree add: %v\n%s", err, out)
}
}
// Test for the migrate command
func TestDoMigrate(t *testing.T) {
defer func(x string) { _home = x }(_home)
_home = ""
homeOnce = &sync.Once{}
tmpdir := newTempDir(t)
defer func(y []string) { _localRepositoryRoots = y }(_localRepositoryRoots)
setEnv(t, envGhqRoot, tmpdir)
_localRepositoryRoots = nil
localRepoOnce = &sync.Once{}
// Test case: successful migration
t.Run("migrate_success", func(t *testing.T) {
srcdir := filepath.Join(tmpdir, "sources", "proj")
os.MkdirAll(srcdir, 0755)
c1 := exec.Command("git", "init")
c1.Dir = srcdir
c1.Run()
c2 := exec.Command("git", "remote", "add", "origin", "https://github.com/alice/proj.git")
c2.Dir = srcdir
c2.Run()
a := newApp()
e := a.Run(context.Background(), []string{"ghq", "migrate", "-y", srcdir})
if e != nil {
t.Fatal(e)
}
dest := filepath.Join(tmpdir, "github.com", "alice", "proj")
if _, err := os.Stat(dest); os.IsNotExist(err) {
t.Error("dest not found")
}
})
// Test case: nonexistent directory
t.Run("migrate_nonexist", func(t *testing.T) {
a := newApp()
e := a.Run(context.Background(), []string{"ghq", "migrate", "-y", "/does/not/exist"})
if e == nil {
t.Error("expected error")
}
})
// Test case: dry run
t.Run("migrate_dryrun", func(t *testing.T) {
srcdir := filepath.Join(tmpdir, "sources2", "proj2")
os.MkdirAll(srcdir, 0755)
c1 := exec.Command("git", "init")
c1.Dir = srcdir
c1.Run()
c2 := exec.Command("git", "remote", "add", "origin", "https://github.com/bob/proj2.git")
c2.Dir = srcdir
c2.Run()
a := newApp()
e := a.Run(context.Background(), []string{"ghq", "migrate", "--dry-run", srcdir})
if e != nil {
t.Fatal(e)
}
if _, err := os.Stat(srcdir); os.IsNotExist(err) {
t.Error("source should still exist")
}
})
// Test case: migrate repo with linked worktrees repairs forward references
t.Run("migrate_with_linked_worktrees", func(t *testing.T) {
srcdir := initGitRepo(t, filepath.Join(tmpdir, "sources_wt", "main"),
"https://github.com/wt-user/main.git")
wtDir := filepath.Join(tmpdir, "sources_wt", "wt")
addWorktree(t, srcdir, wtDir, "wt-branch")
a := newApp()
e := a.Run(context.Background(), []string{"ghq", "migrate", "-y", srcdir})
if e != nil {
t.Fatal(e)
}
dest := filepath.Join(tmpdir, "github.com", "wt-user", "main")
if _, err := os.Stat(dest); os.IsNotExist(err) {
t.Error("dest not found")
}
// Verify worktree's .git file has exact gitdir: reference to new location
content, err := os.ReadFile(filepath.Join(wtDir, ".git"))
if err != nil {
t.Fatal(err)
}
wantGitdir := "gitdir: " + filepath.ToSlash(filepath.Join(dest, ".git", "worktrees", "wt"))
if got := strings.TrimSpace(string(content)); got != wantGitdir {
t.Errorf("worktree .git:\n got: %s\n want: %s", got, wantGitdir)
}
// Verify git status works in the worktree after migration
c := exec.Command("git", "status")
c.Dir = wtDir
if out, err := c.CombinedOutput(); err != nil {
t.Errorf("git status in worktree failed after migration: %v\n%s", err, out)
}
})
// Test case: dry run with linked worktrees mentions repair
t.Run("migrate_dryrun_with_worktrees", func(t *testing.T) {
srcdir := initGitRepo(t, filepath.Join(tmpdir, "sources_wt_dry", "main"),
"https://github.com/wt-dry/proj.git")
wtDir := filepath.Join(tmpdir, "sources_wt_dry", "wt")
addWorktree(t, srcdir, wtDir, "wt-dry-branch")
out, _, err := capture(func() {
a := newApp()
a.Run(context.Background(), []string{"ghq", "migrate", "--dry-run", srcdir})
})
if err != nil {
t.Fatal(err)
}
if !strings.Contains(out, "Would migrate") {
t.Errorf("expected dry-run migration message, got: %s", out)
}
if !strings.Contains(out, "worktree repair") {
t.Errorf("expected worktree repair mention in dry-run, got: %s", out)
}
if _, err := os.Stat(srcdir); os.IsNotExist(err) {
t.Error("source should still exist in dry-run mode")
}
})
// Test case: worktree inside the repo directory moves along with it
t.Run("migrate_with_internal_worktree", func(t *testing.T) {
srcdir := initGitRepo(t, filepath.Join(tmpdir, "sources_wt_int", "main"),
"https://github.com/wt-int/proj.git")
// Create worktree INSIDE the repo directory
wtDir := filepath.Join(srcdir, ".worktrees", "feat")
addWorktree(t, srcdir, wtDir, "wt-int-branch")
a := newApp()
e := a.Run(context.Background(), []string{"ghq", "migrate", "-y", srcdir})
if e != nil {
t.Fatal(e)
}
dest := filepath.Join(tmpdir, "github.com", "wt-int", "proj")
if _, err := os.Stat(dest); os.IsNotExist(err) {
t.Error("dest not found")
}
// The worktree moved with the repo — verify its .git file
// has exact gitdir: reference to the new main repo location
newWtDir := filepath.Join(dest, ".worktrees", "feat")
content, err := os.ReadFile(filepath.Join(newWtDir, ".git"))
if err != nil {
t.Fatal(err)
}
wantGitdir := "gitdir: " + filepath.ToSlash(filepath.Join(dest, ".git", "worktrees", "feat"))
if got := strings.TrimSpace(string(content)); got != wantGitdir {
t.Errorf("internal worktree .git:\n got: %s\n want: %s", got, wantGitdir)
}
// Verify git status works in the internal worktree after migration
c := exec.Command("git", "status")
c.Dir = newWtDir
if out, err := c.CombinedOutput(); err != nil {
t.Errorf("git status in internal worktree failed after migration: %v\n%s", err, out)
}
})
}
func TestMigrateEdgeCases(t *testing.T) {
defer func(x string) { _home = x }(_home)
_home = ""
homeOnce = &sync.Once{}
tmpdir := newTempDir(t)
defer func(y []string) { _localRepositoryRoots = y }(_localRepositoryRoots)
setEnv(t, envGhqRoot, tmpdir)
_localRepositoryRoots = nil
localRepoOnce = &sync.Once{}
t.Run("no_vcs_backend", func(t *testing.T) {
srcdir := filepath.Join(tmpdir, "src3", "not-repo")
os.MkdirAll(srcdir, 0755)
a := newApp()
e := a.Run(context.Background(), []string{"ghq", "migrate", "-y", srcdir})
if e == nil {
t.Error("should fail when no VCS found")
}
})
t.Run("no_remote_url", func(t *testing.T) {
srcdir := filepath.Join(tmpdir, "src4", "no-rem")
os.MkdirAll(srcdir, 0755)
c := exec.Command("git", "init")
c.Dir = srcdir
c.Run()
a := newApp()
e := a.Run(context.Background(), []string{"ghq", "migrate", "-y", srcdir})
if e == nil {
t.Error("should fail when no remote")
}
})
t.Run("dest_already_exists", func(t *testing.T) {
srcdir := filepath.Join(tmpdir, "src5", "exist")
os.MkdirAll(srcdir, 0755)
c1 := exec.Command("git", "init")
c1.Dir = srcdir
c1.Run()
c2 := exec.Command("git", "remote", "add", "origin", "https://github.com/user3/exist.git")
c2.Dir = srcdir
c2.Run()
dest := filepath.Join(tmpdir, "github.com", "user3", "exist")
os.MkdirAll(dest, 0755)
a := newApp()
e := a.Run(context.Background(), []string{"ghq", "migrate", "-y", srcdir})
if e == nil {
t.Error("should fail when dest exists")
}
})
t.Run("migrate_worktree_refused", func(t *testing.T) {
srcdir := initGitRepo(t, filepath.Join(tmpdir, "src_wt_ref", "main"),
"https://github.com/wt-ref/proj.git")
wtDir := filepath.Join(tmpdir, "src_wt_ref", "wt")
addWorktree(t, srcdir, wtDir, "wt-ref-branch")
a := newApp()
e := a.Run(context.Background(), []string{"ghq", "migrate", "-y", wtDir})
if e == nil {
t.Fatal("expected error migrating a worktree")
}
if !strings.Contains(e.Error(), "worktree or submodule") {
t.Errorf("error should mention worktree or submodule, got: %v", e)
}
if !strings.Contains(e.Error(), ".git") {
t.Errorf("error should mention .git link target, got: %v", e)
}
})
t.Run("unsupported_vcs", func(t *testing.T) {
// Create a CVS repository structure to test unsupported VCS
srcdir := filepath.Join(tmpdir, "src6", "cvs-repo")
cvsDir := filepath.Join(srcdir, "CVS")
os.MkdirAll(cvsDir, 0755)
// Create a minimal CVS/Repository file
repoFile := filepath.Join(cvsDir, "Repository")
os.WriteFile(repoFile, []byte("test-repo\n"), 0644)
a := newApp()
e := a.Run(context.Background(), []string{"ghq", "migrate", "-y", srcdir})
if e == nil {
t.Error("should fail for unsupported VCS (CVS)")
}
// Check that the error message mentions unsupported VCS
if e != nil && !strings.Contains(e.Error(), "not supported") {
t.Errorf("expected 'not supported' error, got: %v", e)
}
})
}
func TestMoveDir(t *testing.T) {
tmpdir := newTempDir(t)
t.Run("move_same_device", func(t *testing.T) {
srcDir := filepath.Join(tmpdir, "move_src")
dstDir := filepath.Join(tmpdir, "move_dst")
os.MkdirAll(srcDir, 0755)
os.WriteFile(filepath.Join(srcDir, "testfile.txt"), []byte("test"), 0644)
if err := moveDir(srcDir, dstDir); err != nil {
t.Fatal(err)
}
// Verify destination exists
if _, err := os.Stat(dstDir); os.IsNotExist(err) {
t.Error("destination directory does not exist")
}
// Verify source is gone
if _, err := os.Stat(srcDir); !os.IsNotExist(err) {
t.Error("source directory still exists")
}
// Verify content
content, err := os.ReadFile(filepath.Join(dstDir, "testfile.txt"))
if err != nil {
t.Fatal(err)
}
if string(content) != "test" {
t.Errorf("content mismatch: got %q, want %q", content, "test")
}
})
t.Run("move_with_subdirectories", func(t *testing.T) {
srcDir := filepath.Join(tmpdir, "move_src2")
dstDir := filepath.Join(tmpdir, "move_dst2")
os.MkdirAll(filepath.Join(srcDir, "sub1", "sub2"), 0755)
os.WriteFile(filepath.Join(srcDir, "root.txt"), []byte("root"), 0644)
os.WriteFile(filepath.Join(srcDir, "sub1", "file1.txt"), []byte("file1"), 0644)
os.WriteFile(filepath.Join(srcDir, "sub1", "sub2", "file2.txt"), []byte("file2"), 0644)
if err := moveDir(srcDir, dstDir); err != nil {
t.Fatal(err)
}
// Verify all files exist
files := []string{
"root.txt",
"sub1/file1.txt",
"sub1/sub2/file2.txt",
}
for _, f := range files {
path := filepath.Join(dstDir, f)
if _, err := os.Stat(path); os.IsNotExist(err) {
t.Errorf("file %s does not exist", f)
}
}
// Verify source is gone
if _, err := os.Stat(srcDir); !os.IsNotExist(err) {
t.Error("source directory still exists")
}
})
// Note: moveDir also has a cross-device (EXDEV) fallback path which is
// difficult to exercise reliably in unit tests because it depends on
// running across different filesystems. That behavior is validated in
// higher-level integration tests / environments that provide multiple
// mounts, rather than in this unit test.
}
func TestIsLinkedGitDir(t *testing.T) {
tmpdir := newTempDir(t)
t.Run("regular_repo", func(t *testing.T) {
dir := filepath.Join(tmpdir, "regular")
os.MkdirAll(dir, 0755)
c := exec.Command("git", "init")
c.Dir = dir
if out, err := c.CombinedOutput(); err != nil {
t.Fatalf("git init: %v\n%s", err, out)
}
linked, _, err := isLinkedGitDir(dir)
if err != nil {
t.Fatal(err)
}
if linked {
t.Error("regular repo should not be detected as linked")
}
})
t.Run("no_git", func(t *testing.T) {
dir := filepath.Join(tmpdir, "nogit")
os.MkdirAll(dir, 0755)
linked, _, err := isLinkedGitDir(dir)
if err != nil {
t.Fatal(err)
}
if linked {
t.Error("directory without .git should not be detected as linked")
}
})
t.Run("submodule_gitfile", func(t *testing.T) {
dir := filepath.Join(tmpdir, "submod")
os.MkdirAll(dir, 0755)
// Simulate a submodule's .git file pointing to .git/modules/
os.WriteFile(filepath.Join(dir, ".git"),
[]byte("gitdir: ../.git/modules/submod\n"), 0644)
linked, target, err := isLinkedGitDir(dir)
if err != nil {
t.Fatal(err)
}
if !linked {
t.Error("submodule should be detected as linked")
}
if !strings.Contains(target, "modules") {
t.Errorf("target should reference modules dir, got: %s", target)
}
})
t.Run("actual_worktree", func(t *testing.T) {
mainDir := initGitRepo(t, filepath.Join(tmpdir, "wt_main"),
"https://github.com/dummy/wt-main.git")
wtDir := filepath.Join(tmpdir, "wt_linked")
addWorktree(t, mainDir, wtDir, "wt-test")
linked, target, err := isLinkedGitDir(wtDir)
if err != nil {
t.Fatal(err)
}
if !linked {
t.Error("worktree should be detected as linked")
}
if !strings.Contains(target, "worktrees") {
t.Errorf("target should reference worktrees dir, got: %s", target)
}
})
}

108
cmd_rm.go
View file

@ -1,18 +1,22 @@
package main
import (
"context"
"fmt"
"os"
"os/exec"
"strings"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v3"
"github.com/x-motemen/ghq/logger"
)
func doRm(c *cli.Context) error {
func doRm(ctx context.Context, cmd *cli.Command) error {
var (
name = c.Args().First()
dry = c.Bool("dry-run")
w = c.App.Writer
bare = c.Bool("bare")
name = cmd.Args().First()
dry = cmd.Bool("dry-run")
w = cmd.Root().Writer
bare = cmd.Bool("bare")
)
if name == "" {
@ -38,12 +42,56 @@ func doRm(c *cli.Context) error {
return fmt.Errorf("directory %q does not exist", p)
}
// Scenario A: Is this path itself a linked worktree?
isWorktree := false
var gitdirTarget string
if linked, target, linkErr := isLinkedGitDir(p); linkErr != nil {
return fmt.Errorf("failed to check worktree status: %w", linkErr)
} else if linked && isWorktreeGitDir(target) {
isWorktree = true
gitdirTarget = target
}
// Scenario B: Does this repo have linked worktrees?
var worktreePaths []string
if !isWorktree {
if hasWt, wtErr := hasLinkedWorktrees(p); wtErr != nil {
return fmt.Errorf("failed to check for linked worktrees: %w", wtErr)
} else if hasWt {
worktreePaths, err = listLinkedWorktreePaths(p)
if err != nil {
return fmt.Errorf("failed to list linked worktrees: %w", err)
}
}
}
// Dry-run
if dry {
fmt.Fprintf(w, "Would remove %s\n", p)
if isWorktree {
fmt.Fprintf(w, "Would remove worktree %s (linked to %s)\n", p, gitdirTarget)
} else if len(worktreePaths) > 0 {
fmt.Fprintf(w, "Would remove %s and its %d linked worktree(s):\n", p, len(worktreePaths))
for _, wt := range worktreePaths {
fmt.Fprintf(w, " %s\n", wt)
}
} else {
fmt.Fprintf(w, "Would remove %s\n", p)
}
return nil
}
ok, err = confirm(fmt.Sprintf("Remove %s?", p))
// Confirmation
var confirmMsg string
if isWorktree {
confirmMsg = fmt.Sprintf("Remove worktree %s?", p)
} else if len(worktreePaths) > 0 {
confirmMsg = fmt.Sprintf("Remove %s and its %d linked worktree(s)?\n %s",
p, len(worktreePaths), strings.Join(worktreePaths, "\n "))
} else {
confirmMsg = fmt.Sprintf("Remove %s?", p)
}
ok, err = confirm(confirmMsg)
if err != nil {
return err
}
@ -51,8 +99,48 @@ func doRm(c *cli.Context) error {
return fmt.Errorf("aborted")
}
if err := os.RemoveAll(p); err != nil {
return err
// Removal
if isWorktree {
// Use git worktree remove to properly unregister from parent repo.
// Resolve the main repo directory so we don't run git from inside
// the directory being deleted.
removed := false
if mainRepoDir, dirErr := resolveMainRepoDir(gitdirTarget); dirErr == nil {
gitCmd := exec.Command("git", "worktree", "remove", "--force", p)
gitCmd.Dir = mainRepoDir
if out, gitErr := gitCmd.CombinedOutput(); gitErr != nil {
logger.Log("warning", fmt.Sprintf("git worktree remove failed: %v\n%s", gitErr, out))
} else {
removed = true
}
} else {
logger.Log("warning", fmt.Sprintf("cannot resolve main repo dir: %v", dirErr))
}
if !removed {
logger.Log("warning", "falling back to direct removal")
if err := os.RemoveAll(p); err != nil {
return err
}
// Best-effort cleanup of dangling .git/worktrees/<name> entry
if gitdirTarget != "" {
os.RemoveAll(gitdirTarget)
}
}
} else {
// Prune linked worktrees before removing main repo
for _, wt := range worktreePaths {
if _, statErr := os.Stat(wt); os.IsNotExist(statErr) {
continue // already gone
}
gitCmd := exec.Command("git", "worktree", "remove", "--force", wt)
gitCmd.Dir = p
if out, gitErr := gitCmd.CombinedOutput(); gitErr != nil {
return fmt.Errorf("failed to remove worktree %s: %w\n%s", wt, gitErr, out)
}
}
if err := os.RemoveAll(p); err != nil {
return err
}
}
fmt.Fprintf(w, "Removed %s\n", p)

View file

@ -1,10 +1,12 @@
package main
import (
"context"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"sync"
"testing"
@ -39,13 +41,25 @@ func TestRmCommand(t *testing.T) {
name: "simple",
input: []string{"rm", "motemen/ghqq"},
setup: func(t *testing.T) {
os.MkdirAll(filepath.Join(tmpd, "github.com", "motemen", "ghqq"), 0755)
if err := os.MkdirAll(filepath.Join(tmpd, "github.com", "motemen", "ghqq", ".git"), 0755); err != nil {
t.Fatal(err)
}
},
expectErr: false,
},
{
name: "empty directory",
input: []string{"rm", "motemen/ghqqq"},
name: "empty directory",
input: []string{"rm", "motemen/ghqqq"},
setup: func(t *testing.T) {
if err := os.MkdirAll(filepath.Join(tmpd, "github.com", "motemen", "ghqqq"), 0755); err != nil {
t.Fatal(err)
}
},
expectErr: true,
},
{
name: "missing directory",
input: []string{"rm", "motemen/missing"},
setup: func(t *testing.T) {},
expectErr: true,
},
@ -85,6 +99,21 @@ func TestRmCommand(t *testing.T) {
if tc.cmdRun != nil {
cmdutil.CommandRunner = tc.cmdRun
}
var runErr error
_, _, err := captureWithInput([]string{"y"}, func() {
a := newApp()
args := append([]string{"ghq"}, tc.input...)
runErr = a.Run(context.Background(), args)
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if gotErr := runErr != nil; gotErr != tc.expectErr {
t.Fatalf("error = %v, expectErr = %v", runErr, tc.expectErr)
}
})
}
}
@ -117,7 +146,9 @@ func TestRmDryRunCommand(t *testing.T) {
name: "simple",
input: []string{"rm", "--dry-run", "motemen/ghqq"},
setup: func(t *testing.T) {
os.MkdirAll(filepath.Join(tmpd, "github.com", "motemen", "ghqq"), 0755)
if err := os.MkdirAll(filepath.Join(tmpd, "github.com", "motemen", "ghqq", ".git"), 0755); err != nil {
t.Fatal(err)
}
},
expectErr: false,
},
@ -137,9 +168,9 @@ func TestRmDryRunCommand(t *testing.T) {
},
{
name: "permission denied",
input: []string{"rm", "--dry-run", "motemen/ghqq"},
input: []string{"rm", "--dry-run", "motemen/ghq-notpermitted"},
setup: func(t *testing.T) {
f := filepath.Join(tmpd, "github.com", "motemen", "ghqq")
f := filepath.Join(tmpd, "github.com", "motemen", "ghq-notpermitted")
os.MkdirAll(f, 0000)
t.Cleanup(func() {
os.Chmod(f, 0755)
@ -163,6 +194,182 @@ func TestRmDryRunCommand(t *testing.T) {
if tc.cmdRun != nil {
cmdutil.CommandRunner = tc.cmdRun
}
var runErr error
_, _, err := capture(func() {
a := newApp()
args := append([]string{"ghq"}, tc.input...)
runErr = a.Run(context.Background(), args)
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if gotErr := runErr != nil; gotErr != tc.expectErr {
t.Fatalf("error = %v, expectErr = %v", runErr, tc.expectErr)
}
})
}
}
func TestRmWorktree(t *testing.T) {
defer func(orig func(cmd *exec.Cmd) error) {
cmdutil.CommandRunner = orig
}(cmdutil.CommandRunner)
cmdutil.CommandRunner = func(cmd *exec.Cmd) error { return nil }
defer func(orig string) { _home = orig }(_home)
_home = ""
homeOnce = &sync.Once{}
tmpd := newTempDir(t)
defer func(orig []string) { _localRepositoryRoots = orig }(_localRepositoryRoots)
setEnv(t, envGhqRoot, tmpd)
_localRepositoryRoots = nil
localRepoOnce = &sync.Once{}
t.Run("rm_linked_worktree", func(t *testing.T) {
// Create main repo inside ghq root
mainDir := initGitRepo(t, filepath.Join(tmpd, "github.com", "wt-rm", "main"),
"https://github.com/wt-rm/main.git")
// Create worktree registered under ghq root so ghq rm can resolve it
wtDir := filepath.Join(tmpd, "github.com", "wt-rm", "wt-linked")
addWorktree(t, mainDir, wtDir, "wt-rm-branch")
_, _, err := captureWithInput([]string{"y"}, func() {
a := newApp()
if e := a.Run(context.Background(), []string{"ghq", "rm", "wt-rm/wt-linked"}); e != nil {
t.Fatal(e)
}
})
if err != nil {
t.Fatal(err)
}
// Worktree directory should be gone
if _, err := os.Stat(wtDir); !os.IsNotExist(err) {
t.Error("worktree directory should be removed")
}
// Parent repo's .git/worktrees/<name> should be cleaned up
wtEntry := filepath.Join(mainDir, ".git", "worktrees", "wt-linked")
if _, err := os.Stat(wtEntry); !os.IsNotExist(err) {
t.Error("parent repo's worktree entry should be cleaned up")
}
// Parent repo should still work
c := exec.Command("git", "status")
c.Dir = mainDir
if out, err := c.CombinedOutput(); err != nil {
t.Errorf("git status in parent repo failed: %v\n%s", err, out)
}
})
t.Run("rm_dryrun_worktree", func(t *testing.T) {
mainDir := initGitRepo(t, filepath.Join(tmpd, "github.com", "wt-dry", "main"),
"https://github.com/wt-dry/main.git")
wtDir := filepath.Join(tmpd, "github.com", "wt-dry", "wt-linked")
addWorktree(t, mainDir, wtDir, "wt-dry-branch")
out, _, err := capture(func() {
a := newApp()
if e := a.Run(context.Background(), []string{"ghq", "rm", "--dry-run", "wt-dry/wt-linked"}); e != nil {
t.Fatal(e)
}
})
if err != nil {
t.Fatal(err)
}
if !strings.Contains(out, "Would remove worktree") {
t.Errorf("expected 'Would remove worktree' in output, got: %s", out)
}
if _, err := os.Stat(wtDir); os.IsNotExist(err) {
t.Error("worktree should still exist after dry-run")
}
})
t.Run("rm_repo_with_linked_worktrees", func(t *testing.T) {
mainDir := initGitRepo(t, filepath.Join(tmpd, "github.com", "wt-parent", "repo"),
"https://github.com/wt-parent/repo.git")
// Create two external worktrees
wt1 := filepath.Join(tmpd, "external-wt1")
wt2 := filepath.Join(tmpd, "external-wt2")
addWorktree(t, mainDir, wt1, "branch1")
addWorktree(t, mainDir, wt2, "branch2")
_, _, err := captureWithInput([]string{"y"}, func() {
a := newApp()
if e := a.Run(context.Background(), []string{"ghq", "rm", "wt-parent/repo"}); e != nil {
t.Fatal(e)
}
})
if err != nil {
t.Fatal(err)
}
// Main repo should be gone
if _, err := os.Stat(mainDir); !os.IsNotExist(err) {
t.Error("main repo should be removed")
}
// Both worktree directories should be gone
if _, err := os.Stat(wt1); !os.IsNotExist(err) {
t.Error("worktree 1 should be removed")
}
if _, err := os.Stat(wt2); !os.IsNotExist(err) {
t.Error("worktree 2 should be removed")
}
})
t.Run("rm_dryrun_with_linked_worktrees", func(t *testing.T) {
mainDir := initGitRepo(t, filepath.Join(tmpd, "github.com", "wt-dry2", "repo"),
"https://github.com/wt-dry2/repo.git")
wt1 := filepath.Join(tmpd, "dry-wt1")
addWorktree(t, mainDir, wt1, "dry-branch1")
out, _, err := capture(func() {
a := newApp()
if e := a.Run(context.Background(), []string{"ghq", "rm", "--dry-run", "wt-dry2/repo"}); e != nil {
t.Fatal(e)
}
})
if err != nil {
t.Fatal(err)
}
if !strings.Contains(out, "linked worktree") {
t.Errorf("expected 'linked worktree' in output, got: %s", out)
}
if _, err := os.Stat(mainDir); os.IsNotExist(err) {
t.Error("repo should still exist after dry-run")
}
})
t.Run("rm_repo_with_already_deleted_worktree", func(t *testing.T) {
mainDir := initGitRepo(t, filepath.Join(tmpd, "github.com", "wt-gone", "repo"),
"https://github.com/wt-gone/repo.git")
wt := filepath.Join(tmpd, "gone-wt")
addWorktree(t, mainDir, wt, "gone-branch")
// Manually delete the worktree directory (simulating user deleting it)
os.RemoveAll(wt)
_, _, err := captureWithInput([]string{"y"}, func() {
a := newApp()
if e := a.Run(context.Background(), []string{"ghq", "rm", "wt-gone/repo"}); e != nil {
t.Fatal(e)
}
})
if err != nil {
t.Fatal(err)
}
// Main repo should be gone regardless
if _, err := os.Stat(mainDir); !os.IsNotExist(err) {
t.Error("main repo should be removed even with pre-deleted worktree")
}
})
}

View file

@ -1,22 +1,23 @@
package main
import (
"context"
"fmt"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v3"
)
func doRoot(c *cli.Context) error {
func doRoot(ctx context.Context, cmd *cli.Command) error {
roots, err := localRepositoryRoots(true)
if err != nil {
return err
}
if !c.Bool("all") {
if !cmd.Bool("all") {
roots = roots[:1] // only the first root is needed
}
for _, root := range roots {
_, err := fmt.Fprintln(c.App.Writer, root)
_, err := fmt.Fprintln(cmd.Root().Writer, root)
if err != nil {
return err
}

View file

@ -1,6 +1,7 @@
package main
import (
"context"
"os"
"path/filepath"
"runtime"
@ -90,13 +91,13 @@ func TestDoRoot(t *testing.T) {
homeOnce = &sync.Once{}
tc.setup(t)
out, _, _ := capture(func() {
newApp().Run([]string{"", "root"})
newApp().Run(context.Background(), []string{"", "root"})
})
if !samePaths(out, tc.expect) {
t.Errorf("got: %s, expect: %s", out, tc.expect)
}
out, _, _ = capture(func() {
newApp().Run([]string{"", "root", "--all"})
newApp().Run(context.Background(), []string{"", "root", "--all"})
})
if !samePaths(out, tc.allExpect) {
t.Errorf("got: %s, expect: %s", out, tc.allExpect)

View file

@ -1,10 +1,12 @@
package main
import (
"context"
"fmt"
"slices"
"strings"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v3"
)
var commands = []*cli.Command{
@ -13,6 +15,7 @@ var commands = []*cli.Command{
commandRm,
commandRoot,
commandCreate,
commandMigrate,
}
var commandGet = &cli.Command{
@ -38,6 +41,16 @@ var commandGet = &cli.Command{
Usage: "Specify `branch` name. This flag implies --single-branch on Git"},
&cli.BoolFlag{Name: "parallel", Aliases: []string{"P"}, Usage: "Import parallelly"},
&cli.BoolFlag{Name: "bare", Usage: "Do a bare clone"},
&cli.StringFlag{
Name: "partial",
Usage: "Do a partial clone. Can specify either \"blobless\" or \"treeless\"",
Action: func(ctx context.Context, cmd *cli.Command, v string) error {
expected := []string{"blobless", "treeless"}
if !slices.Contains(expected, v) {
return fmt.Errorf("flag partial value \"%v\" is not allowed", v)
}
return nil
}},
},
}
@ -66,6 +79,7 @@ var commandRm = &cli.Command{
Action: doRm,
Flags: []cli.Flag{
&cli.BoolFlag{Name: "dry-run", Usage: "Do not remove actually"},
&cli.BoolFlag{Name: "bare", Usage: "Remove a bare repository"},
},
}
@ -94,11 +108,12 @@ type commandDoc struct {
}
var commandDocs = map[string]commandDoc{
"get": {"", "[-u] [-p] [--shallow] [--vcs <vcs>] [--look] [--silent] [--branch <branch>] [--no-recursive] [--bare] <repository URL>|<project>|<user>/<project>|<host>/<user>/<project>"},
"list": {"", "[-p] [-e] [<query>]"},
"create": {"", "<project>|<user>/<project>|<host>/<user>/<project>"},
"rm": {"", "<project>|<user>/<project>|<host>/<user>/<project>"},
"root": {"", "[-all]"},
"get": {"", "[-u] [-p] [--shallow] [--vcs <vcs>] [--look] [--silent] [--branch <branch>] [--no-recursive] [--bare] [--partial blobless|treeless] <repository URL>|<project>|<user>/<project>|<host>/<user>/<project>"},
"list": {"", "[-p] [-e] [<query>]"},
"create": {"", "<project>|<user>/<project>|<host>/<user>/<project>"},
"rm": {"", "<project>|<user>/<project>|<host>/<user>/<project>"},
"root": {"", "[-all]"},
"migrate": {"", "[-y] [--dry-run] <repository-directory>"},
}
// Makes template conditionals to generate per-command documents.
@ -127,3 +142,17 @@ OPTIONS:
{{end}}
{{end}}`
}
var commandMigrate = &cli.Command{
Name: "migrate",
Usage: "Migrate existing repository to ghq-managed directory",
Description: `
Migrate an existing repository directory to the ghq-managed directory structure.
The command detects the VCS backend, retrieves the remote URL, and moves
the repository to the appropriate location under ghq root.`,
Action: doMigrate,
Flags: []cli.Flag{
&cli.BoolFlag{Name: "y", Usage: "Skip confirmation prompt"},
&cli.BoolFlag{Name: "dry-run", Usage: "Show what would happen without moving"},
},
}

View file

@ -14,6 +14,8 @@ type _cloneArgs struct {
branch string
recursive bool
bare bool
silent bool
partial string
}
type _updateArgs struct {
@ -39,6 +41,8 @@ func withFakeGitBackend(t *testing.T, block func(*testing.T, string, *_cloneArgs
branch: vg.branch,
recursive: vg.recursive,
bare: vg.bare,
silent: vg.silent,
partial: vg.partial,
}
return nil
},

View file

@ -1,6 +1,7 @@
package main
import (
"context"
"fmt"
"net/url"
"os"
@ -25,10 +26,10 @@ type getInfo struct {
type getter struct {
update, shallow, silent, ssh, recursive, bare bool
vcs, branch string
vcs, branch, partial string
}
func (g *getter) get(argURL string) (getInfo, error) {
func (g *getter) get(ctx context.Context, argURL string) (getInfo, error) {
u, err := newURL(argURL, g.ssh, false)
if err != nil {
return getInfo{}, fmt.Errorf("could not parse URL %q: %w", argURL, err)
@ -42,13 +43,13 @@ func (g *getter) get(argURL string) (getInfo, error) {
return getInfo{}, err
}
return g.getRemoteRepository(remote, branch)
return g.getRemoteRepository(ctx, remote, branch)
}
// getRemoteRepository clones or updates a remote repository remote.
// If doUpdate is true, updates the locally cloned repository. Otherwise does nothing.
// If isShallow is true, does shallow cloning. (no effect if already cloned or the VCS is Mercurial and git-svn)
func (g *getter) getRemoteRepository(remote RemoteRepository, branch string) (getInfo, error) {
func (g *getter) getRemoteRepository(ctx context.Context, remote RemoteRepository, branch string) (getInfo, error) {
remoteURL := remote.URL()
local, err := LocalRepositoryFromURL(remoteURL, g.bare)
if err != nil {
@ -113,6 +114,7 @@ func (g *getter) getRemoteRepository(remote RemoteRepository, branch string) (ge
branch: branch,
recursive: g.recursive,
bare: g.bare,
partial: g.partial,
})
}
return info, nil
@ -148,8 +150,8 @@ func detectLocalRepoRoot(remotePath, repoPath string) string {
pathParts = pathParts[1:]
for i := 0; i < len(pathParts); i++ {
subPath := "/" + path.Join(pathParts[i:]...)
if subIdx := strings.Index(remotePath, subPath); subIdx >= 0 {
return remotePath[0:subIdx] + subPath
if before, _, ok := strings.Cut(remotePath, subPath); ok {
return before + subPath
}
}
return ""

31
go.mod
View file

@ -1,28 +1,29 @@
module github.com/x-motemen/ghq
go 1.23.3
go 1.26.0
require (
github.com/Songmu/gitconfig v0.2.0
github.com/mattn/go-isatty v0.0.20
github.com/Songmu/gitconfig v0.2.2
github.com/mattn/go-isatty v0.0.22
github.com/motemen/go-colorine v0.0.0-20180816141035-45d19169413a
github.com/otiai10/copy v1.14.1
github.com/saracen/walker v0.1.4
github.com/urfave/cli/v2 v2.27.5
golang.org/x/net v0.31.0
golang.org/x/sync v0.9.0
github.com/urfave/cli/v3 v3.10.1
golang.org/x/net v0.57.0
golang.org/x/sync v0.22.0
)
require (
github.com/cli/go-gh/v2 v2.13.0 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/otiai10/mint v1.6.3 // indirect
)
require (
github.com/cli/go-gh v1.2.1 // indirect
github.com/cli/safeexec v1.0.1 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
github.com/daviddengcn/go-colortext v1.0.0 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/goccy/go-yaml v1.13.6 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
golang.org/x/sys v0.27.0 // indirect
golang.org/x/text v0.20.0
github.com/goccy/go-yaml v1.19.2 // indirect
golang.org/x/sys v0.47.0 // indirect
golang.org/x/text v0.40.0
gopkg.in/yaml.v3 v3.0.1 // indirect
)

128
go.sum
View file

@ -1,125 +1,51 @@
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
github.com/Songmu/gitconfig v0.2.0 h1:pX2++u4KUq+K2k/ZCzGXLtkD3ceCqIdi0tDyb+IbSyo=
github.com/Songmu/gitconfig v0.2.0/go.mod h1:cB5bYJer+pl7W8g6RHFwL/0X6aJROVrYuHlvc7PT+hE=
github.com/cli/browser v1.1.0/go.mod h1:HKMQAt9t12kov91Mn7RfZxyJQQgWgyS/3SZswlZ5iTI=
github.com/cli/go-gh v0.1.0/go.mod h1:eTGWl99EMZ+3Iau5C6dHyGAJRRia65MtdBtuhWc+84o=
github.com/cli/go-gh v1.2.1 h1:xFrjejSsgPiwXFP6VYynKWwxLQcNJy3Twbu82ZDlR/o=
github.com/cli/go-gh v1.2.1/go.mod h1:Jxk8X+TCO4Ui/GarwY9tByWm/8zp4jJktzVZNlTW5VM=
github.com/cli/safeexec v1.0.0/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q=
github.com/Songmu/gitconfig v0.2.2 h1:tiQY2KZqgdgjs1C7slWd31EB8NbItN7BOKxCIHSlK4Y=
github.com/Songmu/gitconfig v0.2.2/go.mod h1:lZ0lKL6K+xvT+QzmmGxHQgOnneiWcbhGAV2DKI6PBpM=
github.com/cli/go-gh/v2 v2.13.0 h1:jEHZu/VPVoIJkciK3pzZd3rbT8J90swsK5Ui4ewH1ys=
github.com/cli/go-gh/v2 v2.13.0/go.mod h1:Us/NbQ8VNM0fdaILgoXSz6PKkV5PWaEzkJdc9vR2geM=
github.com/cli/safeexec v1.0.1 h1:e/C79PbXF4yYTN/wauC4tviMxEV13BwljGj0N9j+N00=
github.com/cli/safeexec v1.0.1/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q=
github.com/cli/shurcooL-graphql v0.0.1/go.mod h1:U7gCSuMZP/Qy7kbqkk5PrqXEeDgtfG5K+W+u8weorps=
github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
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/daviddengcn/go-colortext v1.0.0 h1:ANqDyC0ys6qCSvuEK7l3g5RaehL/Xck9EX8ATG8oKsE=
github.com/daviddengcn/go-colortext v1.0.0/go.mod h1:zDqEI5NVUop5QPpVJUxE9UO10hRnmkD5G4Pmri9+m4c=
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA=
github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/goccy/go-yaml v1.9.5/go.mod h1:U/jl18uSupI5rdI2jmuCswEA2htH9eXfferR3KfscvA=
github.com/goccy/go-yaml v1.13.6 h1:pa3JkBPBseTtfqpG9DiSFhyxNPSpJ0BFa39BlMZE16E=
github.com/goccy/go-yaml v1.13.6/go.mod h1:IjYwxUiJDoqpx2RmbdjMUceGHZwYLon3sfOGl5Hi9lc=
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/golangplus/bytes v0.0.0-20160111154220-45c989fe5450/go.mod h1:Bk6SMAONeMXrxql8uvOKuAZSu8aM5RUGv+1C6IJaEho=
github.com/golangplus/bytes v1.0.0/go.mod h1:AdRaCFwmc/00ZzELMWb01soso6W1R/++O1XL80yAn+A=
github.com/golangplus/fmt v1.0.0/go.mod h1:zpM0OfbMCjPtd2qkTD/jX2MgiFCqklhSUFyDW44gVQE=
github.com/golangplus/testing v1.0.0 h1:+ZeeiKZENNOMkTTELoSySazi+XaEhVO0mb+eanrSEUQ=
github.com/golangplus/testing v1.0.0/go.mod h1:ZDreixUV3YzhoVraIDyOzHrr76p6NUh6k/pPg/Q3gYA=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
github.com/henvic/httpretty v0.0.6/go.mod h1:X38wLjWXHkXT7r2+uK8LjCMne9rsuNaBLJ+5cU2/Pmo=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
github.com/motemen/go-colorine v0.0.0-20180816141035-45d19169413a h1:CONqI/36EjYzkAzrMD0UWuL/lRDr7UdoID4fDGke+Yc=
github.com/motemen/go-colorine v0.0.0-20180816141035-45d19169413a/go.mod h1:PU2urRC7j30rrabSyp1MGGhyoiWSninPD8ckjzBSgkU=
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
github.com/otiai10/copy v1.14.1 h1:5/7E6qsUMBaH5AnQ0sSLzzTg1oTECmcCmT6lvF45Na8=
github.com/otiai10/copy v1.14.1/go.mod h1:oQwrEDDOci3IM8dJF0d8+jnbfPDllW6vUjNc3DoZm9I=
github.com/otiai10/mint v1.6.3 h1:87qsV/aw1F5as1eH1zS/yqHY85ANKVMgkDrf9rcxbQs=
github.com/otiai10/mint v1.6.3/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM=
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.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
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/saracen/walker v0.1.4 h1:/WCOt98GRkQ0KgL6hXJFBpoH21XY6iCD2N6LQWBFiaU=
github.com/saracen/walker v0.1.4/go.mod h1:2F+hfOidTHfXP2AmlKOqpO+yewf8fIvNUDBNJogpJbk=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e/go.mod h1:/Tnicc6m/lsJE0irFMA0LfIwTBo4QP7A8IfyIv4zZKI=
github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w=
github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
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-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210319071255-635bc2c9138d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220818161305-2296e01440c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
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/v3 v3.10.1 h1:7Kx9H50hrHbRbyxgO1KP6/BcbiGRz0uYh5YyQ30JEEY=
github.com/urfave/cli/v3 v3.10.1/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/h2non/gock.v1 v1.1.2/go.mod h1:n7UGz/ckNChHiK05rDoiC4MYSunEC/lyaUm2WWaDva0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
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=

View file

@ -78,7 +78,7 @@ func captureWithInput(in []string, block func()) (string, string, error) {
os.Stdin, stdin = rIn, os.Stdin
defer func() { os.Stdin = stdin }()
for _, line := range in {
fmt.Fprintln(wIn, line)
}

View file

@ -6,6 +6,7 @@ import (
"net/url"
"os"
"path/filepath"
"slices"
"strings"
"sync"
@ -166,7 +167,7 @@ func (repo *LocalRepository) repoRootCandidates() []string {
hostRoot := filepath.Join(repo.RootPath, repo.PathParts[0])
nonHostParts := repo.PathParts[1:]
candidates := make([]string, len(nonHostParts))
for i := 0; i < len(nonHostParts); i++ {
for i := range nonHostParts {
candidates[i] = filepath.Join(append(
[]string{hostRoot}, nonHostParts[0:len(nonHostParts)-i]...)...)
}
@ -184,13 +185,7 @@ func (repo *LocalRepository) IsUnderPrimaryRoot() bool {
// Matches checks if any subpath of the local repository equals the query.
func (repo *LocalRepository) Matches(pathQuery string) bool {
for _, p := range repo.Subpaths() {
if p == pathQuery {
return true
}
}
return false
return slices.Contains(repo.Subpaths(), pathQuery)
}
// VCS returns VCSBackend of the repository

View file

@ -8,29 +8,55 @@ import (
"github.com/motemen/go-colorine"
)
var logger = colorine.NewLogger(
colorine.Prefixes{
"git": colorine.Verbose,
"hg": colorine.Verbose,
"svn": colorine.Verbose,
"darcs": colorine.Verbose,
"pijul": colorine.Verbose,
"bzr": colorine.Verbose,
"fossil": colorine.Verbose,
"skip": colorine.Verbose,
"cd": colorine.Verbose,
"resolved": colorine.Verbose,
var (
NoColor = colorine.TextStyle{Foreground: colorine.None, Background: colorine.None}
VerboseColor = colorine.Verbose // white
InfoColor = colorine.Info // green
NoticeColor = colorine.Notice // blue
WarnColor = colorine.Warn // yellow
ErrorColor = colorine.Error // red
)
"open": colorine.Warn,
"exists": colorine.Warn,
"warning": colorine.Warn,
var (
logger = colorine.NewLogger( // default logger with color
colorine.Prefixes{
// verbose
"git": VerboseColor,
"hg": VerboseColor,
"svn": VerboseColor,
"darcs": VerboseColor,
"pijul": VerboseColor,
"bzr": VerboseColor,
"fossil": VerboseColor,
"skip": VerboseColor,
"cd": VerboseColor,
"resolved": VerboseColor,
// notice
"authorized": NoticeColor,
// warn
"open": WarnColor,
"exists": WarnColor,
"warning": WarnColor,
// error
"error": ErrorColor,
},
InfoColor, // default is info
)
"authorized": colorine.Notice,
"error": colorine.Error,
}, colorine.Info)
loggerWithoutColor = colorine.NewLogger(
colorine.Prefixes{},
NoColor,
)
)
func init() {
selectLogger()
}
func selectLogger() {
if os.Getenv("NO_COLOR") != "" {
logger = loggerWithoutColor
}
SetOutput(os.Stderr)
}
@ -39,12 +65,12 @@ func SetOutput(w io.Writer) {
logger.SetOutput(w)
}
// Log output
// Log outputs log
func Log(prefix, message string) {
logger.Log(prefix, message)
}
// Logf output log with format
func Logf(prefix, msg string, args ...interface{}) {
// Logf outputs log with format
func Logf(prefix, msg string, args ...any) {
Log(prefix, fmt.Sprintf(msg, args...))
}

View file

@ -1,12 +1,41 @@
package logger
import "testing"
import (
"os"
"testing"
)
func TestLog(t *testing.T) {
Log("default", "shows this color")
Log("error", "shows this color")
Log("open", "shows this color")
Log("authorized", "shows this color")
Log("skip", "shows this color")
Log("git", "shows this color")
t.Run("with color", func(t *testing.T) {
t.Logf("NO_COLOR: %s", os.Getenv("NO_COLOR"))
selectLogger()
// info
Log("default", "should be green")
// verbose
Log("git", "should be white")
Log("skip", "should be white")
// notice
Log("authorized", "should be blue")
// warn
Log("open", "should be yellow")
// error
Log("error", "should be red")
})
t.Run("without color", func(t *testing.T) {
t.Setenv("NO_COLOR", "true")
t.Logf("NO_COLOR: %s", os.Getenv("NO_COLOR"))
selectLogger()
// info
Log("default", "should be none")
// verbose
Log("git", "should be none")
Log("skip", "should be none")
// notice
Log("authorized", "should be none")
// warn
Log("open", "should be none")
// error
Log("error", "should be none")
})
}

30
main.go
View file

@ -1,19 +1,20 @@
package main
import (
"context"
"fmt"
"os"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v3"
"github.com/x-motemen/ghq/logger"
)
const version = "1.7.1"
const version = "1.10.1"
var revision = "HEAD"
func main() {
if err := newApp().Run(os.Args); err != nil {
if err := newApp().Run(context.Background(), os.Args); err != nil {
exitCode := 1
if excoder, ok := err.(cli.ExitCoder); ok {
exitCode = excoder.ExitCode()
@ -23,18 +24,13 @@ func main() {
}
}
func newApp() *cli.App {
app := cli.NewApp()
app.Name = "ghq"
app.Usage = "Manage remote repository clones"
app.Version = fmt.Sprintf("%s (rev:%s)", version, revision)
app.Authors = []*cli.Author{{
Name: "motemen",
Email: "motemen@gmail.com",
}, {
Name: "Songmu",
Email: "y.songmu@gmail.com",
}}
app.Commands = commands
return app
func newApp() *cli.Command {
return &cli.Command{
Name: "ghq",
Usage: "Manage remote repository clones",
Version: fmt.Sprintf("%s (rev:%s)", version, revision),
Authors: []any{"motemen <motemen@gmail.com>", "Songmu <y.songmu@gmail.com>"},
Suggest: true,
Commands: commands,
}
}

View file

@ -24,6 +24,8 @@ export GHQ_ROOT=$tmpdir
ghq get https://svn.apache.org/repos/asf/subversion
ghq get --shallow hub.darcs.net/byorgey/split
ghq get --bare x-motemen/gore
ghq get --partial blobless x-motemen/blogsync
ghq get --partial treeless x-motemen/gobump
test -d $tmpdir/github.com/x-motemen/ghq/.git
test -d $tmpdir/www.mercurial-scm.org/repo/hello/.hg
@ -34,11 +36,15 @@ export GHQ_ROOT=$tmpdir
test -d $tmpdir/svn.apache.org/repos/asf/subversion/.svn
test -d $tmpdir/hub.darcs.net/byorgey/split/_darcs
test -d $tmpdir/github.com/x-motemen/gore.git/refs
grep --quiet "partialclonefilter = blob:none" $tmpdir/github.com/x-motemen/blogsync/.git/config
grep --quiet "partialclonefilter = tree:0" $tmpdir/github.com/x-motemen/gobump/.git/config
: testing 'ghq list'
cat <<EOF | sort > $tmpdir/expect
chiselapp.com/user/sti/repository/fossil-gui
github.com/x-motemen/blogsync
github.com/x-motemen/ghq
github.com/x-motemen/gobump
github.com/x-motemen/gore.git
www.mercurial-scm.org/repo/hello
launchpad.net/shutter

View file

@ -1,21 +1,84 @@
function _ghq () {
_ghq() {
local cur prev words cword
_init_completion || return
case $cword in
1)
COMPREPLY=( $(compgen -W "get list rm" -- $cur) );;
2)
case $prev in
get)
COMPREPLY=( $(compgen -W "$(ghq list --unique)" -- $cur) );;
local subcommands="get clone list root rm create migrate help"
local global_opts="--help -h"
if [[ $cword = 1 ]]; then
COMPREPLY=( $(compgen -W "$subcommands $global_opts --version -v" -- "$cur") )
return 0
fi
local vcs_backends="git github codecommit svn subversion git-svn hg mercurial darcs pijul fossil bzr bazaar"
case "${words[1]}" in
get|clone)
local opts="--update -u -p --shallow --look -l --vcs --silent -s --no-recursive --branch -b --parallel -P --bare --partial"
if [[ $cur = -* ]]; then
COMPREPLY=( $(compgen -W "$opts $global_opts" -- "$cur") )
return 0
fi
case "$prev" in
--branch|-b)
# expects branch name
;;
--partial)
COMPREPLY=( $(compgen -W "blobless treeless" -- "$cur") );;
--vcs)
COMPREPLY=( $(compgen -W "$vcs_backends" -- "$cur") );;
*)
local arg
for arg in "${words[@]}"; do
case "$arg" in
--update|-u)
COMPREPLY=( $(compgen -W "$(ghq list)" -- "$cur") )
break;;
esac
done;;
esac;;
list)
COMPREPLY=( $(compgen -W "$(ghq list)" -- $cur) );;
local opts="--exact -e --vcs --full-path -p --unique --bare"
if [[ $cur = -* ]]; then
COMPREPLY=( $(compgen -W "$opts $global_opts" -- "$cur") )
return 0
fi
case "$prev" in
--vcs)
COMPREPLY=( $(compgen -W "$vcs_backends" -- "$cur") );;
esac;;
root)
local opts="--all"
if [[ $cur = -* ]]; then
COMPREPLY=( $(compgen -W "$opts $global_opts" -- "$cur") )
return 0
fi;;
rm)
COMPREPLY=( $(compgen -W "$(ghq list)" -- $cur) );;
esac;;
*)
COMPREPLY=( $(compgen -W "$(ls)" -- $cur) );;
local opts="--dry-run --bare"
if [[ $cur = -* ]]; then
COMPREPLY=( $(compgen -W "$opts $global_opts" -- "$cur") )
return 0
fi
COMPREPLY=( $(compgen -W "$(ghq list)" -- "$cur") );;
create)
local opts="--vcs --bare"
if [[ $cur = -* ]]; then
COMPREPLY=( $(compgen -W "$opts $global_opts" -- "$cur") )
return 0
fi
case "$prev" in
--vcs)
COMPREPLY=( $(compgen -W "$vcs_backends" -- "$cur") );;
esac;;
migrate)
local opts="-y --dry-run"
if [[ $cur = -* ]]; then
COMPREPLY=( $(compgen -W "$opts $global_opts" -- "$cur") )
return 0
fi
_filedir -d;;
help)
COMPREPLY=( $(compgen -W "$subcommands $global_opts" -- "$cur") );;
esac
}

View file

@ -1,6 +1,6 @@
function __fish_ghq_needs_subcommand
set -l cmd (commandline -opc)
for subcmd in get list rm root create h help
for subcmd in get clone list rm root create migrate h help
if contains -- $subcmd $cmd
return 1
end
@ -18,42 +18,57 @@ complete -c ghq -s h -l help -d 'Show help'
complete -c ghq -n __fish_ghq_needs_subcommand -s v -l version -d 'Print the version'
# Global subcommands
complete -c ghq -n __fish_ghq_needs_subcommand -a get -d 'Clone/sync with a remote repository'
complete -c ghq -n __fish_ghq_needs_subcommand -a 'get clone' -d 'Clone/sync with a remote repository'
complete -c ghq -n __fish_ghq_needs_subcommand -a list -d 'List local repositories'
complete -c ghq -n __fish_ghq_needs_subcommand -a rm -d 'Remove local repository'
complete -c ghq -n __fish_ghq_needs_subcommand -a root -d 'Show repositories\' root'
complete -c ghq -n __fish_ghq_needs_subcommand -a create -d 'Create a new repository'
complete -c ghq -n __fish_ghq_needs_subcommand -a migrate -d 'Migrate existing repository to ghq-managed directory'
complete -c ghq -n __fish_ghq_needs_subcommand -a 'h help' -d 'Shows a list of commands or help for one command'
# Arguments for subcommands
complete -c ghq -n '__fish_seen_subcommand_from get' -s u -l update -d 'Update local repository if cloned already'
complete -c ghq -n '__fish_seen_subcommand_from get' -s p -d 'Clone with SSH'
complete -c ghq -n '__fish_seen_subcommand_from get' -l shallow -d 'Do a shallow clone'
complete -c ghq -n '__fish_seen_subcommand_from get' -s l -l look -d 'Look after get'
complete -c ghq -n '__fish_seen_subcommand_from get' -l vcs -d 'Specify vcs backend for cloning'
complete -c ghq -n '__fish_seen_subcommand_from get' -s s -l silent -d 'Clone or update silently'
complete -c ghq -n '__fish_seen_subcommand_from get' -l no-recursive -d 'Prevent recursive fetching'
complete -c ghq -n '__fish_seen_subcommand_from get' -s b -l branch -d 'Specify branch name. This flag implies --single-branch on Git'
complete -c ghq -n '__fish_seen_subcommand_from get' -s P -l parallel -d 'Import parallelly'
complete -c ghq -n '__fish_seen_subcommand_from get' -l bare -d 'Do a bare clone'
complete -c ghq -n '__fish_seen_subcommand_from get clone' -s u -l update -d 'Update local repository if cloned already'
complete -c ghq -n '__fish_seen_subcommand_from get clone' -s p -d 'Clone with SSH'
complete -c ghq -n '__fish_seen_subcommand_from get clone' -l shallow -d 'Do a shallow clone'
complete -c ghq -n '__fish_seen_subcommand_from get clone' -s l -l look -d 'Look after get'
complete -c ghq -n '__fish_seen_subcommand_from get clone' -l vcs -d 'Specify vcs backend for cloning'
complete -c ghq -n '__fish_seen_subcommand_from get clone' -s s -l silent -d 'Clone or update silently'
complete -c ghq -n '__fish_seen_subcommand_from get clone' -l no-recursive -d 'Prevent recursive fetching'
complete -c ghq -n '__fish_seen_subcommand_from get clone' -s b -l branch -d 'Specify branch name. This flag implies --single-branch on Git'
complete -c ghq -n '__fish_seen_subcommand_from get clone' -s P -l parallel -d 'Import parallelly'
complete -c ghq -n '__fish_seen_subcommand_from get clone' -l bare -d 'Do a bare clone'
function __complete_get_partial
printf '%s\t%s\n' 'blobless' 'Do a blobless clone'
printf '%s\t%s\n' 'treeless' 'Do a treeless clone'
end
complete -c ghq -n '__fish_seen_subcommand_from get clone' -l partial -d 'Do a partial clone' -xa '(__complete_get_partial)'
# When updating an existing repository (-u/--update), complete with local repositories
complete -c ghq -n '__fish_seen_subcommand_from get clone' -n '__fish_seen_argument -s u -l update' -xa '(ghq list)'
complete -c ghq -n '__fish_seen_subcommand_from list' -s e -l exact -d 'Perform an exact match'
complete -c ghq -n '__fish_seen_subcommand_from list' -l vcs -d 'Specify vcs backend for matching'
complete -c ghq -n '__fish_seen_subcommand_from list' -s p -l full-path -d 'Print full paths'
complete -c ghq -n '__fish_seen_subcommand_from list' -l unique -d 'Print unique subpaths'
complete -c ghq -n '__fish_seen_subcommand_from list' -l bare -d 'Query bare repositories'
complete -c ghq -n '__fish_seen_subcommand_from rm' -l dry-run -d 'Do not remove actually'
complete -c ghq -n '__fish_seen_subcommand_from rm' -l bare -d 'Remove a bare repository'
complete -c ghq -n '__fish_seen_subcommand_from rm' -xa '(ghq list)'
complete -c ghq -n '__fish_seen_subcommand_from root' -l all -d 'Show all roots'
complete -c ghq -n '__fish_seen_subcommand_from create' -l vcs -d 'Specify vcs backend explicitly'
complete -c ghq -n '__fish_seen_subcommand_from create' -l bare -d 'Create a bare repository'
complete -c ghq -n '__fish_seen_subcommand_from migrate' -s y -d 'Skip confirmation prompt'
complete -c ghq -n '__fish_seen_subcommand_from migrate' -l dry-run -d 'Show what would happen without moving'
# Complete VCS backend options for supported subcommands
complete -c ghq -n '__fish_seen_subcommand_from get list create' -n '__fish_seen_argument --vcs' -l vcs -x -a 'git github codecommit' -d git
complete -c ghq -n '__fish_seen_subcommand_from get list create' -n '__fish_seen_argument --vcs' -l vcs -x -a 'svn subversion' -d subversion
complete -c ghq -n '__fish_seen_subcommand_from get list create' -n '__fish_seen_argument --vcs' -l vcs -x -a git-svn -d git-svn
complete -c ghq -n '__fish_seen_subcommand_from get list create' -n '__fish_seen_argument --vcs' -l vcs -x -a 'hg mercurial' -d mercurial
complete -c ghq -n '__fish_seen_subcommand_from get list create' -n '__fish_seen_argument --vcs' -l vcs -x -a darcs -d darcs
complete -c ghq -n '__fish_seen_subcommand_from get list create' -n '__fish_seen_argument --vcs' -l vcs -x -a pijul -d pijul
complete -c ghq -n '__fish_seen_subcommand_from get list create' -n '__fish_seen_argument --vcs' -l vcs -x -a fossil -d fossil
complete -c ghq -n '__fish_seen_subcommand_from get list create' -n '__fish_seen_argument --vcs' -l vcs -x -a 'bzr bazaar' -d bazaar
complete -c ghq -n '__fish_seen_subcommand_from get clone list create' -n '__fish_seen_argument --vcs' -l vcs -x -a 'git github codecommit' -d git
complete -c ghq -n '__fish_seen_subcommand_from get clone list create' -n '__fish_seen_argument --vcs' -l vcs -x -a 'svn subversion' -d subversion
complete -c ghq -n '__fish_seen_subcommand_from get clone list create' -n '__fish_seen_argument --vcs' -l vcs -x -a git-svn -d git-svn
complete -c ghq -n '__fish_seen_subcommand_from get clone list create' -n '__fish_seen_argument --vcs' -l vcs -x -a 'hg mercurial' -d mercurial
complete -c ghq -n '__fish_seen_subcommand_from get clone list create' -n '__fish_seen_argument --vcs' -l vcs -x -a darcs -d darcs
complete -c ghq -n '__fish_seen_subcommand_from get clone list create' -n '__fish_seen_argument --vcs' -l vcs -x -a pijul -d pijul
complete -c ghq -n '__fish_seen_subcommand_from get clone list create' -n '__fish_seen_argument --vcs' -l vcs -x -a fossil -d fossil
complete -c ghq -n '__fish_seen_subcommand_from get clone list create' -n '__fish_seen_argument --vcs' -l vcs -x -a 'bzr bazaar' -d bazaar

View file

@ -1,4 +1,4 @@
#compdef ghq
#compdef ghq ghq-dev
function _ghq () {
local context curcontext=$curcontext state line
@ -15,27 +15,32 @@ function _ghq () {
case $state in
(args)
case $words[1] in
(get)
(get|clone)
_arguments -C \
'(-u --update)'{-u,--update}'[Update local repository if cloned already]' \
'-p[Clone with SSH]' \
'--shallow[Do a shallow clone]' \
'(-l --look)'{-l,--look}'[Look after get]' \
'--vcs[Specify vcs backend for cloning]' \
'--vcs[Specify vcs backend for cloning]: :(git github codecommit svn subversion git-svn hg mercurial darcs pijul fossil bzr bazaar)' \
'(-s --silent)'{-s,--silent}'[Clone or update silently]' \
'--no-recursive[Prevent recursive fetching]' \
'--bare[Do a bare clone]' \
'(-b --branch)'{-b,--branch}'[Specify branch name]' \
'(-P --parallel)'{-P,--parallel}'[Import parallelly]' \
'--partial[Do a partial clone]: :(blobless treeless)' \
'(-)*:: :->null_state' \
&& ret=0
if (( ${words[(I)-u]} )) || (( ${words[(I)--update]} )); then
__ghq_all_repositories && ret=0
fi
;;
(list)
_arguments -C \
'(-e --exact)'{-e,--exact}'[Perform an exact match]' \
'--vcs[Specify vcs backend for matching]' \
'--vcs[Specify vcs backend for matching]: :(git github codecommit svn subversion git-svn hg mercurial darcs pijul fossil bzr bazaar)' \
'(-p --full-path)'{-p,--full-path}'[Print full paths]' \
'--unique[Print unique subpaths]' \
'--bare[Query bare repositories]' \
'(-)*:: :->null_state' \
&& ret=0
;;
@ -47,14 +52,23 @@ function _ghq () {
;;
(create)
_arguments -C \
'--vcs[Specify vcs backend explicitly]' \
'--vcs[Specify vcs backend explicitly]: :(git github codecommit svn subversion git-svn hg mercurial darcs pijul fossil bzr bazaar)' \
'--bare[Create a bare repository]' \
'(-)*:: :->null_state' \
&& ret=0
;;
(rm)
_arguments -C \
'--dry-run[Do not remove actually]' \
'(-)*:: :->null_state' \
'--bare[Remove a bare repository]' \
'(-)*: :__ghq_all_repositories' \
&& ret=0
;;
(migrate)
_arguments -C \
'-y[Skip confirmation prompt]' \
'--dry-run[Show what would happen without moving]' \
':repository directory:_directories' \
&& ret=0
;;
(help|h)
@ -73,14 +87,24 @@ __ghq_repositories () {
_describe -t repositories Repositories _repos
}
__ghq_all_repositories () {
local -a _repos
_repos=( ${(@f)"$(_call_program repositories ghq list)"} )
_describe -t repositories Repositories _repos
}
__ghq_commands () {
local -a _c
_c=(
'get:Clone/sync with a remote repository'
'clone:Clone/sync with a remote repository'
'list:List local repositories'
'rm:Remove local repository'
'create:Create a new repository'
'migrate:Migrate existing repository to ghq-managed directory'
"root:Show repositories' root"
'help:Show a list of commands or help for one command'
'h:Show a list of commands or help for one command'
)
_describe -t commands Commands _c

7
url.go
View file

@ -139,7 +139,12 @@ func newURL(ref string, ssh, forceMe bool) (*url.URL, error) {
}
}
u.Scheme = "https"
u.Host = "github.com"
host, err := gitconfig.Get("ghq.defaultHost")
if (err != nil && !gitconfig.IsNotFound(err)) || host != "" {
u.Host = host
} else {
u.Host = "github.com"
}
if u.Path[0] != '/' {
u.Path = "/" + u.Path
}

View file

@ -66,6 +66,15 @@ completeUser = false`))
url: "peco",
expect: "https://github.com/peco/peco",
host: "github.com",
}, {
name: "configured default host",
setup: func(t *testing.T) {
t.Cleanup(gitconfig.WithConfig(t, `[ghq]
defaultHost = gitlab.com`))
},
url: "gnuwget/wget2",
expect: "https://gitlab.com/gnuwget/wget2",
host: "gitlab.com",
}}
for _, tc := range testCases {

155
vcs.go
View file

@ -38,13 +38,60 @@ type VCSBackend struct {
Init func(dir string) error
// Returns VCS specific files
Contents []string
// Returns the remote URL of the repository at the given directory.
// If nil, the VCS backend does not support retrieving remote URLs.
RemoteURL func(dir string) (string, error)
}
type vcsGetOption struct {
url *url.URL
dir string
recursive, shallow, silent, bare bool
branch string
branch, partial string
}
// getGitRemoteURL retrieves the remote URL from a git repository.
// It tries 'origin' first, then falls back to the first remote.
func getGitRemoteURL(dir string) (string, error) {
// Try 'origin' first
originCmd := exec.Command("git", "remote", "get-url", "origin")
originCmd.Dir = dir
originOut, originErr := originCmd.Output()
if originErr == nil {
originURL := strings.TrimSpace(string(originOut))
if originURL != "" {
return originURL, nil
}
}
// List all remotes
listCmd := exec.Command("git", "remote")
listCmd.Dir = dir
listOut, listErr := listCmd.Output()
if listErr != nil {
return "", fmt.Errorf("failed to list remotes: %w", listErr)
}
allRemotes := strings.Split(strings.TrimSpace(string(listOut)), "\n")
if len(allRemotes) == 0 || allRemotes[0] == "" {
return "", fmt.Errorf("no remotes found")
}
// Get first remote URL
first := allRemotes[0]
urlCmd := exec.Command("git", "remote", "get-url", first)
urlCmd.Dir = dir
urlOut, urlErr := urlCmd.Output()
if urlErr != nil {
return "", fmt.Errorf("failed to get URL of remote %q: %w", first, urlErr)
}
finalURL := strings.TrimSpace(string(urlOut))
if finalURL == "" {
return "", fmt.Errorf("remote %q has no URL", first)
}
return finalURL, nil
}
// GitBackend is the VCSBackend of git
@ -70,6 +117,11 @@ var GitBackend = &VCSBackend{
if vg.bare {
args = append(args, "--bare")
}
if vg.partial == "blobless" {
args = append(args, "--filter=blob:none")
} else if vg.partial == "treeless" {
args = append(args, "--filter=tree:0")
}
args = append(args, vg.url.String(), vg.dir)
return run(vg.silent)("git", args...)
@ -106,6 +158,9 @@ var GitBackend = &VCSBackend{
return cmdutil.RunInDir(dir, "git", args...)
},
Contents: []string{".git"},
RemoteURL: func(dir string) (string, error) {
return getGitRemoteURL(dir)
},
}
/*
@ -138,8 +193,8 @@ func replaceOnce(reg *regexp.Regexp, str, replace string) string {
}
func svnBase(p string) string {
if strings.HasSuffix(p, trunk) {
return strings.TrimSuffix(p, trunk)
if before, ok := strings.CutSuffix(p, trunk); ok {
return before
}
return replaceOnce(svnReg, p, "")
}
@ -179,6 +234,19 @@ var SubversionBackend = &VCSBackend{
return runInDir(vg.silent)(vg.dir, "svn", "update")
},
Contents: []string{".svn"},
RemoteURL: func(dir string) (string, error) {
cmd := exec.Command("svn", "info", "--show-item", "repos-root-url")
cmd.Dir = dir
output, err := cmd.Output()
if err != nil {
return "", fmt.Errorf("failed to get repository root URL: %w", err)
}
url := strings.TrimSpace(string(output))
if url == "" {
return "", fmt.Errorf("repository root URL is empty")
}
return url, nil
},
}
var svnLastRevReg = regexp.MustCompile(`(?m)^Last Changed Rev: (\d+)$`)
@ -246,6 +314,10 @@ var GitsvnBackend = &VCSBackend{
return runInDir(vg.silent)(vg.dir, "git", "svn", "rebase")
},
Contents: []string{".git/svn"},
RemoteURL: func(dir string) (string, error) {
// git-svn repos are git repos, use git remote logic
return getGitRemoteURL(dir)
},
}
// MercurialBackend is the VCSBackend for mercurial
@ -272,6 +344,19 @@ var MercurialBackend = &VCSBackend{
return cmdutil.RunInDir(dir, "hg", "init")
},
Contents: []string{".hg"},
RemoteURL: func(dir string) (string, error) {
cmd := exec.Command("hg", "paths", "default")
cmd.Dir = dir
output, err := cmd.Output()
if err != nil {
return "", fmt.Errorf("failed to get default path: %w", err)
}
url := strings.TrimSpace(string(output))
if url == "" {
return "", fmt.Errorf("default path is empty")
}
return url, nil
},
}
// DarcsBackend is the VCSBackend for darcs
@ -302,6 +387,27 @@ var DarcsBackend = &VCSBackend{
return cmdutil.RunInDir(dir, "darcs", "init")
},
Contents: []string{"_darcs"},
RemoteURL: func(dir string) (string, error) {
cmd := exec.Command("darcs", "show", "repo")
cmd.Dir = dir
output, err := cmd.Output()
if err != nil {
return "", fmt.Errorf("failed to show repo: %w", err)
}
lines := strings.SplitSeq(string(output), "\n")
for line := range lines {
if strings.HasPrefix(strings.TrimSpace(line), "Default Remote:") {
parts := strings.SplitN(line, ":", 2)
if len(parts) == 2 {
url := strings.TrimSpace(parts[1])
if url != "" {
return url, nil
}
}
}
}
return "", fmt.Errorf("no default remote found")
},
}
// PijulBackend is the VCSBackend for pijul
@ -328,6 +434,23 @@ var PijulBackend = &VCSBackend{
return cmdutil.RunInDir(dir, "pijul", "init")
},
Contents: []string{".pijul"},
RemoteURL: func(dir string) (string, error) {
cmd := exec.Command("pijul", "remote")
cmd.Dir = dir
output, err := cmd.Output()
if err != nil {
return "", fmt.Errorf("failed to list remotes: %w", err)
}
lines := strings.SplitSeq(strings.TrimSpace(string(output)), "\n")
for line := range lines {
trimmed := strings.TrimSpace(line)
if trimmed != "" {
// First non-empty line is the first remote
return trimmed, nil
}
}
return "", fmt.Errorf("no remotes found")
},
}
var cvsDummyBackend = &VCSBackend{
@ -367,6 +490,19 @@ var FossilBackend = &VCSBackend{
return cmdutil.RunInDir(dir, "fossil", "open", fossilRepoName)
},
Contents: []string{".fslckout", "_FOSSIL_"},
RemoteURL: func(dir string) (string, error) {
cmd := exec.Command("fossil", "remote-url")
cmd.Dir = dir
output, err := cmd.Output()
if err != nil {
return "", fmt.Errorf("failed to get remote URL: %w", err)
}
url := strings.TrimSpace(string(output))
if url == "" || url == "off" {
return "", fmt.Errorf("no remote URL configured")
}
return url, nil
},
}
// BazaarBackend is the VCSBackend for bazaar
@ -391,6 +527,19 @@ var BazaarBackend = &VCSBackend{
return cmdutil.RunInDir(dir, "bzr", "init")
},
Contents: []string{".bzr"},
RemoteURL: func(dir string) (string, error) {
cmd := exec.Command("bzr", "config", "parent_location")
cmd.Dir = dir
output, err := cmd.Output()
if err != nil {
return "", fmt.Errorf("failed to get parent location: %w", err)
}
url := strings.TrimSpace(string(output))
if url == "" {
return "", fmt.Errorf("parent location is empty")
}
return url, nil
},
}
var vcsRegistry = map[string]*VCSBackend{

View file

@ -136,6 +136,26 @@ func TestVCSBackend(t *testing.T) {
})
},
expect: []string{"git", "clone", "--bare", remoteDummyURL.String(), localDir},
}, {
name: "[git] (partial) blobless clone",
f: func() error {
return GitBackend.Clone(&vcsGetOption{
url: remoteDummyURL,
dir: localDir,
partial: "blobless",
})
},
expect: []string{"git", "clone", "--filter=blob:none", remoteDummyURL.String(), localDir},
}, {
name: "[git] (partial) treeless clone",
f: func() error {
return GitBackend.Clone(&vcsGetOption{
url: remoteDummyURL,
dir: localDir,
partial: "treeless",
})
},
expect: []string{"git", "clone", "--filter=tree:0", remoteDummyURL.String(), localDir},
}, {
name: "[git] switch git-svn on update",
f: func() error {

202
worktree.go Normal file
View file

@ -0,0 +1,202 @@
package main
import (
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"syscall"
)
// isNotADirectory returns true if err indicates a "not a directory" condition
// (e.g., trying to traverse a path component that is a regular file).
func isNotADirectory(err error) bool {
return errors.Is(err, syscall.ENOTDIR)
}
// isLinkedGitDir checks whether dir has a .git file (not directory) with a
// gitdir: reference. This is the case for both linked worktrees and
// submodules — either way, the directory cannot be migrated independently.
// When true, it returns the resolved gitdir target path.
func isLinkedGitDir(dir string) (bool, string, error) {
dotGit := filepath.Join(dir, ".git")
fi, err := os.Lstat(dotGit)
if err != nil {
if os.IsNotExist(err) {
return false, "", nil
}
return false, "", err
}
// .git is a directory → regular repo, safe to migrate
if fi.IsDir() {
return false, "", nil
}
// .git is a file → linked checkout (worktree or submodule)
content, err := os.ReadFile(dotGit)
if err != nil {
return false, "", err
}
line := strings.TrimSpace(string(content))
if !strings.HasPrefix(line, "gitdir: ") {
return false, "", nil
}
gitdir := strings.TrimPrefix(line, "gitdir: ")
// Resolve relative paths
if !filepath.IsAbs(gitdir) {
gitdir = filepath.Join(dir, gitdir)
}
gitdir = filepath.Clean(gitdir)
return true, gitdir, nil
}
// isWorktreeGitDir returns true if gitdirTarget looks like a worktree entry
// (.git/worktrees/<name>) rather than a submodule (.git/modules/<name>).
func isWorktreeGitDir(gitdirTarget string) bool {
return strings.Contains(filepath.ToSlash(gitdirTarget), ".git/worktrees/")
}
// hasLinkedWorktrees reports whether the Git repository at dir has any linked
// worktrees (entries under .git/worktrees/).
//
// Known limitation: bare repos store worktrees in <bare-repo>/worktrees/
// (no .git/ prefix). This check only looks at .git/worktrees/ and would
// miss bare repo worktrees.
func hasLinkedWorktrees(dir string) (bool, error) {
worktreesDir := filepath.Join(dir, ".git", "worktrees")
entries, err := os.ReadDir(worktreesDir)
if err != nil {
if os.IsNotExist(err) || isNotADirectory(err) {
return false, nil
}
return false, err
}
for _, e := range entries {
if e.IsDir() {
return true, nil
}
}
return false, nil
}
// listLinkedWorktreePaths reads .git/worktrees/*/gitdir in dir and returns
// the worktree working-directory paths.
func listLinkedWorktreePaths(dir string) ([]string, error) {
worktreesDir := filepath.Join(dir, ".git", "worktrees")
entries, err := os.ReadDir(worktreesDir)
if err != nil {
if os.IsNotExist(err) || isNotADirectory(err) {
return nil, nil
}
return nil, err
}
var paths []string
for _, e := range entries {
if !e.IsDir() {
continue
}
gitdirFile := filepath.Join(worktreesDir, e.Name(), "gitdir")
content, err := os.ReadFile(gitdirFile)
if err != nil {
continue
}
wtPath := strings.TrimSpace(string(content))
if wtPath == "" {
continue
}
// Resolve to native path
wtPath = filepath.FromSlash(wtPath)
if !filepath.IsAbs(wtPath) {
wtPath = filepath.Join(worktreesDir, e.Name(), wtPath)
}
wtPath = filepath.Clean(wtPath)
// The gitdir file stores the path to the worktree's .git file
// (e.g., "/path/to/wt/.git"); strip trailing /.git to get working dir.
wtDir := strings.TrimSuffix(filepath.ToSlash(wtPath), "/.git")
paths = append(paths, filepath.FromSlash(wtDir))
}
return paths, nil
}
// resolveMainRepoDir resolves the main repository working directory from a
// worktree's gitdir target path (e.g., /path/to/main/.git/worktrees/<name>).
// It reads the commondir file to find the shared .git directory.
func resolveMainRepoDir(gitdirTarget string) (string, error) {
commondirFile := filepath.Join(gitdirTarget, "commondir")
content, err := os.ReadFile(commondirFile)
if err != nil {
return "", fmt.Errorf("failed to read commondir: %w", err)
}
commondir := strings.TrimSpace(string(content))
if !filepath.IsAbs(commondir) {
commondir = filepath.Join(gitdirTarget, commondir)
}
commondir = filepath.Clean(commondir)
// commondir points to the .git directory; the working tree is its parent
return filepath.Dir(commondir), nil
}
// repairWorktreeBackPointers reads .git/worktrees/*/gitdir in destDir and
// returns the current worktree working-directory paths. For worktrees that
// were inside the old repo directory (oldDir), it rewrites the gitdir file
// to reflect the new location so that a subsequent "git worktree repair"
// can match them.
func repairWorktreeBackPointers(oldDir, destDir string) ([]string, error) {
worktreesDir := filepath.Join(destDir, ".git", "worktrees")
entries, err := os.ReadDir(worktreesDir)
if err != nil {
if os.IsNotExist(err) {
return nil, nil
}
return nil, err
}
// Normalize paths to forward slashes for comparison, since Git uses forward slashes
// in gitdir files even on Windows
oldPrefixNorm := filepath.ToSlash(oldDir) + "/"
var paths []string
for _, e := range entries {
if !e.IsDir() {
continue
}
gitdirFile := filepath.Join(worktreesDir, e.Name(), "gitdir")
content, err := os.ReadFile(gitdirFile)
if err != nil {
continue // skip entries without a gitdir file
}
wtPath := strings.TrimSpace(string(content))
if wtPath == "" {
continue
}
// Internal worktree: moved along with the repo → fix back-pointer
// Normalize wtPath for comparison since Git writes forward slashes on all platforms
wtPathNorm := filepath.ToSlash(wtPath)
if strings.HasPrefix(wtPathNorm, oldPrefixNorm) {
// Compute new path: take the relative portion and join with destDir
relativePart := wtPathNorm[len(oldPrefixNorm):]
newPath := filepath.ToSlash(filepath.Join(destDir, relativePart))
if err := os.WriteFile(gitdirFile, []byte(newPath+"\n"), 0644); err != nil {
return nil, fmt.Errorf("failed to rewrite gitdir for worktree %s: %w", e.Name(), err)
}
wtPath = newPath
}
// The gitdir file stores the path to the worktree's .git file
// (e.g., "/path/to/wt/.git"), but git worktree repair expects
// the worktree working directory (e.g., "/path/to/wt").
// Since wtPath is normalized to forward slashes, trim "/.git"
wtDir := strings.TrimSuffix(wtPath, "/.git")
paths = append(paths, wtDir)
}
return paths, nil
}