Merge branch 'stable-2.14'

* stable-2.14:
  Upgrade bazlets and use released 2.14.1 API
  Upgrade JGit to 4.7.1.201706071930-r
  Implement Git LFS 2.0 locking
  Validate user permission to Git LFS lock operation
  Unify exceptions handling and error reporting with LfsApiServlet
  Create REST stub for Git LFS 2.0 Lock API

Change-Id: I0d2533e6468647767fa7cebbd53624efa013c77e
diff --git a/WORKSPACE b/WORKSPACE
index 7c12869..6da3da8 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -3,7 +3,7 @@
 load("//:bazlets.bzl", "load_bazlets")
 
 load_bazlets(
-    commit = "74b31c8fae3a92c6c3e46a046b57cf1d8a6549d4",
+    commit = "28aa2290c7f7742261d69b358f3de30d2e87c13b",
     #    local_path = "/home/<user>/projects/bazlets",
 )
 
@@ -12,43 +12,43 @@
      "GERRIT",
      "MAVEN_CENTRAL")
 
-JGIT_VERS = "4.7.0.201704051617-r"
+JGIT_VERS = "4.7.1.201706071930-r"
 
 JGIT_REPO = MAVEN_CENTRAL
 
 maven_jar(
     name = "jgit_http_apache",
     artifact = "org.eclipse.jgit:org.eclipse.jgit.http.apache:" + JGIT_VERS,
-    sha1 = "da1fc3c8f6ce89dd846125a38fd4c35f2436caf3",
+    sha1 = "16d49a8824753f2d421151c68be05e0869e0b8f6",
     repository = JGIT_REPO,
 )
 
 maven_jar(
     name = "jgit_lfs",
     artifact = "org.eclipse.jgit:org.eclipse.jgit.lfs:" + JGIT_VERS,
-    sha1 = "d71dbf2f97544cc6deff966e6d069b76049958e0",
+    sha1 = "35e8245b5c77822581dc354387e8e78846cf4e7e",
     repository = JGIT_REPO,
 )
 
 maven_jar(
     name = "jgit_lfs_server",
     artifact = "org.eclipse.jgit:org.eclipse.jgit.lfs.server:" + JGIT_VERS,
-    sha1 = "edf48b77d97abc70f0e048bd04864132b30d2be5",
+    sha1 = "9c4fc91f095b13348081acf40f6c402e10b7255d",
     repository = JGIT_REPO,
 )
 
 # Release Plugin API
-#load("@com_googlesource_gerrit_bazlets//:gerrit_api.bzl",
-#     "gerrit_api")
+load("@com_googlesource_gerrit_bazlets//:gerrit_api.bzl",
+     "gerrit_api")
 
 # Snapshot Plugin API
-load(
-    "@com_googlesource_gerrit_bazlets//:gerrit_api_maven_local.bzl",
-    "gerrit_api_maven_local",
-)
+#load(
+#    "@com_googlesource_gerrit_bazlets//:gerrit_api_maven_local.bzl",
+#    "gerrit_api_maven_local",
+#)
 
 # Load release Plugin API
-#gerrit_api()
+gerrit_api()
 
 # Load snapshot Plugin API
-gerrit_api_maven_local()
+#gerrit_api_maven_local()
diff --git a/external_plugin_deps.bzl b/external_plugin_deps.bzl
index deb2dde..ed005bc 100644
--- a/external_plugin_deps.bzl
+++ b/external_plugin_deps.bzl
@@ -1,13 +1,13 @@
 load("//tools/bzl:maven_jar.bzl", "maven_jar", "GERRIT", "MAVEN_LOCAL", "MAVEN_CENTRAL")
 
