├── core ├── gogh │ ├── appname.go │ ├── hostname.go │ └── constants_test.go ├── overlay │ ├── store.go │ ├── service.go │ ├── overlay.go │ └── content_store_mock_test.go ├── script │ ├── store.go │ ├── service.go │ └── script.go ├── store │ └── store.go ├── repository │ ├── reference_json.go │ ├── validate.go │ ├── location.go │ └── reference.go ├── hook │ └── service.go ├── fs │ ├── tilde.go │ └── tilde_test.go ├── auth │ └── authenticate_service.go ├── workspace │ ├── finder_service.go │ ├── workspace_service.go │ └── layout_service.go ├── hosting │ └── repository.go ├── extra │ └── extra_service.go └── repository_mock │ └── gen_location_format_mock.go ├── doc ├── image │ └── gogh.jpg └── usage │ ├── gogh_roots_list.md │ ├── gogh_auth_list.md │ ├── gogh_auth_logout.md │ ├── gogh_config_show.md │ ├── gogh_hook_remove.md │ ├── gogh_config_migrate.md │ ├── gogh_roots_remove.md │ ├── gogh_script_remove.md │ ├── gogh_overlay_remove.md │ ├── gogh_hook_show.md │ ├── gogh_script_edit.md │ ├── gogh_auth_login.md │ ├── gogh_overlay_edit.md │ ├── gogh_roots_set-primary.md │ ├── gogh_roots_add.md │ ├── gogh_config_set-default-host.md │ ├── gogh_hook_list.md │ ├── gogh_script_create.md │ ├── gogh_script_add.md │ ├── gogh_config_set-default-owner.md │ ├── gogh_bundle.md │ ├── gogh_script_list.md │ ├── gogh_script_show.md │ ├── gogh_overlay_list.md │ ├── gogh_overlay_show.md │ ├── gogh_script_update.md │ ├── gogh_bundle_dump.md │ ├── gogh_auth.md │ ├── gogh_config_auth.md │ ├── gogh_extra_show.md │ ├── gogh_overlay_update.md │ ├── gogh_roots.md │ ├── gogh_extra_create.md │ ├── gogh_extra_list.md │ ├── gogh_extra_apply.md │ ├── gogh_hook.md │ ├── gogh_hook_add.md │ ├── gogh_config_roots.md │ ├── gogh_extra_remove.md │ ├── gogh_hook_update.md │ ├── gogh_config.md │ ├── gogh_completion_powershell.md │ ├── gogh_bundle_restore.md │ ├── gogh_completion_fish.md │ ├── gogh_completion.md │ ├── gogh_hook_invoke.md │ ├── gogh_extra.md │ ├── gogh_script.md │ ├── gogh_extra_save.md │ ├── gogh_overlay_add.md │ ├── gogh_completion_bash.md │ ├── gogh_completion_zsh.md │ ├── gogh_fork.md │ ├── gogh.md │ ├── gogh_clone.md │ ├── gogh_delete.md │ ├── gogh_script_invoke-instant.md │ ├── gogh_cwd.md │ ├── gogh_overlay_apply.md │ ├── gogh_script_invoke.md │ ├── gogh_repos.md │ └── gogh_list.md ├── infra ├── githubv4 │ ├── graphql.config.toml │ ├── genqlient.yaml │ ├── CONTRIBUTING.md │ ├── Makefile │ └── genqlient.graphql └── logger │ └── logger.go ├── .github ├── actions │ └── pkgbuild │ │ ├── Dockerfile │ │ ├── action.yaml │ │ ├── PKGBUILD.tmpl │ │ └── entrypoint.sh ├── workflows │ ├── review.yml │ ├── assign.yml │ ├── release.yml │ └── release-on-tag.yml └── dependabot.yml ├── cmd └── gogh │ └── main_test.go ├── codecov.yml ├── typ ├── enum.go ├── ptr.go ├── tristate.go ├── map.go └── ptr_test.go ├── .gitignore ├── ui └── cli │ ├── commands │ ├── auth.go │ ├── bundle.go │ ├── hook.go │ ├── script.go │ ├── config.go │ ├── auth_test.go │ ├── config_template.txt │ ├── cwd_test.go │ ├── man_test.go │ ├── clone_test.go │ ├── fork_test.go │ ├── list_test.go │ ├── repos_test.go │ ├── roots_test.go │ ├── bundle_test.go │ ├── config_test.go │ ├── create_test.go │ ├── delete_test.go │ ├── auth_list_test.go │ ├── migrate_test.go │ ├── roots_add_test.go │ ├── auth_login_test.go │ ├── roots_list_test.go │ ├── auth_logout_test.go │ ├── bundle_dump_test.go │ ├── config_show_test.go │ ├── overlay_list_test.go │ ├── overlay_show_test.go │ ├── roots_remove_test.go │ ├── bundle_restore_test.go │ ├── overlay_apply_test.go │ ├── set_default_host_test.go │ ├── roots_set_primary_test.go │ ├── set_default_owner_test.go │ ├── extra.go │ ├── hook_remove.go │ ├── hook_show.go │ ├── set_default_host.go │ ├── script_list.go │ ├── overlay_list.go │ ├── edit.go │ ├── hook_list.go │ ├── set_default_owner.go │ ├── script_remove.go │ ├── script_show.go │ ├── overlay_remove.go │ ├── extra_show.go │ ├── auth_list.go │ ├── migrate.go │ ├── script_add.go │ ├── overlay_show.go │ ├── extra_list.go │ ├── extra_apply.go │ ├── script_run.go │ ├── extra_remove.go │ ├── script_update.go │ ├── overlay_test.go │ ├── script_edit.go │ ├── overlay_edit.go │ ├── extra_create.go │ ├── overlay_update.go │ ├── bundle_dump.go │ ├── cwd.go │ ├── overlay.go │ ├── hook_invoke.go │ └── script_create.go │ ├── view │ ├── try_clone_notify.go │ └── process_with_confirmation.go │ └── flags │ └── repository_format.go ├── app ├── hook │ ├── remove │ │ └── usecase.go │ ├── list │ │ └── usecase.go │ ├── show │ │ └── usecase.go │ └── describe │ │ └── usecase.go ├── auth │ ├── logout │ │ ├── usecase.go │ │ └── usecase_test.go │ ├── list │ │ └── usecase.go │ └── login │ │ └── usecase.go ├── script │ ├── remove │ │ └── usecase.go │ ├── add │ │ └── usecase.go │ ├── update │ │ └── usecase.go │ ├── edit │ │ └── usecase.go │ ├── list │ │ └── usecase.go │ ├── show │ │ └── usecase.go │ └── run │ │ └── usecase.go ├── overlay │ ├── remove │ │ ├── usecase.go │ │ └── usecase_test.go │ ├── add │ │ └── usecase.go │ ├── update │ │ └── usecase.go │ ├── edit │ │ └── usecase.go │ ├── list │ │ └── usecase.go │ └── show │ │ └── usecase.go ├── repoprint │ ├── repotab │ │ ├── time.go │ │ └── cell.go │ └── usecase_test.go ├── cwd │ └── cwd_usecase.go ├── config │ ├── flags_store_v0.go │ ├── flags_store.go │ ├── store_helpers.go │ ├── materialize.go │ ├── script_source_store.go │ └── overlay_content_store.go ├── list │ └── usecase.go ├── extra │ ├── show │ │ └── usecase.go │ └── list │ │ └── usecase.go └── service │ └── service_set.go ├── arch-go.yml ├── .golangci.yml ├── Makefile ├── lua └── gogh.lua ├── LICENSE └── .goreleaser.yml /core/gogh/appname.go: -------------------------------------------------------------------------------- 1 | package gogh 2 | 3 | const AppName = "gogh" 4 | -------------------------------------------------------------------------------- /doc/image/gogh.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kyoh86/gogh/HEAD/doc/image/gogh.jpg -------------------------------------------------------------------------------- /infra/githubv4/graphql.config.toml: -------------------------------------------------------------------------------- 1 | schema = "./schema.graphql" 2 | documents = "./*.graphql" 3 | -------------------------------------------------------------------------------- /core/gogh/hostname.go: -------------------------------------------------------------------------------- 1 | package gogh 2 | 3 | // DefaultHost is the default host for gogh. 4 | const DefaultHost = "github.com" 5 | -------------------------------------------------------------------------------- /.github/actions/pkgbuild/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM archlinux:latest 2 | 3 | COPY entrypoint.sh /entrypoint.sh 4 | COPY PKGBUILD.tmpl /PKGBUILD.tmpl 5 | 6 | ENTRYPOINT ["/entrypoint.sh"] 7 | -------------------------------------------------------------------------------- /cmd/gogh/main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestMain(_ *testing.T) { 8 | // This tests only ensures that the main can be called without errors. 9 | main() 10 | } 11 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | ignore: 2 | - "**/*_mock/" 3 | - "**/*_mock.go" 4 | - "**/gen.go" 5 | - "**/generated.go" 6 | - "ui/cli/" # CLI code is not tested 7 | - "infra/github" # GitHub integration code is not tested because it requires a GitHub host 8 | -------------------------------------------------------------------------------- /.github/actions/pkgbuild/action.yaml: -------------------------------------------------------------------------------- 1 | name: Generate PKGBUILD for a go package 2 | author: kyoh86 3 | description: Generate PKGBUILD for a go package 4 | branding: 5 | color: blue 6 | icon: chevron-up 7 | runs: 8 | using: 'docker' 9 | image: 'Dockerfile' 10 | -------------------------------------------------------------------------------- /doc/usage/gogh_roots_list.md: -------------------------------------------------------------------------------- 1 | ## gogh roots list 2 | 3 | List all of the roots 4 | 5 | ``` 6 | gogh roots list [flags] 7 | ``` 8 | 9 | ### Options 10 | 11 | ``` 12 | -h, --help help for list 13 | ``` 14 | 15 | ### SEE ALSO 16 | 17 | * [gogh roots](gogh_roots.md) - Manage roots 18 | 19 | -------------------------------------------------------------------------------- /doc/usage/gogh_auth_list.md: -------------------------------------------------------------------------------- 1 | ## gogh auth list 2 | 3 | Listup authenticated host and owners 4 | 5 | ``` 6 | gogh auth list [flags] 7 | ``` 8 | 9 | ### Options 10 | 11 | ``` 12 | -h, --help help for list 13 | ``` 14 | 15 | ### SEE ALSO 16 | 17 | * [gogh auth](gogh_auth.md) - Manage tokens 18 | 19 | -------------------------------------------------------------------------------- /doc/usage/gogh_auth_logout.md: -------------------------------------------------------------------------------- 1 | ## gogh auth logout 2 | 3 | Logout from the host and owner 4 | 5 | ``` 6 | gogh auth logout [flags] 7 | ``` 8 | 9 | ### Options 10 | 11 | ``` 12 | -h, --help help for logout 13 | ``` 14 | 15 | ### SEE ALSO 16 | 17 | * [gogh auth](gogh_auth.md) - Manage tokens 18 | 19 | -------------------------------------------------------------------------------- /typ/enum.go: -------------------------------------------------------------------------------- 1 | package typ 2 | 3 | import "errors" 4 | 5 | func Remap[S comparable, V any](v *V, m map[S]V, s S) error { 6 | var es S 7 | if s == es { 8 | return nil 9 | } 10 | x, exists := m[s] 11 | if !exists { 12 | return errors.New("invalid value") 13 | } 14 | *v = x 15 | return nil 16 | } 17 | -------------------------------------------------------------------------------- /doc/usage/gogh_config_show.md: -------------------------------------------------------------------------------- 1 | ## gogh config show 2 | 3 | Show configurations 4 | 5 | ``` 6 | gogh config show [flags] 7 | ``` 8 | 9 | ### Options 10 | 11 | ``` 12 | -h, --help help for show 13 | ``` 14 | 15 | ### SEE ALSO 16 | 17 | * [gogh config](gogh_config.md) - Show/change configurations 18 | 19 | -------------------------------------------------------------------------------- /doc/usage/gogh_hook_remove.md: -------------------------------------------------------------------------------- 1 | ## gogh hook remove 2 | 3 | Remove a registered hook 4 | 5 | ``` 6 | gogh hook remove [flags] 7 | ``` 8 | 9 | ### Options 10 | 11 | ``` 12 | -h, --help help for remove 13 | ``` 14 | 15 | ### SEE ALSO 16 | 17 | * [gogh hook](gogh_hook.md) - Manage repository hooks 18 | 19 | -------------------------------------------------------------------------------- /doc/usage/gogh_config_migrate.md: -------------------------------------------------------------------------------- 1 | ## gogh config migrate 2 | 3 | Migrate configurations 4 | 5 | ``` 6 | gogh config migrate [flags] 7 | ``` 8 | 9 | ### Options 10 | 11 | ``` 12 | -h, --help help for migrate 13 | ``` 14 | 15 | ### SEE ALSO 16 | 17 | * [gogh config](gogh_config.md) - Show/change configurations 18 | 19 | -------------------------------------------------------------------------------- /doc/usage/gogh_roots_remove.md: -------------------------------------------------------------------------------- 1 | ## gogh roots remove 2 | 3 | Remove a directory from the roots 4 | 5 | ``` 6 | gogh roots remove [flags] [] 7 | ``` 8 | 9 | ### Options 10 | 11 | ``` 12 | -h, --help help for remove 13 | ``` 14 | 15 | ### SEE ALSO 16 | 17 | * [gogh roots](gogh_roots.md) - Manage roots 18 | 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # for goreleaser output with .goreleaser.yml 2 | /dist 3 | /doc/man 4 | 5 | # Binaries for programs and plugins 6 | *.exe 7 | *.exe~ 8 | *.dll 9 | *.so 10 | *.dylib 11 | 12 | # Test binary, build with `go test -c` 13 | *.test 14 | 15 | # Output of the go coverage tool, specifically when used with LiteIDE 16 | *.out 17 | -------------------------------------------------------------------------------- /doc/usage/gogh_script_remove.md: -------------------------------------------------------------------------------- 1 | ## gogh script remove 2 | 3 | Remove a script 4 | 5 | ``` 6 | gogh script remove [flags] 7 | ``` 8 | 9 | ### Options 10 | 11 | ``` 12 | -h, --help help for remove 13 | ``` 14 | 15 | ### SEE ALSO 16 | 17 | * [gogh script](gogh_script.md) - Manage repository script files 18 | 19 | -------------------------------------------------------------------------------- /doc/usage/gogh_overlay_remove.md: -------------------------------------------------------------------------------- 1 | ## gogh overlay remove 2 | 3 | Remove an overlay 4 | 5 | ``` 6 | gogh overlay remove [flags] 7 | ``` 8 | 9 | ### Options 10 | 11 | ``` 12 | -h, --help help for remove 13 | ``` 14 | 15 | ### SEE ALSO 16 | 17 | * [gogh overlay](gogh_overlay.md) - Manage repository overlay files 18 | 19 | -------------------------------------------------------------------------------- /doc/usage/gogh_hook_show.md: -------------------------------------------------------------------------------- 1 | ## gogh hook show 2 | 3 | Show a hook 4 | 5 | ``` 6 | gogh hook show [flags] 7 | ``` 8 | 9 | ### Options 10 | 11 | ``` 12 | -h, --help help for show 13 | --json Output in JSON format 14 | ``` 15 | 16 | ### SEE ALSO 17 | 18 | * [gogh hook](gogh_hook.md) - Manage repository hooks 19 | 20 | -------------------------------------------------------------------------------- /doc/usage/gogh_script_edit.md: -------------------------------------------------------------------------------- 1 | ## gogh script edit 2 | 3 | Edit an existing script (with $EDITOR) 4 | 5 | ``` 6 | gogh script edit [flags] 7 | ``` 8 | 9 | ### Options 10 | 11 | ``` 12 | -h, --help help for edit 13 | ``` 14 | 15 | ### SEE ALSO 16 | 17 | * [gogh script](gogh_script.md) - Manage repository script files 18 | 19 | -------------------------------------------------------------------------------- /doc/usage/gogh_auth_login.md: -------------------------------------------------------------------------------- 1 | ## gogh auth login 2 | 3 | Login for the host and owner 4 | 5 | ``` 6 | gogh auth login [flags] 7 | ``` 8 | 9 | ### Options 10 | 11 | ``` 12 | -h, --help help for login 13 | --host string Host name to login 14 | ``` 15 | 16 | ### SEE ALSO 17 | 18 | * [gogh auth](gogh_auth.md) - Manage tokens 19 | 20 | -------------------------------------------------------------------------------- /doc/usage/gogh_overlay_edit.md: -------------------------------------------------------------------------------- 1 | ## gogh overlay edit 2 | 3 | Edit an existing overlay (with $EDITOR) 4 | 5 | ``` 6 | gogh overlay edit [flags] 7 | ``` 8 | 9 | ### Options 10 | 11 | ``` 12 | -h, --help help for edit 13 | ``` 14 | 15 | ### SEE ALSO 16 | 17 | * [gogh overlay](gogh_overlay.md) - Manage repository overlay files 18 | 19 | -------------------------------------------------------------------------------- /doc/usage/gogh_roots_set-primary.md: -------------------------------------------------------------------------------- 1 | ## gogh roots set-primary 2 | 3 | Set a directory as the primary in the roots 4 | 5 | ``` 6 | gogh roots set-primary [flags] [] 7 | ``` 8 | 9 | ### Options 10 | 11 | ``` 12 | -h, --help help for set-primary 13 | ``` 14 | 15 | ### SEE ALSO 16 | 17 | * [gogh roots](gogh_roots.md) - Manage roots 18 | 19 | -------------------------------------------------------------------------------- /ui/cli/commands/auth.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/kyoh86/gogh/v4/app/service" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func NewAuthCommand(_ context.Context, _ *service.ServiceSet) (*cobra.Command, error) { 11 | return &cobra.Command{ 12 | Use: "auth", 13 | Short: "Manage tokens", 14 | }, nil 15 | } 16 | -------------------------------------------------------------------------------- /doc/usage/gogh_roots_add.md: -------------------------------------------------------------------------------- 1 | ## gogh roots add 2 | 3 | Add a directory into the roots 4 | 5 | ``` 6 | gogh roots add [flags] 7 | ``` 8 | 9 | ### Options 10 | 11 | ``` 12 | --as-primary Set as primary root 13 | -h, --help help for add 14 | ``` 15 | 16 | ### SEE ALSO 17 | 18 | * [gogh roots](gogh_roots.md) - Manage roots 19 | 20 | -------------------------------------------------------------------------------- /ui/cli/commands/bundle.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/kyoh86/gogh/v4/app/service" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func NewBundleCommand(_ context.Context, _ *service.ServiceSet) (*cobra.Command, error) { 11 | return &cobra.Command{ 12 | Use: "bundle", 13 | Short: "Manage bundle", 14 | }, nil 15 | } 16 | -------------------------------------------------------------------------------- /infra/githubv4/genqlient.yaml: -------------------------------------------------------------------------------- 1 | # Default genqlient config; for full documentation see: 2 | # https://github.com/Khan/genqlient/blob/main/docs/genqlient.yaml 3 | schema: schema.graphql 4 | operations: 5 | - genqlient.graphql 6 | generated: generated.go 7 | bindings: 8 | URI: 9 | type: string 10 | DateTime: 11 | type: time.Time 12 | GitSSHRemote: 13 | type: string 14 | -------------------------------------------------------------------------------- /doc/usage/gogh_config_set-default-host.md: -------------------------------------------------------------------------------- 1 | ## gogh config set-default-host 2 | 3 | Set the default host for the repository 4 | 5 | ``` 6 | gogh config set-default-host [flags] 7 | ``` 8 | 9 | ### Options 10 | 11 | ``` 12 | -h, --help help for set-default-host 13 | ``` 14 | 15 | ### SEE ALSO 16 | 17 | * [gogh config](gogh_config.md) - Show/change configurations 18 | 19 | -------------------------------------------------------------------------------- /ui/cli/commands/hook.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/kyoh86/gogh/v4/app/service" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func NewHookCommand(_ context.Context, svc *service.ServiceSet) (*cobra.Command, error) { 11 | cmd := &cobra.Command{ 12 | Use: "hook", 13 | Short: "Manage repository hooks", 14 | } 15 | return cmd, nil 16 | } 17 | -------------------------------------------------------------------------------- /doc/usage/gogh_hook_list.md: -------------------------------------------------------------------------------- 1 | ## gogh hook list 2 | 3 | List registered hooks 4 | 5 | ``` 6 | gogh hook list [flags] 7 | ``` 8 | 9 | ### Options 10 | 11 | ``` 12 | -h, --help help for list 13 | --json Output in JSON format 14 | --source Output with source code 15 | ``` 16 | 17 | ### SEE ALSO 18 | 19 | * [gogh hook](gogh_hook.md) - Manage repository hooks 20 | 21 | -------------------------------------------------------------------------------- /doc/usage/gogh_script_create.md: -------------------------------------------------------------------------------- 1 | ## gogh script create 2 | 3 | Create a new script (with $EDITOR) 4 | 5 | ``` 6 | gogh script create [flags] 7 | ``` 8 | 9 | ### Options 10 | 11 | ``` 12 | -h, --help help for create 13 | --name string Name of the script 14 | ``` 15 | 16 | ### SEE ALSO 17 | 18 | * [gogh script](gogh_script.md) - Manage repository script files 19 | 20 | -------------------------------------------------------------------------------- /doc/usage/gogh_script_add.md: -------------------------------------------------------------------------------- 1 | ## gogh script add 2 | 3 | Add an existing Lua script as script 4 | 5 | ``` 6 | gogh script add [flags] 7 | ``` 8 | 9 | ### Options 10 | 11 | ``` 12 | -h, --help help for add 13 | --name string Name of the script 14 | ``` 15 | 16 | ### SEE ALSO 17 | 18 | * [gogh script](gogh_script.md) - Manage repository script files 19 | 20 | -------------------------------------------------------------------------------- /ui/cli/commands/script.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/kyoh86/gogh/v4/app/service" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func NewScriptCommand(_ context.Context, svc *service.ServiceSet) (*cobra.Command, error) { 11 | cmd := &cobra.Command{ 12 | Use: "script", 13 | Short: "Manage repository script files", 14 | } 15 | return cmd, nil 16 | } 17 | -------------------------------------------------------------------------------- /doc/usage/gogh_config_set-default-owner.md: -------------------------------------------------------------------------------- 1 | ## gogh config set-default-owner 2 | 3 | Set the default owner for a host for the repository 4 | 5 | ``` 6 | gogh config set-default-owner [flags] 7 | ``` 8 | 9 | ### Options 10 | 11 | ``` 12 | -h, --help help for set-default-owner 13 | ``` 14 | 15 | ### SEE ALSO 16 | 17 | * [gogh config](gogh_config.md) - Show/change configurations 18 | 19 | -------------------------------------------------------------------------------- /doc/usage/gogh_bundle.md: -------------------------------------------------------------------------------- 1 | ## gogh bundle 2 | 3 | Manage bundle 4 | 5 | ### Options 6 | 7 | ``` 8 | -h, --help help for bundle 9 | ``` 10 | 11 | ### SEE ALSO 12 | 13 | * [gogh](gogh.md) - GO GitHub local repository manager 14 | * [gogh bundle dump](gogh_bundle_dump.md) - Export current local repository list 15 | * [gogh bundle restore](gogh_bundle_restore.md) - Get dumped local repositoiries 16 | 17 | -------------------------------------------------------------------------------- /doc/usage/gogh_script_list.md: -------------------------------------------------------------------------------- 1 | ## gogh script list 2 | 3 | List registered scripts 4 | 5 | ``` 6 | gogh script list [flags] 7 | ``` 8 | 9 | ### Options 10 | 11 | ``` 12 | -h, --help help for list 13 | --json Output in JSON format 14 | --source Output with source code 15 | ``` 16 | 17 | ### SEE ALSO 18 | 19 | * [gogh script](gogh_script.md) - Manage repository script files 20 | 21 | -------------------------------------------------------------------------------- /doc/usage/gogh_script_show.md: -------------------------------------------------------------------------------- 1 | ## gogh script show 2 | 3 | Show a script 4 | 5 | ``` 6 | gogh script show [flags] 7 | ``` 8 | 9 | ### Options 10 | 11 | ``` 12 | -h, --help help for show 13 | --json Output in JSON format 14 | --source Output with source code 15 | ``` 16 | 17 | ### SEE ALSO 18 | 19 | * [gogh script](gogh_script.md) - Manage repository script files 20 | 21 | -------------------------------------------------------------------------------- /.github/workflows/review.yml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json 2 | 3 | name: Review 4 | on: [pull_request] 5 | jobs: 6 | lint: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | - uses: reviewdog/action-golangci-lint@v2 11 | with: 12 | level: info 13 | github_token: ${{ secrets.GITHUB_TOKEN }} 14 | -------------------------------------------------------------------------------- /doc/usage/gogh_overlay_list.md: -------------------------------------------------------------------------------- 1 | ## gogh overlay list 2 | 3 | List registered overlays 4 | 5 | ``` 6 | gogh overlay list [flags] 7 | ``` 8 | 9 | ### Options 10 | 11 | ``` 12 | -h, --help help for list 13 | --json Output in JSON format 14 | --source Output with source code 15 | ``` 16 | 17 | ### SEE ALSO 18 | 19 | * [gogh overlay](gogh_overlay.md) - Manage repository overlay files 20 | 21 | -------------------------------------------------------------------------------- /core/overlay/store.go: -------------------------------------------------------------------------------- 1 | package overlay 2 | 3 | import ( 4 | "context" 5 | "io" 6 | ) 7 | 8 | // ContentStore is an abstraction for managing overlay content (file, DB, etc). 9 | type ContentStore interface { 10 | Save(ctx context.Context, overlayID string, content io.Reader) error 11 | Open(ctx context.Context, overlayID string) (io.ReadCloser, error) 12 | Remove(ctx context.Context, overlayID string) error 13 | } 14 | -------------------------------------------------------------------------------- /doc/usage/gogh_overlay_show.md: -------------------------------------------------------------------------------- 1 | ## gogh overlay show 2 | 3 | Show an overlay 4 | 5 | ``` 6 | gogh overlay show [flags] 7 | ``` 8 | 9 | ### Options 10 | 11 | ``` 12 | -h, --help help for show 13 | --json Output in JSON format 14 | --source Output with source code 15 | ``` 16 | 17 | ### SEE ALSO 18 | 19 | * [gogh overlay](gogh_overlay.md) - Manage repository overlay files 20 | 21 | -------------------------------------------------------------------------------- /typ/ptr.go: -------------------------------------------------------------------------------- 1 | package typ 2 | 3 | func NilablePtr[T comparable](v T) *T { 4 | if v == defaultValue[T]() { 5 | return nil 6 | } 7 | return &v 8 | } 9 | 10 | // defaultValue returns the zero value for a given type. 11 | func defaultValue[T any]() T { 12 | var zero T 13 | return zero 14 | } 15 | 16 | // Ptr converts a value of any type to a pointer to that value. 17 | func Ptr[T any](v T) *T { 18 | return &v 19 | } 20 | -------------------------------------------------------------------------------- /core/script/store.go: -------------------------------------------------------------------------------- 1 | package script 2 | 3 | import ( 4 | "context" 5 | "io" 6 | ) 7 | 8 | // ScriptSourceStore defines abstraction for saving, opening, and removing script source. 9 | type ScriptSourceStore interface { 10 | Save(ctx context.Context, scriptID string, content io.Reader) error 11 | Open(ctx context.Context, scriptID string) (io.ReadCloser, error) 12 | Remove(ctx context.Context, scriptID string) error 13 | } 14 | -------------------------------------------------------------------------------- /.github/workflows/assign.yml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json 2 | 3 | name: Issue assignment 4 | on: 5 | issues: 6 | types: [opened] 7 | jobs: 8 | auto-assign: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | issues: write 12 | steps: 13 | - name: 'Auto-assign issue' 14 | uses: pozil/auto-assign-issue@v2 15 | with: 16 | assignees: kyoh86 17 | -------------------------------------------------------------------------------- /doc/usage/gogh_script_update.md: -------------------------------------------------------------------------------- 1 | ## gogh script update 2 | 3 | Update an existing script 4 | 5 | ``` 6 | gogh script update [flags] 7 | ``` 8 | 9 | ### Options 10 | 11 | ``` 12 | -h, --help help for update 13 | --name string Name of the script 14 | --source string Script source file path 15 | ``` 16 | 17 | ### SEE ALSO 18 | 19 | * [gogh script](gogh_script.md) - Manage repository script files 20 | 21 | -------------------------------------------------------------------------------- /ui/cli/commands/config.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | _ "embed" 6 | 7 | "github.com/kyoh86/gogh/v4/app/service" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | func NewConfigCommand(_ context.Context, svc *service.ServiceSet) (*cobra.Command, error) { 12 | return &cobra.Command{ 13 | Use: "config", 14 | Short: "Show/change configurations", 15 | Aliases: []string{"conf", "setting", "context"}, 16 | }, nil 17 | } 18 | -------------------------------------------------------------------------------- /app/hook/remove/usecase.go: -------------------------------------------------------------------------------- 1 | package remove 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/kyoh86/gogh/v4/core/hook" 7 | ) 8 | 9 | type Usecase struct { 10 | hookService hook.HookService 11 | } 12 | 13 | func NewUsecase(hookService hook.HookService) *Usecase { 14 | return &Usecase{hookService: hookService} 15 | } 16 | 17 | func (uc *Usecase) Execute(ctx context.Context, hookID string) error { 18 | return uc.hookService.Remove(ctx, hookID) 19 | } 20 | -------------------------------------------------------------------------------- /doc/usage/gogh_bundle_dump.md: -------------------------------------------------------------------------------- 1 | ## gogh bundle dump 2 | 3 | Export current local repository list 4 | 5 | ``` 6 | gogh bundle dump [flags] 7 | ``` 8 | 9 | ### Options 10 | 11 | ``` 12 | -f, --file string A file to output; if it's empty("") or hyphen("-"), output to stdout (default "/home/kyoh86/.config/gogh/bundle.txt") 13 | -h, --help help for dump 14 | ``` 15 | 16 | ### SEE ALSO 17 | 18 | * [gogh bundle](gogh_bundle.md) - Manage bundle 19 | 20 | -------------------------------------------------------------------------------- /doc/usage/gogh_auth.md: -------------------------------------------------------------------------------- 1 | ## gogh auth 2 | 3 | Manage tokens 4 | 5 | ### Options 6 | 7 | ``` 8 | -h, --help help for auth 9 | ``` 10 | 11 | ### SEE ALSO 12 | 13 | * [gogh](gogh.md) - GO GitHub local repository manager 14 | * [gogh auth list](gogh_auth_list.md) - Listup authenticated host and owners 15 | * [gogh auth login](gogh_auth_login.md) - Login for the host and owner 16 | * [gogh auth logout](gogh_auth_logout.md) - Logout from the host and owner 17 | 18 | -------------------------------------------------------------------------------- /app/auth/logout/usecase.go: -------------------------------------------------------------------------------- 1 | package logout 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/kyoh86/gogh/v4/core/auth" 7 | ) 8 | 9 | type Usecase struct { 10 | tokenService auth.TokenService 11 | } 12 | 13 | func NewUsecase(tokenService auth.TokenService) *Usecase { 14 | return &Usecase{ 15 | tokenService: tokenService, 16 | } 17 | } 18 | 19 | func (uc *Usecase) Execute(_ context.Context, host, owner string) error { 20 | return uc.tokenService.Delete(host, owner) 21 | } 22 | -------------------------------------------------------------------------------- /app/script/remove/usecase.go: -------------------------------------------------------------------------------- 1 | package remove 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/kyoh86/gogh/v4/core/script" 7 | ) 8 | 9 | type Usecase struct { 10 | scriptService script.ScriptService 11 | } 12 | 13 | func NewUsecase(scriptService script.ScriptService) *Usecase { 14 | return &Usecase{scriptService: scriptService} 15 | } 16 | 17 | func (uc *Usecase) Execute(ctx context.Context, scriptID string) error { 18 | return uc.scriptService.Remove(ctx, scriptID) 19 | } 20 | -------------------------------------------------------------------------------- /app/auth/list/usecase.go: -------------------------------------------------------------------------------- 1 | package list 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/kyoh86/gogh/v4/core/auth" 7 | ) 8 | 9 | type Usecase struct { 10 | tokenService auth.TokenService 11 | } 12 | 13 | func NewUsecase(tokenService auth.TokenService) *Usecase { 14 | return &Usecase{ 15 | tokenService: tokenService, 16 | } 17 | } 18 | 19 | func (uc *Usecase) Execute(_ context.Context) ([]auth.TokenEntry, error) { 20 | tokens := uc.tokenService.Entries() 21 | return tokens, nil 22 | } 23 | -------------------------------------------------------------------------------- /doc/usage/gogh_config_auth.md: -------------------------------------------------------------------------------- 1 | ## gogh config auth 2 | 3 | Manage tokens 4 | 5 | ### Options 6 | 7 | ``` 8 | -h, --help help for auth 9 | ``` 10 | 11 | ### SEE ALSO 12 | 13 | * [gogh config](gogh_config.md) - Show/change configurations 14 | * [gogh config auth list](gogh_config_auth_list.md) - Listup authenticated host and owners 15 | * [gogh config auth login](gogh_config_auth_login.md) - Login for the host and owner 16 | * [gogh config auth logout](gogh_config_auth_logout.md) - Logout from the host and owner 17 | 18 | -------------------------------------------------------------------------------- /ui/cli/commands/auth_test.go: -------------------------------------------------------------------------------- 1 | package commands_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/kyoh86/gogh/v4/app/service" 8 | "github.com/kyoh86/gogh/v4/ui/cli/commands" 9 | ) 10 | 11 | func TestNewAuthCommand(t *testing.T) { 12 | // Setup 13 | ctx := context.Background() 14 | serviceSet := &service.ServiceSet{} 15 | 16 | // Execute 17 | _, err := commands.NewAuthCommand(ctx, serviceSet) 18 | 19 | // Verify no error occurs 20 | if err != nil { 21 | t.Fatalf("Expected no error, got %v", err) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /infra/githubv4/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # CONTRIBUTING (infra/githubv4) 2 | 3 | If you want to change internal github v4 client library in this package, 4 | you *SHOULD* take the following steps. 5 | 6 | 1. Write query (`*.graphql`) 7 | 2. Re-generate client code with `make` 8 | 9 | ## To edit GrqphQL 10 | 11 | - You can use [GraphiQL](https://github.com/skevy/graphiql-app) or the [API Explorer](https://docs.github.com/en/graphql/overview/explorer). 12 | 13 | NOTE: GraphiQL does not work on linux now 14 | https://github.com/skevy/graphiql-app/issues/175 15 | -------------------------------------------------------------------------------- /app/overlay/remove/usecase.go: -------------------------------------------------------------------------------- 1 | package remove 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/kyoh86/gogh/v4/core/overlay" 7 | ) 8 | 9 | // Usecase represents the create use case 10 | type Usecase struct { 11 | overlayService overlay.OverlayService 12 | } 13 | 14 | func NewUsecase(overlayService overlay.OverlayService) *Usecase { 15 | return &Usecase{ 16 | overlayService: overlayService, 17 | } 18 | } 19 | 20 | func (uc *Usecase) Execute(ctx context.Context, overlayID string) error { 21 | return uc.overlayService.Remove(ctx, overlayID) 22 | } 23 | -------------------------------------------------------------------------------- /doc/usage/gogh_extra_show.md: -------------------------------------------------------------------------------- 1 | ## gogh extra show 2 | 3 | Show details of an extra 4 | 5 | ### Synopsis 6 | 7 | Show detailed information about an extra. 8 | 9 | You can specify either an extra ID or name (for named extras). 10 | Use --json to output in JSON format. 11 | 12 | ``` 13 | gogh extra show [flags] 14 | ``` 15 | 16 | ### Options 17 | 18 | ``` 19 | -h, --help help for show 20 | -j, --json Output in JSON format 21 | ``` 22 | 23 | ### SEE ALSO 24 | 25 | * [gogh extra](gogh_extra.md) - Manage repository extra files 26 | 27 | -------------------------------------------------------------------------------- /doc/usage/gogh_overlay_update.md: -------------------------------------------------------------------------------- 1 | ## gogh overlay update 2 | 3 | Update an existing overlay 4 | 5 | ``` 6 | gogh overlay update [flags] 7 | ``` 8 | 9 | ### Options 10 | 11 | ``` 12 | -h, --help help for update 13 | --name string Name of the overlay 14 | --relative-path string Relative path of the overlay in the repository 15 | --source string Overlay source file path 16 | ``` 17 | 18 | ### SEE ALSO 19 | 20 | * [gogh overlay](gogh_overlay.md) - Manage repository overlay files 21 | 22 | -------------------------------------------------------------------------------- /ui/cli/commands/config_template.txt: -------------------------------------------------------------------------------- 1 | Now gogh is executed in the following context. 2 | 3 | ## Default names 4 | (from {{.defaultNameSource}}) 5 | 6 | Host: {{if ne .defaultHost ""}}{{.defaultHost}}{{else}}github.com{{end}} 7 | Owners: 8 | {{range $host, $owner := .defaultNames}} {{$host}}: {{$owner}} 9 | {{end}} 10 | ## Workspaces 11 | (from {{.workspaceSource}}) 12 | 13 | {{range .roots}} {{.}} 14 | {{end}} 15 | ## Tokens 16 | (from {{.tokenSource}}) 17 | 18 | {{range .tokens}} {{.}} 19 | {{end}} 20 | ## Flags 21 | (from {{.flagsSource}}) 22 | 23 | {{.flags}} 24 | -------------------------------------------------------------------------------- /ui/cli/commands/cwd_test.go: -------------------------------------------------------------------------------- 1 | package commands_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/kyoh86/gogh/v4/app/config" 8 | "github.com/kyoh86/gogh/v4/app/service" 9 | "github.com/kyoh86/gogh/v4/ui/cli/commands" 10 | ) 11 | 12 | func TestNewCwdCommand(t *testing.T) { 13 | // Setup 14 | ctx := context.Background() 15 | serviceSet := &service.ServiceSet{Flags: &config.Flags{}} 16 | 17 | // Execute and verify no error occurs 18 | _, err := commands.NewCwdCommand(ctx, serviceSet) 19 | if err != nil { 20 | t.Fatalf("Expected no error, got %v", err) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /ui/cli/commands/man_test.go: -------------------------------------------------------------------------------- 1 | package commands_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/kyoh86/gogh/v4/app/config" 8 | "github.com/kyoh86/gogh/v4/app/service" 9 | "github.com/kyoh86/gogh/v4/ui/cli/commands" 10 | ) 11 | 12 | func TestNewManCommand(t *testing.T) { 13 | // Setup 14 | ctx := context.Background() 15 | serviceSet := &service.ServiceSet{Flags: &config.Flags{}} 16 | 17 | // Execute and verify no error occurs 18 | _, err := commands.NewManCommand(ctx, serviceSet) 19 | if err != nil { 20 | t.Fatalf("Expected no error, got %v", err) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /ui/cli/commands/clone_test.go: -------------------------------------------------------------------------------- 1 | package commands_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/kyoh86/gogh/v4/app/config" 8 | "github.com/kyoh86/gogh/v4/app/service" 9 | "github.com/kyoh86/gogh/v4/ui/cli/commands" 10 | ) 11 | 12 | func TestNewCloneCommand(t *testing.T) { 13 | // Setup 14 | ctx := context.Background() 15 | serviceSet := &service.ServiceSet{Flags: &config.Flags{}} 16 | 17 | // Execute and verify no error occurs 18 | _, err := commands.NewCloneCommand(ctx, serviceSet) 19 | if err != nil { 20 | t.Fatalf("Expected no error, got %v", err) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /ui/cli/commands/fork_test.go: -------------------------------------------------------------------------------- 1 | package commands_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/kyoh86/gogh/v4/app/config" 8 | "github.com/kyoh86/gogh/v4/app/service" 9 | "github.com/kyoh86/gogh/v4/ui/cli/commands" 10 | ) 11 | 12 | func TestNewForkCommand(t *testing.T) { 13 | // Setup 14 | ctx := context.Background() 15 | serviceSet := &service.ServiceSet{Flags: &config.Flags{}} 16 | 17 | // Execute and verify no error occurs 18 | _, err := commands.NewForkCommand(ctx, serviceSet) 19 | if err != nil { 20 | t.Fatalf("Expected no error, got %v", err) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /ui/cli/commands/list_test.go: -------------------------------------------------------------------------------- 1 | package commands_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/kyoh86/gogh/v4/app/config" 8 | "github.com/kyoh86/gogh/v4/app/service" 9 | "github.com/kyoh86/gogh/v4/ui/cli/commands" 10 | ) 11 | 12 | func TestNewListCommand(t *testing.T) { 13 | // Setup 14 | ctx := context.Background() 15 | serviceSet := &service.ServiceSet{Flags: &config.Flags{}} 16 | 17 | // Execute and verify no error occurs 18 | _, err := commands.NewListCommand(ctx, serviceSet) 19 | if err != nil { 20 | t.Fatalf("Expected no error, got %v", err) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /ui/cli/commands/repos_test.go: -------------------------------------------------------------------------------- 1 | package commands_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/kyoh86/gogh/v4/app/config" 8 | "github.com/kyoh86/gogh/v4/app/service" 9 | "github.com/kyoh86/gogh/v4/ui/cli/commands" 10 | ) 11 | 12 | func TestNewReposCommand(t *testing.T) { 13 | // Setup 14 | ctx := context.Background() 15 | serviceSet := &service.ServiceSet{Flags: &config.Flags{}} 16 | 17 | // Execute and verify no error occurs 18 | _, err := commands.NewReposCommand(ctx, serviceSet) 19 | if err != nil { 20 | t.Fatalf("Expected no error, got %v", err) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /ui/cli/commands/roots_test.go: -------------------------------------------------------------------------------- 1 | package commands_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/kyoh86/gogh/v4/app/config" 8 | "github.com/kyoh86/gogh/v4/app/service" 9 | "github.com/kyoh86/gogh/v4/ui/cli/commands" 10 | ) 11 | 12 | func TestNewRootsCommand(t *testing.T) { 13 | // Setup 14 | ctx := context.Background() 15 | serviceSet := &service.ServiceSet{Flags: &config.Flags{}} 16 | 17 | // Execute and verify no error occurs 18 | _, err := commands.NewRootsCommand(ctx, serviceSet) 19 | if err != nil { 20 | t.Fatalf("Expected no error, got %v", err) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /ui/cli/commands/bundle_test.go: -------------------------------------------------------------------------------- 1 | package commands_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/kyoh86/gogh/v4/app/config" 8 | "github.com/kyoh86/gogh/v4/app/service" 9 | "github.com/kyoh86/gogh/v4/ui/cli/commands" 10 | ) 11 | 12 | func TestNewBundleCommand(t *testing.T) { 13 | // Setup 14 | ctx := context.Background() 15 | serviceSet := &service.ServiceSet{Flags: &config.Flags{}} 16 | 17 | // Execute and verify no error occurs 18 | _, err := commands.NewBundleCommand(ctx, serviceSet) 19 | if err != nil { 20 | t.Fatalf("Expected no error, got %v", err) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /ui/cli/commands/config_test.go: -------------------------------------------------------------------------------- 1 | package commands_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/kyoh86/gogh/v4/app/config" 8 | "github.com/kyoh86/gogh/v4/app/service" 9 | "github.com/kyoh86/gogh/v4/ui/cli/commands" 10 | ) 11 | 12 | func TestNewConfigCommand(t *testing.T) { 13 | // Setup 14 | ctx := context.Background() 15 | serviceSet := &service.ServiceSet{Flags: &config.Flags{}} 16 | 17 | // Execute and verify no error occurs 18 | _, err := commands.NewConfigCommand(ctx, serviceSet) 19 | if err != nil { 20 | t.Fatalf("Expected no error, got %v", err) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /ui/cli/commands/create_test.go: -------------------------------------------------------------------------------- 1 | package commands_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/kyoh86/gogh/v4/app/config" 8 | "github.com/kyoh86/gogh/v4/app/service" 9 | "github.com/kyoh86/gogh/v4/ui/cli/commands" 10 | ) 11 | 12 | func TestNewCreateCommand(t *testing.T) { 13 | // Setup 14 | ctx := context.Background() 15 | serviceSet := &service.ServiceSet{Flags: &config.Flags{}} 16 | 17 | // Execute and verify no error occurs 18 | _, err := commands.NewCreateCommand(ctx, serviceSet) 19 | if err != nil { 20 | t.Fatalf("Expected no error, got %v", err) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /ui/cli/commands/delete_test.go: -------------------------------------------------------------------------------- 1 | package commands_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/kyoh86/gogh/v4/app/config" 8 | "github.com/kyoh86/gogh/v4/app/service" 9 | "github.com/kyoh86/gogh/v4/ui/cli/commands" 10 | ) 11 | 12 | func TestNewDeleteCommand(t *testing.T) { 13 | // Setup 14 | ctx := context.Background() 15 | serviceSet := &service.ServiceSet{Flags: &config.Flags{}} 16 | 17 | // Execute and verify no error occurs 18 | _, err := commands.NewDeleteCommand(ctx, serviceSet) 19 | if err != nil { 20 | t.Fatalf("Expected no error, got %v", err) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /doc/usage/gogh_roots.md: -------------------------------------------------------------------------------- 1 | ## gogh roots 2 | 3 | Manage roots 4 | 5 | ``` 6 | gogh roots [flags] 7 | ``` 8 | 9 | ### Options 10 | 11 | ``` 12 | -h, --help help for roots 13 | ``` 14 | 15 | ### SEE ALSO 16 | 17 | * [gogh](gogh.md) - GO GitHub local repository manager 18 | * [gogh roots add](gogh_roots_add.md) - Add a directory into the roots 19 | * [gogh roots list](gogh_roots_list.md) - List all of the roots 20 | * [gogh roots remove](gogh_roots_remove.md) - Remove a directory from the roots 21 | * [gogh roots set-primary](gogh_roots_set-primary.md) - Set a directory as the primary in the roots 22 | 23 | -------------------------------------------------------------------------------- /ui/cli/commands/auth_list_test.go: -------------------------------------------------------------------------------- 1 | package commands_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/kyoh86/gogh/v4/app/config" 8 | "github.com/kyoh86/gogh/v4/app/service" 9 | "github.com/kyoh86/gogh/v4/ui/cli/commands" 10 | ) 11 | 12 | func TestNewAuthListCommand(t *testing.T) { 13 | // Setup 14 | ctx := context.Background() 15 | serviceSet := &service.ServiceSet{Flags: &config.Flags{}} 16 | 17 | // Execute and verify no error occurs 18 | _, err := commands.NewAuthListCommand(ctx, serviceSet) 19 | if err != nil { 20 | t.Fatalf("Expected no error, got %v", err) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /ui/cli/commands/migrate_test.go: -------------------------------------------------------------------------------- 1 | package commands_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/kyoh86/gogh/v4/app/config" 8 | "github.com/kyoh86/gogh/v4/app/service" 9 | "github.com/kyoh86/gogh/v4/ui/cli/commands" 10 | ) 11 | 12 | func TestNewMigrateCommand(t *testing.T) { 13 | // Setup 14 | ctx := context.Background() 15 | serviceSet := &service.ServiceSet{Flags: &config.Flags{}} 16 | 17 | // Execute and verify no error occurs 18 | _, err := commands.NewMigrateCommand(ctx, serviceSet) 19 | if err != nil { 20 | t.Fatalf("Expected no error, got %v", err) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /ui/cli/commands/roots_add_test.go: -------------------------------------------------------------------------------- 1 | package commands_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/kyoh86/gogh/v4/app/config" 8 | "github.com/kyoh86/gogh/v4/app/service" 9 | "github.com/kyoh86/gogh/v4/ui/cli/commands" 10 | ) 11 | 12 | func TestNewRootsAddCommand(t *testing.T) { 13 | // Setup 14 | ctx := context.Background() 15 | serviceSet := &service.ServiceSet{Flags: &config.Flags{}} 16 | 17 | // Execute and verify no error occurs 18 | _, err := commands.NewRootsAddCommand(ctx, serviceSet) 19 | if err != nil { 20 | t.Fatalf("Expected no error, got %v", err) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /core/store/store.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | type Content interface { 8 | // HasChanges returns true if the content has changes 9 | HasChanges() bool 10 | // MarkSaved marks the content as saved 11 | MarkSaved() 12 | } 13 | 14 | type Loader[T any] interface { 15 | Source() (string, error) 16 | Load(ctx context.Context, initial func() T) (T, error) 17 | } 18 | 19 | type Saver[T Content] interface { 20 | Source() (string, error) 21 | Save(ctx context.Context, v T, force bool) error 22 | } 23 | 24 | type Store[T Content] interface { 25 | Loader[T] 26 | Saver[T] 27 | } 28 | -------------------------------------------------------------------------------- /ui/cli/commands/auth_login_test.go: -------------------------------------------------------------------------------- 1 | package commands_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/kyoh86/gogh/v4/app/config" 8 | "github.com/kyoh86/gogh/v4/app/service" 9 | "github.com/kyoh86/gogh/v4/ui/cli/commands" 10 | ) 11 | 12 | func TestNewAuthLoginCommand(t *testing.T) { 13 | // Setup 14 | ctx := context.Background() 15 | serviceSet := &service.ServiceSet{Flags: &config.Flags{}} 16 | 17 | // Execute and verify no error occurs 18 | _, err := commands.NewAuthLoginCommand(ctx, serviceSet) 19 | if err != nil { 20 | t.Fatalf("Expected no error, got %v", err) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /ui/cli/commands/roots_list_test.go: -------------------------------------------------------------------------------- 1 | package commands_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/kyoh86/gogh/v4/app/config" 8 | "github.com/kyoh86/gogh/v4/app/service" 9 | "github.com/kyoh86/gogh/v4/ui/cli/commands" 10 | ) 11 | 12 | func TestNewRootsListCommand(t *testing.T) { 13 | // Setup 14 | ctx := context.Background() 15 | serviceSet := &service.ServiceSet{Flags: &config.Flags{}} 16 | 17 | // Execute and verify no error occurs 18 | _, err := commands.NewRootsListCommand(ctx, serviceSet) 19 | if err != nil { 20 | t.Fatalf("Expected no error, got %v", err) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /ui/cli/commands/auth_logout_test.go: -------------------------------------------------------------------------------- 1 | package commands_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/kyoh86/gogh/v4/app/config" 8 | "github.com/kyoh86/gogh/v4/app/service" 9 | "github.com/kyoh86/gogh/v4/ui/cli/commands" 10 | ) 11 | 12 | func TestNewAuthLogoutCommand(t *testing.T) { 13 | // Setup 14 | ctx := context.Background() 15 | serviceSet := &service.ServiceSet{Flags: &config.Flags{}} 16 | 17 | // Execute and verify no error occurs 18 | _, err := commands.NewAuthLogoutCommand(ctx, serviceSet) 19 | if err != nil { 20 | t.Fatalf("Expected no error, got %v", err) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /ui/cli/commands/bundle_dump_test.go: -------------------------------------------------------------------------------- 1 | package commands_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/kyoh86/gogh/v4/app/config" 8 | "github.com/kyoh86/gogh/v4/app/service" 9 | "github.com/kyoh86/gogh/v4/ui/cli/commands" 10 | ) 11 | 12 | func TestNewBundleDumpCommand(t *testing.T) { 13 | // Setup 14 | ctx := context.Background() 15 | serviceSet := &service.ServiceSet{Flags: &config.Flags{}} 16 | 17 | // Execute and verify no error occurs 18 | _, err := commands.NewBundleDumpCommand(ctx, serviceSet) 19 | if err != nil { 20 | t.Fatalf("Expected no error, got %v", err) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /ui/cli/commands/config_show_test.go: -------------------------------------------------------------------------------- 1 | package commands_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/kyoh86/gogh/v4/app/config" 8 | "github.com/kyoh86/gogh/v4/app/service" 9 | "github.com/kyoh86/gogh/v4/ui/cli/commands" 10 | ) 11 | 12 | func TestNewConfigShowCommand(t *testing.T) { 13 | // Setup 14 | ctx := context.Background() 15 | serviceSet := &service.ServiceSet{Flags: &config.Flags{}} 16 | 17 | // Execute and verify no error occurs 18 | _, err := commands.NewConfigShowCommand(ctx, serviceSet) 19 | if err != nil { 20 | t.Fatalf("Expected no error, got %v", err) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /ui/cli/commands/overlay_list_test.go: -------------------------------------------------------------------------------- 1 | package commands_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/kyoh86/gogh/v4/app/config" 8 | "github.com/kyoh86/gogh/v4/app/service" 9 | "github.com/kyoh86/gogh/v4/ui/cli/commands" 10 | ) 11 | 12 | func TestNewOverlayListCommand(t *testing.T) { 13 | // Setup 14 | ctx := context.Background() 15 | serviceSet := &service.ServiceSet{Flags: &config.Flags{}} 16 | 17 | // Execute and verify no error occurs 18 | _, err := commands.NewOverlayListCommand(ctx, serviceSet) 19 | if err != nil { 20 | t.Fatalf("Expected no error, got %v", err) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /ui/cli/commands/overlay_show_test.go: -------------------------------------------------------------------------------- 1 | package commands_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/kyoh86/gogh/v4/app/config" 8 | "github.com/kyoh86/gogh/v4/app/service" 9 | "github.com/kyoh86/gogh/v4/ui/cli/commands" 10 | ) 11 | 12 | func TestNewOverlayShowCommand(t *testing.T) { 13 | // Setup 14 | ctx := context.Background() 15 | serviceSet := &service.ServiceSet{Flags: &config.Flags{}} 16 | 17 | // Execute and verify no error occurs 18 | _, err := commands.NewOverlayShowCommand(ctx, serviceSet) 19 | if err != nil { 20 | t.Fatalf("Expected no error, got %v", err) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /ui/cli/commands/roots_remove_test.go: -------------------------------------------------------------------------------- 1 | package commands_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/kyoh86/gogh/v4/app/config" 8 | "github.com/kyoh86/gogh/v4/app/service" 9 | "github.com/kyoh86/gogh/v4/ui/cli/commands" 10 | ) 11 | 12 | func TestNewRootsRemoveCommand(t *testing.T) { 13 | // Setup 14 | ctx := context.Background() 15 | serviceSet := &service.ServiceSet{Flags: &config.Flags{}} 16 | 17 | // Execute and verify no error occurs 18 | _, err := commands.NewRootsRemoveCommand(ctx, serviceSet) 19 | if err != nil { 20 | t.Fatalf("Expected no error, got %v", err) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /doc/usage/gogh_extra_create.md: -------------------------------------------------------------------------------- 1 | ## gogh extra create 2 | 3 | Create a named extra template 4 | 5 | ### Synopsis 6 | 7 | Create a named extra template from overlays. 8 | 9 | This creates a reusable template that can be applied to any repository later. 10 | 11 | ``` 12 | gogh extra create [flags] 13 | ``` 14 | 15 | ### Options 16 | 17 | ``` 18 | -h, --help help for create 19 | -o, --overlay strings Overlay names to include in the extra 20 | -s, --source string Source repository 21 | ``` 22 | 23 | ### SEE ALSO 24 | 25 | * [gogh extra](gogh_extra.md) - Manage repository extra files 26 | 27 | -------------------------------------------------------------------------------- /ui/cli/commands/bundle_restore_test.go: -------------------------------------------------------------------------------- 1 | package commands_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/kyoh86/gogh/v4/app/config" 8 | "github.com/kyoh86/gogh/v4/app/service" 9 | "github.com/kyoh86/gogh/v4/ui/cli/commands" 10 | ) 11 | 12 | func TestNewBundleRestoreCommand(t *testing.T) { 13 | // Setup 14 | ctx := context.Background() 15 | serviceSet := &service.ServiceSet{Flags: &config.Flags{}} 16 | 17 | // Execute and verify no error occurs 18 | _, err := commands.NewBundleRestoreCommand(ctx, serviceSet) 19 | if err != nil { 20 | t.Fatalf("Expected no error, got %v", err) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /ui/cli/commands/overlay_apply_test.go: -------------------------------------------------------------------------------- 1 | package commands_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/kyoh86/gogh/v4/app/config" 8 | "github.com/kyoh86/gogh/v4/app/service" 9 | "github.com/kyoh86/gogh/v4/ui/cli/commands" 10 | ) 11 | 12 | func TestNewOverlayApplyCommand(t *testing.T) { 13 | // Setup 14 | ctx := context.Background() 15 | serviceSet := &service.ServiceSet{Flags: &config.Flags{}} 16 | 17 | // Execute and verify no error occurs 18 | _, err := commands.NewOverlayApplyCommand(ctx, serviceSet) 19 | if err != nil { 20 | t.Fatalf("Expected no error, got %v", err) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /ui/cli/commands/set_default_host_test.go: -------------------------------------------------------------------------------- 1 | package commands_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/kyoh86/gogh/v4/app/config" 8 | "github.com/kyoh86/gogh/v4/app/service" 9 | "github.com/kyoh86/gogh/v4/ui/cli/commands" 10 | ) 11 | 12 | func TestNewSetDefaultHostCommand(t *testing.T) { 13 | // Setup 14 | ctx := context.Background() 15 | serviceSet := &service.ServiceSet{Flags: &config.Flags{}} 16 | 17 | // Execute and verify no error occurs 18 | _, err := commands.NewSetDefaultHostCommand(ctx, serviceSet) 19 | if err != nil { 20 | t.Fatalf("Expected no error, got %v", err) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /doc/usage/gogh_extra_list.md: -------------------------------------------------------------------------------- 1 | ## gogh extra list 2 | 3 | List extras 4 | 5 | ### Synopsis 6 | 7 | List all extras. 8 | 9 | By default, lists all extras in one-line format. 10 | Use --type to filter by extra type (auto, named). 11 | Use --json to output in JSON format. 12 | 13 | ``` 14 | gogh extra list [flags] 15 | ``` 16 | 17 | ### Options 18 | 19 | ``` 20 | -h, --help help for list 21 | -j, --json Output in JSON format 22 | -t, --type string Filter by type (all, auto, named) (default "all") 23 | ``` 24 | 25 | ### SEE ALSO 26 | 27 | * [gogh extra](gogh_extra.md) - Manage repository extra files 28 | 29 | -------------------------------------------------------------------------------- /ui/cli/commands/roots_set_primary_test.go: -------------------------------------------------------------------------------- 1 | package commands_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/kyoh86/gogh/v4/app/config" 8 | "github.com/kyoh86/gogh/v4/app/service" 9 | "github.com/kyoh86/gogh/v4/ui/cli/commands" 10 | ) 11 | 12 | func TestNewRootsSetPrimaryCommand(t *testing.T) { 13 | // Setup 14 | ctx := context.Background() 15 | serviceSet := &service.ServiceSet{Flags: &config.Flags{}} 16 | 17 | // Execute and verify no error occurs 18 | _, err := commands.NewRootsSetPrimaryCommand(ctx, serviceSet) 19 | if err != nil { 20 | t.Fatalf("Expected no error, got %v", err) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /ui/cli/commands/set_default_owner_test.go: -------------------------------------------------------------------------------- 1 | package commands_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/kyoh86/gogh/v4/app/config" 8 | "github.com/kyoh86/gogh/v4/app/service" 9 | "github.com/kyoh86/gogh/v4/ui/cli/commands" 10 | ) 11 | 12 | func TestNewSetDefaultOwnerCommand(t *testing.T) { 13 | // Setup 14 | ctx := context.Background() 15 | serviceSet := &service.ServiceSet{Flags: &config.Flags{}} 16 | 17 | // Execute and verify no error occurs 18 | _, err := commands.NewSetDefaultOwnerCommand(ctx, serviceSet) 19 | if err != nil { 20 | t.Fatalf("Expected no error, got %v", err) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /doc/usage/gogh_extra_apply.md: -------------------------------------------------------------------------------- 1 | ## gogh extra apply 2 | 3 | Apply a named extra to a repository 4 | 5 | ### Synopsis 6 | 7 | Apply a named extra template to a repository. 8 | 9 | This applies all overlays in the named extra to the target repository. 10 | By default, it applies to the current directory's repository. 11 | 12 | ``` 13 | gogh extra apply [flags] 14 | ``` 15 | 16 | ### Options 17 | 18 | ``` 19 | -h, --help help for apply 20 | -t, --target string Target repository (default: current directory) 21 | ``` 22 | 23 | ### SEE ALSO 24 | 25 | * [gogh extra](gogh_extra.md) - Manage repository extra files 26 | 27 | -------------------------------------------------------------------------------- /doc/usage/gogh_hook.md: -------------------------------------------------------------------------------- 1 | ## gogh hook 2 | 3 | Manage repository hooks 4 | 5 | ### Options 6 | 7 | ``` 8 | -h, --help help for hook 9 | ``` 10 | 11 | ### SEE ALSO 12 | 13 | * [gogh](gogh.md) - GO GitHub local repository manager 14 | * [gogh hook add](gogh_hook_add.md) - Add a new hook 15 | * [gogh hook invoke](gogh_hook_invoke.md) - Manually invoke a hook for a repository 16 | * [gogh hook list](gogh_hook_list.md) - List registered hooks 17 | * [gogh hook remove](gogh_hook_remove.md) - Remove a registered hook 18 | * [gogh hook show](gogh_hook_show.md) - Show a hook 19 | * [gogh hook update](gogh_hook_update.md) - Update an existing hook 20 | 21 | -------------------------------------------------------------------------------- /ui/cli/commands/extra.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/kyoh86/gogh/v4/app/service" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func NewExtraCommand(_ context.Context, _ *service.ServiceSet) (*cobra.Command, error) { 11 | cmd := &cobra.Command{ 12 | Use: "extra", 13 | Short: "Manage repository extra files", 14 | Long: `Manage extra files that are typically ignored by git. 15 | 16 | There are two types of extra: 17 | 1. Auto-apply extra: Automatically applied when cloning the repository 18 | 2. Named extra: Templates that can be manually applied to any repository`, 19 | } 20 | return cmd, nil 21 | } 22 | -------------------------------------------------------------------------------- /core/script/service.go: -------------------------------------------------------------------------------- 1 | package script 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "iter" 7 | 8 | "github.com/kyoh86/gogh/v4/core/store" 9 | ) 10 | 11 | // ScriptService defines the hook management interface 12 | type ScriptService interface { 13 | store.Content 14 | 15 | List() iter.Seq2[Script, error] 16 | Add(ctx context.Context, entry Entry) (id string, _ error) 17 | Get(ctx context.Context, idlike string) (Script, error) 18 | Update(ctx context.Context, idlike string, entry Entry) error 19 | Remove(ctx context.Context, idlike string) error 20 | Open(ctx context.Context, idlike string) (io.ReadCloser, error) 21 | Load(iter.Seq2[Script, error]) error 22 | } 23 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "gomod" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | - package-ecosystem: "github-actions" 13 | directory: "/" 14 | schedule: 15 | interval: "weekly" 16 | -------------------------------------------------------------------------------- /app/repoprint/repotab/time.go: -------------------------------------------------------------------------------- 1 | package repotab 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "time" 7 | ) 8 | 9 | func FuzzyAgoAbbr(now time.Time, at time.Time) string { 10 | // Handle future dates 11 | if at.After(now) { 12 | return "now" 13 | } 14 | 15 | ago := now.Sub(at) 16 | if ago < time.Minute { 17 | return "now" 18 | } 19 | if ago < time.Hour { 20 | return fmt.Sprintf("%dm", int(math.Round(ago.Minutes()))) 21 | } 22 | if ago < 24*time.Hour { 23 | return fmt.Sprintf("%dh", int(math.Round(ago.Hours()))) 24 | } 25 | if ago < 30*24*time.Hour { 26 | return fmt.Sprintf("%dd", int(math.Round(ago.Hours()/24))) 27 | } 28 | return at.Format("2006-01-02") 29 | } 30 | -------------------------------------------------------------------------------- /app/script/add/usecase.go: -------------------------------------------------------------------------------- 1 | package add 2 | 3 | import ( 4 | "context" 5 | "io" 6 | 7 | "github.com/kyoh86/gogh/v4/core/script" 8 | ) 9 | 10 | type Usecase struct { 11 | scriptService script.ScriptService 12 | } 13 | 14 | func NewUsecase(scriptService script.ScriptService) *Usecase { 15 | return &Usecase{scriptService: scriptService} 16 | } 17 | 18 | func (uc *Usecase) Execute(ctx context.Context, name string, content io.Reader) (script.Script, error) { 19 | e := script.Entry{ 20 | Name: name, 21 | Content: content, 22 | } 23 | id, err := uc.scriptService.Add(ctx, e) 24 | if err != nil { 25 | return nil, err 26 | } 27 | return uc.scriptService.Get(ctx, id) 28 | } 29 | -------------------------------------------------------------------------------- /doc/usage/gogh_hook_add.md: -------------------------------------------------------------------------------- 1 | ## gogh hook add 2 | 3 | Add a new hook 4 | 5 | ``` 6 | gogh hook add [flags] 7 | ``` 8 | 9 | ### Options 10 | 11 | ``` 12 | -h, --help help for add 13 | --name string Name of the hook 14 | --operation-id string Operation resource ID 15 | --operation-type string Operation type; it can accept "overlay" or "script" 16 | --repo-pattern string Repository pattern 17 | --trigger-event string event that triggers the hook; it can accept "", "post-clone", "post-fork" or "post-create" 18 | ``` 19 | 20 | ### SEE ALSO 21 | 22 | * [gogh hook](gogh_hook.md) - Manage repository hooks 23 | 24 | -------------------------------------------------------------------------------- /core/repository/reference_json.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import "encoding/json" 4 | 5 | type referenceMarshalable struct { 6 | Host string `json:"host"` 7 | Owner string `json:"owner"` 8 | Name string `json:"name"` 9 | } 10 | 11 | func (r *Reference) UnmarshalJSON(b []byte) error { 12 | var m referenceMarshalable 13 | if err := json.Unmarshal(b, &m); err != nil { 14 | return err 15 | } 16 | r.host = m.Host 17 | r.owner = m.Owner 18 | r.name = m.Name 19 | return nil 20 | } 21 | 22 | func (r Reference) MarshalJSON() ([]byte, error) { 23 | m := referenceMarshalable{ 24 | Host: r.host, 25 | Owner: r.owner, 26 | Name: r.name, 27 | } 28 | return json.Marshal(m) 29 | } 30 | -------------------------------------------------------------------------------- /doc/usage/gogh_config_roots.md: -------------------------------------------------------------------------------- 1 | ## gogh config roots 2 | 3 | Manage roots 4 | 5 | ``` 6 | gogh config roots [flags] 7 | ``` 8 | 9 | ### Options 10 | 11 | ``` 12 | -h, --help help for roots 13 | ``` 14 | 15 | ### SEE ALSO 16 | 17 | * [gogh config](gogh_config.md) - Show/change configurations 18 | * [gogh config roots add](gogh_config_roots_add.md) - Add a directory into the roots 19 | * [gogh config roots list](gogh_config_roots_list.md) - List all of the roots 20 | * [gogh config roots remove](gogh_config_roots_remove.md) - Remove a directory from the roots 21 | * [gogh config roots set-primary](gogh_config_roots_set-primary.md) - Set a directory as the primary in the roots 22 | 23 | -------------------------------------------------------------------------------- /ui/cli/commands/hook_remove.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/kyoh86/gogh/v4/app/hook/remove" 7 | "github.com/kyoh86/gogh/v4/app/service" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | func NewHookRemoveCommand(_ context.Context, svc *service.ServiceSet) (*cobra.Command, error) { 12 | cmd := &cobra.Command{ 13 | Use: "remove [flags] ", 14 | Short: "Remove a registered hook", 15 | Args: cobra.ExactArgs(1), 16 | RunE: func(cmd *cobra.Command, args []string) error { 17 | ctx := cmd.Context() 18 | hookID := args[0] 19 | return remove.NewUsecase(svc.HookService).Execute(ctx, hookID) 20 | }, 21 | } 22 | return cmd, nil 23 | } 24 | -------------------------------------------------------------------------------- /ui/cli/view/try_clone_notify.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/apex/log" 7 | "github.com/kyoh86/gogh/v4/app/clone/try" 8 | ) 9 | 10 | // TryCloneNotify is a wrapper for the TryCloneNotify function to log the status. 11 | func TryCloneNotify( 12 | ctx context.Context, 13 | notify try.Notify, 14 | ) try.Notify { 15 | return func(n try.Status) error { 16 | switch n { 17 | case try.StatusEmpty: 18 | log.FromContext(ctx).Info("created empty repository") 19 | case try.StatusRetry: 20 | log.FromContext(ctx).Info("waiting the remote repository is ready") 21 | } 22 | if notify != nil { 23 | return notify(n) 24 | } 25 | return nil 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /doc/usage/gogh_extra_remove.md: -------------------------------------------------------------------------------- 1 | ## gogh extra remove 2 | 3 | Remove an extra 4 | 5 | ### Synopsis 6 | 7 | Remove an extra. 8 | 9 | You can remove by: 10 | - ID: Use --id flag 11 | - Name (for named extras): Use --name flag 12 | - Repository (for auto extras): Use --repository flag 13 | 14 | ``` 15 | gogh extra remove [flags] 16 | ``` 17 | 18 | ### Options 19 | 20 | ``` 21 | -h, --help help for remove 22 | -i, --id string Extra ID to remove 23 | -n, --name string Named extra to remove 24 | -r, --repository string Repository whose auto extra to remove 25 | ``` 26 | 27 | ### SEE ALSO 28 | 29 | * [gogh extra](gogh_extra.md) - Manage repository extra files 30 | 31 | -------------------------------------------------------------------------------- /doc/usage/gogh_hook_update.md: -------------------------------------------------------------------------------- 1 | ## gogh hook update 2 | 3 | Update an existing hook 4 | 5 | ``` 6 | gogh hook update [flags] 7 | ``` 8 | 9 | ### Options 10 | 11 | ``` 12 | -h, --help help for update 13 | --name string Name of the hook 14 | --operation-id string Operation resource ID 15 | --operation-type string Operation type; it can accept "overlay" or "script" 16 | --repo-pattern string Repository pattern 17 | --trigger-event string event to hook automatically; it can accept "post-clone", "post-fork" or "post-create" 18 | ``` 19 | 20 | ### SEE ALSO 21 | 22 | * [gogh hook](gogh_hook.md) - Manage repository hooks 23 | 24 | -------------------------------------------------------------------------------- /app/overlay/add/usecase.go: -------------------------------------------------------------------------------- 1 | package add 2 | 3 | import ( 4 | "context" 5 | "io" 6 | 7 | "github.com/kyoh86/gogh/v4/core/overlay" 8 | ) 9 | 10 | // Usecase represents the create use case 11 | type Usecase struct { 12 | overlayService overlay.OverlayService 13 | } 14 | 15 | func NewUsecase(overlayService overlay.OverlayService) *Usecase { 16 | return &Usecase{ 17 | overlayService: overlayService, 18 | } 19 | } 20 | 21 | func (uc *Usecase) Execute(ctx context.Context, name, relativePath string, content io.Reader) (string, error) { 22 | e := overlay.Entry{ 23 | Name: name, 24 | RelativePath: relativePath, 25 | Content: content, 26 | } 27 | return uc.overlayService.Add(ctx, e) 28 | } 29 | -------------------------------------------------------------------------------- /core/hook/service.go: -------------------------------------------------------------------------------- 1 | package hook 2 | 3 | import ( 4 | "context" 5 | "iter" 6 | 7 | "github.com/kyoh86/gogh/v4/core/repository" 8 | "github.com/kyoh86/gogh/v4/core/store" 9 | ) 10 | 11 | // HookService defines the hook management interface 12 | type HookService interface { 13 | store.Content 14 | 15 | List() iter.Seq2[Hook, error] 16 | ListFor(reference repository.Reference, event Event) iter.Seq2[Hook, error] 17 | Add(ctx context.Context, entry Entry) (id string, _ error) 18 | Get(ctx context.Context, idlike string) (Hook, error) 19 | Update(ctx context.Context, idlike string, entry Entry) error 20 | Remove(ctx context.Context, idlike string) error 21 | Load(iter.Seq2[Hook, error]) error 22 | } 23 | -------------------------------------------------------------------------------- /arch-go.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | dependenciesRules: 3 | - package: "typ.**" 4 | shouldNotDependsOn: 5 | internal: [] 6 | - package: "cmd.**" 7 | shouldNotDependsOn: 8 | internal: [] 9 | - package: "core.**" 10 | shouldNotDependsOn: 11 | internal: 12 | - "app.**" 13 | - "infra.**" 14 | - "ui.**" 15 | - package: "app.**" 16 | shouldNotDependsOn: 17 | internal: 18 | - "infra.**" 19 | - "ui.**" 20 | - package: "infra.**" 21 | shouldNotDependsOn: 22 | internal: 23 | - "app.**" 24 | - "ui.**" 25 | - package: "ui.**" 26 | shouldNotDependsOn: 27 | internal: 28 | - "core.**" 29 | - "infra.**" 30 | -------------------------------------------------------------------------------- /infra/githubv4/Makefile: -------------------------------------------------------------------------------- 1 | generated.go: schema.graphql genqlient.graphql genqlient.yaml graphql.config.toml 2 | go tool genqlient 3 | 4 | # Smart download function that only updates if content changed 5 | define smart_download 6 | @echo "Downloading $(2)..." 7 | @mkdir -p $$(dirname $(2)) 8 | @curl -sSL $(1) -o $(2).tmp 9 | @if [ ! -f $(2) ] || ! cmp -s $(2) $(2).tmp; then \ 10 | mv $(2).tmp $(2); \ 11 | echo "Updated $(2)"; \ 12 | else \ 13 | rm $(2).tmp; \ 14 | echo "No changes to $(2)"; \ 15 | fi 16 | endef 17 | 18 | schema.graphql: 19 | $(call smart_download,https://docs.github.com/public/fpt/schema.docs.graphql,./schema.graphql) 20 | 21 | .PHONY: clean 22 | clean: 23 | rm -f ./generated.go 24 | rm -f ./schema.graphql 25 | -------------------------------------------------------------------------------- /doc/usage/gogh_config.md: -------------------------------------------------------------------------------- 1 | ## gogh config 2 | 3 | Show/change configurations 4 | 5 | ### Options 6 | 7 | ``` 8 | -h, --help help for config 9 | ``` 10 | 11 | ### SEE ALSO 12 | 13 | * [gogh](gogh.md) - GO GitHub local repository manager 14 | * [gogh config auth](gogh_config_auth.md) - Manage tokens 15 | * [gogh config migrate](gogh_config_migrate.md) - Migrate configurations 16 | * [gogh config roots](gogh_config_roots.md) - Manage roots 17 | * [gogh config set-default-host](gogh_config_set-default-host.md) - Set the default host for the repository 18 | * [gogh config set-default-owner](gogh_config_set-default-owner.md) - Set the default owner for a host for the repository 19 | * [gogh config show](gogh_config_show.md) - Show configurations 20 | 21 | -------------------------------------------------------------------------------- /app/script/update/usecase.go: -------------------------------------------------------------------------------- 1 | package update 2 | 3 | import ( 4 | "context" 5 | "io" 6 | 7 | "github.com/kyoh86/gogh/v4/core/script" 8 | ) 9 | 10 | // Usecase is a struct that encapsulates the script service for updating scripts. 11 | type Usecase struct { 12 | scriptService script.ScriptService 13 | } 14 | 15 | // NewUsecase creates a new instance of Usecase for updating scripts. 16 | func NewUsecase(scriptService script.ScriptService) *Usecase { 17 | return &Usecase{scriptService: scriptService} 18 | } 19 | 20 | // Execute applies a new script identified by its ID. 21 | func (uc *Usecase) Execute(ctx context.Context, scriptID, name string, content io.Reader) error { 22 | return uc.scriptService.Update(ctx, scriptID, script.Entry{ 23 | Name: name, 24 | Content: content, 25 | }) 26 | } 27 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | linters: 3 | enable: 4 | - bodyclose 5 | - dogsled 6 | - gocritic 7 | - godox 8 | - misspell 9 | - nakedret 10 | - staticcheck 11 | - unconvert 12 | - unparam 13 | - whitespace 14 | settings: 15 | gocritic: 16 | disabled-checks: 17 | - elseif 18 | exclusions: 19 | generated: lax 20 | presets: 21 | - comments 22 | - common-false-positives 23 | - legacy 24 | - std-error-handling 25 | paths: 26 | - third_party$ 27 | - builtin$ 28 | - examples$ 29 | issues: 30 | max-same-issues: 0 31 | formatters: 32 | enable: 33 | - gofmt 34 | - goimports 35 | exclusions: 36 | generated: lax 37 | paths: 38 | - third_party$ 39 | - builtin$ 40 | - examples$ 41 | -------------------------------------------------------------------------------- /doc/usage/gogh_completion_powershell.md: -------------------------------------------------------------------------------- 1 | ## gogh completion powershell 2 | 3 | Generate the autocompletion script for powershell 4 | 5 | ### Synopsis 6 | 7 | Generate the autocompletion script for powershell. 8 | 9 | To load completions in your current shell session: 10 | 11 | gogh completion powershell | Out-String | Invoke-Expression 12 | 13 | To load completions for every new session, add the output of the above command 14 | to your powershell profile. 15 | 16 | 17 | ``` 18 | gogh completion powershell [flags] 19 | ``` 20 | 21 | ### Options 22 | 23 | ``` 24 | -h, --help help for powershell 25 | --no-descriptions disable completion descriptions 26 | ``` 27 | 28 | ### SEE ALSO 29 | 30 | * [gogh completion](gogh_completion.md) - Generate the autocompletion script for the specified shell 31 | 32 | -------------------------------------------------------------------------------- /doc/usage/gogh_bundle_restore.md: -------------------------------------------------------------------------------- 1 | ## gogh bundle restore 2 | 3 | Get dumped local repositoiries 4 | 5 | ``` 6 | gogh bundle restore [flags] 7 | ``` 8 | 9 | ### Options 10 | 11 | ``` 12 | --clone-retry-limit int The number of retries to clone a repository (default 3) 13 | --clone-retry-timeout duration Timeout for each clone attempt (default 5m0s) 14 | --dry-run Displays the operations that would be performed using the specified command without actually running them 15 | -f, --file string Read the file as input; if it's empty("") or hyphen("-"), read from stdin (default "/home/kyoh86/.config/gogh/bundle.txt") 16 | -h, --help help for restore 17 | ``` 18 | 19 | ### SEE ALSO 20 | 21 | * [gogh bundle](gogh_bundle.md) - Manage bundle 22 | 23 | -------------------------------------------------------------------------------- /ui/cli/commands/hook_show.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/kyoh86/gogh/v4/app/hook/show" 7 | "github.com/kyoh86/gogh/v4/app/service" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | func NewHookShowCommand(_ context.Context, svc *service.ServiceSet) (*cobra.Command, error) { 12 | var f struct { 13 | json bool 14 | source bool 15 | } 16 | cmd := &cobra.Command{ 17 | Use: "show [flags] ", 18 | Short: "Show a hook", 19 | Args: cobra.ExactArgs(1), 20 | RunE: func(cmd *cobra.Command, args []string) error { 21 | ctx := cmd.Context() 22 | hookID := args[0] 23 | return show.NewUsecase(svc.HookService, cmd.OutOrStdout()).Execute(ctx, hookID, f.json) 24 | }, 25 | } 26 | cmd.Flags().BoolVarP(&f.json, "json", "", false, "Output in JSON format") 27 | return cmd, nil 28 | } 29 | -------------------------------------------------------------------------------- /core/gogh/constants_test.go: -------------------------------------------------------------------------------- 1 | package gogh_test 2 | 3 | import ( 4 | "testing" 5 | 6 | testtarget "github.com/kyoh86/gogh/v4/core/gogh" 7 | ) 8 | 9 | func TestConstants(t *testing.T) { 10 | // Test DefaultHost constant 11 | if testtarget.DefaultHost != "github.com" { 12 | t.Errorf("DefaultHost = %q, want %q", testtarget.DefaultHost, "github.com") 13 | } 14 | 15 | // Test AppName constant 16 | if testtarget.AppName != "gogh" { 17 | t.Errorf("AppName = %q, want %q", testtarget.AppName, "gogh") 18 | } 19 | } 20 | 21 | func TestConstantsUsage(t *testing.T) { 22 | // Demonstrate usage of constants 23 | t.Logf("Application name: %s", testtarget.AppName) 24 | t.Logf("Default host: %s", testtarget.DefaultHost) 25 | 26 | // Verify constants are exported and accessible 27 | _ = testtarget.DefaultHost 28 | _ = testtarget.AppName 29 | } 30 | -------------------------------------------------------------------------------- /core/overlay/service.go: -------------------------------------------------------------------------------- 1 | package overlay 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "iter" 7 | 8 | "github.com/kyoh86/gogh/v4/core/store" 9 | ) 10 | 11 | // OverlayService interface extends store.Content and provides overlay management logic. 12 | type OverlayService interface { 13 | store.Content 14 | 15 | List() iter.Seq2[Overlay, error] 16 | Add(ctx context.Context, entry Entry) (id string, _ error) 17 | // Get retrieves an overlay by its ID-like string. 18 | // If multiple overlays match or no overlay matches, it should return the error. 19 | Get(ctx context.Context, idlike string) (Overlay, error) 20 | Update(ctx context.Context, idlike string, entry Entry) error 21 | Remove(ctx context.Context, idlike string) error 22 | Open(ctx context.Context, idlike string) (io.ReadCloser, error) 23 | Load(iter.Seq2[Overlay, error]) error 24 | } 25 | -------------------------------------------------------------------------------- /doc/usage/gogh_completion_fish.md: -------------------------------------------------------------------------------- 1 | ## gogh completion fish 2 | 3 | Generate the autocompletion script for fish 4 | 5 | ### Synopsis 6 | 7 | Generate the autocompletion script for the fish shell. 8 | 9 | To load completions in your current shell session: 10 | 11 | gogh completion fish | source 12 | 13 | To load completions for every new session, execute once: 14 | 15 | gogh completion fish > ~/.config/fish/completions/gogh.fish 16 | 17 | You will need to start a new shell for this setup to take effect. 18 | 19 | 20 | ``` 21 | gogh completion fish [flags] 22 | ``` 23 | 24 | ### Options 25 | 26 | ``` 27 | -h, --help help for fish 28 | --no-descriptions disable completion descriptions 29 | ``` 30 | 31 | ### SEE ALSO 32 | 33 | * [gogh completion](gogh_completion.md) - Generate the autocompletion script for the specified shell 34 | 35 | -------------------------------------------------------------------------------- /typ/tristate.go: -------------------------------------------------------------------------------- 1 | package typ 2 | 3 | import "fmt" 4 | 5 | // Tristate represents a filter state for boolean repository attributes 6 | type Tristate int 7 | 8 | const ( 9 | // TristateZero indicates no filtering should be applied 10 | TristateZero Tristate = iota 11 | 12 | // TristateTrue filters for repositories where the attribute is true 13 | TristateTrue 14 | // TristateFalse filters for repositories where the attribute is false 15 | TristateFalse 16 | ) 17 | 18 | // AsBoolPtr converts the BooleanFilter to a pointer to a boolean value 19 | func (f Tristate) AsBoolPtr() (*bool, error) { 20 | var r *bool 21 | if err := Remap(&r, map[Tristate]*bool{ 22 | TristateZero: nil, 23 | TristateTrue: Ptr(true), 24 | TristateFalse: Ptr(false), 25 | }, f); err != nil { 26 | return nil, fmt.Errorf("invalid Tristate: %w", err) 27 | } 28 | return r, nil 29 | } 30 | -------------------------------------------------------------------------------- /ui/cli/commands/set_default_host.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/apex/log" 8 | "github.com/kyoh86/gogh/v4/app/service" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | func NewSetDefaultHostCommand(_ context.Context, svc *service.ServiceSet) (*cobra.Command, error) { 13 | cmd := &cobra.Command{ 14 | Use: "set-default-host [flags] ", 15 | Short: "Set the default host for the repository", 16 | Args: cobra.ExactArgs(1), 17 | RunE: func(cmd *cobra.Command, args []string) error { 18 | ctx := cmd.Context() 19 | host := args[0] 20 | if err := svc.DefaultNameService.SetDefaultHost(host); err != nil { 21 | return fmt.Errorf("setting default host: %w", err) 22 | } 23 | log.FromContext(ctx).Infof("Default host set to %s\n", host) 24 | return nil 25 | }, 26 | } 27 | return cmd, nil 28 | } 29 | -------------------------------------------------------------------------------- /app/overlay/update/usecase.go: -------------------------------------------------------------------------------- 1 | package update 2 | 3 | import ( 4 | "context" 5 | "io" 6 | 7 | "github.com/kyoh86/gogh/v4/core/overlay" 8 | ) 9 | 10 | // Usecase is a struct that encapsulates the overlay service for updating overlays. 11 | type Usecase struct { 12 | overlayService overlay.OverlayService 13 | } 14 | 15 | // NewUsecase creates a new instance of Usecase for updating overlays. 16 | func NewUsecase(overlayService overlay.OverlayService) *Usecase { 17 | return &Usecase{overlayService: overlayService} 18 | } 19 | 20 | // Execute applies a new overlay identified by its ID. 21 | func (uc *Usecase) Execute(ctx context.Context, overlayID, name, relativePath string, content io.Reader) error { 22 | return uc.overlayService.Update(ctx, overlayID, overlay.Entry{ 23 | Name: name, 24 | RelativePath: relativePath, 25 | Content: content, 26 | }) 27 | } 28 | -------------------------------------------------------------------------------- /ui/cli/commands/script_list.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/kyoh86/gogh/v4/app/script/list" 7 | "github.com/kyoh86/gogh/v4/app/service" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | func NewScriptListCommand(_ context.Context, svc *service.ServiceSet) (*cobra.Command, error) { 12 | var f struct { 13 | json bool 14 | source bool 15 | } 16 | cmd := &cobra.Command{ 17 | Use: "list", 18 | Short: "List registered scripts", 19 | Args: cobra.NoArgs, 20 | RunE: func(cmd *cobra.Command, args []string) error { 21 | return list.NewUsecase(svc.ScriptService, cmd.OutOrStdout()).Execute(cmd.Context(), f.json, f.source) 22 | }, 23 | } 24 | cmd.Flags().BoolVarP(&f.json, "json", "", false, "Output in JSON format") 25 | cmd.Flags().BoolVarP(&f.source, "source", "", false, "Output with source code") 26 | return cmd, nil 27 | } 28 | -------------------------------------------------------------------------------- /ui/cli/commands/overlay_list.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/kyoh86/gogh/v4/app/overlay/list" 7 | "github.com/kyoh86/gogh/v4/app/service" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | func NewOverlayListCommand(_ context.Context, svc *service.ServiceSet) (*cobra.Command, error) { 12 | var f struct { 13 | json bool 14 | source bool 15 | } 16 | cmd := &cobra.Command{ 17 | Use: "list", 18 | Short: "List registered overlays", 19 | Args: cobra.NoArgs, 20 | RunE: func(cmd *cobra.Command, args []string) error { 21 | return list.NewUsecase(svc.OverlayService, cmd.OutOrStdout()).Execute(cmd.Context(), f.json, f.source) 22 | }, 23 | } 24 | cmd.Flags().BoolVarP(&f.json, "json", "", false, "Output in JSON format") 25 | cmd.Flags().BoolVarP(&f.source, "source", "", false, "Output with source code") 26 | return cmd, nil 27 | } 28 | -------------------------------------------------------------------------------- /ui/cli/commands/edit.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "os" 5 | "os/exec" 6 | 7 | "github.com/cli/safeexec" 8 | "github.com/kballard/go-shellquote" 9 | ) 10 | 11 | func lookPath(name string) ([]string, error) { 12 | exe, err := safeexec.LookPath(name) 13 | if err != nil { 14 | return nil, err 15 | } 16 | return []string{exe}, nil 17 | } 18 | 19 | func edit(editor, fileName string) error { 20 | words, err := shellquote.Split(editor) 21 | if err != nil { 22 | return err 23 | } 24 | words = append(words, fileName) 25 | editorExe, err := lookPath(words[0]) 26 | if err != nil { 27 | return err 28 | } 29 | words = append(editorExe, words[1:]...) 30 | 31 | cmdEdit := exec.Command(words[0], words[1:]...) 32 | cmdEdit.Env = os.Environ() 33 | cmdEdit.Stdin = os.Stdin 34 | cmdEdit.Stdout = os.Stdout 35 | cmdEdit.Stderr = os.Stderr 36 | return cmdEdit.Run() 37 | } 38 | -------------------------------------------------------------------------------- /ui/cli/commands/hook_list.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/kyoh86/gogh/v4/app/hook/list" 7 | "github.com/kyoh86/gogh/v4/app/service" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | func NewHookListCommand(_ context.Context, svc *service.ServiceSet) (*cobra.Command, error) { 12 | var f struct { 13 | json bool 14 | source bool 15 | } 16 | cmd := &cobra.Command{ 17 | Use: "list", 18 | Short: "List registered hooks", 19 | Args: cobra.NoArgs, 20 | RunE: func(cmd *cobra.Command, args []string) error { 21 | usecase := list.NewUsecase(svc.HookService, cmd.OutOrStdout()) 22 | return usecase.Execute(cmd.Context(), f.json) 23 | }, 24 | } 25 | cmd.Flags().BoolVarP(&f.json, "json", "", false, "Output in JSON format") 26 | cmd.Flags().BoolVarP(&f.source, "source", "", false, "Output with source code") 27 | return cmd, nil 28 | } 29 | -------------------------------------------------------------------------------- /doc/usage/gogh_completion.md: -------------------------------------------------------------------------------- 1 | ## gogh completion 2 | 3 | Generate the autocompletion script for the specified shell 4 | 5 | ### Synopsis 6 | 7 | Generate the autocompletion script for gogh for the specified shell. 8 | See each sub-command's help for details on how to use the generated script. 9 | 10 | 11 | ### Options 12 | 13 | ``` 14 | -h, --help help for completion 15 | ``` 16 | 17 | ### SEE ALSO 18 | 19 | * [gogh](gogh.md) - GO GitHub local repository manager 20 | * [gogh completion bash](gogh_completion_bash.md) - Generate the autocompletion script for bash 21 | * [gogh completion fish](gogh_completion_fish.md) - Generate the autocompletion script for fish 22 | * [gogh completion powershell](gogh_completion_powershell.md) - Generate the autocompletion script for powershell 23 | * [gogh completion zsh](gogh_completion_zsh.md) - Generate the autocompletion script for zsh 24 | 25 | -------------------------------------------------------------------------------- /core/fs/tilde.go: -------------------------------------------------------------------------------- 1 | package fs 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | ) 8 | 9 | // ReplaceTildeWithHome replaces a tilde (~) at the beginning of a path with the user's home directory. 10 | func ReplaceTildeWithHome(p string) (string, error) { 11 | runes := []rune(p) 12 | switch len(runes) { 13 | case 0: 14 | return p, nil 15 | case 1: 16 | if runes[0] == '~' { 17 | homeDir, err := os.UserHomeDir() 18 | if err != nil { 19 | return "", fmt.Errorf("searching user home dir: %w", err) 20 | } 21 | return homeDir, nil 22 | } 23 | default: 24 | if runes[0] == '~' && (runes[1] == filepath.Separator || runes[1] == '/') { 25 | homeDir, err := os.UserHomeDir() 26 | if err != nil { 27 | return "", fmt.Errorf("searching user home dir: %w", err) 28 | } 29 | return filepath.Join(homeDir, string(runes[2:])), nil 30 | } 31 | } 32 | return p, nil 33 | } 34 | -------------------------------------------------------------------------------- /doc/usage/gogh_hook_invoke.md: -------------------------------------------------------------------------------- 1 | ## gogh hook invoke 2 | 3 | Manually invoke a hook for a repository 4 | 5 | ``` 6 | gogh hook invoke [flags] [[/]/] 7 | ``` 8 | 9 | ### Examples 10 | 11 | ``` 12 | invoke github.com/owner/repo 13 | invoke owner/repo 14 | invoke repo 15 | invoke . # Use current directory repository 16 | 17 | It accepts a short notation for each repository 18 | (for example, "github.com/kyoh86/example") like below. 19 | - "": e.g. "example" 20 | - "/": e.g. "kyoh86/example" 21 | - "." for the current directory repository 22 | They'll be completed with the default host and owner set by "config set-default{-host|-owner}". 23 | ``` 24 | 25 | ### Options 26 | 27 | ``` 28 | -h, --help help for invoke 29 | ``` 30 | 31 | ### SEE ALSO 32 | 33 | * [gogh hook](gogh_hook.md) - Manage repository hooks 34 | 35 | -------------------------------------------------------------------------------- /ui/cli/commands/set_default_owner.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/apex/log" 8 | "github.com/kyoh86/gogh/v4/app/service" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | func NewSetDefaultOwnerCommand(_ context.Context, svc *service.ServiceSet) (*cobra.Command, error) { 13 | cmd := &cobra.Command{ 14 | Use: "set-default-owner [flags] ", 15 | Short: "Set the default owner for a host for the repository", 16 | Args: cobra.ExactArgs(2), 17 | RunE: func(cmd *cobra.Command, args []string) error { 18 | ctx := cmd.Context() 19 | host, owner := args[0], args[1] 20 | if err := svc.DefaultNameService.SetDefaultOwnerFor(host, owner); err != nil { 21 | return fmt.Errorf("setting default host: %w", err) 22 | } 23 | log.FromContext(ctx).Infof("Default owner for %s host set to %s\n", host, owner) 24 | return nil 25 | }, 26 | } 27 | return cmd, nil 28 | } 29 | -------------------------------------------------------------------------------- /ui/cli/commands/script_remove.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/apex/log" 7 | "github.com/kyoh86/gogh/v4/app/script/remove" 8 | "github.com/kyoh86/gogh/v4/app/service" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | func NewScriptRemoveCommand(_ context.Context, svc *service.ServiceSet) (*cobra.Command, error) { 13 | cmd := &cobra.Command{ 14 | Use: "remove [flags] ", 15 | Aliases: []string{"rm", "del", "delete"}, 16 | Short: "Remove a script", 17 | Args: cobra.ExactArgs(1), 18 | RunE: func(cmd *cobra.Command, args []string) error { 19 | ctx := cmd.Context() 20 | logger := log.FromContext(ctx) 21 | 22 | scriptID := args[0] 23 | if err := remove.NewUsecase(svc.ScriptService).Execute(ctx, scriptID); err != nil { 24 | return err 25 | } 26 | 27 | logger.Infof("Removed script %s", scriptID) 28 | return nil 29 | }, 30 | } 31 | return cmd, nil 32 | } 33 | -------------------------------------------------------------------------------- /ui/cli/commands/script_show.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/kyoh86/gogh/v4/app/script/show" 7 | "github.com/kyoh86/gogh/v4/app/service" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | func NewScriptShowCommand(_ context.Context, svc *service.ServiceSet) (*cobra.Command, error) { 12 | var f struct { 13 | json bool 14 | source bool 15 | } 16 | cmd := &cobra.Command{ 17 | Use: "show [flags] ", 18 | Short: "Show a script", 19 | Args: cobra.ExactArgs(1), 20 | RunE: func(cmd *cobra.Command, args []string) error { 21 | ctx := cmd.Context() 22 | scriptID := args[0] 23 | return show.NewUsecase(svc.ScriptService, cmd.OutOrStdout()).Execute(ctx, scriptID, f.json, f.source) 24 | }, 25 | } 26 | cmd.Flags().BoolVarP(&f.json, "json", "", false, "Output in JSON format") 27 | cmd.Flags().BoolVarP(&f.source, "source", "", false, "Output with source code") 28 | return cmd, nil 29 | } 30 | -------------------------------------------------------------------------------- /app/script/edit/usecase.go: -------------------------------------------------------------------------------- 1 | package edit 2 | 3 | import ( 4 | "context" 5 | "io" 6 | 7 | "github.com/kyoh86/gogh/v4/core/script" 8 | ) 9 | 10 | type Usecase struct { 11 | scriptService script.ScriptService 12 | } 13 | 14 | func NewUsecase(scriptService script.ScriptService) *Usecase { 15 | return &Usecase{scriptService: scriptService} 16 | } 17 | 18 | // ExtractScript extracts the script by its ID and writes it to the provided writer. 19 | func (uc *Usecase) ExtractScript(ctx context.Context, scriptID string, w io.Writer) error { 20 | r, err := uc.scriptService.Open(ctx, scriptID) 21 | if err != nil { 22 | return err 23 | } 24 | defer r.Close() 25 | _, err = io.Copy(w, r) 26 | return err 27 | } 28 | 29 | // UpdateScript applies a new script identified by its ID. 30 | func (uc *Usecase) UpdateScript(ctx context.Context, scriptID string, r io.Reader) error { 31 | return uc.scriptService.Update(ctx, scriptID, script.Entry{Content: r}) 32 | } 33 | -------------------------------------------------------------------------------- /ui/cli/commands/overlay_remove.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/apex/log" 7 | "github.com/kyoh86/gogh/v4/app/overlay/remove" 8 | "github.com/kyoh86/gogh/v4/app/service" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | func NewOverlayRemoveCommand(_ context.Context, svc *service.ServiceSet) (*cobra.Command, error) { 13 | cmd := &cobra.Command{ 14 | Use: "remove [flags] ", 15 | Aliases: []string{"rm", "del", "delete"}, 16 | Short: "Remove an overlay", 17 | Args: cobra.ExactArgs(1), 18 | RunE: func(cmd *cobra.Command, args []string) error { 19 | ctx := cmd.Context() 20 | logger := log.FromContext(ctx) 21 | 22 | overlayID := args[0] 23 | if err := remove.NewUsecase(svc.OverlayService).Execute(ctx, overlayID); err != nil { 24 | return err 25 | } 26 | 27 | logger.Infof("Removed overlay %s", overlayID) 28 | return nil 29 | }, 30 | } 31 | return cmd, nil 32 | } 33 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | VERSION ?= `git vertag get` 2 | COMMIT ?= `git rev-parse HEAD` 3 | DATE ?= `date --iso-8601` 4 | 5 | .PHONY: clean 6 | clean: 7 | $(MAKE) -C ./core clean 8 | $(MAKE) -C ./infra/githubv4 clean 9 | 10 | # Alias for gen 11 | .PHONY: generate 12 | generate: gen 13 | 14 | .PHONY: gen 15 | gen: 16 | $(MAKE) -C ./infra/githubv4 17 | $(MAKE) -C ./core 18 | 19 | lint: gen 20 | go tool golangci-lint run 21 | go tool arch-go 22 | .PHONY: lint 23 | 24 | test: gen 25 | go test -v --race ./... 26 | .PHONY: test 27 | 28 | man: gen 29 | rm -rf ./doc/usage/**.md 30 | rm -rf ./doc/man/* 31 | GOGH_FLAG_PATH=./dummy.yaml go run -ldflags "-X=main.version=$(VERSION) -X=main.commit=$(COMMIT) -X=main.date=$(DATE)" ./cmd/gogh man 32 | .PHONY: man 33 | 34 | install: test 35 | go install -a -ldflags "-X=main.version=$(VERSION) -X=main.commit=$(COMMIT) -X=main.date=$(DATE)" ./cmd/gogh/... 36 | .PHONY: install 37 | 38 | default: lint test 39 | .DEFAULT_GOAL := default 40 | -------------------------------------------------------------------------------- /app/overlay/edit/usecase.go: -------------------------------------------------------------------------------- 1 | package edit 2 | 3 | import ( 4 | "context" 5 | "io" 6 | 7 | "github.com/kyoh86/gogh/v4/core/overlay" 8 | ) 9 | 10 | type Usecase struct { 11 | overlayService overlay.OverlayService 12 | } 13 | 14 | func NewUsecase(overlayService overlay.OverlayService) *Usecase { 15 | return &Usecase{overlayService: overlayService} 16 | } 17 | 18 | // ExtractOverlay extracts the overlay by its ID and writes it to the provided writer. 19 | func (uc *Usecase) ExtractOverlay(ctx context.Context, overlayID string, w io.Writer) error { 20 | r, err := uc.overlayService.Open(ctx, overlayID) 21 | if err != nil { 22 | return err 23 | } 24 | defer r.Close() 25 | _, err = io.Copy(w, r) 26 | return err 27 | } 28 | 29 | // UpdateOverlay applies a new overlay identified by its ID. 30 | func (uc *Usecase) UpdateOverlay(ctx context.Context, overlayID string, r io.Reader) error { 31 | return uc.overlayService.Update(ctx, overlayID, overlay.Entry{Content: r}) 32 | } 33 | -------------------------------------------------------------------------------- /doc/usage/gogh_extra.md: -------------------------------------------------------------------------------- 1 | ## gogh extra 2 | 3 | Manage repository extra files 4 | 5 | ### Synopsis 6 | 7 | Manage extra files that are typically ignored by git. 8 | 9 | There are two types of extra: 10 | 1. Auto-apply extra: Automatically applied when cloning the repository 11 | 2. Named extra: Templates that can be manually applied to any repository 12 | 13 | ### Options 14 | 15 | ``` 16 | -h, --help help for extra 17 | ``` 18 | 19 | ### SEE ALSO 20 | 21 | * [gogh](gogh.md) - GO GitHub local repository manager 22 | * [gogh extra apply](gogh_extra_apply.md) - Apply a named extra to a repository 23 | * [gogh extra create](gogh_extra_create.md) - Create a named extra template 24 | * [gogh extra list](gogh_extra_list.md) - List extras 25 | * [gogh extra remove](gogh_extra_remove.md) - Remove an extra 26 | * [gogh extra save](gogh_extra_save.md) - Save excluded files as auto-apply extra 27 | * [gogh extra show](gogh_extra_show.md) - Show details of an extra 28 | 29 | -------------------------------------------------------------------------------- /core/auth/authenticate_service.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import "context" 4 | 5 | // DeviceAuthResponse represents the response from a device authentication request. 6 | type DeviceAuthResponse struct { 7 | // VerificationURI is the URI where the user can verify the authentication. 8 | VerificationURI string 9 | // UserCode is the code that the user needs to enter to verify the authentication. 10 | UserCode string 11 | } 12 | 13 | // Verify is a function type that takes a context and a DeviceAuthResponse 14 | type Verify func( 15 | ctx context.Context, 16 | response DeviceAuthResponse, 17 | ) error 18 | 19 | // AuthenticateService is an interface that defines a method for authenticating users. 20 | type AuthenticateService interface { 21 | // Authenticate the user with the given host. 22 | // The function will return a user name and a token if the authentication is successful. 23 | Authenticate(ctx context.Context, host string, verify Verify) (string, *Token, error) 24 | } 25 | -------------------------------------------------------------------------------- /typ/map.go: -------------------------------------------------------------------------------- 1 | package typ 2 | 3 | type Map[TKey comparable, TVal any] map[TKey]TVal 4 | 5 | func (m *Map[TKey, TVal]) Set(key TKey, val TVal) { 6 | if *m == nil { 7 | *m = map[TKey]TVal{} 8 | } 9 | (*m)[key] = val 10 | } 11 | 12 | func (m *Map[TKey, TVal]) Delete(key TKey) { 13 | if *m == nil { 14 | return 15 | } 16 | delete(*m, key) 17 | } 18 | 19 | func (m *Map[TKey, TVal]) Has(key TKey) bool { 20 | if *m == nil { 21 | return false 22 | } 23 | _, ok := (*m)[key] 24 | return ok 25 | } 26 | 27 | func (m *Map[TKey, TVal]) TryGet(key TKey) (TVal, bool) { 28 | var d TVal 29 | if *m == nil { 30 | return d, false 31 | } 32 | v, ok := (*m)[key] 33 | return v, ok 34 | } 35 | 36 | func (m *Map[TKey, TVal]) GetOrSet(key TKey, setValue TVal) TVal { 37 | if *m == nil { 38 | *m = map[TKey]TVal{ 39 | key: setValue, 40 | } 41 | return setValue 42 | } 43 | if v, ok := (*m)[key]; ok { 44 | return v 45 | } 46 | (*m)[key] = setValue 47 | return setValue 48 | } 49 | -------------------------------------------------------------------------------- /ui/cli/commands/extra_show.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | "os" 6 | 7 | "github.com/kyoh86/gogh/v4/app/extra/show" 8 | "github.com/kyoh86/gogh/v4/app/service" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | func NewExtraShowCommand(_ context.Context, svc *service.ServiceSet) (*cobra.Command, error) { 13 | var asJSON bool 14 | 15 | cmd := &cobra.Command{ 16 | Use: "show ", 17 | Short: "Show details of an extra", 18 | Long: `Show detailed information about an extra. 19 | 20 | You can specify either an extra ID or name (for named extras). 21 | Use --json to output in JSON format.`, 22 | Args: cobra.ExactArgs(1), 23 | RunE: func(cmd *cobra.Command, args []string) error { 24 | usecase := show.NewUsecase(svc.ExtraService, os.Stdout) 25 | return usecase.Execute(cmd.Context(), args[0], asJSON) 26 | }, 27 | } 28 | 29 | cmd.Flags().BoolVarP(&asJSON, "json", "j", false, "Output in JSON format") 30 | 31 | return cmd, nil 32 | } 33 | -------------------------------------------------------------------------------- /doc/usage/gogh_script.md: -------------------------------------------------------------------------------- 1 | ## gogh script 2 | 3 | Manage repository script files 4 | 5 | ### Options 6 | 7 | ``` 8 | -h, --help help for script 9 | ``` 10 | 11 | ### SEE ALSO 12 | 13 | * [gogh](gogh.md) - GO GitHub local repository manager 14 | * [gogh script add](gogh_script_add.md) - Add an existing Lua script as script 15 | * [gogh script create](gogh_script_create.md) - Create a new script (with $EDITOR) 16 | * [gogh script edit](gogh_script_edit.md) - Edit an existing script (with $EDITOR) 17 | * [gogh script invoke](gogh_script_invoke.md) - Invoke an script in a repository 18 | * [gogh script invoke-instant](gogh_script_invoke-instant.md) - Run a temporary script in a repository without storing it 19 | * [gogh script list](gogh_script_list.md) - List registered scripts 20 | * [gogh script remove](gogh_script_remove.md) - Remove a script 21 | * [gogh script show](gogh_script_show.md) - Show a script 22 | * [gogh script update](gogh_script_update.md) - Update an existing script 23 | 24 | -------------------------------------------------------------------------------- /ui/cli/commands/auth_list.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | 8 | "github.com/kyoh86/gogh/v4/app/auth/list" 9 | "github.com/kyoh86/gogh/v4/app/service" 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | func NewAuthListCommand(_ context.Context, svc *service.ServiceSet) (*cobra.Command, error) { 14 | return &cobra.Command{ 15 | Use: "list", 16 | Short: "Listup authenticated host and owners", 17 | Args: cobra.NoArgs, 18 | RunE: func(cmd *cobra.Command, _ []string) error { 19 | ctx := cmd.Context() 20 | entries, err := list.NewUsecase(svc.TokenService).Execute(ctx) 21 | if err != nil { 22 | return fmt.Errorf("listing up tokens: %w", err) 23 | } 24 | if len(entries) == 0 { 25 | return errors.New("no valid token found: you need to set token by `gogh auth login`") 26 | } 27 | for _, entry := range entries { 28 | fmt.Printf("%s/%s\n", entry.Host, entry.Owner) 29 | } 30 | return nil 31 | }, 32 | }, nil 33 | } 34 | -------------------------------------------------------------------------------- /ui/cli/commands/migrate.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/kyoh86/gogh/v4/app/service" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | func NewMigrateCommand(_ context.Context, svc *service.ServiceSet) (*cobra.Command, error) { 12 | cmd := &cobra.Command{ 13 | Use: "migrate", 14 | Short: "Migrate configurations", 15 | Args: cobra.NoArgs, 16 | RunE: func(cmd *cobra.Command, _ []string) error { 17 | ctx := cmd.Context() 18 | if err := svc.DefaultNameStore.Save(ctx, svc.DefaultNameService, true); err != nil { 19 | return fmt.Errorf("saving default names: %w", err) 20 | } 21 | if err := svc.TokenStore.Save(ctx, svc.TokenService, true); err != nil { 22 | return fmt.Errorf("saving tokens: %w", err) 23 | } 24 | if err := svc.WorkspaceStore.Save(ctx, svc.WorkspaceService, true); err != nil { 25 | return fmt.Errorf("saving workspaces: %w", err) 26 | } 27 | return nil 28 | }, 29 | } 30 | return cmd, nil 31 | } 32 | -------------------------------------------------------------------------------- /doc/usage/gogh_extra_save.md: -------------------------------------------------------------------------------- 1 | ## gogh extra save 2 | 3 | Save excluded files as auto-apply extra 4 | 5 | ### Synopsis 6 | 7 | Save files that are excluded by .gitignore as auto-apply extra. 8 | These extra will be automatically applied when the repository is cloned. 9 | 10 | ``` 11 | gogh extra save [flags] 12 | ``` 13 | 14 | ### Examples 15 | 16 | ``` 17 | save github.com/kyoh86/example 18 | save . # Save from current directory repository 19 | 20 | It accepts a short notation for the repository 21 | (for example, "github.com/kyoh86/example") like below. 22 | - "": e.g. "example"; 23 | - "/": e.g. "kyoh86/example" 24 | - "." for the current directory repository 25 | They'll be completed with the default host and owner set by "config set-default{-host|-owner}". 26 | ``` 27 | 28 | ### Options 29 | 30 | ``` 31 | -h, --help help for save 32 | ``` 33 | 34 | ### SEE ALSO 35 | 36 | * [gogh extra](gogh_extra.md) - Manage repository extra files 37 | 38 | -------------------------------------------------------------------------------- /doc/usage/gogh_overlay_add.md: -------------------------------------------------------------------------------- 1 | ## gogh overlay add 2 | 3 | Add an overlay file 4 | 5 | ``` 6 | gogh overlay add [flags] 7 | ``` 8 | 9 | ### Examples 10 | 11 | ``` 12 | Add an overlay file to a repository. 13 | The is the name of the overlay, which is used to identify it. 14 | The is the path where the overlay file will be copied to in the repository. 15 | The is the path to the file you want to add as an overlay. 16 | 17 | For example, to add a custom VSCode settings file to a repository, you can run: 18 | 19 | gogh overlay add vsc-setting /path/to/source/vscode/settings.json .vscode/settings.json 20 | 21 | The overlay file will be copied to the repository when you run `gogh overlay apply`. 22 | ``` 23 | 24 | ### Options 25 | 26 | ``` 27 | --for-init Register the overlay for 'gogh create' command 28 | -h, --help help for add 29 | ``` 30 | 31 | ### SEE ALSO 32 | 33 | * [gogh overlay](gogh_overlay.md) - Manage repository overlay files 34 | 35 | -------------------------------------------------------------------------------- /app/hook/list/usecase.go: -------------------------------------------------------------------------------- 1 | package list 2 | 3 | import ( 4 | "context" 5 | "io" 6 | 7 | "github.com/kyoh86/gogh/v4/app/hook/describe" 8 | "github.com/kyoh86/gogh/v4/core/hook" 9 | ) 10 | 11 | type Usecase struct { 12 | hookService hook.HookService 13 | writer io.Writer 14 | } 15 | 16 | func NewUsecase( 17 | hookService hook.HookService, 18 | writer io.Writer, 19 | ) *Usecase { 20 | return &Usecase{ 21 | hookService: hookService, 22 | writer: writer, 23 | } 24 | } 25 | 26 | func (uc *Usecase) Execute(ctx context.Context, asJSON bool) error { 27 | var usecase interface { 28 | Execute(ctx context.Context, s describe.Hook) error 29 | } 30 | if asJSON { 31 | usecase = describe.NewJSONUsecase(uc.writer) 32 | } else { 33 | usecase = describe.NewOnelineUsecase(uc.writer) 34 | } 35 | for s, err := range uc.hookService.List() { 36 | if err != nil { 37 | return err 38 | } 39 | if s == nil { 40 | continue 41 | } 42 | if err := usecase.Execute(ctx, s); err != nil { 43 | return err 44 | } 45 | } 46 | return nil 47 | } 48 | -------------------------------------------------------------------------------- /lua/gogh.lua: -------------------------------------------------------------------------------- 1 | ---@class gogh.Repo 2 | ---@field full_path string 3 | ---@field path string 4 | ---@field host string 5 | ---@field owner string 6 | ---@field name string 7 | 8 | ---@class gogh.Hook 9 | ---@field id string Hook UUID 10 | ---@field name string Hook name 11 | ---@field repoPattern string Pattern that matched 12 | ---@field triggerEvent string Event that triggered the hook 13 | ---@field operationType string Type of operation that triggered the hook: "script" always. 14 | ---@field operationId string Operation UUID (script UUID) 15 | 16 | ---@class gogh 17 | ---@field repo gogh.Repo 18 | ---@field hook gogh.Hook 19 | ---@field parent? gogh.Repo|nil 20 | 21 | ---@type gogh.Repo 22 | local repo = { 23 | full_path = "", 24 | path = "", 25 | host = "", 26 | owner = "", 27 | name = "", 28 | } 29 | 30 | ---@type gogh.Hook 31 | local hook = { 32 | id = "", 33 | name = "", 34 | repoPattern = "", 35 | triggerEvent = "", 36 | operationType = "script", 37 | operationId = "", 38 | } 39 | 40 | ---@type gogh 41 | _G.gogh = { repo = repo, hook = hook } 42 | -------------------------------------------------------------------------------- /app/cwd/cwd_usecase.go: -------------------------------------------------------------------------------- 1 | package cwd 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/kyoh86/gogh/v4/core/repository" 9 | "github.com/kyoh86/gogh/v4/core/workspace" 10 | ) 11 | 12 | // Usecase defines the use case for listing repository locations 13 | type Usecase struct { 14 | workspaceService workspace.WorkspaceService 15 | finderService workspace.FinderService 16 | } 17 | 18 | // NewUsecase creates a new instance of Usecase 19 | func NewUsecase( 20 | workspaceService workspace.WorkspaceService, 21 | finderService workspace.FinderService, 22 | ) *Usecase { 23 | return &Usecase{ 24 | workspaceService: workspaceService, 25 | finderService: finderService, 26 | } 27 | } 28 | 29 | // Execute retrieves the repository location for the current working directory 30 | func (uc *Usecase) Execute(ctx context.Context) (*repository.Location, error) { 31 | wd, err := os.Getwd() 32 | if err != nil { 33 | return nil, fmt.Errorf("getting working directory: %w", err) 34 | } 35 | return uc.finderService.FindByPath(ctx, uc.workspaceService, wd) 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 kyoh86 4 | 5 | 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: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | 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. 10 | -------------------------------------------------------------------------------- /doc/usage/gogh_completion_bash.md: -------------------------------------------------------------------------------- 1 | ## gogh completion bash 2 | 3 | Generate the autocompletion script for bash 4 | 5 | ### Synopsis 6 | 7 | Generate the autocompletion script for the bash shell. 8 | 9 | This script depends on the 'bash-completion' package. 10 | If it is not installed already, you can install it via your OS's package manager. 11 | 12 | To load completions in your current shell session: 13 | 14 | source <(gogh completion bash) 15 | 16 | To load completions for every new session, execute once: 17 | 18 | #### Linux: 19 | 20 | gogh completion bash > /etc/bash_completion.d/gogh 21 | 22 | #### macOS: 23 | 24 | gogh completion bash > $(brew --prefix)/etc/bash_completion.d/gogh 25 | 26 | You will need to start a new shell for this setup to take effect. 27 | 28 | 29 | ``` 30 | gogh completion bash 31 | ``` 32 | 33 | ### Options 34 | 35 | ``` 36 | -h, --help help for bash 37 | --no-descriptions disable completion descriptions 38 | ``` 39 | 40 | ### SEE ALSO 41 | 42 | * [gogh completion](gogh_completion.md) - Generate the autocompletion script for the specified shell 43 | 44 | -------------------------------------------------------------------------------- /ui/cli/commands/script_add.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/kyoh86/gogh/v4/app/script/add" 9 | "github.com/kyoh86/gogh/v4/app/service" 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | func NewScriptAddCommand(_ context.Context, svc *service.ServiceSet) (*cobra.Command, error) { 14 | var f struct { 15 | name string 16 | } 17 | cmd := &cobra.Command{ 18 | Use: "add [flags] ", 19 | Short: "Add an existing Lua script as script", 20 | Args: cobra.ExactArgs(1), 21 | RunE: func(cmd *cobra.Command, args []string) error { 22 | ctx := cmd.Context() 23 | path := args[0] 24 | content, err := os.Open(path) 25 | if err != nil { 26 | return err 27 | } 28 | defer content.Close() 29 | h, err := add.NewUsecase(svc.ScriptService).Execute(ctx, f.name, content) 30 | if err != nil { 31 | return fmt.Errorf("adding script: %w", err) 32 | } 33 | fmt.Printf("Script added %s\n", h.ID()) 34 | return nil 35 | }, 36 | } 37 | cmd.Flags().StringVar(&f.name, "name", "", "Name of the script") 38 | return cmd, nil 39 | } 40 | -------------------------------------------------------------------------------- /ui/cli/commands/overlay_show.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/kyoh86/gogh/v4/app/overlay/show" 8 | "github.com/kyoh86/gogh/v4/app/service" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | func NewOverlayShowCommand(_ context.Context, svc *service.ServiceSet) (*cobra.Command, error) { 13 | var f struct { 14 | json bool 15 | source bool 16 | } 17 | cmd := &cobra.Command{ 18 | Use: "show [flags] ", 19 | Short: "Show an overlay", 20 | Args: cobra.ExactArgs(1), 21 | RunE: func(cmd *cobra.Command, args []string) error { 22 | ctx := cmd.Context() 23 | overlayID := args[0] 24 | overlayShowUsecase := show.NewUsecase(svc.OverlayService, cmd.OutOrStdout()) 25 | if err := overlayShowUsecase.Execute(ctx, overlayID, f.json, f.source); err != nil { 26 | return fmt.Errorf("showing overlay %s: %w", overlayID, err) 27 | } 28 | return nil 29 | }, 30 | } 31 | cmd.Flags().BoolVarP(&f.json, "json", "", false, "Output in JSON format") 32 | cmd.Flags().BoolVarP(&f.source, "source", "", false, "Output with source code") 33 | return cmd, nil 34 | } 35 | -------------------------------------------------------------------------------- /app/hook/show/usecase.go: -------------------------------------------------------------------------------- 1 | package show 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | 8 | "github.com/kyoh86/gogh/v4/app/hook/describe" 9 | "github.com/kyoh86/gogh/v4/core/hook" 10 | ) 11 | 12 | // Usecase for running hook hooks 13 | type Usecase struct { 14 | hookService hook.HookService 15 | writer io.Writer 16 | } 17 | 18 | func NewUsecase( 19 | hookService hook.HookService, 20 | writer io.Writer, 21 | ) *Usecase { 22 | return &Usecase{ 23 | hookService: hookService, 24 | writer: writer, 25 | } 26 | } 27 | 28 | func (uc *Usecase) Execute(ctx context.Context, hookID string, asJSON bool) error { 29 | hook, err := uc.hookService.Get(ctx, hookID) 30 | if err != nil { 31 | return fmt.Errorf("get hook by ID: %w", err) 32 | } 33 | var usecase interface { 34 | Execute(ctx context.Context, s describe.Hook) error 35 | } 36 | if asJSON { 37 | usecase = describe.NewJSONUsecase(uc.writer) 38 | } else { 39 | usecase = describe.NewOnelineUsecase(uc.writer) 40 | } 41 | if err := usecase.Execute(ctx, hook); err != nil { 42 | return fmt.Errorf("execute dehookion: %w", err) 43 | } 44 | return nil 45 | } 46 | -------------------------------------------------------------------------------- /ui/cli/commands/extra_list.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | "os" 6 | 7 | "github.com/kyoh86/gogh/v4/app/extra/list" 8 | "github.com/kyoh86/gogh/v4/app/service" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | func NewExtraListCommand(_ context.Context, svc *service.ServiceSet) (*cobra.Command, error) { 13 | var ( 14 | asJSON bool 15 | extraType string 16 | ) 17 | 18 | cmd := &cobra.Command{ 19 | Use: "list", 20 | Aliases: []string{"ls"}, 21 | Short: "List extras", 22 | Long: `List all extras. 23 | 24 | By default, lists all extras in one-line format. 25 | Use --type to filter by extra type (auto, named). 26 | Use --json to output in JSON format.`, 27 | Args: cobra.NoArgs, 28 | RunE: func(cmd *cobra.Command, args []string) error { 29 | usecase := list.NewUsecase(svc.ExtraService, os.Stdout) 30 | return usecase.Execute(cmd.Context(), asJSON, extraType) 31 | }, 32 | } 33 | 34 | cmd.Flags().StringVarP(&extraType, "type", "t", "all", "Filter by type (all, auto, named)") 35 | cmd.Flags().BoolVarP(&asJSON, "json", "j", false, "Output in JSON format") 36 | 37 | return cmd, nil 38 | } 39 | -------------------------------------------------------------------------------- /app/config/flags_store_v0.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | 8 | yaml "github.com/goccy/go-yaml" 9 | "github.com/kyoh86/gogh/v4/core/store" 10 | ) 11 | 12 | type FlagsStoreV0 struct{} 13 | 14 | // Load implements store.Loader 15 | func (d *FlagsStoreV0) Load(ctx context.Context, initial func() *Flags) (*Flags, error) { 16 | v := initial() 17 | v.RawHasChanges = true 18 | source, err := d.Source() 19 | if err != nil { 20 | return nil, err 21 | } 22 | file, err := os.Open(source) 23 | if err != nil { 24 | return nil, err 25 | } 26 | defer file.Close() 27 | if err := yaml.NewDecoder(file).Decode(v); err != nil { 28 | return nil, err 29 | } 30 | return v, nil 31 | } 32 | 33 | func NewFlagsStoreV0() *FlagsStoreV0 { 34 | return &FlagsStoreV0{} 35 | } 36 | 37 | // Source implements store.Loader 38 | func (d *FlagsStoreV0) Source() (string, error) { 39 | path, err := AppContextPathFunc("GOGH_FLAG_PATH", os.UserConfigDir, "flag.yaml") 40 | if err != nil { 41 | return "", fmt.Errorf("search flags path: %w", err) 42 | } 43 | return path, nil 44 | } 45 | 46 | var _ store.Loader[*Flags] = (*FlagsStoreV0)(nil) 47 | -------------------------------------------------------------------------------- /doc/usage/gogh_completion_zsh.md: -------------------------------------------------------------------------------- 1 | ## gogh completion zsh 2 | 3 | Generate the autocompletion script for zsh 4 | 5 | ### Synopsis 6 | 7 | Generate the autocompletion script for the zsh shell. 8 | 9 | If shell completion is not already enabled in your environment you will need 10 | to enable it. You can execute the following once: 11 | 12 | echo "autoload -U compinit; compinit" >> ~/.zshrc 13 | 14 | To load completions in your current shell session: 15 | 16 | source <(gogh completion zsh) 17 | 18 | To load completions for every new session, execute once: 19 | 20 | #### Linux: 21 | 22 | gogh completion zsh > "${fpath[1]}/_gogh" 23 | 24 | #### macOS: 25 | 26 | gogh completion zsh > $(brew --prefix)/share/zsh/site-functions/_gogh 27 | 28 | You will need to start a new shell for this setup to take effect. 29 | 30 | 31 | ``` 32 | gogh completion zsh [flags] 33 | ``` 34 | 35 | ### Options 36 | 37 | ``` 38 | -h, --help help for zsh 39 | --no-descriptions disable completion descriptions 40 | ``` 41 | 42 | ### SEE ALSO 43 | 44 | * [gogh completion](gogh_completion.md) - Generate the autocompletion script for the specified shell 45 | 46 | -------------------------------------------------------------------------------- /doc/usage/gogh_fork.md: -------------------------------------------------------------------------------- 1 | ## gogh fork 2 | 3 | Fork a repository 4 | 5 | ``` 6 | gogh fork [flags] [/]/ 7 | ``` 8 | 9 | ### Examples 10 | 11 | ``` 12 | It accepts a short notation for a repository 13 | (for example, "github.com/kyoh86/example") like "/": e.g. "kyoh86/example" 14 | They'll be completed with the default host set by "config set-default-host" 15 | ``` 16 | 17 | ### Options 18 | 19 | ``` 20 | --clone-retry-limit int The number of retries to clone a repository (default 3) 21 | -t, --clone-retry-timeout duration Timeout for each clone attempt (default 5m0s) 22 | --default-branch-only Only fork the default branch 23 | -h, --help help for fork 24 | --to string Fork to the specified repository. It accepts a notation like '/' or '/='. If not specified, it will be forked to the default owner and same name as the original repository. If the alias is specified, it will be set as the local repository name 25 | ``` 26 | 27 | ### SEE ALSO 28 | 29 | * [gogh](gogh.md) - GO GitHub local repository manager 30 | 31 | -------------------------------------------------------------------------------- /ui/cli/commands/extra_apply.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/kyoh86/gogh/v4/app/extra/apply" 7 | "github.com/kyoh86/gogh/v4/app/service" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | func NewExtraApplyCommand(_ context.Context, svc *service.ServiceSet) (*cobra.Command, error) { 12 | usecase := apply.NewUsecase( 13 | svc.ExtraService, 14 | svc.OverlayService, 15 | svc.WorkspaceService, 16 | svc.FinderService, 17 | svc.ReferenceParser, 18 | ) 19 | 20 | var opts apply.Options 21 | 22 | cmd := &cobra.Command{ 23 | Use: "apply ", 24 | Short: "Apply a named extra to a repository", 25 | Long: `Apply a named extra template to a repository. 26 | 27 | This applies all overlays in the named extra to the target repository. 28 | By default, it applies to the current directory's repository.`, 29 | Args: cobra.ExactArgs(1), 30 | RunE: func(cmd *cobra.Command, args []string) error { 31 | opts.Name = args[0] 32 | return usecase.Execute(cmd.Context(), opts) 33 | }, 34 | } 35 | 36 | cmd.Flags().StringVarP(&opts.TargetRepo, "target", "t", "", "Target repository (default: current directory)") 37 | 38 | return cmd, nil 39 | } 40 | -------------------------------------------------------------------------------- /infra/logger/logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "os" 7 | 8 | "github.com/apex/log" 9 | "github.com/apex/log/handlers/cli" 10 | "github.com/apex/log/handlers/level" 11 | "github.com/apex/log/handlers/multi" 12 | ) 13 | 14 | // StdoutLogHandler implementation. 15 | type StdoutLogHandler struct { 16 | Handler log.Handler 17 | } 18 | 19 | // HandleLog implements log.Handler. 20 | // It filters out error level logs and above, allowing only info/debug logs to pass through to stdout. 21 | func (h *StdoutLogHandler) HandleLog(e *log.Entry) error { 22 | if e.Level >= log.ErrorLevel { 23 | return nil 24 | } 25 | 26 | return h.Handler.HandleLog(e) 27 | } 28 | 29 | // NewLogger creates a new logger instance. 30 | func NewLogger(ctx context.Context, outWriter io.Writer, errWriter io.Writer) context.Context { 31 | errLog := level.New(cli.New(errWriter), log.WarnLevel) 32 | outLog := &StdoutLogHandler{Handler: cli.New(outWriter)} 33 | level := log.InfoLevel 34 | if os.Getenv("GOGH_DEBUG") != "" { 35 | level = log.DebugLevel 36 | } 37 | return log.NewContext(ctx, &log.Logger{ 38 | Handler: multi.New(outLog, errLog), 39 | Level: level, 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /ui/cli/commands/script_run.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | "encoding/gob" 6 | "fmt" 7 | "os" 8 | 9 | "github.com/kyoh86/gogh/v4/app/script/run" 10 | "github.com/kyoh86/gogh/v4/app/service" 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | func NewScriptRunCommand(_ context.Context, svc *service.ServiceSet) (*cobra.Command, error) { 15 | cmd := &cobra.Command{ 16 | Use: "run", 17 | Short: "Run a script gob from stdin (it is internal command used by gogh script-invoke command)", 18 | Hidden: true, 19 | Args: cobra.NoArgs, 20 | RunE: func(cmd *cobra.Command, args []string) error { 21 | // Check if we're in test mode to prevent infinite recursion 22 | if os.Getenv("GOGH_TEST_MODE") == "1" { 23 | // In test mode, just exit successfully without executing 24 | return nil 25 | } 26 | 27 | ctx := cmd.Context() 28 | var script run.Script 29 | gob.Register(map[string]any{}) 30 | dec := gob.NewDecoder(os.Stdin) 31 | if err := dec.Decode(&script); err != nil { 32 | return fmt.Errorf("decoding script from stdin: %w", err) 33 | } 34 | return run.NewUsecase().Execute(ctx, script) 35 | }, 36 | } 37 | return cmd, nil 38 | } 39 | -------------------------------------------------------------------------------- /doc/usage/gogh.md: -------------------------------------------------------------------------------- 1 | ## gogh 2 | 3 | GO GitHub local repository manager 4 | 5 | ### Options 6 | 7 | ``` 8 | -h, --help help for gogh 9 | ``` 10 | 11 | ### SEE ALSO 12 | 13 | * [gogh auth](gogh_auth.md) - Manage tokens 14 | * [gogh bundle](gogh_bundle.md) - Manage bundle 15 | * [gogh clone](gogh_clone.md) - Clone remote repositories to local 16 | * [gogh completion](gogh_completion.md) - Generate the autocompletion script for the specified shell 17 | * [gogh config](gogh_config.md) - Show/change configurations 18 | * [gogh create](gogh_create.md) - Create a new local and remote repository 19 | * [gogh cwd](gogh_cwd.md) - Print the local repository which the current working directory belongs to 20 | * [gogh delete](gogh_delete.md) - Delete local and remote repository 21 | * [gogh extra](gogh_extra.md) - Manage repository extra files 22 | * [gogh fork](gogh_fork.md) - Fork a repository 23 | * [gogh hook](gogh_hook.md) - Manage repository hooks 24 | * [gogh list](gogh_list.md) - List local repositories 25 | * [gogh overlay](gogh_overlay.md) - Manage repository overlay files 26 | * [gogh repos](gogh_repos.md) - List remote repositories 27 | * [gogh roots](gogh_roots.md) - Manage roots 28 | * [gogh script](gogh_script.md) - Manage repository script files 29 | 30 | -------------------------------------------------------------------------------- /ui/cli/flags/repository_format.go: -------------------------------------------------------------------------------- 1 | package flags 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | // RepositoryFormatFlag adds a flag to the command for specifying the format of the repository output. 10 | func RepositoryFormatFlag(cmd *cobra.Command, format *string, defaultValue string) error { 11 | // UNDONE: opt ...Options Accepts NameOption, ShortUsageOption, ShorthandOption 12 | cmd.Flags().StringVarP(format, "format", "f", defaultValue, RepositoryFormatShortUsage) 13 | if err := cmd.RegisterFlagCompletionFunc("format", CompleteRepositoryFormat); err != nil { 14 | return fmt.Errorf("registering completion function for format flag: %w", err) 15 | } 16 | return nil 17 | } 18 | 19 | // RepositoryFormatShortUsage is the short usage description for the repository format flag. 20 | const RepositoryFormatShortUsage = ` 21 | Print each repository in a given format, where [format] can be one of "table", "ref", 22 | "url" or "json". 23 | ` 24 | 25 | // CompleteRepositoryFormat provides completion options for the repository format flag. 26 | func CompleteRepositoryFormat(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { 27 | return []string{"table", "ref", "url", "json"}, cobra.ShellCompDirectiveDefault 28 | } 29 | -------------------------------------------------------------------------------- /app/list/usecase.go: -------------------------------------------------------------------------------- 1 | package list 2 | 3 | import ( 4 | "context" 5 | "iter" 6 | 7 | "github.com/kyoh86/gogh/v4/core/repository" 8 | "github.com/kyoh86/gogh/v4/core/workspace" 9 | ) 10 | 11 | // Usecase defines the use case for listing repositories 12 | type Usecase struct { 13 | workspaceService workspace.WorkspaceService 14 | finderService workspace.FinderService 15 | } 16 | 17 | // NewUsecase creates a new instance of Usecase 18 | func NewUsecase( 19 | workspaceService workspace.WorkspaceService, 20 | finderService workspace.FinderService, 21 | ) *Usecase { 22 | return &Usecase{ 23 | workspaceService: workspaceService, 24 | finderService: finderService, 25 | } 26 | } 27 | 28 | type ListOptions = workspace.ListOptions 29 | 30 | type Options struct { 31 | Primary bool 32 | ListOptions 33 | } 34 | 35 | // Execute retrieves a list of repositories under the specified workspace roots 36 | func (uc *Usecase) Execute(ctx context.Context, opts Options) iter.Seq2[*repository.Location, error] { 37 | ws := uc.workspaceService 38 | if opts.Primary { 39 | layout := ws.GetLayoutFor(ws.GetPrimaryRoot()) 40 | return uc.finderService.ListRepositoryInRoot(ctx, layout, opts.ListOptions) 41 | } 42 | return uc.finderService.ListAllRepository(ctx, ws, opts.ListOptions) 43 | } 44 | -------------------------------------------------------------------------------- /ui/cli/commands/extra_remove.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/kyoh86/gogh/v4/app/extra/remove" 7 | "github.com/kyoh86/gogh/v4/app/service" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | func NewExtraRemoveCommand(_ context.Context, svc *service.ServiceSet) (*cobra.Command, error) { 12 | usecase := remove.NewUsecase( 13 | svc.ExtraService, 14 | svc.ReferenceParser, 15 | ) 16 | 17 | var opts remove.Options 18 | 19 | cmd := &cobra.Command{ 20 | Use: "remove", 21 | Aliases: []string{"rm", "delete"}, 22 | Short: "Remove an extra", 23 | Long: `Remove an extra. 24 | 25 | You can remove by: 26 | - ID: Use --id flag 27 | - Name (for named extras): Use --name flag 28 | - Repository (for auto extras): Use --repository flag`, 29 | Args: cobra.NoArgs, 30 | RunE: func(cmd *cobra.Command, args []string) error { 31 | return usecase.Execute(cmd.Context(), opts) 32 | }, 33 | } 34 | 35 | cmd.Flags().StringVarP(&opts.ID, "id", "i", "", "Extra ID to remove") 36 | cmd.Flags().StringVarP(&opts.Name, "name", "n", "", "Named extra to remove") 37 | cmd.Flags().StringVarP(&opts.Repository, "repository", "r", "", "Repository whose auto extra to remove") 38 | cmd.MarkFlagsMutuallyExclusive("id", "name", "repository") 39 | 40 | return cmd, nil 41 | } 42 | -------------------------------------------------------------------------------- /ui/cli/commands/script_update.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "os" 8 | 9 | "github.com/kyoh86/gogh/v4/app/script/update" 10 | "github.com/kyoh86/gogh/v4/app/service" 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | func NewScriptUpdateCommand(_ context.Context, svc *service.ServiceSet) (*cobra.Command, error) { 15 | var f struct { 16 | name string 17 | sourcePath string 18 | } 19 | cmd := &cobra.Command{ 20 | Use: "update [flags] ", 21 | Short: "Update an existing script", 22 | Args: cobra.ExactArgs(1), 23 | RunE: func(cmd *cobra.Command, args []string) error { 24 | ctx := cmd.Context() 25 | scriptID := args[0] 26 | var content io.Reader 27 | if f.sourcePath != "" { 28 | c, err := os.Open(f.sourcePath) 29 | if err != nil { 30 | return err 31 | } 32 | defer c.Close() 33 | content = c 34 | } 35 | if err := update.NewUsecase(svc.ScriptService).Execute(ctx, scriptID, f.name, content); err != nil { 36 | return fmt.Errorf("updating script metadata: %w", err) 37 | } 38 | return nil 39 | }, 40 | } 41 | cmd.Flags().StringVar(&f.name, "name", "", "Name of the script") 42 | cmd.Flags().StringVar(&f.sourcePath, "source", "", "Script source file path") 43 | return cmd, nil 44 | } 45 | -------------------------------------------------------------------------------- /app/extra/show/usecase.go: -------------------------------------------------------------------------------- 1 | package show 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | 8 | "github.com/kyoh86/gogh/v4/app/extra/describe" 9 | "github.com/kyoh86/gogh/v4/core/extra" 10 | ) 11 | 12 | // Usecase represents the extra show use case 13 | type Usecase struct { 14 | extraService extra.ExtraService 15 | writer io.Writer 16 | } 17 | 18 | // NewUsecase creates a new extra show use case 19 | func NewUsecase(extraService extra.ExtraService, writer io.Writer) *Usecase { 20 | return &Usecase{ 21 | extraService: extraService, 22 | writer: writer, 23 | } 24 | } 25 | 26 | // Execute performs the extra show operation 27 | func (uc *Usecase) Execute(ctx context.Context, identifier string, asJSON bool) error { 28 | // Try as ID first 29 | e, err := uc.extraService.Get(ctx, identifier) 30 | if err != nil { 31 | // Try as name for named extras 32 | e, err = uc.extraService.GetNamedExtra(ctx, identifier) 33 | if err != nil { 34 | return fmt.Errorf("extra not found: %w", err) 35 | } 36 | } 37 | 38 | var usecase interface { 39 | Execute(ctx context.Context, e describe.Extra) error 40 | } 41 | 42 | if asJSON { 43 | usecase = describe.NewJSONUsecase(uc.writer) 44 | } else { 45 | usecase = describe.NewDetailUsecase(uc.writer) 46 | } 47 | 48 | return usecase.Execute(ctx, *e) 49 | } 50 | -------------------------------------------------------------------------------- /core/workspace/finder_service.go: -------------------------------------------------------------------------------- 1 | package workspace 2 | 3 | import ( 4 | "context" 5 | "iter" 6 | 7 | "github.com/kyoh86/gogh/v4/core/repository" 8 | ) 9 | 10 | // ListOptions is the options for repository search 11 | type ListOptions struct { 12 | // Limit is the maximum number of repositories to return 13 | // If 0, all repositories will be returned 14 | Limit int 15 | // Patterns to match repository paths 16 | // If empty, all repositories will be returned 17 | Patterns []string 18 | } 19 | 20 | // FinderService is a service for searching repositories 21 | type FinderService interface { 22 | // FindByReference retrieves a repository by its reference 23 | FindByReference(ctx context.Context, ws WorkspaceService, reference repository.Reference) (*repository.Location, error) 24 | 25 | // FindByPath retrieves a repository by its path 26 | FindByPath(ctx context.Context, ws WorkspaceService, path string) (*repository.Location, error) 27 | 28 | // ListAllRepository retrieves a list of repositories under all roots 29 | ListAllRepository(context.Context, WorkspaceService, ListOptions) iter.Seq2[*repository.Location, error] 30 | 31 | // ListRepositoryInRoot retrieves a list of repositories under a root 32 | ListRepositoryInRoot(context.Context, LayoutService, ListOptions) iter.Seq2[*repository.Location, error] 33 | } 34 | -------------------------------------------------------------------------------- /ui/cli/commands/overlay_test.go: -------------------------------------------------------------------------------- 1 | package commands_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/kyoh86/gogh/v4/app/config" 8 | "github.com/kyoh86/gogh/v4/app/service" 9 | "github.com/kyoh86/gogh/v4/ui/cli/commands" 10 | ) 11 | 12 | func TestNewOverlayCommand(t *testing.T) { 13 | // Setup 14 | ctx := context.Background() 15 | serviceSet := &service.ServiceSet{Flags: &config.Flags{}} 16 | 17 | // Execute and verify no error occurs 18 | _, err := commands.NewOverlayCommand(ctx, serviceSet) 19 | if err != nil { 20 | t.Fatalf("Expected no error, got %v", err) 21 | } 22 | } 23 | 24 | func TestNewOverlayAddCommand(t *testing.T) { 25 | // Setup 26 | ctx := context.Background() 27 | serviceSet := &service.ServiceSet{Flags: &config.Flags{}} 28 | 29 | // Execute and verify no error occurs 30 | _, err := commands.NewOverlayAddCommand(ctx, serviceSet) 31 | if err != nil { 32 | t.Fatalf("Expected no error, got %v", err) 33 | } 34 | } 35 | 36 | func TestNewOverlayRemoveCommand(t *testing.T) { 37 | // Setup 38 | ctx := context.Background() 39 | serviceSet := &service.ServiceSet{Flags: &config.Flags{}} 40 | 41 | // Execute and verify no error occurs 42 | _, err := commands.NewOverlayRemoveCommand(ctx, serviceSet) 43 | if err != nil { 44 | t.Fatalf("Expected no error, got %v", err) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /doc/usage/gogh_clone.md: -------------------------------------------------------------------------------- 1 | ## gogh clone 2 | 3 | Clone remote repositories to local 4 | 5 | ``` 6 | gogh clone [flags] [[[/]/][=]...] 7 | ``` 8 | 9 | ### Examples 10 | 11 | ``` 12 | It accepts a short notation for a repository 13 | (for example, "github.com/kyoh86/example") like below. 14 | - "": e.g. "example"; 15 | - "/": e.g. "kyoh86/example" 16 | They'll be completed with the default host and owner set by "config set-default{-host|-owner}". 17 | 18 | It also accepts an alias for each repository. 19 | The alias is used for a local repository. 20 | For example: 21 | - "kyoh86/example=sample" 22 | - "kyoh86/example=kyoh86-tryouts/tryout" 23 | For each them will be cloned from "github.com/kyoh86/example" into the local as: 24 | - "$(gogh root)/github.com/kyoh86/sample" 25 | - "$(gogh root)/github.com/kyoh86-tryouts/tryout" 26 | ``` 27 | 28 | ### Options 29 | 30 | ``` 31 | -t, --clone-retry-timeout duration Timeout for each clone attempt (default 5m0s) 32 | --dry-run Displays the operations that would be performed using the specified command without actually running them 33 | -h, --help help for clone 34 | ``` 35 | 36 | ### SEE ALSO 37 | 38 | * [gogh](gogh.md) - GO GitHub local repository manager 39 | 40 | -------------------------------------------------------------------------------- /app/script/list/usecase.go: -------------------------------------------------------------------------------- 1 | package list 2 | 3 | import ( 4 | "context" 5 | "io" 6 | 7 | "github.com/kyoh86/gogh/v4/app/script/describe" 8 | "github.com/kyoh86/gogh/v4/core/script" 9 | ) 10 | 11 | type Usecase struct { 12 | scriptService script.ScriptService 13 | writer io.Writer 14 | } 15 | 16 | func NewUsecase( 17 | scriptService script.ScriptService, 18 | writer io.Writer, 19 | ) *Usecase { 20 | return &Usecase{ 21 | scriptService: scriptService, 22 | writer: writer, 23 | } 24 | } 25 | 26 | func (uc *Usecase) Execute(ctx context.Context, asJSON, withSource bool) error { 27 | var usecase interface { 28 | Execute(ctx context.Context, s describe.Script) error 29 | } 30 | if asJSON { 31 | if withSource { 32 | usecase = describe.NewJSONWithSourceUsecase(uc.scriptService, uc.writer) 33 | } else { 34 | usecase = describe.NewJSONUsecase(uc.writer) 35 | } 36 | } else { 37 | if withSource { 38 | usecase = describe.NewDetailUsecase(uc.scriptService, uc.writer) 39 | } else { 40 | usecase = describe.NewOnelineUsecase(uc.writer) 41 | } 42 | } 43 | for s, err := range uc.scriptService.List() { 44 | if err != nil { 45 | return err 46 | } 47 | if s == nil { 48 | continue 49 | } 50 | if err := usecase.Execute(ctx, s); err != nil { 51 | return err 52 | } 53 | } 54 | return nil 55 | } 56 | -------------------------------------------------------------------------------- /core/workspace/workspace_service.go: -------------------------------------------------------------------------------- 1 | package workspace 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/kyoh86/gogh/v4/core/store" 7 | ) 8 | 9 | var ( 10 | // ErrRootNotFound is an error when the root is not found 11 | ErrRootNotFound = errors.New("repository root not found") 12 | 13 | // ErrRootAlreadyExists is an error when the root already exists 14 | ErrRootAlreadyExists = errors.New("repository root already exists") 15 | 16 | // ErrNoPrimaryRoot is an error when no primary root is configured 17 | ErrNoPrimaryRoot = errors.New("no primary repository root configured") 18 | ) 19 | 20 | type Root = string 21 | 22 | // WorkspaceService manages a collection of repository roots 23 | type WorkspaceService interface { 24 | // GetRoots returns all registered roots 25 | GetRoots() []Root 26 | 27 | // GetPrimaryRoot returns the primary root 28 | GetPrimaryRoot() Root 29 | 30 | // GetLayoutFor returns a Layout for the root 31 | GetLayoutFor(root Root) LayoutService 32 | 33 | // GetPrimaryLayout returns a Layout for the primary root 34 | GetPrimaryLayout() LayoutService 35 | 36 | // SetPrimaryRoot sets the primary root 37 | SetPrimaryRoot(Root) error 38 | 39 | // AddRoot adds a new root 40 | AddRoot(root Root, asPrimary bool) error 41 | 42 | // RemoveRoot removes a root 43 | RemoveRoot(root Root) error 44 | 45 | store.Content 46 | } 47 | -------------------------------------------------------------------------------- /app/overlay/list/usecase.go: -------------------------------------------------------------------------------- 1 | package list 2 | 3 | import ( 4 | "context" 5 | "io" 6 | 7 | "github.com/kyoh86/gogh/v4/app/overlay/describe" 8 | "github.com/kyoh86/gogh/v4/core/overlay" 9 | ) 10 | 11 | type Usecase struct { 12 | overlayService overlay.OverlayService 13 | writer io.Writer 14 | } 15 | 16 | func NewUsecase( 17 | overlayService overlay.OverlayService, 18 | writer io.Writer, 19 | ) *Usecase { 20 | return &Usecase{ 21 | overlayService: overlayService, 22 | writer: writer, 23 | } 24 | } 25 | 26 | func (uc *Usecase) Execute(ctx context.Context, asJSON, withSource bool) error { 27 | var usecase interface { 28 | Execute(ctx context.Context, s describe.Overlay) error 29 | } 30 | if asJSON { 31 | if withSource { 32 | usecase = describe.NewJSONWithContentUsecase(uc.overlayService, uc.writer) 33 | } else { 34 | usecase = describe.NewJSONUsecase(uc.writer) 35 | } 36 | } else { 37 | if withSource { 38 | usecase = describe.NewDetailUsecase(uc.overlayService, uc.writer) 39 | } else { 40 | usecase = describe.NewOnelineUsecase(uc.writer) 41 | } 42 | } 43 | for s, err := range uc.overlayService.List() { 44 | if err != nil { 45 | return err 46 | } 47 | if s == nil { 48 | continue 49 | } 50 | if err := usecase.Execute(ctx, s); err != nil { 51 | return err 52 | } 53 | } 54 | return nil 55 | } 56 | -------------------------------------------------------------------------------- /ui/cli/commands/script_edit.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | "os" 6 | 7 | scriptedit "github.com/kyoh86/gogh/v4/app/script/edit" 8 | "github.com/kyoh86/gogh/v4/app/service" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | func NewScriptEditCommand(_ context.Context, svc *service.ServiceSet) (*cobra.Command, error) { 13 | cmd := &cobra.Command{ 14 | Use: "edit [flags] ", 15 | Short: "Edit an existing script (with $EDITOR)", 16 | Args: cobra.ExactArgs(1), 17 | RunE: func(cmd *cobra.Command, args []string) error { 18 | ctx := cmd.Context() 19 | scriptID := args[0] 20 | // Extract the script to a temporary file 21 | tmpFile, err := os.CreateTemp("", "gogh_script_edit_*.lua") 22 | if err != nil { 23 | return err 24 | } 25 | defer os.Remove(tmpFile.Name()) 26 | 27 | if err := scriptedit.NewUsecase(svc.ScriptService).ExtractScript(ctx, scriptID, tmpFile); err != nil { 28 | return err 29 | } 30 | tmpFile.Close() 31 | 32 | if err := edit(os.Getenv("EDITOR"), tmpFile.Name()); err != nil { 33 | return err 34 | } 35 | 36 | edited, err := os.Open(tmpFile.Name()) 37 | if err != nil { 38 | return err 39 | } 40 | defer edited.Close() 41 | return scriptedit.NewUsecase(svc.ScriptService).UpdateScript(ctx, scriptID, edited) 42 | }, 43 | } 44 | return cmd, nil 45 | } 46 | -------------------------------------------------------------------------------- /doc/usage/gogh_delete.md: -------------------------------------------------------------------------------- 1 | ## gogh delete 2 | 3 | Delete local and remote repository 4 | 5 | ``` 6 | gogh delete [flags] [[[/]/]] 7 | ``` 8 | 9 | ### Examples 10 | 11 | ``` 12 | It accepts a short notation for a repository 13 | (for example, "github.com/kyoh86/example") like below. 14 | - "": e.g. "example"; 15 | - "/": e.g. "kyoh86/example" 16 | They'll be completed with the default host and owner set by "config set-default{-host|-owner}". 17 | 18 | It also accepts an alias for each repository. 19 | The alias is a local name for the remote repository. 20 | For example: 21 | - "kyoh86/example=sample" 22 | - "kyoh86/example=kyoh86-tryouts/tryout" 23 | For each them will be cloned from "github.com/kyoh86/example" into the local as: 24 | - "$(gogh root)/github.com/kyoh86/sample" 25 | - "$(gogh root)/github.com/kyoh86-tryouts/tryout" 26 | ``` 27 | 28 | ### Options 29 | 30 | ``` 31 | --dry-run Displays the operations that would be performed using the specified command without actually running them 32 | --force Do NOT confirm to delete 33 | -h, --help help for delete 34 | --local Delete local repository (default true) 35 | --remote Delete remote repository 36 | ``` 37 | 38 | ### SEE ALSO 39 | 40 | * [gogh](gogh.md) - GO GitHub local repository manager 41 | 42 | -------------------------------------------------------------------------------- /ui/cli/commands/overlay_edit.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | "os" 6 | 7 | overlayedit "github.com/kyoh86/gogh/v4/app/overlay/edit" 8 | "github.com/kyoh86/gogh/v4/app/service" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | func NewOverlayEditCommand(_ context.Context, svc *service.ServiceSet) (*cobra.Command, error) { 13 | cmd := &cobra.Command{ 14 | Use: "edit [flags] ", 15 | Short: "Edit an existing overlay (with $EDITOR)", 16 | Args: cobra.ExactArgs(1), 17 | RunE: func(cmd *cobra.Command, args []string) error { 18 | ctx := cmd.Context() 19 | overlayID := args[0] 20 | // Extract the overlay to a temporary file 21 | tmpFile, err := os.CreateTemp("", "gogh_overlay_edit_*.lua") 22 | if err != nil { 23 | return err 24 | } 25 | defer os.Remove(tmpFile.Name()) 26 | 27 | if err := overlayedit.NewUsecase(svc.OverlayService).ExtractOverlay(ctx, overlayID, tmpFile); err != nil { 28 | return err 29 | } 30 | tmpFile.Close() 31 | 32 | if err := edit(os.Getenv("EDITOR"), tmpFile.Name()); err != nil { 33 | return err 34 | } 35 | 36 | edited, err := os.Open(tmpFile.Name()) 37 | if err != nil { 38 | return err 39 | } 40 | defer edited.Close() 41 | return overlayedit.NewUsecase(svc.OverlayService).UpdateOverlay(ctx, overlayID, edited) 42 | }, 43 | } 44 | return cmd, nil 45 | } 46 | -------------------------------------------------------------------------------- /core/overlay/overlay.go: -------------------------------------------------------------------------------- 1 | package overlay 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/google/uuid" 7 | ) 8 | 9 | type Entry struct { 10 | Name string 11 | RelativePath string 12 | Content io.Reader 13 | } 14 | 15 | // Overlay represents the metadata for an overlay entry. 16 | type Overlay interface { 17 | ID() string 18 | UUID() uuid.UUID 19 | Name() string 20 | RelativePath() string 21 | } 22 | 23 | // ConcreteOverlay creates an Overlay with the given parameters. 24 | func ConcreteOverlay( 25 | id uuid.UUID, 26 | name string, 27 | relativePath string, 28 | ) Overlay { 29 | return overlayElement{ 30 | id: id, 31 | name: name, 32 | relativePath: relativePath, 33 | } 34 | } 35 | 36 | func NewOverlay(entry Entry) Overlay { 37 | return overlayElement{ 38 | id: uuid.Must(uuid.NewRandom()), 39 | name: entry.Name, 40 | relativePath: entry.RelativePath, 41 | } 42 | } 43 | 44 | type overlayElement struct { 45 | id uuid.UUID 46 | name string 47 | relativePath string 48 | } 49 | 50 | func (o overlayElement) ID() string { 51 | return o.id.String() 52 | } 53 | 54 | func (o overlayElement) UUID() uuid.UUID { 55 | return o.id 56 | } 57 | 58 | func (o overlayElement) Name() string { 59 | return o.name 60 | } 61 | 62 | func (o overlayElement) RelativePath() string { 63 | return o.relativePath 64 | } 65 | -------------------------------------------------------------------------------- /ui/cli/commands/extra_create.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/kyoh86/gogh/v4/app/extra/create" 7 | "github.com/kyoh86/gogh/v4/app/service" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | func NewExtraCreateCommand(_ context.Context, svc *service.ServiceSet) (*cobra.Command, error) { 12 | usecase := create.NewUsecase( 13 | svc.WorkspaceService, 14 | svc.FinderService, 15 | svc.ExtraService, 16 | svc.OverlayService, 17 | svc.ReferenceParser, 18 | ) 19 | 20 | var opts create.Options 21 | 22 | cmd := &cobra.Command{ 23 | Use: "create ", 24 | Short: "Create a named extra template", 25 | Long: `Create a named extra template from overlays. 26 | 27 | This creates a reusable template that can be applied to any repository later.`, 28 | Args: cobra.ExactArgs(1), 29 | RunE: func(cmd *cobra.Command, args []string) error { 30 | opts.Name = args[0] 31 | return usecase.Execute(cmd.Context(), opts) 32 | }, 33 | } 34 | 35 | cmd.Flags().StringVarP(&opts.SourceRepo, "source", "s", "", "Source repository") 36 | cmd.Flags().StringSliceVarP(&opts.OverlayNames, "overlay", "o", nil, "Overlay names to include in the extra") 37 | if err := cmd.MarkFlagRequired("source"); err != nil { 38 | return nil, err 39 | } 40 | if err := cmd.MarkFlagRequired("overlay"); err != nil { 41 | return nil, err 42 | } 43 | 44 | return cmd, nil 45 | } 46 | -------------------------------------------------------------------------------- /doc/usage/gogh_script_invoke-instant.md: -------------------------------------------------------------------------------- 1 | ## gogh script invoke-instant 2 | 3 | Run a temporary script in a repository without storing it 4 | 5 | ``` 6 | gogh script invoke-instant [flags] [[[/]/]...] 7 | ``` 8 | 9 | ### Examples 10 | 11 | ``` 12 | invoke-instant --file script.lua repo1 repo2 13 | invoke-instant --file - repo1 < script.lua 14 | echo 'print(gogh.repo.name)' | gogh script invoke-instant --file - repo1 15 | invoke-instant --file script.lua . # Use current directory repository 16 | invoke-instant --file script.lua --all 17 | invoke-instant --file script.lua --pattern 18 | 19 | It accepts a short notation for each repository 20 | (for example, "github.com/kyoh86/example") like below. 21 | - "": e.g. "example"; 22 | - "/": e.g. "kyoh86/example" 23 | - "." for the current directory repository 24 | They'll be completed with the default host and owner set by "config set-default{-host|-owner}". 25 | ``` 26 | 27 | ### Options 28 | 29 | ``` 30 | --all Apply to all repositories in the workspace 31 | -f, --file string Path to script file to invoke (use '-' for stdin) 32 | -h, --help help for invoke-instant 33 | -p, --pattern strings Patterns for selecting repositories 34 | ``` 35 | 36 | ### SEE ALSO 37 | 38 | * [gogh script](gogh_script.md) - Manage repository script files 39 | 40 | -------------------------------------------------------------------------------- /app/config/flags_store.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/kyoh86/gogh/v4/core/store" 9 | ) 10 | 11 | // FlagsStore is a store for flags. 12 | type FlagsStore struct{} 13 | 14 | // NewFlagsStore creates a new FlagsStore. 15 | func NewFlagsStore() *FlagsStore { 16 | return &FlagsStore{} 17 | } 18 | 19 | // Load implements store.Loader 20 | func (s *FlagsStore) Load(ctx context.Context, initial func() *Flags) (*Flags, error) { 21 | source, err := s.Source() 22 | if err != nil { 23 | return nil, err 24 | } 25 | 26 | v, err := loadTOMLFile[Flags](source) 27 | if err != nil { 28 | return nil, err 29 | } 30 | 31 | return v, nil 32 | } 33 | 34 | // Save implements repository.FlagsRepository. 35 | func (d *FlagsStore) Save(ctx context.Context, flags *Flags, force bool) error { 36 | if !flags.HasChanges() && !force { 37 | return nil 38 | } 39 | source, err := d.Source() 40 | if err != nil { 41 | return err 42 | } 43 | if err := saveTOMLFile(source, flags); err != nil { 44 | return err 45 | } 46 | flags.MarkSaved() 47 | return nil 48 | } 49 | 50 | func (s *FlagsStore) Source() (string, error) { 51 | path, err := AppContextPathFunc("GOGH_FLAG_PATH", os.UserConfigDir, "flags.v4.toml") 52 | if err != nil { 53 | return "", fmt.Errorf("search flags path: %w", err) 54 | } 55 | return path, nil 56 | } 57 | 58 | var _ store.Store[*Flags] = (*FlagsStore)(nil) 59 | -------------------------------------------------------------------------------- /typ/ptr_test.go: -------------------------------------------------------------------------------- 1 | package typ_test 2 | 3 | import ( 4 | "testing" 5 | 6 | testtarget "github.com/kyoh86/gogh/v4/typ" 7 | ) 8 | 9 | func TestPtr(t *testing.T) { 10 | t.Run("int", func(t *testing.T) { 11 | val := 42 12 | ptr := testtarget.Ptr(val) 13 | if *ptr != val { 14 | t.Errorf("ptr() = %v, want %v", *ptr, val) 15 | } 16 | }) 17 | 18 | t.Run("string", func(t *testing.T) { 19 | val := "hello" 20 | ptr := testtarget.Ptr(val) 21 | if *ptr != val { 22 | t.Errorf("ptr() = %v, want %v", *ptr, val) 23 | } 24 | }) 25 | } 26 | 27 | func TestNilablePtr(t *testing.T) { 28 | t.Run("int non-zero", func(t *testing.T) { 29 | val := 42 30 | ptr := testtarget.NilablePtr(val) 31 | if ptr == nil || *ptr != val { 32 | t.Errorf("nilablePtr() = %v, want %v", ptr, val) 33 | } 34 | }) 35 | 36 | t.Run("int zero", func(t *testing.T) { 37 | val := 0 38 | ptr := testtarget.NilablePtr(val) 39 | if ptr != nil { 40 | t.Errorf("nilablePtr() = %v, want nil", ptr) 41 | } 42 | }) 43 | 44 | t.Run("string non-empty", func(t *testing.T) { 45 | val := "hello" 46 | ptr := testtarget.NilablePtr(val) 47 | if ptr == nil || *ptr != val { 48 | t.Errorf("nilablePtr() = %v, want %v", ptr, val) 49 | } 50 | }) 51 | 52 | t.Run("string empty", func(t *testing.T) { 53 | val := "" 54 | ptr := testtarget.NilablePtr(val) 55 | if ptr != nil { 56 | t.Errorf("nilablePtr() = %v, want nil", ptr) 57 | } 58 | }) 59 | } 60 | -------------------------------------------------------------------------------- /app/script/show/usecase.go: -------------------------------------------------------------------------------- 1 | package show 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | 8 | "github.com/kyoh86/gogh/v4/app/script/describe" 9 | "github.com/kyoh86/gogh/v4/core/script" 10 | ) 11 | 12 | // Usecase for running script scripts 13 | type Usecase struct { 14 | scriptService script.ScriptService 15 | writer io.Writer 16 | } 17 | 18 | func NewUsecase( 19 | scriptService script.ScriptService, 20 | writer io.Writer, 21 | ) *Usecase { 22 | return &Usecase{ 23 | scriptService: scriptService, 24 | writer: writer, 25 | } 26 | } 27 | 28 | func (uc *Usecase) Execute(ctx context.Context, scriptID string, asJSON, withSource bool) error { 29 | script, err := uc.scriptService.Get(ctx, scriptID) 30 | if err != nil { 31 | return fmt.Errorf("get script by ID: %w", err) 32 | } 33 | var usecase interface { 34 | Execute(ctx context.Context, s describe.Script) error 35 | } 36 | if asJSON { 37 | if withSource { 38 | usecase = describe.NewJSONWithSourceUsecase(uc.scriptService, uc.writer) 39 | } else { 40 | usecase = describe.NewJSONUsecase(uc.writer) 41 | } 42 | } else { 43 | if withSource { 44 | usecase = describe.NewDetailUsecase(uc.scriptService, uc.writer) 45 | } else { 46 | usecase = describe.NewOnelineUsecase(uc.writer) 47 | } 48 | } 49 | if err := usecase.Execute(ctx, script); err != nil { 50 | return fmt.Errorf("execute description: %w", err) 51 | } 52 | return nil 53 | } 54 | -------------------------------------------------------------------------------- /core/repository/validate.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "errors" 5 | "net/url" 6 | "regexp" 7 | ) 8 | 9 | var ( 10 | ErrEmptyHost = errors.New("empty host") 11 | ErrEmptyOwner = errors.New("empty owner") 12 | ErrEmptyName = errors.New("empty name") 13 | ) 14 | 15 | // ValidateHost validates a host string. 16 | func ValidateHost(host string) error { 17 | if host == "" { 18 | return ErrEmptyHost 19 | } 20 | u, err := url.ParseRequestURI("https://" + host) 21 | if err != nil { 22 | return errors.New("invalid host: " + host) 23 | } 24 | if u.Host != host { 25 | return errors.New("invalid host: " + host) 26 | } 27 | return nil 28 | } 29 | 30 | var validOwnerRegexp = regexp.MustCompile(`^[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*$`) 31 | 32 | func ValidateOwner(owner string) error { 33 | if owner == "" { 34 | return ErrEmptyOwner 35 | } 36 | if !validOwnerRegexp.MatchString(owner) { 37 | return errors.New("invalid owner: " + owner) 38 | } 39 | return nil 40 | } 41 | 42 | var invalidNameRegexp = regexp.MustCompile(`[^\w\-\.]`) 43 | 44 | func ValidateName(name string) error { 45 | if name == "" { 46 | return ErrEmptyName 47 | } 48 | if name == "." { 49 | return errors.New("'.' is reserved name") 50 | } 51 | if name == ".." { 52 | return errors.New("'..' is reserved name") 53 | } 54 | if invalidNameRegexp.MatchString(name) { 55 | return errors.New("invalid name: " + name) 56 | } 57 | return nil 58 | } 59 | -------------------------------------------------------------------------------- /ui/cli/commands/overlay_update.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "os" 8 | 9 | "github.com/kyoh86/gogh/v4/app/overlay/update" 10 | "github.com/kyoh86/gogh/v4/app/service" 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | func NewOverlayUpdateCommand(_ context.Context, svc *service.ServiceSet) (*cobra.Command, error) { 15 | var f struct { 16 | name string 17 | relativePath string 18 | sourcePath string 19 | } 20 | cmd := &cobra.Command{ 21 | Use: "update [flags] ", 22 | Short: "Update an existing overlay", 23 | Args: cobra.ExactArgs(1), 24 | RunE: func(cmd *cobra.Command, args []string) error { 25 | ctx := cmd.Context() 26 | overlayID := args[0] 27 | var content io.Reader 28 | if f.sourcePath != "" { 29 | c, err := os.Open(f.sourcePath) 30 | if err != nil { 31 | return err 32 | } 33 | defer c.Close() 34 | content = c 35 | } 36 | if err := update.NewUsecase(svc.OverlayService).Execute(ctx, overlayID, f.name, f.relativePath, content); err != nil { 37 | return fmt.Errorf("updating overlay: %w", err) 38 | } 39 | return nil 40 | }, 41 | } 42 | cmd.Flags().StringVar(&f.name, "name", "", "Name of the overlay") 43 | cmd.Flags().StringVar(&f.relativePath, "relative-path", "", "Relative path of the overlay in the repository") 44 | cmd.Flags().StringVar(&f.sourcePath, "source", "", "Overlay source file path") 45 | return cmd, nil 46 | } 47 | -------------------------------------------------------------------------------- /.github/actions/pkgbuild/PKGBUILD.tmpl: -------------------------------------------------------------------------------- 1 | # Maintainer: ${OWNER}${EMAIL} 2 | pkgname=$NAME 3 | pkgver=$VERSION 4 | pkgrel=1 5 | pkgdesc='${DESCRIPTION}' 6 | url="${SERVER_URL}/${REPOSITORY}" 7 | arch=('x86_64') 8 | license=('$LICENSE') 9 | makedepends=('go') 10 | depends=('glibc') 11 | source=("$url/archive/refs/tags/v$pkgver.tar.gz") 12 | options=('zipman') 13 | sha256sums=(.) 14 | prepare(){ 15 | cd "$pkgname-$pkgver" || exit 1 16 | mkdir -p build/ 17 | } 18 | build() { 19 | cd "$pkgname-$pkgver" || exit 1 20 | export CGO_CPPFLAGS="${CPPFLAGS}" 21 | export CGO_CFLAGS="${CFLAGS}" 22 | export CGO_CXXFLAGS="${CXXFLAGS}" 23 | export CGO_LDFLAGS="${LDFLAGS}" 24 | LDF="-linkmode=external -s -w -X main.version=${VERSION} -X main.commit=${COMMIT} -X main.date=$(date --iso-8601=seconds)" 25 | go build \ 26 | -trimpath \ 27 | -buildmode=pie \ 28 | -mod=readonly \ 29 | -modcacherw \ 30 | -ldflags="${LDF}" \ 31 | -o build ./cmd/... 32 | go run -ldflags="${LDF}" ./cmd/gogh man 33 | } 34 | check() { 35 | cd "$pkgname-$pkgver" || exit 1 36 | go test ./... 37 | } 38 | package() { 39 | cd "$pkgname-$pkgver" || exit 1 40 | install -Dm755 build/$pkgname "$pkgdir/usr/bin/$pkgname" 41 | if [ -f LICENSE ]; then 42 | install -Dm644 LICENSE "$pkgdir/usr/share/licenses/$pkgname/LICENSE" 43 | fi 44 | if [ -f "./doc/man/$pkgname.1" ]; then 45 | install -Dm644 "./doc/man/$pkgname.1" "$pkgdir/usr/share/man/man1/$pkgname.1" 46 | fi 47 | } 48 | -------------------------------------------------------------------------------- /app/overlay/show/usecase.go: -------------------------------------------------------------------------------- 1 | package show 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | 8 | "github.com/kyoh86/gogh/v4/app/overlay/describe" 9 | "github.com/kyoh86/gogh/v4/core/overlay" 10 | ) 11 | 12 | // Usecase for running overlay overlays 13 | type Usecase struct { 14 | overlayService overlay.OverlayService 15 | writer io.Writer 16 | } 17 | 18 | func NewUsecase( 19 | overlayService overlay.OverlayService, 20 | writer io.Writer, 21 | ) *Usecase { 22 | return &Usecase{ 23 | overlayService: overlayService, 24 | writer: writer, 25 | } 26 | } 27 | 28 | func (uc *Usecase) Execute(ctx context.Context, overlayID string, asJSON, withSource bool) error { 29 | overlay, err := uc.overlayService.Get(ctx, overlayID) 30 | if err != nil { 31 | return fmt.Errorf("get overlay by ID: %w", err) 32 | } 33 | var usecase interface { 34 | Execute(ctx context.Context, s describe.Overlay) error 35 | } 36 | if asJSON { 37 | if withSource { 38 | usecase = describe.NewJSONWithContentUsecase(uc.overlayService, uc.writer) 39 | } else { 40 | usecase = describe.NewJSONUsecase(uc.writer) 41 | } 42 | } else { 43 | if withSource { 44 | usecase = describe.NewDetailUsecase(uc.overlayService, uc.writer) 45 | } else { 46 | usecase = describe.NewOnelineUsecase(uc.writer) 47 | } 48 | } 49 | if err := usecase.Execute(ctx, overlay); err != nil { 50 | return fmt.Errorf("execute description: %w", err) 51 | } 52 | return nil 53 | } 54 | -------------------------------------------------------------------------------- /core/repository/location.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "path" 5 | ) 6 | 7 | // Location is a struct that contains information about a repository location. 8 | type Location struct { 9 | fullPath string 10 | path string 11 | host string 12 | owner string 13 | name string 14 | } 15 | 16 | // FullPath is a full path of the repository (e.g.: "/path/to/root/github.com/kyoh86/gogh") 17 | func (r *Location) FullPath() string { return r.fullPath } 18 | 19 | // Host is a hostname (e.g.: "github.com") 20 | func (r *Location) Host() string { return r.host } 21 | 22 | // Owner is a owner name (e.g.: "kyoh86") 23 | func (r *Location) Owner() string { return r.owner } 24 | 25 | // Name of the repository (e.g.: "gogh") 26 | func (r *Location) Name() string { return r.name } 27 | 28 | // Path returns the path from a root of the repository (e.g.: "github.com/kyoh86/gogh") 29 | func (r *Location) Path() string { 30 | return r.path 31 | } 32 | 33 | // Ref returns a reference to the repository location. 34 | func (r *Location) Ref() Reference { 35 | return NewReference(r.host, r.owner, r.name) 36 | } 37 | 38 | // NewLocation will build a repository location with a full path, host, owner and name. 39 | func NewLocation(fullPath string, host, owner, name string) *Location { 40 | return &Location{ 41 | fullPath: fullPath, 42 | path: path.Join(host, owner, name), 43 | host: host, 44 | owner: owner, 45 | name: name, 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://goreleaser.com/static/schema.json 2 | 3 | version: 2 4 | project_name: gogh 5 | builds: 6 | - id: default 7 | goos: 8 | - linux 9 | - darwin 10 | - windows 11 | goarch: 12 | - amd64 13 | - arm64 14 | main: ./cmd/gogh 15 | binary: gogh 16 | ldflags: -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}} 17 | hooks: 18 | post: 19 | - make man VERSION={{.Version}} COMMIT={{.ShortCommit}} DATE={{.Date}} 20 | brews: 21 | - install: | 22 | bin.install "gogh" 23 | man1.install Dir.glob('gogh*.1') 24 | repository: 25 | owner: kyoh86 26 | name: homebrew-tap 27 | directory: Formula 28 | homepage: https://github.com/kyoh86/gogh 29 | description: GO GitHub project manager 30 | license: MIT 31 | nfpms: 32 | - ids: 33 | - default 34 | maintainer: kyoh86 35 | homepage: https://github.com/kyoh86/gogh 36 | description: GO GitHub project manager 37 | license: MIT 38 | formats: 39 | - apk 40 | - deb 41 | - rpm 42 | archives: 43 | - id: gzip 44 | formats: ['tar.gz'] 45 | format_overrides: 46 | - goos: windows 47 | formats: ['zip'] 48 | files: 49 | - licence* 50 | - LICENCE* 51 | - license* 52 | - LICENSE* 53 | - readme* 54 | - README* 55 | - changelog* 56 | - CHANGELOG* 57 | -------------------------------------------------------------------------------- /doc/usage/gogh_cwd.md: -------------------------------------------------------------------------------- 1 | ## gogh cwd 2 | 3 | Print the local repository which the current working directory belongs to 4 | 5 | ``` 6 | gogh cwd [flags] 7 | ``` 8 | 9 | ### Options 10 | 11 | ``` 12 | -f, --format string 13 | Print local repository in a given format, where [format] can be one of "path", 14 | "full-path", "json", "fields" and "fields:[separator]". 15 | 16 | - path: 17 | 18 | A part of the URL to specify a repository. For example: "github.com/kyoh86/gogh" 19 | 20 | - full-path 21 | 22 | A full path of the local repository. For example: 23 | "/root/Projects/github.com/kyoh86/gogh". 24 | 25 | - fields 26 | 27 | Tab separated all formats and properties of the local repository. 28 | i.e. [full-path]\t[path]\t[host]\t[owner]\t[name] 29 | 30 | - fields:[separator] 31 | 32 | Like "fields" but with the explicit separator. 33 | 34 | -h, --help help for cwd 35 | ``` 36 | 37 | ### SEE ALSO 38 | 39 | * [gogh](gogh.md) - GO GitHub local repository manager 40 | 41 | -------------------------------------------------------------------------------- /doc/usage/gogh_overlay_apply.md: -------------------------------------------------------------------------------- 1 | ## gogh overlay apply 2 | 3 | Apply an overlay to a repository 4 | 5 | ``` 6 | gogh overlay apply [flags] [[/]/] 7 | ``` 8 | 9 | ### Examples 10 | 11 | ``` 12 | invoke [flags] [[[/]/]...] 13 | invoke [flags] --all 14 | invoke [flags] --pattern [--pattern ]... 15 | 16 | It accepts a short notation for each repository 17 | (for example, "github.com/kyoh86/example") like below. 18 | - "": e.g. "example"; 19 | - "/": e.g. "kyoh86/example" 20 | - "." for the current directory repository 21 | They'll be completed with the default host and owner set by "config set-default{-host|-owner}". 22 | 23 | It also accepts an alias for each repository. 24 | The alias is a local name for the remote repository. 25 | For example: 26 | - "kyoh86/example=sample" 27 | - "kyoh86/example=kyoh86-tryouts/tryout" 28 | For each them will be cloned from "github.com/kyoh86/example" into the local as: 29 | - "$(gogh root)/github.com/kyoh86/sample" 30 | - "$(gogh root)/github.com/kyoh86-tryouts/tryout" 31 | ``` 32 | 33 | ### Options 34 | 35 | ``` 36 | --all Apply to all repositories in the workspace 37 | -h, --help help for apply 38 | -p, --pattern strings Patterns for selecting repositories 39 | ``` 40 | 41 | ### SEE ALSO 42 | 43 | * [gogh overlay](gogh_overlay.md) - Manage repository overlay files 44 | 45 | -------------------------------------------------------------------------------- /doc/usage/gogh_script_invoke.md: -------------------------------------------------------------------------------- 1 | ## gogh script invoke 2 | 3 | Invoke an script in a repository 4 | 5 | ``` 6 | gogh script invoke [flags] [[[/]/]...] 7 | ``` 8 | 9 | ### Examples 10 | 11 | ``` 12 | invoke [flags] [[[/]/]...] 13 | invoke [flags] --all 14 | invoke [flags] --pattern [--pattern ]... 15 | 16 | It accepts a short notation for each repository 17 | (for example, "github.com/kyoh86/example") like below. 18 | - "": e.g. "example"; 19 | - "/": e.g. "kyoh86/example" 20 | - "." for the current directory repository 21 | They'll be completed with the default host and owner set by "config set-default{-host|-owner}". 22 | 23 | It also accepts an alias for each repository. 24 | The alias is a local name for the remote repository. 25 | For example: 26 | - "kyoh86/example=sample" 27 | - "kyoh86/example=kyoh86-tryouts/tryout" 28 | For each them will be cloned from "github.com/kyoh86/example" into the local as: 29 | - "$(gogh root)/github.com/kyoh86/sample" 30 | - "$(gogh root)/github.com/kyoh86-tryouts/tryout" 31 | ``` 32 | 33 | ### Options 34 | 35 | ``` 36 | --all Apply to all repositories in the workspace 37 | -h, --help help for invoke 38 | -p, --pattern strings Patterns for selecting repositories 39 | ``` 40 | 41 | ### SEE ALSO 42 | 43 | * [gogh script](gogh_script.md) - Manage repository script files 44 | 45 | -------------------------------------------------------------------------------- /app/extra/list/usecase.go: -------------------------------------------------------------------------------- 1 | package list 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "iter" 7 | 8 | "github.com/kyoh86/gogh/v4/app/extra/describe" 9 | "github.com/kyoh86/gogh/v4/core/extra" 10 | ) 11 | 12 | // Usecase represents the extra list use case 13 | type Usecase struct { 14 | extraService extra.ExtraService 15 | writer io.Writer 16 | } 17 | 18 | // NewUsecase creates a new extra list use case 19 | func NewUsecase(extraService extra.ExtraService, writer io.Writer) *Usecase { 20 | return &Usecase{ 21 | extraService: extraService, 22 | writer: writer, 23 | } 24 | } 25 | 26 | // Execute performs the extra list operation 27 | func (uc *Usecase) Execute(ctx context.Context, asJSON bool, extraType string) error { 28 | var usecase interface { 29 | Execute(ctx context.Context, e describe.Extra) error 30 | } 31 | 32 | if asJSON { 33 | usecase = describe.NewJSONUsecase(uc.writer) 34 | } else { 35 | usecase = describe.NewOnelineUsecase(uc.writer) 36 | } 37 | 38 | var list iter.Seq2[*extra.Extra, error] 39 | switch extraType { 40 | case "auto": 41 | list = uc.extraService.ListByType(ctx, extra.TypeAuto) 42 | case "named": 43 | list = uc.extraService.ListByType(ctx, extra.TypeNamed) 44 | default: // "all" 45 | list = uc.extraService.List(ctx) 46 | } 47 | 48 | for e, err := range list { 49 | if err != nil { 50 | return err 51 | } 52 | if e == nil { 53 | continue 54 | } 55 | if err := usecase.Execute(ctx, *e); err != nil { 56 | return err 57 | } 58 | } 59 | 60 | return nil 61 | } 62 | -------------------------------------------------------------------------------- /doc/usage/gogh_repos.md: -------------------------------------------------------------------------------- 1 | ## gogh repos 2 | 3 | List remote repositories 4 | 5 | ``` 6 | gogh repos [flags] 7 | ``` 8 | 9 | ### Options 10 | 11 | ``` 12 | --archive string Show only archived/not-archived repositories; it can accept "archived" or "not-archived" 13 | --color string Colorize the output; it can accept "auto", "always" or "never" (default "auto") 14 | --fork string Show only forked/not-forked repositories; it can accept "forked" or "not-forked" 15 | -f, --format string 16 | Print each repository in a given format, where [format] can be one of "table", "ref", 17 | "url" or "json". 18 | 19 | -h, --help help for repos 20 | --limit int Max number of repositories to list. -1 means unlimited (default 30) 21 | --order sort Directions in which to order a list of items when provided a sort flag; it can accept "asc", "ascending", "desc" or "descending" 22 | --privacy string Show only public/private repositories; it can accept "private" or "public" 23 | --relation strings The relation of user to each repository; it can accept "owner", "organization-member" or "collaborator" (default [owner,organization-member]) 24 | --sort string Property by which repository be ordered; it can accept "created-at", "name", "pushed-at", "stargazers" or "updated-at" 25 | ``` 26 | 27 | ### SEE ALSO 28 | 29 | * [gogh](gogh.md) - GO GitHub local repository manager 30 | 31 | -------------------------------------------------------------------------------- /ui/cli/view/process_with_confirmation.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "iter" 7 | 8 | "github.com/apex/log" 9 | "github.com/charmbracelet/huh" 10 | ) 11 | 12 | var ErrQuit = errors.New("quit the process") 13 | 14 | // ProcessWithConfirmation processes each entry in the sequence with a confirmation prompt. 15 | func ProcessWithConfirmation[T any](ctx context.Context, seq iter.Seq2[T, error], title func(T) string, process func(entry T) error) error { 16 | logger := log.FromContext(ctx) 17 | var all bool 18 | for entry, err := range seq { 19 | if err != nil { 20 | return err 21 | } 22 | if all { 23 | if err := process(entry); err != nil { 24 | return err 25 | } 26 | continue 27 | } 28 | var selected string 29 | if err := huh.NewForm(huh.NewGroup( 30 | huh.NewInput(). 31 | CharLimit(1). 32 | Inline(true). 33 | Title(title(entry) + " "). 34 | Validate(func(s string) error { 35 | if s == "y" || s == "n" || s == "q" || s == "a" { 36 | return nil 37 | } 38 | return errors.New("invalid selection") 39 | }). 40 | Prompt("(y/n/q/a): "). 41 | Value(&selected), 42 | )).Run(); err != nil { 43 | return err 44 | } 45 | switch selected { 46 | case "a": 47 | all = true 48 | fallthrough 49 | case "y": 50 | if err := process(entry); err != nil { 51 | return err 52 | } 53 | case "n": 54 | logger.Info("Skipped") 55 | case "q": 56 | logger.Info("Quit") 57 | return ErrQuit 58 | } 59 | } 60 | return nil 61 | } 62 | -------------------------------------------------------------------------------- /app/auth/login/usecase.go: -------------------------------------------------------------------------------- 1 | package login 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/kyoh86/gogh/v4/core/auth" 8 | "github.com/kyoh86/gogh/v4/core/hosting" 9 | ) 10 | 11 | // Usecase is the services to handle login authentication. 12 | type Usecase struct { 13 | tokenService auth.TokenService 14 | authService auth.AuthenticateService 15 | hostingService hosting.HostingService 16 | } 17 | 18 | // NewUsecase creates a new Usecase instance with the provided services. 19 | func NewUsecase( 20 | tokenService auth.TokenService, 21 | authService auth.AuthenticateService, 22 | hostingService hosting.HostingService, 23 | ) *Usecase { 24 | return &Usecase{ 25 | tokenService: tokenService, 26 | authService: authService, 27 | hostingService: hostingService, 28 | } 29 | } 30 | 31 | // DeviceAuthResponse represents the response from a device authentication request. 32 | type DeviceAuthResponse = auth.DeviceAuthResponse 33 | 34 | // Verify is a function type to verify the authentication response. 35 | type Verify = auth.Verify 36 | 37 | // Execute performs the authentication process. 38 | func (uc *Usecase) Execute(ctx context.Context, host string, verify Verify) error { 39 | user, token, err := uc.authService.Authenticate(ctx, host, verify) 40 | if err != nil { 41 | return fmt.Errorf("authenticating: %w", err) 42 | } 43 | if token == nil { 44 | return fmt.Errorf("token is nil") 45 | } 46 | if err := uc.tokenService.Set(host, user, *token); err != nil { 47 | return fmt.Errorf("setting token: %w", err) 48 | } 49 | return nil 50 | } 51 | -------------------------------------------------------------------------------- /core/overlay/content_store_mock_test.go: -------------------------------------------------------------------------------- 1 | package overlay 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "io" 7 | "path/filepath" 8 | 9 | "github.com/spf13/afero" 10 | ) 11 | 12 | // MockContentStore implements ContentStore using afero 13 | type MockContentStore struct { 14 | fs afero.Fs 15 | baseDir string 16 | } 17 | 18 | func NewMockContentStore() *MockContentStore { 19 | return &MockContentStore{ 20 | fs: afero.NewMemMapFs(), 21 | baseDir: "/content", 22 | } 23 | } 24 | 25 | func (a *MockContentStore) Save(ctx context.Context, overlayID string, content io.Reader) error { 26 | if content == nil { 27 | return errors.New("content is nil") 28 | } 29 | 30 | // Create base directory if it doesn't exist 31 | if err := a.fs.MkdirAll(a.baseDir, 0755); err != nil { 32 | return err 33 | } 34 | 35 | // Generate a unique location 36 | location := filepath.Join(a.baseDir, overlayID) 37 | 38 | // Create the file 39 | file, err := a.fs.Create(location) 40 | if err != nil { 41 | return err 42 | } 43 | defer file.Close() 44 | 45 | // Copy content to file 46 | _, err = io.Copy(file, content) 47 | if err != nil { 48 | return err 49 | } 50 | 51 | return nil 52 | } 53 | 54 | func (a *MockContentStore) Open(ctx context.Context, overlayID string) (io.ReadCloser, error) { 55 | location := filepath.Join(a.baseDir, overlayID) 56 | return a.fs.Open(location) 57 | } 58 | 59 | func (a *MockContentStore) Remove(ctx context.Context, overlayID string) error { 60 | location := filepath.Join(a.baseDir, overlayID) 61 | return a.fs.Remove(location) 62 | } 63 | -------------------------------------------------------------------------------- /core/script/script.go: -------------------------------------------------------------------------------- 1 | package script 2 | 3 | import ( 4 | "io" 5 | "time" 6 | 7 | "github.com/google/uuid" 8 | ) 9 | 10 | type Entry struct { 11 | Name string 12 | Content io.Reader 13 | } 14 | 15 | type Script interface { 16 | ID() string 17 | UUID() uuid.UUID 18 | Name() string 19 | CreatedAt() time.Time 20 | UpdatedAt() time.Time 21 | } 22 | 23 | // NewScript creates a new Script with the given entry. 24 | func NewScript(entry Entry) Script { 25 | now := time.Now() 26 | return scriptElement{ 27 | id: uuid.Must(uuid.NewRandom()), 28 | name: entry.Name, 29 | createdAt: now, 30 | updatedAt: now, 31 | } 32 | } 33 | 34 | // ConcreteScript creates a Script with the given parameters. 35 | func ConcreteScript(id uuid.UUID, name string, createdAt, updatedAt time.Time) Script { 36 | return &scriptElement{ 37 | id: id, 38 | name: name, 39 | createdAt: createdAt, 40 | updatedAt: updatedAt, 41 | } 42 | } 43 | 44 | type scriptElement struct { 45 | id uuid.UUID 46 | name string 47 | 48 | createdAt time.Time 49 | updatedAt time.Time 50 | } 51 | 52 | func (h scriptElement) ID() string { 53 | return h.id.String() 54 | } 55 | 56 | func (h scriptElement) UUID() uuid.UUID { 57 | return h.id 58 | } 59 | 60 | func (h scriptElement) Name() string { 61 | return h.name 62 | } 63 | 64 | func (h scriptElement) CreatedAt() time.Time { 65 | return h.createdAt 66 | } 67 | 68 | func (h scriptElement) UpdatedAt() time.Time { 69 | return h.updatedAt 70 | } 71 | 72 | func (h *scriptElement) update() { 73 | h.updatedAt = time.Now() 74 | } 75 | -------------------------------------------------------------------------------- /ui/cli/commands/bundle_dump.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/kyoh86/gogh/v4/app/config" 9 | "github.com/kyoh86/gogh/v4/app/dump" 10 | "github.com/kyoh86/gogh/v4/app/service" 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | func NewBundleDumpCommand(_ context.Context, svc *service.ServiceSet) (*cobra.Command, error) { 15 | var f config.BundleDumpFlags 16 | cmd := &cobra.Command{ 17 | Use: "dump", 18 | Aliases: []string{"export"}, 19 | Short: "Export current local repository list", 20 | Args: cobra.NoArgs, 21 | RunE: func(cmd *cobra.Command, _ []string) error { 22 | out := cmd.OutOrStdout() 23 | if f.File != "" && f.File != "-" { 24 | file, err := os.OpenFile( 25 | f.File, 26 | os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 27 | 0644, 28 | ) 29 | if err != nil { 30 | return fmt.Errorf("opening file: %w", err) 31 | } 32 | defer file.Close() 33 | out = file 34 | } 35 | for entry, err := range dump.NewUsecase(svc.WorkspaceService, svc.FinderService, svc.HostingService, svc.GitService).Execute(cmd.Context(), dump.Options{}) { 36 | if err != nil { 37 | return err 38 | } 39 | if entry.Alias == nil { 40 | fmt.Fprintln(out, entry.Name) 41 | } else { 42 | fmt.Fprintf(out, "%s=%s\n", *entry.Alias, entry.Name) 43 | } 44 | } 45 | return nil 46 | }, 47 | } 48 | 49 | cmd.Flags().StringVarP(&f.File, "file", "f", svc.Flags.BundleDump.File, `A file to output; if it's empty("") or hyphen("-"), output to stdout`) 50 | return cmd, nil 51 | } 52 | -------------------------------------------------------------------------------- /.github/actions/pkgbuild/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | pacman -Syu --noconfirm --needed base-devel pacman-contrib 5 | 6 | # Makepkg does not allow running as root 7 | # Create a new user `builder` 8 | # `builder` needs to have a home directory because some PKGBUILDs will try to 9 | # write to it (e.g. for cache) 10 | useradd builder -m 11 | 12 | # When installing dependencies, makepkg will use sudo 13 | # Give user `builder` passwordless sudo access 14 | echo "builder ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers 15 | 16 | # Give all users (particularly builder) full access to these files 17 | chmod -R a+rw . 18 | 19 | export VERSION="${GITHUB_REF##*/v}" 20 | if [[ "${VERSION}" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then 21 | : 22 | else 23 | FILE="$(basename "$0")" 24 | echo "::error file=$FILE,line=$LINENO::Ref ${GITHUB_REF} mismatched as a tag for the semantic-version" 25 | exit 1 26 | fi 27 | export COMMIT="${GITHUB_SHA}" 28 | export NAME="${GITHUB_REPOSITORY#*/}" 29 | export OWNER="${GITHUB_REPOSITORY%/*}" 30 | export REPOSITORY="${GITHUB_REPOSITORY}" 31 | export SERVER_URL="${GITHUB_SERVER_URL}" 32 | if [ -n "${EMAIL}" ]; then 33 | export EMAIL=" <${EMAIL}>" 34 | fi 35 | export DESCRIPTION 36 | export LICENSE 37 | envsubst "\$VERSION \$COMMIT \$NAME \$DESCRIPTION \$LICENSE \$OWNER \$REPOSITORY \$SERVER_URL \$EMAIL" < /PKGBUILD.tmpl > PKGBUILD 38 | chmod 777 PKGBUILD 39 | sudo -H -u builder updpkgsums PKGBUILD 40 | sudo -H -u builder makepkg --printsrcinfo PKGBUILD | sudo -H -u builder tee .SRCINFO 41 | sudo -H -u builder tar -cvzf "${NAME}_PKGBUILD.tar.gz" PKGBUILD .SRCINFO 42 | -------------------------------------------------------------------------------- /core/hosting/repository.go: -------------------------------------------------------------------------------- 1 | package hosting 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/kyoh86/gogh/v4/core/repository" 7 | ) 8 | 9 | // Repository represents a repository on a remote source 10 | type Repository struct { 11 | // Ref is a reference of the repository 12 | Ref repository.Reference 13 | // URL is a full URL for the repository (e.g.: "https://github.com/kyoh86/gogh") 14 | URL string 15 | 16 | // CloneURL is a clone URL for the repository (e.g.: "https://github.com/kyoh86/gogh.git") 17 | CloneURL string 18 | 19 | // UpdatedAt is the last updated time of the repository 20 | UpdatedAt time.Time 21 | // Parent is the parent repository if it is a fork 22 | Parent *ParentRepository 23 | 24 | // Description is a description of the repository (e.g.: "Gogh is a collection of themes for Gnome Terminal and Pantheon Terminal") 25 | Description string 26 | // Homepage is a homepage of the repository (e.g.: "https://example.com") 27 | Homepage string 28 | // Language is a primary language of the repository (e.g.: "Go") 29 | Language string 30 | // Archived is if the repository is archived 31 | Archived bool 32 | // Private is if the repository is private 33 | Private bool 34 | // IsTemplate is if the repository is a template 35 | IsTemplate bool 36 | // Fork is if the repository is a fork 37 | Fork bool 38 | } 39 | 40 | // ParentRepository represents a parent repository of a fork 41 | type ParentRepository struct { 42 | // Ref is a reference of the parent repository 43 | Ref repository.Reference 44 | // CloneURL is a clone URL for the parent repository 45 | CloneURL string 46 | } 47 | -------------------------------------------------------------------------------- /app/service/service_set.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "github.com/kyoh86/gogh/v4/app/config" 5 | "github.com/kyoh86/gogh/v4/core/auth" 6 | "github.com/kyoh86/gogh/v4/core/extra" 7 | "github.com/kyoh86/gogh/v4/core/git" 8 | "github.com/kyoh86/gogh/v4/core/hook" 9 | "github.com/kyoh86/gogh/v4/core/hosting" 10 | "github.com/kyoh86/gogh/v4/core/overlay" 11 | "github.com/kyoh86/gogh/v4/core/repository" 12 | "github.com/kyoh86/gogh/v4/core/script" 13 | "github.com/kyoh86/gogh/v4/core/store" 14 | "github.com/kyoh86/gogh/v4/core/workspace" 15 | ) 16 | 17 | type ServiceSet struct { 18 | DefaultNameStore store.Saver[repository.DefaultNameService] 19 | DefaultNameService repository.DefaultNameService 20 | 21 | TokenStore store.Saver[auth.TokenService] 22 | TokenService auth.TokenService 23 | 24 | WorkspaceStore store.Saver[workspace.WorkspaceService] 25 | WorkspaceService workspace.WorkspaceService 26 | 27 | FlagsStore store.Saver[*config.Flags] 28 | Flags *config.Flags 29 | 30 | OverlayStore store.Saver[overlay.OverlayService] 31 | OverlayService overlay.OverlayService 32 | 33 | ScriptStore store.Saver[script.ScriptService] 34 | ScriptService script.ScriptService 35 | 36 | HookStore store.Saver[hook.HookService] 37 | HookService hook.HookService 38 | 39 | ExtraStore *config.ExtraStore 40 | ExtraService extra.ExtraService 41 | 42 | ReferenceParser repository.ReferenceParser 43 | HostingService hosting.HostingService 44 | FinderService workspace.FinderService 45 | AuthenticateService auth.AuthenticateService 46 | GitService git.GitService 47 | } 48 | -------------------------------------------------------------------------------- /core/workspace/layout_service.go: -------------------------------------------------------------------------------- 1 | package workspace 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/kyoh86/gogh/v4/core/repository" 7 | ) 8 | 9 | var ErrNotMatched = errors.New("repository not matched for a layout") 10 | 11 | // LayoutService defines the layout structure of a repository under a root 12 | type LayoutService interface { 13 | // GetRoot returns the root of the layout 14 | GetRoot() string 15 | 16 | // Match returns the repository reference corresponding the given path 17 | // If the path does not match the layout, it returns the error `repository.ErrNotMatched` 18 | // Example: 19 | // Match("github.com/owner/repo") returns "github.com/owner/repo" 20 | // Match("github.com/owner/repo/foo") returns "github.com/owner/repo" 21 | Match(path string) (*repository.Reference, error) 22 | 23 | // ExactMatch returns the repository reference corresponding exactly to the given path 24 | // If the path does not match the layout, it returns the error `repository.ErrNotMatched` 25 | // Example: 26 | // ExactMatch("github.com/owner/repo") returns "github.com/owner/repo" 27 | // ExactMatch("github.com/owner/repo/foo") returns `repository.ErrNotMatched` 28 | ExactMatch(path string) (*repository.Reference, error) 29 | 30 | // PathFor returns the path corresponding to the given reference 31 | PathFor(ref repository.Reference) string 32 | 33 | // CreateRepositoryFolder creates a new folder for the repository 34 | CreateRepositoryFolder(reference repository.Reference) (string, error) 35 | 36 | // DeleteRepository deletes the repository 37 | DeleteRepository(reference repository.Reference) error 38 | } 39 | -------------------------------------------------------------------------------- /app/config/store_helpers.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/pelletier/go-toml/v2" 9 | ) 10 | 11 | // loadTOMLFile loads TOML data from a file 12 | func loadTOMLFile[T any](source string) (*T, error) { 13 | file, err := os.Open(source) 14 | if err != nil { 15 | return nil, err 16 | } 17 | defer file.Close() 18 | 19 | var data T 20 | if err := toml.NewDecoder(file).Decode(&data); err != nil { 21 | return nil, fmt.Errorf("decode TOML: %w", err) 22 | } 23 | return &data, nil 24 | } 25 | 26 | // saveTOMLFile saves data to a TOML file, ensuring the directory exists 27 | func saveTOMLFile[T any](source string, data T) error { 28 | if err := os.MkdirAll(filepath.Dir(source), 0o755); err != nil { 29 | return fmt.Errorf("create directory: %w", err) 30 | } 31 | 32 | file, err := os.OpenFile(source, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o644) 33 | if err != nil { 34 | return fmt.Errorf("open file: %w", err) 35 | } 36 | defer file.Close() 37 | 38 | if err := toml.NewEncoder(file).Encode(data); err != nil { 39 | return fmt.Errorf("encode TOML: %w", err) 40 | } 41 | return nil 42 | } 43 | 44 | // ensureDirectoryExists creates the directory for the given file path if it doesn't exist 45 | func ensureDirectoryExists(filePath string) error { 46 | dir := filepath.Dir(filePath) 47 | if err := os.MkdirAll(dir, 0o755); err != nil { 48 | return fmt.Errorf("create directory %s: %w", dir, err) 49 | } 50 | return nil 51 | } 52 | 53 | // openFileForWrite opens a file for writing, creating it if necessary 54 | func openFileForWrite(path string) (*os.File, error) { 55 | return os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o644) 56 | } 57 | -------------------------------------------------------------------------------- /app/overlay/remove/usecase_test.go: -------------------------------------------------------------------------------- 1 | package remove_test 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "testing" 7 | 8 | testtarget "github.com/kyoh86/gogh/v4/app/overlay/remove" 9 | "github.com/kyoh86/gogh/v4/core/overlay_mock" 10 | "go.uber.org/mock/gomock" 11 | ) 12 | 13 | func TestUsecase_Execute(t *testing.T) { 14 | tests := []struct { 15 | name string 16 | id string 17 | mockSetup func(*overlay_mock.MockOverlayService) error 18 | wantErr bool 19 | }{ 20 | { 21 | name: "正常系:オーバーレイを削除", 22 | id: "overlay1", 23 | mockSetup: func(m *overlay_mock.MockOverlayService) error { 24 | m.EXPECT().Remove(gomock.Any(), "overlay1").Return(nil) 25 | return nil 26 | }, 27 | wantErr: false, 28 | }, 29 | { 30 | name: "エラー系:削除に失敗", 31 | id: "overlay2", 32 | mockSetup: func(m *overlay_mock.MockOverlayService) error { 33 | err := errors.New("overlay not found") 34 | m.EXPECT().Remove(gomock.Any(), "overlay2").Return(err) 35 | return err 36 | }, 37 | wantErr: true, 38 | }, 39 | } 40 | 41 | for _, tt := range tests { 42 | t.Run(tt.name, func(t *testing.T) { 43 | ctrl := gomock.NewController(t) 44 | defer ctrl.Finish() 45 | 46 | mockService := overlay_mock.NewMockOverlayService(ctrl) 47 | expectedErr := tt.mockSetup(mockService) 48 | 49 | uc := testtarget.NewUsecase(mockService) 50 | err := uc.Execute(context.Background(), tt.id) 51 | 52 | if (err != nil) != tt.wantErr { 53 | t.Errorf("Usecase.Execute() error = %v, wantErr %v", err, tt.wantErr) 54 | } 55 | 56 | if err != nil && expectedErr != nil && err != expectedErr { 57 | t.Errorf("Expected error doesn't match: got %v", err) 58 | } 59 | }) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /doc/usage/gogh_list.md: -------------------------------------------------------------------------------- 1 | ## gogh list 2 | 3 | List local repositories 4 | 5 | ``` 6 | gogh list [flags] 7 | ``` 8 | 9 | ### Options 10 | 11 | ``` 12 | -f, --format string 13 | Print local repository in a given format, where [format] can be one of "path", 14 | "full-path", "json", "fields" and "fields:[separator]". 15 | 16 | - path: 17 | 18 | A part of the URL to specify a repository. For example: "github.com/kyoh86/gogh" 19 | 20 | - full-path 21 | 22 | A full path of the local repository. For example: 23 | "/root/Projects/github.com/kyoh86/gogh". 24 | 25 | - fields 26 | 27 | Tab separated all formats and properties of the local repository. 28 | i.e. [full-path]\t[path]\t[host]\t[owner]\t[name] 29 | 30 | - fields:[separator] 31 | 32 | Like "fields" but with the explicit separator. 33 | 34 | -h, --help help for list 35 | --limit int Max number of repositories to list. -1 means unlimited (default 100) 36 | -p, --pattern strings Patterns for selecting repositories 37 | --primary List up repositories in just a primary root 38 | ``` 39 | 40 | ### SEE ALSO 41 | 42 | * [gogh](gogh.md) - GO GitHub local repository manager 43 | 44 | -------------------------------------------------------------------------------- /ui/cli/commands/cwd.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/apex/log" 8 | "github.com/kyoh86/gogh/v4/app/config" 9 | "github.com/kyoh86/gogh/v4/app/cwd" 10 | "github.com/kyoh86/gogh/v4/app/service" 11 | "github.com/kyoh86/gogh/v4/ui/cli/flags" 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | // NewCwdCommand creates a new command to print the local repository which the current working directory belongs to. 16 | func NewCwdCommand(ctx context.Context, svc *service.ServiceSet) (*cobra.Command, error) { 17 | var format flags.LocationFormat 18 | 19 | cmd := &cobra.Command{ 20 | Use: "cwd", 21 | Short: "Print the local repository which the current working directory belongs to", 22 | Args: cobra.NoArgs, 23 | RunE: func(cmd *cobra.Command, _ []string) error { 24 | formatter, err := config.LocationFormatter(format.String()) 25 | if err != nil { 26 | return fmt.Errorf("invalid format: %w", err) 27 | } 28 | 29 | ctx := cmd.Context() 30 | repo, err := cwd.NewUsecase(svc.WorkspaceService, svc.FinderService).Execute(ctx) 31 | if err != nil { 32 | return fmt.Errorf("finding repository in current directory: %w", err) 33 | } 34 | str, err := formatter.Format(*repo) 35 | if err != nil { 36 | log.FromContext(ctx).WithFields(log.Fields{ 37 | "error": err, 38 | "format": format.String(), 39 | "path": repo.FullPath(), 40 | }).Info("Failed to format") 41 | return nil 42 | } 43 | fmt.Println(str) 44 | return nil 45 | }, 46 | } 47 | 48 | if err := flags.LocationFormatFlag(cmd, &format, svc.Flags.Cwd.Format); err != nil { 49 | return nil, fmt.Errorf("adding location format flag: %w", err) 50 | } 51 | return cmd, nil 52 | } 53 | -------------------------------------------------------------------------------- /ui/cli/commands/overlay.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/kyoh86/gogh/v4/app/service" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func NewOverlayCommand(_ context.Context, svc *service.ServiceSet) (*cobra.Command, error) { 11 | cmd := &cobra.Command{ 12 | Use: "overlay", 13 | Short: "Manage repository overlay files", 14 | Example: ` Overlay files are used to put custom files into repositories. 15 | They are useful to add files that are not tracked by the repository, such as editor configurations or scripts. 16 | 17 | For example, to add a custom VSCode settings file to a repository, you can run: 18 | 19 | gogh overlay add /path/to/source/vscode/settings.json "github.com/owner/repo" .vscode/settings.json 20 | 21 | Then when you run ` + "`gogh create`, `gogh clone` or `gogh fork`" + `, the files will be copied to the repository. 22 | 23 | You can also apply template files only for the ` + "`gogh create`" + ` command by using the ` + "`--for-init`" + ` flag: 24 | 25 | gogh overlay add --for-init /path/to/source/deno.jsonc "github.com/owner/deno-*" deno.jsonc 26 | 27 | This will copy the ` + "`deno.jsonc`" + ` file to the root of the repository only when you run ` + "`gogh create`" + ` 28 | if the repository matches the pattern ` + "`github.com/owner/deno-*`" + `. 29 | 30 | And then you can use the ` + "`gogh overlay apply`" + ` command to apply the overlay files manually. 31 | 32 | You can create overlay files that never be applied to the repository automatically, 33 | (and only be applied manually by ` + "`gogh overlay apply`" + ` command), 34 | you can set the ` + "`--repo-pattern`" + ` flag to never match any repository.`, 35 | } 36 | return cmd, nil 37 | } 38 | -------------------------------------------------------------------------------- /app/script/run/usecase.go: -------------------------------------------------------------------------------- 1 | package run 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | libs "github.com/vadv/gopher-lua-libs" 8 | lua "github.com/yuin/gopher-lua" 9 | ) 10 | 11 | // Usecase for running script scripts 12 | type Usecase struct{} 13 | 14 | func NewUsecase() *Usecase { 15 | return &Usecase{} 16 | } 17 | 18 | // Globals represents a map of global variables to be passed to Lua 19 | type Globals map[string]any 20 | 21 | // ToLuaTable converts the Globals map to a Lua table on the given state 22 | func (g Globals) ToLuaTable(l *lua.LState) *lua.LTable { 23 | table := l.NewTable() 24 | for key, value := range g { 25 | switch v := value.(type) { 26 | case string: 27 | table.RawSetString(key, lua.LString(v)) 28 | case int: 29 | table.RawSetString(key, lua.LNumber(v)) 30 | case float64: 31 | table.RawSetString(key, lua.LNumber(v)) 32 | case bool: 33 | table.RawSetString(key, lua.LBool(v)) 34 | case map[string]any: 35 | // Recursively convert nested maps 36 | table.RawSetString(key, Globals(v).ToLuaTable(l)) 37 | default: 38 | // For complex types, convert to a simple representation 39 | table.RawSetString(key, lua.LString(fmt.Sprintf("%v", v))) 40 | } 41 | } 42 | return table 43 | } 44 | 45 | type Script struct { 46 | Code string 47 | Globals Globals 48 | } 49 | 50 | func (uc *Usecase) Execute(ctx context.Context, script Script) error { 51 | l := lua.NewState() 52 | defer l.Close() 53 | 54 | // Load standard libraries 55 | libs.Preload(l) 56 | 57 | // Set up the global 'gogh' table 58 | goghTable := script.Globals.ToLuaTable(l) 59 | l.SetGlobal("gogh", goghTable) 60 | 61 | if err := l.DoString(script.Code); err != nil { 62 | return fmt.Errorf("run Lua: %w", err) 63 | } 64 | return nil 65 | } 66 | -------------------------------------------------------------------------------- /core/fs/tilde_test.go: -------------------------------------------------------------------------------- 1 | package fs_test 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "testing" 7 | 8 | testtarget "github.com/kyoh86/gogh/v4/core/fs" 9 | ) 10 | 11 | func TestReplaceTildeWithHome(t *testing.T) { 12 | t.Run("empty", func(t *testing.T) { 13 | path, err := testtarget.ReplaceTildeWithHome("") 14 | if err != nil { 15 | t.Fatalf("ReplaceTildeWithHome failed with error: %v", err) 16 | } 17 | if path != "" { 18 | t.Errorf("expected empty path, got %q", path) 19 | } 20 | }) 21 | 22 | t.Run("tilde only", func(t *testing.T) { 23 | homeDir, err := os.UserHomeDir() 24 | if err != nil { 25 | t.Fatalf("failed to get user home dir: %v", err) 26 | } 27 | 28 | path, err := testtarget.ReplaceTildeWithHome("~") 29 | if err != nil { 30 | t.Fatalf("ReplaceTildeWithHome failed with error: %v", err) 31 | } 32 | if path != homeDir { 33 | t.Errorf("expected %q, got %q", homeDir, path) 34 | } 35 | }) 36 | 37 | t.Run("tilde with path", func(t *testing.T) { 38 | homeDir, err := os.UserHomeDir() 39 | if err != nil { 40 | t.Fatalf("failed to get user home dir: %v", err) 41 | } 42 | expected := filepath.Join(homeDir, "test") 43 | 44 | path, err := testtarget.ReplaceTildeWithHome("~/test") 45 | if err != nil { 46 | t.Fatalf("ReplaceTildeWithHome failed with error: %v", err) 47 | } 48 | if path != expected { 49 | t.Errorf("expected %q, got %q", expected, path) 50 | } 51 | }) 52 | 53 | t.Run("normal path", func(t *testing.T) { 54 | expected := "/normal/path" 55 | path, err := testtarget.ReplaceTildeWithHome("/normal/path") 56 | if err != nil { 57 | t.Fatalf("ReplaceTildeWithHome failed with error: %v", err) 58 | } 59 | if path != expected { 60 | t.Errorf("expected %q, got %q", expected, path) 61 | } 62 | }) 63 | } 64 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json 2 | 3 | name: Release binary to the GitHub Release 4 | on: 5 | workflow_dispatch: 6 | inputs: 7 | method: 8 | description: | 9 | Which number to increment in the semantic versioning. 10 | required: true 11 | type: choice 12 | options: 13 | - major 14 | - minor 15 | - patch 16 | jobs: 17 | release-binary: 18 | name: Release Binary 19 | runs-on: ubuntu-latest 20 | steps: 21 | - name: Check Actor 22 | if: github.actor != 'kyoh86' 23 | run: exit 1 24 | - name: Check Branch 25 | if: github.ref != 'refs/heads/main' 26 | run: exit 1 27 | - name: Wait commit status 28 | uses: cloudposse/github-action-wait-commit-status@main 29 | with: 30 | repository: ${{ github.repository }} 31 | sha: ${{ github.sha }} 32 | status: releasable 33 | token: ${{ github.token }} 34 | check-retry-count: 20 35 | check-retry-interval: 20 36 | - name: Checkout Sources 37 | uses: actions/checkout@v4 38 | - name: Bump-up Semantic Version 39 | uses: kyoh86/git-vertag-action@v1 40 | with: 41 | # method: "major", "minor" or "patch" to update tag with semver 42 | method: "${{ github.event.inputs.method }}" 43 | - name: Setup Go 44 | uses: actions/setup-go@v5 45 | with: 46 | go-version: 1.24 47 | - name: Run GoReleaser 48 | uses: goreleaser/goreleaser-action@v6 49 | env: 50 | GITHUB_TOKEN: ${{ secrets.GORELEASER_GITHUB_TOKEN }} 51 | with: 52 | version: ~> v2 53 | args: release --clean 54 | -------------------------------------------------------------------------------- /core/repository/reference.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "path" 5 | ) 6 | 7 | // Reference is a struct that contains a host, owner and name of a repository. 8 | // It is used to identify a repository in a hosting service or a location in a root. 9 | type Reference struct { 10 | host string 11 | owner string 12 | name string 13 | } 14 | 15 | // Host is a hostname (e.g.: "github.com") 16 | func (r Reference) Host() string { return r.host } 17 | 18 | // Owner is a owner name (e.g.: "kyoh86") 19 | func (r Reference) Owner() string { return r.owner } 20 | 21 | // Name of the repository (e.g.: "gogh") 22 | func (r Reference) Name() string { return r.name } 23 | 24 | func (r Reference) String() string { 25 | return path.Join(r.host, r.owner, r.name) 26 | } 27 | 28 | // NewReference will build a Reference with a host, owner and name. 29 | func NewReference(host, owner, name string) Reference { 30 | return Reference{ 31 | host: host, 32 | owner: owner, 33 | name: name, 34 | } 35 | } 36 | 37 | // ReferenceWithAlias is a struct that contains a Reference and an optional alias. 38 | type ReferenceWithAlias struct { 39 | // Reference is the main reference. 40 | Reference Reference 41 | // Alias is an optional alias for the reference if needed. 42 | Alias *Reference 43 | } 44 | 45 | // Local returns the local reference. If an alias is set, it returns the alias; otherwise, it returns the main reference. 46 | func (r ReferenceWithAlias) Local() Reference { 47 | if r.Alias != nil { 48 | return *r.Alias 49 | } 50 | return r.Reference 51 | } 52 | 53 | // String returns a string representation of the reference with alias. 54 | func (r ReferenceWithAlias) String() string { 55 | if r.Alias != nil { 56 | return r.Reference.String() + "=" + r.Alias.String() 57 | } 58 | return r.Reference.String() 59 | } 60 | -------------------------------------------------------------------------------- /.github/workflows/release-on-tag.yml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json 2 | 3 | name: Release binary to the GitHub Release on specific tag 4 | on: 5 | workflow_dispatch: 6 | inputs: 7 | next-version: 8 | description: | 9 | Which number to increment in the semantic versioning. 10 | required: true 11 | type: string 12 | jobs: 13 | release-binary: 14 | name: Release Binary 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Check Actor 18 | if: github.actor != 'kyoh86' 19 | run: exit 1 20 | - name: Check Branch 21 | if: github.ref != 'refs/heads/main' 22 | run: exit 1 23 | - name: Wait commit status 24 | uses: cloudposse/github-action-wait-commit-status@main 25 | with: 26 | repository: ${{ github.repository }} 27 | sha: ${{ github.sha }} 28 | status: releasable 29 | token: ${{ github.token }} 30 | check-retry-count: 20 31 | check-retry-interval: 20 32 | - name: Checkout Sources 33 | uses: actions/checkout@v4 34 | - name: Set git tag 35 | run: | 36 | git config user.name "github-actions[bot]" 37 | git config user.email "41898282+github-actions[bot]@users.noreply.github.com" 38 | git tag -a "${{ github.event.inputs.next-version }}" -m "Release version ${{ github.event.inputs.next-version }}" 39 | - name: Setup Go 40 | uses: actions/setup-go@v5 41 | with: 42 | go-version: 1.24 43 | - name: Run GoReleaser 44 | uses: goreleaser/goreleaser-action@v6 45 | env: 46 | GITHUB_TOKEN: ${{ secrets.GORELEASER_GITHUB_TOKEN }} 47 | with: 48 | version: ~> v2 49 | args: release --clean 50 | 51 | -------------------------------------------------------------------------------- /app/config/materialize.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "io/fs" 8 | "os" 9 | "path/filepath" 10 | 11 | "github.com/kyoh86/gogh/v4/core/gogh" 12 | "github.com/kyoh86/gogh/v4/core/store" 13 | ) 14 | 15 | // AppContextPath returns the path to the app's configuration file. 16 | // 17 | // If the environment variable `envar` is set, it returns that. 18 | // Specify a function to get the parent directory where the file will be placed, such as os.UserConfigDir. 19 | // The `rel` is the relative path to the file from the dir. 20 | // 21 | // It will make the path that is formed as {getDir()}/{AppName=gogh}/{rel...}` 22 | func AppContextPath(envar string, getDir func() (string, error), rel ...string) (string, error) { 23 | if env := os.Getenv(envar); env != "" { 24 | return env, nil 25 | } 26 | dir, err := getDir() 27 | if err != nil { 28 | return "", fmt.Errorf("search app file dir for %s: %w", rel, err) 29 | } 30 | return filepath.Join(append([]string{dir, gogh.AppName}, rel...)...), nil 31 | } 32 | 33 | var AppContextPathFunc = AppContextPath 34 | 35 | // LoadAlternative loads a value of type T using the provided loaders. 36 | // It tries each loader in order until one succeeds or all fail. 37 | // If all loaders fail, it returns the initial value provided by the initial function. 38 | func LoadAlternative[T store.Content](ctx context.Context, initial func() T, loaders ...store.Loader[T]) (T, error) { 39 | for i, loader := range loaders { 40 | svc, err := loader.Load(ctx, initial) 41 | if os.IsNotExist(err) { 42 | continue 43 | } 44 | if errors.Is(err, fs.ErrNotExist) { 45 | continue 46 | } 47 | if err != nil { 48 | var empty T 49 | return empty, fmt.Errorf("loading at %dth loader: %w", i+1, err) 50 | } 51 | return svc, nil 52 | } 53 | return initial(), nil 54 | } 55 | -------------------------------------------------------------------------------- /infra/githubv4/genqlient.graphql: -------------------------------------------------------------------------------- 1 | fragment PageInfoFragment on PageInfo { 2 | endCursor 3 | hasNextPage 4 | } 5 | 6 | fragment LanguageFragment on Language { 7 | name 8 | } 9 | 10 | fragment OwnerFragment on RepositoryOwner { 11 | login 12 | } 13 | 14 | fragment ParentRepositoryFragment on Repository { 15 | name 16 | owner { 17 | ...OwnerFragment 18 | } 19 | sshUrl 20 | } 21 | 22 | fragment RepositoryFragment on Repository { 23 | url 24 | homepageUrl 25 | sshUrl 26 | primaryLanguage { 27 | ...LanguageFragment 28 | } 29 | name 30 | owner { 31 | ...OwnerFragment 32 | } 33 | description 34 | createdAt 35 | isArchived 36 | isFork 37 | isPrivate 38 | isTemplate 39 | updatedAt 40 | parent { 41 | ...ParentRepositoryFragment 42 | } 43 | } 44 | 45 | query ListRepos( 46 | $first: Int = 30, 47 | # @genqlient(omitempty: true) 48 | $after: String, 49 | # @genqlient(omitempty: true, pointer: true) 50 | $isFork: Boolean, 51 | # @genqlient(omitempty: true) 52 | $privacy: RepositoryPrivacy, 53 | # @genqlient(omitempty: true) 54 | $affiliations: [RepositoryAffiliation], 55 | # @genqlient(omitempty: true) 56 | $orderBy: RepositoryOrder = {field: PUSHED_AT, direction: DESC}, 57 | # @genqlient(omitempty: true, pointer: true) 58 | $isArchived: Boolean, 59 | ) { 60 | viewer { 61 | repositories( 62 | first: $first, 63 | after: $after, 64 | isArchived: $isArchived, 65 | isFork: $isFork, 66 | privacy: $privacy, 67 | ownerAffiliations: $affiliations, 68 | affiliations: $affiliations, 69 | orderBy: $orderBy 70 | ) { 71 | edges { 72 | node { 73 | ...RepositoryFragment 74 | } 75 | } 76 | totalCount 77 | pageInfo { 78 | ...PageInfoFragment 79 | } 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /ui/cli/commands/hook_invoke.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/kyoh86/gogh/v4/app/cwd" 8 | "github.com/kyoh86/gogh/v4/app/hook/invoke" 9 | "github.com/kyoh86/gogh/v4/app/service" 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | func NewHookInvokeCommand(_ context.Context, svc *service.ServiceSet) (*cobra.Command, error) { 14 | cmd := &cobra.Command{ 15 | Use: "invoke [flags] [[/]/]", 16 | Short: "Manually invoke a hook for a repository", 17 | Args: cobra.ExactArgs(2), 18 | Example: ` invoke github.com/owner/repo 19 | invoke owner/repo 20 | invoke repo 21 | invoke . # Use current directory repository 22 | 23 | It accepts a short notation for each repository 24 | (for example, "github.com/kyoh86/example") like below. 25 | - "": e.g. "example" 26 | - "/": e.g. "kyoh86/example" 27 | - "." for the current directory repository 28 | They'll be completed with the default host and owner set by "config set-default{-host|-owner}".`, 29 | RunE: func(cmd *cobra.Command, args []string) error { 30 | ctx := cmd.Context() 31 | hookID := args[0] 32 | repoRef := args[1] 33 | 34 | // Use current directory if reference is "." 35 | if repoRef == "." { 36 | repo, err := cwd.NewUsecase(svc.WorkspaceService, svc.FinderService).Execute(ctx) 37 | if err != nil { 38 | return fmt.Errorf("finding repository from current directory: %w", err) 39 | } 40 | repoRef = repo.Ref().String() 41 | } 42 | 43 | return invoke.NewUsecase( 44 | svc.WorkspaceService, 45 | svc.FinderService, 46 | svc.HookService, 47 | svc.OverlayService, 48 | svc.ScriptService, 49 | svc.ReferenceParser, 50 | ).Invoke(ctx, hookID, repoRef) 51 | }, 52 | } 53 | return cmd, nil 54 | } 55 | -------------------------------------------------------------------------------- /app/hook/describe/usecase.go: -------------------------------------------------------------------------------- 1 | package describe 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | 9 | "github.com/kyoh86/gogh/v4/core/hook" 10 | ) 11 | 12 | // Hook represents the hook type 13 | type Hook = hook.Hook 14 | 15 | // JSONUsecase represents the use case for showing hooks in JSON format 16 | type JSONUsecase struct { 17 | enc *json.Encoder 18 | } 19 | 20 | // NewJSONUsecase creates a new use case for showing hooks in JSON format 21 | func NewJSONUsecase(writer io.Writer) *JSONUsecase { 22 | return &JSONUsecase{enc: json.NewEncoder(writer)} 23 | } 24 | 25 | // Execute executes the use case to show a hook in JSON format 26 | func (uc *JSONUsecase) Execute(ctx context.Context, s Hook) error { 27 | return uc.enc.Encode(map[string]any{ 28 | "id": s.ID(), 29 | "name": s.Name(), 30 | "repo_pattern": s.RepoPattern(), 31 | "trigger_event": s.TriggerEvent(), 32 | "operation_type": s.OperationType(), 33 | "operation_id": s.OperationID(), 34 | }) 35 | } 36 | 37 | // OnelineUsecase represents the use case for showing hooks in a single line format 38 | type OnelineUsecase struct { 39 | writer io.Writer 40 | } 41 | 42 | // NewOnelineUsecase creates a new use case for showing overlays in text format 43 | func NewOnelineUsecase(writer io.Writer) *OnelineUsecase { 44 | return &OnelineUsecase{writer: writer} 45 | } 46 | 47 | // Execute executes the use case to show a hook in a single line format 48 | func (uc *OnelineUsecase) Execute(ctx context.Context, s hook.Hook) error { 49 | pattern := s.RepoPattern() 50 | if pattern == "" { 51 | pattern = "*" 52 | } 53 | _, err := fmt.Fprintf( 54 | uc.writer, 55 | "[%s] %s for repos(%s) @%s: %s(%s)\n", 56 | s.ID()[:8], 57 | s.Name(), 58 | pattern, 59 | s.TriggerEvent(), 60 | s.OperationType(), 61 | s.OperationID()[:8], 62 | ) 63 | return err 64 | } 65 | -------------------------------------------------------------------------------- /app/repoprint/usecase_test.go: -------------------------------------------------------------------------------- 1 | package repoprint_test 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "iter" 7 | "testing" 8 | "time" 9 | 10 | testtarget "github.com/kyoh86/gogh/v4/app/repoprint" 11 | "github.com/kyoh86/gogh/v4/core/hosting" 12 | "github.com/kyoh86/gogh/v4/core/repository" 13 | ) 14 | 15 | func sliceToIter2[T any](slices []T) iter.Seq2[T, error] { 16 | return func(yield func(T, error) bool) { 17 | for _, v := range slices { 18 | if !yield(v, nil) { 19 | return 20 | } 21 | } 22 | } 23 | } 24 | 25 | func TestRepositoryPrinter(t *testing.T) { 26 | uat, err := time.Parse(time.RFC3339, "2021-05-01T01:00:00Z") 27 | if err != nil { 28 | t.Fatal(err) 29 | } 30 | ref := repository.NewReference("github.com", "kyoh86", "gogh") 31 | repo := hosting.Repository{ 32 | UpdatedAt: uat, 33 | Ref: ref, 34 | URL: "https://github.com/kyoh86/gogh", 35 | } 36 | for _, testcase := range []struct { 37 | title string 38 | format string 39 | want string 40 | }{ 41 | { 42 | title: "ref", 43 | format: "ref", 44 | want: "github.com/kyoh86/gogh\n", 45 | }, 46 | { 47 | title: "url", 48 | format: "url", 49 | want: "https://github.com/kyoh86/gogh\n", 50 | }, 51 | { 52 | title: "json", 53 | format: "json", 54 | want: `{"ref":{"host":"github.com","owner":"kyoh86","name":"gogh"},"url":"https://github.com/kyoh86/gogh","updatedAt":"2021-05-01T01:00:00Z"}` + "\n", 55 | }, 56 | } { 57 | t.Run(testcase.title, func(t *testing.T) { 58 | var buf bytes.Buffer 59 | printer := testtarget.NewUsecase(&buf, testcase.format) 60 | if err := printer.Execute(context.Background(), sliceToIter2([]*hosting.Repository{&repo})); err != nil { 61 | t.Fatal(err) 62 | } 63 | got := buf.String() 64 | if testcase.want != got { 65 | t.Errorf("result mismatched; want: %s; got: %s", testcase.want, got) 66 | } 67 | }) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /app/config/script_source_store.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "os" 8 | "path/filepath" 9 | 10 | "github.com/kyoh86/gogh/v4/core/script" 11 | ) 12 | 13 | type ScriptSourceStore struct{} 14 | 15 | func NewScriptSourceStore() *ScriptSourceStore { return &ScriptSourceStore{} } 16 | 17 | func (cs *ScriptSourceStore) Save(ctx context.Context, scriptID string, content io.Reader) error { 18 | source, err := cs.Source() 19 | if err != nil { 20 | return fmt.Errorf("get content source: %w", err) 21 | } 22 | if err := os.MkdirAll(source, 0o755); err != nil { 23 | return fmt.Errorf("failed to create content directory: %w", err) 24 | } 25 | filePath := filepath.Join(source, scriptID) 26 | f, err := os.Create(filePath) 27 | if err != nil { 28 | return fmt.Errorf("failed to create content file: %w", err) 29 | } 30 | defer f.Close() 31 | if _, err := io.Copy(f, content); err != nil { 32 | return fmt.Errorf("failed to write script: %w", err) 33 | } 34 | return nil 35 | } 36 | 37 | func (cs *ScriptSourceStore) Open(ctx context.Context, scriptID string) (io.ReadCloser, error) { 38 | source, err := cs.Source() 39 | if err != nil { 40 | return nil, fmt.Errorf("get content source: %w", err) 41 | } 42 | return os.Open(filepath.Join(source, scriptID)) 43 | } 44 | 45 | func (cs *ScriptSourceStore) Remove(ctx context.Context, scriptID string) error { 46 | source, err := cs.Source() 47 | if err != nil { 48 | return fmt.Errorf("get content source: %w", err) 49 | } 50 | return os.Remove(filepath.Join(source, scriptID)) 51 | } 52 | 53 | func (*ScriptSourceStore) Source() (string, error) { 54 | path, err := AppContextPathFunc("GOGH_HOOK_CONTENT_PATH", os.UserConfigDir, "script.v4") 55 | if err != nil { 56 | return "", fmt.Errorf("search script content path: %w", err) 57 | } 58 | return path, nil 59 | } 60 | 61 | var _ script.ScriptSourceStore = (*ScriptSourceStore)(nil) 62 | -------------------------------------------------------------------------------- /app/repoprint/repotab/cell.go: -------------------------------------------------------------------------------- 1 | package repotab 2 | 3 | import ( 4 | "strings" 5 | "time" 6 | 7 | "github.com/kyoh86/gogh/v4/core/hosting" 8 | "github.com/morikuni/aec" 9 | ) 10 | 11 | type CellBuilder interface { 12 | Build(hosting.Repository) (content string, style aec.ANSI) 13 | } 14 | 15 | type CellBuildFunc func(r hosting.Repository) (content string, style aec.ANSI) 16 | 17 | func (f CellBuildFunc) Build(r hosting.Repository) (content string, style aec.ANSI) { 18 | return f(r) 19 | } 20 | 21 | var RepoRefCell = CellBuildFunc(func(r hosting.Repository) (content string, style aec.ANSI) { 22 | content = r.Ref.String() 23 | return content, aec.Bold 24 | }) 25 | 26 | var DescriptionCell = CellBuildFunc(func(r hosting.Repository) (content string, style aec.ANSI) { 27 | content = r.Description 28 | return content, aec.DefaultF.With(aec.DefaultB) 29 | }) 30 | 31 | var EmojiAttributesCell = CellBuildFunc(func(r hosting.Repository) (content string, style aec.ANSI) { 32 | var parts []string 33 | 34 | if r.Private { 35 | parts = append(parts, "🔒") 36 | } 37 | if r.Fork { 38 | parts = append(parts, "🔀") 39 | } 40 | if r.Archived { 41 | parts = append(parts, "🗃️") 42 | } 43 | 44 | return strings.Join(parts, " "), aec.EmptyBuilder.ANSI 45 | }) 46 | 47 | var AttributesCell = CellBuildFunc(func(r hosting.Repository) (content string, style aec.ANSI) { 48 | contents := []string{""} 49 | if r.Private { 50 | style = aec.YellowF 51 | contents[0] = "private" 52 | } else { 53 | style = aec.LightBlackF 54 | contents[0] = "public" 55 | } 56 | if r.Fork { 57 | contents = append(contents, "fork") 58 | } 59 | if r.Archived { 60 | contents = append(contents, "archived") 61 | } 62 | return strings.Join(contents, ","), style 63 | }) 64 | 65 | var UpdatedAtCell = CellBuildFunc(func(r hosting.Repository) (content string, style aec.ANSI) { 66 | return FuzzyAgoAbbr(time.Now(), r.UpdatedAt), aec.LightBlackF 67 | }) 68 | -------------------------------------------------------------------------------- /ui/cli/commands/script_create.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/kyoh86/gogh/v4/app/script/add" 9 | "github.com/kyoh86/gogh/v4/app/service" 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | func NewScriptCreateCommand(_ context.Context, svc *service.ServiceSet) (*cobra.Command, error) { 14 | var f struct { 15 | name string 16 | } 17 | cmd := &cobra.Command{ 18 | Use: "create [flags]", 19 | Short: "Create a new script (with $EDITOR)", 20 | Args: cobra.NoArgs, 21 | RunE: func(cmd *cobra.Command, args []string) error { 22 | ctx := cmd.Context() 23 | 24 | // Create a temporary file for editing 25 | tmpFile, err := os.CreateTemp("", "gogh_script_create_*.lua") 26 | if err != nil { 27 | return err 28 | } 29 | defer os.Remove(tmpFile.Name()) 30 | 31 | // Write initial template 32 | initialContent := `-- Gogh script 33 | -- Available globals: 34 | -- gogh.repository.owner: Repository owner 35 | -- gogh.repository.name: Repository name 36 | -- gogh.repository.url: Repository URL 37 | -- gogh.repository.path: Local repository path 38 | 39 | ` 40 | if _, err := tmpFile.WriteString(initialContent); err != nil { 41 | return err 42 | } 43 | tmpFile.Close() 44 | 45 | // Open editor 46 | if err := edit(os.Getenv("EDITOR"), tmpFile.Name()); err != nil { 47 | return err 48 | } 49 | 50 | // Read the edited content 51 | content, err := os.Open(tmpFile.Name()) 52 | if err != nil { 53 | return err 54 | } 55 | defer content.Close() 56 | 57 | // Add the script 58 | h, err := add.NewUsecase(svc.ScriptService).Execute(ctx, f.name, content) 59 | if err != nil { 60 | return fmt.Errorf("adding script: %w", err) 61 | } 62 | fmt.Printf("Script created %s\n", h.ID()) 63 | return nil 64 | }, 65 | } 66 | cmd.Flags().StringVar(&f.name, "name", "", "Name of the script") 67 | return cmd, nil 68 | } 69 | -------------------------------------------------------------------------------- /core/extra/extra_service.go: -------------------------------------------------------------------------------- 1 | package extra 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "iter" 7 | 8 | "github.com/kyoh86/gogh/v4/core/repository" 9 | "github.com/kyoh86/gogh/v4/core/store" 10 | ) 11 | 12 | var ( 13 | // ErrExtraNotFound is returned when extra is not found 14 | ErrExtraNotFound = errors.New("extra not found") 15 | // ErrExtraAlreadyExists is returned when extra already exists 16 | ErrExtraAlreadyExists = errors.New("extra already exists") 17 | ) 18 | 19 | // ExtraService manages extra 20 | type ExtraService interface { 21 | store.Content 22 | 23 | // AddAutoExtra adds auto-apply extra for a repository 24 | AddAutoExtra(ctx context.Context, repo repository.Reference, source repository.Reference, items []Item) (string, error) 25 | 26 | // AddNamedExtra adds named extra 27 | AddNamedExtra(ctx context.Context, name string, source repository.Reference, items []Item) (string, error) 28 | 29 | // GetAutoExtra retrieves auto-apply extra for a repository 30 | GetAutoExtra(ctx context.Context, repo repository.Reference) (*Extra, error) 31 | 32 | // GetNamedExtra retrieves named extra by name 33 | GetNamedExtra(ctx context.Context, name string) (*Extra, error) 34 | 35 | // Get retrieves extra by ID 36 | Get(ctx context.Context, idlike string) (*Extra, error) 37 | 38 | // RemoveAutoExtra removes auto-apply extra for a repository 39 | RemoveAutoExtra(ctx context.Context, repo repository.Reference) error 40 | 41 | // RemoveNamedExtra removes named extra by name 42 | RemoveNamedExtra(ctx context.Context, name string) error 43 | 44 | // Remove removes extra by ID 45 | Remove(ctx context.Context, idlike string) error 46 | 47 | // List lists all extra 48 | List(ctx context.Context) iter.Seq2[*Extra, error] 49 | 50 | // ListByType lists extra by type 51 | ListByType(ctx context.Context, extraType Type) iter.Seq2[*Extra, error] 52 | 53 | // Load loads extras from an iterator 54 | Load(iter.Seq2[*Extra, error]) error 55 | } 56 | -------------------------------------------------------------------------------- /app/config/overlay_content_store.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "os" 8 | "path/filepath" 9 | 10 | "github.com/kyoh86/gogh/v4/core/overlay" 11 | ) 12 | 13 | type OverlayContentStore struct{} 14 | 15 | func NewOverlayContentStore() *OverlayContentStore { 16 | return &OverlayContentStore{} 17 | } 18 | 19 | func (cs *OverlayContentStore) Save(ctx context.Context, overlayID string, content io.Reader) error { 20 | source, err := cs.Source() 21 | if err != nil { 22 | return fmt.Errorf("get content source: %w", err) 23 | } 24 | if err := os.MkdirAll(source, 0o755); err != nil { 25 | return fmt.Errorf("failed to create content directory: %w", err) 26 | } 27 | filePath := filepath.Join(source, overlayID) 28 | f, err := os.Create(filePath) 29 | if err != nil { 30 | return fmt.Errorf("failed to create content file: %w", err) 31 | } 32 | defer f.Close() 33 | if _, err := io.Copy(f, content); err != nil { 34 | return fmt.Errorf("failed to write content: %w", err) 35 | } 36 | return nil 37 | } 38 | 39 | func (cs *OverlayContentStore) Open(ctx context.Context, overlayID string) (io.ReadCloser, error) { 40 | source, err := cs.Source() 41 | if err != nil { 42 | return nil, fmt.Errorf("get content source: %w", err) 43 | } 44 | return os.Open(filepath.Join(source, overlayID)) 45 | } 46 | 47 | func (cs *OverlayContentStore) Remove(ctx context.Context, overlayID string) error { 48 | source, err := cs.Source() 49 | if err != nil { 50 | return fmt.Errorf("get content source: %w", err) 51 | } 52 | return os.Remove(filepath.Join(source, overlayID)) 53 | } 54 | 55 | func (*OverlayContentStore) Source() (string, error) { 56 | path, err := AppContextPathFunc("GOGH_OVERLAY_CONTENT_PATH", os.UserConfigDir, "overlay.v4") 57 | if err != nil { 58 | return "", fmt.Errorf("search overlay content path: %w", err) 59 | } 60 | return path, nil 61 | } 62 | 63 | var _ overlay.ContentStore = (*OverlayContentStore)(nil) 64 | -------------------------------------------------------------------------------- /app/auth/logout/usecase_test.go: -------------------------------------------------------------------------------- 1 | package logout_test 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "testing" 7 | 8 | testtarget "github.com/kyoh86/gogh/v4/app/auth/logout" 9 | "github.com/kyoh86/gogh/v4/core/auth_mock" 10 | "go.uber.org/mock/gomock" 11 | ) 12 | 13 | func TestUsecase_Execute(t *testing.T) { 14 | // Define test cases 15 | testCases := []struct { 16 | name string 17 | host string 18 | owner string 19 | setupMock func(*auth_mock.MockTokenService) 20 | expectError bool 21 | }{ 22 | { 23 | name: "Normal case: Token is deleted correctly", 24 | host: "github.com", 25 | owner: "kyoh86", 26 | setupMock: func(mockService *auth_mock.MockTokenService) { 27 | mockService.EXPECT().Delete("github.com", "kyoh86").Return(nil) 28 | }, 29 | expectError: false, 30 | }, 31 | { 32 | name: "Error case: Error occurs during token deletion", 33 | host: "github.com", 34 | owner: "kyoh86", 35 | setupMock: func(mockService *auth_mock.MockTokenService) { 36 | mockService.EXPECT().Delete("github.com", "kyoh86").Return(errors.New("delete token error")) 37 | }, 38 | expectError: true, 39 | }, 40 | } 41 | 42 | // Execute each test case 43 | for _, tc := range testCases { 44 | t.Run(tc.name, func(t *testing.T) { 45 | // Set up mock controller 46 | ctrl := gomock.NewController(t) 47 | defer ctrl.Finish() 48 | 49 | // Create TokenService mock 50 | mockTokenService := auth_mock.NewMockTokenService(ctrl) 51 | tc.setupMock(mockTokenService) 52 | 53 | // Create Usecase under test 54 | usecase := testtarget.NewUsecase(mockTokenService) 55 | 56 | // Execute 57 | err := usecase.Execute(context.Background(), tc.host, tc.owner) 58 | 59 | // Verify result 60 | if tc.expectError && err == nil { 61 | t.Error("Expected error but got none") 62 | } 63 | if !tc.expectError && err != nil { 64 | t.Errorf("Expected no error but got: %v", err) 65 | } 66 | }) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /core/repository_mock/gen_location_format_mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: repository/location_format.go 3 | // 4 | // Generated by this command: 5 | // 6 | // mockgen -source repository/location_format.go -destination repository_mock/gen_location_format_mock.go -package repository_mock 7 | // 8 | 9 | // Package repository_mock is a generated GoMock package. 10 | package repository_mock 11 | 12 | import ( 13 | reflect "reflect" 14 | 15 | repository "github.com/kyoh86/gogh/v4/core/repository" 16 | gomock "go.uber.org/mock/gomock" 17 | ) 18 | 19 | // MockLocationFormat is a mock of LocationFormat interface. 20 | type MockLocationFormat struct { 21 | ctrl *gomock.Controller 22 | recorder *MockLocationFormatMockRecorder 23 | isgomock struct{} 24 | } 25 | 26 | // MockLocationFormatMockRecorder is the mock recorder for MockLocationFormat. 27 | type MockLocationFormatMockRecorder struct { 28 | mock *MockLocationFormat 29 | } 30 | 31 | // NewMockLocationFormat creates a new mock instance. 32 | func NewMockLocationFormat(ctrl *gomock.Controller) *MockLocationFormat { 33 | mock := &MockLocationFormat{ctrl: ctrl} 34 | mock.recorder = &MockLocationFormatMockRecorder{mock} 35 | return mock 36 | } 37 | 38 | // EXPECT returns an object that allows the caller to indicate expected use. 39 | func (m *MockLocationFormat) EXPECT() *MockLocationFormatMockRecorder { 40 | return m.recorder 41 | } 42 | 43 | // Format mocks base method. 44 | func (m *MockLocationFormat) Format(ref repository.Location) (string, error) { 45 | m.ctrl.T.Helper() 46 | ret := m.ctrl.Call(m, "Format", ref) 47 | ret0, _ := ret[0].(string) 48 | ret1, _ := ret[1].(error) 49 | return ret0, ret1 50 | } 51 | 52 | // Format indicates an expected call of Format. 53 | func (mr *MockLocationFormatMockRecorder) Format(ref any) *gomock.Call { 54 | mr.mock.ctrl.T.Helper() 55 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Format", reflect.TypeOf((*MockLocationFormat)(nil).Format), ref) 56 | } 57 | --------------------------------------------------------------------------------