ace-git-worktree 0.26.1 → 0.27.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.ace-defaults/nav/protocols/skill-sources/ace-git-worktree.yml +0 -0
- data/.ace-defaults/nav/protocols/wfi-sources/ace-git-worktree.yml +0 -0
- data/CHANGELOG.md +62 -0
- data/LICENSE +0 -0
- data/README.md +0 -0
- data/Rakefile +0 -0
- data/docs/demo/ace-git-worktree-getting-started.gif +0 -0
- data/docs/demo/ace-git-worktree-getting-started.tape.yml +0 -0
- data/docs/demo/fixtures/README.md +0 -0
- data/docs/demo/fixtures/sample.txt +0 -0
- data/docs/getting-started.md +7 -4
- data/docs/handbook.md +0 -0
- data/docs/usage.md +153 -24
- data/handbook/agents/worktree.ag.md +10 -6
- data/handbook/skills/as-git-worktree/SKILL.md +0 -0
- data/handbook/skills/as-git-worktree-cleanup/SKILL.md +0 -0
- data/handbook/skills/as-git-worktree-create/SKILL.md +0 -0
- data/handbook/skills/as-git-worktree-manage/SKILL.md +0 -0
- data/handbook/workflow-instructions/git/worktree-create.wf.md +3 -3
- data/handbook/workflow-instructions/git/worktree-manage.wf.md +6 -6
- data/handbook/workflow-instructions/git/worktree.wf.md +2 -2
- data/lib/ace/git/worktree/atoms/duration_parser.rb +81 -0
- data/lib/ace/git/worktree/atoms/git_command.rb +0 -0
- data/lib/ace/git/worktree/atoms/path_expander.rb +0 -0
- data/lib/ace/git/worktree/atoms/relative_age_formatter.rb +59 -0
- data/lib/ace/git/worktree/atoms/slug_generator.rb +0 -0
- data/lib/ace/git/worktree/atoms/task_id_extractor.rb +20 -1
- data/lib/ace/git/worktree/atoms/text_truncator.rb +67 -0
- data/lib/ace/git/worktree/cli/commands/bootstrap.rb +0 -0
- data/lib/ace/git/worktree/cli/commands/cleanup.rb +0 -0
- data/lib/ace/git/worktree/cli/commands/config.rb +0 -0
- data/lib/ace/git/worktree/cli/commands/create.rb +0 -0
- data/lib/ace/git/worktree/cli/commands/list.rb +29 -8
- data/lib/ace/git/worktree/cli/commands/prune.rb +0 -0
- data/lib/ace/git/worktree/cli/commands/remove.rb +0 -0
- data/lib/ace/git/worktree/cli/commands/shared_helpers.rb +0 -0
- data/lib/ace/git/worktree/cli/commands/switch.rb +0 -0
- data/lib/ace/git/worktree/cli.rb +1 -1
- data/lib/ace/git/worktree/commands/bootstrap_command.rb +0 -0
- data/lib/ace/git/worktree/commands/list_command.rb +90 -94
- data/lib/ace/git/worktree/commands/prune_command.rb +0 -0
- data/lib/ace/git/worktree/commands/remove_command.rb +0 -0
- data/lib/ace/git/worktree/commands/switch_command.rb +0 -0
- data/lib/ace/git/worktree/configuration.rb +0 -0
- data/lib/ace/git/worktree/models/task_index.rb +186 -0
- data/lib/ace/git/worktree/models/worktree_info.rb +52 -3
- data/lib/ace/git/worktree/models/worktree_metadata.rb +0 -0
- data/lib/ace/git/worktree/molecules/bootstrap_executor.rb +0 -0
- data/lib/ace/git/worktree/molecules/cleanup_applier.rb +0 -0
- data/lib/ace/git/worktree/molecules/config_loader.rb +0 -0
- data/lib/ace/git/worktree/molecules/current_task_linker.rb +0 -0
- data/lib/ace/git/worktree/molecules/hook_executor.rb +0 -0
- data/lib/ace/git/worktree/molecules/parent_task_resolver.rb +0 -0
- data/lib/ace/git/worktree/molecules/pr_status_resolver.rb +172 -0
- data/lib/ace/git/worktree/molecules/recent_branch_counter.rb +70 -0
- data/lib/ace/git/worktree/molecules/task_association_resolver.rb +121 -0
- data/lib/ace/git/worktree/molecules/task_fetcher.rb +0 -0
- data/lib/ace/git/worktree/molecules/task_index_builder.rb +121 -0
- data/lib/ace/git/worktree/molecules/task_pusher.rb +0 -0
- data/lib/ace/git/worktree/molecules/task_status_updater.rb +0 -0
- data/lib/ace/git/worktree/molecules/toolchain_truster.rb +0 -0
- data/lib/ace/git/worktree/molecules/worktree_footer_formatter.rb +43 -0
- data/lib/ace/git/worktree/molecules/worktree_lister.rb +22 -93
- data/lib/ace/git/worktree/molecules/worktree_probe.rb +196 -0
- data/lib/ace/git/worktree/molecules/worktree_remover.rb +0 -0
- data/lib/ace/git/worktree/molecules/worktree_table_formatter.rb +241 -0
- data/lib/ace/git/worktree/organisms/worktree_list_report.rb +315 -0
- data/lib/ace/git/worktree/organisms/worktree_manager.rb +22 -20
- data/lib/ace/git/worktree/organisms/worktree_probe_runner.rb +320 -0
- data/lib/ace/git/worktree/version.rb +1 -1
- data/lib/ace/git/worktree.rb +14 -0
- metadata +15 -2
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ace
|
|
4
|
+
module Git
|
|
5
|
+
module Worktree
|
|
6
|
+
module Atoms
|
|
7
|
+
# Formats a commit age as one compact unit for the `Age` table column.
|
|
8
|
+
#
|
|
9
|
+
# Ladder (floor of the current unit, no `ago` suffix):
|
|
10
|
+
# 0-59m -> "Nm" 1-23h -> "Nh"
|
|
11
|
+
# 1-13d -> "Nd" 14-59d -> "Nw"
|
|
12
|
+
# 60-364d -> "Nmo" >=365d -> "Ny"
|
|
13
|
+
#
|
|
14
|
+
# `nil` means the branch has no commits ("never"); clock skew/future
|
|
15
|
+
# timestamps render "0m" so the row still reads as active.
|
|
16
|
+
#
|
|
17
|
+
# @example
|
|
18
|
+
# RelativeAgeFormatter.format(9 * 24 * 3600) # => "9d"
|
|
19
|
+
# RelativeAgeFormatter.format(nil) # => "never"
|
|
20
|
+
module RelativeAgeFormatter
|
|
21
|
+
MINUTE = 60
|
|
22
|
+
HOUR = 60 * MINUTE
|
|
23
|
+
DAY = 24 * HOUR
|
|
24
|
+
WEEK = 7 * DAY
|
|
25
|
+
|
|
26
|
+
# Unit thresholds in seconds, largest unit last.
|
|
27
|
+
# The day->week switch is at 14 days and week->month at 60 days so
|
|
28
|
+
# short-lived worktrees keep a day count (e.g. "9d") while older
|
|
29
|
+
# ones step up to weeks ("3w") and then months ("2mo").
|
|
30
|
+
WEEK_THRESHOLD = 14 * DAY
|
|
31
|
+
MONTH_THRESHOLD = 60 * DAY
|
|
32
|
+
MONTH = 30 * DAY
|
|
33
|
+
YEAR_THRESHOLD = 365 * DAY
|
|
34
|
+
|
|
35
|
+
class << self
|
|
36
|
+
# @param age_seconds [Numeric, nil] age in seconds (nil = no commits)
|
|
37
|
+
# @return [String] compact age label
|
|
38
|
+
def format(age_seconds)
|
|
39
|
+
return "never" if age_seconds.nil?
|
|
40
|
+
|
|
41
|
+
seconds = age_seconds.to_f
|
|
42
|
+
return "0m" if seconds.negative?
|
|
43
|
+
|
|
44
|
+
case seconds
|
|
45
|
+
when 0...MINUTE then "0m"
|
|
46
|
+
when MINUTE...HOUR then "#{(seconds / MINUTE).floor}m"
|
|
47
|
+
when HOUR...DAY then "#{(seconds / HOUR).floor}h"
|
|
48
|
+
when DAY...WEEK_THRESHOLD then "#{(seconds / DAY).floor}d"
|
|
49
|
+
when WEEK_THRESHOLD...MONTH_THRESHOLD then "#{(seconds / WEEK).floor}w"
|
|
50
|
+
when MONTH_THRESHOLD...YEAR_THRESHOLD then "#{(seconds / MONTH).floor}mo"
|
|
51
|
+
else "#{(seconds / YEAR_THRESHOLD).floor}y"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
File without changes
|
|
@@ -53,6 +53,22 @@ module Ace
|
|
|
53
53
|
task_data[:task_number]&.to_s || "unknown"
|
|
54
54
|
end
|
|
55
55
|
|
|
56
|
+
# Derive the short form of a full Base36 task id.
|
|
57
|
+
#
|
|
58
|
+
# The short form is the three-character suffix plus an optional
|
|
59
|
+
# subtask segment (numeric or single character).
|
|
60
|
+
#
|
|
61
|
+
# @param full_id [String, nil] formatted task id (e.g. "8xg.t.k9y",
|
|
62
|
+
# "8wr.t.qk0.3")
|
|
63
|
+
# @return [String, nil] short id (e.g. "k9y", "qk0.3") or nil when the
|
|
64
|
+
# argument is not a full task id
|
|
65
|
+
def self.short_id(full_id)
|
|
66
|
+
match = full_id.to_s.strip.match(/\A[0-9a-z]{3}\.[a-z]\.([0-9a-z]{3})(?:\.([0-9a-z]+))?\z/)
|
|
67
|
+
return nil unless match
|
|
68
|
+
|
|
69
|
+
match[2] ? "#{match[1]}.#{match[2]}" : match[1]
|
|
70
|
+
end
|
|
71
|
+
|
|
56
72
|
# Normalize a task reference string to simple ID format
|
|
57
73
|
#
|
|
58
74
|
# @param task_ref [String] Task reference (e.g., "121.01", "v.0.9.0+task.121.01")
|
|
@@ -63,7 +79,7 @@ module Ace
|
|
|
63
79
|
|
|
64
80
|
# Regex patterns for recognized ACE task ID formats
|
|
65
81
|
# B36TS full ID: "8pp.t.hy4" -> "hy4", or subtask "8pp.t.hy4.a" -> "hy4.a"
|
|
66
|
-
if match = ref.match(/\A[0-9a-z]{3}\.[a-z]\.([0-9a-z]{3}(?:\.[
|
|
82
|
+
if match = ref.match(/\A[0-9a-z]{3}\.[a-z]\.([0-9a-z]{3}(?:\.[0-9a-z]+)?)\z/)
|
|
67
83
|
match[1]
|
|
68
84
|
# B36TS short ref: exactly 3 lowercase alphanumeric chars (e.g., "hy4")
|
|
69
85
|
elsif match = ref.match(/\A([0-9a-z]{3})\z/)
|
|
@@ -72,6 +88,9 @@ module Ace
|
|
|
72
88
|
# Try hierarchical pattern first (e.g., "121.01", "task.121.01")
|
|
73
89
|
elsif match = ref.match(/(\d{3})\.(\d{2})(?:\b|$)/)
|
|
74
90
|
"#{match[1]}.#{match[2]}"
|
|
91
|
+
# B36TS short ref with subtask segment (e.g., "qk0.3", "hy4.a")
|
|
92
|
+
elsif match = ref.match(/\A([0-9a-z]{3})\.([0-9a-z]+)\z/)
|
|
93
|
+
"#{match[1]}.#{match[2]}"
|
|
75
94
|
# Try ace-task/ace-t or task/t directory at end of path (e.g., "ace-task.hy4", "/path/to/task.121")
|
|
76
95
|
elsif match = ref.match(/(?:ace-)?(?:task|t)\.([0-9a-z]{3})\/?$/)
|
|
77
96
|
match[1]
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ace
|
|
4
|
+
module Git
|
|
5
|
+
module Worktree
|
|
6
|
+
module Atoms
|
|
7
|
+
# Truncates single-column text using the single-character ellipsis `…`.
|
|
8
|
+
#
|
|
9
|
+
# Each helper returns a string no wider than `width` columns (the
|
|
10
|
+
# ellipsis counts as one column). Text that already fits is returned
|
|
11
|
+
# unchanged. Widths below 1 render an empty string; width 1 renders
|
|
12
|
+
# only the ellipsis.
|
|
13
|
+
#
|
|
14
|
+
# @example Branches keep both ends
|
|
15
|
+
# TextTruncator.middle("codex/ei9-campaign-composition-join", 25)
|
|
16
|
+
# # => "codex/ei9-ca…osition-join"
|
|
17
|
+
module TextTruncator
|
|
18
|
+
ELLIPSIS = "…"
|
|
19
|
+
|
|
20
|
+
class << self
|
|
21
|
+
# Keep the prefix and suffix; elide the middle.
|
|
22
|
+
# @param text [String, nil]
|
|
23
|
+
# @param width [Integer] target column width
|
|
24
|
+
# @return [String]
|
|
25
|
+
def middle(text, width)
|
|
26
|
+
value = text.to_s
|
|
27
|
+
return "" if width <= 0
|
|
28
|
+
return value if value.length <= width
|
|
29
|
+
return ELLIPSIS if width == 1
|
|
30
|
+
|
|
31
|
+
remaining = width - 1
|
|
32
|
+
prefix_length = (remaining + 1) / 2
|
|
33
|
+
suffix_length = remaining - prefix_length
|
|
34
|
+
"#{value[0, prefix_length]}#{ELLIPSIS}#{value[-suffix_length, suffix_length]}"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Keep the prefix; truncate the tail.
|
|
38
|
+
# @param text [String, nil]
|
|
39
|
+
# @param width [Integer] target column width
|
|
40
|
+
# @return [String]
|
|
41
|
+
def right(text, width)
|
|
42
|
+
value = text.to_s
|
|
43
|
+
return "" if width <= 0
|
|
44
|
+
return value if value.length <= width
|
|
45
|
+
return ELLIPSIS if width == 1
|
|
46
|
+
|
|
47
|
+
"#{value[0, width - 1]}#{ELLIPSIS}"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Keep the suffix; elide the head.
|
|
51
|
+
# @param text [String, nil]
|
|
52
|
+
# @param width [Integer] target column width
|
|
53
|
+
# @return [String]
|
|
54
|
+
def left(text, width)
|
|
55
|
+
value = text.to_s
|
|
56
|
+
return "" if width <= 0
|
|
57
|
+
return value if value.length <= width
|
|
58
|
+
return ELLIPSIS if width == 1
|
|
59
|
+
|
|
60
|
+
"#{ELLIPSIS}#{value[-(width - 1), width - 1]}"
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -12,28 +12,42 @@ module Ace
|
|
|
12
12
|
class List < Ace::Support::Cli::Command
|
|
13
13
|
include SharedHelpers
|
|
14
14
|
|
|
15
|
-
desc
|
|
15
|
+
desc <<~DESC.chomp
|
|
16
|
+
List active worktrees with task, activity, dirty-state, and PR context
|
|
17
|
+
Columns: Task, Branch, Age, Commit, Changes, PR, Path. A worktree is
|
|
18
|
+
active when its last commit is inside --active-within (default 18h) or
|
|
19
|
+
it has uncommitted changes. Task ids are inferred from recorded task
|
|
20
|
+
metadata, the _current symlink, validated path/branch names, or
|
|
21
|
+
.ace-tasks files changed in the worktree; PRs match by branch, then
|
|
22
|
+
HEAD commit sha, then a unique task reference. Columns shrink to the
|
|
23
|
+
terminal width and Path drops on very narrow terminals. Probing runs
|
|
24
|
+
in isolated child processes with a 5 second per-worktree budget;
|
|
25
|
+
unreadable probes render "?".
|
|
26
|
+
DESC
|
|
16
27
|
|
|
17
28
|
example [
|
|
18
|
-
" #
|
|
19
|
-
"--
|
|
20
|
-
"--
|
|
29
|
+
" # Worktrees active in the last 18h",
|
|
30
|
+
"--full # Every worktree with absolute paths",
|
|
31
|
+
"--active-within 3d # Wider activity window",
|
|
32
|
+
"--format json # Machine-readable output",
|
|
21
33
|
"--search auth # Filter by branch pattern"
|
|
22
34
|
]
|
|
23
35
|
|
|
24
36
|
option :format, desc: "Output format: table, json, simple", aliases: [], default: "table"
|
|
25
|
-
option :
|
|
37
|
+
option :compact, desc: "Active worktrees only (default)", type: :boolean
|
|
38
|
+
option :full, desc: "List every worktree with absolute paths", type: :boolean
|
|
39
|
+
option :active_within, desc: "Activity window (Nm, Nh, Nd, or bare hours; default 18h)", aliases: []
|
|
26
40
|
option :task_associated, desc: "Show only task-associated worktrees", type: :boolean
|
|
27
41
|
option :usable, desc: "Show only usable worktrees", type: :boolean
|
|
28
42
|
option :search, desc: "Filter by branch name pattern", aliases: []
|
|
29
|
-
option :quiet, type: :boolean, aliases: ["-q"], desc: "Suppress
|
|
43
|
+
option :quiet, type: :boolean, aliases: ["-q"], desc: "Suppress the count footer"
|
|
30
44
|
option :verbose, type: :boolean, aliases: ["-v"], desc: "Show verbose output"
|
|
31
45
|
option :debug, type: :boolean, aliases: ["-d"], desc: "Show debug output"
|
|
32
46
|
|
|
33
47
|
def call(**options)
|
|
34
48
|
display_config_summary("list", options) unless options[:format] == "json"
|
|
35
49
|
|
|
36
|
-
# Keep explicit false values as --no-* flags so
|
|
50
|
+
# Keep explicit false values as --no-* flags so the parser receives filters.
|
|
37
51
|
args = list_options_to_args(options)
|
|
38
52
|
|
|
39
53
|
Ace::Git::Worktree::Commands::ListCommand.new.run(args)
|
|
@@ -44,7 +58,14 @@ module Ace
|
|
|
44
58
|
def list_options_to_args(options)
|
|
45
59
|
args = []
|
|
46
60
|
args.concat(format_arg(options))
|
|
47
|
-
args << "--
|
|
61
|
+
args << "--compact" if options[:compact]
|
|
62
|
+
args << "--full" if options[:full]
|
|
63
|
+
args << "--quiet" if options[:quiet]
|
|
64
|
+
|
|
65
|
+
if options[:active_within].is_a?(String) && !options[:active_within].empty?
|
|
66
|
+
args << "--active-within"
|
|
67
|
+
args << options[:active_within]
|
|
68
|
+
end
|
|
48
69
|
|
|
49
70
|
unless options[:task_associated].nil?
|
|
50
71
|
args << (options[:task_associated] ? "--task-associated" : "--no-task-associated")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
data/lib/ace/git/worktree/cli.rb
CHANGED
|
@@ -38,7 +38,7 @@ module Ace
|
|
|
38
38
|
|
|
39
39
|
HELP_EXAMPLES = [
|
|
40
40
|
"ace-git-worktree create --task 148 # Isolated worktree for task",
|
|
41
|
-
"ace-git-worktree list --
|
|
41
|
+
"ace-git-worktree list --full # Every worktree with task context",
|
|
42
42
|
"ace-git-worktree switch 148 # Get path for cd",
|
|
43
43
|
"ace-git-worktree prune --dry-run # Preview stale cleanup"
|
|
44
44
|
].freeze
|
|
File without changes
|
|
@@ -6,21 +6,29 @@ module Ace
|
|
|
6
6
|
module Commands
|
|
7
7
|
# List command
|
|
8
8
|
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
9
|
+
# Prints the active-worktree table: one enriched row per worktree whose
|
|
10
|
+
# last activity falls inside the activity window (committed or dirty),
|
|
11
|
+
# plus a two-line count footer. `--full` lists every worktree,
|
|
12
|
+
# `--format json` is the machine surface, `--format simple` is the
|
|
13
|
+
# branch-at-path listing.
|
|
11
14
|
#
|
|
12
|
-
# @example
|
|
15
|
+
# @example Daily driver (last 18h)
|
|
13
16
|
# ListCommand.new.run([])
|
|
14
17
|
#
|
|
15
|
-
# @example
|
|
16
|
-
# ListCommand.new.run(["--format", "json"])
|
|
18
|
+
# @example Wider window in JSON
|
|
19
|
+
# ListCommand.new.run(["--active-within", "3d", "--format", "json"])
|
|
17
20
|
#
|
|
18
|
-
# @example
|
|
19
|
-
# ListCommand.new.run(["--
|
|
21
|
+
# @example Exhaustive listing
|
|
22
|
+
# ListCommand.new.run(["--full"])
|
|
20
23
|
class ListCommand
|
|
24
|
+
# Supported output formats
|
|
25
|
+
FORMATS = %w[table json simple].freeze
|
|
26
|
+
|
|
21
27
|
# Initialize a new ListCommand
|
|
22
|
-
|
|
23
|
-
|
|
28
|
+
#
|
|
29
|
+
# @param reporter [Organisms::WorktreeListReport, nil] injectable report
|
|
30
|
+
def initialize(reporter: nil)
|
|
31
|
+
@reporter = reporter
|
|
24
32
|
end
|
|
25
33
|
|
|
26
34
|
# Run the list command
|
|
@@ -33,13 +41,10 @@ module Ace
|
|
|
33
41
|
|
|
34
42
|
validate_options(options)
|
|
35
43
|
|
|
36
|
-
|
|
37
|
-
options[:format] = options[:format].to_sym if options[:format]
|
|
38
|
-
|
|
39
|
-
result = @manager.list_all(options)
|
|
44
|
+
result = reporter.run(options)
|
|
40
45
|
|
|
41
46
|
if result[:success]
|
|
42
|
-
|
|
47
|
+
puts result[:output] unless result[:output].to_s.empty?
|
|
43
48
|
0
|
|
44
49
|
else
|
|
45
50
|
puts "Failed to list worktrees: #{result[:error]}"
|
|
@@ -47,8 +52,6 @@ module Ace
|
|
|
47
52
|
end
|
|
48
53
|
rescue ArgumentError => e
|
|
49
54
|
puts "Error: #{e.message}"
|
|
50
|
-
puts
|
|
51
|
-
show_help
|
|
52
55
|
1
|
|
53
56
|
rescue => e
|
|
54
57
|
puts "Error: #{e.message}"
|
|
@@ -63,49 +66,61 @@ module Ace
|
|
|
63
66
|
ace-git-worktree list - List worktrees
|
|
64
67
|
|
|
65
68
|
USAGE:
|
|
66
|
-
ace-git-worktree list [OPTIONS]
|
|
69
|
+
ace-git-worktree list [--compact | --full] [--active-within DURATION] [OPTIONS]
|
|
70
|
+
|
|
71
|
+
VIEW:
|
|
72
|
+
--compact Active worktrees only (default)
|
|
73
|
+
--full List every worktree with absolute paths
|
|
74
|
+
--active-within <dur> Activity window: Nm, Nh, Nd, or bare hours (default: 18h)
|
|
67
75
|
|
|
68
|
-
OUTPUT
|
|
76
|
+
OUTPUT:
|
|
69
77
|
--format <format> Output format: table, json, simple (default: table)
|
|
70
|
-
|
|
78
|
+
-q, --quiet Suppress the count footer
|
|
71
79
|
|
|
72
80
|
FILTERING:
|
|
73
|
-
--task-associated Show only task
|
|
74
|
-
--no-task-associated Show only
|
|
75
|
-
--usable
|
|
76
|
-
--no-usable
|
|
81
|
+
--task-associated Show only worktrees with a resolved task id
|
|
82
|
+
--no-task-associated Show only worktrees without a task id
|
|
83
|
+
--usable Show only usable worktrees
|
|
84
|
+
--no-usable Show only unusable worktrees
|
|
77
85
|
--search <pattern> Filter by branch name pattern
|
|
78
86
|
|
|
79
87
|
EXAMPLES:
|
|
80
|
-
#
|
|
88
|
+
# Worktrees active in the last 18 hours
|
|
81
89
|
ace-git-worktree list
|
|
82
90
|
|
|
83
|
-
#
|
|
84
|
-
ace-git-worktree list --
|
|
85
|
-
|
|
86
|
-
#
|
|
87
|
-
ace-git-worktree list --
|
|
88
|
-
|
|
89
|
-
#
|
|
90
|
-
ace-git-worktree list --
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
91
|
+
# Everything, including inactive worktrees
|
|
92
|
+
ace-git-worktree list --full
|
|
93
|
+
|
|
94
|
+
# After a weekend
|
|
95
|
+
ace-git-worktree list --active-within 3d
|
|
96
|
+
|
|
97
|
+
# Machine-readable output
|
|
98
|
+
ace-git-worktree list --format json
|
|
99
|
+
|
|
100
|
+
COLUMNS:
|
|
101
|
+
Task Resolved task id (inferred from metadata, the _current symlink,
|
|
102
|
+
validated names, or task files changed in the worktree)
|
|
103
|
+
Branch Checked-out branch (middle-elided when truncated)
|
|
104
|
+
Age Age of the last commit, one compact unit (8m, 3h, 9d, 3w, 2mo, 1y)
|
|
105
|
+
Commit Last commit subject (right-truncated)
|
|
106
|
+
Changes Uncommitted file count (staged + unstaged + untracked)
|
|
107
|
+
PR #N, #N draft, #N merged, #N closed, or - (matched by branch,
|
|
108
|
+
HEAD commit, or a unique task reference)
|
|
109
|
+
Path Worktree path (~ shortened; --full prints it absolute)
|
|
110
|
+
|
|
111
|
+
Columns shrink to fit the terminal width; Path is dropped on very
|
|
112
|
+
narrow terminals. Probing runs in isolated child processes with a
|
|
113
|
+
5 second per-worktree budget; unreadable probes render "?".
|
|
103
114
|
HELP
|
|
104
115
|
0
|
|
105
116
|
end
|
|
106
117
|
|
|
107
118
|
private
|
|
108
119
|
|
|
120
|
+
def reporter
|
|
121
|
+
@reporter || Organisms::WorktreeListReport.new(env: ENV)
|
|
122
|
+
end
|
|
123
|
+
|
|
109
124
|
# Parse command line arguments
|
|
110
125
|
#
|
|
111
126
|
# @param args [Array<String>] Command arguments
|
|
@@ -113,10 +128,15 @@ module Ace
|
|
|
113
128
|
def parse_arguments(args)
|
|
114
129
|
options = {
|
|
115
130
|
format: "table",
|
|
116
|
-
|
|
131
|
+
compact: false,
|
|
132
|
+
full: false,
|
|
133
|
+
active_within: nil,
|
|
117
134
|
task_associated: nil,
|
|
118
135
|
usable: nil,
|
|
119
136
|
search: nil,
|
|
137
|
+
quiet: false,
|
|
138
|
+
verbose: false,
|
|
139
|
+
debug: false,
|
|
120
140
|
help: false
|
|
121
141
|
}
|
|
122
142
|
|
|
@@ -128,13 +148,17 @@ module Ace
|
|
|
128
148
|
when "--format"
|
|
129
149
|
i += 1
|
|
130
150
|
format = args[i]&.downcase
|
|
131
|
-
|
|
132
|
-
options[:format] = format
|
|
133
|
-
else
|
|
151
|
+
unless FORMATS.include?(format)
|
|
134
152
|
raise ArgumentError, "Invalid format: #{format}. Use: table, json, simple"
|
|
135
153
|
end
|
|
136
|
-
|
|
137
|
-
|
|
154
|
+
options[:format] = format
|
|
155
|
+
when "--compact"
|
|
156
|
+
options[:compact] = true
|
|
157
|
+
when "--full"
|
|
158
|
+
options[:full] = true
|
|
159
|
+
when "--active-within"
|
|
160
|
+
i += 1
|
|
161
|
+
options[:active_within] = args[i]
|
|
138
162
|
when "--task-associated"
|
|
139
163
|
options[:task_associated] = true
|
|
140
164
|
when "--no-task-associated"
|
|
@@ -146,6 +170,12 @@ module Ace
|
|
|
146
170
|
when "--search"
|
|
147
171
|
i += 1
|
|
148
172
|
options[:search] = args[i]
|
|
173
|
+
when "--quiet", "-q"
|
|
174
|
+
options[:quiet] = true
|
|
175
|
+
when "--verbose", "-v"
|
|
176
|
+
options[:verbose] = true
|
|
177
|
+
when "--debug", "-d"
|
|
178
|
+
options[:debug] = true
|
|
149
179
|
when "--help", "-h"
|
|
150
180
|
options[:help] = true
|
|
151
181
|
when /^--/
|
|
@@ -164,14 +194,23 @@ module Ace
|
|
|
164
194
|
#
|
|
165
195
|
# @param options [Hash] Parsed options
|
|
166
196
|
def validate_options(options)
|
|
197
|
+
if options[:compact] && options[:full]
|
|
198
|
+
raise ArgumentError, "--compact and --full are mutually exclusive"
|
|
199
|
+
end
|
|
200
|
+
|
|
167
201
|
if options[:search] && options[:search].empty?
|
|
168
202
|
raise ArgumentError, "Search pattern cannot be empty"
|
|
169
203
|
end
|
|
170
204
|
|
|
171
|
-
if options[:format] &&
|
|
205
|
+
if options[:format] && !FORMATS.include?(options[:format])
|
|
172
206
|
raise ArgumentError, "Invalid format: #{options[:format]}"
|
|
173
207
|
end
|
|
174
208
|
|
|
209
|
+
# Parse eagerly so invalid windows fail before any output.
|
|
210
|
+
if options[:active_within]
|
|
211
|
+
Atoms::DurationParser.parse(options[:active_within])
|
|
212
|
+
end
|
|
213
|
+
|
|
175
214
|
# Security validation for search patterns
|
|
176
215
|
if options[:search] && contains_dangerous_patterns?(options[:search])
|
|
177
216
|
raise ArgumentError, "Search pattern contains potentially dangerous characters"
|
|
@@ -197,49 +236,6 @@ module Ace
|
|
|
197
236
|
|
|
198
237
|
dangerous_patterns.any? { |pattern| value.match?(pattern) }
|
|
199
238
|
end
|
|
200
|
-
|
|
201
|
-
# Display list result
|
|
202
|
-
#
|
|
203
|
-
# @param result [Hash] List result
|
|
204
|
-
# @param options [Hash] Command options
|
|
205
|
-
def display_list_result(result, options)
|
|
206
|
-
if result[:worktrees].empty?
|
|
207
|
-
puts "No worktrees found."
|
|
208
|
-
return
|
|
209
|
-
end
|
|
210
|
-
|
|
211
|
-
# Display the formatted output
|
|
212
|
-
puts result[:formatted_output]
|
|
213
|
-
|
|
214
|
-
# Display summary if requested or if not JSON format
|
|
215
|
-
if options[:format] != :json
|
|
216
|
-
display_summary(result, options)
|
|
217
|
-
end
|
|
218
|
-
end
|
|
219
|
-
|
|
220
|
-
# Display summary information
|
|
221
|
-
#
|
|
222
|
-
# @param result [Hash] List result
|
|
223
|
-
# @param options [Hash] Command options
|
|
224
|
-
def display_summary(result, options)
|
|
225
|
-
stats = result[:statistics]
|
|
226
|
-
puts "\nSummary:"
|
|
227
|
-
puts " Total worktrees: #{stats[:total]}"
|
|
228
|
-
puts " Task-associated: #{stats[:task_associated]}"
|
|
229
|
-
puts " Usable: #{stats[:usable]}"
|
|
230
|
-
|
|
231
|
-
if options[:show_tasks] && stats[:task_ids].any?
|
|
232
|
-
puts " Tasks with worktrees: #{stats[:task_ids].join(", ")}"
|
|
233
|
-
end
|
|
234
|
-
|
|
235
|
-
if stats[:branches].any?
|
|
236
|
-
puts " Branches: #{stats[:branches].join(", ")}"
|
|
237
|
-
end
|
|
238
|
-
|
|
239
|
-
# Show active worktrees count
|
|
240
|
-
active_count = result[:worktrees].count { |wt| wt.exists? && wt.usable? }
|
|
241
|
-
puts " Active worktrees: #{active_count}"
|
|
242
|
-
end
|
|
243
239
|
end
|
|
244
240
|
end
|
|
245
241
|
end
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|