Repository: treeverse/lakeFS
Pull request #8777: Hooks: Lua  stat_object get metadata
Author: login: nopcoder
Labels: name: area/hooks
description: improvements or additions to the hooks subsystem
name: include-changelog
description: PR description should be included in next release changelog
Created: 2025-03-09T10:02:13Z | Merged: 2025-03-11T12:27:13Z

- Support get user metadata as part of stat_object
- Extract methods to reduce code complexity

Close https://github.com/treeverse/lakeFS/issues/8787



Diff:
diff --git a/docs/howto/hooks/lua.md b/docs/howto/hooks/lua.md
index 8f1b5a5f2b6..ce3c9f051f6 100644
--- a/docs/howto/hooks/lua.md
+++ b/docs/howto/hooks/lua.md
@@ -516,9 +516,36 @@ Returns 2 values:
 
 Returns an object-wise diff of uncommitted changes on `branch_id`.
 
-### `lakefs/stat_object(repository_id, ref_id, path)`
+### `lakefs/stat_object(repository_id, ref_id, path[, user_metadata])`
 
 Returns a stat object for the given path under the given reference and repository.
+Returns 2 values:
+
+1. The HTTP status code returned by the lakeFS API
+2. The stat response object as a JSON string
+
+Parameters:
+
+- `repository_id`: The repository ID
+- `ref_id`: The reference to stat from (branch, tag, commit ID)
+- `path`: Path to the object to stat
+- `user_metadata`: (Optional) Boolean flag to include user metadata in response
+
+### `lakefs/update_object_user_metadata(repository_id, branch_id, path, metadata)`
+
+Update user metadata for an object.
+
+Parameters:
+
+- `repository_id`: The repository ID
+- `branch_id`: The branch containing the object
+- `path`: Path to the object to update
+- `metadata`: A table containing key-value pairs to set as user metadata
+
+Returns 2 values:
+
+1. The HTTP status code returned by the lakeFS API (204 on success)
+2. Empty on success, error on failure
 
 ### `lakefs/catalogexport/glue_exporter.get_full_table_name(descriptor, action_info)`
 
