gdkbox 0.1.10 → 0.1.13

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: 75c5c8ed1f89cb877856f68e45c39f4b5460131ea5a312e5af10e7cb7de2688b
4
- data.tar.gz: a0eb3697bb1b630665be5799d33ffb8bb4aec0999858e075861e3469bd312431
3
+ metadata.gz: 8df6faa5d2c2a7a0fe888195af92305774354b997ef5e0e2d1a7ee61469b60bf
4
+ data.tar.gz: 4c859a064721f68cdadc7a7140dc73b0d3a179a57fe3b538c63699c021115109
5
5
  SHA512:
6
- metadata.gz: 344bba38a105dcf4bf899893d18eb7ef9f82ef03ef6d2229af810346e27f18b24c46e09c02c7f83e406255f79e8d033ff37c159ced868aad284e540c6cf7948c
7
- data.tar.gz: ce33e8b7bd1aa403ea5fe77127b7c46fd1c4eb7079736689cc6447b6b0649bd270b880db82d31a33bb0d090f818366b1e2de847e6b3d70f918dbae257200014a
6
+ metadata.gz: 9ebd61583a5e01145206ba5407e2fbaad1cf456bbfbb040ecc58e66d0d471c3a5566fca4d194467f9dcc988f5540f57f54c73edf96ac5805248a3c34996f4095
7
+ data.tar.gz: fd01809df5f90d1eff5229918f424c311577f15a756ea32328157ebdcaf237e5d1c998a1db87d004a07dbc77be413c4260cc17baaa9db58248f5087cd3488467
data/README.md CHANGED
@@ -92,6 +92,7 @@ Or run it straight from the checkout without installing:
92
92
  | `gdkbox set-host` | Add the `gdk.local` entry to your `/etc/hosts` so box web UIs resolve (`--remove` to undo). Also runs during `up`. |
93
93
  | `gdkbox set-git NAME` | Seed your git identity (`user.name`/`user.email`) into the box so `git commit` works. Also runs during `up`. |
94
94
  | `gdkbox set-remote NAME [REMOTE]` | Point the box's GitLab checkout at a different remote (URL or `namespace/project`, e.g. `gitlab-org/gitlab`); default from config.yml. |
95
+ | `gdkbox hydrate NAME` | Backfill the box's treeless GitLab clone so deep rebases/blame/bisect work (`--trees` for a much smaller trees-only fetch). |
95
96
  | `gdkbox start NAME` | Start a stopped box (and re-enable SSH). |
96
97
  | `gdkbox stop NAME` | Stop a running box. |
97
98
  | `gdkbox rm NAME` | Remove a box: container, metadata, and SSH entry. |
@@ -136,6 +137,23 @@ gdkbox stop demo
136
137
  gdkbox rm demo
