mirror of
https://github.com/x-motemen/ghq.git
synced 2026-09-10 15:36:19 -04:00
Compare commits
207 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2474201bcc | ||
|
|
b221561f45 | ||
|
|
9d181bfa4b | ||
|
|
daef1aadff | ||
|
|
58ae404952 | ||
|
|
f5e8c9f2d6 | ||
|
|
b6861f0e51 | ||
|
|
274da4a759 | ||
|
|
5e4f3de9fc | ||
|
|
d5bad01dff | ||
|
|
743f5e5a5f | ||
|
|
a24083e6ca | ||
|
|
d092abdbc7 | ||
|
|
4b6891a472 | ||
|
|
468bfe6cdb | ||
|
|
27f9bd6efb | ||
|
|
e89f3a5e8b | ||
|
|
df13d57bd2 | ||
|
|
b75675d37c | ||
|
|
54bdb6d41f | ||
|
|
26231fbe6e | ||
|
|
5d175304f6 | ||
|
|
de2f1d0c1a | ||
|
|
2c78efff02 | ||
|
|
d014ab3965 | ||
|
|
ac1694b66c | ||
|
|
927ff49ab7 | ||
|
|
cadcad0ab9 | ||
|
|
9be4e0f352 | ||
|
|
d968bd3e48 | ||
|
|
f0ea1c9648 | ||
|
|
39912f3fe5 | ||
|
|
687e7ae85c | ||
|
|
182ca13043 | ||
|
|
83b68bdfbb | ||
|
|
e1bf2f7a47 | ||
|
|
d602bc06b3 | ||
|
|
1307087804 | ||
|
|
5f27c38fab | ||
|
|
d91aa22c47 | ||
|
|
4f1c46dec6 | ||
|
|
9e24f0b0c2 | ||
|
|
7c228ec3ce | ||
|
|
88811c2645 | ||
|
|
336b7790d3 | ||
|
|
112f9b2a9d | ||
|
|
bbe6528a13 | ||
|
|
5c17f32b34 | ||
|
|
4e51d4d660 | ||
|
|
b9fd98e162 | ||
|
|
e3f7e3d035 | ||
|
|
41e6d08a74 | ||
|
|
9201500760 | ||
|
|
f6b96648d8 | ||
|
|
0a809d8b51 | ||
|
|
439da3bd1d | ||
|
|
f600896542 | ||
|
|
500a99a5e9 | ||
|
|
f51d964aa9 | ||
|
|
3b1f4ad5a1 | ||
|
|
1ce2e915fb | ||
|
|
8506f1f526 | ||
|
|
ec14d34d39 | ||
|
|
b6ab1e9a45 | ||
|
|
12ffcb2d6c | ||
|
|
385a9940bb | ||
|
|
8a99bf1670 | ||
|
|
435ed0ec87 | ||
|
|
723bf49fd6 | ||
|
|
c33ed436cf | ||
|
|
c1ee89a04f | ||
|
|
43600f28a6 | ||
|
|
d25a363d0a | ||
|
|
6a0021954f | ||
|
|
d45b3d0e95 | ||
|
|
bdace9708d | ||
|
|
d284552480 | ||
|
|
a8c8f050e9 | ||
|
|
bdf5b4c80c | ||
|
|
54915f57a6 | ||
|
|
9c3659fffe | ||
|
|
03b8efaa81 | ||
|
|
bd68bf70aa | ||
|
|
3425b00fbc | ||
|
|
7dff4b0aa8 | ||
|
|
8b888f0737 | ||
|
|
c042fa4d99 | ||
|
|
f0d52d2307 | ||
|
|
c71ae6ae4b | ||
|
|
e3decba1c0 | ||
|
|
719b543464 | ||
|
|
8edf244e22 | ||
|
|
fdc03b5080 | ||
|
|
54c751514b | ||
|
|
ed40ead1db | ||
|
|
c25ed1ad70 | ||
|
|
ed918b5315 | ||
|
|
c9d0ca0ad1 | ||
|
|
ffd505d1a5 | ||
|
|
57f0bc66e7 | ||
|
|
be2ccb10b3 | ||
|
|
c5d8347d7b | ||
|
|
e7882d7169 | ||
|
|
531d4f1a5f | ||
|
|
39ead659b5 | ||
|
|
2cf21aeacb | ||
|
|
216dc5de57 | ||
|
|
98d4faa146 | ||
|
|
46be3fa40c | ||
|
|
a12656da90 | ||
|
|
d82dd80f86 | ||
|
|
73d8a79b05 | ||
|
|
0c5582e422 | ||
|
|
8f58aeffe0 | ||
|
|
ea2820c4a6 | ||
|
|
d2c9c36798 | ||
|
|
f4db0e785e | ||
|
|
cefa808653 | ||
|
|
32914e6738 | ||
|
|
17a1d6216e | ||
|
|
7caa68bcd0 | ||
|
|
936f79524f | ||
|
|
1b4f893fad | ||
|
|
d50c58368e | ||
|
|
483e6fd0b4 | ||
|
|
c16d46e8bb | ||
|
|
5bb07f7c74 | ||
|
|
af48fe25ac | ||
|
|
92e49a0463 | ||
|
|
344f570f5e | ||
|
|
6a0a293e64 | ||
|
|
ca5bd93797 | ||
|
|
e3f393527d | ||
|
|
d17d6c2257 | ||
|
|
351082d1ed | ||
|
|
875224e621 | ||
|
|
c695205e97 | ||
|
|
0661bf9429 | ||
|
|
deaa4ad5f0 | ||
|
|
cdbf4a59e0 | ||
|
|
b88484299f | ||
|
|
d99f5303a1 | ||
|
|
9d0eae2507 | ||
|
|
63d3a6a3f2 | ||
|
|
863f3cce19 | ||
|
|
8ecdc6603d | ||
|
|
af2759f3fa | ||
|
|
640fb3f223 | ||
|
|
9515744ee5 | ||
|
|
12f129c2f0 | ||
|
|
7557b26547 | ||
|
|
c94a21c4ec | ||
|
|
78f8afc82c | ||
|
|
641e2d9679 | ||
|
|
4c104d74ad | ||
|
|
7e57039d92 | ||
|
|
4010026f51 | ||
|
|
929852152e | ||
|
|
0ffd7f26a4 | ||
|
|
9503ad3620 | ||
|
|
9ba67d64a7 | ||
|
|
f28beb574d | ||
|
|
f7ca71c498 | ||
|
|
7a5333a16e | ||
|
|
2eee0eee71 | ||
|
|
21fb2efbea | ||
|
|
d422d4c612 | ||
|
|
d8981c15a3 | ||
|
|
0c5ad41e30 | ||
|
|
7a1ec399ff | ||
|
|
e1e2b654d1 | ||
|
|
4d03bbc189 | ||
|
|
56ad4bca21 | ||
|
|
382d74f827 | ||
|
|
3c04f664ff | ||
|
|
fd8b62aee1 | ||
|
|
61a751b92a | ||
|
|
73682d3064 | ||
|
|
7c7882b83a | ||
|
|
691e4ec805 | ||
|
|
650c877e62 | ||
|
|
66a60071be | ||
|
|
287b1bbe3e | ||
|
|
4f4abb143d | ||
|
|
4b56f98e01 | ||
|
|
511c23c8b7 | ||
|
|
d4f29e7ede | ||
|
|
baff4ac333 | ||
|
|
82927c2aad | ||
|
|
a09becab6a | ||
|
|
406c7dc8d6 | ||
|
|
a38442787a | ||
|
|
1143ba4b21 | ||
|
|
e0e71a3449 | ||
|
|
39c5d070de | ||
|
|
80c4db33fd | ||
|
|
8631279352 | ||
|
|
63e148f646 | ||
|
|
a02175071d | ||
|
|
a655d1d81f | ||
|
|
ed4b406086 | ||
|
|
e69c7b189a | ||
|
|
ea5ed54d7c | ||
|
|
a8f99be06c | ||
|
|
e50e60bbc2 | ||
|
|
2d66830943 | ||
|
|
cf15ec26ef |
2
.github/actions/release/action.yml
vendored
2
.github/actions/release/action.yml
vendored
|
|
@ -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
20
.github/copilot-instructions.md
vendored
Normal 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
18
.github/dependabot.yml
vendored
Normal 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/*"
|
||||
9
.github/workflows/integration.yaml
vendored
9
.github/workflows/integration.yaml
vendored
|
|
@ -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
|
||||
|
|
|
|||
8
.github/workflows/release.yaml
vendored
8
.github/workflows/release.yaml
vendored
|
|
@ -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 }}
|
||||
|
|
|
|||
37
.github/workflows/reviewdog.yaml
vendored
37
.github/workflows/reviewdog.yaml
vendored
|
|
@ -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
|
||||
|
|
|
|||
13
.github/workflows/tagpr.yaml
vendored
13
.github/workflows/tagpr.yaml
vendored
|
|
@ -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
|
||||
|
|
|
|||
17
.github/workflows/test.yaml
vendored
17
.github/workflows/test.yaml
vendored
|
|
@ -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
1
.gitignore
vendored
|
|
@ -3,3 +3,4 @@
|
|||
/dist
|
||||
.vscode
|
||||
.idea
|
||||
coverage.out
|
||||
|
|
|
|||
58
CHANGELOG.md
58
CHANGELOG.md
|
|
@ -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
390
CREDITS
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
42
README.adoc
42
README.adoc
|
|
@ -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
|
||||
|
||||
----
|
||||
|
|
|
|||
|
|
@ -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 == "" {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
49
cmd_get.go
49
cmd_get.go
|
|
@ -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 {
|
||||
|
|
|
|||
209
cmd_get_test.go
209
cmd_get_test.go
|
|
@ -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 {
|
||||
|
|
|
|||
19
cmd_list.go
19
cmd_list.go
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
205
cmd_migrate.go
Normal 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
459
cmd_migrate_test.go
Normal 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
108
cmd_rm.go
|
|
@ -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)
|
||||
|
|
|
|||
219
cmd_rm_test.go
219
cmd_rm_test.go
|
|
@ -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")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
41
commands.go
41
commands.go
|
|
@ -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"},
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
},
|
||||
|
|
|
|||
14
getter.go
14
getter.go
|
|
@ -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
31
go.mod
|
|
@ -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
128
go.sum
|
|
@ -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=
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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...))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
30
main.go
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
7
url.go
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
155
vcs.go
|
|
@ -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{
|
||||
|
|
|
|||
20
vcs_test.go
20
vcs_test.go
|
|
@ -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
202
worktree.go
Normal 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
|
||||
}
|
||||
Loading…
Reference in a new issue