Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Init command works #37

Merged
merged 43 commits into from
Jul 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
18f5453
Add Implement in Go
kkrull Jul 23, 2024
59de921
Add Go package structure
kkrull Jul 23, 2024
bc170b5
Annotate
kkrull Jul 23, 2024
c72aced
Cleanup
kkrull Jul 23, 2024
858881e
Add runtime dependency
kkrull Jul 23, 2024
8d13600
Describe test structure
kkrull Jul 23, 2024
3039412
Distinguish core and shell
kkrull Jul 23, 2024
683c06c
Make more accurate
kkrull Jul 23, 2024
a609d11
Organize diagram test code
kkrull Jul 23, 2024
f2a8f90
Clean up dependencies
kkrull Jul 23, 2024
ccebf71
Avoid initializer fucntions
kkrull Jul 23, 2024
ff69498
Add TODO
kkrull Jul 23, 2024
c04c097
Format
kkrull Jul 23, 2024
6ab6955
Case
kkrull Jul 24, 2024
dd2f2a5
Add flag for metaRepoHome
kkrull Jul 24, 2024
d81545f
Add singleton Config
kkrull Jul 24, 2024
17f6545
Look up flag
kkrull Jul 24, 2024
5e814a3
Add godoc
kkrull Jul 24, 2024
b02e5b7
Debug more
kkrull Jul 24, 2024
47f17c9
Organize
kkrull Jul 24, 2024
363950c
Get path at runtime
kkrull Jul 24, 2024
2bf9093
Remove unnecessary code
kkrull Jul 24, 2024
312027d
Move path information out of AppFactory
kkrull Jul 24, 2024
63f9d2f
Update error messages
kkrull Jul 24, 2024
307a98c
Inline
kkrull Jul 24, 2024
cc5a82e
Remove unnecessary code
kkrull Jul 24, 2024
6c4bbf1
Sort
kkrull Jul 24, 2024
eccbe27
DRY
kkrull Jul 24, 2024
a657026
Move path for now
kkrull Jul 24, 2024
8031735
Make AppFactory differently
kkrull Jul 24, 2024
24c6ab7
Make commands differently
kkrull Jul 24, 2024
4f5b0cc
Make it compile
kkrull Jul 24, 2024
c2c412b
Next step
kkrull Jul 24, 2024
619c262
Move stuff around again
kkrull Jul 24, 2024
01dca9f
remove redundancy
kkrull Jul 24, 2024
281afd7
Apply default path
kkrull Jul 24, 2024
60da0bc
Make meta-repo a persistent flag
kkrull Jul 24, 2024
1667d70
Move debug flag to config
kkrull Jul 24, 2024
f12de55
Disallow arguments
kkrull Jul 24, 2024
e6f9e37
Parse flags earlier
kkrull Jul 24, 2024
5c72eba
Clean up parsing and running
kkrull Jul 24, 2024
5e42d65
Organize run
kkrull Jul 24, 2024
1c5f87e
Get rid of global variable within package
kkrull Jul 24, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 163 additions & 3 deletions doc/decisions.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ Repo Management Tool, or "marmot" for short.
shell commands for a category of repositories as if they are all part of a single unit.
- Store meta data about categories externally, instead of in the Git repositories themselves.

## 01: Target Z Shell
## 01: ~~Target Z Shell~~

