Skip to content

Commit f28f1cc

Browse files
committed
enhance: add latest_revision for writing files to a workspace
This new field acts as an optimistic locking for writing to files. Signed-off-by: Donnie Adams <[email protected]>
1 parent 744b25b commit f28f1cc

File tree

2 files changed

+208
-1
lines changed

2 files changed

+208
-1
lines changed

workspace.go

+32-1
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,13 @@ import (
66
"encoding/json"
77
"fmt"
88
"os"
9+
"regexp"
910
"strings"
1011
"time"
1112
)
1213

14+
var conflictErrParser = regexp.MustCompile(`^.+500 Internal Server Error: conflict: (.+)/([^/]+) \(latest revision: (-?\d+), current revision: (-?\d+)\)$`)
15+
1316
type NotFoundInWorkspaceError struct {
1417
id string
1518
name string
@@ -23,6 +26,29 @@ func newNotFoundInWorkspaceError(id, name string) *NotFoundInWorkspaceError {
2326
return &NotFoundInWorkspaceError{id: id, name: name}
2427
}
2528

29+
type ConflictInWorkspaceError struct {
30+
ID string
31+
Name string
32+
LatestRevision string
33+
CurrentRevision string
34+
}
35+
36+
func parsePossibleConflictInWorkspaceError(err error) error {
37+
if err == nil {
38+
return err
39+
}
40+
41+
matches := conflictErrParser.FindStringSubmatch(err.Error())
42+
if len(matches) != 5 {
43+
return err
44+
}
45+
return &ConflictInWorkspaceError{ID: matches[1], Name: matches[2], LatestRevision: matches[3], CurrentRevision: matches[4]}
46+
}
47+
48+
func (e *ConflictInWorkspaceError) Error() string {
49+
return fmt.Sprintf("conflict: %s/%s (latest revision: %s, current revision: %s)", e.ID, e.Name, e.LatestRevision, e.CurrentRevision)
50+
}
51+
2652
func (g *GPTScript) CreateWorkspace(ctx context.Context, providerType string, fromWorkspaces ...string) (string, error) {
2753
out, err := g.runBasicCommand(ctx, "workspaces/create", map[string]any{
2854
"providerType": providerType,
@@ -123,6 +149,7 @@ func (g *GPTScript) RemoveAll(ctx context.Context, opts ...RemoveAllOptions) err
123149
type WriteFileInWorkspaceOptions struct {
124150
WorkspaceID string
125151
CreateRevision *bool
152+
LatestRevision string
126153
}
127154

128155
func (g *GPTScript) WriteFileInWorkspace(ctx context.Context, filePath string, contents []byte, opts ...WriteFileInWorkspaceOptions) error {
@@ -134,6 +161,9 @@ func (g *GPTScript) WriteFileInWorkspace(ctx context.Context, filePath string, c
134161
if o.CreateRevision != nil {
135162
opt.CreateRevision = o.CreateRevision
136163
}
164+
if o.LatestRevision != "" {
165+
opt.LatestRevision = o.LatestRevision
166+
}
137167
}
138168

139169
if opt.WorkspaceID == "" {
@@ -145,11 +175,12 @@ func (g *GPTScript) WriteFileInWorkspace(ctx context.Context, filePath string, c
145175
"contents": base64.StdEncoding.EncodeToString(contents),
146176
"filePath": filePath,
147177
"createRevision": opt.CreateRevision,
178+
"latestRevision": opt.LatestRevision,
148179
"workspaceTool": g.globalOpts.WorkspaceTool,
149180
"env": g.globalOpts.Env,
150181
})
151182

152-
return err
183+
return parsePossibleConflictInWorkspaceError(err)
153184
}
154185

155186
type DeleteFileInWorkspaceOptions struct {

workspace_test.go

+176
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,92 @@ func TestDisableCreateRevisionsForFileInWorkspace(t *testing.T) {
307307
}
308308
}
309309

310+
func TestConflictsForFileInWorkspace(t *testing.T) {
311+
id, err := g.CreateWorkspace(context.Background(), "directory")
312+
if err != nil {
313+
t.Fatalf("Error creating workspace: %v", err)
314+
}
315+
316+
t.Cleanup(func() {
317+
err := g.DeleteWorkspace(context.Background(), id)
318+
if err != nil {
319+
t.Errorf("Error deleting workspace: %v", err)
320+
}
321+
})
322+
323+
ce := (*ConflictInWorkspaceError)(nil)
324+
// Writing a new file with a non-zero latest revision should fail
325+
err = g.WriteFileInWorkspace(context.Background(), "test.txt", []byte("test0"), WriteFileInWorkspaceOptions{WorkspaceID: id, LatestRevision: "1"})
326+
if err == nil || !errors.As(err, &ce) {
327+
t.Errorf("Expected error writing file with non-zero latest revision: %v", err)
328+
}
329+
330+
err = g.WriteFileInWorkspace(context.Background(), "test.txt", []byte("test0"), WriteFileInWorkspaceOptions{WorkspaceID: id})
331+
if err != nil {
332+
t.Fatalf("Error creating file: %v", err)
333+
}
334+
335+
err = g.WriteFileInWorkspace(context.Background(), "test.txt", []byte("test1"), WriteFileInWorkspaceOptions{WorkspaceID: id})
336+
if err != nil {
337+
t.Fatalf("Error creating file: %v", err)
338+
}
339+
340+
revisions, err := g.ListRevisionsForFileInWorkspace(context.Background(), "test.txt", ListRevisionsForFileInWorkspaceOptions{WorkspaceID: id})
341+
if err != nil {
342+
t.Errorf("Error reading file: %v", err)
343+
}
344+
345+
if len(revisions) != 1 {
346+
t.Errorf("Unexpected number of revisions: %d", len(revisions))
347+
}
348+
349+
// Writing to the file with the latest revision should succeed
350+
err = g.WriteFileInWorkspace(context.Background(), "test.txt", []byte("test2"), WriteFileInWorkspaceOptions{WorkspaceID: id, LatestRevision: revisions[0].RevisionID})
351+
if err != nil {
352+
t.Fatalf("Error creating file: %v", err)
353+
}
354+
355+
revisions, err = g.ListRevisionsForFileInWorkspace(context.Background(), "test.txt", ListRevisionsForFileInWorkspaceOptions{WorkspaceID: id})
356+
if err != nil {
357+
t.Errorf("Error reading file: %v", err)
358+
}
359+
360+
if len(revisions) != 2 {
361+
t.Errorf("Unexpected number of revisions: %d", len(revisions))
362+
}
363+
364+
// Writing to the file with the same revision should fail
365+
err = g.WriteFileInWorkspace(context.Background(), "test.txt", []byte("test3"), WriteFileInWorkspaceOptions{WorkspaceID: id, LatestRevision: revisions[0].RevisionID})
366+
if err == nil || !errors.As(err, &ce) {
367+
t.Errorf("Expected error writing file with same revision: %v", err)
368+
}
369+
370+
err = g.DeleteRevisionForFileInWorkspace(context.Background(), "test.txt", revisions[1].RevisionID, DeleteRevisionForFileInWorkspaceOptions{WorkspaceID: id})
371+
if err != nil {
372+
t.Errorf("Error deleting revision for file: %v", err)
373+
}
374+
375+
revisions, err = g.ListRevisionsForFileInWorkspace(context.Background(), "test.txt", ListRevisionsForFileInWorkspaceOptions{WorkspaceID: id})
376+
if err != nil {
377+
t.Errorf("Error reading file: %v", err)
378+
}
379+
380+
if len(revisions) != 1 {
381+
t.Errorf("Unexpected number of revisions: %d", len(revisions))
382+
}
383+
384+
// Ensure we can write a new file after deleting the latest revision
385+
err = g.WriteFileInWorkspace(context.Background(), "test.txt", []byte("test4"), WriteFileInWorkspaceOptions{WorkspaceID: id, LatestRevision: revisions[0].RevisionID})
386+
if err != nil {
387+
t.Fatalf("Error creating file: %v", err)
388+
}
389+
390+
err = g.DeleteFileInWorkspace(context.Background(), "test.txt", DeleteFileInWorkspaceOptions{WorkspaceID: id})
391+
if err != nil {
392+
t.Errorf("Error deleting file: %v", err)
393+
}
394+
}
395+
310396
func TestLsComplexWorkspace(t *testing.T) {
311397
id, err := g.CreateWorkspace(context.Background(), "directory")
312398
if err != nil {
@@ -690,6 +776,96 @@ func TestRevisionsForFileInWorkspaceS3(t *testing.T) {
690776
}
691777
}
692778

779+
func TestConflictsForFileInWorkspaceS3(t *testing.T) {
780+
if os.Getenv("AWS_ACCESS_KEY_ID") == "" || os.Getenv("AWS_SECRET_ACCESS_KEY") == "" || os.Getenv("WORKSPACE_PROVIDER_S3_BUCKET") == "" {
781+
t.Skip("Skipping test because AWS credentials are not set")
782+
}
783+
784+
id, err := g.CreateWorkspace(context.Background(), "s3")
785+
if err != nil {
786+
t.Fatalf("Error creating workspace: %v", err)
787+
}
788+
789+
t.Cleanup(func() {
790+
err := g.DeleteWorkspace(context.Background(), id)
791+
if err != nil {
792+
t.Errorf("Error deleting workspace: %v", err)
793+
}
794+
})
795+
796+
ce := (*ConflictInWorkspaceError)(nil)
797+
// Writing a new file with a non-zero latest revision should fail
798+
err = g.WriteFileInWorkspace(context.Background(), "test.txt", []byte("test0"), WriteFileInWorkspaceOptions{WorkspaceID: id, LatestRevision: "1"})
799+
if err == nil || !errors.As(err, &ce) {
800+
t.Errorf("Expected error writing file with non-zero latest revision: %v", err)
801+
}
802+
803+
err = g.WriteFileInWorkspace(context.Background(), "test.txt", []byte("test0"), WriteFileInWorkspaceOptions{WorkspaceID: id})
804+
if err != nil {
805+
t.Fatalf("Error creating file: %v", err)
806+
}
807+
808+
err = g.WriteFileInWorkspace(context.Background(), "test.txt", []byte("test1"), WriteFileInWorkspaceOptions{WorkspaceID: id})
809+
if err != nil {
810+
t.Fatalf("Error creating file: %v", err)
811+
}
812+
813+
revisions, err := g.ListRevisionsForFileInWorkspace(context.Background(), "test.txt", ListRevisionsForFileInWorkspaceOptions{WorkspaceID: id})
814+
if err != nil {
815+
t.Errorf("Error reading file: %v", err)
816+
}
817+
818+
if len(revisions) != 1 {
819+
t.Errorf("Unexpected number of revisions: %d", len(revisions))
820+
}
821+
822+
// Writing to the file with the latest revision should succeed
823+
err = g.WriteFileInWorkspace(context.Background(), "test.txt", []byte("test2"), WriteFileInWorkspaceOptions{WorkspaceID: id, LatestRevision: revisions[0].RevisionID})
824+
if err != nil {
825+
t.Fatalf("Error creating file: %v", err)
826+
}
827+
828+
revisions, err = g.ListRevisionsForFileInWorkspace(context.Background(), "test.txt", ListRevisionsForFileInWorkspaceOptions{WorkspaceID: id})
829+
if err != nil {
830+
t.Errorf("Error reading file: %v", err)
831+
}
832+
833+
if len(revisions) != 2 {
834+
t.Errorf("Unexpected number of revisions: %d", len(revisions))
835+
}
836+
837+
// Writing to the file with the same revision should fail
838+
err = g.WriteFileInWorkspace(context.Background(), "test.txt", []byte("test3"), WriteFileInWorkspaceOptions{WorkspaceID: id, LatestRevision: revisions[0].RevisionID})
839+
if err == nil || !errors.As(err, &ce) {
840+
t.Errorf("Expected error writing file with same revision: %v", err)
841+
}
842+
843+
err = g.DeleteRevisionForFileInWorkspace(context.Background(), "test.txt", revisions[1].RevisionID, DeleteRevisionForFileInWorkspaceOptions{WorkspaceID: id})
844+
if err != nil {
845+
t.Errorf("Error deleting revision for file: %v", err)
846+
}
847+
848+
revisions, err = g.ListRevisionsForFileInWorkspace(context.Background(), "test.txt", ListRevisionsForFileInWorkspaceOptions{WorkspaceID: id})
849+
if err != nil {
850+
t.Errorf("Error reading file: %v", err)
851+
}
852+
853+
if len(revisions) != 1 {
854+
t.Errorf("Unexpected number of revisions: %d", len(revisions))
855+
}
856+
857+
// Ensure we can write a new file after deleting the latest revision
858+
err = g.WriteFileInWorkspace(context.Background(), "test.txt", []byte("test4"), WriteFileInWorkspaceOptions{WorkspaceID: id, LatestRevision: revisions[0].RevisionID})
859+
if err != nil {
860+
t.Fatalf("Error creating file: %v", err)
861+
}
862+
863+
err = g.DeleteFileInWorkspace(context.Background(), "test.txt", DeleteFileInWorkspaceOptions{WorkspaceID: id})
864+
if err != nil {
865+
t.Errorf("Error deleting file: %v", err)
866+
}
867+
}
868+
693869
func TestDisableCreatingRevisionsForFileInWorkspaceS3(t *testing.T) {
694870
if os.Getenv("AWS_ACCESS_KEY_ID") == "" || os.Getenv("AWS_SECRET_ACCESS_KEY") == "" || os.Getenv("WORKSPACE_PROVIDER_S3_BUCKET") == "" {
695871
t.Skip("Skipping test because AWS credentials are not set")

0 commit comments

Comments
 (0)