Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .task/checksum/docs
Original file line number Diff line number Diff line change
@@ -1 +1 @@
8cdc8a0d92ea52364fe347f5b64a72e6
f998a0f05834950ec6719dfc2c4a6c95
83 changes: 42 additions & 41 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,10 +119,10 @@ hourgit init [--project <name>] [--force] [--merge] [--yes]

| Flag | Default | Description |
|------|---------|-------------|
| `--project` | auto-detect | Assign repository to a project by name or ID (creates if needed) |
| `--force` | `false` | Overwrite existing post-checkout hook |
| `--merge` | `false` | Append to existing post-checkout hook |
| `--yes` | `false` | Skip confirmation prompt |
| `-p`, `--project` | auto-detect | Assign repository to a project by name or ID (creates if needed) |
| `-f`, `--force` | `false` | Overwrite existing post-checkout hook |
| `-m`, `--merge` | `false` | Append to existing post-checkout hook |
| `-y`, `--yes` | `false` | Skip confirmation prompt |

#### `hourgit log`

Expand All @@ -134,12 +134,12 @@ hourgit log [MESSAGE] [--duration <dur>] [--from <time>] [--to <time>] [--date <

| Flag | Default | Description |
|------|---------|-------------|
| `--project` | auto-detect | Project name or ID |
| `--duration` | — | Duration to log (e.g. `30m`, `3h`, `1d3h30m`) |
| `--from` | — | Start time (e.g. `9am`, `14:00`) |
| `--to` | — | End time (e.g. `5pm`, `17:00`) |
| `--date` | today | Date to log for (`YYYY-MM-DD`) |
| `--task` | — | Task label for this entry |
| `-p`, `--project` | auto-detect | Project name or ID |
| `-d`, `--duration` | — | Duration to log (e.g. `30m`, `3h`, `1d3h30m`) |
| `-F`, `--from` | — | Start time (e.g. `9am`, `14:00`) |
| `-T`, `--to` | — | End time (e.g. `5pm`, `17:00`) |
| `-D`, `--date` | today | Date to log for (`YYYY-MM-DD`) |
| `-t`, `--task` | — | Task label for this entry |

> `--duration` and `--from`/`--to` are mutually exclusive. A message is always required (prompted if not provided).

Expand Down Expand Up @@ -170,13 +170,14 @@ hourgit edit <hash> [--duration <dur>] [--from <time>] [--to <time>] [--date <da

| Flag | Default | Description |
|------|---------|-------------|
| `--project` | auto-detect | Project name or ID |
| `--duration` | — | New duration (e.g. `30m`, `3h`, `3h30m`) |
| `--from` | — | New start time (e.g. `9am`, `14:00`) |
| `--to` | — | New end time (e.g. `5pm`, `17:00`) |
| `--date` | — | New date (`YYYY-MM-DD`) |
| `--task` | — | New task label (empty string clears it) |
| `-p`, `--project` | auto-detect | Project name or ID |
| `-d`, `--duration` | — | New duration (e.g. `30m`, `3h`, `3h30m`) |
| `-F`, `--from` | — | New start time (e.g. `9am`, `14:00`) |
| `-T`, `--to` | — | New end time (e.g. `5pm`, `17:00`) |
| `-D`, `--date` | — | New date (`YYYY-MM-DD`) |
| `-t`, `--task` | — | New task label (empty string clears it) |
| `-m`, `--message` | — | New message |
| `-y`, `--yes` | `false` | Skip confirmation prompt |

> `--duration` and `--from`/`--to` are mutually exclusive. `--from` only: keeps existing end time, recalculates duration. `--to` only: keeps existing start time, recalculates duration. Entry ID and creation timestamp are preserved. If the entry is not found in the current repo's project, all projects are searched.

Expand All @@ -200,8 +201,8 @@ hourgit remove <hash> [--project <name>] [--yes]

| Flag | Default | Description |
|------|---------|-------------|
| `--project` | auto-detect | Project name or ID |
| `--yes` | `false` | Skip confirmation prompt |
| `-p`, `--project` | auto-detect | Project name or ID |
| `-y`, `--yes` | `false` | Skip confirmation prompt |

> Works with both log and checkout entries (unlike `edit`, which only supports log entries). Shows entry details and asks for confirmation before deleting. If the entry is not found in the current repo's project, all projects are searched.

Expand All @@ -215,7 +216,7 @@ hourgit sync [--project <name>]

| Flag | Default | Description |
|------|---------|-------------|
| `--project` | auto-detect | Project name or ID |
| `-p`, `--project` | auto-detect | Project name or ID |

#### `hourgit report`

Expand All @@ -227,11 +228,11 @@ hourgit report [--month <1-12>] [--week <1-53>] [--year <YYYY>] [--project <name

| Flag | Default | Description |
|------|---------|-------------|
| `--month` | current month | Month number 1-12 |
| `--week` | — | ISO week number 1-53 |
| `--year` | current year | Year (complementary to `--month` or `--week`) |
| `--project` | auto-detect | Project name or ID |
| `--export` | — | Export format (`pdf`); auto-generates filename based on period |
| `-m`, `--month` | current month | Month number 1-12 |
| `-w`, `--week` | — | ISO week number 1-53 |
| `-y`, `--year` | current year | Year (complementary to `--month` or `--week`) |
| `-p`, `--project` | auto-detect | Project name or ID |
| `-e`, `--export` | — | Export format (`pdf`); auto-generates filename based on period |

> `--month` and `--week` cannot be used together. `--year` alone is not valid — it must be paired with `--month` or `--week`. Neither flag defaults to the current month.

Expand Down Expand Up @@ -270,8 +271,8 @@ hourgit history [--project <name>] [--limit <N>]

| Flag | Default | Description |
|------|---------|-------------|
| `--project` | all projects | Filter by project name or ID |
| `--limit` | `50` | Maximum number of entries to show (use `0` for all) |
| `-p`, `--project` | all projects | Filter by project name or ID |
| `-l`, `--limit` | `50` | Maximum number of entries to show (use `0` for all) |

> Each line shows the entry hash, timestamp, type (log or checkout), project name, and details. Log entries display duration + task label (if set) + message. Checkout entries display previous branch → next branch.

Expand All @@ -285,7 +286,7 @@ hourgit status [--project <name>]

| Flag | Default | Description |
|------|---------|-------------|
| `--project` | auto-detect | Project name or ID |
| `-p`, `--project` | auto-detect | Project name or ID |

**Output includes:**

Expand Down Expand Up @@ -321,8 +322,8 @@ hourgit project assign <name> [--force] [--yes]

| Flag | Default | Description |
|------|---------|-------------|
| `--force` | `false` | Reassign repository to a different project |
| `--yes` | `false` | Skip confirmation prompt |
| `-f`, `--force` | `false` | Reassign repository to a different project |
| `-y`, `--yes` | `false` | Skip confirmation prompt |

#### `hourgit project list`

Expand All @@ -344,7 +345,7 @@ hourgit project remove <name> [--yes]

| Flag | Default | Description |
|------|---------|-------------|
| `--yes` | `false` | Skip confirmation prompt |
| `-y`, `--yes` | `false` | Skip confirmation prompt |

### Schedule Configuration

Expand All @@ -362,7 +363,7 @@ hourgit config get [--project <name>]

| Flag | Default | Description |
|------|---------|-------------|
| `--project` | auto-detect | Project name or ID |
| `-p`, `--project` | auto-detect | Project name or ID |

#### `hourgit config set`

Expand All @@ -374,7 +375,7 @@ hourgit config set [--project <name>]

| Flag | Default | Description |
|------|---------|-------------|
| `--project` | auto-detect | Project name or ID |
| `-p`, `--project` | auto-detect | Project name or ID |

#### `hourgit config reset`

Expand All @@ -386,8 +387,8 @@ hourgit config reset [--project <name>] [--yes]

| Flag | Default | Description |
|------|---------|-------------|
| `--project` | auto-detect | Project name or ID |
| `--yes` | `false` | Skip confirmation prompt |
| `-p`, `--project` | auto-detect | Project name or ID |
| `-y`, `--yes` | `false` | Skip confirmation prompt |

#### `hourgit config report`

Expand All @@ -399,9 +400,9 @@ hourgit config report [--project <name>] [--month <1-12>] [--year <YYYY>]

| Flag | Default | Description |
|------|---------|-------------|
| `--project` | auto-detect | Project name or ID |
| `--month` | current month | Month number 1-12 |
| `--year` | current year | Year |
| `-p`, `--project` | auto-detect | Project name or ID |
| `-m`, `--month` | current month | Month number 1-12 |
| `-y`, `--year` | current year | Year |

### Default Schedule

Expand Down Expand Up @@ -439,7 +440,7 @@ hourgit defaults reset [--yes]

| Flag | Default | Description |
|------|---------|-------------|
| `--yes` | `false` | Skip confirmation prompt |
| `-y`, `--yes` | `false` | Skip confirmation prompt |

#### `hourgit defaults report`

Expand All @@ -451,8 +452,8 @@ hourgit defaults report [--month <1-12>] [--year <YYYY>]

| Flag | Default | Description |
|------|---------|-------------|
| `--month` | current month | Month number 1-12 |
| `--year` | current year | Year |
| `-m`, `--month` | current month | Month number 1-12 |
| `-y`, `--year` | current year | Year |

### Shell Completions

Expand All @@ -470,7 +471,7 @@ hourgit completion install [SHELL] [--yes]

| Flag | Default | Description |
|------|---------|-------------|
| `--yes` | `false` | Skip confirmation prompt |
| `-y`, `--yes` | `false` | Skip confirmation prompt |

#### `hourgit completion generate`

Expand Down
13 changes: 9 additions & 4 deletions internal/cli/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import "github.com/spf13/cobra"

// BoolFlag defines a boolean flag for a command.
type BoolFlag struct {
Name string
Usage string
Default bool
Name string
Shorthand string
Usage string
Default bool
}

// StringFlag defines a string flag for a command.
Expand Down Expand Up @@ -37,7 +38,11 @@ func (lc LeafCommand) Build() *cobra.Command {
RunE: lc.RunE,
}
for _, f := range lc.BoolFlags {
cmd.Flags().Bool(f.Name, f.Default, f.Usage)
if f.Shorthand != "" {
cmd.Flags().BoolP(f.Name, f.Shorthand, f.Default, f.Usage)
} else {
cmd.Flags().Bool(f.Name, f.Default, f.Usage)
}
}
for _, f := range lc.StrFlags {
if f.Shorthand != "" {
Expand Down
33 changes: 31 additions & 2 deletions internal/cli/command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ func TestLeafCommandBuild(t *testing.T) {
Short: "A test command",
Args: cobra.ExactArgs(1),
BoolFlags: []BoolFlag{
{Name: "verbose", Usage: "enable verbose output", Default: false},
{Name: "verbose", Shorthand: "v", Usage: "enable verbose output", Default: false},
{Name: "dry-run", Usage: "simulate execution", Default: true},
},
StrFlags: []StringFlag{
{Name: "output", Usage: "output file", Default: "out.txt"},
{Name: "output", Shorthand: "o", Usage: "output file", Default: "out.txt"},
},
RunE: func(cmd *cobra.Command, args []string) error { return nil },
}.Build()
Expand All @@ -31,6 +31,7 @@ func TestLeafCommandBuild(t *testing.T) {
verbose := cmd.Flags().Lookup("verbose")
require.NotNil(t, verbose)
assert.Equal(t, "false", verbose.DefValue)
assert.Equal(t, "v", verbose.Shorthand)

dryRun := cmd.Flags().Lookup("dry-run")
require.NotNil(t, dryRun)
Expand All @@ -39,6 +40,34 @@ func TestLeafCommandBuild(t *testing.T) {
output := cmd.Flags().Lookup("output")
require.NotNil(t, output)
assert.Equal(t, "out.txt", output.DefValue)
assert.Equal(t, "o", output.Shorthand)
}

func TestLeafCommandShortFlags(t *testing.T) {
var gotVerbose bool
var gotOutput string

cmd := LeafCommand{
Use: "test",
Short: "A test command",
BoolFlags: []BoolFlag{
{Name: "verbose", Shorthand: "v", Usage: "enable verbose output"},
},
StrFlags: []StringFlag{
{Name: "output", Shorthand: "o", Usage: "output file"},
},
RunE: func(cmd *cobra.Command, args []string) error {
gotVerbose, _ = cmd.Flags().GetBool("verbose")
gotOutput, _ = cmd.Flags().GetString("output")
return nil
},
}.Build()

cmd.SetArgs([]string{"-v", "-o", "result.txt"})
require.NoError(t, cmd.Execute())

assert.True(t, gotVerbose)
assert.Equal(t, "result.txt", gotOutput)
}

func TestLeafCommandBuildNoFlags(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion internal/cli/completion_install_cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ var completionInstallCmd = LeafCommand{
Short: "Install shell completions into your shell config",
Args: cobra.RangeArgs(0, 1),
BoolFlags: []BoolFlag{
{Name: "yes", Usage: "Skip confirmation prompt"},
{Name: "yes", Shorthand: "y", Usage: "Skip confirmation prompt"},
},
RunE: func(cmd *cobra.Command, args []string) error {
shell := ""
Expand Down
2 changes: 1 addition & 1 deletion internal/cli/config_get.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ var configGetCmd = LeafCommand{
Use: "get",
Short: "Show the schedule configuration for a project",
StrFlags: []StringFlag{
{Name: "project", Usage: "project name or ID (auto-detected from repo if omitted)"},
{Name: "project", Shorthand: "p", Usage: "project name or ID (auto-detected from repo if omitted)"},
},
RunE: func(cmd *cobra.Command, args []string) error {
homeDir, repoDir, err := getContextPaths()
Expand Down
6 changes: 3 additions & 3 deletions internal/cli/config_report.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ var configReportCmd = LeafCommand{
Use: "report",
Short: "Show expanded working hours for a given month",
StrFlags: []StringFlag{
{Name: "project", Usage: "project name or ID (auto-detected from repo if omitted)"},
{Name: "month", Usage: "month number 1-12 (default: current)"},
{Name: "year", Usage: "year (default: current)"},
{Name: "project", Shorthand: "p", Usage: "project name or ID (auto-detected from repo if omitted)"},
{Name: "month", Shorthand: "m", Usage: "month number 1-12 (default: current)"},
{Name: "year", Shorthand: "y", Usage: "year (default: current)"},
},
RunE: func(cmd *cobra.Command, args []string) error {
homeDir, repoDir, err := getContextPaths()
Expand Down
4 changes: 2 additions & 2 deletions internal/cli/config_reset.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ var configResetCmd = LeafCommand{
Use: "reset",
Short: "Reset a project's schedule to the defaults",
StrFlags: []StringFlag{
{Name: "project", Usage: "project name or ID (auto-detected from repo if omitted)"},
{Name: "project", Shorthand: "p", Usage: "project name or ID (auto-detected from repo if omitted)"},
},
BoolFlags: []BoolFlag{
{Name: "yes", Usage: "skip confirmation prompt"},
{Name: "yes", Shorthand: "y", Usage: "skip confirmation prompt"},
},
RunE: func(cmd *cobra.Command, args []string) error {
homeDir, repoDir, err := getContextPaths()
Expand Down
2 changes: 1 addition & 1 deletion internal/cli/config_set.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ var configSetCmd = LeafCommand{
Use: "set",
Short: "Interactively edit a project's schedule",
StrFlags: []StringFlag{
{Name: "project", Usage: "project name or ID (auto-detected from repo if omitted)"},
{Name: "project", Shorthand: "p", Usage: "project name or ID (auto-detected from repo if omitted)"},
},
RunE: func(cmd *cobra.Command, args []string) error {
homeDir, repoDir, err := getContextPaths()
Expand Down
4 changes: 2 additions & 2 deletions internal/cli/defaults_report.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ var defaultsReportCmd = LeafCommand{
Use: "report",
Short: "Show expanded default working hours for a given month",
StrFlags: []StringFlag{
{Name: "month", Usage: "month number 1-12 (default: current)"},
{Name: "year", Usage: "year (default: current)"},
{Name: "month", Shorthand: "m", Usage: "month number 1-12 (default: current)"},
{Name: "year", Shorthand: "y", Usage: "year (default: current)"},
},
RunE: func(cmd *cobra.Command, args []string) error {
homeDir, err := os.UserHomeDir()
Expand Down
2 changes: 1 addition & 1 deletion internal/cli/defaults_reset.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ var defaultsResetCmd = LeafCommand{
Use: "reset",
Short: "Reset the default schedule to factory settings (Mon-Fri 9am-5pm)",
BoolFlags: []BoolFlag{
{Name: "yes", Usage: "skip confirmation prompt"},
{Name: "yes", Shorthand: "y", Usage: "skip confirmation prompt"},
},
RunE: func(cmd *cobra.Command, args []string) error {
homeDir, err := os.UserHomeDir()
Expand Down
14 changes: 7 additions & 7 deletions internal/cli/edit.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,15 @@ var editCmd = LeafCommand{
Short: "Edit an existing log entry",
Args: cobra.ExactArgs(1),
BoolFlags: []BoolFlag{
{Name: "yes", Usage: "skip confirmation prompts"},
{Name: "yes", Shorthand: "y", Usage: "skip confirmation prompts"},
},
StrFlags: []StringFlag{
{Name: "project", Usage: "project name or ID (auto-detected from repo if omitted)"},
{Name: "duration", Usage: "new duration (e.g. 30m, 3h, 3h30m)"},
{Name: "from", Usage: "new start time (e.g. 9am, 14:00)"},
{Name: "to", Usage: "new end time (e.g. 5pm, 17:00)"},
{Name: "date", Usage: "new date (YYYY-MM-DD)"},
{Name: "task", Usage: "new task label (empty string clears it)"},
{Name: "project", Shorthand: "p", Usage: "project name or ID (auto-detected from repo if omitted)"},
{Name: "duration", Shorthand: "d", Usage: "new duration (e.g. 30m, 3h, 3h30m)"},
{Name: "from", Shorthand: "F", Usage: "new start time (e.g. 9am, 14:00)"},
{Name: "to", Shorthand: "T", Usage: "new end time (e.g. 5pm, 17:00)"},
{Name: "date", Shorthand: "D", Usage: "new date (YYYY-MM-DD)"},
{Name: "task", Shorthand: "t", Usage: "new task label (empty string clears it)"},
{Name: "message", Shorthand: "m", Usage: "new message"},
},
RunE: func(cmd *cobra.Command, args []string) error {
Expand Down
4 changes: 2 additions & 2 deletions internal/cli/history.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ var historyCmd = LeafCommand{
Use: "history",
Short: "Show a chronological feed of all recorded activity",
StrFlags: []StringFlag{
{Name: "project", Usage: "filter by project name or ID"},
{Name: "limit", Usage: "maximum number of entries to show (0 = all)", Default: "50"},
{Name: "project", Shorthand: "p", Usage: "filter by project name or ID"},
{Name: "limit", Shorthand: "l", Usage: "maximum number of entries to show (0 = all)", Default: "50"},
},
RunE: func(cmd *cobra.Command, args []string) error {
homeDir, err := os.UserHomeDir()
Expand Down
Loading