Superseded by: [Implement in Go](#06-implement-in-go).

Implement marmot in *nix tools that are widely-available on the platforms I use - e.g. MacOS, Linux,
and Windows Subsystem for Linux. Writing it in Z Shell can make it easier to try new ideas, while
Expand All @@ -50,7 +52,7 @@ back, or even share with teammates.
### Decisions

- Store meta data in JSON files.
- Use tools like `jq` and `jo` to query and construct JSON data from marmot.
- ~~Use tools like `jq` and `jo` to query and construct JSON data from marmot.~~
- Store meta data in its own Git repository.

## 03: Directory Structure in the Meta Repo
Expand Down Expand Up @@ -82,7 +84,9 @@ over what changes merit what kind of version bump.
- Minor version: Increment when adding a new feature (e.g. a command or sub-command).
- Patch version: Increment when refactoring to prepare for another feature.

## 05: Apply Single Responsibility Principle to scripts
## 05: ~~Apply Single Responsibility Principle to scripts~~

Superseded by: [Implement in Go](#06-implement-in-go).

Scripts are getting more complex, leading to duplication of concepts and algorithms. Applying the
Single Responsibility Principle (SRP) can help manage complexity and avoid unnecessary duplication.
Expand Down Expand Up @@ -119,3 +123,159 @@ Source: <https://unix.stackexchange.com/a/365417/37734>
to `source` dependencies and transitive dependencies.
- This is approach is intended to avoid any complexities in the same code being sourced twice. I
have no idea what could happen then, and I'd rather not have to find out.

## 06: Implement in Go

Supersedes: [Target Z Shell](#01-target-z-shell).

Compartmentalizing and organizing scripts helped with maintenance and extension, but it still became
difficult to split machine- and repository-specific data into separate files. Use of an external
data migration script provided a limited means to detect bugs by including some semi-formal test
automation, but it relied heavily upon the development platform; e.g. it used real Git repositories
on specific paths.

These factors led to some thinking about which language could replace the shell scripts. It would
need to be capable of targeting the same platforms, while offering a better means to structure data,
look up references, and refactor call sites. It would also need robust tools for test automation
and for creating Command Line Interfaces. Go offers all of those, while currently being a bit
easier to deploy to end users than Python or Ruby. Go also has potentially-compelling libraries
such as [`bubbletea`](https://github.com/charmbracelet/bubbletea), which raises the possibility of
making `marmot` more interactive and easier to use.

### Decision

Sprout a new codebase written in Go, until it has enough features to replace the ZSH version.

## 07: Go package structure

Developers will need a safe and effective way to add new entities and CLI commands, in order to add
new features. Distinguishing core entities (e.g. repositories and categories) and behavior (e.g.
categorizing Git repositories) from implementation details (e.g. interaction with the file system)
minimizes the amount of existing code that has to be modified in order to add new entities.

### Decisions

Structure Go code along these dimensions:

- Put all code in one repository. Use Go packages to distinguish the parts.
- Create `core` packages like `corerepository` for basic entities, data structures, and interfaces.
- Create `use` packages like `userepository` for operations upon each context.
- Create `svc` packages like `svcfs` for service implementations, such as using the file system.
- Create `main` package(s) like `mainfactory` to create dependencies and wire everything together.

This leads to the following dependencies (compile-time; runtime dependencies are dashed lines) among
top-level packages:

```mermaid
graph LR

%% Core and dependencies
subgraph FunctionalCore [Functional Core]
core(core<br/>Data structures<br/>Service interfaces)
svc(svc<br/>Services)
use(use<br/>Use Cases)

svc -->|CRUD<br/>implement| core
use -->|CRUD| core
use -.->|runtime| svc
end

%% Main program
subgraph ImperativeShell [Imperative Shell]
cmd(cmd<br/>CLI)
mainfactory(mainfactory<br/>Factories)
marmot(marmot<br/>Executable)

cmd -->|CRUD| core
cmd --> use
%%mainfactory -->|create| cmd
mainfactory -->|create| use
mainfactory -->|create| svc
marmot --> cmd
marmot --> mainfactory
end
```

Note: The code in the "functional core" is not always necessarily written in a functional style,
although that's an idea worth considering.

## 08: Go test strategy

As described in [Implement in Go](#06-implement-in-go), using a script-based architecture did not
offer a sufficiently-granular approach to testing. Prior experiences with formal testing tools–i.e.
[`bats`](https://github.com/sstephenson/bats)–proved impractical for team sizes greater than one.

Another factor involved in [Go package structure](#07-go-package-structure) relates to test
automation: Separating packages by bounded context also offers a practical means to distinguish test
automation that is more highly rewarding from that which is somewhat less rewarding. In other
words, tests on invariants and core logic tend to be easier to write and survive refactoring, while
tests on wiring and implementation details tend to be harder to write and are more readily thrown
out during refactoring.

### Decisions

- Focus test automation on core logic; e.g. "test from the middle".
- For small- to medium-sized tests of regular code:
- **Sources**: Co-locate with production code and package as `_test`, according to Go conventions.
- **Support code**: Add `testsupport` packages as necessary.
- **Test doubles**: Create additional `*mock` packages as necessary, such as `corerepositorymock`.
- **Tools**: Use `ginkgo` to clearly describe behavior.
- For medium- to large-sized tests of user-facing features:
- **Sources**: place sources in separate `cuke*` packages.
- **Support code**: Add `cukesupport` as necessary.
- **Tools**: Use `godog` to clearly describe features in Gherkin.

### Control Flow

```mermaid
graph TB

subgraph MarmotCore [Functional Core]
direction LR
core(core<br/>Data structures)
svc(svc<br/>Services)
use(use<br/>Use Cases)

svc -.-> core
use -.-> core
use -.-> svc
end

subgraph Tests
subgraph SmallTests [Small Tests]
direction LR
ginkgotests(_test<br/>Ginkgo tests)
ginkgomocks(*mock<br/>Test doubles)
ginkgosupport(testsupport*<br/>Test support)

ginkgotests -.-> ginkgomocks
ginkgotests -.-> ginkgosupport
end

subgraph LargeTests [Large Tests]
direction LR
godogfeatures(cukefeature<br/>godog scenarios)
godogsteps(cukesteps<br/>Step definitions)
godogsupport(cukesupport<br/>Helpers<br/>Hooks)

godogfeatures -.-> godogsupport
godogfeatures -.-> godogsteps
godogsteps -.-> godogsupport
end
end

LargeTests -->|validate| MarmotCore
SmallTests -->|verify| svc
SmallTests -->|verify| use
```

### Not Tested

```mermaid
graph LR

subgraph ImperativeShell [Production Code: Imperative Shell]
cmd(cmd<br/>CLI)
marmot(marmot<br/>Executable)
end
```
3 changes: 2 additions & 1 deletion src/go/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,9 @@ ginkgo-watch:
install-tools:
go install github.com/go-delve/delve/cmd/dlv@latest
go install github.com/onsi/ginkgo/v2/ginkgo
go install mvdan.cc/gofumpt@latest
go install github.com/spf13/cobra-cli@latest
go install golang.org/x/tools/cmd/godoc@latest
go install mvdan.cc/gofumpt@latest

.PHONY: run
run:
Expand Down
76 changes: 76 additions & 0 deletions src/go/cmd/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package cmd

import (
"fmt"
"io"
"os"
"path/filepath"

"github.com/kkrull/marmot/use"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)

/* Configuration */

func AddFlags(cobraCmd *cobra.Command) error {
addDebugFlag(cobraCmd)
if metaRepoErr := addMetaRepoFlag(cobraCmd); metaRepoErr != nil {
return metaRepoErr
} else {
return nil
}
}

func addDebugFlag(cobraCmd *cobra.Command) {
cobraCmd.PersistentFlags().Bool("debug", false, "print CLI debugging information")
cobraCmd.PersistentFlags().Lookup("debug").Hidden = true
}

func addMetaRepoFlag(cobraCmd *cobra.Command) error {
if homeDir, homeErr := os.UserHomeDir(); homeErr != nil {
return fmt.Errorf("failed to locate home directory; %w", homeErr)
} else {
cobraCmd.PersistentFlags().String(
"meta-repo",
filepath.Join(homeDir, "meta"),
"Meta repo to use",
)
return nil
}
}

/* Use */

func ParseFlags(cobraCmd *cobra.Command) (*Config, error) {
flags := cobraCmd.Flags()
if debug, debugErr := flags.GetBool("debug"); debugErr != nil {
return nil, debugErr
} else if metaRepoPath, metaRepoPathErr := flags.GetString("meta-repo"); metaRepoPathErr != nil {
return nil, metaRepoPathErr
} else {
return &Config{
AppFactory: *use.NewAppFactory(),
Debug: debug,
MetaRepoPath: metaRepoPath,
flagSet: flags,
}, nil
}
}

type Config struct {
AppFactory use.AppFactory
Debug bool
MetaRepoPath string
flagSet *pflag.FlagSet
}

func (config Config) PrintDebug(writer io.Writer) {
fmt.Fprintf(writer, "Flags:\n")

debugFlag := config.flagSet.Lookup("debug")
fmt.Fprintf(writer, "- debug [%v]: %v\n", debugFlag.DefValue, debugFlag.Value)

metaRepoFlag := config.flagSet.Lookup("meta-repo")
fmt.Fprintf(writer, "- meta-repo [%v]: %v\n", metaRepoFlag.DefValue, metaRepoFlag.Value)
}
74 changes: 29 additions & 45 deletions src/go/cmd/root_command.go
Original file line number Diff line number Diff line change
@@ -1,37 +1,39 @@
package cmd

import (
"fmt"
"io"

"github.com/spf13/cobra"
)

var (
debugFlag *bool
rootCmd = &cobra.Command{
Long: "marmot manages a Meta Repository that organizes content in other (Git) repositories.",
RunE: func(cmd *cobra.Command, args []string) error {
if *debugFlag {
printDebug()
return nil
} else if len(args) == 0 {
return cmd.Help()
} else {
return nil
}
},
Short: "Meta Repo Management Tool",
Use: "marmot [--help|--version]",
// Configure the root command with the given I/O and version identifier, then return for use.
func NewRootCommand(stdout io.Writer, stderr io.Writer, version string) (*cobra.Command, error) {
rootCmd := &cobra.Command{
Long: "marmot manages a Meta Repository that organizes content in other (Git) repositories.",
RunE: runRoot,
Short: "Meta Repo Management Tool",
Use: "marmot",
Version: version,
}
)

// Configure the root command with the given I/O and version identifier, then return for use.
func NewRootCommand(stdout io.Writer, stderr io.Writer, version string) *cobra.Command {
AddFlags(rootCmd)
addGroups(rootCmd)
rootCmd.SetOut(stdout)
rootCmd.SetErr(stderr)
rootCmd.Version = version
return rootCmd
return rootCmd, nil
}

func runRoot(cobraCmd *cobra.Command, args []string) error {
if config, parseErr := ParseFlags(cobraCmd); parseErr != nil {
return parseErr
} else if config.Debug {
config.PrintDebug(cobraCmd.OutOrStdout())
return nil
} else if len(args) == 0 {
return cobraCmd.Help()
} else {
return nil
}
}

/* Child commands */
Expand All @@ -40,29 +42,11 @@ const (
metaRepoGroup = "meta-repo"
)

func AddMetaRepoCommand(child *cobra.Command) {
child.GroupID = metaRepoGroup
rootCmd.AddCommand(child)
}

/* Configuration */

func init() {
initFlags()
initGroups()
}

func initFlags() {
debugFlag = rootCmd.PersistentFlags().Bool("debug", false, "print CLI debugging information")
rootCmd.PersistentFlags().Lookup("debug").Hidden = true
func addGroups(cobraCmd *cobra.Command) {
cobraCmd.AddGroup(&cobra.Group{ID: metaRepoGroup, Title: "Meta Repo Commands"})
}

func initGroups() {
rootCmd.AddGroup(&cobra.Group{ID: metaRepoGroup, Title: "Meta Repo Commands"})
}

/* Pseudo-commands */

func printDebug() {
fmt.Printf("--debug: %v\n", *debugFlag)
func AddMetaRepoCommand(parent *cobra.Command, child cobra.Command) {
child.GroupID = metaRepoGroup
parent.AddCommand(&child)
}
Loading