Create REST stub for Git LFS 2.0 Lock API
The change introduces `locks` endpoint with stubs for the following
operations (according to [1]):
POST /locks - create lock
GET /locks - list locks
POST /locks/verify - list locks for verification
POST /locks/lock_id/unlock - delete lock
[1] https://github.com/git-lfs/git-lfs/blob/master/docs/api/locking.md
Depends-On: I8299000c827b5a34d6de1ed5fc650f74be4164a2
Change-Id: I7e2debbc7a3b63694139f4759805863b37e1fd48
Signed-off-by: Jacek Centkowski <jcentkowski@collab.net>
diff --git a/src/main/java/com/googlesource/gerrit/plugins/lfs/HttpModule.java b/src/main/java/com/googlesource/gerrit/plugins/lfs/HttpModule.java
index eb5132f..1ab3cb7 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/lfs/HttpModule.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/lfs/HttpModule.java
@@ -15,6 +15,7 @@
package com.googlesource.gerrit.plugins.lfs;
import static com.googlesource.gerrit.plugins.lfs.LfsApiServlet.LFS_OBJECTS_REGEX_REST;
+import static com.googlesource.gerrit.plugins.lfs.locks.LfsLocksServlet.LFS_LOCKS_REGEX_REST;
import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.gerrit.extensions.webui.JavaScriptPlugin;
@@ -23,6 +24,7 @@
import com.google.inject.Inject;
import com.googlesource.gerrit.plugins.lfs.fs.LfsFsContentServlet;
import com.googlesource.gerrit.plugins.lfs.fs.LocalLargeFileRepository;
+import com.googlesource.gerrit.plugins.lfs.locks.LfsLocksServlet;
import com.googlesource.gerrit.plugins.lfs.s3.S3LargeFileRepository;
import java.util.Map;
@@ -54,6 +56,7 @@
@Override
protected void configureServlets() {
serveRegex(LFS_OBJECTS_REGEX_REST).with(LfsApiServlet.class);
+ serveRegex(LFS_LOCKS_REGEX_REST).with(LfsLocksServlet.class);
populateRepository(defaultBackend);
for (LfsBackend backend : backends.values()) {
populateRepository(backend);
diff --git a/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsCreateLockInput.java b/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsCreateLockInput.java
new file mode 100644
index 0000000..c2fd783
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsCreateLockInput.java
@@ -0,0 +1,23 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.lfs.locks;
+
+public class LfsCreateLockInput {
+ public final String path;
+
+ LfsCreateLockInput(String path) {
+ this.path = path;
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsDeleteLockInput.java b/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsDeleteLockInput.java
new file mode 100644
index 0000000..1dc5432
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsDeleteLockInput.java
@@ -0,0 +1,23 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.lfs.locks;
+
+public class LfsDeleteLockInput {
+ public final Boolean force;
+
+ LfsDeleteLockInput(Boolean force) {
+ this.force = force;
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsGetLocksResponse.java b/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsGetLocksResponse.java
new file mode 100644
index 0000000..3dc3056
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsGetLocksResponse.java
@@ -0,0 +1,27 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.lfs.locks;
+
+import java.util.List;
+
+public class LfsGetLocksResponse {
+ public final List<LfsLock> locks;
+ public final String nextCursor;
+
+ LfsGetLocksResponse(List<LfsLock> locks, String nextCursor) {
+ this.locks = locks;
+ this.nextCursor = nextCursor;
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsLock.java b/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsLock.java
new file mode 100644
index 0000000..8abf989
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsLock.java
@@ -0,0 +1,29 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.lfs.locks;
+
+public class LfsLock {
+ public final String id;
+ public final String path;
+ public final String lockedAt;
+ public final LfsLockOwner owner;
+
+ LfsLock(String id, String path, String lockedAt, LfsLockOwner owner) {
+ this.id = id;
+ this.path = path;
+ this.lockedAt = lockedAt;
+ this.owner = owner;
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsLockOwner.java b/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsLockOwner.java
new file mode 100644
index 0000000..7bec022
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsLockOwner.java
@@ -0,0 +1,23 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.lfs.locks;
+
+public class LfsLockOwner {
+ public final String name;
+
+ LfsLockOwner(String name) {
+ this.name = name;
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsLocksServlet.java b/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsLocksServlet.java
new file mode 100644
index 0000000..6195ded
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsLocksServlet.java
@@ -0,0 +1,281 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.lfs.locks;
+
+import static com.google.gerrit.extensions.api.lfs.LfsDefinitions.CONTENTTYPE_VND_GIT_LFS_JSON;
+import static com.google.gerrit.extensions.api.lfs.LfsDefinitions.LFS_LOCKS_PATH_REGEX;
+import static com.google.gerrit.extensions.api.lfs.LfsDefinitions.LFS_URL_REGEX_TEMPLATE;
+import static com.google.gerrit.extensions.api.lfs.LfsDefinitions.LFS_VERIFICATION_PATH;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.apache.http.HttpStatus.SC_INTERNAL_SERVER_ERROR;
+import static org.apache.http.HttpStatus.SC_OK;
+
+import com.google.common.base.Strings;
+import com.google.common.base.Supplier;
+import com.google.common.base.Suppliers;
+import com.google.common.collect.ImmutableList;
+import com.google.gson.FieldNamingPolicy;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.inject.Singleton;
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.io.Reader;
+import java.io.Writer;
+import java.util.Collections;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.joda.time.format.DateTimeFormatter;
+import org.joda.time.format.ISODateTimeFormat;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Singleton
+public class LfsLocksServlet extends HttpServlet {
+ private static final Logger log = LoggerFactory.getLogger(LfsLocksServlet.class);
+ private static final long serialVersionUID = 1L;
+ private static final Pattern LFS_LOCKS_URL_PATTERN =
+ Pattern.compile(String.format(LFS_URL_REGEX_TEMPLATE, LFS_LOCKS_PATH_REGEX));
+ private static final Pattern LFS_VERIFICATION_URL_PATTERN =
+ Pattern.compile(String.format(LFS_URL_REGEX_TEMPLATE, LFS_VERIFICATION_PATH));
+ private static final DateTimeFormatter ISO = ISODateTimeFormat.dateTime();
+
+ public static final String LFS_LOCKS_REGEX_REST =
+ String.format(LFS_URL_REGEX_TEMPLATE, LFS_LOCKS_PATH_REGEX + "|" + LFS_VERIFICATION_PATH);
+
+ @Override
+ protected void doGet(HttpServletRequest req, HttpServletResponse resp)
+ throws ServletException, IOException {
+ Action action = new Action(req, resp);
+ Matcher matcher = LFS_LOCKS_URL_PATTERN.matcher(action.path);
+ if (matcher.matches()) {
+ String project = matcher.group(1);
+ listLocks(project, action);
+ return;
+ }
+
+ action.sendError(
+ SC_INTERNAL_SERVER_ERROR, String.format("Unsupported path %s was provided", action.path));
+ }
+
+ @Override
+ protected void doPost(HttpServletRequest req, HttpServletResponse resp)
+ throws ServletException, IOException {
+ Action action = new Action(req, resp);
+ Matcher matcher = LFS_LOCKS_URL_PATTERN.matcher(action.path);
+ if (matcher.matches()) {
+ String project = matcher.group(1);
+ String lockId = matcher.group(2);
+ if (Strings.isNullOrEmpty(lockId)) {
+ createLock(project, action);
+ } else {
+ deleteLock(project, lockId, action);
+ }
+ return;
+ }
+
+ matcher = LFS_VERIFICATION_URL_PATTERN.matcher(action.path);
+ if (matcher.matches()) {
+ verifyLocks(matcher.group(1), action);
+ return;
+ }
+
+ action.sendError(
+ SC_INTERNAL_SERVER_ERROR, String.format("Unsupported path %s was provided", action.path));
+ }
+
+ private void verifyLocks(String project, Action action) throws IOException {
+ log.debug("Verify list of locks for {} project", project);
+ //TODO method stub for verifying locks
+ action.sendResponse(
+ new LfsVerifyLocksResponse(Collections.emptyList(), Collections.emptyList(), null));
+ }
+
+ private void listLocks(String project, Action action) throws IOException {
+ log.debug("Get list of locks for {} project", project);
+ //TODO method stub for getting project's locks list
+
+ // stub for searching lock by path
+ String path = action.getParam("path");
+ if (!Strings.isNullOrEmpty(path)) {
+ action.sendResponse(
+ new LfsGetLocksResponse(
+ ImmutableList.<LfsLock>builder()
+ .add(
+ new LfsLock(
+ "random_id",
+ path,
+ now(),
+ new LfsLockOwner("Lock Owner <lock_owner@example.com>")))
+ .build(),
+ null));
+ return;
+ }
+
+ // stub for searching lock by id
+ String id = action.getParam("id");
+ if (!Strings.isNullOrEmpty(id)) {
+ action.sendResponse(
+ new LfsGetLocksResponse(
+ ImmutableList.<LfsLock>builder()
+ .add(
+ new LfsLock(
+ id,
+ "path/to/file",
+ now(),
+ new LfsLockOwner("Lock Owner <lock_owner@example.com>")))
+ .build(),
+ null));
+ return;
+ }
+
+ // stub for returning all locks
+ action.sendResponse(new LfsGetLocksResponse(Collections.emptyList(), null));
+ }
+
+ private void deleteLock(String project, String lockId, Action action) throws IOException {
+ LfsDeleteLockInput input = action.input(LfsDeleteLockInput.class);
+ log.debug(
+ "Delete (-f {}) lock for {} in project {}",
+ Boolean.TRUE.equals(input.force),
+ lockId,
+ project);
+ //TODO: this is just the method stub for lock deletion
+ LfsLock lock =
+ new LfsLock(
+ "random_id",
+ "some/path/to/file",
+ now(),
+ new LfsLockOwner("Lock Owner <lock_owner@example.com>"));
+ action.sendResponse(lock);
+ }
+
+ private void createLock(String project, Action action) throws IOException {
+ LfsCreateLockInput input = action.input(LfsCreateLockInput.class);
+ log.debug("Create lock for {} in project {}", input.path, project);
+ //TODO: this is just the method stub lock creation
+ LfsLock lock =
+ new LfsLock(
+ "random_id",
+ input.path,
+ now(),
+ new LfsLockOwner("Lock Owner <lock_owner@example.com>"));
+ action.sendResponse(lock);
+ }
+
+ private String now() {
+ return ISO.print(DateTime.now().toDateTime(DateTimeZone.UTC));
+ }
+
+ private class Action {
+ public final String path;
+
+ private final HttpServletRequest req;
+ private final HttpServletResponse res;
+ private final Supplier<Writer> writer;
+ private final Supplier<Reader> reader;
+ private final Gson gson;
+
+ private Action(final HttpServletRequest req, final HttpServletResponse res) {
+ this.path = req.getPathInfo().startsWith("/") ? req.getPathInfo() : "/" + req.getPathInfo();
+ this.req = req;
+ this.res = res;
+ this.writer =
+ Suppliers.memoize(
+ new Supplier<Writer>() {
+ @Override
+ public Writer get() {
+ try {
+ return new BufferedWriter(new OutputStreamWriter(res.getOutputStream(), UTF_8));
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ });
+ this.reader =
+ Suppliers.memoize(
+ new Supplier<Reader>() {
+ @Override
+ public Reader get() {
+ try {
+ return new BufferedReader(new InputStreamReader(req.getInputStream(), UTF_8));
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ });
+ this.gson = createGson();
+ setLfsResponseType();
+ }
+
+ String getParam(String name) {
+ return req.getParameter(name);
+ }
+
+ <T> T input(Class<T> clazz) {
+ return gson.fromJson(getReader(), clazz);
+ }
+
+ <T> void sendResponse(T content) throws IOException {
+ res.setStatus(SC_OK);
+ gson.toJson(content, getWriter());
+ getWriter().flush();
+ }
+
+ void sendError(int status, String message) throws IOException {
+ log.error(message);
+ res.setStatus(status);
+ gson.toJson(new Error(message), getWriter());
+ getWriter().flush();
+ }
+
+ Writer getWriter() {
+ return writer.get();
+ }
+
+ Reader getReader() {
+ return reader.get();
+ }
+
+ void setLfsResponseType() {
+ res.setContentType(CONTENTTYPE_VND_GIT_LFS_JSON);
+ }
+
+ private Gson createGson() {
+ return new GsonBuilder()
+ .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
+ .disableHtmlEscaping()
+ .create();
+ }
+ }
+
+ /** copied from org.eclipse.jgit.lfs.server.LfsProtocolServlet.Error */
+ static class Error {
+ String message;
+
+ Error(String m) {
+ this.message = m;
+ }
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsVerifyLocksInput.java b/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsVerifyLocksInput.java
new file mode 100644
index 0000000..2819d73
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsVerifyLocksInput.java
@@ -0,0 +1,25 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.lfs.locks;
+
+public class LfsVerifyLocksInput {
+ public final String cursor;
+ public final Integer limit;
+
+ LfsVerifyLocksInput(String cursor, Integer limit) {
+ this.cursor = cursor;
+ this.limit = limit;
+ }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsVerifyLocksResponse.java b/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsVerifyLocksResponse.java
new file mode 100644
index 0000000..d55ed0f
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsVerifyLocksResponse.java
@@ -0,0 +1,29 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.lfs.locks;
+
+import java.util.List;
+
+public class LfsVerifyLocksResponse {
+ public final List<LfsLock> ours;
+ public final List<LfsLock> theirs;
+ public final String nextCursor;
+
+ LfsVerifyLocksResponse(List<LfsLock> ours, List<LfsLock> theirs, String nextCursor) {
+ this.ours = ours;
+ this.theirs = theirs;
+ this.nextCursor = nextCursor;
+ }
+}