137
138
  ```
138
139
 
140
+ ### Working with git history in a box
141
+
142
+ The image's GitLab checkout is a **treeless clone** (`--filter=tree:0`, ~550 MB
143
+ instead of many GB). Day-to-day work near `master` is fine, but operations that
144
+ span a large history gap — rebasing a branch hundreds of commits behind,
145
+ `git blame` or `git bisect` across old ranges — trigger storms of lazy object
146
+ fetches and fail with errors like `could not fetch <sha> from promisor remote`
147
+ (not merge conflicts, even when a rebase reports failure). Either keep branches
148
+ current and do deep-history work elsewhere, or hydrate the box once:
149
+
150
+ ```sh
151
+ gdkbox hydrate demo --trees # fetch missing trees only: much smaller, usually
152
+ # enough for deep rebases (blobs stay lazy)
153
+ gdkbox hydrate demo # full backfill: several GB, then the clone
154
+ # behaves like a normal full clone
155
+ ```
156
+
139
157
  ## Orchestrating a fleet of agents
140
158
 
141
159
  The end goal of `gdkbox` is to back an **orchestrator** that runs a pool of
data/lib/gdkbox/box.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "shellwords"
3
4
  require "time"
4
5
 
5
6
  module GDKBox
@@ -184,15 +185,85 @@ module GDKBox
184
185
  "git@gitlab.com:#{token}.git"
185
186
  end
186
187
 
188
+ # The host an SSH-style git URL connects to, or nil for non-SSH remotes
189
+ # (https needs no SSH client preparation).
190
+ def self.remote_ssh_host(url)
191
+ if (m = url.match(%r{\Assh://(?:[^@/]+@)?([^:/]+)}))
192
+ m[1]
193
+ elsif (m = url.match(/\A(?:[^@:\/]+@)([^:]+):/))
194
+ m[1]
195
+ end
196
+ end
197
+
187
198
  # Point the box's GitLab checkout at a different remote (URL or
188
- # "namespace/project"). Returns the URL that origin now uses.
199
+ # "namespace/project"). SSH remotes get the box's SSH client prepared
200
+ # first (host key acceptance + connection multiplexing) so the follow-up
201
+ # fetch — and later lazy fetches from the treeless clone — succeed.
202
+ # Returns the URL that origin now uses.
189
203
  def set_gitlab_remote!(remote)
190
204
  url = self.class.expand_remote(remote)
191
- Provisioner.new(docker: @docker, config: @config)
192
- .setup_gitlab_remote(container_name, url)
205
+ provisioner = Provisioner.new(docker: @docker, config: @config)
206
+ if (host = self.class.remote_ssh_host(url))
207
+ provisioner.setup_git_ssh(container_name, host)
208
+ end
209
+ provisioner.setup_gitlab_remote(container_name, url)
193
210
  url
194
211
  end
195
212
 
213
+ # The hydrate fetch, run inside the box. The filter is passed to `git
214
+ # fetch` directly and only persisted to config after the fetch succeeds
215
+ # (and, for a full hydrate, the previous filter is put back on failure),
216
+ # so an interrupted hydrate cannot leave the config claiming objects the
217
+ # store does not have (issue #8).
218
+ HYDRATE_SCRIPT = <<~'BASH'
219
+ set -e
220
+ if [ -n "$GDKBOX_HYDRATE_FILTER" ]; then
221
+ git fetch --refetch --progress --filter="$GDKBOX_HYDRATE_FILTER" origin
222
+ git config remote.origin.partialclonefilter "$GDKBOX_HYDRATE_FILTER"
223
+ else
224
+ prev=$(git config --get remote.origin.partialclonefilter || true)
225
+ git config --unset-all remote.origin.partialclonefilter || true
226
+ if ! git fetch --refetch --progress origin; then
227
+ if [ -n "$prev" ]; then git config remote.origin.partialclonefilter "$prev"; fi
228
+ echo "hydrate: fetch failed; previous filter config restored" >&2
229
+ exit 1
230
+ fi
231
+ git config --unset remote.origin.promisor || true
232
+ fi
233
+ BASH
234
+
235
+ # Backfill the box's treeless GitLab clone so deep-history operations
236
+ # (old rebases, blame, bisect) work. trees: true fetches only the missing
237
+ # trees (much smaller; file contents stay lazy).
238
+ #
239
+ # The fetch runs over SSH (`ssh -F <generated config> <alias>`), not
240
+ # `docker exec`: the remote is typically git@gitlab.com, and only an SSH
241
+ # session carries the user's forwarded agent — docker exec has no
242
+ # SSH_AUTH_SOCK, so it cannot authenticate at all (issue #8). The SSH
243
+ # client stanza (host key + multiplexing) is seeded first for SSH
244
+ # origins. Streams git's progress lines to the block; raises
245
+ # CommandError when the fetch fails.
246
+ def hydrate!(trees: false, &progress)
247
+ origin = @docker.exec(
248
+ container_name, "git remote get-url origin",
249
+ user: @config.ssh_user, workdir: @config.gitlab_checkout_path
250
+ ).stdout.strip
251
+ if (host = self.class.remote_ssh_host(origin))
252
+ Provisioner.new(docker: @docker, config: @config)
253
+ .setup_git_ssh(container_name, host)
254
+ end
255
+
256
+ filter = trees ? "blob:none" : ""
257
+ remote_cmd = "cd #{Shellwords.escape(@config.gitlab_checkout_path)} && " \
258
+ "GDKBOX_HYDRATE_FILTER=#{Shellwords.escape(filter)} " \
259
+ "bash -c #{Shellwords.escape(HYDRATE_SCRIPT)}"
260
+ argv = ["ssh", "-F", @config.ssh_config_path, ssh_host_alias, remote_cmd]
261
+ result = @shell.stream_tty(*argv, &progress)
262
+ raise CommandError.new(argv, result.status, "") unless result.success?
263
+
264
+ result
265
+ end
266
+
196
267
  # Seed a git identity into the box so `git commit` works there. Falls back
