gdkbox 0.1.10 → 0.1.12

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: b71b4f7e72836332b32feca0a6e3e0c485f8255fcfb0f1dc93c7792c3f6fc5ab
4
+ data.tar.gz: 66e0331f211fd1df2b67694e42e65d8971502615b42c61093f86a3a465e06425
5
5
  SHA512:
6
- metadata.gz: 344bba38a105dcf4bf899893d18eb7ef9f82ef03ef6d2229af810346e27f18b24c46e09c02c7f83e406255f79e8d033ff37c159ced868aad284e540c6cf7948c
7
- data.tar.gz: ce33e8b7bd1aa403ea5fe77127b7c46fd1c4eb7079736689cc6447b6b0649bd270b880db82d31a33bb0d090f818366b1e2de847e6b3d70f918dbae257200014a
6
+ metadata.gz: b70d9d914b6ce31f0a644cae9260a98adb3540b0d76555a1ed4e72fc423b9602deb84b338b1b91cf29bf455546b248e875dbfa2e7fdc5dafc9b37322cb2d58be
7
+ data.tar.gz: 73ebbfbfb11732b3d5fb9f40ea0d795a846c61ccb24d513c51684542424d2d78a7eef919e03d5d2007f64df3c3f0704cb75f07dbc3e7b494f90f9ae892daec43
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
@@ -184,15 +184,49 @@ module GDKBox
184
184
  "git@gitlab.com:#{token}.git"
185
185
  end
186
186
 
