ace-git-worktree 0.27.0 → 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/CHANGELOG.md +21 -0
- data/docs/usage.md +34 -22
- data/lib/ace/git/worktree/models/task_index.rb +18 -3
- data/lib/ace/git/worktree/molecules/worktree_probe.rb +22 -2
- data/lib/ace/git/worktree/molecules/worktree_table_formatter.rb +61 -56
- data/lib/ace/git/worktree/organisms/worktree_list_report.rb +39 -1
- data/lib/ace/git/worktree/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3fbcf3673de821e4ed42fd7415a7cc0fa88fca4176d2d3cfbf7cff65e6d50332
|
|
4
|
+
data.tar.gz: ec88eedd7b9f6d3822835b70777e235a5581cacf3a413a98ee4a4bdd66eb5ae1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8e3038cf6f5465be9074866bd6c174da7c79839fa94eb0e6abc902ea1f7300dc4fb511e6132a55841e92de594b0b9c89f9a8401de08020a37f7ae0baec512bf0
|
|
7
|
+
data.tar.gz: 73ee36beff2acc5bcda6d456db1715b95b3dc580193ce07acaab7e9ec89a591674756a456036d179b2ee30b4f054bd5a2d007e99c24b7f54e2b9f6423fac503a
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.27.1] - 2026-10-18
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- `list` table rows now print the full, never-truncated worktree path on its
|
|
15
|
+
own line under the row (indented to the `Commit` column) followed by a blank
|
|
16
|
+
separator line, replacing the truncated `Path` column. Every displayed path
|
|
17
|
+
is directly usable (`cd`, copy/paste); `--full` prints absolute paths.
|
|
18
|
+
- `Changes` counts staged + unstaged + untracked files under `.ace-tasks/`;
|
|
19
|
+
untracked files outside the task tree are no longer scanned or counted.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- Large-repository listing performance: the deep untracked walk
|
|
24
|
+
(`--untracked-files=all`) dominated runtime and degraded further under
|
|
25
|
+
parallel probing (88 worktrees: ~18s). Probes now read tracked-only status
|
|
26
|
+
and run the deep `.ace-tasks/` untracked scan as a selective second round
|
|
27
|
+
for worktrees the cheap association tiers cannot resolve (~7s).
|
|
28
|
+
- Task index lookups no longer re-`realpath` every task directory on every
|
|
29
|
+
lookup; canonical paths are resolved once per index.
|
|
30
|
+
|
|
10
31
|
## [0.27.0] - 2026-10-17
|
|
11
32
|
|
|
12
33
|
### Added
|
data/docs/usage.md
CHANGED
|
@@ -292,21 +292,29 @@ association is always inferred.
|
|
|
292
292
|
|
|
293
293
|
```text
|
|
294
294
|
Active worktrees (last 18h) -- 3 of 88:
|
|
295
|
-
Task Branch Age Commit Changes PR
|
|
296
|
-
|
|
297
|
-
k9y codex/r3-bounded-convergence 17h feat(review): define bo… 2 #142 draft
|
|
298
|
-
|
|
295
|
+
Task Branch Age Commit Changes PR
|
|
296
|
+
---------------------------------------------------------------------------------------
|
|
297
|
+
k9y codex/r3-bounded-convergence 17h feat(review): define bo… 2 #142 draft
|
|
298
|
+
~/Ps/ace/.ace-wt/codex-r3-bounded-convergence
|
|
299
|
+
|
|
300
|
+
- codex/ei9-campaign-join 8m feat(ace-assign): compl… - -
|
|
301
|
+
~/Ps/ace/.ace-wt/ei9-campaign-composition-join
|
|
299
302
|
|
|
300
303
|
Inactive: 85 (use --full to list all)
|
|
301
304
|
Task-associated: 14 Usable: 71 Recent branches without worktrees: 4
|
|
302
305
|
```
|
|
303
306
|
|
|
307
|
+
Each worktree renders as three lines: the aligned columns, the
|
|
308
|
+
never-truncated path (indented to the `Commit` column, `$HOME` shortened to
|
|
309
|
+
`~` in compact mode, absolute with `--full`), and a blank separator line, so
|
|
310
|
+
every displayed path is directly usable (`cd`, copy/paste).
|
|
311
|
+
|
|
304
312
|
**Activity:** a worktree is active when the last commit reachable from its
|
|
305
|
-
checked-out HEAD is inside the window **or** it has uncommitted changes
|
|
306
|
-
unstaged, or untracked). Bare worktrees are never
|
|
307
|
-
the active/inactive split, so the header and
|
|
308
|
-
filtered set. `Recent branches without
|
|
309
|
-
affected by worktree filters.
|
|
313
|
+
checked-out HEAD is inside the window **or** it has uncommitted changes
|
|
314
|
+
(staged, unstaged, or untracked under `.ace-tasks/`). Bare worktrees are never
|
|
315
|
+
active. Filters apply before the active/inactive split, so the header and
|
|
316
|
+
footer counts always describe the filtered set. `Recent branches without
|
|
317
|
+
worktrees` is branch-based and is not affected by worktree filters.
|
|
310
318
|
|
|
311
319
|
**Columns:**
|
|
312
320
|
|
|
@@ -316,11 +324,11 @@ affected by worktree filters.
|
|
|
316
324
|
* `Age` - last commit age in one unit (`8m`, `3h`, `9d`, `3w`, `2mo`, `1y`);
|
|
317
325
|
`never` when the branch has no commits; future timestamps render `0m`.
|
|
318
326
|
* `Commit` - last commit subject, right-truncated with `…`.
|
|
319
|
-
* `Changes` - uncommitted file count (staged + unstaged + untracked
|
|
320
|
-
clean.
|
|
327
|
+
* `Changes` - uncommitted file count (staged + unstaged + untracked files
|
|
328
|
+
under `.ace-tasks/`); `-` when clean. Untracked files outside the task tree
|
|
329
|
+
are not scanned: the deep untracked walk dominates listing time on large
|
|
330
|
+
repositories, and stray artifacts are not an activity signal.
|
|
321
331
|
* `PR` - `#N` (open), `#N draft`, `#N merged`, `#N closed`, or `-`.
|
|
322
|
-
* `Path` - `$HOME` shortened to `~` and left-elided in compact mode; absolute
|
|
323
|
-
and untruncated with `--full`.
|
|
324
332
|
|
|
325
333
|
**Task association (name-independent), in precedence order:**
|
|
326
334
|
|
|
@@ -335,15 +343,19 @@ affected by worktree filters.
|
|
|
335
343
|
name, then by HEAD commit sha, then by a unique task reference in the PR
|
|
336
344
|
title/body. Unconfigured/offline forges render `-` and never fail the command.
|
|
337
345
|
|
|
338
|
-
**Width behavior:** the table targets `120` columns, shrinks `
|
|
339
|
-
`
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
**Probe isolation:** each worktree is probed in its own forked
|
|
344
|
-
(one child per worktree, at most 16 alive, hard 5 second budget
|
|
345
|
-
|
|
346
|
-
|
|
346
|
+
**Width behavior:** the table targets `120` columns, shrinks `Commit`, then
|
|
347
|
+
`Branch` down to their minimums, and drops `Commit` on very narrow terminals.
|
|
348
|
+
Width comes from `COLUMNS`, else the terminal size, else `120`. Path lines are
|
|
349
|
+
never truncated; `--full` is exempt from truncation entirely.
|
|
350
|
+
|
|
351
|
+
**Probe isolation and performance:** each worktree is probed in its own forked
|
|
352
|
+
child process (one child per worktree, at most 16 alive, hard 5 second budget
|
|
353
|
+
per probe). The probe reads only cheap signals: HEAD metadata, tracked changes
|
|
354
|
+
(`git status --untracked-files=no`), committed task-file diffs, and the
|
|
355
|
+
`_current` symlink. The expensive deep untracked scan of `.ace-tasks/` runs as
|
|
356
|
+
a selective second round, only for worktrees the cheap association tiers could
|
|
357
|
+
not resolve. Probe results are ordered by `git worktree list` order, so output
|
|
358
|
+
is deterministic; a failed or timed-out probe renders `?` in `Age`/`Commit`/
|
|
347
359
|
`Changes` while the rest of the table renders and the command exits 0. Where
|
|
348
360
|
`fork` is unavailable, probes run sequentially with the same timeouts.
|
|
349
361
|
|
|
@@ -36,6 +36,8 @@ module Ace
|
|
|
36
36
|
@entries = entries.freeze
|
|
37
37
|
@available = available
|
|
38
38
|
@project_root = project_root
|
|
39
|
+
@canonical_directories = {}
|
|
40
|
+
@canonical_worktree_paths = {}
|
|
39
41
|
end
|
|
40
42
|
|
|
41
43
|
attr_reader :entries, :project_root
|
|
@@ -92,7 +94,7 @@ module Ace
|
|
|
92
94
|
|
|
93
95
|
expanded = canonical_path(path)
|
|
94
96
|
@entries.find do |entry|
|
|
95
|
-
entry.worktree_path &&
|
|
97
|
+
entry.worktree_path && canonical_worktree_path(entry) == expanded
|
|
96
98
|
end
|
|
97
99
|
end
|
|
98
100
|
|
|
@@ -114,7 +116,7 @@ module Ace
|
|
|
114
116
|
return nil if directory.to_s.empty?
|
|
115
117
|
|
|
116
118
|
expanded = canonical_path(directory)
|
|
117
|
-
@entries.find { |entry|
|
|
119
|
+
@entries.find { |entry| canonical_directory(entry) == expanded }
|
|
118
120
|
end
|
|
119
121
|
|
|
120
122
|
# Map changed file paths to task candidates.
|
|
@@ -146,11 +148,24 @@ module Ace
|
|
|
146
148
|
# under the task directory, including nested spec/artifact folders).
|
|
147
149
|
def find_by_enclosing_directory(absolute_path)
|
|
148
150
|
@entries.find do |entry|
|
|
149
|
-
directory =
|
|
151
|
+
directory = canonical_directory(entry)
|
|
150
152
|
absolute_path == directory || absolute_path.start_with?("#{directory}/")
|
|
151
153
|
end
|
|
152
154
|
end
|
|
153
155
|
|
|
156
|
+
# Canonical forms are compared on every lookup; resolve each entry's
|
|
157
|
+
# directory / recorded worktree path exactly once (realpath is a
|
|
158
|
+
# syscall per path component and dominates lookup time otherwise).
|
|
159
|
+
def canonical_directory(entry)
|
|
160
|
+
@canonical_directories.fetch(entry) { @canonical_directories[entry] = canonical_path(entry.directory) }
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def canonical_worktree_path(entry)
|
|
164
|
+
@canonical_worktree_paths.fetch(entry) do
|
|
165
|
+
@canonical_worktree_paths[entry] = entry.worktree_path ? canonical_path(entry.worktree_path) : nil
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
154
169
|
# Canonical comparison form for paths that may be reached through a
|
|
155
170
|
# symlinked root (e.g. macOS /var -> /private/var).
|
|
156
171
|
def canonical_path(value)
|
|
@@ -86,6 +86,24 @@ module Ace
|
|
|
86
86
|
File.expand_path(value)
|
|
87
87
|
end
|
|
88
88
|
|
|
89
|
+
# Deep-scan fallback for the task tree only: untracked files under
|
|
90
|
+
# `.ace-tasks/`. This walk is expensive on large trees, so the probe
|
|
91
|
+
# itself never performs it; callers run it selectively (second round)
|
|
92
|
+
# for worktrees the cheap association tiers could not resolve.
|
|
93
|
+
#
|
|
94
|
+
# @param path [String] worktree directory
|
|
95
|
+
# @return [Array<String>] repo-relative untracked task paths
|
|
96
|
+
def untracked_task_files(path)
|
|
97
|
+
return [] unless path && File.directory?(path)
|
|
98
|
+
|
|
99
|
+
output = git(path, "ls-files", "--others", "--exclude-standard", "--", TASK_TREE_PREFIX)
|
|
100
|
+
return [] if output.nil?
|
|
101
|
+
|
|
102
|
+
output.lines.map { |line| scrub(line.chomp) }.reject(&:empty?).select { |p| task_tree_path?(p) }
|
|
103
|
+
rescue StandardError
|
|
104
|
+
[]
|
|
105
|
+
end
|
|
106
|
+
|
|
89
107
|
private
|
|
90
108
|
|
|
91
109
|
# @return [Hash, nil] `{sha:, committer_epoch:, subject:}` for HEAD
|
|
@@ -101,9 +119,11 @@ module Ace
|
|
|
101
119
|
nil
|
|
102
120
|
end
|
|
103
121
|
|
|
104
|
-
# @return [Hash, nil] staged/unstaged
|
|
122
|
+
# @return [Hash, nil] staged/unstaged counts and changed paths
|
|
123
|
+
# (tracked files only; untracked discovery is opt-in via
|
|
124
|
+
# {#untracked_task_files})
|
|
105
125
|
def status_snapshot(path)
|
|
106
|
-
output = git(path, "status", "--porcelain=v1", "-z", "--untracked-files=
|
|
126
|
+
output = git(path, "status", "--porcelain=v1", "-z", "--untracked-files=no")
|
|
107
127
|
return nil if output.nil?
|
|
108
128
|
|
|
109
129
|
staged = 0
|
|
@@ -11,13 +11,18 @@ module Ace
|
|
|
11
11
|
module Molecules
|
|
12
12
|
# Renders the width-aware listing table.
|
|
13
13
|
#
|
|
14
|
+
# Each worktree renders as three lines: the aligned columns
|
|
15
|
+
# (Task/Branch/Age/Commit/Changes/PR), the never-truncated path
|
|
16
|
+
# indented to the Commit column, and a blank separator line, so each
|
|
17
|
+
# entry is visually grouped and the displayed path is always directly
|
|
18
|
+
# usable (copy/paste, cd).
|
|
19
|
+
#
|
|
14
20
|
# Layout targets (two-space gutters): Task = max(task cells, 4),
|
|
15
|
-
# Branch 25 (min 12), Age 5, Commit 24 (min 10), Changes 7, PR 11
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
# and is exempt from width limits.
|
|
21
|
+
# Branch 25 (min 12), Age 5, Commit 24 (min 10), Changes 7, PR 11.
|
|
22
|
+
# When the target total exceeds the detected width, columns shrink in
|
|
23
|
+
# priority order Commit -> Branch down to their minima, then Commit is
|
|
24
|
+
# dropped. Column lines never exceed the detected width; `--full`
|
|
25
|
+
# prints absolute paths and is exempt from width limits.
|
|
21
26
|
class WorktreeTableFormatter
|
|
22
27
|
GUTTER = " "
|
|
23
28
|
COLUMNS_MIN_WIDTH = 40
|
|
@@ -30,12 +35,11 @@ module Ace
|
|
|
30
35
|
COMMIT_MIN = 10
|
|
31
36
|
CHANGES_WIDTH = 7
|
|
32
37
|
PR_WIDTH = 11
|
|
33
|
-
PATH_TARGET = 32
|
|
34
|
-
PATH_MIN = 10
|
|
35
38
|
TASK_MIN = 4
|
|
39
|
+
PATH_INDENT = " "
|
|
36
40
|
|
|
37
41
|
# @param width [Integer] detected terminal width
|
|
38
|
-
# @param full [Boolean] exhaustive listing (
|
|
42
|
+
# @param full [Boolean] exhaustive listing (absolute paths)
|
|
39
43
|
# @param env [Hash] environment (HOME shortening, COLUMNS detection)
|
|
40
44
|
def initialize(width: nil, full: false, env: ENV, stdout: $stdout)
|
|
41
45
|
@width = width || self.class.detected_width(env: env, stdout: stdout)
|
|
@@ -65,75 +69,66 @@ module Ace
|
|
|
65
69
|
DEFAULT_WIDTH
|
|
66
70
|
end
|
|
67
71
|
|
|
68
|
-
# Render header + separator +
|
|
72
|
+
# Render header + separator + three lines per worktree (columns,
|
|
73
|
+
# indented full path, blank separator).
|
|
69
74
|
#
|
|
70
75
|
# @param worktrees [Array<Models::WorktreeInfo>]
|
|
71
76
|
# @return [Array<String>] output lines
|
|
72
77
|
def render(worktrees)
|
|
73
78
|
rows = Array(worktrees)
|
|
74
79
|
layout = compute_layout(rows)
|
|
75
|
-
[
|
|
80
|
+
[
|
|
81
|
+
header_line(layout),
|
|
82
|
+
separator_line(layout),
|
|
83
|
+
*rows.flat_map { |worktree| [row_line(worktree, layout), path_line(worktree, layout), ""] }
|
|
84
|
+
]
|
|
76
85
|
end
|
|
77
86
|
|
|
78
87
|
private
|
|
79
88
|
|
|
80
|
-
Layout = Struct.new(:task, :branch, :age, :commit, :changes, :pr, :
|
|
89
|
+
Layout = Struct.new(:task, :branch, :age, :commit, :changes, :pr, :total_width,
|
|
81
90
|
keyword_init: true)
|
|
82
91
|
|
|
83
92
|
def compute_layout(worktrees)
|
|
84
93
|
task_width = [TASK_MIN, *worktrees.map { |wt| task_cell(wt).length }].max
|
|
85
94
|
branch_width = BRANCH_TARGET
|
|
86
95
|
commit_width = COMMIT_TARGET
|
|
87
|
-
path_width = PATH_TARGET
|
|
88
|
-
|
|
89
|
-
if @full
|
|
90
|
-
path_width = [PATH_TARGET, *worktrees.map { |wt| full_path(wt).length }].max
|
|
91
|
-
return build_layout(task_width, branch_width, commit_width, path_width)
|
|
92
|
-
end
|
|
93
96
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
97
|
+
unless @full
|
|
98
|
+
while total_width(task_width, branch_width, commit_width) > @width
|
|
99
|
+
if commit_width > COMMIT_MIN
|
|
100
|
+
commit_width -= 1
|
|
101
|
+
elsif branch_width > BRANCH_MIN
|
|
102
|
+
branch_width -= 1
|
|
103
|
+
else
|
|
104
|
+
break
|
|
105
|
+
end
|
|
103
106
|
end
|
|
104
107
|
end
|
|
105
108
|
|
|
106
|
-
layout = build_layout(task_width, branch_width, commit_width
|
|
109
|
+
layout = build_layout(task_width, branch_width, commit_width)
|
|
107
110
|
return layout if layout[:total_width] <= @width
|
|
108
111
|
|
|
109
|
-
|
|
112
|
+
drop_commit(layout)
|
|
110
113
|
end
|
|
111
114
|
|
|
112
|
-
def build_layout(task_width, branch_width, commit_width
|
|
113
|
-
total = total_width(task_width, branch_width, commit_width
|
|
115
|
+
def build_layout(task_width, branch_width, commit_width)
|
|
116
|
+
total = total_width(task_width, branch_width, commit_width)
|
|
114
117
|
Layout.new(task: task_width, branch: branch_width, age: AGE_WIDTH, commit: commit_width,
|
|
115
|
-
changes: CHANGES_WIDTH, pr: PR_WIDTH,
|
|
118
|
+
changes: CHANGES_WIDTH, pr: PR_WIDTH, total_width: total)
|
|
116
119
|
end
|
|
117
120
|
|
|
118
|
-
def total_width(task_width, branch_width, commit_width
|
|
121
|
+
def total_width(task_width, branch_width, commit_width)
|
|
119
122
|
task_width + branch_width + AGE_WIDTH + commit_width + CHANGES_WIDTH + PR_WIDTH +
|
|
120
|
-
|
|
123
|
+
(GUTTER.length * 5)
|
|
121
124
|
end
|
|
122
125
|
|
|
123
|
-
# Drop the
|
|
124
|
-
|
|
125
|
-
def drop_path(layout)
|
|
126
|
-
without_path = Layout.new(
|
|
127
|
-
task: layout[:task], branch: layout[:branch], age: layout[:age], commit: layout[:commit],
|
|
128
|
-
changes: layout[:changes], pr: layout[:pr], path: 0,
|
|
129
|
-
total_width: layout[:total_width] - layout[:path] - GUTTER.length
|
|
130
|
-
)
|
|
131
|
-
return without_path if without_path[:total_width] <= @width
|
|
132
|
-
|
|
126
|
+
# Drop the Commit column when even the minimum layout overflows.
|
|
127
|
+
def drop_commit(layout)
|
|
133
128
|
Layout.new(
|
|
134
|
-
task:
|
|
135
|
-
commit: 0, changes:
|
|
136
|
-
total_width:
|
|
129
|
+
task: layout[:task], branch: layout[:branch], age: layout[:age],
|
|
130
|
+
commit: 0, changes: layout[:changes], pr: layout[:pr],
|
|
131
|
+
total_width: layout[:total_width] - layout[:commit] - GUTTER.length
|
|
137
132
|
)
|
|
138
133
|
end
|
|
139
134
|
|
|
@@ -145,7 +140,6 @@ module Ace
|
|
|
145
140
|
cells << pad("Commit", layout[:commit], :left) if layout[:commit].positive?
|
|
146
141
|
cells << pad("Changes", layout[:changes], :right)
|
|
147
142
|
cells << pad("PR", layout[:pr], :left)
|
|
148
|
-
cells << pad("Path", layout[:path], :left) if layout[:path].positive?
|
|
149
143
|
cells.join(GUTTER).rstrip
|
|
150
144
|
end
|
|
151
145
|
|
|
@@ -161,10 +155,28 @@ module Ace
|
|
|
161
155
|
cells << pad(commit_cell(worktree, layout[:commit]), layout[:commit], :left) if layout[:commit].positive?
|
|
162
156
|
cells << pad(changes_cell(worktree), layout[:changes], :right)
|
|
163
157
|
cells << pad(pr_cell(worktree), layout[:pr], :left)
|
|
164
|
-
cells << pad(path_cell(worktree, layout[:path]), layout[:path], :left) if layout[:path].positive?
|
|
165
158
|
cells.join(GUTTER).rstrip
|
|
166
159
|
end
|
|
167
160
|
|
|
161
|
+
# The full, never-truncated worktree path on its own line, indented
|
|
162
|
+
# to the Commit column so it reads as a continuation of the row
|
|
163
|
+
# (falls back to the Branch column, then a minimal indent).
|
|
164
|
+
def path_line(worktree, layout)
|
|
165
|
+
path = @full ? full_path(worktree) : compact_path(worktree)
|
|
166
|
+
"#{path_indent(layout)}#{path}"
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def path_indent(layout)
|
|
170
|
+
if layout[:commit].positive?
|
|
171
|
+
" " * (layout[:task] + GUTTER.length + layout[:branch] + GUTTER.length +
|
|
172
|
+
layout[:age] + GUTTER.length)
|
|
173
|
+
elsif layout[:branch].positive?
|
|
174
|
+
" " * (layout[:task] + GUTTER.length)
|
|
175
|
+
else
|
|
176
|
+
PATH_INDENT
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
168
180
|
# --- Cell contents ---
|
|
169
181
|
|
|
170
182
|
def task_cell(worktree)
|
|
@@ -204,13 +216,6 @@ module Ace
|
|
|
204
216
|
PrStatusResolver.cell(worktree.pr)
|
|
205
217
|
end
|
|
206
218
|
|
|
207
|
-
def path_cell(worktree, width)
|
|
208
|
-
return "" if width <= 0
|
|
209
|
-
|
|
210
|
-
path = @full ? full_path(worktree) : compact_path(worktree)
|
|
211
|
-
Atoms::TextTruncator.left(path, width)
|
|
212
|
-
end
|
|
213
|
-
|
|
214
219
|
def full_path(worktree)
|
|
215
220
|
File.expand_path(worktree.path)
|
|
216
221
|
end
|
|
@@ -65,6 +65,7 @@ module Ace
|
|
|
65
65
|
project_root = task_index.project_root || @project_root
|
|
66
66
|
|
|
67
67
|
probes = probe_worktrees(raw_worktrees, project_root)
|
|
68
|
+
supplement_untracked_task_files(raw_worktrees, probes, project_root, task_index)
|
|
68
69
|
enriched = enrich(raw_worktrees, probes, task_index, now, options)
|
|
69
70
|
selected = apply_filters(enriched, options)
|
|
70
71
|
active, inactive = selected.partition { |worktree| worktree.active_within_window }
|
|
@@ -111,6 +112,40 @@ module Ace
|
|
|
111
112
|
runner.run(raw_worktrees)
|
|
112
113
|
end
|
|
113
114
|
|
|
115
|
+
# Second probe round, only for worktrees the cheap association tiers
|
|
116
|
+
# (metadata, `_current` symlink, validated name, tracked task-file
|
|
117
|
+
# changes, committed task files) could not resolve. The deep
|
|
118
|
+
# `.ace-tasks/` untracked scan dominates listing time on large
|
|
119
|
+
# repositories, so it runs selectively and merges its findings back
|
|
120
|
+
# into the round-one payloads (counts + changed task paths) before
|
|
121
|
+
# enrichment classifies activity and renders.
|
|
122
|
+
def supplement_untracked_task_files(raw_worktrees, probes, project_root, task_index)
|
|
123
|
+
return unless task_index.available?
|
|
124
|
+
|
|
125
|
+
resolver = Molecules::TaskAssociationResolver.new(task_index: task_index)
|
|
126
|
+
unresolved = raw_worktrees.each_index.select do |index|
|
|
127
|
+
probe = probes[index]
|
|
128
|
+
probe && resolver.resolve(raw_worktrees[index], probe).task_id.nil?
|
|
129
|
+
end
|
|
130
|
+
return if unresolved.empty?
|
|
131
|
+
|
|
132
|
+
probe_molecule = Molecules::WorktreeProbe.new(project_root: project_root)
|
|
133
|
+
supplement = ->(worktree) { {untracked_task_files: probe_molecule.untracked_task_files(worktree.path)} }
|
|
134
|
+
runner = WorktreeProbeRunner.new(project_root: project_root, probe: supplement)
|
|
135
|
+
payloads = runner.run(unresolved.map { |index| raw_worktrees[index] })
|
|
136
|
+
|
|
137
|
+
unresolved.each_with_index do |probe_index, position|
|
|
138
|
+
payload = payloads[position]
|
|
139
|
+
next if payload.nil? || payload[:untracked_task_files].to_a.empty?
|
|
140
|
+
|
|
141
|
+
paths = payload[:untracked_task_files]
|
|
142
|
+
probe = probes[probe_index]
|
|
143
|
+
probe[:untracked] = probe[:untracked].to_i + paths.length
|
|
144
|
+
probe[:uncommitted_paths] = Array(probe[:uncommitted_paths]) + paths
|
|
145
|
+
probe[:changed_task_files] = Array(probe[:changed_task_files]) + paths
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
114
149
|
def enrich(raw_worktrees, probes, task_index, now, options)
|
|
115
150
|
resolver = Molecules::TaskAssociationResolver.new(task_index: task_index)
|
|
116
151
|
pr_resolver = @pr_resolver || Molecules::PrStatusResolver.new
|
|
@@ -252,14 +287,17 @@ module Ace
|
|
|
252
287
|
end
|
|
253
288
|
|
|
254
289
|
rows = options[:full] ? (active + inactive) : active
|
|
290
|
+
table_ended_with_blank = false
|
|
255
291
|
if rows.empty?
|
|
256
292
|
lines << footer.no_active_message
|
|
257
293
|
else
|
|
258
294
|
lines.concat(table.render(rows))
|
|
295
|
+
table_ended_with_blank = true
|
|
259
296
|
end
|
|
260
297
|
|
|
261
298
|
unless options[:quiet]
|
|
262
|
-
|
|
299
|
+
# The table already ends each entry with a blank separator line.
|
|
300
|
+
lines << "" unless table_ended_with_blank
|
|
263
301
|
lines.concat(footer.lines(
|
|
264
302
|
inactive: inactive.length,
|
|
265
303
|
task_associated: statistics[:task_associated],
|