197
268
  # to the host's `git config` when name/email are not given; raises when
198
269
  # neither source has anything to seed. Returns the [name, email] seeded.
data/lib/gdkbox/cli.rb CHANGED
@@ -288,6 +288,42 @@ module GDKBox
288
288
  end
289
289
  map "set-remote" => :set_remote
290
290
 
291
+ desc "hydrate NAME", "Backfill the box's treeless GitLab clone (deep rebases, blame, bisect)"
292
+ long_desc <<~DESC
293
+ The GDK-in-a-box image clones GitLab treeless (--filter=tree:0), so
294
+ operations spanning a large history gap — rebasing a branch far behind
295
+ master, blame or bisect across old ranges — storm the network with lazy
296
+ fetches and fail. Hydrating backfills the missing objects once.
297
+
298
+ By default everything is fetched and the clone becomes a normal full
299
+ clone (a large one-time download, likely several GB). With --trees only
300
+ the missing trees are fetched — a fraction of the size, and usually
301
+ enough for deep rebases; file contents are still fetched lazily as
302
+ needed. Progress is streamed live.
303
+ DESC
304
+ option :trees, type: :boolean, default: false,
305
+ desc: "Fetch missing trees only (much smaller; blobs stay lazy)"
306
+ option :force, type: :boolean, default: false, desc: "Skip confirmation"
307
+ def hydrate(name)
308
+ box = load_box!(name)
309
+ unless options[:force]
310
+ estimate = options[:trees] ? "a sizeable download" : "a large download, likely several GB"
311
+ return unless yes?("Hydrate '#{name}' (#{estimate})? [y/N]")
312
+ end
313
+
314
+ say "Hydrating '#{name}' (#{options[:trees] ? 'trees only' : 'full'})...", :green
315
+ tty = $stderr.tty?
316
+ box.hydrate!(trees: options[:trees]) do |line|
317
+ if tty
318
+ $stderr.print("\r\e[K#{line}")
319
+ elsif !line.include?("%") # skip per-percent spam when piped
320
+ $stderr.puts(line)
321
+ end
322
+ end
323
+ $stderr.print("\n") if tty
324
+ say "Done. '#{name}' can now work across the full history.", :green
325
+ end
326
+
291
327
  desc "set-host", "Add (default) or remove the gdk.local entry in /etc/hosts"
292
328
  long_desc <<~DESC
293
329
  GDK generates URLs and redirects that use the `gdk.local` hostname, so
@@ -475,10 +511,11 @@ module GDKBox
475
511
  url = box.set_gitlab_remote!(remote)
476
512
  say "Pointed the GitLab checkout at #{url}.", :green unless options[:json]
477
513
  rescue StandardError => e