187
+ # The host an SSH-style git URL connects to, or nil for non-SSH remotes
188
+ # (https needs no SSH client preparation).
189
+ def self.remote_ssh_host(url)
190
+ if (m = url.match(%r{\Assh://(?:[^@/]+@)?([^:/]+)}))
191
+ m[1]
192
+ elsif (m = url.match(/\A(?:[^@:\/]+@)([^:]+):/))
193
+ m[1]
194
+ end
195
+ end
196
+
187
197
  # Point the box's GitLab checkout at a different remote (URL or
188
- # "namespace/project"). Returns the URL that origin now uses.
198
+ # "namespace/project"). SSH remotes get the box's SSH client prepared
199
+ # first (host key acceptance + connection multiplexing) so the follow-up
200
+ # fetch — and later lazy fetches from the treeless clone — succeed.
201
+ # Returns the URL that origin now uses.
189
202
  def set_gitlab_remote!(remote)
190
203
  url = self.class.expand_remote(remote)
191
- Provisioner.new(docker: @docker, config: @config)
192
- .setup_gitlab_remote(container_name, url)
204
+ provisioner = Provisioner.new(docker: @docker, config: @config)
205
+ if (host = self.class.remote_ssh_host(url))
206
+ provisioner.setup_git_ssh(container_name, host)
207
+ end
208
+ provisioner.setup_gitlab_remote(container_name, url)
193
209
  url
194
210
  end
195
211
 
212
+ # Backfill the box's treeless GitLab clone so deep-history operations
213
+ # (old rebases, blame, bisect) work. trees: true fetches only the missing
214
+ # trees (much smaller; file contents stay lazy). Prepares the SSH client
215
+ # first when origin is an SSH remote — a refetch without multiplexing
216
+ # would hit the same throttling the lazy fetches do. Streams git's
217
+ # progress lines to the block.
218
+ def hydrate!(trees: false, &progress)
219
+ provisioner = Provisioner.new(docker: @docker, config: @config)
220
+ origin = @docker.exec(
221
+ container_name, "git remote get-url origin",
222
+ user: @config.ssh_user, workdir: @config.gitlab_checkout_path
223
+ ).stdout.strip
224
+ if (host = self.class.remote_ssh_host(origin))
225
+ provisioner.setup_git_ssh(container_name, host)
226
+ end
227
+ provisioner.hydrate(container_name, trees: trees, &progress)
228
+ end
229
+
196
230
  # Seed a git identity into the box so `git commit` works there. Falls back
197
231
  # to the host's `git config` when name/email are not given; raises when
198
232
  # 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
data/lib/gdkbox/docker.rb CHANGED
@@ -223,6 +223,21 @@ module GDKBox
223
223
  check ? @shell.run!(*cmd, input: input) : @shell.run(*cmd, input: input)
224
224
  end
225
225
 
226
+ # Like #exec, but for long-running scripts whose output should be seen
227
+ # live (e.g. a multi-GB git fetch): runs under a PTY and yields each
228
+ # output line as it appears. Raises CommandError on a non-zero exit.
229
+ def exec_stream(name, script, user: nil, workdir: nil, env: {}, &block)
230
+ cmd = ["docker", "exec"]
231
+ cmd.push("-u", user) if user
232
+ cmd.push("-w", workdir) if workdir
233
+ env.each { |key, value| cmd.push("-e", "#{key}=#{value}") }
234
+ cmd.push(name, "bash", "-lc", script)
235
+ result = @shell.stream_tty(*cmd, &block)
236
+ raise CommandError.new(cmd, result.status, "") unless result.success?
237
+
238
+ result
239
+ end
240
+
226
241
  # Copy a host path into a container at dest (a full destination path).
227
242
  def cp_into(name, src, dest)
228
243
  @shell.run!("docker", "cp", src, "#{name}:#{dest}")
@@ -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
@@ -152,6 +183,27 @@ module GDKBox
152
183
  git fetch --quiet origin
153
184
  BASH
154
185
 
186
+ # Backfills the treeless GitLab clone (the image clones with
187
+ # --filter=tree:0, so operations spanning a large history gap storm the
188
+ # network with lazy fetches and fail; see #7). Two levels:
189
+ #
190
+ # - GDKBOX_HYDRATE_FILTER=blob:none — fetch all missing trees, keep file
191
+ # contents lazy. A fraction of the download; enough for deep rebases,
192
+ # which mostly storm tree fetches.
193
+ # - no filter (full) — fetch everything and drop the promisor config;
194
+ # the repository behaves like a normal full clone afterwards.
195
+ HYDRATE = <<~'BASH'
196
+ set -e
197
+ if [ -n "$GDKBOX_HYDRATE_FILTER" ]; then
198
+ git config remote.origin.partialclonefilter "$GDKBOX_HYDRATE_FILTER"
199
+ git fetch --refetch --progress origin
200
+ else
201
+ git config --unset-all remote.origin.partialclonefilter || true
202
+ git fetch --refetch --progress origin
203
+ git config --unset remote.origin.promisor || true
204
+ fi
205
+ BASH
206
+
155
207
  def initialize(docker:, config:)
156
208
  @docker = docker
157
209
  @config = config
@@ -200,6 +252,26 @@ module GDKBox
200
252
  )
201
253
  end
202
254
 
255
+ # Hydrate the GitLab checkout, streaming git's progress lines to the
256
+ # block. trees: true fetches trees only (blobs stay lazy).
257
+ def hydrate(container_name, trees: false, &block)
258
+ @docker.exec_stream(
259
+ container_name, HYDRATE,
260
+ user: @config.ssh_user,
261
+ workdir: @config.gitlab_checkout_path,
262
+ env: { "GDKBOX_HYDRATE_FILTER" => (trees ? "blob:none" : "") },
263
+ &block
264
+ )
265
+ end
266
+
267
+ def setup_git_ssh(container_name, host)
268
+ @docker.exec(
269
+ container_name, GIT_SSH_SETUP,
270
+ user: @config.ssh_user,
271
+ env: { "GDKBOX_GIT_HOST" => host }
272
+ )
273
+ end
274
+
203
275
  def setup_gitlab_remote(container_name, url)
204
276
  @docker.exec(
205
277
  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.12"
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.12
5
5
  platform: ruby
6
6
  authors:
7
7
  - jotolo