dev_context 1.0.0

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.
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DevContext
4
+ module Commands
5
+ module ContextLifecycle
6
+ def cmd_add
7
+ args = argv.dup
8
+ opts = parse_common_flags!(args, allow_context: true, allow_norun: true)
9
+ return usage_error("dx add PATH|URL ...") if args.empty?
10
+ if opts[:context] && args.length != 1
11
+ return usage_error("dx add -c NAME PATH|URL")
12
+ end
13
+
14
+ exit_code = 0
15
+ args.each do |target|
16
+ code = add_single_target(target, explicit_context: opts[:context], opts: opts)
17
+ exit_code = code unless code.zero?
18
+ end
19
+
20
+ exit_code
21
+ rescue OptionParser::InvalidOption => e
22
+ err.puts("dx: #{e.message}")
23
+ usage_error("dx add [-c NAME] [-n] [-v] PATH|URL ...")
24
+ end
25
+
26
+ def cmd_clone
27
+ args = argv.dup
28
+ opts = parse_common_flags!(args, allow_context: true, allow_norun: true)
29
+ url = args.shift
30
+ explicit_path = args.shift
31
+ return usage_error("dx clone URL [PATH]") if blank?(url)
32
+ return usage_error("dx clone URL [PATH]") unless url?(url)
33
+
34
+ clone_and_register!(url, explicit_path: explicit_path, explicit_name: opts[:context], opts: opts)
35
+ rescue OptionParser::InvalidOption => e
36
+ err.puts("dx: #{e.message}")
37
+ usage_error("dx clone [-c NAME] [-n] [-v] URL [PATH]")
38
+ end
39
+
40
+ def cmd_create
41
+ args = argv.dup
42
+ opts = parse_common_flags!(args, allow_context: true, allow_norun: true)
43
+ name = args.shift
44
+ name = opts[:context] if blank?(name)
45
+ return usage_error("dx create NAME [PATH] [BRANCH]") if blank?(name)
46
+
47
+ repo_path = args.shift || pwd
48
+ branch = args.shift || current_branch(repo_path) || "main"
49
+ if opts[:norun]
50
+ out.puts("Would create context #{config.normalize_name(name)} -> #{File.expand_path(repo_path)} @ #{branch}")
51
+ return 0
52
+ end
53
+
54
+ context_name = config.add_context(name: name, repo_path: repo_path, branch: branch)
55
+ out.puts("Created context #{context_name} -> #{File.expand_path(repo_path)} @ #{branch}")
56
+ 0
57
+ rescue OptionParser::InvalidOption => e
58
+ err.puts("dx: #{e.message}")
59
+ usage_error("dx create [-c NAME] [-n] [-v] NAME [PATH] [BRANCH]")
60
+ end
61
+
62
+ def cmd_activate
63
+ target = argv.shift
64
+ return usage_error("dx activate CONTEXT|PATH|URL or dx cd CONTEXT|PATH|URL") if blank?(target)
65
+
66
+ context, code = resolve_or_create_context(target)
67
+ return code unless code.zero?
68
+
69
+ config.activate_context!(context.fetch("name"))
70
+ script = ShellEmitter.new(
71
+ context: context,
72
+ remote_name: env.fetch("DX_GIT_REMOTE_NAME", "USE-REPO"),
73
+ auto_create_local_branch: truthy?(env.fetch("DX_AUTO_CREATE_LOCAL_BRANCH", "true"))
74
+ ).activation_script
75
+ out.write(script)
76
+ 0
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DevContext
4
+ module Commands
5
+ module Find
6
+ def cmd_find
7
+ args = argv.dup
8
+ opts = { stashes: false, branches: false, paths: false, all: false }
9
+ parser = OptionParser.new
10
+ parser.on("--stashes", "Search stash entry titles") { opts[:stashes] = true }
11
+ parser.on("--branches", "Search current branch names") { opts[:branches] = true }
12
+ parser.on("--paths", "Search repo paths") { opts[:paths] = true }
13
+ parser.on("--all", "Show likely-forgotten work (stashes + dirty repos + detached HEAD)") { opts[:all] = true }
14
+ parser.parse!(args)
15
+
16
+ pattern = args.shift
17
+ return usage_error("dx find [--stashes|--branches|--paths] <pattern>\n dx find --all") unless args.empty?
18
+ return usage_error("dx find [--stashes|--branches|--paths] <pattern>\n dx find --all") if opts[:all] && pattern
19
+ return usage_error("dx find [--stashes|--branches|--paths] <pattern>\n dx find --all") if !opts[:all] && blank?(pattern)
20
+
21
+ scopes = find_scopes(opts)
22
+ rows = opts[:all] ? find_all_rows(scopes) : find_pattern_rows(pattern, scopes)
23
+ if rows.empty?
24
+ out.puts(opts[:all] ? "No likely-forgotten work found" : "No matches")
25
+ return 0
26
+ end
27
+
28
+ render_find_table(rows)
29
+ 0
30
+ rescue OptionParser::InvalidOption => e
31
+ err.puts("dx: #{e.message}")
32
+ usage_error("dx find [--stashes|--branches|--paths] <pattern>\n dx find --all")
33
+ end
34
+
35
+ private
36
+
37
+ def find_scopes(opts)
38
+ selected = []
39
+ selected << :stashes if opts[:stashes]
40
+ selected << :branches if opts[:branches]
41
+ selected << :paths if opts[:paths]
42
+ selected.empty? ? %i[stashes branches paths] : selected
43
+ end
44
+
45
+ def find_pattern_rows(pattern, scopes)
46
+ rows = []
47
+
48
+ if scopes.include?(:paths)
49
+ known_repo_rows.each do |repo|
50
+ next unless pattern_match?(repo[:path], pattern)
51
+
52
+ rows << { type: "path", path: repo[:path], branch: repo[:branch], detail: repo[:path] }
53
+ end
54
+ end
55
+
56
+ if scopes.include?(:branches)
57
+ known_repo_rows.each do |repo|
58
+ next unless pattern_match?(repo[:branch], pattern)
59
+
60
+ rows << { type: "branch", path: repo[:path], branch: repo[:branch], detail: repo[:branch] }
61
+ end
62
+ end
63
+
64
+ if scopes.include?(:stashes)
65
+ known_repo_stash_rows.each do |repo|
66
+ repo[:entries].each do |entry|
67
+ next unless pattern_match?(entry, pattern)
68
+
69
+ rows << { type: "stash", path: repo[:path], branch: repo[:branch], detail: entry }
70
+ end
71
+ end
72
+ end
73
+
74
+ dedupe_and_sort_find_rows(rows)
75
+ end
76
+
77
+ def find_all_rows(scopes)
78
+ rows = []
79
+ known = known_repo_rows
80
+
81
+ if scopes.include?(:paths)
82
+ known.each do |repo|
83
+ status = ::DevContext::Status.new(repo_path: repo[:path]).one_line
84
+ rows << { type: "dirty", path: repo[:path], branch: repo[:branch], detail: compact_status(status) } unless status.start_with?("Up to date")
85
+ end
86
+ end
87
+
88
+ if scopes.include?(:branches)
89
+ known.each do |repo|
90
+ rows << { type: "detached", path: repo[:path], branch: "HEAD", detail: "detached HEAD" } if repo[:branch] == "HEAD"
91
+ end
92
+ end
93
+
94
+ if scopes.include?(:stashes)
95
+ known_repo_stash_rows.each do |repo|
96
+ rows << { type: "stash", path: repo[:path], branch: repo[:branch], detail: "s:#{repo[:count]}" }
97
+ end
98
+ end
99
+
100
+ dedupe_and_sort_find_rows(rows)
101
+ end
102
+
103
+ def dedupe_and_sort_find_rows(rows)
104
+ rows
105
+ .uniq { |r| [r[:type], r[:path], r[:branch], r[:detail]] }
106
+ .sort_by { |r| [r[:type], r[:path], r[:detail]] }
107
+ end
108
+
109
+ def render_find_table(rows)
110
+ display_rows = rows.map { |row| row.merge(path: display_path(row[:path])) }
111
+ type_w = [display_rows.map { |r| r[:type].length }.max || 0, "Type".length].max
112
+ path_w = [display_rows.map { |r| r[:path].length }.max || 0, "Path".length].max
113
+ branch_w = [display_rows.map { |r| r[:branch].length }.max || 0, "Branch".length].max
114
+
115
+ out.puts(format("%-#{type_w}s %-#{path_w}s %-#{branch_w}s %s", "Type", "Path", "Branch", "Match"))
116
+ out.puts(format("%-#{type_w}s %-#{path_w}s %-#{branch_w}s %s", "-" * type_w, "-" * path_w, "-" * branch_w, "-" * 5))
117
+ display_rows.each do |row|
118
+ out.puts(format("%-#{type_w}s %-#{path_w}s %-#{branch_w}s %s", row[:type], row[:path], row[:branch], row[:detail]))
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,163 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DevContext
4
+ module Commands
5
+ module GitOps
6
+ def cmd_active
7
+ active = config.active_contexts
8
+ if active.empty?
9
+ out.puts("No active contexts")
10
+ return 0
11
+ end
12
+
13
+ active.each do |ctx|
14
+ status = Status.new(repo_path: ctx.fetch("repo_path")).one_line
15
+ out.puts("#{ctx.fetch("name")} #{ctx.fetch("repo_path")} #{ctx.fetch("branch")} #{status}")
16
+ end
17
+ 0
18
+ end
19
+
20
+ def cmd_deactivate
21
+ target = argv.shift
22
+ return usage_error("dx deactivate CONTEXT") if blank?(target)
23
+
24
+ context = matcher.resolve(target)
25
+ return not_found(target) unless context
26
+
27
+ config.deactivate_context!(context.fetch("name"))
28
+ out.puts("Deactivated #{context.fetch("name")}")
29
+ 0
30
+ end
31
+
32
+ def cmd_pop
33
+ target = argv.shift
34
+
35
+ if target
36
+ context = matcher.resolve(target)
37
+ return not_found(target) unless context
38
+
39
+ removed = config.deactivate_context!(context.fetch("name"))
40
+ return not_found(target) unless removed
41
+
42
+ out.puts("Popped #{context.fetch('name')}")
43
+ else
44
+ top_name = config.active_stack.first
45
+ if top_name.nil?
46
+ out.puts("No active contexts")
47
+ return 0
48
+ end
49
+
50
+ context = config.contexts.fetch(top_name)
51
+ config.deactivate_context!(top_name)
52
+ out.puts("Popped #{context.fetch('name')}")
53
+ end
54
+
55
+ new_top = config.active_contexts.first
56
+ return 0 unless new_top
57
+
58
+ out.puts("cd #{Shellwords.escape(new_top.fetch('repo_path'))}")
59
+ 0
60
+ end
61
+
62
+ def cmd_diff
63
+ target = argv.shift
64
+ contexts = target ? [matcher.resolve(target)].compact : config.active_contexts
65
+ return not_found(target) if target && contexts.empty?
66
+ if contexts.empty?
67
+ out.puts("No active contexts")
68
+ return 0
69
+ end
70
+
71
+ contexts.each do |ctx|
72
+ path = ctx.fetch("repo_path")
73
+ out.puts("# #{ctx.fetch('name')} (#{path})")
74
+ out.print(`git -C "#{path}" diff`)
75
+ end
76
+ 0
77
+ end
78
+
79
+ def cmd_branches
80
+ target = argv.shift
81
+ return usage_error("dx branches|br [CONTEXT|PATTERN]") unless argv.empty?
82
+
83
+ if target
84
+ context = matcher.resolve(target)
85
+ if context
86
+ contexts = [context]
87
+ contexts.each do |ctx|
88
+ path = ctx.fetch("repo_path")
89
+ out.puts("# #{ctx.fetch('name')} (#{path})")
90
+ out.print(`git -C "#{path}" branch --list`)
91
+ end
92
+ return 0
93
+ end
94
+
95
+ rows = known_repo_rows.filter_map do |repo|
96
+ next unless pattern_match?(repo[:branch], target)
97
+
98
+ repo
99
+ end.sort_by { |row| [row[:branch], row[:path]] }
100
+
101
+ if rows.empty?
102
+ out.puts("No matching branches")
103
+ return 0
104
+ end
105
+
106
+ display_rows = rows.map { |r| r.merge(path: display_path(r[:path])) }
107
+ path_w = [display_rows.map { |r| r[:path].length }.max || 0, "Repo".length].max
108
+ branch_w = [display_rows.map { |r| r[:branch].length }.max || 0, "Branch".length].max
109
+ out.puts(format("%-#{path_w}s %-#{branch_w}s", "Repo", "Branch"))
110
+ out.puts(format("%-#{path_w}s %-#{branch_w}s", "-" * path_w, "-" * branch_w))
111
+ display_rows.each { |row| out.puts(format("%-#{path_w}s %-#{branch_w}s", row[:path], row[:branch])) }
112
+ return 0
113
+ end
114
+
115
+ contexts = config.active_contexts
116
+ if contexts.empty?
117
+ out.puts("No active contexts")
118
+ return 0
119
+ end
120
+
121
+ contexts.each do |ctx|
122
+ path = ctx.fetch("repo_path")
123
+ out.puts("# #{ctx.fetch('name')} (#{path})")
124
+ out.print(`git -C "#{path}" branch --list`)
125
+ end
126
+ 0
127
+ end
128
+
129
+ def cmd_checkout
130
+ args = argv.dup
131
+ create_flag = false
132
+ if args.first == "-b"
133
+ create_flag = true
134
+ args.shift
135
+ end
136
+ feature = args.shift
137
+ target = args.shift
138
+ return usage_error("dx co|checkout [-b] FEATURE [CONTEXT]") if blank?(feature)
139
+
140
+ context = if target
141
+ matcher.resolve(target)
142
+ else
143
+ config.active_contexts.first
144
+ end
145
+ return not_found(target || "active context") unless context
146
+
147
+ cmd = create_flag ? %(git -C "#{context.fetch('repo_path')}" checkout -b #{Shellwords.escape(feature)} 2>&1) :
148
+ %(git -C "#{context.fetch('repo_path')}" checkout #{Shellwords.escape(feature)} 2>&1)
149
+ output = `#{cmd}`
150
+ unless $?.success?
151
+ err.puts(output)
152
+ return 28
153
+ end
154
+
155
+ context_name = context.fetch("name")
156
+ config.contexts[context_name]["branch"] = feature
157
+ config.send(:save!)
158
+ out.puts("Checked out #{feature} in #{context_name}")
159
+ 0
160
+ end
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DevContext
4
+ module Commands
5
+ module Help
6
+ def cmd_help(topic = nil)
7
+ topic ||= argv.first
8
+ if topic
9
+ resolution = resolve_command(topic)
10
+ normalized = resolution.is_a?(Hash) ? resolution[:command] : resolution
11
+ return 1 if normalized.nil?
12
+
13
+ return cmd_help_topic(normalized)
14
+ end
15
+
16
+ out.puts <<~HELP
17
+ dx - developer context manager
18
+
19
+ Commands:
20
+ dx active
21
+ dx add PATH|URL ...
22
+ dx activate CONTEXT|PATH|URL
23
+ dx branches|br [CONTEXT|PATTERN]
24
+ dx cd CONTEXT|PATH|URL
25
+ dx checkout|co [-b] FEATURE [CONTEXT]
26
+ dx clone URL [PATH]
27
+ dx create NAME [PATH] [BRANCH]
28
+ dx deactivate CONTEXT
29
+ dx diff [CONTEXT]
30
+ dx find [--stashes|--branches|--paths] <pattern>
31
+ dx find --all
32
+ dx <pattern> # shorthand for: dx find <pattern>
33
+ dx help
34
+ dx init
35
+ dx popd [CONTEXT]
36
+ dx pushd CONTEXT|PATH|URL
37
+ dx repos [PATTERN]
38
+ dx remove CONTEXT|PATH
39
+ dx stashes [--list] [PATTERN]
40
+ dx status|wip
41
+ HELP
42
+ 0
43
+ end
44
+
45
+ def cmd_help_topic(command)
46
+ text = case command
47
+ when "status", "wip"
48
+ "Usage: dx status [--all] [--dirty] [-b BRANCHPATTERN] [-p PATHPATTERN]\n dx wip [--all] [--dirty] [-b BRANCHPATTERN] [-p PATHPATTERN]\n\nShow branch and git status for active contexts. Use --all for all known repos, --dirty to filter non-clean repos, and -b/-p to filter by branch/path."
49
+ when "repos"
50
+ "Usage: dx repos [-b BRANCHPATTERN] [-p PATHPATTERN] [PATTERN]\n\nList all known repos without git status details. PATTERN is shorthand for path filtering."
51
+ when "stashes"
52
+ "Usage: dx stashes [--list] [PATTERN]\n\nShow repos with stashes. Provide PATTERN to filter stash titles; use --list to print matching entries."
53
+ when "find"
54
+ "Usage: dx find [--stashes|--branches|--paths] <pattern>\n dx find --all\n\nSearch stash titles, branches, and repo paths. Use --all to show likely-forgotten work."
55
+ when "add"
56
+ "Usage: dx add [-c NAME] [-n] [-v] PATH|URL ..."
57
+ when "clone"
58
+ "Usage: dx clone [-c NAME] [-n] [-v] URL [PATH]"
59
+ when "create"
60
+ "Usage: dx create [-c NAME] [-n] [-v] NAME [PATH] [BRANCH]"
61
+ when "activate", "cd", "pushd"
62
+ "Usage: dx #{command} CONTEXT|PATH|URL"
63
+ when "popd"
64
+ "Usage: dx popd [CONTEXT]"
65
+ when "remove"
66
+ "Usage: dx remove CONTEXT|PATH"
67
+ when "diff"
68
+ "Usage: dx diff [CONTEXT]"
69
+ when "branches", "br"
70
+ "Usage: dx branches|br [CONTEXT|PATTERN]\n\nWith a context, list git branches in that repo. With PATTERN, find repos whose current branch matches."
71
+ when "checkout", "co"
72
+ "Usage: dx checkout|co [-b] FEATURE [CONTEXT]"
73
+ else
74
+ "Usage: dx #{command}"
75
+ end
76
+ out.puts(text)
77
+ 0
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DevContext
4
+ module Commands
5
+ module Init
6
+ def cmd_init
7
+ created = config.init!
8
+ shell_status = ShellSetup.new.install!
9
+
10
+ out.puts(created ? "Initialized ~/.dxcf" : "~/.dxcf already exists")
11
+ shell_message = case shell_status
12
+ when :created then "Created ~/.dx.sh"
13
+ when :updated then "Updated ~/.dx.sh"
14
+ else "~/.dx.sh already exists"
15
+ end
16
+ out.puts(shell_message)
17
+ out.puts('Add `source ~/.dx.sh` to ~/.zshrc or ~/.bashrc')
18
+ 0
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DevContext
4
+ module Commands
5
+ module Remove
6
+ def cmd_remove
7
+ target = argv.shift
8
+ return usage_error("dx remove CONTEXT|PATH") if blank?(target)
9
+
10
+ context = matcher.resolve(target)
11
+ return not_found(target) unless context
12
+
13
+ name = context.fetch("name")
14
+ config.active_stack.delete(name)
15
+ config.contexts.delete(name)
16
+ config.send(:save!)
17
+ out.puts("Removed #{name}")
18
+ 0
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DevContext
4
+ module Commands
5
+ module Repos
6
+ def cmd_repos
7
+ args = argv.dup
8
+ branch_pattern = nil
9
+ path_pattern = nil
10
+ parser = OptionParser.new
11
+ parser.on("-b", "--branch BRANCHPATTERN", "Filter by branch pattern") { |v| branch_pattern = v }
12
+ parser.on("-p", "--path PATHPATTERN", "Filter by path pattern") { |v| path_pattern = v }
13
+ parser.parse!(args)
14
+ shortcut_pattern = args.shift
15
+ return usage_error("dx repos [-b BRANCHPATTERN] [-p PATHPATTERN] [PATTERN]") unless args.empty?
16
+ path_pattern ||= shortcut_pattern
17
+
18
+ rows = known_repo_rows
19
+
20
+ rows = rows.select { |row| pattern_match?(row[:branch], branch_pattern) }
21
+ rows = rows.select { |row| pattern_match?(row[:path], path_pattern) }
22
+ if rows.empty?
23
+ out.puts("No known repos")
24
+ return 0
25
+ end
26
+
27
+ display_rows = rows.map { |r| r.merge(path: display_path(r[:path])) }.sort_by { |r| r[:path] }
28
+ path_w = [display_rows.map { |r| r[:path].length }.max || 0, "Repo".length].max
29
+ branch_w = [display_rows.map { |r| r[:branch].length }.max || 0, "Branch".length].max
30
+
31
+ out.puts(format("%-#{path_w}s %-#{branch_w}s", "Repo", "Branch"))
32
+ out.puts(format("%-#{path_w}s %-#{branch_w}s", "-" * path_w, "-" * branch_w))
33
+ display_rows.each { |row| out.puts(format("%-#{path_w}s %-#{branch_w}s", row[:path], row[:branch])) }
34
+ 0
35
+ rescue OptionParser::InvalidOption => e
36
+ err.puts("dx: #{e.message}")
37
+ usage_error("dx repos [-b BRANCHPATTERN] [-p PATHPATTERN] [PATTERN]")
38
+ end
39
+
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DevContext
4
+ module Commands
5
+ module SearchHelpers
6
+ private
7
+
8
+ def known_repo_rows
9
+ config.repos.values.map { |repo| repo["path"] }.uniq.filter_map do |path|
10
+ expanded = File.expand_path(path)
11
+ next unless File.directory?(expanded)
12
+
13
+ {
14
+ path: expanded,
15
+ branch: current_branch(expanded) || "-"
16
+ }
17
+ end
18
+ end
19
+
20
+ def known_repo_stash_rows(pattern: nil)
21
+ known_repo_rows.filter_map do |repo|
22
+ entries = stash_entries(repo[:path])
23
+ entries = entries.select { |entry| pattern_match?(entry, pattern) } if pattern
24
+ next if entries.empty?
25
+
26
+ repo.merge(
27
+ count: entries.length,
28
+ entries: entries
29
+ )
30
+ end.sort_by { |row| [-row[:count], row[:path]] }
31
+ end
32
+
33
+ def stash_entries(repo_path)
34
+ output = `git -C "#{repo_path}" stash list 2>/dev/null`
35
+ return [] unless $?.success?
36
+
37
+ output.lines.map(&:chomp).reject(&:empty?)
38
+ end
39
+
40
+ def pattern_match?(value, pattern)
41
+ return true if pattern.nil? || pattern.empty?
42
+
43
+ val = value.to_s
44
+ pat = pattern.to_s
45
+ if pat.match?(/[*?\[]/)
46
+ File.fnmatch?(pat, val, File::FNM_CASEFOLD)
47
+ else
48
+ val.downcase.include?(pat.downcase)
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DevContext
4
+ module Commands
5
+ module Stashes
6
+ def cmd_stashes
7
+ args = argv.dup
8
+ show_list = false
9
+ parser = OptionParser.new
10
+ parser.on("-l", "--list", "Show stash entry titles per repo") { show_list = true }
11
+ parser.parse!(args)
12
+ pattern = args.shift
13
+ return usage_error("dx stashes [--list] [PATTERN]") unless args.empty?
14
+
15
+ rows = known_repo_stash_rows(pattern: pattern)
16
+ if rows.empty?
17
+ out.puts("No repos with stashes")
18
+ return 0
19
+ end
20
+
21
+ render_stash_table(rows)
22
+ return 0 unless show_list
23
+
24
+ out.puts
25
+ rows.each do |row|
26
+ out.puts("#{display_path(row[:path])}:")
27
+ row[:entries].each { |entry| out.puts(" - #{entry}") }
28
+ end
29
+ 0
30
+ rescue OptionParser::InvalidOption => e
31
+ err.puts("dx: #{e.message}")
32
+ usage_error("dx stashes [--list] [PATTERN]")
33
+ end
34
+
35
+ private
36
+
37
+ def render_stash_table(rows)
38
+ display_rows = rows.map { |row| row.merge(path: display_path(row[:path])) }
39
+ path_w = [display_rows.map { |r| r[:path].length }.max || 0, "Path".length].max
40
+ branch_w = [display_rows.map { |r| r[:branch].length }.max || 0, "Branch".length].max
41
+
42
+ out.puts(format("%-#{path_w}s %-#{branch_w}s %s", "Path", "Branch", "Stashes"))
43
+ out.puts(format("%-#{path_w}s %-#{branch_w}s %s", "-" * path_w, "-" * branch_w, "-" * 7))
44
+ display_rows.each do |row|
45
+ out.puts(format("%-#{path_w}s %-#{branch_w}s s:%d", row[:path], row[:branch], row[:count]))
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end