Git LFS support v2 (#122)

* Import github.com/git-lfs/lfs-test-server as lfs module base

Imported commit is 3968aac269a77b73924649b9412ae03f7ccd3198

Removed:

Dockerfile CONTRIBUTING.md mgmt* script/ vendor/ kvlogger.go
.dockerignore .gitignore README.md

* Remove config, add JWT support from github.com/mgit-at/lfs-test-server

Imported commit f0cdcc5a01599c5a955dc1bbf683bb4acecdba83

* Add LFS settings

* Add LFS meta object model

* Add LFS routes and initialization

* Import github.com/dgrijalva/jwt-go into vendor/

* Adapt LFS module: handlers, routing, meta store

* Move LFS routes to /user/repo/info/lfs/*

* Add request header checks to LFS BatchHandler / PostHandler

* Implement LFS basic authentication

* Rework JWT secret generation / load

* Implement LFS SSH token authentication with JWT

Specification: https://github.com/github/git-lfs/tree/master/docs/api

* Integrate LFS settings into install process

* Remove LFS objects when repository is deleted

Only removes objects from content store when deleted repo is the only
referencing repository

* Make LFS module stateless

Fixes bug where LFS would not work after installation without
restarting Gitea

* Change 500 'Internal Server Error' to 400 'Bad Request'

* Change sql query to xorm call

* Remove unneeded type from LFS module

* Change internal imports to code.gitea.io/gitea/

* Add Gitea authors copyright

* Change basic auth realm to "gitea-lfs"

* Add unique indexes to LFS model

* Use xorm count function in LFS check on repository delete

* Return io.ReadCloser from content store and close after usage

* Add LFS info to runWeb()

* Export LFS content store base path

* LFS file download from UI

* Work around git-lfs client issue with unauthenticated requests

Returning a dummy Authorization header for unauthenticated requests
lets git-lfs client skip asking for auth credentials
See: https://github.com/github/git-lfs/issues/1088

* Fix unauthenticated UI downloads from public repositories

* Authentication check order, Finish LFS file view logic

* Ignore LFS hooks if installed for current OS user

Fixes Gitea UI actions for repositories tracking LFS files.
Checks for minimum needed git version by parsing the semantic version
string.

* Hide LFS metafile diff from commit view, marking as binary

* Show LFS notice if file in commit view is tracked

* Add notbefore/nbf JWT claim

* Correct lint suggestions - comments for structs and functions

- Add comments to LFS model
- Function comment for GetRandomBytesAsBase64
- LFS server function comments and lint variable suggestion

* Move secret generation code out of conditional

Ensures no LFS code may run with an empty secret

* Do not hand out JWT tokens if LFS server support is disabled
This commit is contained in:
Fabian Zaremba 2016-12-26 02:16:37 +01:00 committed by Lunny Xiao
parent 4b7594d9fa
commit 2e7ccecfe6
37 changed files with 2632 additions and 11 deletions

View file

@ -23,6 +23,7 @@ type InstallForm struct {
AppName string `binding:"Required" locale:"install.app_name"`
RepoRootPath string `binding:"Required"`
LFSRootPath string
RunUser string `binding:"Required"`
Domain string `binding:"Required"`
SSHPort int

View file

@ -12,6 +12,7 @@ import (
"encoding/hex"
"fmt"
"html/template"
"io"
"math"
"math/big"
"net/http"
@ -103,6 +104,18 @@ func GetRandomString(n int) (string, error) {
return string(buffer), nil
}
// GetRandomBytesAsBase64 generates a random base64 string from n bytes
func GetRandomBytesAsBase64(n int) string {
bytes := make([]byte, 32)
_, err := io.ReadFull(rand.Reader, bytes)
if err != nil {
log.Fatal(4, "Error reading random bytes: %s", err)
}
return base64.RawURLEncoding.EncodeToString(bytes)
}
func randomInt(max *big.Int) (int, error) {
rand, err := rand.Int(rand.Reader, max)
if err != nil {

20
modules/lfs/LICENSE Normal file
View file

@ -0,0 +1,20 @@
Copyright (c) 2016 The Gitea Authors
Copyright (c) GitHub, Inc. and LFS Test Server contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,94 @@
package lfs
import (
"code.gitea.io/gitea/models"
"crypto/sha256"
"encoding/hex"
"errors"
"io"
"os"
"path/filepath"
)
var (
errHashMismatch = errors.New("Content hash does not match OID")
errSizeMismatch = errors.New("Content size does not match")
)
// ContentStore provides a simple file system based storage.
type ContentStore struct {
BasePath string
}
// Get takes a Meta object and retreives the content from the store, returning
// it as an io.Reader. If fromByte > 0, the reader starts from that byte
func (s *ContentStore) Get(meta *models.LFSMetaObject, fromByte int64) (io.ReadCloser, error) {
path := filepath.Join(s.BasePath, transformKey(meta.Oid))
f, err := os.Open(path)
if err != nil {
return nil, err
}
if fromByte > 0 {
_, err = f.Seek(fromByte, os.SEEK_CUR)
}
return f, err
}
// Put takes a Meta object and an io.Reader and writes the content to the store.
func (s *ContentStore) Put(meta *models.LFSMetaObject, r io.Reader) error {
path := filepath.Join(s.BasePath, transformKey(meta.Oid))
tmpPath := path + ".tmp"
dir := filepath.Dir(path)
if err := os.MkdirAll(dir, 0750); err != nil {
return err
}
file, err := os.OpenFile(tmpPath, os.O_CREATE|os.O_WRONLY|os.O_EXCL, 0640)
if err != nil {
return err
}
defer os.Remove(tmpPath)
hash := sha256.New()
hw := io.MultiWriter(hash, file)
written, err := io.Copy(hw, r)
if err != nil {
file.Close()
return err
}
file.Close()
if written != meta.Size {
return errSizeMismatch
}
shaStr := hex.EncodeToString(hash.Sum(nil))
if shaStr != meta.Oid {
return errHashMismatch
}
if err := os.Rename(tmpPath, path); err != nil {
return err
}
return nil
}
// Exists returns true if the object exists in the content store.
func (s *ContentStore) Exists(meta *models.LFSMetaObject) bool {
path := filepath.Join(s.BasePath, transformKey(meta.Oid))
if _, err := os.Stat(path); os.IsNotExist(err) {
return false
}
return true
}
func transformKey(key string) string {
if len(key) < 5 {
return key
}
return filepath.Join(key[0:2], key[2:4], key[4:len(key)])
}

549
modules/lfs/server.go Normal file
View file

@ -0,0 +1,549 @@
package lfs
import (
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"regexp"
"strconv"
"strings"
"time"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"github.com/dgrijalva/jwt-go"
"gopkg.in/macaron.v1"
)
const (
contentMediaType = "application/vnd.git-lfs"
metaMediaType = contentMediaType + "+json"
)
// RequestVars contain variables from the HTTP request. Variables from routing, json body decoding, and
// some headers are stored.
type RequestVars struct {
Oid string
Size int64
User string
Password string
Repo string
Authorization string
}
// BatchVars contains multiple RequestVars processed in one batch operation.
// https://github.com/git-lfs/git-lfs/blob/master/docs/api/batch.md
type BatchVars struct {
Transfers []string `json:"transfers,omitempty"`
Operation string `json:"operation"`
Objects []*RequestVars `json:"objects"`
}
// BatchResponse contains multiple object metadata Representation structures
// for use with the batch API.
type BatchResponse struct {
Transfer string `json:"transfer,omitempty"`
Objects []*Representation `json:"objects"`
}
// Representation is object medata as seen by clients of the lfs server.
type Representation struct {
Oid string `json:"oid"`
Size int64 `json:"size"`
Actions map[string]*link `json:"actions"`
Error *ObjectError `json:"error,omitempty"`
}
// ObjectError defines the JSON structure returned to the client in case of an error
type ObjectError struct {
Code int `json:"code"`
Message string `json:"message"`
}
// ObjectLink builds a URL linking to the object.
func (v *RequestVars) ObjectLink() string {
return fmt.Sprintf("%s%s/%s/info/lfs/objects/%s", setting.AppURL, v.User, v.Repo, v.Oid)
}
// link provides a structure used to build a hypermedia representation of an HTTP link.
type link struct {
Href string `json:"href"`
Header map[string]string `json:"header,omitempty"`
ExpiresAt time.Time `json:"expires_at,omitempty"`
}
// ObjectOidHandler is the main request routing entry point into LFS server functions
func ObjectOidHandler(ctx *context.Context) {
if !setting.LFS.StartServer {
writeStatus(ctx, 404)
return
}
if ctx.Req.Method == "GET" || ctx.Req.Method == "HEAD" {
if MetaMatcher(ctx.Req) {
GetMetaHandler(ctx)
return
}
if ContentMatcher(ctx.Req) || len(ctx.Params("filename")) > 0 {
GetContentHandler(ctx)
return
}
} else if ctx.Req.Method == "PUT" && ContentMatcher(ctx.Req) {
PutHandler(ctx)
return
}
}
// GetContentHandler gets the content from the content store
func GetContentHandler(ctx *context.Context) {
rv := unpack(ctx)
meta, err := models.GetLFSMetaObjectByOid(rv.Oid)
if err != nil {
writeStatus(ctx, 404)
return
}
repository, err := models.GetRepositoryByID(meta.RepositoryID)
if err != nil {
writeStatus(ctx, 404)
return
}
if !authenticate(ctx, repository, rv.Authorization, false) {
requireAuth(ctx)
return
}
// Support resume download using Range header
var fromByte int64
statusCode := 200
if rangeHdr := ctx.Req.Header.Get("Range"); rangeHdr != "" {
regex := regexp.MustCompile(`bytes=(\d+)\-.*`)
match := regex.FindStringSubmatch(rangeHdr)
if match != nil && len(match) > 1 {
statusCode = 206
fromByte, _ = strconv.ParseInt(match[1], 10, 32)
ctx.Resp.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", fromByte, meta.Size-1, int64(meta.Size)-fromByte))
}
}
contentStore := &ContentStore{BasePath: setting.LFS.ContentPath}
content, err := contentStore.Get(meta, fromByte)
if err != nil {
writeStatus(ctx, 404)
return
}
ctx.Resp.Header().Set("Content-Length", strconv.FormatInt(meta.Size, 10))
ctx.Resp.Header().Set("Content-Type", "application/octet-stream")
filename := ctx.Params("filename")
if len(filename) > 0 {
decodedFilename, err := base64.RawURLEncoding.DecodeString(filename)
if err == nil {
ctx.Resp.Header().Set("Content-Disposition", "attachment; filename=\""+string(decodedFilename)+"\"")
}
}
ctx.Resp.WriteHeader(statusCode)
io.Copy(ctx.Resp, content)
content.Close()
logRequest(ctx.Req, statusCode)
}
// GetMetaHandler retrieves metadata about the object
func GetMetaHandler(ctx *context.Context) {
rv := unpack(ctx)
meta, err := models.GetLFSMetaObjectByOid(rv.Oid)
if err != nil {
writeStatus(ctx, 404)
return
}
repository, err := models.GetRepositoryByID(meta.RepositoryID)
if err != nil {
writeStatus(ctx, 404)
return
}
if !authenticate(ctx, repository, rv.Authorization, false) {
requireAuth(ctx)
return
}
ctx.Resp.Header().Set("Content-Type", metaMediaType)
if ctx.Req.Method == "GET" {
enc := json.NewEncoder(ctx.Resp)
enc.Encode(Represent(rv, meta, true, false))
}
logRequest(ctx.Req, 200)
}
// PostHandler instructs the client how to upload data
func PostHandler(ctx *context.Context) {
if !setting.LFS.StartServer {
writeStatus(ctx, 404)
return
}
if !MetaMatcher(ctx.Req) {
writeStatus(ctx, 400)
return
}
rv := unpack(ctx)
repositoryString := rv.User + "/" + rv.Repo
repository, err := models.GetRepositoryByRef(repositoryString)
if err != nil {
log.Debug("Could not find repository: %s - %s", repositoryString, err)
writeStatus(ctx, 404)
return
}
if !authenticate(ctx, repository, rv.Authorization, true) {
requireAuth(ctx)
}
meta, err := models.NewLFSMetaObject(&models.LFSMetaObject{Oid: rv.Oid, Size: rv.Size, RepositoryID: repository.ID})
if err != nil {
writeStatus(ctx, 404)
return
}
ctx.Resp.Header().Set("Content-Type", metaMediaType)
sentStatus := 202
contentStore := &ContentStore{BasePath: setting.LFS.ContentPath}
if meta.Existing && contentStore.Exists(meta) {
sentStatus = 200
}
ctx.Resp.WriteHeader(sentStatus)
enc := json.NewEncoder(ctx.Resp)
enc.Encode(Represent(rv, meta, meta.Existing, true))
logRequest(ctx.Req, sentStatus)
}
// BatchHandler provides the batch api
func BatchHandler(ctx *context.Context) {
if !setting.LFS.StartServer {
writeStatus(ctx, 404)
return
}
if !MetaMatcher(ctx.Req) {
writeStatus(ctx, 400)
return
}
bv := unpackbatch(ctx)
var responseObjects []*Representation
// Create a response object
for _, object := range bv.Objects {
repositoryString := object.User + "/" + object.Repo
repository, err := models.GetRepositoryByRef(repositoryString)
if err != nil {
log.Debug("Could not find repository: %s - %s", repositoryString, err)
writeStatus(ctx, 404)
return
}
requireWrite := false
if bv.Operation == "upload" {
requireWrite = true
}
if !authenticate(ctx, repository, object.Authorization, requireWrite) {
requireAuth(ctx)
return
}
meta, err := models.GetLFSMetaObjectByOid(object.Oid)
contentStore := &ContentStore{BasePath: setting.LFS.ContentPath}
if err == nil && contentStore.Exists(meta) { // Object is found and exists
responseObjects = append(responseObjects, Represent(object, meta, true, false))
continue
}
// Object is not found
meta, err = models.NewLFSMetaObject(&models.LFSMetaObject{Oid: object.Oid, Size: object.Size, RepositoryID: repository.ID})
if err == nil {
responseObjects = append(responseObjects, Represent(object, meta, meta.Existing, true))
}
}
ctx.Resp.Header().Set("Content-Type", metaMediaType)
respobj := &BatchResponse{Objects: responseObjects}
enc := json.NewEncoder(ctx.Resp)
enc.Encode(respobj)
logRequest(ctx.Req, 200)
}
// PutHandler receives data from the client and puts it into the content store
func PutHandler(ctx *context.Context) {
rv := unpack(ctx)
meta, err := models.GetLFSMetaObjectByOid(rv.Oid)
if err != nil {
writeStatus(ctx, 404)
return
}
repository, err := models.GetRepositoryByID(meta.RepositoryID)
if err != nil {
writeStatus(ctx, 404)
return
}
if !authenticate(ctx, repository, rv.Authorization, true) {
requireAuth(ctx)
return
}
contentStore := &ContentStore{BasePath: setting.LFS.ContentPath}
if err := contentStore.Put(meta, ctx.Req.Body().ReadCloser()); err != nil {
models.RemoveLFSMetaObjectByOid(rv.Oid)
ctx.Resp.WriteHeader(500)
fmt.Fprintf(ctx.Resp, `{"message":"%s"}`, err)
return
}
logRequest(ctx.Req, 200)
}
// Represent takes a RequestVars and Meta and turns it into a Representation suitable
// for json encoding
func Represent(rv *RequestVars, meta *models.LFSMetaObject, download, upload bool) *Representation {
rep := &Representation{
Oid: meta.Oid,
Size: meta.Size,
Actions: make(map[string]*link),
}
header := make(map[string]string)
header["Accept"] = contentMediaType
if rv.Authorization == "" {
//https://github.com/github/git-lfs/issues/1088
header["Authorization"] = "Authorization: Basic dummy"
} else {
header["Authorization"] = rv.Authorization
}
if download {
rep.Actions["download"] = &link{Href: rv.ObjectLink(), Header: header}
}
if upload {
rep.Actions["upload"] = &link{Href: rv.ObjectLink(), Header: header}
}
return rep
}
// ContentMatcher provides a mux.MatcherFunc that only allows requests that contain
// an Accept header with the contentMediaType
func ContentMatcher(r macaron.Request) bool {
mediaParts := strings.Split(r.Header.Get("Accept"), ";")
mt := mediaParts[0]
return mt == contentMediaType
}
// MetaMatcher provides a mux.MatcherFunc that only allows requests that contain
// an Accept header with the metaMediaType
func MetaMatcher(r macaron.Request) bool {
mediaParts := strings.Split(r.Header.Get("Accept"), ";")
mt := mediaParts[0]
return mt == metaMediaType
}
func unpack(ctx *context.Context) *RequestVars {
r := ctx.Req
rv := &RequestVars{
User: ctx.Params("username"),
Repo: strings.TrimSuffix(ctx.Params("reponame"), ".git"),
Oid: ctx.Params("oid"),
Authorization: r.Header.Get("Authorization"),
}
if r.Method == "POST" { // Maybe also check if +json
var p RequestVars
dec := json.NewDecoder(r.Body().ReadCloser())
err := dec.Decode(&p)
if err != nil {
return rv
}
rv.Oid = p.Oid
rv.Size = p.Size
}
return rv
}
// TODO cheap hack, unify with unpack
func unpackbatch(ctx *context.Context) *BatchVars {
r := ctx.Req
var bv BatchVars
dec := json.NewDecoder(r.Body().ReadCloser())
err := dec.Decode(&bv)
if err != nil {
return &bv
}
for i := 0; i < len(bv.Objects); i++ {
bv.Objects[i].User = ctx.Params("username")
bv.Objects[i].Repo = strings.TrimSuffix(ctx.Params("reponame"), ".git")
bv.Objects[i].Authorization = r.Header.Get("Authorization")
}
return &bv
}
func writeStatus(ctx *context.Context, status int) {
message := http.StatusText(status)
mediaParts := strings.Split(ctx.Req.Header.Get("Accept"), ";")
mt := mediaParts[0]
if strings.HasSuffix(mt, "+json") {
message = `{"message":"` + message + `"}`
}
ctx.Resp.WriteHeader(status)
fmt.Fprint(ctx.Resp, message)
logRequest(ctx.Req, status)
}
func logRequest(r macaron.Request, status int) {
log.Debug("LFS request - Method: %s, URL: %s, Status %d", r.Method, r.URL, status)
}
// authenticate uses the authorization string to determine whether
// or not to proceed. This server assumes an HTTP Basic auth format.
func authenticate(ctx *context.Context, repository *models.Repository, authorization string, requireWrite bool) bool {
accessMode := models.AccessModeRead
if requireWrite {
accessMode = models.AccessModeWrite
}
if !repository.IsPrivate && !requireWrite {
return true
}
if ctx.IsSigned {
accessCheck, _ := models.HasAccess(ctx.User, repository, accessMode)
return accessCheck
}
if authorization == "" {
return false
}
if authenticateToken(repository, authorization, requireWrite) {
return true
}
if !strings.HasPrefix(authorization, "Basic ") {
return false
}
c, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(authorization, "Basic "))
if err != nil {
return false
}
cs := string(c)
i := strings.IndexByte(cs, ':')
if i < 0 {
return false
}
user, password := cs[:i], cs[i+1:]
userModel, err := models.GetUserByName(user)
if err != nil {
return false
}
if !userModel.ValidatePassword(password) {
return false
}
accessCheck, _ := models.HasAccess(userModel, repository, accessMode)
return accessCheck
}
func authenticateToken(repository *models.Repository, authorization string, requireWrite bool) bool {
if !strings.HasPrefix(authorization, "Bearer ") {
return false
}
token, err := jwt.Parse(authorization[7:], func(t *jwt.Token) (interface{}, error) {
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
}
return setting.LFS.JWTSecretBytes, nil
})
if err != nil {
return false
}
claims, claimsOk := token.Claims.(jwt.MapClaims)
if !token.Valid || !claimsOk {
return false
}
opStr, ok := claims["op"].(string)
if !ok {
return false
}
if requireWrite && opStr != "upload" {
return false
}
repoID, ok := claims["repo"].(float64)
if !ok {
return false
}
if repository.ID != int64(repoID) {
return false
}
return true
}
func requireAuth(ctx *context.Context) {
ctx.Resp.Header().Set("WWW-Authenticate", "Basic realm=gitea-lfs")
writeStatus(ctx, 401)
}

View file

@ -5,7 +5,10 @@
package setting
import (
"crypto/rand"
"encoding/base64"
"fmt"
"io"
"net/mail"
"net/url"
"os"
@ -17,6 +20,7 @@ import (
"strings"
"time"
"code.gitea.io/git"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/user"
"github.com/Unknwon/com"
@ -89,6 +93,13 @@ var (
MinimumKeySizes map[string]int `ini:"-"`
}
LFS struct {
StartServer bool `ini:"LFS_START_SERVER"`
ContentPath string `ini:"LFS_CONTENT_PATH"`
JWTSecretBase64 string `ini:"LFS_JWT_SECRET"`
JWTSecretBytes []byte `ini:"-"`
}
// Security settings
InstallLock bool
SecretKey string
@ -583,6 +594,85 @@ please consider changing to GITEA_CUSTOM`)
}
}
if err = Cfg.Section("server").MapTo(&LFS); err != nil {
log.Fatal(4, "Fail to map LFS settings: %v", err)
}
if LFS.StartServer {
if err := os.MkdirAll(LFS.ContentPath, 0700); err != nil {
log.Fatal(4, "Fail to create '%s': %v", LFS.ContentPath, err)
}
LFS.JWTSecretBytes = make([]byte, 32)
n, err := base64.RawURLEncoding.Decode(LFS.JWTSecretBytes, []byte(LFS.JWTSecretBase64))
if err != nil || n != 32 {
//Generate new secret and save to config
_, err := io.ReadFull(rand.Reader, LFS.JWTSecretBytes)
if err != nil {
log.Fatal(4, "Error reading random bytes: %s", err)
}
LFS.JWTSecretBase64 = base64.RawURLEncoding.EncodeToString(LFS.JWTSecretBytes)
// Save secret
cfg := ini.Empty()
if com.IsFile(CustomConf) {
// Keeps custom settings if there is already something.
if err := cfg.Append(CustomConf); err != nil {
log.Error(4, "Fail to load custom conf '%s': %v", CustomConf, err)
}
}
cfg.Section("server").Key("LFS_JWT_SECRET").SetValue(LFS.JWTSecretBase64)
os.MkdirAll(filepath.Dir(CustomConf), os.ModePerm)
if err := cfg.SaveTo(CustomConf); err != nil {
log.Fatal(4, "Error saving generated JWT Secret to custom config: %v", err)
return
}
}
//Disable LFS client hooks if installed for the current OS user
//Needs at least git v2.1.2
binVersion, err := git.BinVersion()
if err != nil {
log.Fatal(4, "Error retrieving git version: %s", err)
}
splitVersion := strings.SplitN(binVersion, ".", 3)
majorVersion, err := strconv.ParseUint(splitVersion[0], 10, 64)
if err != nil {
log.Fatal(4, "Error parsing git major version: %s", err)
}
minorVersion, err := strconv.ParseUint(splitVersion[1], 10, 64)
if err != nil {
log.Fatal(4, "Error parsing git minor version: %s", err)
}
revisionVersion, err := strconv.ParseUint(splitVersion[2], 10, 64)
if err != nil {
log.Fatal(4, "Error parsing git revision version: %s", err)
}
if !((majorVersion > 2) || (majorVersion == 2 && minorVersion > 1) ||
(majorVersion == 2 && minorVersion == 1 && revisionVersion >= 2)) {
LFS.StartServer = false
log.Error(4, "LFS server support needs at least Git v2.1.2")
} else {
git.GlobalCommandArgs = append(git.GlobalCommandArgs, "-c", "filter.lfs.required=",
"-c", "filter.lfs.smudge=", "-c", "filter.lfs.clean=")
}
}
sec = Cfg.Section("security")
InstallLock = sec.Key("INSTALL_LOCK").MustBool(false)
SecretKey = sec.Key("SECRET_KEY").MustString("!#@FDEWREWR&*(")