diff --git a/models/lfs_lock.go b/models/lfs_lock.go
index ba1a452815..3e56a7960b 100644
--- a/models/lfs_lock.go
+++ b/models/lfs_lock.go
@@ -49,7 +49,7 @@ func (l *LFSLock) AfterLoad(session *xorm.Session) {
 }
 
 func cleanPath(p string) string {
-	return path.Clean(p)
+	return path.Clean("/" + p)[1:]
 }
 
 // APIFormat convert a Release to lfs.LFSLock
@@ -71,6 +71,8 @@ func CreateLFSLock(lock *LFSLock) (*LFSLock, error) {
 		return nil, err
 	}
 
+	lock.Path = cleanPath(lock.Path)
+
 	l, err := GetLFSLock(lock.Repo, lock.Path)
 	if err == nil {
 		return l, ErrLFSLockAlreadyExist{lock.RepoID, lock.Path}
@@ -110,9 +112,24 @@ func GetLFSLockByID(id int64) (*LFSLock, error) {
 }
 
 // GetLFSLockByRepoID returns a list of locks of repository.
-func GetLFSLockByRepoID(repoID int64) (locks []*LFSLock, err error) {
-	err = x.Where("repo_id = ?", repoID).Find(&locks)
-	return
+func GetLFSLockByRepoID(repoID int64, page, pageSize int) ([]*LFSLock, error) {
+	sess := x.NewSession()
+	defer sess.Close()
+
+	if page >= 0 && pageSize > 0 {
+		start := 0
+		if page > 0 {
+			start = (page - 1) * pageSize
+		}
+		sess.Limit(pageSize, start)
+	}
+	lfsLocks := make([]*LFSLock, 0, pageSize)
+	return lfsLocks, sess.Find(&lfsLocks, &LFSLock{RepoID: repoID})
+}
+
+// CountLFSLockByRepoID returns a count of all LFSLocks associated with a repository.
+func CountLFSLockByRepoID(repoID int64) (int64, error) {
+	return x.Count(&LFSLock{RepoID: repoID})
 }
 
 // DeleteLFSLockByID deletes a lock by given ID.
diff --git a/models/repo.go b/models/repo.go
index d5ea29c501..c904449bbd 100644
--- a/models/repo.go
+++ b/models/repo.go
@@ -2913,7 +2913,7 @@ func (repo *Repository) GetOriginalURLHostname() string {
 // GetTreePathLock returns LSF lock for the treePath
 func (repo *Repository) GetTreePathLock(treePath string) (*LFSLock, error) {
 	if setting.LFS.StartServer {
-		locks, err := GetLFSLockByRepoID(repo.ID)
+		locks, err := GetLFSLockByRepoID(repo.ID, 0, 0)
 		if err != nil {
 			return nil, err
 		}
diff --git a/modules/git/repo_attribute.go b/modules/git/repo_attribute.go
new file mode 100644
index 0000000000..c10c96f558
--- /dev/null
+++ b/modules/git/repo_attribute.go
@@ -0,0 +1,84 @@
+// Copyright 2019 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package git
+
+import (
+	"bytes"
+	"fmt"
+
+	"github.com/mcuadros/go-version"
+)
+
+// CheckAttributeOpts represents the possible options to CheckAttribute
+type CheckAttributeOpts struct {
+	CachedOnly    bool
+	AllAttributes bool
+	Attributes    []string
+	Filenames     []string
+}
+
+// CheckAttribute return the Blame object of file
+func (repo *Repository) CheckAttribute(opts CheckAttributeOpts) (map[string]map[string]string, error) {
+	binVersion, err := BinVersion()
+	if err != nil {
+		return nil, fmt.Errorf("Git version missing: %v", err)
+	}
+
+	stdOut := new(bytes.Buffer)
+	stdErr := new(bytes.Buffer)
+
+	cmdArgs := []string{"check-attr", "-z"}
+
+	if opts.AllAttributes {
+		cmdArgs = append(cmdArgs, "-a")
+	} else {
+		for _, attribute := range opts.Attributes {
+			if attribute != "" {
+				cmdArgs = append(cmdArgs, attribute)
+			}
+		}
+	}
+
+	// git check-attr --cached first appears in git 1.7.8
+	if opts.CachedOnly && version.Compare(binVersion, "1.7.8", ">=") {
+		cmdArgs = append(cmdArgs, "--cached")
+	}
+
+	cmdArgs = append(cmdArgs, "--")
+
+	for _, arg := range opts.Filenames {
+		if arg != "" {
+			cmdArgs = append(cmdArgs, arg)
+		}
+	}
+
+	cmd := NewCommand(cmdArgs...)
+
+	if err := cmd.RunInDirPipeline(repo.Path, stdOut, stdErr); err != nil {
+		return nil, fmt.Errorf("Failed to run check-attr: %v\n%s\n%s", err, stdOut.String(), stdErr.String())
+	}
+
+	fields := bytes.Split(stdOut.Bytes(), []byte{'\000'})
+
+	if len(fields)%3 != 1 {
+		return nil, fmt.Errorf("Wrong number of fields in return from check-attr")
+	}
+
+	var name2attribute2info = make(map[string]map[string]string)
+
+	for i := 0; i < (len(fields) / 3); i++ {
+		filename := string(fields[3*i])
+		attribute := string(fields[3*i+1])
+		info := string(fields[3*i+2])
+		attribute2info := name2attribute2info[filename]
+		if attribute2info == nil {
+			attribute2info = make(map[string]string)
+		}
+		attribute2info[attribute] = info
+		name2attribute2info[filename] = attribute2info
+	}
+
+	return name2attribute2info, nil
+}
diff --git a/modules/lfs/locks.go b/modules/lfs/locks.go
index 9ffe6b9d59..b077cd2d0b 100644
--- a/modules/lfs/locks.go
+++ b/modules/lfs/locks.go
@@ -110,7 +110,7 @@ func GetListLockHandler(ctx *context.Context) {
 	}
 
 	//If no query params path or id
-	lockList, err := models.GetLFSLockByRepoID(repository.ID)
+	lockList, err := models.GetLFSLockByRepoID(repository.ID, 0, 0)
 	if err != nil {
 		ctx.JSON(500, api.LFSLockError{
 			Message: "unable to list locks : " + err.Error(),
@@ -220,7 +220,7 @@ func VerifyLockHandler(ctx *context.Context) {
 	}
 
 	//TODO handle body json cursor and limit
-	lockList, err := models.GetLFSLockByRepoID(repository.ID)
+	lockList, err := models.GetLFSLockByRepoID(repository.ID, 0, 0)
 	if err != nil {
 		ctx.JSON(500, api.LFSLockError{
 			Message: "unable to list locks : " + err.Error(),
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 98133cdab3..c6fd3b863f 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -1438,9 +1438,19 @@ settings.lfs_filelist=LFS files stored in this repository
 settings.lfs_no_lfs_files=No LFS files stored in this repository
 settings.lfs_findcommits=Find commits
 settings.lfs_lfs_file_no_commits=No Commits found for this LFS file
+settings.lfs_noattribute=This path does not have the lockable attribute in the default branch
 settings.lfs_delete=Delete LFS file with OID %s
 settings.lfs_delete_warning=Deleting an LFS file may cause 'object does not exist' errors on checkout. Are you sure?
 settings.lfs_findpointerfiles=Find pointer files
+settings.lfs_locks=Locks
+settings.lfs_invalid_locking_path=Invalid path: %s
+settings.lfs_invalid_lock_directory=Cannot lock directory: %s
+settings.lfs_lock_already_exists=Lock already exists: %s
+settings.lfs_lock=Lock
+settings.lfs_lock_path=Filepath to lock...
+settings.lfs_locks_no_locks=No Locks
+settings.lfs_lock_file_no_exist=Locked file does not exist in default branch
+settings.lfs_force_unlock=Force Unlock
 settings.lfs_pointers.found=Found %d blob pointer(s) - %d associated, %d unassociated (%d missing from store)
 settings.lfs_pointers.sha=Blob SHA
 settings.lfs_pointers.oid=OID
diff --git a/routers/repo/lfs.go b/routers/repo/lfs.go
index de5020c944..c3266844b4 100644
--- a/routers/repo/lfs.go
+++ b/routers/repo/lfs.go
@@ -12,6 +12,7 @@ import (
 	"io"
 	"io/ioutil"
 	"os"
+	"path"
 	"path/filepath"
 	"sort"
 	"strconv"
@@ -38,6 +39,7 @@ import (
 
 const (
 	tplSettingsLFS         base.TplName = "repo/settings/lfs"
+	tplSettingsLFSLocks    base.TplName = "repo/settings/lfs_locks"
 	tplSettingsLFSFile     base.TplName = "repo/settings/lfs_file"
 	tplSettingsLFSFileFind base.TplName = "repo/settings/lfs_file_find"
 	tplSettingsLFSPointers base.TplName = "repo/settings/lfs_pointers"
@@ -58,6 +60,7 @@ func LFSFiles(ctx *context.Context) {
 		ctx.ServerError("LFSFiles", err)
 		return
 	}
+	ctx.Data["Total"] = total
 
 	pager := context.NewPagination(int(total), setting.UI.ExplorePagingNum, page, 5)
 	ctx.Data["Title"] = ctx.Tr("repo.settings.lfs")
@@ -72,6 +75,179 @@ func LFSFiles(ctx *context.Context) {
 	ctx.HTML(200, tplSettingsLFS)
 }
 
+// LFSLocks shows a repository's LFS locks
+func LFSLocks(ctx *context.Context) {
+	if !setting.LFS.StartServer {
+		ctx.NotFound("LFSLocks", nil)
+		return
+	}
+	ctx.Data["LFSFilesLink"] = ctx.Repo.RepoLink + "/settings/lfs"
+
+	page := ctx.QueryInt("page")
+	if page <= 1 {
+		page = 1
+	}
+	total, err := models.CountLFSLockByRepoID(ctx.Repo.Repository.ID)
+	if err != nil {
+		ctx.ServerError("LFSLocks", err)
+		return
+	}
+	ctx.Data["Total"] = total
+
+	pager := context.NewPagination(int(total), setting.UI.ExplorePagingNum, page, 5)
+	ctx.Data["Title"] = ctx.Tr("repo.settings.lfs_locks")
+	ctx.Data["PageIsSettingsLFS"] = true
+	lfsLocks, err := models.GetLFSLockByRepoID(ctx.Repo.Repository.ID, pager.Paginater.Current(), setting.UI.ExplorePagingNum)
+	if err != nil {
+		ctx.ServerError("LFSLocks", err)
+		return
+	}
+	ctx.Data["LFSLocks"] = lfsLocks
+
+	if len(lfsLocks) == 0 {
+		ctx.Data["Page"] = pager
+		ctx.HTML(200, tplSettingsLFSLocks)
+		return
+	}
+
+	// Clone base repo.
+	tmpBasePath, err := models.CreateTemporaryPath("locks")
+	if err != nil {
+		log.Error("Failed to create temporary path: %v", err)
+		ctx.ServerError("LFSLocks", err)
+		return
+	}
+	defer func() {
+		if err := models.RemoveTemporaryPath(tmpBasePath); err != nil {
+			log.Error("LFSLocks: RemoveTemporaryPath: %v", err)
+		}
+	}()
+
+	if err := git.Clone(ctx.Repo.Repository.RepoPath(), tmpBasePath, git.CloneRepoOptions{
+		Bare:   true,
+		Shared: true,
+	}); err != nil {
+		log.Error("Failed to clone repository: %s (%v)", ctx.Repo.Repository.FullName(), err)
+		ctx.ServerError("LFSLocks", fmt.Errorf("Failed to clone repository: %s (%v)", ctx.Repo.Repository.FullName(), err))
+	}
+
+	gitRepo, err := git.OpenRepository(tmpBasePath)
+	if err != nil {
+		log.Error("Unable to open temporary repository: %s (%v)", tmpBasePath, err)
+		ctx.ServerError("LFSLocks", fmt.Errorf("Failed to open new temporary repository in: %s %v", tmpBasePath, err))
+	}
+
+	filenames := make([]string, len(lfsLocks))
+
+	for i, lock := range lfsLocks {
+		filenames[i] = lock.Path
+	}
+
+	if err := gitRepo.ReadTreeToIndex(ctx.Repo.Repository.DefaultBranch); err != nil {
+		log.Error("Unable to read the default branch to the index: %s (%v)", ctx.Repo.Repository.DefaultBranch, err)
+		ctx.ServerError("LFSLocks", fmt.Errorf("Unable to read the default branch to the index: %s (%v)", ctx.Repo.Repository.DefaultBranch, err))
+	}
+
+	name2attribute2info, err := gitRepo.CheckAttribute(git.CheckAttributeOpts{
+		Attributes: []string{"lockable"},
+		Filenames:  filenames,
+		CachedOnly: true,
+	})
+	if err != nil {
+		log.Error("Unable to check attributes in %s (%v)", tmpBasePath, err)
+		ctx.ServerError("LFSLocks", err)
+	}
+
+	lockables := make([]bool, len(lfsLocks))
+	for i, lock := range lfsLocks {
+		attribute2info, has := name2attribute2info[lock.Path]
+		if !has {
+			continue
+		}
+		if attribute2info["lockable"] != "set" {
+			continue
+		}
+		lockables[i] = true
+	}
+	ctx.Data["Lockables"] = lockables
+
+	filelist, err := gitRepo.LsFiles(filenames...)
+	if err != nil {
+		log.Error("Unable to lsfiles in %s (%v)", tmpBasePath, err)
+		ctx.ServerError("LFSLocks", err)
+	}
+
+	filemap := make(map[string]bool, len(filelist))
+	for _, name := range filelist {
+		filemap[name] = true
+	}
+
+	linkable := make([]bool, len(lfsLocks))
+	for i, lock := range lfsLocks {
+		linkable[i] = filemap[lock.Path]
+	}
+	ctx.Data["Linkable"] = linkable
+
+	ctx.Data["Page"] = pager
+	ctx.HTML(200, tplSettingsLFSLocks)
+}
+
+// LFSLockFile locks a file
+func LFSLockFile(ctx *context.Context) {
+	if !setting.LFS.StartServer {
+		ctx.NotFound("LFSLocks", nil)
+		return
+	}
+	originalPath := ctx.Query("path")
+	lockPath := originalPath
+	if len(lockPath) == 0 {
+		ctx.Flash.Error(ctx.Tr("repo.settings.lfs_invalid_locking_path", originalPath))
+		ctx.Redirect(ctx.Repo.RepoLink + "/settings/lfs/locks")
+		return
+	}
+	if lockPath[len(lockPath)-1] == '/' {
+		ctx.Flash.Error(ctx.Tr("repo.settings.lfs_invalid_lock_directory", originalPath))
+		ctx.Redirect(ctx.Repo.RepoLink + "/settings/lfs/locks")
+		return
+	}
+	lockPath = path.Clean("/" + lockPath)[1:]
+	if len(lockPath) == 0 {
+		ctx.Flash.Error(ctx.Tr("repo.settings.lfs_invalid_locking_path", originalPath))
+		ctx.Redirect(ctx.Repo.RepoLink + "/settings/lfs/locks")
+		return
+	}
+
+	_, err := models.CreateLFSLock(&models.LFSLock{
+		Repo:  ctx.Repo.Repository,
+		Path:  lockPath,
+		Owner: ctx.User,
+	})
+	if err != nil {
+		if models.IsErrLFSLockAlreadyExist(err) {
+			ctx.Flash.Error(ctx.Tr("repo.settings.lfs_lock_already_exists", originalPath))
+			ctx.Redirect(ctx.Repo.RepoLink + "/settings/lfs/locks")
+			return
+		}
+		ctx.ServerError("LFSLockFile", err)
+		return
+	}
+	ctx.Redirect(ctx.Repo.RepoLink + "/settings/lfs/locks")
+}
+
+// LFSUnlock forcibly unlocks an LFS lock
+func LFSUnlock(ctx *context.Context) {
+	if !setting.LFS.StartServer {
+		ctx.NotFound("LFSUnlock", nil)
+		return
+	}
+	_, err := models.DeleteLFSLockByID(ctx.ParamsInt64("lid"), ctx.User, true)
+	if err != nil {
+		ctx.ServerError("LFSUnlock", err)
+		return
+	}
+	ctx.Redirect(ctx.Repo.RepoLink + "/settings/lfs/locks")
+}
+
 // LFSFileGet serves a single LFS file
 func LFSFileGet(ctx *context.Context) {
 	if !setting.LFS.StartServer {
diff --git a/routers/routes/routes.go b/routers/routes/routes.go
index cdbbfaee04..cfd4a60974 100644
--- a/routers/routes/routes.go
+++ b/routers/routes/routes.go
@@ -685,6 +685,11 @@ func RegisterRoutes(m *macaron.Macaron) {
 				m.Get("/pointers", repo.LFSPointerFiles)
 				m.Post("/pointers/associate", repo.LFSAutoAssociate)
 				m.Get("/find", repo.LFSFileFind)
+				m.Group("/locks", func() {
+					m.Get("/", repo.LFSLocks)
+					m.Post("/", repo.LFSLockFile)
+					m.Post("/:lid/unlock", repo.LFSUnlock)
+				})
 			})
 
 		}, func(ctx *context.Context) {
diff --git a/templates/repo/settings/lfs.tmpl b/templates/repo/settings/lfs.tmpl
index e4480a8b97..f43f9479a2 100644
--- a/templates/repo/settings/lfs.tmpl
+++ b/templates/repo/settings/lfs.tmpl
@@ -5,9 +5,10 @@
 	<div class="ui container">
 		{{template "base/alert" .}}
 		<h4 class="ui top attached header">
-			{{.i18n.Tr "repo.settings.lfs_filelist"}}
+			{{.i18n.Tr "repo.settings.lfs_filelist"}} ({{.i18n.Tr "admin.total" .Total}})
 			<div class="ui right">
-				<a class="ui blue tiny show-panel button" href="{{.Link}}/pointers">{{.i18n.Tr "repo.settings.lfs_findpointerfiles"}}</a>
+				<a class="ui black tiny show-panel button" href="{{.Link}}/locks"><i class="octicon octicon-lock octicon-tiny"></i>{{.i18n.Tr "repo.settings.lfs_locks"}}</a>
+				<a class="ui blue tiny show-panel button" href="{{.Link}}/pointers"><i class="octicon octicon-search octicon-tiny"></i>&nbsp;{{.i18n.Tr "repo.settings.lfs_findpointerfiles"}}</a>
 			</div>
 		</h4>
 		<table id="lfs-files-table" class="ui attached segment single line table">
diff --git a/templates/repo/settings/lfs_locks.tmpl b/templates/repo/settings/lfs_locks.tmpl
new file mode 100644
index 0000000000..8a5f6e1658
--- /dev/null
+++ b/templates/repo/settings/lfs_locks.tmpl
@@ -0,0 +1,61 @@
+{{template "base/head" .}}
+<div class="repository settings lfs">
+	{{template "repo/header" .}}
+	{{template "repo/settings/navbar" .}}
+	<div class="ui container repository file list">
+		{{template "base/alert" .}}
+		<div class="tab-size-8 non-diff-file-content">
+			<h4 class="ui top attached header">
+				<a href="{{.LFSFilesLink}}">{{.i18n.Tr "repo.settings.lfs"}}</a> / {{.i18n.Tr "repo.settings.lfs_locks"}} ({{.i18n.Tr "admin.total" .Total}})
+			</h4>
+			<div class="ui attached segment">
+				<form class="ui form ignore-dirty" method="POST">
+					{{$.CsrfTokenHtml}}
+					<div class="ui fluid action input">
+						<input name="path" value="" placeholder="{{.i18n.Tr "repo.settings.lfs_lock_path"}}" autofocus>
+						<button class="ui blue button">{{.i18n.Tr "repo.settings.lfs_lock"}}</button>
+					</div>
+				</form>
+			</div>
+			<table id="lfs-files-locks-table" class="ui attached segment single line table">
+				<tbody>
+					{{range $index, $lock := .LFSLocks}}
+						<tr>
+							<td>
+								{{if index $.Linkable $index}}
+								<span class="octicon octicon-file-text"></span>
+								<a href="{{EscapePound $.RepoLink}}/src/branch/{{EscapePound $lock.Repo.DefaultBranch}}/{{EscapePound $lock.Path}}" title="{{$lock.Path}}">{{$lock.Path}}</a>
+								{{else}}
+								<span class="octicon octicon-diff"></span>
+								<span class="poping up" title="{{$.i18n.Tr "repo.settings.lfs_lock_file_no_exist"}}">{{$lock.Path}}</span>
+								{{end}}
+								{{if not (index $.Lockables $index)}}
+									<i class="octicon octicon-alert poping up" title="{{$.i18n.Tr "repo.settings.lfs_noattribute"}}"></i>
+								{{end}}
+							</td>
+							<td>
+								<a href="{{$.AppSubUrl}}/{{$lock.Owner.Name}}">
+									<img class="ui avatar image" src="{{$lock.Owner.RelAvatarLink}}">
+									{{$lock.Owner.DisplayName}}
+								</a>
+							</td>
+							<td>{{TimeSince .Created $.Lang}}</td>
+							<td class="right aligned">
+								<form action="{{$.LFSFilesLink}}/locks/{{$lock.ID}}/unlock" method="POST">
+									{{$.CsrfTokenHtml}}
+									<button class="ui blue button"><i class="octicon octicon-lock btn-octicon"></i>{{$.i18n.Tr "repo.settings.lfs_force_unlock"}}</button>
+								</form>
+							</td>
+						</tr>
+					{{else}}
+						<tr>
+							<td colspan="4">{{.i18n.Tr "repo.settings.lfs_locks_no_locks"}}</td>
+						</tr>
+					{{end}}
+				</tbody>
+			</table>
+			{{template "base/paginate" .}}
+		</div>
+	</div>
+</div>
+{{template "base/footer" .}}
diff --git a/web_src/less/_base.less b/web_src/less/_base.less
index 0fb12878ff..34a647f9a9 100644
--- a/web_src/less/_base.less
+++ b/web_src/less/_base.less
@@ -1112,3 +1112,7 @@ i.icon.centerlock {
         background: #fff866;
     }
 }
+
+.octicon-tiny {
+    font-size: 0.85714286rem;
+}