dev_context 1.2.1 → 2.0.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 +36 -0
- data/README.md +15 -3
- data/bin/dev_context-rb +6 -0
- data/lib/dev_context/cli.rb +15 -1
- data/lib/dev_context/commands/context_lifecycle.rb +2 -1
- data/lib/dev_context/commands/doctor.rb +7 -1
- data/lib/dev_context/commands/git_ops.rb +94 -15
- data/lib/dev_context/commands/help.rb +6 -0
- data/lib/dev_context/commands/stashes.rb +55 -0
- data/lib/dev_context/shell_emitter.rb +17 -2
- data/lib/dev_context/shell_setup.rb +3 -4
- data/lib/dev_context/version.rb +6 -4
- metadata +18 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a909bb66276763e6e8cf92b56a703f63a7a81637977dd4e2c2ac6a2495ac6a0f
|
|
4
|
+
data.tar.gz: dbf0c96d8324235871af7ffda020f761287d66c980d7d34ae58082c7018ebffa
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d6169e888f0c0d6ec0ad01828ccbf24f1e961616f7ff01d044c63b9bef40cccc03b131a6b77ddae3ad05161c0a6a105545d555ceeec3659c1c3ec43bbcbdb903
|
|
7
|
+
data.tar.gz: 45842d4f6b81a31a2323683313bf11772318e62c45a2ef5fa9df4fa80809cc8e83dfe3f167b4bfe036ae8fe48a6bd7768d89284206698bdd2d767de703055558
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,41 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
### Monorepo tooling (root-level)
|
|
4
|
+
|
|
5
|
+
- Add `CLAUDE.md` with codebase guidance for Claude Code.
|
|
6
|
+
- Add root `Gemfile` proxying to `implementations/ruby/` via `eval_gemfile` so `bundle install` works from repo root.
|
|
7
|
+
- Add root `.rspec` and `spec/` symlinks so `bundle exec rspec` works from repo root without `cd`.
|
|
8
|
+
- Add `ruby-bundle-ready` Makefile prerequisite so `test-ruby`, `install-ruby`, and `release-ruby` ensure Bundler state before running.
|
|
9
|
+
|
|
10
|
+
## [2.0.1] - 2026-06-25
|
|
11
|
+
|
|
12
|
+
- cleaned up erb and updated psych gems to avoid ARM crash on an older version of psych.
|
|
13
|
+
|
|
14
|
+
## [2.0.0] - 2026-06-23
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- Add `dx import` to ingest shell dirstack into `dx` active stack and resynchronize shell stack via eval script.
|
|
19
|
+
- Add optional auto-reconcile gate via `DX_AUTO_IMPORT_SHELL_STACK=true` for stateful commands (`activate|cd`, `pushd`, `popd`).
|
|
20
|
+
- Add shell wrapper transport of live dirstack via `DX_SHELL_STACK` for wrapped stack-aware commands.
|
|
21
|
+
- Add end-to-end coverage in `bash` and `zsh` for mixed shell/dx dirstack mutations and reconciliation.
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- Strengthen `dx`/shell stack interoperability so manual shell `pushd`/`popd` changes can be reconciled by `dx import`.
|
|
26
|
+
- Extend wrapped command handling in shell integration to include `import`.
|
|
27
|
+
|
|
28
|
+
### Breaking Changes
|
|
29
|
+
|
|
30
|
+
- Shell integration semantics now include dirstack payload transport (`DX_SHELL_STACK`) and `import` in wrapped eval paths.
|
|
31
|
+
- Users relying on previous loose coupling between shell dirstack and `dx` active stack should treat this release as a behavior-boundary change.
|
|
32
|
+
|
|
33
|
+
### Migration Notes
|
|
34
|
+
|
|
35
|
+
- Run `dx init` and re-source `~/.dx.sh` after upgrading.
|
|
36
|
+
- Use `dx import` to reconcile shell stack changes into `dx` state on demand.
|
|
37
|
+
- Enable `DX_AUTO_IMPORT_SHELL_STACK=true` only if you want reconciliation to happen automatically before stateful `dx` commands.
|
|
38
|
+
|
|
3
39
|
## [1.1.0] - 2026-05-15
|
|
4
40
|
|
|
5
41
|
- Add `dx doctor` diagnostics command with `dx check` alias.
|
data/README.md
CHANGED
|
@@ -14,7 +14,8 @@ A context is one `(repo_dir, branch)` pair. Context names can be explicit, or im
|
|
|
14
14
|
Install the gem in the current ruby gem path, with executables installed as:
|
|
15
15
|
|
|
16
16
|
- `~/bin/dev_context` (launcher, honors `DX_IMPL`)
|
|
17
|
-
- `~/bin/dev_context-
|
|
17
|
+
- `~/bin/dev_context-rb` (Ruby implementation entrypoint)
|
|
18
|
+
- `~/bin/dev_context-ruby` (legacy Ruby implementation alias)
|
|
18
19
|
|
|
19
20
|
To clone from the github repo:
|
|
20
21
|
|
|
@@ -41,17 +42,28 @@ To support `cd`, `dx` should be wrapped by a shell function that evaluates shell
|
|
|
41
42
|
|
|
42
43
|
```bash
|
|
43
44
|
dx() {
|
|
45
|
+
local bin
|
|
46
|
+
case "${DX_IMPL:-go}" in
|
|
47
|
+
go)
|
|
48
|
+
bin=dev_context-go ;;
|
|
49
|
+
ruby)
|
|
50
|
+
bin=dev_context-rb ;;
|
|
51
|
+
elixir)
|
|
52
|
+
bin=dev_context-ex ;;
|
|
53
|
+
*)
|
|
54
|
+
echo 1>&2 "No idea what $DX_IMPL means!" ; exit 1 ;;
|
|
55
|
+
esac
|
|
44
56
|
case "$1" in
|
|
45
57
|
cd|activate|pushd|popd|pu|po)
|
|
46
58
|
local out
|
|
47
|
-
out="$(DX_SHELL_WRAPPED=1 command
|
|
59
|
+
out="$(DX_SHELL_WRAPPED=1 command $bin "$@")" || return $?
|
|
48
60
|
case "$out" in
|
|
49
61
|
"# DX_SHELL_EVAL"*) eval "$out" ;;
|
|
50
62
|
*) printf "%s\n" "$out" ;;
|
|
51
63
|
esac
|
|
52
64
|
;;
|
|
53
65
|
*)
|
|
54
|
-
command
|
|
66
|
+
command $bin "$@"
|
|
55
67
|
;;
|
|
56
68
|
esac
|
|
57
69
|
}
|
data/bin/dev_context-rb
ADDED
data/lib/dev_context/cli.rb
CHANGED
|
@@ -64,6 +64,8 @@ module DevContext
|
|
|
64
64
|
return cmd_help_topic(command)
|
|
65
65
|
end
|
|
66
66
|
|
|
67
|
+
maybe_auto_import_shell_stack!(command)
|
|
68
|
+
|
|
67
69
|
case command
|
|
68
70
|
when "version" then cmd_version
|
|
69
71
|
when "init" then cmd_init
|
|
@@ -76,6 +78,8 @@ module DevContext
|
|
|
76
78
|
when "deactivate" then cmd_deactivate
|
|
77
79
|
when "doctor", "check" then cmd_doctor
|
|
78
80
|
when "find" then cmd_find
|
|
81
|
+
when "import" then cmd_import
|
|
82
|
+
when "stash" then cmd_stash
|
|
79
83
|
when "stashes" then cmd_stashes
|
|
80
84
|
when "repos" then cmd_repos
|
|
81
85
|
when "status", "wip" then cmd_status(mode: command)
|
|
@@ -97,9 +101,11 @@ module DevContext
|
|
|
97
101
|
attr_reader :argv, :out, :err, :env, :pwd, :stdin, :config, :matcher
|
|
98
102
|
|
|
99
103
|
COMMANDS = %w[
|
|
100
|
-
version init help add clone create remove active deactivate doctor check find repos stashes status wip diff branches br co checkout cd activate pushd popd
|
|
104
|
+
version init help add clone create remove active deactivate doctor check find repos stash stashes status wip diff branches br co checkout cd activate pushd popd import
|
|
101
105
|
].freeze
|
|
102
106
|
|
|
107
|
+
STATEFUL_COMMANDS = %w[activate cd pushd popd].freeze
|
|
108
|
+
|
|
103
109
|
def default_action
|
|
104
110
|
return cmd_init unless config.initialized?
|
|
105
111
|
|
|
@@ -110,5 +116,13 @@ module DevContext
|
|
|
110
116
|
out.puts("dx #{DevContext::VERSION} (impl: ruby)")
|
|
111
117
|
0
|
|
112
118
|
end
|
|
119
|
+
|
|
120
|
+
def maybe_auto_import_shell_stack!(command)
|
|
121
|
+
return unless STATEFUL_COMMANDS.include?(command)
|
|
122
|
+
return unless truthy?(env.fetch("DX_AUTO_IMPORT_SHELL_STACK", "false"))
|
|
123
|
+
return unless shell_stack_imbalanced?
|
|
124
|
+
|
|
125
|
+
cmd_import(emit_script: false)
|
|
126
|
+
end
|
|
113
127
|
end
|
|
114
128
|
end
|
|
@@ -70,7 +70,8 @@ module DevContext
|
|
|
70
70
|
script = ShellEmitter.new(
|
|
71
71
|
context: context,
|
|
72
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"))
|
|
73
|
+
auto_create_local_branch: truthy?(env.fetch("DX_AUTO_CREATE_LOCAL_BRANCH", "true")),
|
|
74
|
+
stack_paths: config.active_contexts.map { |ctx| ctx.fetch("repo_path") }
|
|
74
75
|
).activation_script
|
|
75
76
|
out.write(script)
|
|
76
77
|
0
|
|
@@ -39,7 +39,6 @@ module DevContext
|
|
|
39
39
|
content = File.read(shell_path)
|
|
40
40
|
if content.include?(ShellSetup::MANAGED_MARKER)
|
|
41
41
|
checks << ok_check("shell-script", "Found managed shell integration at #{shell_path}")
|
|
42
|
-
checks << ok_check("shell-managed", "Shell integration is managed by dx init")
|
|
43
42
|
shell_version = extract_shell_script_version(content)
|
|
44
43
|
if shell_version.nil?
|
|
45
44
|
checks << warn_check("shell-version", "Managed shell integration is missing version marker")
|
|
@@ -93,6 +92,8 @@ module DevContext
|
|
|
93
92
|
checks = []
|
|
94
93
|
if missing.empty?
|
|
95
94
|
checks << ok_check("repo-paths", "All known repo paths exist")
|
|
95
|
+
elsif fixture_mode?
|
|
96
|
+
checks << warn_check("repo-paths", "Fixture mode enabled; ignoring missing repo paths: #{missing.join(', ')}")
|
|
96
97
|
else
|
|
97
98
|
checks << fail_check("repo-paths", "Missing repo paths: #{missing.join(', ')}")
|
|
98
99
|
end
|
|
@@ -162,6 +163,11 @@ module DevContext
|
|
|
162
163
|
rescue StandardError
|
|
163
164
|
false
|
|
164
165
|
end
|
|
166
|
+
|
|
167
|
+
def fixture_mode?
|
|
168
|
+
value = env.fetch("DX_DOCTOR_FIXTURE_MODE", "")
|
|
169
|
+
%w[1 true yes on].include?(value.to_s.strip.downcase)
|
|
170
|
+
end
|
|
165
171
|
end
|
|
166
172
|
end
|
|
167
173
|
end
|
|
@@ -3,6 +3,17 @@
|
|
|
3
3
|
module DevContext
|
|
4
4
|
module Commands
|
|
5
5
|
module GitOps
|
|
6
|
+
def cmd_import(emit_script: true)
|
|
7
|
+
imported_paths = parsed_shell_stack_paths
|
|
8
|
+
imported_contexts = import_contexts_for_paths(imported_paths)
|
|
9
|
+
config.active_stack.replace(imported_contexts.map { |ctx| ctx.fetch("name") })
|
|
10
|
+
config.send(:save!)
|
|
11
|
+
return 0 unless emit_script
|
|
12
|
+
|
|
13
|
+
out.write(stack_sync_script_for_active_contexts)
|
|
14
|
+
0
|
|
15
|
+
end
|
|
16
|
+
|
|
6
17
|
def cmd_active
|
|
7
18
|
active = config.active_contexts
|
|
8
19
|
if active.empty?
|
|
@@ -46,7 +57,8 @@ module DevContext
|
|
|
46
57
|
script = ShellEmitter.new(
|
|
47
58
|
context: context,
|
|
48
59
|
remote_name: env.fetch("DX_GIT_REMOTE_NAME", "USE-REPO"),
|
|
49
|
-
auto_create_local_branch: truthy?(env.fetch("DX_AUTO_CREATE_LOCAL_BRANCH", "true"))
|
|
60
|
+
auto_create_local_branch: truthy?(env.fetch("DX_AUTO_CREATE_LOCAL_BRANCH", "true")),
|
|
61
|
+
stack_paths: config.active_contexts.map { |ctx| ctx.fetch("repo_path") }
|
|
50
62
|
).activation_script
|
|
51
63
|
out.write(script)
|
|
52
64
|
0
|
|
@@ -65,7 +77,7 @@ module DevContext
|
|
|
65
77
|
removed = config.deactivate_context!(context.fetch("name"))
|
|
66
78
|
return not_found(target) unless removed
|
|
67
79
|
|
|
68
|
-
|
|
80
|
+
print_pop_message("Popped #{context.fetch('name')}")
|
|
69
81
|
else
|
|
70
82
|
top_name = config.active_stack.first
|
|
71
83
|
if top_name.nil?
|
|
@@ -75,13 +87,10 @@ module DevContext
|
|
|
75
87
|
|
|
76
88
|
context = config.contexts.fetch(top_name)
|
|
77
89
|
config.deactivate_context!(top_name)
|
|
78
|
-
|
|
90
|
+
print_pop_message("Popped #{context.fetch('name')}")
|
|
79
91
|
end
|
|
80
92
|
|
|
81
|
-
|
|
82
|
-
return 0 unless new_top
|
|
83
|
-
|
|
84
|
-
out.puts("cd #{Shellwords.escape(new_top.fetch('repo_path'))}")
|
|
93
|
+
out.write(stack_sync_script_for_active_contexts)
|
|
85
94
|
0
|
|
86
95
|
end
|
|
87
96
|
|
|
@@ -187,6 +196,56 @@ module DevContext
|
|
|
187
196
|
|
|
188
197
|
private
|
|
189
198
|
|
|
199
|
+
def parsed_shell_stack_paths
|
|
200
|
+
seen = {}
|
|
201
|
+
env.fetch("DX_SHELL_STACK", "")
|
|
202
|
+
.lines
|
|
203
|
+
.map(&:strip)
|
|
204
|
+
.reject(&:empty?)
|
|
205
|
+
.map { |path| File.expand_path(path, pwd) }
|
|
206
|
+
.select { |path| git_repo?(path) }
|
|
207
|
+
.reject do |path|
|
|
208
|
+
already_seen = seen[path]
|
|
209
|
+
seen[path] = true
|
|
210
|
+
already_seen
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def import_contexts_for_paths(paths)
|
|
215
|
+
paths.map do |repo_path|
|
|
216
|
+
branch = current_branch(repo_path) || "main"
|
|
217
|
+
context = config.contexts.values.find { |ctx| ctx["repo_path"] == repo_path && ctx["branch"] == branch } ||
|
|
218
|
+
config.contexts.values.find { |ctx| ctx["repo_path"] == repo_path }
|
|
219
|
+
unless context
|
|
220
|
+
name = implicit_context_name(repo_path, branch)
|
|
221
|
+
context_name = ensure_unique_import_context_name(name, repo_path, branch)
|
|
222
|
+
config.add_context(name: context_name, repo_path: repo_path, branch: branch)
|
|
223
|
+
context = config.get_context(context_name)
|
|
224
|
+
end
|
|
225
|
+
context
|
|
226
|
+
end.compact
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def ensure_unique_import_context_name(base_name, repo_path, branch)
|
|
230
|
+
existing = config.contexts[base_name]
|
|
231
|
+
return base_name unless existing
|
|
232
|
+
return base_name if existing["repo_path"] == repo_path && existing["branch"] == branch
|
|
233
|
+
|
|
234
|
+
suffix = 2
|
|
235
|
+
loop do
|
|
236
|
+
candidate = "#{base_name}-#{suffix}"
|
|
237
|
+
match = config.contexts[candidate]
|
|
238
|
+
return candidate unless match
|
|
239
|
+
return candidate if match["repo_path"] == repo_path && match["branch"] == branch
|
|
240
|
+
|
|
241
|
+
suffix += 1
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def shell_stack_imbalanced?
|
|
246
|
+
parsed_shell_stack_paths != config.active_contexts.map { |ctx| File.expand_path(ctx.fetch("repo_path")) }
|
|
247
|
+
end
|
|
248
|
+
|
|
190
249
|
def rotate_active_stack_to(token)
|
|
191
250
|
stack = config.active_stack
|
|
192
251
|
return no_active_contexts if stack.empty?
|
|
@@ -204,11 +263,13 @@ module DevContext
|
|
|
204
263
|
return 1
|
|
205
264
|
end
|
|
206
265
|
|
|
207
|
-
config.
|
|
266
|
+
config.active_stack.replace(stack.rotate(idx))
|
|
267
|
+
config.send(:save!)
|
|
208
268
|
script = ShellEmitter.new(
|
|
209
269
|
context: context,
|
|
210
270
|
remote_name: env.fetch("DX_GIT_REMOTE_NAME", "USE-REPO"),
|
|
211
|
-
auto_create_local_branch: truthy?(env.fetch("DX_AUTO_CREATE_LOCAL_BRANCH", "true"))
|
|
271
|
+
auto_create_local_branch: truthy?(env.fetch("DX_AUTO_CREATE_LOCAL_BRANCH", "true")),
|
|
272
|
+
stack_paths: config.active_contexts.map { |ctx| ctx.fetch("repo_path") }
|
|
212
273
|
).activation_script
|
|
213
274
|
out.write(script)
|
|
214
275
|
0
|
|
@@ -227,12 +288,8 @@ module DevContext
|
|
|
227
288
|
context_name = stack.delete_at(idx)
|
|
228
289
|
context = config.contexts[context_name]
|
|
229
290
|
config.send(:save!)
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
new_top = config.active_contexts.first
|
|
233
|
-
return 0 unless new_top
|
|
234
|
-
|
|
235
|
-
out.puts("cd #{Shellwords.escape(new_top.fetch('repo_path'))}")
|
|
291
|
+
print_pop_message("Popped #{context ? context.fetch('name') : context_name}")
|
|
292
|
+
out.write(stack_sync_script_for_active_contexts)
|
|
236
293
|
0
|
|
237
294
|
end
|
|
238
295
|
|
|
@@ -250,6 +307,28 @@ module DevContext
|
|
|
250
307
|
out.puts("No active contexts")
|
|
251
308
|
0
|
|
252
309
|
end
|
|
310
|
+
|
|
311
|
+
def stack_sync_script_for_active_contexts
|
|
312
|
+
paths = config.active_contexts.map { |ctx| File.expand_path(ctx.fetch("repo_path")) }
|
|
313
|
+
script = +"#{ShellEmitter::SCRIPT_MARKER}\n"
|
|
314
|
+
script << "dirs -c >/dev/null 2>&1 || true\n"
|
|
315
|
+
return script if paths.empty?
|
|
316
|
+
|
|
317
|
+
script << "cd #{Shellwords.escape(paths.first)}\n"
|
|
318
|
+
paths.drop(1).each do |path|
|
|
319
|
+
script << "pushd #{Shellwords.escape(path)} >/dev/null 2>&1\n"
|
|
320
|
+
script << "pushd +1 >/dev/null 2>&1\n"
|
|
321
|
+
end
|
|
322
|
+
script
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def print_pop_message(message)
|
|
326
|
+
if env["DX_SHELL_WRAPPED"] == "1"
|
|
327
|
+
err.puts(message)
|
|
328
|
+
else
|
|
329
|
+
out.puts(message)
|
|
330
|
+
end
|
|
331
|
+
end
|
|
253
332
|
end
|
|
254
333
|
end
|
|
255
334
|
end
|
|
@@ -33,11 +33,13 @@ module DevContext
|
|
|
33
33
|
dx <pattern> # shorthand for: dx find <pattern>
|
|
34
34
|
dx help
|
|
35
35
|
dx init [ruby|go|elixir]
|
|
36
|
+
dx import
|
|
36
37
|
dx version
|
|
37
38
|
dx popd [CONTEXT|+N|-N]
|
|
38
39
|
dx pushd [CONTEXT|PATH|URL|+N|-N]
|
|
39
40
|
dx repos [PATTERN]
|
|
40
41
|
dx remove CONTEXT|PATH
|
|
42
|
+
dx stash [--list] [--all|-a] [PATTERN]
|
|
41
43
|
dx stashes [--list] [PATTERN]
|
|
42
44
|
dx status|wip
|
|
43
45
|
HELP
|
|
@@ -50,6 +52,8 @@ module DevContext
|
|
|
50
52
|
"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."
|
|
51
53
|
when "repos"
|
|
52
54
|
"Usage: dx repos [-b BRANCHPATTERN] [-p PATHPATTERN] [PATTERN]\n\nList all known repos without git status details. PATTERN is shorthand for path filtering."
|
|
55
|
+
when "stash"
|
|
56
|
+
"Usage: dx stash [--list] [--all|-a] [PATTERN]\n\nShow stashes for the active top context by default. Use --all/-a to scan all known repos. Provide PATTERN to filter stash titles; use --list to print matching entries."
|
|
53
57
|
when "stashes"
|
|
54
58
|
"Usage: dx stashes [--list] [PATTERN]\n\nShow repos with stashes. Provide PATTERN to filter stash titles; use --list to print matching entries."
|
|
55
59
|
when "find"
|
|
@@ -72,6 +76,8 @@ module DevContext
|
|
|
72
76
|
"Usage: dx pushd [CONTEXT|PATH|URL|+N|-N]\n\nCONTEXT accepts exact or fuzzy-matched context names. PATH and URL resolve/create contexts. +N/-N select existing active-stack entries (+0 top/left, -0 bottom/right) and rotate that entry to top."
|
|
73
77
|
when "popd"
|
|
74
78
|
"Usage: dx popd [CONTEXT|+N|-N]\n\nWith no args, same as `dx popd +0`. CONTEXT accepts exact or fuzzy-matched context names. +N counts from top/left (+0 top), -N from bottom/right (-0 bottom)."
|
|
79
|
+
when "import"
|
|
80
|
+
"Usage: dx import\n\nImport shell dirstack into dx active_stack (deduped by absolute path) and emit shell sync script."
|
|
75
81
|
when "remove"
|
|
76
82
|
"Usage: dx remove CONTEXT|PATH"
|
|
77
83
|
when "diff"
|
|
@@ -3,6 +3,41 @@
|
|
|
3
3
|
module DevContext
|
|
4
4
|
module Commands
|
|
5
5
|
module Stashes
|
|
6
|
+
def cmd_stash
|
|
7
|
+
args = argv.dup
|
|
8
|
+
show_list = false
|
|
9
|
+
all = false
|
|
10
|
+
parser = OptionParser.new
|
|
11
|
+
parser.on("-l", "--list", "Show stash entry titles per repo") { show_list = true }
|
|
12
|
+
parser.on("-a", "--all", "Show stashes across all known repos") { all = true }
|
|
13
|
+
parser.parse!(args)
|
|
14
|
+
pattern = args.shift
|
|
15
|
+
return usage_error("dx stash [--list] [--all|-a] [PATTERN]") unless args.empty?
|
|
16
|
+
|
|
17
|
+
rows = if all
|
|
18
|
+
known_repo_stash_rows(pattern: pattern)
|
|
19
|
+
else
|
|
20
|
+
active_top_stash_rows(pattern: pattern)
|
|
21
|
+
end
|
|
22
|
+
if rows.empty?
|
|
23
|
+
out.puts("No repos with stashes")
|
|
24
|
+
return 0
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
render_stash_table(rows)
|
|
28
|
+
return 0 unless show_list
|
|
29
|
+
|
|
30
|
+
out.puts
|
|
31
|
+
rows.each do |row|
|
|
32
|
+
out.puts("#{display_path(row[:path])}:")
|
|
33
|
+
row[:entries].each { |entry| out.puts(" - #{entry}") }
|
|
34
|
+
end
|
|
35
|
+
0
|
|
36
|
+
rescue OptionParser::InvalidOption => e
|
|
37
|
+
err.puts("dx: #{e.message}")
|
|
38
|
+
usage_error("dx stash [--list] [--all|-a] [PATTERN]")
|
|
39
|
+
end
|
|
40
|
+
|
|
6
41
|
def cmd_stashes
|
|
7
42
|
args = argv.dup
|
|
8
43
|
show_list = false
|
|
@@ -34,6 +69,26 @@ module DevContext
|
|
|
34
69
|
|
|
35
70
|
private
|
|
36
71
|
|
|
72
|
+
def active_top_stash_rows(pattern: nil)
|
|
73
|
+
context = config.active_contexts.first
|
|
74
|
+
return [] unless context
|
|
75
|
+
|
|
76
|
+
path = context["repo_path"]
|
|
77
|
+
expanded = File.expand_path(path)
|
|
78
|
+
return [] unless File.directory?(expanded)
|
|
79
|
+
|
|
80
|
+
entries = stash_entries(expanded)
|
|
81
|
+
entries = entries.select { |entry| pattern_match?(entry, pattern) } if pattern
|
|
82
|
+
return [] if entries.empty?
|
|
83
|
+
|
|
84
|
+
[{
|
|
85
|
+
path: expanded,
|
|
86
|
+
branch: current_branch(expanded) || "-",
|
|
87
|
+
count: entries.length,
|
|
88
|
+
entries: entries
|
|
89
|
+
}]
|
|
90
|
+
end
|
|
91
|
+
|
|
37
92
|
def render_stash_table(rows)
|
|
38
93
|
display_rows = rows.map { |row| row.merge(path: display_path(row[:path])) }
|
|
39
94
|
path_w = [display_rows.map { |r| r[:path].length }.max || 0, "Path".length].max
|
|
@@ -6,10 +6,11 @@ module DevContext
|
|
|
6
6
|
class ShellEmitter
|
|
7
7
|
SCRIPT_MARKER = "# DX_SHELL_EVAL".freeze
|
|
8
8
|
|
|
9
|
-
def initialize(context:, remote_name:, auto_create_local_branch:)
|
|
9
|
+
def initialize(context:, remote_name:, auto_create_local_branch:, stack_paths: nil)
|
|
10
10
|
@context = context
|
|
11
11
|
@remote_name = remote_name
|
|
12
12
|
@auto_create_local_branch = auto_create_local_branch
|
|
13
|
+
@stack_paths = Array(stack_paths).map { |path| File.expand_path(path) }
|
|
13
14
|
end
|
|
14
15
|
|
|
15
16
|
def activation_script
|
|
@@ -51,11 +52,25 @@ module DevContext
|
|
|
51
52
|
git remote get-url #{forced_remote} >/dev/null 2>&1 || __dx_fail "dx: remote '#{forced_remote}' does not exist in this repo" 19
|
|
52
53
|
git pull --ff-only --autostash #{forced_remote} #{branch} || __dx_fail "dx: pull failed from #{forced_remote}/#{branch} (you are already in the target repo for investigation)" 20
|
|
53
54
|
fi
|
|
55
|
+
#{stack_sync_script}
|
|
54
56
|
SH
|
|
55
57
|
end
|
|
56
58
|
|
|
57
59
|
private
|
|
58
60
|
|
|
59
|
-
attr_reader :context, :remote_name, :auto_create_local_branch
|
|
61
|
+
attr_reader :context, :remote_name, :auto_create_local_branch, :stack_paths
|
|
62
|
+
|
|
63
|
+
def stack_sync_script
|
|
64
|
+
paths = stack_paths
|
|
65
|
+
return "dirs -c >/dev/null 2>&1 || true" if paths.empty?
|
|
66
|
+
|
|
67
|
+
lines = ["dirs -c >/dev/null 2>&1 || true", "cd #{Shellwords.escape(paths.first)} || __dx_fail \"dx: could not cd into #{Shellwords.escape(paths.first)}\" 21"]
|
|
68
|
+
paths.drop(1).each do |path|
|
|
69
|
+
escaped = Shellwords.escape(path)
|
|
70
|
+
lines << "pushd #{escaped} >/dev/null 2>&1 || __dx_fail \"dx: could not pushd #{escaped}\" 22"
|
|
71
|
+
lines << "pushd +1 >/dev/null 2>&1 || __dx_fail \"dx: could not rotate dirstack\" 23"
|
|
72
|
+
end
|
|
73
|
+
lines.join("\n")
|
|
74
|
+
end
|
|
60
75
|
end
|
|
61
76
|
end
|
|
@@ -46,17 +46,16 @@ module DevContext
|
|
|
46
46
|
go)
|
|
47
47
|
bin=dev_context-go ;;
|
|
48
48
|
ruby)
|
|
49
|
-
|
|
49
|
+
bin=dev_context-rb ;;
|
|
50
50
|
elixir)
|
|
51
51
|
bin=dev_context-ex ;;
|
|
52
52
|
*)
|
|
53
53
|
echo 1>&2 "No idea what $DX_IMPL means!" ; exit 1 ;;
|
|
54
54
|
esac
|
|
55
|
-
local out
|
|
56
55
|
case "$1" in
|
|
57
|
-
cd|activate|pushd|popd)
|
|
56
|
+
cd|activate|pushd|popd|pu|po|import)
|
|
58
57
|
local out
|
|
59
|
-
out="$(DX_SHELL_WRAPPED=1 command $bin "$@")" || return $?
|
|
58
|
+
out="$(DX_SHELL_WRAPPED=1 DX_SHELL_STACK="$(dirs -p 2>/dev/null || pwd)" command $bin "$@")" || return $?
|
|
60
59
|
case "$out" in
|
|
61
60
|
"# DX_SHELL_EVAL"*) eval "$out" ;;
|
|
62
61
|
*) printf "%s\\n" "$out" ;;
|
data/lib/dev_context/version.rb
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module DevContext
|
|
4
|
-
SHELL_VERSION = "
|
|
4
|
+
SHELL_VERSION = "2"
|
|
5
5
|
RELEASES = [
|
|
6
|
-
["
|
|
6
|
+
["2.0.1", '2026-06-25'],
|
|
7
|
+
["2.0.0", '2026-06-23'],
|
|
8
|
+
["1.2.0", '2026-05-28'],
|
|
7
9
|
["1.1.0", '2026-05-15'],
|
|
8
|
-
["1.
|
|
10
|
+
["1.0.1", '2026-05-12']
|
|
9
11
|
].freeze
|
|
10
|
-
VERSION = RELEASES
|
|
12
|
+
VERSION = RELEASES[0][0]
|
|
11
13
|
end
|
metadata
CHANGED
|
@@ -1,15 +1,29 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: dev_context
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 2.0.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Alan Stebbens
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-06-26 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: psych
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '5.4'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '5.4'
|
|
13
27
|
- !ruby/object:Gem::Dependency
|
|
14
28
|
name: fuubar
|
|
15
29
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -60,6 +74,7 @@ email:
|
|
|
60
74
|
- aks@stebbens.org
|
|
61
75
|
executables:
|
|
62
76
|
- dev_context
|
|
77
|
+
- dev_context-rb
|
|
63
78
|
- dev_context-ruby
|
|
64
79
|
extensions: []
|
|
65
80
|
extra_rdoc_files: []
|
|
@@ -71,6 +86,7 @@ files:
|
|
|
71
86
|
- README.md
|
|
72
87
|
- bin/console
|
|
73
88
|
- bin/dev_context
|
|
89
|
+
- bin/dev_context-rb
|
|
74
90
|
- bin/dev_context-ruby
|
|
75
91
|
- bin/setup
|
|
76
92
|
- lib/dev_context.rb
|