-JGIT_VERSION = '4.7.0.201704051617-r'
+JGIT_VERSION = '4.7.1.201706071930-r'
 REPO = MAVEN_CENTRAL
 
 def external_plugin_deps():
   maven_jar(
     name = 'jgit_http_apache',
     artifact = 'org.eclipse.jgit:org.eclipse.jgit.http.apache:' + JGIT_VERSION,
-    sha1 = 'da1fc3c8f6ce89dd846125a38fd4c35f2436caf3',
+    sha1 = '16d49a8824753f2d421151c68be05e0869e0b8f6',
     repository = REPO,
     unsign = True,
     exclude = [
@@ -19,7 +19,7 @@
   maven_jar(
     name = 'jgit_lfs',
     artifact = 'org.eclipse.jgit:org.eclipse.jgit.lfs:' + JGIT_VERSION,
-    sha1 = 'd71dbf2f97544cc6deff966e6d069b76049958e0',
+    sha1 = '35e8245b5c77822581dc354387e8e78846cf4e7e',
     repository = REPO,
     unsign = True,
     exclude = [
@@ -31,7 +31,7 @@
   maven_jar(
     name = 'jgit_lfs_server',
     artifact = 'org.eclipse.jgit:org.eclipse.jgit.lfs.server:' + JGIT_VERSION,
-    sha1 = 'edf48b77d97abc70f0e048bd04864132b30d2be5',
+    sha1 = '9c4fc91f095b13348081acf40f6c402e10b7255d',
     repository = REPO,
     unsign = True,
     exclude = [
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/LfsAuthUserProvider.java b/src/main/java/com/googlesource/gerrit/plugins/lfs/LfsAuthUserProvider.java
index aa39abb..ddf46f4 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/lfs/LfsAuthUserProvider.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/lfs/LfsAuthUserProvider.java
@@ -28,7 +28,7 @@
 import com.google.inject.Singleton;
 
 @Singleton
-class LfsAuthUserProvider {
+public class LfsAuthUserProvider {
   private static final String BASIC_AUTH_PREFIX = "Basic ";
 
   private final Provider<AnonymousUser> anonymous;
@@ -51,7 +51,7 @@
     this.userFactory = userFactory;
   }
 
-  CurrentUser getUser(String auth, String project, String operation) {
+  public CurrentUser getUser(String auth, String project, String operation) {
     if (!Strings.isNullOrEmpty(auth)) {
       if (auth.startsWith(BASIC_AUTH_PREFIX)) {
         return user.get();
diff --git a/src/main/java/com/googlesource/gerrit/plugins/lfs/Module.java b/src/main/java/com/googlesource/gerrit/plugins/lfs/Module.java
index 089ebc4..a6769a0 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/lfs/Module.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/lfs/Module.java
@@ -22,6 +22,7 @@
 import com.google.inject.internal.UniqueAnnotations;
 import com.googlesource.gerrit.plugins.lfs.fs.LfsFsContentServlet;
 import com.googlesource.gerrit.plugins.lfs.fs.LocalLargeFileRepository;
+import com.googlesource.gerrit.plugins.lfs.locks.LfsLocksModule;
 import com.googlesource.gerrit.plugins.lfs.s3.S3LargeFileRepository;
 
 public class Module extends FactoryModule {
@@ -43,5 +44,6 @@
     factory(S3LargeFileRepository.Factory.class);
     factory(LocalLargeFileRepository.Factory.class);
     factory(LfsFsContentServlet.Factory.class);
+    install(new LfsLocksModule());
   }
 }
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/LfsGetLocksAction.java b/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsGetLocksAction.java
new file mode 100644
index 0000000..e30703e
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsGetLocksAction.java
@@ -0,0 +1,84 @@
+// 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.LFS_LOCKS_PATH_REGEX;
+import static com.google.gerrit.extensions.api.lfs.LfsDefinitions.LFS_URL_REGEX_TEMPLATE;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.server.project.ProjectState;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+import com.googlesource.gerrit.plugins.lfs.LfsAuthUserProvider;
+import java.io.IOException;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import org.eclipse.jgit.lfs.errors.LfsException;
+import org.eclipse.jgit.lfs.errors.LfsUnauthorized;
+
+public class LfsGetLocksAction extends LfsLocksAction {
+  interface Factory extends LfsLocksAction.Factory<LfsGetLocksAction> {}
+
+  static final Pattern LFS_LOCKS_URL_PATTERN =
+      Pattern.compile(String.format(LFS_URL_REGEX_TEMPLATE, LFS_LOCKS_PATH_REGEX));
+
+  @Inject
+  LfsGetLocksAction(
+      ProjectCache projectCache,
+      LfsAuthUserProvider userProvider,
+      LfsLocksHandler handler,
+      @Assisted LfsLocksContext context) {
+    super(projectCache, userProvider, handler, context);
+  }
+
+  @Override
+  protected String getProjectName() throws LfsException {
+    Matcher matcher = LFS_LOCKS_URL_PATTERN.matcher(context.path);
+    if (matcher.matches()) {
+      return matcher.group(1);
+    }
+
+    throw new LfsException("no repository at " + context.path);
+  }
+
+  @Override
+  protected void authorizeUser(ProjectControl control) throws LfsUnauthorized {
+    if (!control.isReadable()) {
+      throwUnauthorizedOp("list locks", control);
+    }
+  }
+
+  @Override
+  protected void doRun(ProjectState project, CurrentUser user) throws LfsException, IOException {
+    Project.NameKey name = project.getProject().getNameKey();
+    String path = context.getParam("path");
+    if (!Strings.isNullOrEmpty(path)) {
+      context.sendResponse(handler.listLocksByPath(name, path));
+      return;
+    }
+
+    String id = context.getParam("id");
+    if (!Strings.isNullOrEmpty(id)) {
+      context.sendResponse(handler.listLocksById(name, id));
+      return;
+    }
+
+    context.sendResponse(handler.listLocks(name));
+  }
+}
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..e6a9b0a
--- /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.Collection;
+
+public class LfsGetLocksResponse {
+  public final Collection<LfsLock> locks;
+  public final String nextCursor;
+
+  LfsGetLocksResponse(Collection<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/LfsLockFile.java b/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsLockFile.java
new file mode 100644
index 0000000..6d7f489
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsLockFile.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 org.eclipse.jgit.internal.storage.file.LockFile;
+
+public class LfsLockFile {
+  public final LfsLock lfsLock;
+  public final LockFile fileLock;
+
+  LfsLockFile(LfsLock lfsLock, LockFile fileLock) {
+    this.lfsLock = lfsLock;
+    this.fileLock = fileLock;
+  }
+}
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/LfsLockResponse.java b/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsLockResponse.java
new file mode 100644
index 0000000..a82e71d
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsLockResponse.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 LfsLockResponse {
+  public final LfsLock lock;
+
+  LfsLockResponse(LfsLock lock) {
+    this.lock = lock;
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsLocksAction.java b/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsLocksAction.java
new file mode 100644
index 0000000..8a13514
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsLocksAction.java
@@ -0,0 +1,116 @@
+// 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.client.ProjectState.HIDDEN;
+import static org.apache.http.HttpStatus.SC_CONFLICT;
+import static org.apache.http.HttpStatus.SC_INTERNAL_SERVER_ERROR;
+import static org.apache.http.HttpStatus.SC_NOT_FOUND;
+import static org.apache.http.HttpStatus.SC_UNAUTHORIZED;
+import static org.eclipse.jgit.util.HttpSupport.HDR_AUTHORIZATION;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.common.ProjectUtil;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.server.project.ProjectState;
+import com.googlesource.gerrit.plugins.lfs.LfsAuthUserProvider;
+import com.googlesource.gerrit.plugins.lfs.locks.LfsLocksHandler.LfsLockExistsException;
+import java.io.IOException;
+import org.eclipse.jgit.lfs.errors.LfsException;
+import org.eclipse.jgit.lfs.errors.LfsRepositoryNotFound;
+import org.eclipse.jgit.lfs.errors.LfsUnauthorized;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+abstract class LfsLocksAction {
+  interface Factory<T extends LfsLocksAction> {
+    T create(LfsLocksContext context);
+  }
+
+  private static final Logger log = LoggerFactory.getLogger(LfsLocksAction.class);
+  /** Git LFS client uses 'upload' operation to authorize SSH Lock requests */
+  private static final String LFS_LOCKING_OPERATION = "upload";
+
+  protected final ProjectCache projectCache;
+  protected final LfsAuthUserProvider userProvider;
+  protected final LfsLocksHandler handler;
+  protected final LfsLocksContext context;
+
+  protected LfsLocksAction(
+      ProjectCache projectCache,
+      LfsAuthUserProvider userProvider,
+      LfsLocksHandler handler,
+      LfsLocksContext context) {
+    this.projectCache = projectCache;
+    this.userProvider = userProvider;
+    this.handler = handler;
+    this.context = context;
+  }
+
+  public void run() throws IOException {
+    try {
+      String name = getProjectName();
+      ProjectState project = getProject(name);
+      CurrentUser user = getUser(name);
+      ProjectControl control = project.controlFor(user);
+      authorizeUser(control);
+      doRun(project, user);
+    } catch (LfsUnauthorized e) {
+      context.sendError(SC_UNAUTHORIZED, e.getMessage());
+    } catch (LfsRepositoryNotFound e) {
+      context.sendError(SC_NOT_FOUND, e.getMessage());
+    } catch (LfsLockExistsException e) {
+      context.sendError(SC_CONFLICT, e.error);
+    } catch (LfsException e) {
+      context.sendError(SC_INTERNAL_SERVER_ERROR, e.getMessage());
+    }
+  }
+
+  protected abstract String getProjectName() throws LfsException;
+
+  protected abstract void authorizeUser(ProjectControl control) throws LfsUnauthorized;
+
+  protected abstract void doRun(ProjectState project, CurrentUser user)
+      throws LfsException, IOException;
+
+  protected ProjectState getProject(String name) throws LfsRepositoryNotFound {
+    Project.NameKey project = Project.NameKey.parse(ProjectUtil.stripGitSuffix(name));
+    ProjectState state = projectCache.get(project);
+    if (state == null || state.getProject().getState() == HIDDEN) {
+      throw new LfsRepositoryNotFound(project.get());
+    }
+    return state;
+  }
+
+  protected CurrentUser getUser(String project) {
+    return userProvider.getUser(
+        context.getHeader(HDR_AUTHORIZATION), project, LFS_LOCKING_OPERATION);
+  }
+
+  protected void throwUnauthorizedOp(String op, ProjectControl control) throws LfsUnauthorized {
+    String project = control.getProject().getName();
+    String userName =
+        Strings.isNullOrEmpty(control.getUser().getUserName())
+            ? "anonymous"
+            : control.getUser().getUserName();
+    log.debug(
+        String.format(
+            "operation %s unauthorized for user %s on project %s", op, userName, project));
+    throw new LfsUnauthorized(op, project);
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsLocksContext.java b/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsLocksContext.java
new file mode 100644
index 0000000..fa5f360
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsLocksContext.java
@@ -0,0 +1,137 @@
+// 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 java.nio.charset.StandardCharsets.UTF_8;
+import static org.apache.http.HttpStatus.SC_OK;
+
+import com.google.common.base.Supplier;
+import com.google.common.base.Suppliers;
+import com.google.gson.FieldNamingPolicy;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+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 javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+class LfsLocksContext {
+  private static final Logger log = LoggerFactory.getLogger(LfsLocksContext.class);
+
+  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;
+
+  LfsLocksContext(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 getHeader(String name) {
+    return req.getHeader(name);
+  }
+
+  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 {
+    sendError(status, new Error(message));
+  }
+
+  void sendError(int status, Error error) throws IOException {
+    log.error(error.message);
+    res.setStatus(status);
+    gson.toJson(error, 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/LfsLocksHandler.java b/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsLocksHandler.java
new file mode 100644
index 0000000..f6ea9f1
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsLocksHandler.java
@@ -0,0 +1,168 @@
+// 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 com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.cache.CacheModule;
+import com.google.inject.Inject;
+import com.google.inject.Module;
+import com.google.inject.Singleton;
+import com.google.inject.name.Named;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import org.eclipse.jgit.lfs.errors.LfsException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Singleton
+class LfsLocksHandler {
+  static class LfsLockExistsException extends LfsException {
+    private static final long serialVersionUID = 1L;
+
+    public final LfsLocksContext.Error error;
+
+    public LfsLockExistsException(LfsLock lock) {
+      super("Lock is already created");
+      this.error = new LockError(getMessage(), lock);
+    }
+  }
+
+  static class LockError extends LfsLocksContext.Error {
+    public final LfsLock lock;
+
+    LockError(String m, LfsLock lock) {
+      super(m);
+      this.lock = lock;
+    }
+  }
+
+  private static final Logger log = LoggerFactory.getLogger(LfsLocksHandler.class);
+  private static final String CACHE_NAME = "lfs_project_locks";
+
+  static Module module() {
+    return new CacheModule() {
+      @Override
+      protected void configure() {
+        cache(CACHE_NAME, Project.NameKey.class, LfsProjectLocks.class).loader(Loader.class);
+      }
+    };
+  }
+
+  private final PathToLockId toLockId;
+  private final LoadingCache<Project.NameKey, LfsProjectLocks> projectLocks;
+
+  @Inject
+  LfsLocksHandler(
+      PathToLockId toLockId,
+      @Named(CACHE_NAME) LoadingCache<Project.NameKey, LfsProjectLocks> projectLocks) {
+    this.toLockId = toLockId;
+    this.projectLocks = projectLocks;
+  }
+
+  LfsLockResponse createLock(Project.NameKey project, CurrentUser user, LfsCreateLockInput input)
+      throws LfsException {
+    log.debug("Create lock for {} in project {}", input.path, project);
+    LfsProjectLocks locks = projectLocks.getUnchecked(project);
+    LfsLock lock = locks.createLock(user, input);
+    return new LfsLockResponse(lock);
+  }
+
+  LfsLockResponse deleteLock(
+      Project.NameKey project, CurrentUser user, String lockId, LfsDeleteLockInput input)
+      throws LfsException {
+    log.debug(
+        "Delete (-f {}) lock for {} in project {}",
+        Boolean.TRUE.equals(input.force),
+        lockId,
+        project);
+    LfsProjectLocks locks = projectLocks.getUnchecked(project);
+    Optional<LfsLock> hasLock = locks.getLock(lockId);
+    if (!hasLock.isPresent()) {
+      throw new LfsException(
+          String.format("there is no lock id %s in project %s", lockId, project));
+    }
+
+    LfsLock lock = hasLock.get();
+    if (lock.owner.name.equals(user.getUserName())) {
+      locks.deleteLock(lock);
+      return new LfsLockResponse(lock);
+    } else if (input.force) {
+      locks.deleteLock(lock);
+      return new LfsLockResponse(lock);
+    }
+
+    throw new LfsException(
+        String.format("Lock %s is owned by different user %s", lockId, lock.owner.name));
+  }
+
+  LfsVerifyLocksResponse verifyLocks(Project.NameKey project, final CurrentUser user) {
+    log.debug("Verify list of locks for {} project and user {}", project, user);
+    LfsProjectLocks locks = projectLocks.getUnchecked(project);
+    Function<LfsLock, Boolean> isOurs =
+        new Function<LfsLock, Boolean>() {
+          @Override
+          public Boolean apply(LfsLock input) {
+            return input.owner.name.equals(user.getUserName());
+          }
+        };
+    Map<Boolean, List<LfsLock>> groupByOurs =
+        locks.getLocks().stream().collect(Collectors.groupingBy(isOurs));
+    return new LfsVerifyLocksResponse(groupByOurs.get(true), groupByOurs.get(false), null);
+  }
+
+  LfsGetLocksResponse listLocksByPath(Project.NameKey project, String path) {
+    log.debug("Get lock for {} path in {} project", path, project);
+    String lockId = toLockId.apply(path);
+    return listLocksById(project, lockId);
+  }
+
+  LfsGetLocksResponse listLocksById(Project.NameKey project, String id) {
+    log.debug("Get lock for {} id in {} project", id, project);
+    LfsProjectLocks locks = projectLocks.getUnchecked(project);
+    Optional<LfsLock> lock = locks.getLock(id);
+    List<LfsLock> locksById =
+        (lock.isPresent() ? ImmutableList.of(lock.get()) : Collections.emptyList());
+    return new LfsGetLocksResponse(locksById, null);
+  }
+
+  LfsGetLocksResponse listLocks(Project.NameKey project) {
+    log.debug("Get locks for {} project", project);
+    return new LfsGetLocksResponse(projectLocks.getUnchecked(project).getLocks(), null);
+  }
+
+  static class Loader extends CacheLoader<Project.NameKey, LfsProjectLocks> {
+    private final LfsProjectLocks.Factory factory;
+
+    @Inject
+    Loader(LfsProjectLocks.Factory factory) {
+      this.factory = factory;
+    }
+
+    @Override
+    public LfsProjectLocks load(Project.NameKey project) throws Exception {
+      LfsProjectLocks locks = factory.create(project);
+      locks.load();
+      return locks;
+    }
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsLocksModule.java b/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsLocksModule.java
new file mode 100644
index 0000000..457a3b9
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsLocksModule.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 com.google.gerrit.extensions.config.FactoryModule;
+
+public class LfsLocksModule extends FactoryModule {
+  @Override
+  protected void configure() {
+    factory(LfsGetLocksAction.Factory.class);
+    factory(LfsPutLocksAction.Factory.class);
+    factory(LfsProjectLocks.Factory.class);
+    install(LfsLocksHandler.module());
+  }
+}
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..9cdeb62
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsLocksServlet.java
@@ -0,0 +1,58 @@
+// 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.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 com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+@Singleton
+public class LfsLocksServlet extends HttpServlet {
+  private static final long serialVersionUID = 1L;
+
+  public static final String LFS_LOCKS_REGEX_REST =
+      String.format(LFS_URL_REGEX_TEMPLATE, LFS_LOCKS_PATH_REGEX + "|" + LFS_VERIFICATION_PATH);
+
+  private final LfsGetLocksAction.Factory getters;
+  private final LfsPutLocksAction.Factory putters;
+
+  @Inject
+  LfsLocksServlet(LfsGetLocksAction.Factory getters, LfsPutLocksAction.Factory putters) {
+    this.getters = getters;
+    this.putters = putters;
+  }
+
+  @Override
+  protected void doGet(HttpServletRequest req, HttpServletResponse resp)
+      throws ServletException, IOException {
+    LfsLocksContext context = new LfsLocksContext(req, resp);
+    getters.create(context).run();
+  }
+
+  @Override
+  protected void doPost(HttpServletRequest req, HttpServletResponse resp)
+      throws ServletException, IOException {
+    LfsLocksContext context = new LfsLocksContext(req, resp);
+    putters.create(context).run();
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsProjectLocks.java b/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsProjectLocks.java
new file mode 100644
index 0000000..934ee00
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsProjectLocks.java
@@ -0,0 +1,196 @@
+// 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 com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
+import com.google.gerrit.extensions.annotations.PluginData;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gson.FieldNamingPolicy;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+import com.googlesource.gerrit.plugins.lfs.locks.LfsLocksHandler.LfsLockExistsException;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.io.Reader;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Collection;
+import java.util.Optional;
+import org.eclipse.jgit.internal.storage.file.LockFile;
+import org.eclipse.jgit.lfs.errors.LfsException;
+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;
+
+class LfsProjectLocks {
+  interface Factory {
+    LfsProjectLocks create(Project.NameKey project);
+  }
+
+  private static final Logger log = LoggerFactory.getLogger(LfsProjectLocks.class);
+  private static final DateTimeFormatter ISO = ISODateTimeFormat.dateTime();
+  private static final Gson gson =
+      new GsonBuilder()
+          .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
+          .disableHtmlEscaping()
+          .create();
+
+  private final PathToLockId toLockId;
+  private final String project;
+  private final Path locksPath;
+  private final Cache<String, LfsLock> locks;
+
+  @Inject
+  LfsProjectLocks(
+      PathToLockId toLockId, @PluginData Path defaultDataDir, @Assisted Project.NameKey project) {
+    this.toLockId = toLockId;
+    this.project = project.get();
+    this.locksPath = Paths.get(defaultDataDir.toString(), "lfs_locks", this.project);
+    this.locks = CacheBuilder.newBuilder().build();
+  }
+
+  void load() {
+    if (!Files.exists(locksPath)) {
+      return;
+    }
+    try {
+      Files.list(locksPath)
+          .filter(Files::isRegularFile)
+          .forEach(
+              path -> {
+                if (!Files.isReadable(path)) {
+                  log.warn("Lock file [{}] in project {} is not readable", path, project);
+                  return;
+                }
+
+                try (Reader in = Files.newBufferedReader(path)) {
+                  LfsLock lock = gson.fromJson(in, LfsLock.class);
+                  locks.put(lock.id, lock);
+                } catch (IOException e) {
+                  log.warn("Reading lock [{}] failed", path, e);
+                }
+              });
+    } catch (IOException e) {
+      log.warn("Reading locks in project {} failed", project, e);
+    }
+  }
+
+  Optional<LfsLock> getLock(String lockId) {
+    return Optional.ofNullable(locks.getIfPresent(lockId));
+  }
+
+  LfsLock createLock(CurrentUser user, LfsCreateLockInput input) throws LfsException {
+    log.debug("Create lock for {} in project {}", input.path, project);
+    String lockId = toLockId.apply(input.path);
+    LfsLock lock = locks.getIfPresent(lockId);
+    if (lock != null) {
+      throw new LfsLockExistsException(lock);
+    }
+
+    lock = new LfsLock(lockId, input.path, now(), new LfsLockOwner(user.getUserName()));
+    LockFile fileLock = new LockFile(locksPath.resolve(lockId).toFile());
+    try {
+      if (!fileLock.lock()) {
+        log.warn("Cannot lock path [{}] in project {}", input.path, project);
+        throw new LfsLockExistsException(lock);
+      }
+    } catch (IOException e) {
+      String error =
+          String.format(
+              "Locking path [%s] in project %s failed with error %s",
+              input.path, project, e.getMessage());
+      log.warn(error);
+      throw new LfsException(error);
+    }
+
+    try {
+      try (OutputStreamWriter out = new OutputStreamWriter(fileLock.getOutputStream())) {
+        gson.toJson(lock, out);
+      } catch (IOException e) {
+        String error =
+            String.format(
+                "Locking path [%s] in project %s failed during write with error %s",
+                input.path, project, e.getMessage());
+        log.warn(error);
+        throw new LfsException(error);
+      }
+      if (!fileLock.commit()) {
+        String error =
+            String.format("Committing lock to path [%s] in project %s failed", input.path, project);
+        log.warn(error);
+        throw new LfsException(error);
+      }
+      // put lock object to cache while file lock is being hold so that
+      // there is no chance that other process performs lock operation
+      // in the meantime (either cache returns with existing object or
+      // LockFile.lock fails on locking attempt)
+      locks.put(lockId, lock);
+    } finally {
+      fileLock.unlock();
+    }
+
+    return lock;
+  }
+
+  void deleteLock(LfsLock lock) throws LfsException {
+    LockFile fileLock = new LockFile(locksPath.resolve(lock.id).toFile());
+    try {
+      if (!fileLock.lock()) {
+        String error =
+            String.format(
+                "Deleting lock on path [%s] in project %s is not possible", lock.path, project);
+        log.warn(error);
+        throw new LfsException(error);
+      }
+    } catch (IOException e) {
+      String error =
+          String.format(
+              "Getting lock on path [%s] in project %s failed with error %s",
+              lock.path, project, e.getMessage());
+      log.warn(error);
+      throw new LfsException(error);
+    }
+
+    try {
+      Files.deleteIfExists(locksPath.resolve(lock.id));
+      locks.invalidate(lock.id);
+    } catch (IOException e) {
+      String error =
+          String.format(
+              "Deleting lock on path [%s] in project %s failed with error %s",
+              lock.path, project, e.getMessage());
+      log.warn(error);
+      throw new LfsException(error);
+    } finally {
+      fileLock.unlock();
+    }
+  }
+
+  Collection<LfsLock> getLocks() {
+    return locks.asMap().values();
+  }
+
+  private String now() {
+    return ISO.print(DateTime.now().toDateTime(DateTimeZone.UTC));
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsPutLocksAction.java b/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsPutLocksAction.java
new file mode 100644
index 0000000..ffbcc8d
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/LfsPutLocksAction.java
@@ -0,0 +1,141 @@
+// 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.LFS_URL_REGEX_TEMPLATE;
+import static com.google.gerrit.extensions.api.lfs.LfsDefinitions.LFS_VERIFICATION_PATH;
+import static com.googlesource.gerrit.plugins.lfs.locks.LfsGetLocksAction.LFS_LOCKS_URL_PATTERN;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.common.data.Capable;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.server.project.ProjectState;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+import com.googlesource.gerrit.plugins.lfs.LfsAuthUserProvider;
+import java.io.IOException;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import org.eclipse.jgit.lfs.errors.LfsException;
+import org.eclipse.jgit.lfs.errors.LfsUnauthorized;
+
+public class LfsPutLocksAction extends LfsLocksAction {
+  interface Factory extends LfsLocksAction.Factory<LfsPutLocksAction> {}
+
+  private static final Pattern LFS_VERIFICATION_URL_PATTERN =
+      Pattern.compile(String.format(LFS_URL_REGEX_TEMPLATE, LFS_VERIFICATION_PATH));
+
+  protected LockAction action;
+
+  @Inject
+  LfsPutLocksAction(
+      ProjectCache projectCache,
+      LfsAuthUserProvider userProvider,
+      LfsLocksHandler handler,
+      @Assisted LfsLocksContext context) {
+    super(projectCache, userProvider, handler, context);
+  }
+
+  @Override
+  protected String getProjectName() throws LfsException {
+    Matcher matcher = LFS_LOCKS_URL_PATTERN.matcher(context.path);
+    if (matcher.matches()) {
+      String project = matcher.group(1);
+      String lockId = matcher.group(2);
+      if (Strings.isNullOrEmpty(lockId)) {
+        action = new CreateLock();
+      } else {
+        action = new DeleteLock(lockId);
+      }
+      return project;
+    }
+
+    matcher = LFS_VERIFICATION_URL_PATTERN.matcher(context.path);
+    if (matcher.matches()) {
+      action = new VerifyLock();
+      return matcher.group(1);
+    }
+
+    throw new LfsException(String.format("Unsupported path %s was provided", context.path));
+  }
+
+  @Override
+  protected void authorizeUser(ProjectControl control) throws LfsUnauthorized {
+    // all operations require push permission
+    if (Capable.OK != control.canPushToAtLeastOneRef()) {
+      throwUnauthorizedOp(action.getName(), control);
+    }
+  }
+
+  @Override
+  protected void doRun(ProjectState project, CurrentUser user) throws LfsException, IOException {
+    action.run(project, user);
+  }
+
+  private interface LockAction {
+    String getName();
+
+    void run(ProjectState project, CurrentUser user) throws LfsException, IOException;
+  }
+
+  private class CreateLock implements LockAction {
+    @Override
+    public String getName() {
+      return "create lock";
+    }
+
+    @Override
+    public void run(ProjectState project, CurrentUser user) throws LfsException, IOException {
+      LfsCreateLockInput input = context.input(LfsCreateLockInput.class);
+      LfsLockResponse lock = handler.createLock(project.getProject().getNameKey(), user, input);
+      context.sendResponse(lock);
+    }
+  }
+
+  private class DeleteLock implements LockAction {
+    private final String lockId;
+
+    private DeleteLock(String lockId) {
+      this.lockId = lockId;
+    }
+
+    @Override
+    public String getName() {
+      return "delete lock";
+    }
+
+    @Override
+    public void run(ProjectState project, CurrentUser user) throws LfsException, IOException {
+      LfsDeleteLockInput input = context.input(LfsDeleteLockInput.class);
+      LfsLockResponse lock =
+          handler.deleteLock(project.getProject().getNameKey(), user, lockId, input);
+      context.sendResponse(lock);
+    }
+  }
+
+  private class VerifyLock implements LockAction {
+    @Override
+    public String getName() {
+      return "verify lock";
+    }
+
+    @Override
+    public void run(ProjectState project, CurrentUser user) throws LfsException, IOException {
+      context.sendResponse(handler.verifyLocks(project.getProject().getNameKey(), user));
+    }
+  }
+}
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;
+  }
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/PathToLockId.java b/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/PathToLockId.java
new file mode 100644
index 0000000..d90ebb8
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/lfs/locks/PathToLockId.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 com.google.common.base.Function;
+import com.google.common.hash.HashCode;
+import com.google.common.hash.Hashing;
+import com.google.common.io.BaseEncoding;
+import java.nio.charset.StandardCharsets;
+
+public class PathToLockId implements Function<String, String> {
+  @Override
+  public String apply(String path) {
+    HashCode hash = Hashing.sha256().hashString(path, StandardCharsets.UTF_8);
+    return BaseEncoding.base16().lowerCase().encode(hash.asBytes());
+  }
+}