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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +18 -0
- data/DESIGN.md +250 -0
- data/LICENSE.txt +21 -0
- data/README.md +184 -0
- data/bin/console +11 -0
- data/bin/dev_context.rb +6 -0
- data/bin/setup +8 -0
- data/lib/dev_context/cli.rb +103 -0
- data/lib/dev_context/commands/context_lifecycle.rb +80 -0
- data/lib/dev_context/commands/find.rb +123 -0
- data/lib/dev_context/commands/git_ops.rb +163 -0
- data/lib/dev_context/commands/help.rb +81 -0
- data/lib/dev_context/commands/init.rb +22 -0
- data/lib/dev_context/commands/remove.rb +22 -0
- data/lib/dev_context/commands/repos.rb +42 -0
- data/lib/dev_context/commands/search_helpers.rb +53 -0
- data/lib/dev_context/commands/stashes.rb +50 -0
- data/lib/dev_context/commands/status.rb +100 -0
- data/lib/dev_context/commands/support.rb +361 -0
- data/lib/dev_context/config.rb +123 -0
- data/lib/dev_context/matcher.rb +56 -0
- data/lib/dev_context/shell_emitter.rb +61 -0
- data/lib/dev_context/shell_setup.rb +59 -0
- data/lib/dev_context/status.rb +51 -0
- data/lib/dev_context/version.rb +8 -0
- data/lib/dev_context.rb +24 -0
- metadata +116 -0
|
@@ -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
|