workspaces 0.1.13 → 0.1.14
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/README.md +36 -0
- data/lib/workspaces/recipe.rb +33 -2
- data/lib/workspaces/version.rb +1 -1
- data/lib/workspaces/worktree.rb +10 -4
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 160b209187a95a674c6ce4936b4b2863ed5893ac8331f1f0345d550cf09f2f2d
|
|
4
|
+
data.tar.gz: ba7ef0dd3ed8c913ea29683f641c1d4ac314ee15a1c201dd45634d45a8a980bc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 978a8f06eda4d6d9c0c443499f1cf28f41c33800b3394b40fb56dd8771e0f1283001a4e24effcb3f7c52563023a18e1f48bfe884f97bd255642ff9379b9ea095
|
|
7
|
+
data.tar.gz: 8162b0c59925b414a1af0ff511fcc6bee2d79f83bb42d452614ef68f79c7ee52e42cb89531c21c5e77b29bc76b76cdfea4f4b5fff73fff2217b49217a0321a2c
|
data/README.md
CHANGED
|
@@ -39,6 +39,42 @@ The returned JSON includes the workspace ID, dashboard URL, and preview URL. You
|
|
|
39
39
|
create from a pull request with `create --pr NUMBER`, or create a new branch with
|
|
40
40
|
`create --new-branch NAME --from REF`. Run `help` for lifecycle and other commands.
|
|
41
41
|
|
|
42
|
+
### Git LFS
|
|
43
|
+
|
|
44
|
+
If the project keeps large files in [Git LFS](https://git-lfs.com), a new workspace does not
|
|
45
|
+
copy them. Creating a workspace (and fast-forwarding a PR workspace to a newer head) leaves
|
|
46
|
+
those files as small pointer files, and `git status` stays clean. Without this, every
|
|
47
|
+
workspace would hold a full second copy of every LFS file, even though the source
|
|
48
|
+
checkout's LFS store is already shared.
|
|
49
|
+
|
|
50
|
+
List the paths a workspace actually needs in `.workspaces.yml`:
|
|
51
|
+
|
|
52
|
+
```yaml
|
|
53
|
+
lfs:
|
|
54
|
+
fetch:
|
|
55
|
+
- eval/amc/images/golden/**
|
|
56
|
+
- spec/fixtures/images/**
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
That adds a first step, **Fetch Git LFS files**, which runs
|
|
60
|
+
`git lfs pull --include=<paths>` in the workspace. It appears in the step list with its own
|
|
61
|
+
log, time and place in the progress estimate like any other step, and it runs on `start` and
|
|
62
|
+
`prepare` but not on `restart`. Paths are repository-relative patterns; absolute paths,
|
|
63
|
+
`..`, a leading `-`, commas and control characters are rejected when the recipe loads.
|
|
64
|
+
Omit `lfs` and nothing changes except that files stay as pointers.
|
|
65
|
+
|
|
66
|
+
A few things worth knowing:
|
|
67
|
+
|
|
68
|
+
- Files are fetched from the objects the source repository already has when it can, and from
|
|
69
|
+
the remote otherwise. If neither works (no network, or no credentials for LFS) the step
|
|
70
|
+
fails and the workspace reports it, rather than silently leaving pointers.
|
|
71
|
+
- A file that is still a pointer reads as a ~130 byte text file starting with
|
|
72
|
+
`version https://git-lfs`. If a test or build reads an LFS file, add its path to
|
|
73
|
+
`lfs.fetch`.
|
|
74
|
+
- To fetch more by hand in an existing workspace:
|
|
75
|
+
`workspaces exec <id> -- git lfs pull --include='path/**'`.
|
|
76
|
+
- Workspaces created before this behavior keep their real files; nothing is converted.
|
|
77
|
+
|
|
42
78
|
### Step timings
|
|
43
79
|
|
|
44
80
|
Per-step durations are persisted in seconds in workspace step status JSON, so they
|
data/lib/workspaces/recipe.rb
CHANGED
|
@@ -7,7 +7,9 @@ module Workspaces
|
|
|
7
7
|
class Invalid < ArgumentError; end
|
|
8
8
|
RESERVED_ENV = %w(WORKSPACE_ID WORKSPACE_PATH WORKSPACE_REPO_ROOT WORKSPACE_PORT WORKSPACE_URL
|
|
9
9
|
WORKSPACE_HOST WORKSPACE_CACHE_DIR WORKSPACE_RUN_ID).freeze
|
|
10
|
-
|
|
10
|
+
LFS_STEP_ID = 'lfs'.freeze
|
|
11
|
+
LFS_STEP_NAME = 'Fetch Git LFS files'.freeze
|
|
12
|
+
attr_reader :steps, :environment, :default_editable_env, :environment_presets, :lfs_fetch
|
|
11
13
|
|
|
12
14
|
def initialize(root: Config.repo_root)
|
|
13
15
|
@root = root
|
|
@@ -16,7 +18,9 @@ module Workspaces
|
|
|
16
18
|
@default_editable_env = validate_editable_env!(document.fetch('default_editable_env', {}))
|
|
17
19
|
@environment_presets = validate_environment_presets!(document.fetch('environment_presets', {}))
|
|
18
20
|
@environment = validate_env!(document.fetch('env', {}))
|
|
21
|
+
@lfs_fetch = validate_lfs!(document.fetch('lfs', {}))
|
|
19
22
|
@steps = document.fetch('steps').map { |step| validate_step!(step) }
|
|
23
|
+
@steps = [lfs_step] + @steps unless lfs_fetch.empty?
|
|
20
24
|
validate_editable_overlap!
|
|
21
25
|
validate_sequence!
|
|
22
26
|
rescue Errno::ENOENT
|
|
@@ -29,6 +33,13 @@ module Workspaces
|
|
|
29
33
|
restart ? steps.drop(steps.index { |step| step['background'] }) : steps
|
|
30
34
|
end
|
|
31
35
|
|
|
36
|
+
# Files fetched into the workspace come from the LFS objects the source repository already
|
|
37
|
+
# has, then are linked in by `git lfs pull`; the source's store is shared by every workspace.
|
|
38
|
+
def lfs_step
|
|
39
|
+
{ 'id' => LFS_STEP_ID, 'name' => LFS_STEP_NAME, 'timeout' => 600, 'env' => {}, 'background' => false,
|
|
40
|
+
'run' => ['git', 'lfs', 'pull', "--include=#{lfs_fetch.join(',')}"] }
|
|
41
|
+
end
|
|
42
|
+
|
|
32
43
|
def command(step, context)
|
|
33
44
|
command = step.fetch('run')
|
|
34
45
|
return ['sh', '-c', command] if command.is_a?(String)
|
|
@@ -50,7 +61,7 @@ module Workspaces
|
|
|
50
61
|
raise Invalid,
|
|
51
62
|
'Configuration must be a mapping with version: 1'
|
|
52
63
|
end
|
|
53
|
-
unknown_keys!(document, %w(version env default_editable_env environment_presets steps))
|
|
64
|
+
unknown_keys!(document, %w(version env default_editable_env environment_presets lfs steps))
|
|
54
65
|
raise Invalid, 'steps must be a nonempty array' unless document['steps'].is_a?(Array) && !document['steps'].empty?
|
|
55
66
|
end
|
|
56
67
|
|
|
@@ -100,6 +111,26 @@ module Workspaces
|
|
|
100
111
|
'timeout applies to foreground steps, not the server lifetime'
|
|
101
112
|
end
|
|
102
113
|
|
|
114
|
+
# `lfs: { fetch: [path, ...] }` names the Git LFS files a workspace needs. Everything else
|
|
115
|
+
# stays a pointer. Paths are repository-relative globs handed to `git lfs pull --include`, so
|
|
116
|
+
# they are checked for the things that would make that unsafe or surprising.
|
|
117
|
+
def validate_lfs!(value)
|
|
118
|
+
raise Invalid, 'lfs must be a mapping' unless value.is_a?(Hash)
|
|
119
|
+
|
|
120
|
+
unknown_keys!(value, %w(fetch))
|
|
121
|
+
paths = value.fetch('fetch', [])
|
|
122
|
+
raise Invalid, 'lfs.fetch must be an array of paths' unless paths.is_a?(Array)
|
|
123
|
+
|
|
124
|
+
paths.each do |path|
|
|
125
|
+
unless path.is_a?(String) && !path.strip.empty? && path == path.strip
|
|
126
|
+
raise Invalid, 'lfs.fetch paths must be nonempty trimmed strings'
|
|
127
|
+
end
|
|
128
|
+
raise Invalid, 'lfs.fetch paths cannot contain commas or control characters' if path.match?(/[,[:cntrl:]]/)
|
|
129
|
+
raise Invalid, 'lfs.fetch paths must be relative to the repository' if path.start_with?('/', '-') || path.split('/').include?('..')
|
|
130
|
+
end
|
|
131
|
+
paths.uniq
|
|
132
|
+
end
|
|
133
|
+
|
|
103
134
|
def validate_env!(values)
|
|
104
135
|
raise Invalid, 'env must be a mapping' unless values.is_a?(Hash)
|
|
105
136
|
values.each do |key, value|
|
data/lib/workspaces/version.rb
CHANGED
data/lib/workspaces/worktree.rb
CHANGED
|
@@ -12,6 +12,12 @@ module Workspaces
|
|
|
12
12
|
class UpdateError < StandardError; end
|
|
13
13
|
AUTH_FAILURE = Regexp.union(/could not read (?:Username|Password)|terminal prompts disabled|Authentication failed/i,
|
|
14
14
|
/Permission denied \(publickey\)|gh auth login|HTTP 401|HTTP 403/i)
|
|
15
|
+
# A project that keeps large files in Git LFS would otherwise copy every one of them into
|
|
16
|
+
# every workspace at checkout. With smudging skipped they arrive as small pointer files and
|
|
17
|
+
# `git status` stays clean, so nothing downloads until a recipe asks for it (`lfs.fetch`).
|
|
18
|
+
# Applied only where the gem writes files from Git itself; it is not applied to stash or
|
|
19
|
+
# apply, which must never swap a real file someone fetched for its pointer.
|
|
20
|
+
LFS_POINTERS_ONLY = { 'GIT_LFS_SKIP_SMUDGE' => '1' }.freeze
|
|
15
21
|
METADATA = '.workspace.json'.freeze
|
|
16
22
|
PR_METADATA = '.related-pr'.freeze
|
|
17
23
|
attr_reader :id, :path
|
|
@@ -107,7 +113,7 @@ module Workspaces
|
|
|
107
113
|
|
|
108
114
|
stash = save_changes
|
|
109
115
|
begin
|
|
110
|
-
git!('-c', 'core.hooksPath=/dev/null', 'merge', '--ff-only', target, cwd: path)
|
|
116
|
+
git!('-c', 'core.hooksPath=/dev/null', 'merge', '--ff-only', target, cwd: path, env: LFS_POINTERS_ONLY)
|
|
111
117
|
rescue CommandFailedError
|
|
112
118
|
restore_changes(stash) if stash
|
|
113
119
|
raise UpdateError, 'Could not fast-forward the workspace; local changes were restored'
|
|
@@ -117,8 +123,8 @@ module Workspaces
|
|
|
117
123
|
{ 'updated' => true, 'head' => target }
|
|
118
124
|
end
|
|
119
125
|
|
|
120
|
-
def git!(*args, cwd: Config.repo_root)
|
|
121
|
-
output, status = Open3.capture2e(git_environment, 'git', *args, chdir: cwd.to_s)
|
|
126
|
+
def git!(*args, cwd: Config.repo_root, env: {})
|
|
127
|
+
output, status = Open3.capture2e(git_environment.merge(env), 'git', *args, chdir: cwd.to_s)
|
|
122
128
|
raise_authentication_error!(output) unless status.success?
|
|
123
129
|
raise CommandFailedError, "git #{args.join(' ')} failed:\n#{output}" unless status.success?
|
|
124
130
|
|
|
@@ -203,7 +209,7 @@ module Workspaces
|
|
|
203
209
|
raise ArgumentError, 'Workspace branch must not start with a dash' if target_branch.start_with?('-')
|
|
204
210
|
|
|
205
211
|
Config.worktrees_dir.mkpath
|
|
206
|
-
git!('-c', 'core.hooksPath=/dev/null', 'worktree', 'add', '-b', target_branch, path.to_s, sha)
|
|
212
|
+
git!('-c', 'core.hooksPath=/dev/null', 'worktree', 'add', '-b', target_branch, path.to_s, sha, env: LFS_POINTERS_ONLY)
|
|
207
213
|
end
|
|
208
214
|
|
|
209
215
|
def initialize_worktree!(source, target_branch)
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: workspaces
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.14
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Workspaces contributors
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-10-
|
|
11
|
+
date: 2026-10-21 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rack
|