Skip to content

Commit

Permalink
temp: initial large reorganization and polishing
Browse files Browse the repository at this point in the history
+ Make as much private as possible.
+ Split distinct pieces into separate files.
+ Improve names.
  • Loading branch information
telemachus committed Nov 29, 2024
1 parent 5c47304 commit 7450a26
Show file tree
Hide file tree
Showing 9 changed files with 199 additions and 180 deletions.
114 changes: 21 additions & 93 deletions internal/cli/app.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
// Package cli creates and runs command line interfaces.
package cli

import (
"encoding/json"
"flag"
"fmt"
"io"
"os"
"path/filepath"
"slices"
"strings"
)

Expand All @@ -18,102 +14,34 @@ const (
exitFailure = 1
)

// App stores information about the application's state.
type App struct {
HomeDir string
CmdName string
Usage string
ExitValue int
QuietWanted bool
type appEnv struct {
homeDir string
cmd string
subCmd string
usage string
exitValue int
quietWanted bool
}

// NoOp determines whether an App should bail out.
func (app *App) NoOp() bool {
return app.ExitValue != exitSuccess
func (app *appEnv) noOp() bool {
return app.exitValue != exitSuccess
}

// NewApp returns a new App pointer.
func NewApp(cmdUsage string) *App {
func newAppEnv(subCmd string) *appEnv {
homeDir, err := os.UserHomeDir()
if err != nil {
fmt.Fprintf(os.Stderr, "%s: %s\n", gitmirrorName, err)
return &App{ExitValue: exitFailure}
fmt.Fprintf(os.Stderr, "%s %s: %s\n", gitmirrorName, subCmd, err)
return &appEnv{exitValue: exitFailure}
}
return &App{
CmdName: gitmirrorName,
Usage: gitmirrorUsage,
ExitValue: exitSuccess,
HomeDir: homeDir,
return &appEnv{
cmd: gitmirrorName,
subCmd: subCmd,
usage: gitmirrorUsage,
exitValue: exitSuccess,
homeDir: homeDir,
}
}

// Repo stores information about a git repository.
type Repo struct {
URL string
Name string
}

// Flags handles flags and options in my finicky way.
func (app *App) Flags(args []string) (string, bool) {
if app.NoOp() {
return "", false
}
cmdFlags := flag.NewFlagSet("gitmirror", flag.ContinueOnError)
cmdFlags.SetOutput(io.Discard)
var helpWanted bool
cmdFlags.BoolVar(&helpWanted, "help", false, "")
cmdFlags.BoolVar(&helpWanted, "h", false, "")
cmdFlags.BoolVar(&app.QuietWanted, "quiet", false, "")
cmdFlags.BoolVar(&app.QuietWanted, "q", false, "")
var configFile string
var configIsDefault bool
cmdFlags.StringVar(&configFile, "config", "", "")
cmdFlags.StringVar(&configFile, "c", "", "")
err := cmdFlags.Parse(args)
switch {
// This must precede all other checks.
case err != nil:
fmt.Fprintf(os.Stderr, "%s: %s\n%s", app.CmdName, err, app.Usage)
app.ExitValue = exitFailure
case helpWanted:
fmt.Fprintf(os.Stderr, "%s: use 'help' not '-help' or '-h'\n", app.CmdName)
fmt.Fprint(os.Stderr, app.Usage)
app.ExitValue = exitFailure
case configFile == "":
configFile = defaultConfig
configIsDefault = true
}
return configFile, configIsDefault
}

// Unmarshal reads a configuration file and returns a slice of Repo.
func (app *App) Unmarshal(configFile string, configIsDefault bool) []Repo {
if app.NoOp() {
return nil
}
if configIsDefault {
configFile = filepath.Join(app.HomeDir, configFile)
}
blob, err := os.ReadFile(configFile)
if err != nil {
fmt.Fprintf(os.Stderr, "%s: %s\n", app.CmdName, err)
app.ExitValue = exitFailure
return nil
}
repos := make([]Repo, 0, 20)
err = json.Unmarshal(blob, &repos)
if err != nil {
fmt.Fprintf(os.Stderr, "%s: %s\n", app.CmdName, err)
app.ExitValue = exitFailure
return nil
}
// Every repository must have a URL and a directory name.
return slices.DeleteFunc(repos, func(repo Repo) bool {
return repo.URL == "" || repo.Name == ""
})
}

// PrettyPath replaces a user's home directory with ~ in a string.
func (app *App) PrettyPath(s string) string {
return strings.Replace(s, app.HomeDir, "~", 1)
func (app *appEnv) prettyPath(s string) string {
return strings.Replace(s, app.homeDir, "~", 1)
}
38 changes: 18 additions & 20 deletions internal/cli/clone.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,37 +7,36 @@ import (
"path/filepath"
)

// Clone runs git repote update on a group of repositories.
func (app *App) Clone(repos []Repo) {
if app.NoOp() {
func (app *appEnv) clone(repos []Repo) {
if app.noOp() {
return
}
err := os.MkdirAll(filepath.Join(app.HomeDir, defaultStorage), os.ModePerm)
err := os.MkdirAll(filepath.Join(app.homeDir, defaultStorage), os.ModePerm)
if err != nil {
app.ExitValue = exitFailure
app.exitValue = exitFailure
return
}
ch := make(chan result)
for _, repo := range repos {
go app.clone(repo, ch)
go app.cloneOne(repo, ch)
}
for range repos {
res := <-ch
res.publish(app.QuietWanted)
res.publish(app.quietWanted)
}
}

func (app *App) clone(repo Repo, ch chan<- result) {
func (app *appEnv) cloneOne(repo 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.
// If a directory with the repo's name exists, I simply want to send
// a result saying that the repo exists.
repoPath := filepath.Join(app.HomeDir, defaultStorage, repo.Name)
storagePath := filepath.Join(app.HomeDir, defaultStorage)
repoPath := filepath.Join(app.homeDir, defaultStorage, repo.Name)
storagePath := filepath.Join(app.homeDir, defaultStorage)
if _, err := os.Stat(repoPath); err == nil {
prettyPath := app.PrettyPath(storagePath)
prettyPath := app.prettyPath(storagePath)
ch <- result{
isErr: false,
msg: fmt.Sprintf("%s: already present in %s", repo.Name, prettyPath),
Expand All @@ -49,10 +48,10 @@ func (app *App) clone(repo Repo, ch chan<- result) {
noGitPrompt := "GIT_TERMINAL_PROMPT=0"
env := append(os.Environ(), noGitPrompt)
cmd.Env = env
cmd.Dir = filepath.Join(app.HomeDir, defaultStorage)
cmd.Dir = filepath.Join(app.homeDir, defaultStorage)
err := cmd.Run()
if err != nil {
app.ExitValue = exitFailure
app.exitValue = exitFailure
ch <- result{
isErr: true,
msg: fmt.Sprintf("%s: %s", repo.Name, err),
Expand All @@ -65,11 +64,10 @@ func (app *App) clone(repo Repo, ch chan<- result) {
}
}

// CmdClone clones requested repos locally for mirroring.
func CmdClone(args []string) int {
app := NewApp(cloneUsage)
configFile, configIsDefault := app.Flags(args)
repos := app.Unmarshal(configFile, configIsDefault)
app.Clone(repos)
return app.ExitValue
func cmdClone(args []string) int {
app := newAppEnv("clone")
configFile, configIsDefault := app.parse(args)
repos := app.getRepos(configFile, configIsDefault)
app.clone(repos)
return app.exitValue
}
63 changes: 63 additions & 0 deletions internal/cli/flags.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package cli

import (
"flag"
"fmt"
"io"
"os"
)

func (app *appEnv) parse(args []string) (string, bool) {
if app.noOp() {
return "", false
}
cmdFlags := flag.NewFlagSet("gitmirror", flag.ContinueOnError)
cmdFlags.SetOutput(io.Discard)
var helpWanted bool
cmdFlags.BoolVar(&helpWanted, "help", false, "")
cmdFlags.BoolVar(&helpWanted, "h", false, "")
cmdFlags.BoolVar(&app.quietWanted, "quiet", false, "")
cmdFlags.BoolVar(&app.quietWanted, "q", false, "")
var configFile string
var configIsDefault bool
cmdFlags.StringVar(&configFile, "config", "", "")
cmdFlags.StringVar(&configFile, "c", "", "")
err := cmdFlags.Parse(args)
switch {
// This must precede all other checks.
case err != nil:
fmt.Fprintf(
os.Stderr,
"%s %s: %s\n%s",
app.cmd,
app.subCmd,
err,
app.usage,
)
app.exitValue = exitFailure
case helpWanted:
fmt.Fprintf(
os.Stderr,
"%s: use 'help' not '-help' or '-h'\n",
app.cmd,
)
fmt.Fprint(os.Stderr, app.usage)
app.exitValue = exitFailure
case configFile == "":
configFile = defaultConfig
configIsDefault = true
}
extraArgs := cmdFlags.Args()
if len(extraArgs) > 0 {
fmt.Fprintf(
os.Stderr,
"%s %s: unrecognized arguments: %+v\n",
app.cmd,
app.subCmd,
extraArgs,
)
fmt.Fprint(os.Stderr, app.usage)
app.exitValue = exitFailure
}
return configFile, configIsDefault
}
19 changes: 10 additions & 9 deletions internal/cli/gitmirror.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,29 +10,30 @@ const (
gitmirrorVersion = "v0.9.0"
)

// CmdWrapper turns, e.g., `gitmirror update -q` into `gitmirror-update -q`.
// CmdGitmirror runs a subcommand and returns success or failure to the shell.
func CmdGitmirror(args []string) int {
if len(args) < 1 {
fmt.Fprint(os.Stderr, gitmirrorUsage)
return exitFailure
}
// Give the user a hand if they try `gitmirror --help update`.
if args[0] == "--help" || args[0] == "-help" || args[0] == "-h" {
// Give users a hand if they try, e.g., `gitmirror -(-)help|-h update`.
subCmd := args[0]
if subCmd == "--help" || subCmd == "-help" || subCmd == "-h" {
args[0] = "help"
}
var exitValue int
switch args[0] {
switch subCmd {
case "clone":
exitValue = CmdClone(args[1:])
exitValue = cmdClone(args[1:])
case "update", "up":
exitValue = CmdUpdate(args[1:])
exitValue = cmdUpdate(args[1:])
case "version":
exitValue = CmdVersion(args[1:])
exitValue = cmdVersion(args[1:])
case "help":
// TODO: write the help in Asciidoc?
exitValue = CmdHelp(args[1:])
exitValue = cmdHelp(args[1:])
default:
fmt.Fprintf(os.Stderr, "gitmirror: unrecognized subcommand: \"%s\"\n", args[0])
fmt.Fprintf(os.Stderr, "gitmirror: unrecognized subcommand: \"%s\"\n", subCmd)
fmt.Fprint(os.Stderr, gitmirrorUsage)
exitValue = exitFailure
}
Expand Down
20 changes: 9 additions & 11 deletions internal/cli/help.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,20 @@ import (
"os"
)

// Help displays the help message for a command.
func (app *App) Help(args []string) {
if app.NoOp() {
func (app *appEnv) help(args []string) {
if app.noOp() {
return
}
if len(args) < 1 {
fmt.Fprintf(os.Stderr, "%s help: no subcommand given\n", gitmirrorName)
fmt.Fprint(os.Stderr, helpUsage)
app.ExitValue = exitFailure
app.exitValue = exitFailure
return
}
if len(args) > 1 {
fmt.Fprintf(os.Stderr, "%s help: too many arguments: %+v\n", gitmirrorName, args)
fmt.Fprint(os.Stderr, helpUsage)
app.ExitValue = exitFailure
app.exitValue = exitFailure
return
}
switch args[0] {
Expand All @@ -34,13 +33,12 @@ func (app *App) Help(args []string) {
default:
fmt.Fprintf(os.Stderr, "%s help: unrecognized subcommand: \"%s\"\n", gitmirrorName, args[0])
fmt.Fprint(os.Stderr, helpUsage)
app.ExitValue = exitFailure
app.exitValue = exitFailure
}
}

// CmdHelp clones requested repos locally for mirroring.
func CmdHelp(args []string) int {
app := NewApp(helpUsage)
app.Help(args)
return app.ExitValue
func cmdHelp(args []string) int {
app := newAppEnv("help")
app.help(args)
return app.exitValue
}
Loading

0 comments on commit 7450a26

Please sign in to comment.