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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8cc9f7055655e28b11fd7caa92051817825c8487316237972859985aca1d2478
4
- data.tar.gz: dba29a6ffef411ec95e890d6521aca30385f823bb3bb52827bad86a4fe8fd6ba
3
+ metadata.gz: 3fbcf3673de821e4ed42fd7415a7cc0fa88fca4176d2d3cfbf7cff65e6d50332
4
+ data.tar.gz: ec88eedd7b9f6d3822835b70777e235a5581cacf3a413a98ee4a4bdd66eb5ae1
5
5
  SHA512:
6
- metadata.gz: 776ccc9af30e1dc2027d9a27933eefe6589e33c4e4ab3a23a11d3d3e37ef238a1e360480d1ac60739438277e37bf8c27586438588bd6efcc39a7e22010558606
7
- data.tar.gz: 633565bd32222e69ba61d521a3f7d110f28a197c6f7790061eeb131acf5bd714cd302c5ae5558adfb03d7880daf6db8eb5ef2a02d02da4eb9a099e3120bdfea3
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 Path
296
- ----------------------------------------------------------------------------------------------
297
- k9y codex/r3-bounded-convergence 17h feat(review): define bo… 2 #142 draft …/codex-r3-bounded-convergence
298
- - codex/ei9-campaign-join 8m feat(ace-assign): compl… - - …/ei9-campaign-composition-join
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 (staged,
306
- unstaged, or untracked). Bare worktrees are never active. Filters apply before
307
- the active/inactive split, so the header and footer counts always describe the
308
- filtered set. `Recent branches without worktrees` is branch-based and is not
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); `-` when
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 `Path`, then
339
- `Commit`, then `Branch` down to their minimums, and drops `Path` (then `Commit`)
340
- on very narrow terminals. Width comes from `COLUMNS`, else the terminal size,
341
- else `120`. `--full` is exempt from truncation.
342
-
343
- **Probe isolation:** each worktree is probed in its own forked child process
344
- (one child per worktree, at most 16 alive, hard 5 second budget per probe).
345
- Probe results are ordered by `git worktree list` order, so output is
346
- deterministic; a failed or timed-out probe renders `?` in `Age`/`Commit`/
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 && canonical_path(entry.worktree_path) == expanded
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| canonical_path(entry.directory) == expanded }
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 = canonical_path(entry.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/untracked counts and changed paths
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=all")
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
- # Path 32 (min 10). When the target total exceeds the detected width,
17
- # columns shrink in priority order Path -> Commit -> Branch down to
18
- # their minima, then Path is dropped, then Commit. Compact output never
19
- # exceeds the detected width; `--full` prints untruncated absolute paths
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 (no truncation)
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 + rows.
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
- [header_line(layout), separator_line(layout), *rows.map { |worktree| row_line(worktree, layout) }]
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, :path, :total_width,
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
- while total_width(task_width, branch_width, commit_width, path_width) > @width
95
- if path_width > PATH_MIN
96
- path_width -= 1
97
- elsif commit_width > COMMIT_MIN
98
- commit_width -= 1
99
- elsif branch_width > BRANCH_MIN
100
- branch_width -= 1
101
- else
102
- break
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, path_width)
109
+ layout = build_layout(task_width, branch_width, commit_width)
107
110
  return layout if layout[:total_width] <= @width
108
111
 
109
- drop_path(layout)
112
+ drop_commit(layout)
110
113
  end
111
114
 
112
- def build_layout(task_width, branch_width, commit_width, path_width)
113
- total = total_width(task_width, branch_width, commit_width, path_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, path: path_width, total_width: total)
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, path_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
- path_width + (GUTTER.length * 6)
123
+ (GUTTER.length * 5)
121
124
  end
122
125
 
123
- # Drop the Path column when even the minimum layout overflows; then
124
- # drop Commit as the last resort.
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: without_path[:task], branch: without_path[:branch], age: without_path[:age],
135
- commit: 0, changes: without_path[:changes], pr: without_path[:pr], path: 0,
136
- total_width: without_path[:total_width] - without_path[:commit] - GUTTER.length
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
- lines << ""
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],
@@ -3,7 +3,7 @@
3
3
  module Ace
4
4
  module Git
5
5
  module Worktree
6
- VERSION = "0.27.0"
6
+ VERSION = "0.27.1"
7
7
  end
8
8
  end
9
9
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ace-git-worktree
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.27.0
4
+ version: 0.27.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michal Czyz