diff --git a/pkg/actions/lua/lakefs/client.go b/pkg/actions/lua/lakefs/client.go
index 543033cba07..24178396814 100644
--- a/pkg/actions/lua/lakefs/client.go
+++ b/pkg/actions/lua/lakefs/client.go
@@ -9,6 +9,7 @@ import (
 	"net/http"
 	"net/http/httptest"
 	"net/url"
+	"strconv"
 	"strings"
 
 	"github.com/Shopify/go-lua"
@@ -70,196 +71,209 @@ func getLakeFSJSONResponse(l *lua.State, server *http.Server, request *http.Requ
 	rr := httptest.NewRecorder()
 	server.Handler.ServeHTTP(rr, request)
 	l.PushInteger(rr.Code)
+	if rr.Body.Len() == 0 {
+		return 1
+	}
 
-	var output interface{}
+	var output any
 	check(l, json.Unmarshal(rr.Body.Bytes(), &output))
 	return 1 + util.DeepPush(l, output)
 }
 
-func OpenClient(l *lua.State, ctx context.Context, user *model.User, server *http.Server) {
-	clientOpen := func(l *lua.State) int {
-		lua.NewLibrary(l, []lua.RegistryFunction{
-			{Name: "update_object_user_metadata", Function: func(state *lua.State) int {
-				repo := lua.CheckString(l, 1)
-				branch := lua.CheckString(l, 2)
-				objPath := lua.CheckString(l, 3)
-				metadata, err := util.PullStringTable(l, 4)
-				check(l, err)
+// updateObjectUserMetadata handles updating object metadata
+func updateObjectUserMetadata(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	branch := lua.CheckString(l, 2)
+	objPath := lua.CheckString(l, 3)
+	metadata, err := util.PullStringTable(l, 4)
+	check(l, err)
 
-				data, err := json.Marshal(map[string]interface{}{
-					"set": metadata,
-				})
-				check(l, err)
+	data, err := json.Marshal(map[string]any{
+		"set": metadata,
+	})
+	check(l, err)
 
-				reqURL, err := url.JoinPath("/repositories", repo, "branches", branch, "objects/stat/user_metadata")
-				check(l, err)
+	reqURL, err := url.JoinPath("/repositories", repo, "branches", branch, "objects/stat/user_metadata")
+	check(l, err)
 
-				req, err := newLakeFSJSONRequest(ctx, user, http.MethodPut, reqURL, data)
-				check(l, err)
+	req, err := newLakeFSJSONRequest(ctx, user, http.MethodPut, reqURL, data)
+	check(l, err)
+
+	// query params
+	q := req.URL.Query()
+	q.Add("path", objPath)
+	req.URL.RawQuery = q.Encode()
+
+	return getLakeFSJSONResponse(l, server, req)
+}
+
+// createTag handles tag creation
+func createTag(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	data, err := json.Marshal(map[string]string{
+		"ref": lua.CheckString(l, 2),
+		"id":  lua.CheckString(l, 3),
+	})
+	check(l, err)
+	reqURL, err := url.JoinPath("/repositories", repo, "tags")
+	check(l, err)
+	req, err := newLakeFSJSONRequest(ctx, user, http.MethodPost, reqURL, data)
+	check(l, err)
+	return getLakeFSJSONResponse(l, server, req)
+}
 
-				// query params
-				q := req.URL.Query()
-				q.Add("path", objPath)
-				req.URL.RawQuery = q.Encode()
+// diffRefs handles comparing two refs
+func diffRefs(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	leftRef := lua.CheckString(l, 2)
+	rightRef := lua.CheckString(l, 3)
+	reqURL, err := url.JoinPath("/repositories", repo, "refs", leftRef, "diff", rightRef)
+	check(l, err)
+	req, err := newLakeFSJSONRequest(ctx, user, http.MethodGet, reqURL, nil)
+	check(l, err)
+	// query params
+	q := req.URL.Query()
+	if !l.IsNone(4) {
+		q.Add("after", lua.CheckString(l, 4))
+	}
+	if !l.IsNone(5) {
+		q.Add("prefix", lua.CheckString(l, 5))
+	}
+	if !l.IsNone(6) {
+		q.Add("delimiter", lua.CheckString(l, 6))
+	}
+	if !l.IsNone(7) {
+		q.Add("amount", fmt.Sprintf("%d", lua.CheckInteger(l, 7)))
+	}
+	req.URL.RawQuery = q.Encode()
+	return getLakeFSJSONResponse(l, server, req)
+}
 
-				rr := httptest.NewRecorder()
-				server.Handler.ServeHTTP(rr, req)
-				l.PushInteger(rr.Code)
-				l.PushString(rr.Body.String())
-				return 2
+// listObjects handles listing objects in a ref
+func listObjects(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	ref := lua.CheckString(l, 2)
+	reqURL, err := url.JoinPath("/repositories", repo, "refs", ref, "objects/ls")
+	check(l, err)
+	req, err := newLakeFSJSONRequest(ctx, user, http.MethodGet, reqURL, nil)
+	check(l, err)
+	// query params
+	q := req.URL.Query()
+	if !l.IsNone(3) {
+		q.Add("after", lua.CheckString(l, 3))
+	}
+	if !l.IsNone(4) {
+		q.Add("prefix", lua.CheckString(l, 4))
+	}
+	if !l.IsNone(5) {
+		q.Add("delimiter", lua.CheckString(l, 5))
+	}
+	if !l.IsNone(6) {
+		q.Add("amount", fmt.Sprintf("%d", lua.CheckInteger(l, 6)))
+	}
+	if !l.IsNone(7) {
+		withUserMetadata := "false"
+		if l.ToBoolean(7) {
+			withUserMetadata = "true"
+		}
+		q.Add("user_metadata", withUserMetadata)
+	}
+	req.URL.RawQuery = q.Encode()
+	return getLakeFSJSONResponse(l, server, req)
+}
+
+// getObject handles retrieving an object
+func getObject(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	ref := lua.CheckString(l, 2)
+	reqURL, err := url.JoinPath("/repositories", repo, "refs", ref, "objects")
+	check(l, err)
+	req, err := newLakeFSJSONRequest(ctx, user, http.MethodGet, reqURL, nil)
+	check(l, err)
+	// query params
+	q := req.URL.Query()
+	q.Add("path", lua.CheckString(l, 3))
+	req.URL.RawQuery = q.Encode()
+	rr := httptest.NewRecorder()
+	server.Handler.ServeHTTP(rr, req)
+	l.PushInteger(rr.Code)
+	l.PushString(rr.Body.String())
+	return 2
+}
+
+// statObject handles retrieving object stats
+func statObject(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	ref := lua.CheckString(l, 2)
+	reqURL, err := url.JoinPath("/repositories", repo, "refs", ref, "objects", "stat")
+	check(l, err)
+	req, err := newLakeFSJSONRequest(ctx, user, http.MethodGet, reqURL, nil)
+	check(l, err)
+	// query params
+	q := req.URL.Query()
+	q.Add("path", lua.CheckString(l, 3))
+	if !l.IsNone(4) {
+		userMetadata := strconv.FormatBool(l.ToBoolean(4))
+		q.Add("user_metadata", userMetadata)
+	}
+	req.URL.RawQuery = q.Encode()
+	rr := httptest.NewRecorder()
+	server.Handler.ServeHTTP(rr, req)
+	l.PushInteger(rr.Code)
+	l.PushString(rr.Body.String())
+	return 2
+}
+
+// diffBranch handles comparing a branch
+func diffBranch(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	branch := lua.CheckString(l, 2)
+	reqURL, err := url.JoinPath("/repositories", repo, "branches", branch, "diff")
+	check(l, err)
+	req, err := newLakeFSJSONRequest(ctx, user, http.MethodGet, reqURL, nil)
+	check(l, err)
+	// query params
+	q := req.URL.Query()
+	if !l.IsNone(3) {
+		q.Add("after", lua.CheckString(l, 3))
+	}
+	if !l.IsNone(4) {
+		q.Add("amount", fmt.Sprintf("%d", lua.CheckInteger(l, 4)))
+	}
+	if !l.IsNone(5) {
+		q.Add("prefix", lua.CheckString(l, 5))
+	}
+	if !l.IsNone(6) {
+		q.Add("delimiter", lua.CheckString(l, 6))
+	}
+	req.URL.RawQuery = q.Encode()
+	return getLakeFSJSONResponse(l, server, req)
+}
+
+// OpenClient opens a new lakeFS client with the given context, user and server
+func OpenClient(l *lua.State, ctx context.Context, user *model.User, server *http.Server) {
+	clientOpen := func(l *lua.State) int {
+		lua.NewLibrary(l, []lua.RegistryFunction{
+			{Name: "update_object_user_metadata", Function: func(state *lua.State) int {
+				return updateObjectUserMetadata(l, ctx, user, server)
 			}},
 			{Name: "create_tag", Function: func(state *lua.State) int {
-				repo := lua.CheckString(l, 1)
-				data, err := json.Marshal(map[string]string{
-					"ref": lua.CheckString(l, 2),
-					"id":  lua.CheckString(l, 3),
-				})
-				if err != nil {
-					check(l, err)
-				}
-				reqURL, err := url.JoinPath("/repositories", repo, "tags")
-				if err != nil {
-					check(l, err)
-				}
-				req, err := newLakeFSJSONRequest(ctx, user, http.MethodPost, reqURL, data)
-				if err != nil {
-					check(l, err)
-				}
-				return getLakeFSJSONResponse(l, server, req)
+				return createTag(l, ctx, user, server)
 			}},
 			{Name: "diff_refs", Function: func(state *lua.State) int {
-				repo := lua.CheckString(l, 1)
-				leftRef := lua.CheckString(l, 2)
-				rightRef := lua.CheckString(l, 3)
-				reqURL, err := url.JoinPath("/repositories", repo, "refs", leftRef, "diff", rightRef)
-				if err != nil {
-					check(l, err)
-				}
-				req, err := newLakeFSJSONRequest(ctx, user, http.MethodGet, reqURL, nil)
-				if err != nil {
-					check(l, err)
-				}
-				// query params
-				q := req.URL.Query()
-				if !l.IsNone(4) {
-					q.Add("after", lua.CheckString(l, 4))
-				}
-				if !l.IsNone(5) {
-					q.Add("prefix", lua.CheckString(l, 5))
-				}
-				if !l.IsNone(6) {
-					q.Add("delimiter", lua.CheckString(l, 6))
-				}
-				if !l.IsNone(7) {
-					q.Add("amount", fmt.Sprintf("%d", lua.CheckInteger(l, 7)))
-				}
-				req.URL.RawQuery = q.Encode()
-				return getLakeFSJSONResponse(l, server, req)
+				return diffRefs(l, ctx, user, server)
 			}},
 			{Name: "list_objects", Function: func(state *lua.State) int {
-				repo := lua.CheckString(l, 1)
-				ref := lua.CheckString(l, 2)
-				reqURL, err := url.JoinPath("/repositories", repo, "refs", ref, "objects/ls")
-				if err != nil {
-					check(l, err)
-				}
-				req, err := newLakeFSJSONRequest(ctx, user, http.MethodGet, reqURL, nil)
-				if err != nil {
-					check(l, err)
-				}
-				// query params
-				q := req.URL.Query()
-				if !l.IsNone(3) {
-					q.Add("after", lua.CheckString(l, 3))
-				}
-				if !l.IsNone(4) {
-					q.Add("prefix", lua.CheckString(l, 4))
-				}
-				if !l.IsNone(5) {
-					q.Add("delimiter", lua.CheckString(l, 5))
-				}
-				if !l.IsNone(6) {
-					q.Add("amount", fmt.Sprintf("%d", lua.CheckInteger(l, 6)))
-				}
-				if !l.IsNone(7) {
-					withUserMetadata := "false"
-					if l.ToBoolean(7) {
-						withUserMetadata = "true"
-					}
-					q.Add("user_metadata", withUserMetadata)
-				}
-				req.URL.RawQuery = q.Encode()
-				return getLakeFSJSONResponse(l, server, req)
+				return listObjects(l, ctx, user, server)
 			}},
 			{Name: "get_object", Function: func(state *lua.State) int {
-				repo := lua.CheckString(l, 1)
-				ref := lua.CheckString(l, 2)
-				reqURL, err := url.JoinPath("/repositories", repo, "refs", ref, "objects")
-				if err != nil {
-					check(l, err)
-				}
-				req, err := newLakeFSJSONRequest(ctx, user, http.MethodGet, reqURL, nil)
-				if err != nil {
-					check(l, err)
-				}
-				// query params
-				q := req.URL.Query()
-				q.Add("path", lua.CheckString(l, 3))
-				req.URL.RawQuery = q.Encode()
-				rr := httptest.NewRecorder()
-				server.Handler.ServeHTTP(rr, req)
-				l.PushInteger(rr.Code)
-				l.PushString(rr.Body.String())
-				return 2
+				return getObject(l, ctx, user, server)
 			}},
 			{Name: "stat_object", Function: func(state *lua.State) int {
-				repo := lua.CheckString(l, 1)
-				ref := lua.CheckString(l, 2)
-				reqURL, err := url.JoinPath("/repositories", repo, "refs", ref, "objects", "stat")
-				if err != nil {
-					check(l, err)
-				}
-				req, err := newLakeFSJSONRequest(ctx, user, http.MethodGet, reqURL, nil)
-				if err != nil {
-					check(l, err)
-				}
-				// query params
-				q := req.URL.Query()
-				q.Add("path", lua.CheckString(l, 3))
-				req.URL.RawQuery = q.Encode()
-				rr := httptest.NewRecorder()
-				server.Handler.ServeHTTP(rr, req)
-				l.PushInteger(rr.Code)
-				l.PushString(rr.Body.String())
-				return 2
+				return statObject(l, ctx, user, server)
 			}},
 			{Name: "diff_branch", Function: func(state *lua.State) int {
-				repo := lua.CheckString(l, 1)
-				branch := lua.CheckString(l, 2)
-				reqURL, err := url.JoinPath("/repositories", repo, "branches", branch, "diff")
-				if err != nil {
-					check(l, err)
-				}
-				req, err := newLakeFSJSONRequest(ctx, user, http.MethodGet, reqURL, nil)
-				if err != nil {
-					check(l, err)
-				}
-				// query params
-				q := req.URL.Query()
-				if !l.IsNone(3) {
-					q.Add("after", lua.CheckString(l, 3))
-				}
-				if !l.IsNone(4) {
-					q.Add("amount", fmt.Sprintf("%d", lua.CheckInteger(l, 4)))
-				}
-				if !l.IsNone(5) {
-					q.Add("prefix", lua.CheckString(l, 5))
-				}
-				if !l.IsNone(6) {
-					q.Add("delimiter", lua.CheckString(l, 6))
-				}
-				req.URL.RawQuery = q.Encode()
-				return getLakeFSJSONResponse(l, server, req)
+				return diffBranch(l, ctx, user, server)
 			}},
 		})
 		return 1
diff --git a/pkg/actions/lua_test.go b/pkg/actions/lua_test.go
index ff86a7270f4..be389e9a175 100644
--- a/pkg/actions/lua_test.go
+++ b/pkg/actions/lua_test.go
@@ -483,7 +483,7 @@ func TestLuaRun_LakeFS(t *testing.T) {
 			Name: "update_object_user_metadata",
 			Script: `local lakefs = require("lakefs")
 local code, resp = lakefs.update_object_user_metadata("repo", "branch", "path/to/object", {key="value", key2="value2"})
-print(code .. " " .. resp)
+print(code, resp)
 `,
 			ExpectedOutput: "201",
 			ExpectedRequest: map[string]any{
@@ -506,7 +506,7 @@ print(code .. " " .. resp)
 			Name: "update_object_user_metadata-no_meta",
 			Script: `local lakefs = require("lakefs")
 local code, resp = lakefs.update_object_user_metadata("repo", "branch", "object", nil)
-print(code .. " " .. resp)
+print(code, resp)
 `,
 			ExpectedErr: true,
 		},


Reviews:
user: login: Isan-Rivkin
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-09T15:57:02Z
user: login: nopcoder
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-09T17:33:34Z
user: login: nopcoder
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-09T17:34:03Z
user: login: nopcoder
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-09T17:42:31Z
user: login: nopcoder
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-09T17:43:31Z
user: login: nopcoder
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-09T20:25:13Z
user: login: nopcoder
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-09T20:25:57Z
user: login: Isan-Rivkin
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T07:01:27Z
user: login: arielshaqed
body: Blocking on the pre-commit hook escape hatch.  Right now I think we have better and safer options.  I would rather do something less general and than break pre-commits.
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T07:08:46Z
user: login: Isan-Rivkin
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T08:08:44Z
user: login: nopcoder
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T16:13:31Z
user: login: Isan-Rivkin
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T07:03:20Z
user: login: arielshaqed
body: Thanks!  Also really easy to review, which is fun for this reviewer.
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T12:25:45Z

Review comments:
diff_hunk: @@ -76,190 +77,243 @@ func getLakeFSJSONResponse(l *lua.State, server *http.Server, request *http.Requ
 	return 1 + util.DeepPush(l, output)
 }
 
-func OpenClient(l *lua.State, ctx context.Context, user *model.User, server *http.Server) {
-	clientOpen := func(l *lua.State) int {
-		lua.NewLibrary(l, []lua.RegistryFunction{
-			{Name: "update_object_user_metadata", Function: func(state *lua.State) int {
-				repo := lua.CheckString(l, 1)
-				branch := lua.CheckString(l, 2)
-				objPath := lua.CheckString(l, 3)
-				metadata, err := util.PullStringTable(l, 4)
-				check(l, err)
+// updateObjectUserMetadata handles updating object metadata
+func updateObjectUserMetadata(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	branch := lua.CheckString(l, 2)
+	objPath := lua.CheckString(l, 3)
+	metadata, err := util.PullStringTable(l, 4)
+	check(l, err)
+
+	data, err := json.Marshal(map[string]interface{}{
+		"set": metadata,
+	})
+	check(l, err)
+
+	reqURL, err := url.JoinPath("/repositories", repo, "branches", branch, "objects/stat/user_metadata")
+	check(l, err)
+
+	req, err := newLakeFSJSONRequest(ctx, user, http.MethodPut, reqURL, data)
+	check(l, err)
+
+	// query params
+	q := req.URL.Query()
+	q.Add("path", objPath)
+	req.URL.RawQuery = q.Encode()
+
+	rr := httptest.NewRecorder()
+	server.Handler.ServeHTTP(rr, req)
+	l.PushInteger(rr.Code)
+	l.PushString(rr.Body.String())
+	return 2
+}
+
+// createTag handles tag creation
+func createTag(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	data, err := json.Marshal(map[string]string{
+		"ref": lua.CheckString(l, 2),
+		"id":  lua.CheckString(l, 3),
+	})
+	check(l, err)
+	reqURL, err := url.JoinPath("/repositories", repo, "tags")
+	check(l, err)
+	req, err := newLakeFSJSONRequest(ctx, user, http.MethodPost, reqURL, data)
+	check(l, err)
+	return getLakeFSJSONResponse(l, server, req)
+}
+
+// diffRefs handles comparing two refs
+func diffRefs(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	leftRef := lua.CheckString(l, 2)
+	rightRef := lua.CheckString(l, 3)
+	reqURL, err := url.JoinPath("/repositories", repo, "refs", leftRef, "diff", rightRef)
+	check(l, err)
+	req, err := newLakeFSJSONRequest(ctx, user, http.MethodGet, reqURL, nil)
+	check(l, err)
+	// query params
+	q := req.URL.Query()
+	if !l.IsNone(4) {
+		q.Add("after", lua.CheckString(l, 4))
+	}
+	if !l.IsNone(5) {
+		q.Add("prefix", lua.CheckString(l, 5))
+	}
+	if !l.IsNone(6) {
+		q.Add("delimiter", lua.CheckString(l, 6))
+	}
+	if !l.IsNone(7) {
+		q.Add("amount", fmt.Sprintf("%d", lua.CheckInteger(l, 7)))
+	}
+	req.URL.RawQuery = q.Encode()
+	return getLakeFSJSONResponse(l, server, req)
+}
+
+// listObjects handles listing objects in a ref
+func listObjects(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	ref := lua.CheckString(l, 2)
+	reqURL, err := url.JoinPath("/repositories", repo, "refs", ref, "objects/ls")
+	check(l, err)
+	req, err := newLakeFSJSONRequest(ctx, user, http.MethodGet, reqURL, nil)
+	check(l, err)
+	// query params
+	q := req.URL.Query()
+	if !l.IsNone(3) {
+		q.Add("after", lua.CheckString(l, 3))
+	}
+	if !l.IsNone(4) {
+		q.Add("prefix", lua.CheckString(l, 4))
+	}
+	if !l.IsNone(5) {
+		q.Add("delimiter", lua.CheckString(l, 5))
+	}
+	if !l.IsNone(6) {
+		q.Add("amount", fmt.Sprintf("%d", lua.CheckInteger(l, 6)))
+	}
+	if !l.IsNone(7) {
+		withUserMetadata := "false"
+		if l.ToBoolean(7) {
+			withUserMetadata = "true"
+		}
+		q.Add("user_metadata", withUserMetadata)
+	}
+	req.URL.RawQuery = q.Encode()
+	return getLakeFSJSONResponse(l, server, req)
+}
+
+// getObject handles retrieving an object
+func getObject(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	ref := lua.CheckString(l, 2)
+	reqURL, err := url.JoinPath("/repositories", repo, "refs", ref, "objects")
+	check(l, err)
+	req, err := newLakeFSJSONRequest(ctx, user, http.MethodGet, reqURL, nil)
+	check(l, err)
+	// query params
+	q := req.URL.Query()
+	q.Add("path", lua.CheckString(l, 3))
+	req.URL.RawQuery = q.Encode()
+	rr := httptest.NewRecorder()
+	server.Handler.ServeHTTP(rr, req)
+	l.PushInteger(rr.Code)
+	l.PushString(rr.Body.String())
+	return 2
+}
 
-				data, err := json.Marshal(map[string]interface{}{
-					"set": metadata,
-				})
-				check(l, err)
+// statObject handles retrieving object stats
+func statObject(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	ref := lua.CheckString(l, 2)
+	reqURL, err := url.JoinPath("/repositories", repo, "refs", ref, "objects", "stat")
+	check(l, err)
+	req, err := newLakeFSJSONRequest(ctx, user, http.MethodGet, reqURL, nil)
+	check(l, err)
+	// query params
+	q := req.URL.Query()
+	q.Add("path", lua.CheckString(l, 3))
+	if !l.IsNone(4) {
path: pkg/actions/lua/lakefs/client.go
user: login: Isan-Rivkin
body: Add do docs
created_at: 2025-03-09T14:52:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -76,190 +77,243 @@ func getLakeFSJSONResponse(l *lua.State, server *http.Server, request *http.Requ
 	return 1 + util.DeepPush(l, output)
 }
 
-func OpenClient(l *lua.State, ctx context.Context, user *model.User, server *http.Server) {
-	clientOpen := func(l *lua.State) int {
-		lua.NewLibrary(l, []lua.RegistryFunction{
-			{Name: "update_object_user_metadata", Function: func(state *lua.State) int {
-				repo := lua.CheckString(l, 1)
-				branch := lua.CheckString(l, 2)
-				objPath := lua.CheckString(l, 3)
-				metadata, err := util.PullStringTable(l, 4)
-				check(l, err)
+// updateObjectUserMetadata handles updating object metadata
+func updateObjectUserMetadata(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	branch := lua.CheckString(l, 2)
+	objPath := lua.CheckString(l, 3)
+	metadata, err := util.PullStringTable(l, 4)
+	check(l, err)
+
+	data, err := json.Marshal(map[string]interface{}{
+		"set": metadata,
+	})
+	check(l, err)
+
+	reqURL, err := url.JoinPath("/repositories", repo, "branches", branch, "objects/stat/user_metadata")
+	check(l, err)
+
+	req, err := newLakeFSJSONRequest(ctx, user, http.MethodPut, reqURL, data)
+	check(l, err)
+
+	// query params
+	q := req.URL.Query()
+	q.Add("path", objPath)
+	req.URL.RawQuery = q.Encode()
+
+	rr := httptest.NewRecorder()
+	server.Handler.ServeHTTP(rr, req)
+	l.PushInteger(rr.Code)
+	l.PushString(rr.Body.String())
+	return 2
+}
+
+// createTag handles tag creation
+func createTag(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	data, err := json.Marshal(map[string]string{
+		"ref": lua.CheckString(l, 2),
+		"id":  lua.CheckString(l, 3),
+	})
+	check(l, err)
+	reqURL, err := url.JoinPath("/repositories", repo, "tags")
+	check(l, err)
+	req, err := newLakeFSJSONRequest(ctx, user, http.MethodPost, reqURL, data)
+	check(l, err)
+	return getLakeFSJSONResponse(l, server, req)
+}
+
+// diffRefs handles comparing two refs
+func diffRefs(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	leftRef := lua.CheckString(l, 2)
+	rightRef := lua.CheckString(l, 3)
+	reqURL, err := url.JoinPath("/repositories", repo, "refs", leftRef, "diff", rightRef)
+	check(l, err)
+	req, err := newLakeFSJSONRequest(ctx, user, http.MethodGet, reqURL, nil)
+	check(l, err)
+	// query params
+	q := req.URL.Query()
+	if !l.IsNone(4) {
+		q.Add("after", lua.CheckString(l, 4))
+	}
+	if !l.IsNone(5) {
+		q.Add("prefix", lua.CheckString(l, 5))
+	}
+	if !l.IsNone(6) {
+		q.Add("delimiter", lua.CheckString(l, 6))
+	}
+	if !l.IsNone(7) {
+		q.Add("amount", fmt.Sprintf("%d", lua.CheckInteger(l, 7)))
+	}
+	req.URL.RawQuery = q.Encode()
+	return getLakeFSJSONResponse(l, server, req)
+}
+
+// listObjects handles listing objects in a ref
+func listObjects(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	ref := lua.CheckString(l, 2)
+	reqURL, err := url.JoinPath("/repositories", repo, "refs", ref, "objects/ls")
+	check(l, err)
+	req, err := newLakeFSJSONRequest(ctx, user, http.MethodGet, reqURL, nil)
+	check(l, err)
+	// query params
+	q := req.URL.Query()
+	if !l.IsNone(3) {
+		q.Add("after", lua.CheckString(l, 3))
+	}
+	if !l.IsNone(4) {
+		q.Add("prefix", lua.CheckString(l, 4))
+	}
+	if !l.IsNone(5) {
+		q.Add("delimiter", lua.CheckString(l, 5))
+	}
+	if !l.IsNone(6) {
+		q.Add("amount", fmt.Sprintf("%d", lua.CheckInteger(l, 6)))
+	}
+	if !l.IsNone(7) {
+		withUserMetadata := "false"
+		if l.ToBoolean(7) {
+			withUserMetadata = "true"
+		}
+		q.Add("user_metadata", withUserMetadata)
+	}
+	req.URL.RawQuery = q.Encode()
+	return getLakeFSJSONResponse(l, server, req)
+}
+
+// getObject handles retrieving an object
+func getObject(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	ref := lua.CheckString(l, 2)
+	reqURL, err := url.JoinPath("/repositories", repo, "refs", ref, "objects")
+	check(l, err)
+	req, err := newLakeFSJSONRequest(ctx, user, http.MethodGet, reqURL, nil)
+	check(l, err)
+	// query params
+	q := req.URL.Query()
+	q.Add("path", lua.CheckString(l, 3))
+	req.URL.RawQuery = q.Encode()
+	rr := httptest.NewRecorder()
+	server.Handler.ServeHTTP(rr, req)
+	l.PushInteger(rr.Code)
+	l.PushString(rr.Body.String())
+	return 2
+}
 
-				data, err := json.Marshal(map[string]interface{}{
-					"set": metadata,
-				})
-				check(l, err)
+// statObject handles retrieving object stats
+func statObject(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	ref := lua.CheckString(l, 2)
+	reqURL, err := url.JoinPath("/repositories", repo, "refs", ref, "objects", "stat")
+	check(l, err)
+	req, err := newLakeFSJSONRequest(ctx, user, http.MethodGet, reqURL, nil)
+	check(l, err)
+	// query params
+	q := req.URL.Query()
+	q.Add("path", lua.CheckString(l, 3))
+	if !l.IsNone(4) {
+		userMetadata := strconv.FormatBool(l.ToBoolean(4))
+		q.Add("user_metadata", userMetadata)
+	}
+	req.URL.RawQuery = q.Encode()
+	rr := httptest.NewRecorder()
+	server.Handler.ServeHTTP(rr, req)
+	l.PushInteger(rr.Code)
+	l.PushString(rr.Body.String())
+	return 2
+}
 
-				reqURL, err := url.JoinPath("/repositories", repo, "branches", branch, "objects/stat/user_metadata")
-				check(l, err)
+// diffBranch handles comparing a branch
+func diffBranch(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	branch := lua.CheckString(l, 2)
+	reqURL, err := url.JoinPath("/repositories", repo, "branches", branch, "diff")
+	check(l, err)
+	req, err := newLakeFSJSONRequest(ctx, user, http.MethodGet, reqURL, nil)
+	check(l, err)
+	// query params
+	q := req.URL.Query()
+	if !l.IsNone(3) {
+		q.Add("after", lua.CheckString(l, 3))
+	}
+	if !l.IsNone(4) {
+		q.Add("amount", fmt.Sprintf("%d", lua.CheckInteger(l, 4)))
+	}
+	if !l.IsNone(5) {
+		q.Add("prefix", lua.CheckString(l, 5))
+	}
+	if !l.IsNone(6) {
+		q.Add("delimiter", lua.CheckString(l, 6))
+	}
+	req.URL.RawQuery = q.Encode()
+	return getLakeFSJSONResponse(l, server, req)
+}
 
-				req, err := newLakeFSJSONRequest(ctx, user, http.MethodPut, reqURL, data)
-				check(l, err)
+// commitBranch handles committing changes to a branch
+func commitBranch(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
path: pkg/actions/lua/lakefs/client.go
user: login: Isan-Rivkin
body: Add to docs
created_at: 2025-03-09T14:55:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -76,190 +77,243 @@ func getLakeFSJSONResponse(l *lua.State, server *http.Server, request *http.Requ
 	return 1 + util.DeepPush(l, output)
 }
 
-func OpenClient(l *lua.State, ctx context.Context, user *model.User, server *http.Server) {
-	clientOpen := func(l *lua.State) int {
-		lua.NewLibrary(l, []lua.RegistryFunction{
-			{Name: "update_object_user_metadata", Function: func(state *lua.State) int {
-				repo := lua.CheckString(l, 1)
-				branch := lua.CheckString(l, 2)
-				objPath := lua.CheckString(l, 3)
-				metadata, err := util.PullStringTable(l, 4)
-				check(l, err)
+// updateObjectUserMetadata handles updating object metadata
+func updateObjectUserMetadata(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	branch := lua.CheckString(l, 2)
+	objPath := lua.CheckString(l, 3)
+	metadata, err := util.PullStringTable(l, 4)
+	check(l, err)
+
+	data, err := json.Marshal(map[string]interface{}{
+		"set": metadata,
+	})
+	check(l, err)
+
+	reqURL, err := url.JoinPath("/repositories", repo, "branches", branch, "objects/stat/user_metadata")
+	check(l, err)
+
+	req, err := newLakeFSJSONRequest(ctx, user, http.MethodPut, reqURL, data)
+	check(l, err)
+
+	// query params
+	q := req.URL.Query()
+	q.Add("path", objPath)
+	req.URL.RawQuery = q.Encode()
+
+	rr := httptest.NewRecorder()
+	server.Handler.ServeHTTP(rr, req)
+	l.PushInteger(rr.Code)
+	l.PushString(rr.Body.String())
+	return 2
+}
+
+// createTag handles tag creation
+func createTag(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	data, err := json.Marshal(map[string]string{
+		"ref": lua.CheckString(l, 2),
+		"id":  lua.CheckString(l, 3),
+	})
+	check(l, err)
+	reqURL, err := url.JoinPath("/repositories", repo, "tags")
+	check(l, err)
+	req, err := newLakeFSJSONRequest(ctx, user, http.MethodPost, reqURL, data)
+	check(l, err)
+	return getLakeFSJSONResponse(l, server, req)
+}
+
+// diffRefs handles comparing two refs
+func diffRefs(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	leftRef := lua.CheckString(l, 2)
+	rightRef := lua.CheckString(l, 3)
+	reqURL, err := url.JoinPath("/repositories", repo, "refs", leftRef, "diff", rightRef)
+	check(l, err)
+	req, err := newLakeFSJSONRequest(ctx, user, http.MethodGet, reqURL, nil)
+	check(l, err)
+	// query params
+	q := req.URL.Query()
+	if !l.IsNone(4) {
+		q.Add("after", lua.CheckString(l, 4))
+	}
+	if !l.IsNone(5) {
+		q.Add("prefix", lua.CheckString(l, 5))
+	}
+	if !l.IsNone(6) {
+		q.Add("delimiter", lua.CheckString(l, 6))
+	}
+	if !l.IsNone(7) {
+		q.Add("amount", fmt.Sprintf("%d", lua.CheckInteger(l, 7)))
+	}
+	req.URL.RawQuery = q.Encode()
+	return getLakeFSJSONResponse(l, server, req)
+}
+
+// listObjects handles listing objects in a ref
+func listObjects(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	ref := lua.CheckString(l, 2)
+	reqURL, err := url.JoinPath("/repositories", repo, "refs", ref, "objects/ls")
+	check(l, err)
+	req, err := newLakeFSJSONRequest(ctx, user, http.MethodGet, reqURL, nil)
+	check(l, err)
+	// query params
+	q := req.URL.Query()
+	if !l.IsNone(3) {
+		q.Add("after", lua.CheckString(l, 3))
+	}
+	if !l.IsNone(4) {
+		q.Add("prefix", lua.CheckString(l, 4))
+	}
+	if !l.IsNone(5) {
+		q.Add("delimiter", lua.CheckString(l, 5))
+	}
+	if !l.IsNone(6) {
+		q.Add("amount", fmt.Sprintf("%d", lua.CheckInteger(l, 6)))
+	}
+	if !l.IsNone(7) {
+		withUserMetadata := "false"
+		if l.ToBoolean(7) {
+			withUserMetadata = "true"
+		}
+		q.Add("user_metadata", withUserMetadata)
+	}
+	req.URL.RawQuery = q.Encode()
+	return getLakeFSJSONResponse(l, server, req)
+}
+
+// getObject handles retrieving an object
+func getObject(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	ref := lua.CheckString(l, 2)
+	reqURL, err := url.JoinPath("/repositories", repo, "refs", ref, "objects")
+	check(l, err)
+	req, err := newLakeFSJSONRequest(ctx, user, http.MethodGet, reqURL, nil)
+	check(l, err)
+	// query params
+	q := req.URL.Query()
+	q.Add("path", lua.CheckString(l, 3))
+	req.URL.RawQuery = q.Encode()
+	rr := httptest.NewRecorder()
+	server.Handler.ServeHTTP(rr, req)
+	l.PushInteger(rr.Code)
+	l.PushString(rr.Body.String())
+	return 2
+}
 
-				data, err := json.Marshal(map[string]interface{}{
-					"set": metadata,
-				})
-				check(l, err)
+// statObject handles retrieving object stats
+func statObject(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	ref := lua.CheckString(l, 2)
+	reqURL, err := url.JoinPath("/repositories", repo, "refs", ref, "objects", "stat")
+	check(l, err)
+	req, err := newLakeFSJSONRequest(ctx, user, http.MethodGet, reqURL, nil)
+	check(l, err)
+	// query params
+	q := req.URL.Query()
+	q.Add("path", lua.CheckString(l, 3))
+	if !l.IsNone(4) {
+		userMetadata := strconv.FormatBool(l.ToBoolean(4))
+		q.Add("user_metadata", userMetadata)
+	}
+	req.URL.RawQuery = q.Encode()
+	rr := httptest.NewRecorder()
+	server.Handler.ServeHTTP(rr, req)
+	l.PushInteger(rr.Code)
+	l.PushString(rr.Body.String())
+	return 2
+}
 
-				reqURL, err := url.JoinPath("/repositories", repo, "branches", branch, "objects/stat/user_metadata")
-				check(l, err)
+// diffBranch handles comparing a branch
+func diffBranch(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	branch := lua.CheckString(l, 2)
+	reqURL, err := url.JoinPath("/repositories", repo, "branches", branch, "diff")
+	check(l, err)
+	req, err := newLakeFSJSONRequest(ctx, user, http.MethodGet, reqURL, nil)
+	check(l, err)
+	// query params
+	q := req.URL.Query()
+	if !l.IsNone(3) {
+		q.Add("after", lua.CheckString(l, 3))
+	}
+	if !l.IsNone(4) {
+		q.Add("amount", fmt.Sprintf("%d", lua.CheckInteger(l, 4)))
+	}
+	if !l.IsNone(5) {
+		q.Add("prefix", lua.CheckString(l, 5))
+	}
+	if !l.IsNone(6) {
+		q.Add("delimiter", lua.CheckString(l, 6))
+	}
+	req.URL.RawQuery = q.Encode()
+	return getLakeFSJSONResponse(l, server, req)
+}
 
-				req, err := newLakeFSJSONRequest(ctx, user, http.MethodPut, reqURL, data)
-				check(l, err)
+// commitBranch handles committing changes to a branch
+func commitBranch(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	branch := lua.CheckString(l, 2)
+	message := lua.CheckString(l, 3)
path: pkg/actions/lua/lakefs/client.go
user: login: Isan-Rivkin
body: In commit, query param `source_metarange`, we don't want to include it?
I'm OK with that, just want to make sure there's really no reason to add it.
created_at: 2025-03-09T14:56:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -76,190 +77,243 @@ func getLakeFSJSONResponse(l *lua.State, server *http.Server, request *http.Requ
 	return 1 + util.DeepPush(l, output)
 }
 
-func OpenClient(l *lua.State, ctx context.Context, user *model.User, server *http.Server) {
-	clientOpen := func(l *lua.State) int {
-		lua.NewLibrary(l, []lua.RegistryFunction{
-			{Name: "update_object_user_metadata", Function: func(state *lua.State) int {
-				repo := lua.CheckString(l, 1)
-				branch := lua.CheckString(l, 2)
-				objPath := lua.CheckString(l, 3)
-				metadata, err := util.PullStringTable(l, 4)
-				check(l, err)
+// updateObjectUserMetadata handles updating object metadata
+func updateObjectUserMetadata(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	branch := lua.CheckString(l, 2)
+	objPath := lua.CheckString(l, 3)
+	metadata, err := util.PullStringTable(l, 4)
+	check(l, err)
+
+	data, err := json.Marshal(map[string]interface{}{
+		"set": metadata,
+	})
+	check(l, err)
+
+	reqURL, err := url.JoinPath("/repositories", repo, "branches", branch, "objects/stat/user_metadata")
+	check(l, err)
+
+	req, err := newLakeFSJSONRequest(ctx, user, http.MethodPut, reqURL, data)
+	check(l, err)
+
+	// query params
+	q := req.URL.Query()
+	q.Add("path", objPath)
+	req.URL.RawQuery = q.Encode()
+
+	rr := httptest.NewRecorder()
+	server.Handler.ServeHTTP(rr, req)
+	l.PushInteger(rr.Code)
+	l.PushString(rr.Body.String())
+	return 2
+}
+
+// createTag handles tag creation
+func createTag(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	data, err := json.Marshal(map[string]string{
+		"ref": lua.CheckString(l, 2),
+		"id":  lua.CheckString(l, 3),
+	})
+	check(l, err)
+	reqURL, err := url.JoinPath("/repositories", repo, "tags")
+	check(l, err)
+	req, err := newLakeFSJSONRequest(ctx, user, http.MethodPost, reqURL, data)
+	check(l, err)
+	return getLakeFSJSONResponse(l, server, req)
+}
+
+// diffRefs handles comparing two refs
+func diffRefs(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	leftRef := lua.CheckString(l, 2)
+	rightRef := lua.CheckString(l, 3)
+	reqURL, err := url.JoinPath("/repositories", repo, "refs", leftRef, "diff", rightRef)
+	check(l, err)
+	req, err := newLakeFSJSONRequest(ctx, user, http.MethodGet, reqURL, nil)
+	check(l, err)
+	// query params
+	q := req.URL.Query()
+	if !l.IsNone(4) {
+		q.Add("after", lua.CheckString(l, 4))
+	}
+	if !l.IsNone(5) {
+		q.Add("prefix", lua.CheckString(l, 5))
+	}
+	if !l.IsNone(6) {
+		q.Add("delimiter", lua.CheckString(l, 6))
+	}
+	if !l.IsNone(7) {
+		q.Add("amount", fmt.Sprintf("%d", lua.CheckInteger(l, 7)))
+	}
+	req.URL.RawQuery = q.Encode()
+	return getLakeFSJSONResponse(l, server, req)
+}
+
+// listObjects handles listing objects in a ref
+func listObjects(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	ref := lua.CheckString(l, 2)
+	reqURL, err := url.JoinPath("/repositories", repo, "refs", ref, "objects/ls")
+	check(l, err)
+	req, err := newLakeFSJSONRequest(ctx, user, http.MethodGet, reqURL, nil)
+	check(l, err)
+	// query params
+	q := req.URL.Query()
+	if !l.IsNone(3) {
+		q.Add("after", lua.CheckString(l, 3))
+	}
+	if !l.IsNone(4) {
+		q.Add("prefix", lua.CheckString(l, 4))
+	}
+	if !l.IsNone(5) {
+		q.Add("delimiter", lua.CheckString(l, 5))
+	}
+	if !l.IsNone(6) {
+		q.Add("amount", fmt.Sprintf("%d", lua.CheckInteger(l, 6)))
+	}
+	if !l.IsNone(7) {
+		withUserMetadata := "false"
+		if l.ToBoolean(7) {
+			withUserMetadata = "true"
+		}
+		q.Add("user_metadata", withUserMetadata)
+	}
+	req.URL.RawQuery = q.Encode()
+	return getLakeFSJSONResponse(l, server, req)
+}
+
+// getObject handles retrieving an object
+func getObject(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	ref := lua.CheckString(l, 2)
+	reqURL, err := url.JoinPath("/repositories", repo, "refs", ref, "objects")
+	check(l, err)
+	req, err := newLakeFSJSONRequest(ctx, user, http.MethodGet, reqURL, nil)
+	check(l, err)
+	// query params
+	q := req.URL.Query()
+	q.Add("path", lua.CheckString(l, 3))
+	req.URL.RawQuery = q.Encode()
+	rr := httptest.NewRecorder()
+	server.Handler.ServeHTTP(rr, req)
+	l.PushInteger(rr.Code)
+	l.PushString(rr.Body.String())
+	return 2
+}
 
-				data, err := json.Marshal(map[string]interface{}{
-					"set": metadata,
-				})
-				check(l, err)
+// statObject handles retrieving object stats
+func statObject(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	ref := lua.CheckString(l, 2)
+	reqURL, err := url.JoinPath("/repositories", repo, "refs", ref, "objects", "stat")
+	check(l, err)
+	req, err := newLakeFSJSONRequest(ctx, user, http.MethodGet, reqURL, nil)
+	check(l, err)
+	// query params
+	q := req.URL.Query()
+	q.Add("path", lua.CheckString(l, 3))
+	if !l.IsNone(4) {
+		userMetadata := strconv.FormatBool(l.ToBoolean(4))
+		q.Add("user_metadata", userMetadata)
+	}
+	req.URL.RawQuery = q.Encode()
+	rr := httptest.NewRecorder()
+	server.Handler.ServeHTTP(rr, req)
+	l.PushInteger(rr.Code)
+	l.PushString(rr.Body.String())
+	return 2
+}
 
-				reqURL, err := url.JoinPath("/repositories", repo, "branches", branch, "objects/stat/user_metadata")
-				check(l, err)
+// diffBranch handles comparing a branch
+func diffBranch(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	branch := lua.CheckString(l, 2)
+	reqURL, err := url.JoinPath("/repositories", repo, "branches", branch, "diff")
+	check(l, err)
+	req, err := newLakeFSJSONRequest(ctx, user, http.MethodGet, reqURL, nil)
+	check(l, err)
+	// query params
+	q := req.URL.Query()
+	if !l.IsNone(3) {
+		q.Add("after", lua.CheckString(l, 3))
+	}
+	if !l.IsNone(4) {
+		q.Add("amount", fmt.Sprintf("%d", lua.CheckInteger(l, 4)))
+	}
+	if !l.IsNone(5) {
+		q.Add("prefix", lua.CheckString(l, 5))
+	}
+	if !l.IsNone(6) {
+		q.Add("delimiter", lua.CheckString(l, 6))
+	}
+	req.URL.RawQuery = q.Encode()
+	return getLakeFSJSONResponse(l, server, req)
+}
 
-				req, err := newLakeFSJSONRequest(ctx, user, http.MethodPut, reqURL, data)
-				check(l, err)
+// commitBranch handles committing changes to a branch
+func commitBranch(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	branch := lua.CheckString(l, 2)
+	message := lua.CheckString(l, 3)
 
-				// query params
-				q := req.URL.Query()
-				q.Add("path", objPath)
-				req.URL.RawQuery = q.Encode()
+	data := map[string]interface{}{
+		"message": message,
+	}
 
-				rr := httptest.NewRecorder()
-				server.Handler.ServeHTTP(rr, req)
-				l.PushInteger(rr.Code)
-				l.PushString(rr.Body.String())
-				return 2
+	// Handle optional metadata if provided (4th parameter)
+	if !l.IsNone(4) {
+		metadata, err := util.PullStringTable(l, 4)
+		if err != nil {
+			check(l, err)
+		}
+		data["metadata"] = metadata
+	}
+
+	// Handle optional source metadata flag (5th parameter)
+	if !l.IsNone(5) {
+		data["force_if_not_exists"] = l.ToBoolean(5)
path: pkg/actions/lua/lakefs/client.go
user: login: Isan-Rivkin
body: What's that field? It's not part of commit API... 👀 
created_at: 2025-03-09T14:58:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -76,190 +77,243 @@ func getLakeFSJSONResponse(l *lua.State, server *http.Server, request *http.Requ
 	return 1 + util.DeepPush(l, output)
 }
 
-func OpenClient(l *lua.State, ctx context.Context, user *model.User, server *http.Server) {
-	clientOpen := func(l *lua.State) int {
-		lua.NewLibrary(l, []lua.RegistryFunction{
-			{Name: "update_object_user_metadata", Function: func(state *lua.State) int {
-				repo := lua.CheckString(l, 1)
-				branch := lua.CheckString(l, 2)
-				objPath := lua.CheckString(l, 3)
-				metadata, err := util.PullStringTable(l, 4)
-				check(l, err)
+// updateObjectUserMetadata handles updating object metadata
+func updateObjectUserMetadata(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	branch := lua.CheckString(l, 2)
+	objPath := lua.CheckString(l, 3)
+	metadata, err := util.PullStringTable(l, 4)
+	check(l, err)
+
+	data, err := json.Marshal(map[string]interface{}{
+		"set": metadata,
+	})
+	check(l, err)
+
+	reqURL, err := url.JoinPath("/repositories", repo, "branches", branch, "objects/stat/user_metadata")
+	check(l, err)
+
+	req, err := newLakeFSJSONRequest(ctx, user, http.MethodPut, reqURL, data)
+	check(l, err)
+
+	// query params
+	q := req.URL.Query()
+	q.Add("path", objPath)
+	req.URL.RawQuery = q.Encode()
+
+	rr := httptest.NewRecorder()
+	server.Handler.ServeHTTP(rr, req)
+	l.PushInteger(rr.Code)
+	l.PushString(rr.Body.String())
+	return 2
+}
+
+// createTag handles tag creation
+func createTag(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	data, err := json.Marshal(map[string]string{
+		"ref": lua.CheckString(l, 2),
+		"id":  lua.CheckString(l, 3),
+	})
+	check(l, err)
+	reqURL, err := url.JoinPath("/repositories", repo, "tags")
+	check(l, err)
+	req, err := newLakeFSJSONRequest(ctx, user, http.MethodPost, reqURL, data)
+	check(l, err)
+	return getLakeFSJSONResponse(l, server, req)
+}
+
+// diffRefs handles comparing two refs
+func diffRefs(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	leftRef := lua.CheckString(l, 2)
+	rightRef := lua.CheckString(l, 3)
+	reqURL, err := url.JoinPath("/repositories", repo, "refs", leftRef, "diff", rightRef)
+	check(l, err)
+	req, err := newLakeFSJSONRequest(ctx, user, http.MethodGet, reqURL, nil)
+	check(l, err)
+	// query params
+	q := req.URL.Query()
+	if !l.IsNone(4) {
+		q.Add("after", lua.CheckString(l, 4))
+	}
+	if !l.IsNone(5) {
+		q.Add("prefix", lua.CheckString(l, 5))
+	}
+	if !l.IsNone(6) {
+		q.Add("delimiter", lua.CheckString(l, 6))
+	}
+	if !l.IsNone(7) {
+		q.Add("amount", fmt.Sprintf("%d", lua.CheckInteger(l, 7)))
+	}
+	req.URL.RawQuery = q.Encode()
+	return getLakeFSJSONResponse(l, server, req)
+}
+
+// listObjects handles listing objects in a ref
+func listObjects(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	ref := lua.CheckString(l, 2)
+	reqURL, err := url.JoinPath("/repositories", repo, "refs", ref, "objects/ls")
+	check(l, err)
+	req, err := newLakeFSJSONRequest(ctx, user, http.MethodGet, reqURL, nil)
+	check(l, err)
+	// query params
+	q := req.URL.Query()
+	if !l.IsNone(3) {
+		q.Add("after", lua.CheckString(l, 3))
+	}
+	if !l.IsNone(4) {
+		q.Add("prefix", lua.CheckString(l, 4))
+	}
+	if !l.IsNone(5) {
+		q.Add("delimiter", lua.CheckString(l, 5))
+	}
+	if !l.IsNone(6) {
+		q.Add("amount", fmt.Sprintf("%d", lua.CheckInteger(l, 6)))
+	}
+	if !l.IsNone(7) {
+		withUserMetadata := "false"
+		if l.ToBoolean(7) {
+			withUserMetadata = "true"
+		}
+		q.Add("user_metadata", withUserMetadata)
+	}
+	req.URL.RawQuery = q.Encode()
+	return getLakeFSJSONResponse(l, server, req)
+}
+
+// getObject handles retrieving an object
+func getObject(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	ref := lua.CheckString(l, 2)
+	reqURL, err := url.JoinPath("/repositories", repo, "refs", ref, "objects")
+	check(l, err)
+	req, err := newLakeFSJSONRequest(ctx, user, http.MethodGet, reqURL, nil)
+	check(l, err)
+	// query params
+	q := req.URL.Query()
+	q.Add("path", lua.CheckString(l, 3))
+	req.URL.RawQuery = q.Encode()
+	rr := httptest.NewRecorder()
+	server.Handler.ServeHTTP(rr, req)
+	l.PushInteger(rr.Code)
+	l.PushString(rr.Body.String())
+	return 2
+}
 
-				data, err := json.Marshal(map[string]interface{}{
-					"set": metadata,
-				})
-				check(l, err)
+// statObject handles retrieving object stats
+func statObject(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	ref := lua.CheckString(l, 2)
+	reqURL, err := url.JoinPath("/repositories", repo, "refs", ref, "objects", "stat")
+	check(l, err)
+	req, err := newLakeFSJSONRequest(ctx, user, http.MethodGet, reqURL, nil)
+	check(l, err)
+	// query params
+	q := req.URL.Query()
+	q.Add("path", lua.CheckString(l, 3))
+	if !l.IsNone(4) {
+		userMetadata := strconv.FormatBool(l.ToBoolean(4))
+		q.Add("user_metadata", userMetadata)
+	}
+	req.URL.RawQuery = q.Encode()
+	rr := httptest.NewRecorder()
+	server.Handler.ServeHTTP(rr, req)
+	l.PushInteger(rr.Code)
+	l.PushString(rr.Body.String())
+	return 2
+}
 
-				reqURL, err := url.JoinPath("/repositories", repo, "branches", branch, "objects/stat/user_metadata")
-				check(l, err)
+// diffBranch handles comparing a branch
+func diffBranch(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	branch := lua.CheckString(l, 2)
+	reqURL, err := url.JoinPath("/repositories", repo, "branches", branch, "diff")
+	check(l, err)
+	req, err := newLakeFSJSONRequest(ctx, user, http.MethodGet, reqURL, nil)
+	check(l, err)
+	// query params
+	q := req.URL.Query()
+	if !l.IsNone(3) {
+		q.Add("after", lua.CheckString(l, 3))
+	}
+	if !l.IsNone(4) {
+		q.Add("amount", fmt.Sprintf("%d", lua.CheckInteger(l, 4)))
+	}
+	if !l.IsNone(5) {
+		q.Add("prefix", lua.CheckString(l, 5))
+	}
+	if !l.IsNone(6) {
+		q.Add("delimiter", lua.CheckString(l, 6))
+	}
+	req.URL.RawQuery = q.Encode()
+	return getLakeFSJSONResponse(l, server, req)
+}
 
-				req, err := newLakeFSJSONRequest(ctx, user, http.MethodPut, reqURL, data)
-				check(l, err)
+// commitBranch handles committing changes to a branch
+func commitBranch(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
path: pkg/actions/lua/lakefs/client.go
user: login: Isan-Rivkin
body: The rest of API commit fields not exposed on purpose? 
`allow_empty`, `force`, `date`
created_at: 2025-03-09T15:01:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -76,190 +77,243 @@ func getLakeFSJSONResponse(l *lua.State, server *http.Server, request *http.Requ
 	return 1 + util.DeepPush(l, output)
 }
 
-func OpenClient(l *lua.State, ctx context.Context, user *model.User, server *http.Server) {
-	clientOpen := func(l *lua.State) int {
-		lua.NewLibrary(l, []lua.RegistryFunction{
-			{Name: "update_object_user_metadata", Function: func(state *lua.State) int {
-				repo := lua.CheckString(l, 1)
-				branch := lua.CheckString(l, 2)
-				objPath := lua.CheckString(l, 3)
-				metadata, err := util.PullStringTable(l, 4)
-				check(l, err)
+// updateObjectUserMetadata handles updating object metadata
+func updateObjectUserMetadata(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	branch := lua.CheckString(l, 2)
+	objPath := lua.CheckString(l, 3)
+	metadata, err := util.PullStringTable(l, 4)
+	check(l, err)
+
+	data, err := json.Marshal(map[string]interface{}{
+		"set": metadata,
+	})
+	check(l, err)
+
+	reqURL, err := url.JoinPath("/repositories", repo, "branches", branch, "objects/stat/user_metadata")
+	check(l, err)
+
+	req, err := newLakeFSJSONRequest(ctx, user, http.MethodPut, reqURL, data)
+	check(l, err)
+
+	// query params
+	q := req.URL.Query()
+	q.Add("path", objPath)
+	req.URL.RawQuery = q.Encode()
+
+	rr := httptest.NewRecorder()
+	server.Handler.ServeHTTP(rr, req)
+	l.PushInteger(rr.Code)
+	l.PushString(rr.Body.String())
+	return 2
+}
+
+// createTag handles tag creation
+func createTag(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	data, err := json.Marshal(map[string]string{
+		"ref": lua.CheckString(l, 2),
+		"id":  lua.CheckString(l, 3),
+	})
+	check(l, err)
+	reqURL, err := url.JoinPath("/repositories", repo, "tags")
+	check(l, err)
+	req, err := newLakeFSJSONRequest(ctx, user, http.MethodPost, reqURL, data)
+	check(l, err)
+	return getLakeFSJSONResponse(l, server, req)
+}
+
+// diffRefs handles comparing two refs
+func diffRefs(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	leftRef := lua.CheckString(l, 2)
+	rightRef := lua.CheckString(l, 3)
+	reqURL, err := url.JoinPath("/repositories", repo, "refs", leftRef, "diff", rightRef)
+	check(l, err)
+	req, err := newLakeFSJSONRequest(ctx, user, http.MethodGet, reqURL, nil)
+	check(l, err)
+	// query params
+	q := req.URL.Query()
+	if !l.IsNone(4) {
+		q.Add("after", lua.CheckString(l, 4))
+	}
+	if !l.IsNone(5) {
+		q.Add("prefix", lua.CheckString(l, 5))
+	}
+	if !l.IsNone(6) {
+		q.Add("delimiter", lua.CheckString(l, 6))
+	}
+	if !l.IsNone(7) {
+		q.Add("amount", fmt.Sprintf("%d", lua.CheckInteger(l, 7)))
+	}
+	req.URL.RawQuery = q.Encode()
+	return getLakeFSJSONResponse(l, server, req)
+}
+
+// listObjects handles listing objects in a ref
+func listObjects(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	ref := lua.CheckString(l, 2)
+	reqURL, err := url.JoinPath("/repositories", repo, "refs", ref, "objects/ls")
+	check(l, err)
+	req, err := newLakeFSJSONRequest(ctx, user, http.MethodGet, reqURL, nil)
+	check(l, err)
+	// query params
+	q := req.URL.Query()
+	if !l.IsNone(3) {
+		q.Add("after", lua.CheckString(l, 3))
+	}
+	if !l.IsNone(4) {
+		q.Add("prefix", lua.CheckString(l, 4))
+	}
+	if !l.IsNone(5) {
+		q.Add("delimiter", lua.CheckString(l, 5))
+	}
+	if !l.IsNone(6) {
+		q.Add("amount", fmt.Sprintf("%d", lua.CheckInteger(l, 6)))
+	}
+	if !l.IsNone(7) {
+		withUserMetadata := "false"
+		if l.ToBoolean(7) {
+			withUserMetadata = "true"
+		}
+		q.Add("user_metadata", withUserMetadata)
+	}
+	req.URL.RawQuery = q.Encode()
+	return getLakeFSJSONResponse(l, server, req)
+}
+
+// getObject handles retrieving an object
+func getObject(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	ref := lua.CheckString(l, 2)
+	reqURL, err := url.JoinPath("/repositories", repo, "refs", ref, "objects")
+	check(l, err)
+	req, err := newLakeFSJSONRequest(ctx, user, http.MethodGet, reqURL, nil)
+	check(l, err)
+	// query params
+	q := req.URL.Query()
+	q.Add("path", lua.CheckString(l, 3))
+	req.URL.RawQuery = q.Encode()
+	rr := httptest.NewRecorder()
+	server.Handler.ServeHTTP(rr, req)
+	l.PushInteger(rr.Code)
+	l.PushString(rr.Body.String())
+	return 2
+}
 
-				data, err := json.Marshal(map[string]interface{}{
-					"set": metadata,
-				})
-				check(l, err)
+// statObject handles retrieving object stats
+func statObject(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	ref := lua.CheckString(l, 2)
+	reqURL, err := url.JoinPath("/repositories", repo, "refs", ref, "objects", "stat")
+	check(l, err)
+	req, err := newLakeFSJSONRequest(ctx, user, http.MethodGet, reqURL, nil)
+	check(l, err)
+	// query params
+	q := req.URL.Query()
+	q.Add("path", lua.CheckString(l, 3))
+	if !l.IsNone(4) {
+		userMetadata := strconv.FormatBool(l.ToBoolean(4))
+		q.Add("user_metadata", userMetadata)
+	}
+	req.URL.RawQuery = q.Encode()
+	rr := httptest.NewRecorder()
+	server.Handler.ServeHTTP(rr, req)
+	l.PushInteger(rr.Code)
+	l.PushString(rr.Body.String())
+	return 2
+}
 
-				reqURL, err := url.JoinPath("/repositories", repo, "branches", branch, "objects/stat/user_metadata")
-				check(l, err)
+// diffBranch handles comparing a branch
+func diffBranch(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	branch := lua.CheckString(l, 2)
+	reqURL, err := url.JoinPath("/repositories", repo, "branches", branch, "diff")
+	check(l, err)
+	req, err := newLakeFSJSONRequest(ctx, user, http.MethodGet, reqURL, nil)
+	check(l, err)
+	// query params
+	q := req.URL.Query()
+	if !l.IsNone(3) {
+		q.Add("after", lua.CheckString(l, 3))
+	}
+	if !l.IsNone(4) {
+		q.Add("amount", fmt.Sprintf("%d", lua.CheckInteger(l, 4)))
+	}
+	if !l.IsNone(5) {
+		q.Add("prefix", lua.CheckString(l, 5))
+	}
+	if !l.IsNone(6) {
+		q.Add("delimiter", lua.CheckString(l, 6))
+	}
+	req.URL.RawQuery = q.Encode()
+	return getLakeFSJSONResponse(l, server, req)
+}
 
-				req, err := newLakeFSJSONRequest(ctx, user, http.MethodPut, reqURL, data)
-				check(l, err)
+// commitBranch handles committing changes to a branch
+func commitBranch(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	branch := lua.CheckString(l, 2)
+	message := lua.CheckString(l, 3)
 
-				// query params
-				q := req.URL.Query()
-				q.Add("path", objPath)
-				req.URL.RawQuery = q.Encode()
+	data := map[string]interface{}{
+		"message": message,
+	}
 
-				rr := httptest.NewRecorder()
-				server.Handler.ServeHTTP(rr, req)
-				l.PushInteger(rr.Code)
-				l.PushString(rr.Body.String())
-				return 2
+	// Handle optional metadata if provided (4th parameter)
+	if !l.IsNone(4) {
+		metadata, err := util.PullStringTable(l, 4)
+		if err != nil {
+			check(l, err)
+		}
+		data["metadata"] = metadata
path: pkg/actions/lua/lakefs/client.go
user: login: Isan-Rivkin
body: Are you sure that adding metadata works? 
**Im probably doing something wrong:**
The commit works! but the metadata is never added to the objects in the commit.

```yaml
name: dump_all
on:
  post-commit:
  post-merge:
  post-create-tag:
  post-create-branch:
  # pre-commit:
hooks:
  - id: dump_event
    type: lua
    properties:
      script: |
        lakefs = require("lakefs")
        json = require("encoding/json")
        print(json.marshal(action))
        repo = action.repository_id
        branch = action.branch_id
        print("committing to repo: " .. repo .. " from branch: " .. branch .. "to branch: other with metadata")
        md = {}
        md["creation_date"] = action.creation_date
        code, resp = lakefs.commit(repo, "other", "my commit from lua", md)
        if code ~= 201 then
          error("commit error: " .. resp.message)
        end
        print("response:")
        print(json.marshal(resp))
```

created_at: 2025-03-09T15:54:37Z
author_association: CONTRIBUTOR
path: pkg/actions/lua/lakefs/client.go
user: login: Isan-Rivkin
body: I have a general concern regarding usage. 
I did commit and set it as `pre-commit` hook. 
I was able to crush lakeFS server locally. 
**This means that anyone in some dummy repository can take down everything since it's in the same process.** 
I wonder if this make sense to use commit without any guard rails or extra checks for the user? 
created_at: 2025-03-09T15:56:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -76,190 +77,243 @@ func getLakeFSJSONResponse(l *lua.State, server *http.Server, request *http.Requ
 	return 1 + util.DeepPush(l, output)
 }
 
-func OpenClient(l *lua.State, ctx context.Context, user *model.User, server *http.Server) {
-	clientOpen := func(l *lua.State) int {
-		lua.NewLibrary(l, []lua.RegistryFunction{
-			{Name: "update_object_user_metadata", Function: func(state *lua.State) int {
-				repo := lua.CheckString(l, 1)
-				branch := lua.CheckString(l, 2)
-				objPath := lua.CheckString(l, 3)
-				metadata, err := util.PullStringTable(l, 4)
-				check(l, err)
+// updateObjectUserMetadata handles updating object metadata
+func updateObjectUserMetadata(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	branch := lua.CheckString(l, 2)
+	objPath := lua.CheckString(l, 3)
+	metadata, err := util.PullStringTable(l, 4)
+	check(l, err)
+
+	data, err := json.Marshal(map[string]interface{}{
+		"set": metadata,
+	})
+	check(l, err)
+
+	reqURL, err := url.JoinPath("/repositories", repo, "branches", branch, "objects/stat/user_metadata")
+	check(l, err)
+
+	req, err := newLakeFSJSONRequest(ctx, user, http.MethodPut, reqURL, data)
+	check(l, err)
+
+	// query params
+	q := req.URL.Query()
+	q.Add("path", objPath)
+	req.URL.RawQuery = q.Encode()
+
+	rr := httptest.NewRecorder()
+	server.Handler.ServeHTTP(rr, req)
+	l.PushInteger(rr.Code)
+	l.PushString(rr.Body.String())
+	return 2
+}
+
+// createTag handles tag creation
+func createTag(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	data, err := json.Marshal(map[string]string{
+		"ref": lua.CheckString(l, 2),
+		"id":  lua.CheckString(l, 3),
+	})
+	check(l, err)
+	reqURL, err := url.JoinPath("/repositories", repo, "tags")
+	check(l, err)
+	req, err := newLakeFSJSONRequest(ctx, user, http.MethodPost, reqURL, data)
+	check(l, err)
+	return getLakeFSJSONResponse(l, server, req)
+}
+
+// diffRefs handles comparing two refs
+func diffRefs(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	leftRef := lua.CheckString(l, 2)
+	rightRef := lua.CheckString(l, 3)
+	reqURL, err := url.JoinPath("/repositories", repo, "refs", leftRef, "diff", rightRef)
+	check(l, err)
+	req, err := newLakeFSJSONRequest(ctx, user, http.MethodGet, reqURL, nil)
+	check(l, err)
+	// query params
+	q := req.URL.Query()
+	if !l.IsNone(4) {
+		q.Add("after", lua.CheckString(l, 4))
+	}
+	if !l.IsNone(5) {
+		q.Add("prefix", lua.CheckString(l, 5))
+	}
+	if !l.IsNone(6) {
+		q.Add("delimiter", lua.CheckString(l, 6))
+	}
+	if !l.IsNone(7) {
+		q.Add("amount", fmt.Sprintf("%d", lua.CheckInteger(l, 7)))
+	}
+	req.URL.RawQuery = q.Encode()
+	return getLakeFSJSONResponse(l, server, req)
+}
+
+// listObjects handles listing objects in a ref
+func listObjects(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	ref := lua.CheckString(l, 2)
+	reqURL, err := url.JoinPath("/repositories", repo, "refs", ref, "objects/ls")
+	check(l, err)
+	req, err := newLakeFSJSONRequest(ctx, user, http.MethodGet, reqURL, nil)
+	check(l, err)
+	// query params
+	q := req.URL.Query()
+	if !l.IsNone(3) {
+		q.Add("after", lua.CheckString(l, 3))
+	}
+	if !l.IsNone(4) {
+		q.Add("prefix", lua.CheckString(l, 4))
+	}
+	if !l.IsNone(5) {
+		q.Add("delimiter", lua.CheckString(l, 5))
+	}
+	if !l.IsNone(6) {
+		q.Add("amount", fmt.Sprintf("%d", lua.CheckInteger(l, 6)))
+	}
+	if !l.IsNone(7) {
+		withUserMetadata := "false"
+		if l.ToBoolean(7) {
+			withUserMetadata = "true"
+		}
+		q.Add("user_metadata", withUserMetadata)
+	}
+	req.URL.RawQuery = q.Encode()
+	return getLakeFSJSONResponse(l, server, req)
+}
+
+// getObject handles retrieving an object
+func getObject(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	ref := lua.CheckString(l, 2)
+	reqURL, err := url.JoinPath("/repositories", repo, "refs", ref, "objects")
+	check(l, err)
+	req, err := newLakeFSJSONRequest(ctx, user, http.MethodGet, reqURL, nil)
+	check(l, err)
+	// query params
+	q := req.URL.Query()
+	q.Add("path", lua.CheckString(l, 3))
+	req.URL.RawQuery = q.Encode()
+	rr := httptest.NewRecorder()
+	server.Handler.ServeHTTP(rr, req)
+	l.PushInteger(rr.Code)
+	l.PushString(rr.Body.String())
+	return 2
+}
 
-				data, err := json.Marshal(map[string]interface{}{
-					"set": metadata,
-				})
-				check(l, err)
+// statObject handles retrieving object stats
+func statObject(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	ref := lua.CheckString(l, 2)
+	reqURL, err := url.JoinPath("/repositories", repo, "refs", ref, "objects", "stat")
+	check(l, err)
+	req, err := newLakeFSJSONRequest(ctx, user, http.MethodGet, reqURL, nil)
+	check(l, err)
+	// query params
+	q := req.URL.Query()
+	q.Add("path", lua.CheckString(l, 3))
+	if !l.IsNone(4) {
+		userMetadata := strconv.FormatBool(l.ToBoolean(4))
+		q.Add("user_metadata", userMetadata)
+	}
+	req.URL.RawQuery = q.Encode()
+	rr := httptest.NewRecorder()
+	server.Handler.ServeHTTP(rr, req)
+	l.PushInteger(rr.Code)
+	l.PushString(rr.Body.String())
+	return 2
+}
 
-				reqURL, err := url.JoinPath("/repositories", repo, "branches", branch, "objects/stat/user_metadata")
-				check(l, err)
+// diffBranch handles comparing a branch
+func diffBranch(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	branch := lua.CheckString(l, 2)
+	reqURL, err := url.JoinPath("/repositories", repo, "branches", branch, "diff")
+	check(l, err)
+	req, err := newLakeFSJSONRequest(ctx, user, http.MethodGet, reqURL, nil)
+	check(l, err)
+	// query params
+	q := req.URL.Query()
+	if !l.IsNone(3) {
+		q.Add("after", lua.CheckString(l, 3))
+	}
+	if !l.IsNone(4) {
+		q.Add("amount", fmt.Sprintf("%d", lua.CheckInteger(l, 4)))
+	}
+	if !l.IsNone(5) {
+		q.Add("prefix", lua.CheckString(l, 5))
+	}
+	if !l.IsNone(6) {
+		q.Add("delimiter", lua.CheckString(l, 6))
+	}
+	req.URL.RawQuery = q.Encode()
+	return getLakeFSJSONResponse(l, server, req)
+}
 
-				req, err := newLakeFSJSONRequest(ctx, user, http.MethodPut, reqURL, data)
-				check(l, err)
+// commitBranch handles committing changes to a branch
+func commitBranch(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	branch := lua.CheckString(l, 2)
+	message := lua.CheckString(l, 3)
path: pkg/actions/lua/lakefs/client.go
user: login: nopcoder
body: good point: Din't want to include this in the implementation, and when I think about it - the next commit I'll remove also the force option.

These flags are used internally and not in common use - prefer to keep it simple and if there will be a need, we can add them and not try to support them when there is no need.
created_at: 2025-03-09T17:33:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -76,190 +77,243 @@ func getLakeFSJSONResponse(l *lua.State, server *http.Server, request *http.Requ
 	return 1 + util.DeepPush(l, output)
 }
 
-func OpenClient(l *lua.State, ctx context.Context, user *model.User, server *http.Server) {
-	clientOpen := func(l *lua.State) int {
-		lua.NewLibrary(l, []lua.RegistryFunction{
-			{Name: "update_object_user_metadata", Function: func(state *lua.State) int {
-				repo := lua.CheckString(l, 1)
-				branch := lua.CheckString(l, 2)
-				objPath := lua.CheckString(l, 3)
-				metadata, err := util.PullStringTable(l, 4)
-				check(l, err)
+// updateObjectUserMetadata handles updating object metadata
+func updateObjectUserMetadata(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	branch := lua.CheckString(l, 2)
+	objPath := lua.CheckString(l, 3)
+	metadata, err := util.PullStringTable(l, 4)
+	check(l, err)
+
+	data, err := json.Marshal(map[string]interface{}{
+		"set": metadata,
+	})
+	check(l, err)
+
+	reqURL, err := url.JoinPath("/repositories", repo, "branches", branch, "objects/stat/user_metadata")
+	check(l, err)
+
+	req, err := newLakeFSJSONRequest(ctx, user, http.MethodPut, reqURL, data)
+	check(l, err)
+
+	// query params
+	q := req.URL.Query()
+	q.Add("path", objPath)
+	req.URL.RawQuery = q.Encode()
+
+	rr := httptest.NewRecorder()
+	server.Handler.ServeHTTP(rr, req)
+	l.PushInteger(rr.Code)
+	l.PushString(rr.Body.String())
+	return 2
+}
+
+// createTag handles tag creation
+func createTag(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	data, err := json.Marshal(map[string]string{
+		"ref": lua.CheckString(l, 2),
+		"id":  lua.CheckString(l, 3),
+	})
+	check(l, err)
+	reqURL, err := url.JoinPath("/repositories", repo, "tags")
+	check(l, err)
+	req, err := newLakeFSJSONRequest(ctx, user, http.MethodPost, reqURL, data)
+	check(l, err)
+	return getLakeFSJSONResponse(l, server, req)
+}
+
+// diffRefs handles comparing two refs
+func diffRefs(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	leftRef := lua.CheckString(l, 2)
+	rightRef := lua.CheckString(l, 3)
+	reqURL, err := url.JoinPath("/repositories", repo, "refs", leftRef, "diff", rightRef)
+	check(l, err)
+	req, err := newLakeFSJSONRequest(ctx, user, http.MethodGet, reqURL, nil)
+	check(l, err)
+	// query params
+	q := req.URL.Query()
+	if !l.IsNone(4) {
+		q.Add("after", lua.CheckString(l, 4))
+	}
+	if !l.IsNone(5) {
+		q.Add("prefix", lua.CheckString(l, 5))
+	}
+	if !l.IsNone(6) {
+		q.Add("delimiter", lua.CheckString(l, 6))
+	}
+	if !l.IsNone(7) {
+		q.Add("amount", fmt.Sprintf("%d", lua.CheckInteger(l, 7)))
+	}
+	req.URL.RawQuery = q.Encode()
+	return getLakeFSJSONResponse(l, server, req)
+}
+
+// listObjects handles listing objects in a ref
+func listObjects(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	ref := lua.CheckString(l, 2)
+	reqURL, err := url.JoinPath("/repositories", repo, "refs", ref, "objects/ls")
+	check(l, err)
+	req, err := newLakeFSJSONRequest(ctx, user, http.MethodGet, reqURL, nil)
+	check(l, err)
+	// query params
+	q := req.URL.Query()
+	if !l.IsNone(3) {
+		q.Add("after", lua.CheckString(l, 3))
+	}
+	if !l.IsNone(4) {
+		q.Add("prefix", lua.CheckString(l, 4))
+	}
+	if !l.IsNone(5) {
+		q.Add("delimiter", lua.CheckString(l, 5))
+	}
+	if !l.IsNone(6) {
+		q.Add("amount", fmt.Sprintf("%d", lua.CheckInteger(l, 6)))
+	}
+	if !l.IsNone(7) {
+		withUserMetadata := "false"
+		if l.ToBoolean(7) {
+			withUserMetadata = "true"
+		}
+		q.Add("user_metadata", withUserMetadata)
+	}
+	req.URL.RawQuery = q.Encode()
+	return getLakeFSJSONResponse(l, server, req)
+}
+
+// getObject handles retrieving an object
+func getObject(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	ref := lua.CheckString(l, 2)
+	reqURL, err := url.JoinPath("/repositories", repo, "refs", ref, "objects")
+	check(l, err)
+	req, err := newLakeFSJSONRequest(ctx, user, http.MethodGet, reqURL, nil)
+	check(l, err)
+	// query params
+	q := req.URL.Query()
+	q.Add("path", lua.CheckString(l, 3))
+	req.URL.RawQuery = q.Encode()
+	rr := httptest.NewRecorder()
+	server.Handler.ServeHTTP(rr, req)
+	l.PushInteger(rr.Code)
+	l.PushString(rr.Body.String())
+	return 2
+}
 
-				data, err := json.Marshal(map[string]interface{}{
-					"set": metadata,
-				})
-				check(l, err)
+// statObject handles retrieving object stats
+func statObject(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	ref := lua.CheckString(l, 2)
+	reqURL, err := url.JoinPath("/repositories", repo, "refs", ref, "objects", "stat")
+	check(l, err)
+	req, err := newLakeFSJSONRequest(ctx, user, http.MethodGet, reqURL, nil)
+	check(l, err)
+	// query params
+	q := req.URL.Query()
+	q.Add("path", lua.CheckString(l, 3))
+	if !l.IsNone(4) {
+		userMetadata := strconv.FormatBool(l.ToBoolean(4))
+		q.Add("user_metadata", userMetadata)
+	}
+	req.URL.RawQuery = q.Encode()
+	rr := httptest.NewRecorder()
+	server.Handler.ServeHTTP(rr, req)
+	l.PushInteger(rr.Code)
+	l.PushString(rr.Body.String())
+	return 2
+}
 
-				reqURL, err := url.JoinPath("/repositories", repo, "branches", branch, "objects/stat/user_metadata")
-				check(l, err)
+// diffBranch handles comparing a branch
+func diffBranch(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	branch := lua.CheckString(l, 2)
+	reqURL, err := url.JoinPath("/repositories", repo, "branches", branch, "diff")
+	check(l, err)
+	req, err := newLakeFSJSONRequest(ctx, user, http.MethodGet, reqURL, nil)
+	check(l, err)
+	// query params
+	q := req.URL.Query()
+	if !l.IsNone(3) {
+		q.Add("after", lua.CheckString(l, 3))
+	}
+	if !l.IsNone(4) {
+		q.Add("amount", fmt.Sprintf("%d", lua.CheckInteger(l, 4)))
+	}
+	if !l.IsNone(5) {
+		q.Add("prefix", lua.CheckString(l, 5))
+	}
+	if !l.IsNone(6) {
+		q.Add("delimiter", lua.CheckString(l, 6))
+	}
+	req.URL.RawQuery = q.Encode()
+	return getLakeFSJSONResponse(l, server, req)
+}
 
-				req, err := newLakeFSJSONRequest(ctx, user, http.MethodPut, reqURL, data)
-				check(l, err)
+// commitBranch handles committing changes to a branch
+func commitBranch(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	branch := lua.CheckString(l, 2)
+	message := lua.CheckString(l, 3)
 
-				// query params
-				q := req.URL.Query()
-				q.Add("path", objPath)
-				req.URL.RawQuery = q.Encode()
+	data := map[string]interface{}{
+		"message": message,
+	}
 
-				rr := httptest.NewRecorder()
-				server.Handler.ServeHTTP(rr, req)
-				l.PushInteger(rr.Code)
-				l.PushString(rr.Body.String())
-				return 2
+	// Handle optional metadata if provided (4th parameter)
+	if !l.IsNone(4) {
+		metadata, err := util.PullStringTable(l, 4)
+		if err != nil {
+			check(l, err)
+		}
+		data["metadata"] = metadata
+	}
+
+	// Handle optional source metadata flag (5th parameter)
+	if !l.IsNone(5) {
+		data["force_if_not_exists"] = l.ToBoolean(5)
path: pkg/actions/lua/lakefs/client.go
user: login: nopcoder
body: dropping the support to fore commit.
created_at: 2025-03-09T17:34:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -76,190 +77,243 @@ func getLakeFSJSONResponse(l *lua.State, server *http.Server, request *http.Requ
 	return 1 + util.DeepPush(l, output)
 }
 
-func OpenClient(l *lua.State, ctx context.Context, user *model.User, server *http.Server) {
-	clientOpen := func(l *lua.State) int {
-		lua.NewLibrary(l, []lua.RegistryFunction{
-			{Name: "update_object_user_metadata", Function: func(state *lua.State) int {
-				repo := lua.CheckString(l, 1)
-				branch := lua.CheckString(l, 2)
-				objPath := lua.CheckString(l, 3)
-				metadata, err := util.PullStringTable(l, 4)
-				check(l, err)
+// updateObjectUserMetadata handles updating object metadata
+func updateObjectUserMetadata(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	branch := lua.CheckString(l, 2)
+	objPath := lua.CheckString(l, 3)
+	metadata, err := util.PullStringTable(l, 4)
+	check(l, err)
+
+	data, err := json.Marshal(map[string]interface{}{
+		"set": metadata,
+	})
+	check(l, err)
+
+	reqURL, err := url.JoinPath("/repositories", repo, "branches", branch, "objects/stat/user_metadata")
+	check(l, err)
+
+	req, err := newLakeFSJSONRequest(ctx, user, http.MethodPut, reqURL, data)
+	check(l, err)
+
+	// query params
+	q := req.URL.Query()
+	q.Add("path", objPath)
+	req.URL.RawQuery = q.Encode()
+
+	rr := httptest.NewRecorder()
+	server.Handler.ServeHTTP(rr, req)
+	l.PushInteger(rr.Code)
+	l.PushString(rr.Body.String())
+	return 2
+}
+
+// createTag handles tag creation
+func createTag(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	data, err := json.Marshal(map[string]string{
+		"ref": lua.CheckString(l, 2),
+		"id":  lua.CheckString(l, 3),
+	})
+	check(l, err)
+	reqURL, err := url.JoinPath("/repositories", repo, "tags")
+	check(l, err)
+	req, err := newLakeFSJSONRequest(ctx, user, http.MethodPost, reqURL, data)
+	check(l, err)
+	return getLakeFSJSONResponse(l, server, req)
+}
+
+// diffRefs handles comparing two refs
+func diffRefs(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	leftRef := lua.CheckString(l, 2)
+	rightRef := lua.CheckString(l, 3)
+	reqURL, err := url.JoinPath("/repositories", repo, "refs", leftRef, "diff", rightRef)
+	check(l, err)
+	req, err := newLakeFSJSONRequest(ctx, user, http.MethodGet, reqURL, nil)
+	check(l, err)
+	// query params
+	q := req.URL.Query()
+	if !l.IsNone(4) {
+		q.Add("after", lua.CheckString(l, 4))
+	}
+	if !l.IsNone(5) {
+		q.Add("prefix", lua.CheckString(l, 5))
+	}
+	if !l.IsNone(6) {
+		q.Add("delimiter", lua.CheckString(l, 6))
+	}
+	if !l.IsNone(7) {
+		q.Add("amount", fmt.Sprintf("%d", lua.CheckInteger(l, 7)))
+	}
+	req.URL.RawQuery = q.Encode()
+	return getLakeFSJSONResponse(l, server, req)
+}
+
+// listObjects handles listing objects in a ref
+func listObjects(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	ref := lua.CheckString(l, 2)
+	reqURL, err := url.JoinPath("/repositories", repo, "refs", ref, "objects/ls")
+	check(l, err)
+	req, err := newLakeFSJSONRequest(ctx, user, http.MethodGet, reqURL, nil)
+	check(l, err)
+	// query params
+	q := req.URL.Query()
+	if !l.IsNone(3) {
+		q.Add("after", lua.CheckString(l, 3))
+	}
+	if !l.IsNone(4) {
+		q.Add("prefix", lua.CheckString(l, 4))
+	}
+	if !l.IsNone(5) {
+		q.Add("delimiter", lua.CheckString(l, 5))
+	}
+	if !l.IsNone(6) {
+		q.Add("amount", fmt.Sprintf("%d", lua.CheckInteger(l, 6)))
+	}
+	if !l.IsNone(7) {
+		withUserMetadata := "false"
+		if l.ToBoolean(7) {
+			withUserMetadata = "true"
+		}
+		q.Add("user_metadata", withUserMetadata)
+	}
+	req.URL.RawQuery = q.Encode()
+	return getLakeFSJSONResponse(l, server, req)
+}
+
+// getObject handles retrieving an object
+func getObject(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	ref := lua.CheckString(l, 2)
+	reqURL, err := url.JoinPath("/repositories", repo, "refs", ref, "objects")
+	check(l, err)
+	req, err := newLakeFSJSONRequest(ctx, user, http.MethodGet, reqURL, nil)
+	check(l, err)
+	// query params
+	q := req.URL.Query()
+	q.Add("path", lua.CheckString(l, 3))
+	req.URL.RawQuery = q.Encode()
+	rr := httptest.NewRecorder()
+	server.Handler.ServeHTTP(rr, req)
+	l.PushInteger(rr.Code)
+	l.PushString(rr.Body.String())
+	return 2
+}
 
-				data, err := json.Marshal(map[string]interface{}{
-					"set": metadata,
-				})
-				check(l, err)
+// statObject handles retrieving object stats
+func statObject(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	ref := lua.CheckString(l, 2)
+	reqURL, err := url.JoinPath("/repositories", repo, "refs", ref, "objects", "stat")
+	check(l, err)
+	req, err := newLakeFSJSONRequest(ctx, user, http.MethodGet, reqURL, nil)
+	check(l, err)
+	// query params
+	q := req.URL.Query()
+	q.Add("path", lua.CheckString(l, 3))
+	if !l.IsNone(4) {
+		userMetadata := strconv.FormatBool(l.ToBoolean(4))
+		q.Add("user_metadata", userMetadata)
+	}
+	req.URL.RawQuery = q.Encode()
+	rr := httptest.NewRecorder()
+	server.Handler.ServeHTTP(rr, req)
+	l.PushInteger(rr.Code)
+	l.PushString(rr.Body.String())
+	return 2
+}
 
-				reqURL, err := url.JoinPath("/repositories", repo, "branches", branch, "objects/stat/user_metadata")
-				check(l, err)
+// diffBranch handles comparing a branch
+func diffBranch(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	branch := lua.CheckString(l, 2)
+	reqURL, err := url.JoinPath("/repositories", repo, "branches", branch, "diff")
+	check(l, err)
+	req, err := newLakeFSJSONRequest(ctx, user, http.MethodGet, reqURL, nil)
+	check(l, err)
+	// query params
+	q := req.URL.Query()
+	if !l.IsNone(3) {
+		q.Add("after", lua.CheckString(l, 3))
+	}
+	if !l.IsNone(4) {
+		q.Add("amount", fmt.Sprintf("%d", lua.CheckInteger(l, 4)))
+	}
+	if !l.IsNone(5) {
+		q.Add("prefix", lua.CheckString(l, 5))
+	}
+	if !l.IsNone(6) {
+		q.Add("delimiter", lua.CheckString(l, 6))
+	}
+	req.URL.RawQuery = q.Encode()
+	return getLakeFSJSONResponse(l, server, req)
+}
 
-				req, err := newLakeFSJSONRequest(ctx, user, http.MethodPut, reqURL, data)
-				check(l, err)
+// commitBranch handles committing changes to a branch
+func commitBranch(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
path: pkg/actions/lua/lakefs/client.go
user: login: nopcoder
body: good point, I'll keep allow_empty as I think it is the functionality I would like to have.
created_at: 2025-03-09T17:42:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -76,190 +77,243 @@ func getLakeFSJSONResponse(l *lua.State, server *http.Server, request *http.Requ
 	return 1 + util.DeepPush(l, output)
 }
 
-func OpenClient(l *lua.State, ctx context.Context, user *model.User, server *http.Server) {
-	clientOpen := func(l *lua.State) int {
-		lua.NewLibrary(l, []lua.RegistryFunction{
-			{Name: "update_object_user_metadata", Function: func(state *lua.State) int {
-				repo := lua.CheckString(l, 1)
-				branch := lua.CheckString(l, 2)
-				objPath := lua.CheckString(l, 3)
-				metadata, err := util.PullStringTable(l, 4)
-				check(l, err)
+// updateObjectUserMetadata handles updating object metadata
+func updateObjectUserMetadata(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	branch := lua.CheckString(l, 2)
+	objPath := lua.CheckString(l, 3)
+	metadata, err := util.PullStringTable(l, 4)
+	check(l, err)
+
+	data, err := json.Marshal(map[string]interface{}{
+		"set": metadata,
+	})
+	check(l, err)
+
+	reqURL, err := url.JoinPath("/repositories", repo, "branches", branch, "objects/stat/user_metadata")
+	check(l, err)
+
+	req, err := newLakeFSJSONRequest(ctx, user, http.MethodPut, reqURL, data)
+	check(l, err)
+
+	// query params
+	q := req.URL.Query()
+	q.Add("path", objPath)
+	req.URL.RawQuery = q.Encode()
+
+	rr := httptest.NewRecorder()
+	server.Handler.ServeHTTP(rr, req)
+	l.PushInteger(rr.Code)
+	l.PushString(rr.Body.String())
+	return 2
+}
+
+// createTag handles tag creation
+func createTag(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	data, err := json.Marshal(map[string]string{
+		"ref": lua.CheckString(l, 2),
+		"id":  lua.CheckString(l, 3),
+	})
+	check(l, err)
+	reqURL, err := url.JoinPath("/repositories", repo, "tags")
+	check(l, err)
+	req, err := newLakeFSJSONRequest(ctx, user, http.MethodPost, reqURL, data)
+	check(l, err)
+	return getLakeFSJSONResponse(l, server, req)
+}
+
+// diffRefs handles comparing two refs
+func diffRefs(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	leftRef := lua.CheckString(l, 2)
+	rightRef := lua.CheckString(l, 3)
+	reqURL, err := url.JoinPath("/repositories", repo, "refs", leftRef, "diff", rightRef)
+	check(l, err)
+	req, err := newLakeFSJSONRequest(ctx, user, http.MethodGet, reqURL, nil)
+	check(l, err)
+	// query params
+	q := req.URL.Query()
+	if !l.IsNone(4) {
+		q.Add("after", lua.CheckString(l, 4))
+	}
+	if !l.IsNone(5) {
+		q.Add("prefix", lua.CheckString(l, 5))
+	}
+	if !l.IsNone(6) {
+		q.Add("delimiter", lua.CheckString(l, 6))
+	}
+	if !l.IsNone(7) {
+		q.Add("amount", fmt.Sprintf("%d", lua.CheckInteger(l, 7)))
+	}
+	req.URL.RawQuery = q.Encode()
+	return getLakeFSJSONResponse(l, server, req)
+}
+
+// listObjects handles listing objects in a ref
+func listObjects(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	ref := lua.CheckString(l, 2)
+	reqURL, err := url.JoinPath("/repositories", repo, "refs", ref, "objects/ls")
+	check(l, err)
+	req, err := newLakeFSJSONRequest(ctx, user, http.MethodGet, reqURL, nil)
+	check(l, err)
+	// query params
+	q := req.URL.Query()
+	if !l.IsNone(3) {
+		q.Add("after", lua.CheckString(l, 3))
+	}
+	if !l.IsNone(4) {
+		q.Add("prefix", lua.CheckString(l, 4))
+	}
+	if !l.IsNone(5) {
+		q.Add("delimiter", lua.CheckString(l, 5))
+	}
+	if !l.IsNone(6) {
+		q.Add("amount", fmt.Sprintf("%d", lua.CheckInteger(l, 6)))
+	}
+	if !l.IsNone(7) {
+		withUserMetadata := "false"
+		if l.ToBoolean(7) {
+			withUserMetadata = "true"
+		}
+		q.Add("user_metadata", withUserMetadata)
+	}
+	req.URL.RawQuery = q.Encode()
+	return getLakeFSJSONResponse(l, server, req)
+}
+
+// getObject handles retrieving an object
+func getObject(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	ref := lua.CheckString(l, 2)
+	reqURL, err := url.JoinPath("/repositories", repo, "refs", ref, "objects")
+	check(l, err)
+	req, err := newLakeFSJSONRequest(ctx, user, http.MethodGet, reqURL, nil)
+	check(l, err)
+	// query params
+	q := req.URL.Query()
+	q.Add("path", lua.CheckString(l, 3))
+	req.URL.RawQuery = q.Encode()
+	rr := httptest.NewRecorder()
+	server.Handler.ServeHTTP(rr, req)
+	l.PushInteger(rr.Code)
+	l.PushString(rr.Body.String())
+	return 2
+}
 
-				data, err := json.Marshal(map[string]interface{}{
-					"set": metadata,
-				})
-				check(l, err)
+// statObject handles retrieving object stats
+func statObject(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	ref := lua.CheckString(l, 2)
+	reqURL, err := url.JoinPath("/repositories", repo, "refs", ref, "objects", "stat")
+	check(l, err)
+	req, err := newLakeFSJSONRequest(ctx, user, http.MethodGet, reqURL, nil)
+	check(l, err)
+	// query params
+	q := req.URL.Query()
+	q.Add("path", lua.CheckString(l, 3))
+	if !l.IsNone(4) {
+		userMetadata := strconv.FormatBool(l.ToBoolean(4))
+		q.Add("user_metadata", userMetadata)
+	}
+	req.URL.RawQuery = q.Encode()
+	rr := httptest.NewRecorder()
+	server.Handler.ServeHTTP(rr, req)
+	l.PushInteger(rr.Code)
+	l.PushString(rr.Body.String())
+	return 2
+}
 
-				reqURL, err := url.JoinPath("/repositories", repo, "branches", branch, "objects/stat/user_metadata")
-				check(l, err)
+// diffBranch handles comparing a branch
+func diffBranch(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	branch := lua.CheckString(l, 2)
+	reqURL, err := url.JoinPath("/repositories", repo, "branches", branch, "diff")
+	check(l, err)
+	req, err := newLakeFSJSONRequest(ctx, user, http.MethodGet, reqURL, nil)
+	check(l, err)
+	// query params
+	q := req.URL.Query()
+	if !l.IsNone(3) {
+		q.Add("after", lua.CheckString(l, 3))
+	}
+	if !l.IsNone(4) {
+		q.Add("amount", fmt.Sprintf("%d", lua.CheckInteger(l, 4)))
+	}
+	if !l.IsNone(5) {
+		q.Add("prefix", lua.CheckString(l, 5))
+	}
+	if !l.IsNone(6) {
+		q.Add("delimiter", lua.CheckString(l, 6))
+	}
+	req.URL.RawQuery = q.Encode()
+	return getLakeFSJSONResponse(l, server, req)
+}
 
-				req, err := newLakeFSJSONRequest(ctx, user, http.MethodPut, reqURL, data)
-				check(l, err)
+// commitBranch handles committing changes to a branch
+func commitBranch(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	branch := lua.CheckString(l, 2)
+	message := lua.CheckString(l, 3)
 
-				// query params
-				q := req.URL.Query()
-				q.Add("path", objPath)
-				req.URL.RawQuery = q.Encode()
+	data := map[string]interface{}{
+		"message": message,
+	}
 
-				rr := httptest.NewRecorder()
-				server.Handler.ServeHTTP(rr, req)
-				l.PushInteger(rr.Code)
-				l.PushString(rr.Body.String())
-				return 2
+	// Handle optional metadata if provided (4th parameter)
+	if !l.IsNone(4) {
+		metadata, err := util.PullStringTable(l, 4)
+		if err != nil {
+			check(l, err)
+		}
+		data["metadata"] = metadata
path: pkg/actions/lua/lakefs/client.go
user: login: nopcoder
body: checked manually the commit of object metadata, I'll check the commit's optionals too
created_at: 2025-03-09T17:43:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -76,190 +77,243 @@ func getLakeFSJSONResponse(l *lua.State, server *http.Server, request *http.Requ
 	return 1 + util.DeepPush(l, output)
 }
 
-func OpenClient(l *lua.State, ctx context.Context, user *model.User, server *http.Server) {
-	clientOpen := func(l *lua.State) int {
-		lua.NewLibrary(l, []lua.RegistryFunction{
-			{Name: "update_object_user_metadata", Function: func(state *lua.State) int {
-				repo := lua.CheckString(l, 1)
-				branch := lua.CheckString(l, 2)
-				objPath := lua.CheckString(l, 3)
-				metadata, err := util.PullStringTable(l, 4)
-				check(l, err)
+// updateObjectUserMetadata handles updating object metadata
+func updateObjectUserMetadata(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	branch := lua.CheckString(l, 2)
+	objPath := lua.CheckString(l, 3)
+	metadata, err := util.PullStringTable(l, 4)
+	check(l, err)
+
+	data, err := json.Marshal(map[string]interface{}{
+		"set": metadata,
+	})
+	check(l, err)
+
+	reqURL, err := url.JoinPath("/repositories", repo, "branches", branch, "objects/stat/user_metadata")
+	check(l, err)
+
+	req, err := newLakeFSJSONRequest(ctx, user, http.MethodPut, reqURL, data)
+	check(l, err)
+
+	// query params
+	q := req.URL.Query()
+	q.Add("path", objPath)
+	req.URL.RawQuery = q.Encode()
+
+	rr := httptest.NewRecorder()
+	server.Handler.ServeHTTP(rr, req)
+	l.PushInteger(rr.Code)
+	l.PushString(rr.Body.String())
+	return 2
+}
+
+// createTag handles tag creation
+func createTag(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	data, err := json.Marshal(map[string]string{
+		"ref": lua.CheckString(l, 2),
+		"id":  lua.CheckString(l, 3),
+	})
+	check(l, err)
+	reqURL, err := url.JoinPath("/repositories", repo, "tags")
+	check(l, err)
+	req, err := newLakeFSJSONRequest(ctx, user, http.MethodPost, reqURL, data)
+	check(l, err)
+	return getLakeFSJSONResponse(l, server, req)
+}
+
+// diffRefs handles comparing two refs
+func diffRefs(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	leftRef := lua.CheckString(l, 2)
+	rightRef := lua.CheckString(l, 3)
+	reqURL, err := url.JoinPath("/repositories", repo, "refs", leftRef, "diff", rightRef)
+	check(l, err)
+	req, err := newLakeFSJSONRequest(ctx, user, http.MethodGet, reqURL, nil)
+	check(l, err)
+	// query params
+	q := req.URL.Query()
+	if !l.IsNone(4) {
+		q.Add("after", lua.CheckString(l, 4))
+	}
+	if !l.IsNone(5) {
+		q.Add("prefix", lua.CheckString(l, 5))
+	}
+	if !l.IsNone(6) {
+		q.Add("delimiter", lua.CheckString(l, 6))
+	}
+	if !l.IsNone(7) {
+		q.Add("amount", fmt.Sprintf("%d", lua.CheckInteger(l, 7)))
+	}
+	req.URL.RawQuery = q.Encode()
+	return getLakeFSJSONResponse(l, server, req)
+}
+
+// listObjects handles listing objects in a ref
+func listObjects(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	ref := lua.CheckString(l, 2)
+	reqURL, err := url.JoinPath("/repositories", repo, "refs", ref, "objects/ls")
+	check(l, err)
+	req, err := newLakeFSJSONRequest(ctx, user, http.MethodGet, reqURL, nil)
+	check(l, err)
+	// query params
+	q := req.URL.Query()
+	if !l.IsNone(3) {
+		q.Add("after", lua.CheckString(l, 3))
+	}
+	if !l.IsNone(4) {
+		q.Add("prefix", lua.CheckString(l, 4))
+	}
+	if !l.IsNone(5) {
+		q.Add("delimiter", lua.CheckString(l, 5))
+	}
+	if !l.IsNone(6) {
+		q.Add("amount", fmt.Sprintf("%d", lua.CheckInteger(l, 6)))
+	}
+	if !l.IsNone(7) {
+		withUserMetadata := "false"
+		if l.ToBoolean(7) {
+			withUserMetadata = "true"
+		}
+		q.Add("user_metadata", withUserMetadata)
+	}
+	req.URL.RawQuery = q.Encode()
+	return getLakeFSJSONResponse(l, server, req)
+}
+
+// getObject handles retrieving an object
+func getObject(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	ref := lua.CheckString(l, 2)
+	reqURL, err := url.JoinPath("/repositories", repo, "refs", ref, "objects")
+	check(l, err)
+	req, err := newLakeFSJSONRequest(ctx, user, http.MethodGet, reqURL, nil)
+	check(l, err)
+	// query params
+	q := req.URL.Query()
+	q.Add("path", lua.CheckString(l, 3))
+	req.URL.RawQuery = q.Encode()
+	rr := httptest.NewRecorder()
+	server.Handler.ServeHTTP(rr, req)
+	l.PushInteger(rr.Code)
+	l.PushString(rr.Body.String())
+	return 2
+}
 
-				data, err := json.Marshal(map[string]interface{}{
-					"set": metadata,
-				})
-				check(l, err)
+// statObject handles retrieving object stats
+func statObject(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	ref := lua.CheckString(l, 2)
+	reqURL, err := url.JoinPath("/repositories", repo, "refs", ref, "objects", "stat")
+	check(l, err)
+	req, err := newLakeFSJSONRequest(ctx, user, http.MethodGet, reqURL, nil)
+	check(l, err)
+	// query params
+	q := req.URL.Query()
+	q.Add("path", lua.CheckString(l, 3))
+	if !l.IsNone(4) {
+		userMetadata := strconv.FormatBool(l.ToBoolean(4))
+		q.Add("user_metadata", userMetadata)
+	}
+	req.URL.RawQuery = q.Encode()
+	rr := httptest.NewRecorder()
+	server.Handler.ServeHTTP(rr, req)
+	l.PushInteger(rr.Code)
+	l.PushString(rr.Body.String())
+	return 2
+}
 
-				reqURL, err := url.JoinPath("/repositories", repo, "branches", branch, "objects/stat/user_metadata")
-				check(l, err)
+// diffBranch handles comparing a branch
+func diffBranch(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	branch := lua.CheckString(l, 2)
+	reqURL, err := url.JoinPath("/repositories", repo, "branches", branch, "diff")
+	check(l, err)
+	req, err := newLakeFSJSONRequest(ctx, user, http.MethodGet, reqURL, nil)
+	check(l, err)
+	// query params
+	q := req.URL.Query()
+	if !l.IsNone(3) {
+		q.Add("after", lua.CheckString(l, 3))
+	}
+	if !l.IsNone(4) {
+		q.Add("amount", fmt.Sprintf("%d", lua.CheckInteger(l, 4)))
+	}
+	if !l.IsNone(5) {
+		q.Add("prefix", lua.CheckString(l, 5))
+	}
+	if !l.IsNone(6) {
+		q.Add("delimiter", lua.CheckString(l, 6))
+	}
+	req.URL.RawQuery = q.Encode()
+	return getLakeFSJSONResponse(l, server, req)
+}
 
-				req, err := newLakeFSJSONRequest(ctx, user, http.MethodPut, reqURL, data)
-				check(l, err)
+// commitBranch handles committing changes to a branch
+func commitBranch(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	branch := lua.CheckString(l, 2)
+	message := lua.CheckString(l, 3)
 
-				// query params
-				q := req.URL.Query()
-				q.Add("path", objPath)
-				req.URL.RawQuery = q.Encode()
+	data := map[string]interface{}{
+		"message": message,
+	}
 
-				rr := httptest.NewRecorder()
-				server.Handler.ServeHTTP(rr, req)
-				l.PushInteger(rr.Code)
-				l.PushString(rr.Body.String())
-				return 2
+	// Handle optional metadata if provided (4th parameter)
+	if !l.IsNone(4) {
+		metadata, err := util.PullStringTable(l, 4)
+		if err != nil {
+			check(l, err)
+		}
+		data["metadata"] = metadata
path: pkg/actions/lua/lakefs/client.go
user: login: nopcoder
body: show me the action log tomorrow - I've tested commit with metadata and it worked.
created_at: 2025-03-09T20:25:13Z
author_association: CONTRIBUTOR
path: pkg/actions/lua/lakefs/client.go
user: login: nopcoder
body: Added mechanism to prevent recursive commits triggered by a commit call from lua.
created_at: 2025-03-09T20:25:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -76,190 +77,243 @@ func getLakeFSJSONResponse(l *lua.State, server *http.Server, request *http.Requ
 	return 1 + util.DeepPush(l, output)
 }
 
-func OpenClient(l *lua.State, ctx context.Context, user *model.User, server *http.Server) {
-	clientOpen := func(l *lua.State) int {
-		lua.NewLibrary(l, []lua.RegistryFunction{
-			{Name: "update_object_user_metadata", Function: func(state *lua.State) int {
-				repo := lua.CheckString(l, 1)
-				branch := lua.CheckString(l, 2)
-				objPath := lua.CheckString(l, 3)
-				metadata, err := util.PullStringTable(l, 4)
-				check(l, err)
+// updateObjectUserMetadata handles updating object metadata
+func updateObjectUserMetadata(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	branch := lua.CheckString(l, 2)
+	objPath := lua.CheckString(l, 3)
+	metadata, err := util.PullStringTable(l, 4)
+	check(l, err)
+
+	data, err := json.Marshal(map[string]interface{}{
+		"set": metadata,
+	})
+	check(l, err)
+
+	reqURL, err := url.JoinPath("/repositories", repo, "branches", branch, "objects/stat/user_metadata")
+	check(l, err)
+
+	req, err := newLakeFSJSONRequest(ctx, user, http.MethodPut, reqURL, data)
+	check(l, err)
+
+	// query params
+	q := req.URL.Query()
+	q.Add("path", objPath)
+	req.URL.RawQuery = q.Encode()
+
+	rr := httptest.NewRecorder()
+	server.Handler.ServeHTTP(rr, req)
+	l.PushInteger(rr.Code)
+	l.PushString(rr.Body.String())
+	return 2
+}
+
+// createTag handles tag creation
+func createTag(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	data, err := json.Marshal(map[string]string{
+		"ref": lua.CheckString(l, 2),
+		"id":  lua.CheckString(l, 3),
+	})
+	check(l, err)
+	reqURL, err := url.JoinPath("/repositories", repo, "tags")
+	check(l, err)
+	req, err := newLakeFSJSONRequest(ctx, user, http.MethodPost, reqURL, data)
+	check(l, err)
+	return getLakeFSJSONResponse(l, server, req)
+}
+
+// diffRefs handles comparing two refs
+func diffRefs(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	leftRef := lua.CheckString(l, 2)
+	rightRef := lua.CheckString(l, 3)
+	reqURL, err := url.JoinPath("/repositories", repo, "refs", leftRef, "diff", rightRef)
+	check(l, err)
+	req, err := newLakeFSJSONRequest(ctx, user, http.MethodGet, reqURL, nil)
+	check(l, err)
+	// query params
+	q := req.URL.Query()
+	if !l.IsNone(4) {
+		q.Add("after", lua.CheckString(l, 4))
+	}
+	if !l.IsNone(5) {
+		q.Add("prefix", lua.CheckString(l, 5))
+	}
+	if !l.IsNone(6) {
+		q.Add("delimiter", lua.CheckString(l, 6))
+	}
+	if !l.IsNone(7) {
+		q.Add("amount", fmt.Sprintf("%d", lua.CheckInteger(l, 7)))
+	}
+	req.URL.RawQuery = q.Encode()
+	return getLakeFSJSONResponse(l, server, req)
+}
+
+// listObjects handles listing objects in a ref
+func listObjects(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	ref := lua.CheckString(l, 2)
+	reqURL, err := url.JoinPath("/repositories", repo, "refs", ref, "objects/ls")
+	check(l, err)
+	req, err := newLakeFSJSONRequest(ctx, user, http.MethodGet, reqURL, nil)
+	check(l, err)
+	// query params
+	q := req.URL.Query()
+	if !l.IsNone(3) {
+		q.Add("after", lua.CheckString(l, 3))
+	}
+	if !l.IsNone(4) {
+		q.Add("prefix", lua.CheckString(l, 4))
+	}
+	if !l.IsNone(5) {
+		q.Add("delimiter", lua.CheckString(l, 5))
+	}
+	if !l.IsNone(6) {
+		q.Add("amount", fmt.Sprintf("%d", lua.CheckInteger(l, 6)))
+	}
+	if !l.IsNone(7) {
+		withUserMetadata := "false"
+		if l.ToBoolean(7) {
+			withUserMetadata = "true"
+		}
+		q.Add("user_metadata", withUserMetadata)
+	}
+	req.URL.RawQuery = q.Encode()
+	return getLakeFSJSONResponse(l, server, req)
+}
+
+// getObject handles retrieving an object
+func getObject(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	ref := lua.CheckString(l, 2)
+	reqURL, err := url.JoinPath("/repositories", repo, "refs", ref, "objects")
+	check(l, err)
+	req, err := newLakeFSJSONRequest(ctx, user, http.MethodGet, reqURL, nil)
+	check(l, err)
+	// query params
+	q := req.URL.Query()
+	q.Add("path", lua.CheckString(l, 3))
+	req.URL.RawQuery = q.Encode()
+	rr := httptest.NewRecorder()
+	server.Handler.ServeHTTP(rr, req)
+	l.PushInteger(rr.Code)
+	l.PushString(rr.Body.String())
+	return 2
+}
 
-				data, err := json.Marshal(map[string]interface{}{
-					"set": metadata,
-				})
-				check(l, err)
+// statObject handles retrieving object stats
+func statObject(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	ref := lua.CheckString(l, 2)
+	reqURL, err := url.JoinPath("/repositories", repo, "refs", ref, "objects", "stat")
+	check(l, err)
+	req, err := newLakeFSJSONRequest(ctx, user, http.MethodGet, reqURL, nil)
+	check(l, err)
+	// query params
+	q := req.URL.Query()
+	q.Add("path", lua.CheckString(l, 3))
+	if !l.IsNone(4) {
+		userMetadata := strconv.FormatBool(l.ToBoolean(4))
+		q.Add("user_metadata", userMetadata)
+	}
+	req.URL.RawQuery = q.Encode()
+	rr := httptest.NewRecorder()
+	server.Handler.ServeHTTP(rr, req)
+	l.PushInteger(rr.Code)
+	l.PushString(rr.Body.String())
+	return 2
+}
 
-				reqURL, err := url.JoinPath("/repositories", repo, "branches", branch, "objects/stat/user_metadata")
-				check(l, err)
+// diffBranch handles comparing a branch
+func diffBranch(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	branch := lua.CheckString(l, 2)
+	reqURL, err := url.JoinPath("/repositories", repo, "branches", branch, "diff")
+	check(l, err)
+	req, err := newLakeFSJSONRequest(ctx, user, http.MethodGet, reqURL, nil)
+	check(l, err)
+	// query params
+	q := req.URL.Query()
+	if !l.IsNone(3) {
+		q.Add("after", lua.CheckString(l, 3))
+	}
+	if !l.IsNone(4) {
+		q.Add("amount", fmt.Sprintf("%d", lua.CheckInteger(l, 4)))
+	}
+	if !l.IsNone(5) {
+		q.Add("prefix", lua.CheckString(l, 5))
+	}
+	if !l.IsNone(6) {
+		q.Add("delimiter", lua.CheckString(l, 6))
+	}
+	req.URL.RawQuery = q.Encode()
+	return getLakeFSJSONResponse(l, server, req)
+}
 
-				req, err := newLakeFSJSONRequest(ctx, user, http.MethodPut, reqURL, data)
-				check(l, err)
+// commitBranch handles committing changes to a branch
+func commitBranch(l *lua.State, ctx context.Context, user *model.User, server *http.Server) int {
+	repo := lua.CheckString(l, 1)
+	branch := lua.CheckString(l, 2)
+	message := lua.CheckString(l, 3)
 
-				// query params
-				q := req.URL.Query()
-				q.Add("path", objPath)
-				req.URL.RawQuery = q.Encode()
+	data := map[string]interface{}{
+		"message": message,
+	}
 
-				rr := httptest.NewRecorder()
-				server.Handler.ServeHTTP(rr, req)
-				l.PushInteger(rr.Code)
-				l.PushString(rr.Body.String())
-				return 2
+	// Handle optional metadata if provided (4th parameter)
+	if !l.IsNone(4) {
+		metadata, err := util.PullStringTable(l, 4)
+		if err != nil {
+			check(l, err)
+		}
+		data["metadata"] = metadata
path: pkg/actions/lua/lakefs/client.go
user: login: Isan-Rivkin
body: OK it works, the problem was that I passed passed the metadata table as the 4th input but I should of pass `options = { "metadata" : {} }` 
docs should be updated (commenting there).
created_at: 2025-03-10T07:01:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -516,9 +516,61 @@ Returns 2 values:
 
 Returns an object-wise diff of uncommitted changes on `branch_id`.
 
-### `lakefs/stat_object(repository_id, ref_id, path)`
+### `lakefs/stat_object(repository_id, ref_id, path[, user_metadata])`
 
 Returns a stat object for the given path under the given reference and repository.
+Returns 2 values:
+
+1. The HTTP status code returned by the lakeFS API
+2. The stat response object as a JSON string
+
+Parameters:
+
+- `repository_id`: The repository ID
+- `ref_id`: The reference to stat from (branch, tag, commit ID)
+- `path`: Path to the object to stat
+- `user_metadata`: (Optional) Boolean flag to include user metadata in response
+
+### `lakefs/update_object_user_metadata(repository_id, branch_id, path, metadata)`
+
+Update user metadata for an object.
+
+Parameters:
+
+- `repository_id`: The repository ID
+- `branch_id`: The branch containing the object
+- `path`: Path to the object to update
+- `metadata`: A table containing key-value pairs to set as user metadata
+
+Returns 2 values:
+
+1. The HTTP status code returned by the lakeFS API (204 on success)
+2. Empty on success, error on failure
+
+### `lakefs/commit(repository_id, branch_id, message [, options])`
+
+Commit changes to a branch.
+
+Parameters:
+
+- `repository_id`: The repository ID
+- `branch_id`: The branch to commit to
+- `message`: Commit message
+- `metadata`: (Optional) A table containing key-value pairs to attach as commit metadata
path: docs/howto/hooks/lua.md
user: login: Isan-Rivkin
body: Remove
created_at: 2025-03-10T07:01:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2145,7 +2151,7 @@ func (g *Graveler) Commit(ctx context.Context, repository *RepositoryRecord, bra
 			commit.Parents = CommitParents{branch.CommitID}
 		}
 
-		if !repository.ReadOnly {
+		if !repository.ReadOnly && !params.NoHooks {
path: pkg/graveler/graveler.go
user: login: arielshaqed
body: I don't like this, it breaks what pre-commit hooks guarantee!  For instance, [this blog post](https://lakefs.io/blog/lakefs-hooks/#clone-and-start-using-it-now) suggests using it to guarantee that commits have required metadata - and this by now an established pattern with some of our users.  Giving Lua hooks a pass from having metadata enforced is unsafe and unexpected.
created_at: 2025-03-10T07:07:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -45,6 +46,9 @@ func newLakeFSRequest(ctx context.Context, user *model.User, method, reqURL stri
 		body = bytes.NewReader(data)
 	}
 
+	// Set no_hooks to true to disable recursive hooks
+	ctx = context.WithValue(ctx, graveler.ContextKeyNoHooks, true)
path: pkg/actions/lua/lakefs/client.go
user: login: Isan-Rivkin
body: I get why this works but I'm really not a fan of this approach, especially because it's implicit. 
Think of user doing commit to **another** branch they don't need this skip... 

created_at: 2025-03-10T08:08:37Z
author_association: CONTRIBUTOR
path: pkg/actions/lua/lakefs/client.go
user: login: nopcoder
body: Removed the commit functionality
created_at: 2025-03-10T16:13:31Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-03-09T10:10:19Z
body: 
  # E2E Test Results - Quickstart

  <img src="https://svg.test-summary.com/dashboard.svg?p=11&f=0&s=0" alt="11 passed">

author_association: NONE
user: login: github-actions[bot]
created_at: 2025-03-09T10:10:28Z
body: 
  # E2E Test Results - DynamoDB Local - Local Block Adapter

  <img src="https://svg.test-summary.com/dashboard.svg?p=14&f=0&s=0" alt="14 passed">

author_association: NONE
user: login: github-actions[bot]
created_at: 2025-03-09T21:53:34Z
body: 
:recycle: [PR Preview](https://treeverse-lakeFS-preview-pr-8777.surge.sh) f9b10d15f35df218824fa3823181558956272c63 has been successfully destroyed since this PR has been closed.

<a href="https://github.com/treeverse/lakeFS/actions/runs/13787993052"><img width="300" src="https://user-images.githubusercontent.com/507615/98094112-d838f700-1ec3-11eb-8530-381c2276b80e.png"></a>
        
<sub>🤖 By [surge-preview](https://github.com/afc163/surge-preview)</sub>
      
<!-- Sticky Pull Request Comment: Surge Preview preview' -->
author_association: NONE
user: login: nopcoder
created_at: 2025-03-09T21:53:58Z
body: Thanks @Isan-Rivkin great feedback in this review I've improved the code, docs and the edge-cases the code handles.
author_association: CONTRIBUTOR
user: login: nopcoder
created_at: 2025-03-09T22:03:15Z
body: @Isan-Rivkin @arielshaqed would like your input too about the blocking of commit hooks from within lua hook.
My thought was to block any commits pre/post hook in case the commit was triggered from inside lua code. The concern that the caller will perform a recursive commit that will never end.
The other level of supports I was considered:
- scope to commit: block common hooks only if the lua code run inside a commit hook (limit the scope)
- open to all, opt in: block by default by passing option to commit api. lua client will have this flag on by default and the caller can specify true in case it handle all the edge cases.
author_association: CONTRIBUTOR
user: login: arielshaqed
created_at: 2025-03-10T07:02:36Z
body: Hmmm.... nice catch, @Isan-Rivkin!

IMHO: The most important requirement is not to allow a user to crash the lakeFS server.  Committing from a Lua hook to a branch before committing to it is a definite way to do that, so we need to block it absolutely, with no escape hatches.  And that should happen in the very first version, otherwise we have a security bug "authenticated user allowed to install Lua hooks can crash lakeFS server".

That still leaves the question of how to carve out exceptions to make things usable.  The thing is, there are many use-cases for committing during a commit... as long as that commit goes to _another repository_ (or, possibly, another branch).

So ideally we would block commits to the same branch, while allowing them to other repositories and branches.

Some options:

- Do exactly this, with full generality.  For instance, keep on the context the set of all running actions ({<trigger parameters (repo, branch, perhaps more?), action id, hook>, ...}).  Fail any action if it is ever re-inserted into the set.  I like this a lot, because it fixes _all_ cases of a pre- hook that manages to cause itself to run - and I am sure there will be many more.  And also this explicitly only limits clearly wrong cases. ✅ 
- Do exactly this, specifically.  For instance, when a commit starts put on the context an indication of a commit on that branch, and don't allow commits to that branch until it is done.  I don't like this, I think it leaves too much potential to repeat this bug with minor changes and @Isan-Rivkin might not review those.
- Do exactly this, with full generality, but at the Graveler level.  For instance, whenever a branchUpdate starts, keep on the context the branch being updated.  Now refuse to start a new branchUpdate on that context.  I like this a lot too, because it might be simple and quite general. ✅ 
- Don't give Lua pre-commit a "commit" operation.  This will work, and it is safe.  I don't like it that much because it prevents perfectly reasonable operations.

Options I do not like:

- Calls performed by Lua hooks (or hooks in general) don't run their pre- hooks.  Doing this solves the problem, but it means that lakeFS no longer enforces the pre- hook.


author_association: CONTRIBUTOR
user: login: nopcoder
created_at: 2025-03-10T16:04:18Z
body: @Isan-Rivkin @arielshaqed thanks again for all the feedback, my current plan for this PR is to remove the commit functionality and keep the docs and updates for the stats object.

In a different PR will address the issue, based on feedback from @ozkatz, and enable prepare commit event to enable users changes before the commit. 
author_association: CONTRIBUTOR