478
- unless options[:json]
479
- say "Could not repoint the GitLab checkout: #{e.message}", :yellow
480
- say " Retry later with: gdkbox set-remote #{box.name} #{remote}", :yellow
481
- end
514
+ # Warn on stderr even in --json mode: stdout stays clean JSON, but a
515
+ # silently failed repoint costs the user a debugging session later
516
+ # (the box looks fine until the first fetch).
517
+ warn "gdkbox: could not repoint the GitLab checkout: #{e.message}"
518
+ warn "gdkbox: retry later with: gdkbox set-remote #{box.name} #{remote}"
482
519
  end
483
520
 
484
521
  # Ensure gdk.local resolves on the host so the box's web UI is reachable
@@ -11,8 +11,8 @@ module GDKBox
11
11
  SHELLS = %w[bash zsh].freeze
12
12
 
13
13
  # Subcommands whose first positional argument is an existing box.
14
- BOX_COMMANDS = %w[status dispatch ssh code install-agent set-key set-git set-remote start
15
- stop rm add-skill].freeze
14
+ BOX_COMMANDS = %w[status dispatch ssh code install-agent set-key set-git set-remote hydrate
15
+ start stop rm add-skill].freeze
16
16
 
17
17
  def initialize(cli_class = CLI)
18
18
  @cli = cli_class
@@ -141,6 +141,37 @@ module GDKBox
141
141
  true
142
142
  BASH
143
143
 
144
+ # Prepares the box's SSH client for real git traffic against a remote
145
+ # host (issue #6, hit when a gitlab_remote is configured):
146
+ #
147
+ # - `StrictHostKeyChecking accept-new`: a fresh box has an empty
148
+ # known_hosts, so the first fetch would die on host key verification.
149
+ # - Connection multiplexing: lazy fetches from the treeless clone open
150
+ # SSH connections in bursts; gitlab.com throttles that, which surfaces
151
+ # as a misleading "Permission denied (publickey)". One shared control
152
+ # connection absorbs the burst.
153
+ #
154
+ # The stanza is tagged per host, so re-runs are no-ops and different
155
+ # remotes each get their own block.
156
+ GIT_SSH_SETUP = <<~'BASH'
157
+ set -e
158
+ mkdir -p "$HOME/.ssh" && chmod 700 "$HOME/.ssh"
159
+ config="$HOME/.ssh/config"
160
+ touch "$config" && chmod 600 "$config"
161
+ if ! grep -qF "gdkbox:git-ssh $GDKBOX_GIT_HOST" "$config"; then
162
+ cat >> "$config" <<EOF
163
+
164
+ # gdkbox:git-ssh $GDKBOX_GIT_HOST
165
+ Host $GDKBOX_GIT_HOST
166
+ StrictHostKeyChecking accept-new
167
+ ControlMaster auto
168
+ ControlPath ~/.ssh/cm-%r@%h-%p
169
+ ControlPersist 20m
170
+ ServerAliveInterval 30
171
+ EOF
172
+ fi
173
+ BASH
174
+
144
175
  # Points the GitLab checkout's origin at a different repository. The image
145
176
  # hardcodes the community mirror (gitlab-community/gitlab-org/gitlab);
146
177
  # team members typically want the canonical gitlab-org/gitlab. The mirror
@@ -200,6 +231,14 @@ module GDKBox
200
231
  )
201
232
  end
202
233
 
234
+ def setup_git_ssh(container_name, host)
235
+ @docker.exec(
236
+ container_name, GIT_SSH_SETUP,
237
+ user: @config.ssh_user,
238
+ env: { "GDKBOX_GIT_HOST" => host }
239
+ )
240
+ end
241
+
203
242
  def setup_gitlab_remote(container_name, url)
204
243
  @docker.exec(
205
244
  container_name, GITLAB_REMOTE_SETUP,
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module GDKBox
4
- VERSION = "0.1.10"
4
+ VERSION = "0.1.13"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gdkbox
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.10
4
+ version: 0.1.13
5
5
  platform: ruby
6
6
  authors:
7
7
  - jotolo