Skip to content

Commit

Permalink
feat: parse arguments for subcommands too
Browse files Browse the repository at this point in the history
  • Loading branch information
telemachus committed Jan 20, 2025
1 parent c354d56 commit 5e515d2
Show file tree
Hide file tree
Showing 9 changed files with 240 additions and 123 deletions.
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ fmt:

lint: fmt
staticcheck ./...
revive -config revive.toml -exclude internal/optionparser ./...
revive -config revive.toml -exclude internal/flag ./...
golangci-lint run

golangci: fmt
Expand All @@ -18,7 +18,7 @@ staticcheck: fmt
staticcheck ./...

revive: fmt
revive -config revive.toml -exclude internal/optionparser ./...
revive -config revive.toml -exclude internal/flag ./...

test:
go test -shuffle on github.com/telemachus/gitmirror/internal/cli
Expand Down
130 changes: 92 additions & 38 deletions internal/cli/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,42 +11,45 @@ import (
"github.com/telemachus/gitmirror/internal/flag"
)

type appEnv struct {
cmd string
subCmd string
config string
home string
storage string
exitVal int
help bool
quiet bool
version bool
type cmdEnv struct {
name string
version string
subCmdName string
confFile string
homeDir string
dataDir string
subCmdArgs []string
exitVal int
helpWanted bool
quietWanted bool
versionWanted bool
}

func appFrom(args []string) (*appEnv, error) {
app := &appEnv{cmd: cmd, exitVal: exitSuccess}
func cmdFrom(name, version string, args []string) (*cmdEnv, error) {
cmd := &cmdEnv{name: name, version: version, exitVal: exitSuccess}

fs := flag.NewFlagSet("gitmirror")
fs.StringVar(&app.config, "config", "", "")
fs.StringVar(&app.config, "c", "", "")
fs.BoolVar(&app.help, "help", false, "")
fs.BoolVar(&app.help, "h", false, "")
fs.BoolVar(&app.quiet, "quiet", false, "")
fs.BoolVar(&app.quiet, "q", false, "")
fs.BoolVar(&app.version, "version", false, "")
fs := flag.NewFlagSet(cmd.name)
fs.StringVar(&cmd.confFile, "config", "", "")
fs.StringVar(&cmd.confFile, "c", "", "")
fs.BoolVar(&cmd.helpWanted, "help", false, "")
fs.BoolVar(&cmd.helpWanted, "h", false, "")
fs.BoolVar(&cmd.quietWanted, "quiet", false, "")
fs.BoolVar(&cmd.quietWanted, "q", false, "")
fs.BoolVar(&cmd.versionWanted, "V", false, "")
fs.BoolVar(&cmd.versionWanted, "version", false, "")

if err := fs.Parse(args); err != nil {
return nil, err
}

// Quick and dirty, but why be fancy in these cases?
if app.help {
fmt.Print(_usage)
os.Exit(exitSuccess)
if cmd.helpWanted {
fmt.Print(cmdUsage)
os.Exit(cmd.exitVal)
}
if app.version {
fmt.Printf("%s %s\n", cmd, cmdVersion)
os.Exit(exitSuccess)
if cmd.versionWanted {
fmt.Printf("%s %s\n", cmd.name, cmd.version)
os.Exit(cmd.exitVal)
}

// Do not continue if we cannot parse and validate arguments or get the
Expand All @@ -55,31 +58,82 @@ func appFrom(args []string) (*appEnv, error) {
if err := validate(extraArgs); err != nil {
return nil, err
}
home, err := os.UserHomeDir()
cmd.subCmdName = extraArgs[0]
cmd.subCmdArgs = extraArgs[1:]
homeDir, err := os.UserHomeDir()
if err != nil {
return nil, err
}
cmd.homeDir = homeDir

if app.config == "" {
app.config = filepath.Join(home, config)
if cmd.confFile == "" {
cmd.confFile = filepath.Join(cmd.homeDir, confFile)
}
app.storage = filepath.Join(home, storage)
app.subCmd = extraArgs[0]
cmd.dataDir = filepath.Join(cmd.homeDir, dataDir)

return app, nil
return cmd, nil
}

func (app *appEnv) noOp() bool {
return app.exitVal != exitSuccess
func (cmd *cmdEnv) subCmdFrom(args []string) error {
fs := flag.NewFlagSet(cmd.name + " " + cmd.subCmdName)
fs.StringVar(&cmd.confFile, "config", cmd.confFile, "")
fs.StringVar(&cmd.confFile, "c", cmd.confFile, "")
fs.BoolVar(&cmd.helpWanted, "help", false, "")
fs.BoolVar(&cmd.helpWanted, "h", false, "")
fs.BoolVar(&cmd.quietWanted, "quiet", cmd.quietWanted, "")
fs.BoolVar(&cmd.quietWanted, "q", cmd.quietWanted, "")
fs.BoolVar(&cmd.versionWanted, "V", false, "")
fs.BoolVar(&cmd.versionWanted, "version", false, "")

if err := fs.Parse(args); err != nil {
cmd.exitVal = exitFailure
return err
}

// Quick and dirty, but why be fancy in these cases?
if cmd.helpWanted {
// TODO: make this print correct usage for subCmdName.
cmd.subCmdUsage(cmd.subCmdName)
os.Exit(cmd.exitVal)
}
if cmd.versionWanted {
fmt.Printf("%s %s %s\n", cmd.name, cmd.subCmdName, cmd.version)
os.Exit(cmd.exitVal)
}

// There should be no extra arguments.
extraArgs := fs.Args()
if len(extraArgs) != 0 {
return fmt.Errorf("unrecognized arguments: %+v", extraArgs)
}

return nil
}

func (cmd *cmdEnv) subCmdUsage(subCmdName string) {
switch subCmdName {
case "update", "up":
fmt.Print(updateUsage)
case "clone":
fmt.Print(cloneUsage)
case "sync":
fmt.Print(syncUsage)
default:
fmt.Printf("%s %s: unrecognized subcommand %q\n", cmd.name, cmd.subCmdName, subCmdName)
}
}

func (cmd *cmdEnv) noOp() bool {
return cmd.exitVal != exitSuccess
}

func (app *appEnv) prettyPath(s string) string {
return strings.Replace(s, app.home, "~", 1)
func (cmd *cmdEnv) prettyPath(s string) string {
return strings.Replace(s, cmd.homeDir, "~", 1)
}

func validate(extra []string) error {
if len(extra) != 1 {
return errors.New("one (and only one) subcommand is required")
if len(extra) < 1 {
return errors.New("a subcommand is required")
}

// The only recognized subcommands are clone, up(date), and sync.
Expand Down
44 changes: 24 additions & 20 deletions internal/cli/clone.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,30 +7,34 @@ import (
"path/filepath"
)

func subCmdClone(app *appEnv) {
rs := app.repos()
app.clone(rs)
func subCmdClone(cmd *cmdEnv) {
if err := cmd.subCmdFrom(cmd.subCmdArgs); err != nil {
fmt.Fprintf(os.Stderr, "%s %s: %s", cmd.name, cmd.subCmdName, err)
return
}
rs := cmd.repos()
cmd.clone(rs)
}

func (app *appEnv) clone(rs []Repo) {
if app.noOp() {
func (cmd *cmdEnv) clone(rs []Repo) {
if cmd.noOp() {
return
}

err := os.MkdirAll(app.storage, os.ModePerm)
err := os.MkdirAll(cmd.dataDir, os.ModePerm)
if err != nil {
fmt.Fprintf(os.Stderr, "%s %s: %s\n", app.cmd, app.subCmd, err)
app.exitVal = exitFailure
fmt.Fprintf(os.Stderr, "%s %s: %s\n", cmd.name, cmd.subCmdName, err)
cmd.exitVal = exitFailure
return
}

ch := make(chan result)
for _, r := range rs {
go app.cloneOne(r, ch)
go cmd.cloneOne(r, ch)
}
for range rs {
res := <-ch
switch app.quiet {
switch cmd.quietWanted {
case true:
res.publishError()
default:
Expand All @@ -39,35 +43,35 @@ func (app *appEnv) clone(rs []Repo) {
}
}

func (app *appEnv) cloneOne(r Repo, ch chan<- result) {
func (cmd *cmdEnv) cloneOne(r Repo, ch chan<- result) {
// Normally, it is a bad idea to check whether a directory exists
// before trying an operation. However, this case is an exception.
// git clone --mirror /path/to/existing/repo.git will fail with an
// error, but for the purpose of this app, there is no error.
// error, but for the purpose of this command, there is no error.
// If a directory with the repo's name exists, I simply want to skip
// that repo.
rPath := filepath.Join(app.storage, r.Name)
rPath := filepath.Join(cmd.dataDir, r.Name)
if _, err := os.Stat(rPath); err == nil {
ch <- result{
isErr: false,
msg: fmt.Sprintf("%s: already present in %s", r.Name, app.prettyPath(app.storage)),
msg: fmt.Sprintf("%s: already present in %s", r.Name, cmd.prettyPath(cmd.dataDir)),
}
return
}

args := []string{"clone", "--mirror", r.URL, r.Name}
cmd := exec.Command("git", args...)
gitCmd := exec.Command("git", args...)
noGitPrompt := "GIT_TERMINAL_PROMPT=0"
env := append(os.Environ(), noGitPrompt)
cmd.Env = env
cmd.Dir = app.storage
gitCmd.Env = env
gitCmd.Dir = cmd.dataDir

err := cmd.Run()
err := gitCmd.Run()
if err != nil {
app.exitVal = exitFailure
cmd.exitVal = exitFailure
ch <- result{
isErr: true,
msg: fmt.Sprintf("%s %s: %s: %s", app.cmd, app.subCmd, r.Name, err),
msg: fmt.Sprintf("%s %s: %s: %s", cmd.name, cmd.subCmdName, r.Name, err),
}
return
}
Expand Down
24 changes: 12 additions & 12 deletions internal/cli/gitmirror.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,33 +6,33 @@ import (
)

const (
cmd = "gitmirror"
cmdName = "gitmirror"
cmdVersion = "v0.9.5"
config = ".gitmirror.json"
storage = ".local/share/gitmirror"
confFile = ".gitmirror.json"
dataDir = ".local/share/gitmirror"
exitSuccess = 0
exitFailure = 1
)

// Gitmirror runs a subcommand and returns success or failure to the shell.
func Gitmirror(args []string) int {
app, err := appFrom(args)
cmd, err := cmdFrom(cmdName, cmdVersion, args)
if err != nil {
fmt.Fprintf(os.Stderr, "%s: %s\n", cmd, err)
fmt.Fprintf(os.Stderr, "%s: %s\n", cmdName, err)
return exitFailure
}

switch app.subCmd {
switch cmd.subCmdName {
case "update", "up":
subCmdUpdate(app)
subCmdUpdate(cmd)
case "clone":
subCmdClone(app)
subCmdClone(cmd)
case "sync":
subCmdSync(app)
subCmdSync(cmd)
default:
fmt.Fprintf(os.Stderr, "%s: unrecognized subcommand %q\n", app.cmd, app.subCmd)
app.exitVal = exitFailure
fmt.Fprintf(os.Stderr, "%s: unrecognized subcommand %q\n", cmd.name, cmd.subCmdName)
cmd.exitVal = exitFailure
}

return app.exitVal
return cmd.exitVal
}
14 changes: 7 additions & 7 deletions internal/cli/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,15 @@ type Repo struct {
Name string
}

func (app *appEnv) repos() []Repo {
if app.noOp() {
func (cmd *cmdEnv) repos() []Repo {
if cmd.noOp() {
return nil
}

conf, err := os.ReadFile(app.config)
conf, err := os.ReadFile(cmd.confFile)
if err != nil {
app.exitVal = exitFailure
fmt.Fprintf(os.Stderr, "%s %s: %s\n", app.cmd, app.subCmd, err)
cmd.exitVal = exitFailure
fmt.Fprintf(os.Stderr, "%s %s: %s\n", cmd.name, cmd.subCmdName, err)
return nil
}

Expand All @@ -32,8 +32,8 @@ func (app *appEnv) repos() []Repo {
}
err = json.Unmarshal(conf, &cfg)
if err != nil {
app.exitVal = exitFailure
fmt.Fprintf(os.Stderr, "%s %s: %s\n", app.cmd, app.subCmd, err)
cmd.exitVal = exitFailure
fmt.Fprintf(os.Stderr, "%s %s: %s\n", cmd.name, cmd.subCmdName, err)
return nil
}

Expand Down
Loading

0 comments on commit 5e515d2

Please sign in to comment.