# Git LFS Changelog
+## 2.5.1 (2 August, 2018)
+
+This release contains miscellaneous bug fixes since v2.5.0. Most notably,
+release v2.5.1 allows a user to disable automatic Content-Type detection
+(released in v2.5.0) via `git config lfs.contenttype false` for hosts that do
+not support it.
+
+### Features
+
+* tq: make Content-Type detection disable-able #3163 (@ttaylorr)
+
+### Bugs
+
+* Makefile: add explicit rule for commands/mancontent_gen.go #3160 (@jj1bdx)
+* script/install.sh: mark as executable #3155 (@ttaylorr)
+* config: add origin to remote list #3152 (@PastelMobileSuit)
+
+### Misc
+
+* docs/man/mangen.go: don't show non-fatal output without --verbose #3168 (@ttaylorr)
+* LICENSE.md: update copyright year #3156 (@IMJ355)
+* Makefile: silence some output #3164 (@ttaylorr)
+* Makefile: list prerequisites for resource.syso #3153 (@ttaylorr)
+
## 2.5.0 (26 July, 2018)
This release adds three new migration modes, updated developer ergonomics, and
MIT License
-Copyright (c) 2014-2016 GitHub, Inc. and Git LFS contributors
+Copyright (c) 2014-2018 GitHub, Inc. and Git LFS 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
bin/git-lfs-freebsd-amd64 bin/git-lfs-freebsd-386 \
bin/git-lfs-windows-amd64.exe bin/git-lfs-windows-386.exe
+# mangen is a shorthand for ensuring that commands/mancontent_gen.go is kept
+# up-to-date with the contents of docs/man/*.ronn.
+.PHONY : mangen
+mangen : commands/mancontent_gen.go
+
+# commands/mancontent_gen.go is generated by running 'go generate' on package
+# 'commands' of Git LFS. It depends upon the contents of the 'docs' directory
+# and converts those manpages into code.
+commands/mancontent_gen.go : $(wildcard docs/man/*.ronn)
+ $(GO) generate github.com/git-lfs/git-lfs/commands
+
# Targets 'all' and 'build' build binaries of Git LFS for the above release
# matrix.
.PHONY : all build
#
# On Windows, they also depend on the resource.syso target, which installs and
# embeds the versioninfo into the binary.
-bin/git-lfs-darwin-amd64 : $(SOURCES)
+bin/git-lfs-darwin-amd64 : $(SOURCES) mangen
$(call BUILD,darwin,amd64,-darwin-amd64)
-bin/git-lfs-darwin-386 : $(SOURCES)
+bin/git-lfs-darwin-386 : $(SOURCES) mangen
$(call BUILD,darwin,386,-darwin-386)
-bin/git-lfs-linux-amd64 : $(SOURCES)
+bin/git-lfs-linux-amd64 : $(SOURCES) mangen
$(call BUILD,linux,amd64,-linux-amd64)
-bin/git-lfs-linux-386 : $(SOURCES)
+bin/git-lfs-linux-386 : $(SOURCES) mangen
$(call BUILD,linux,386,-linux-386)
-bin/git-lfs-freebsd-amd64 : $(SOURCES)
+bin/git-lfs-freebsd-amd64 : $(SOURCES) mangen
$(call BUILD,freebsd,amd64,-freebsd-amd64)
-bin/git-lfs-freebsd-386 : $(SOURCES)
+bin/git-lfs-freebsd-386 : $(SOURCES) mangen
$(call BUILD,freebsd,386,-freebsd-386)
-bin/git-lfs-windows-amd64.exe : resource.syso $(SOURCES)
+bin/git-lfs-windows-amd64.exe : resource.syso $(SOURCES) mangen
$(call BUILD,windows,amd64,-windows-amd64.exe)
-bin/git-lfs-windows-386.exe : resource.syso $(SOURCES)
+bin/git-lfs-windows-386.exe : resource.syso $(SOURCES) mangen
$(call BUILD,windows,386,-windows-386.exe)
# .DEFAULT_GOAL sets the operating system-appropriate Git LFS binary as the
# bin/git-lfs targets the default output of Git LFS on non-Windows operating
# systems, and respects the build knobs as above.
-bin/git-lfs : $(SOURCES) fmt
+bin/git-lfs : $(SOURCES) fmt mangen
$(call BUILD,$(GOOS),$(GOARCH),)
# bin/git-lfs.exe targets the default output of Git LFS on Windows systems, and
# respects the build knobs as above.
-bin/git-lfs.exe : $(SOURCES) resource.syso
+bin/git-lfs.exe : $(SOURCES) resource.syso mangen
$(call BUILD,$(GOOS),$(GOARCH),.exe)
# resource.syso installs the 'goversioninfo' command and uses it in order to
# generate a binary that has information included necessary to create the
# Windows installer.
-resource.syso:
+#
+# Generating a new resource.syso is a pure function of the contents in the
+# prerequisites listed below.
+resource.syso : \
+versioninfo.json script/windows-installer/git-lfs-logo.bmp \
+script/windows-installer/git-lfs-logo.ico \
+script/windows-installer/git-lfs-wizard-image.bmp
@$(GO) get github.com/josephspurrier/goversioninfo/cmd/goversioninfo
$(GO) generate
.PHONY : fmt
ifeq ($(shell test -x "`which $(GOIMPORTS)`"; echo $$?),0)
fmt : $(SOURCES) | lint
- $(GOIMPORTS) $(GOIMPORTS_EXTRA_OPTS) $?;
+ @$(GOIMPORTS) $(GOIMPORTS_EXTRA_OPTS) $?;
else
fmt : $(SOURCES) | lint
@echo "git-lfs: skipping fmt, no goimports found at \`$(GOIMPORTS)\` ..."
# are vendored in via vendor (see: above).
.PHONY : lint
lint : $(SOURCES)
- $(GO) list -f '{{ join .Deps "\n" }}' . \
+ @$(GO) list -f '{{ join .Deps "\n" }}' . \
| $(XARGS) $(GO) list -f '{{ if not .Standard }}{{ .ImportPath }}{{ end }}' \
| $(GREP) -v "github.com/git-lfs/git-lfs" || exit 0
Print(gitV)
Print("")
+ defaultRemote := ""
if cfg.IsDefaultRemote() {
- endpoint := getAPIClient().Endpoints.Endpoint("download", cfg.Remote())
+ defaultRemote = cfg.Remote()
+ endpoint := getAPIClient().Endpoints.Endpoint("download", defaultRemote)
if len(endpoint.Url) > 0 {
access := getAPIClient().Endpoints.AccessFor(endpoint.Url)
Print("Endpoint=%s (auth=%s)", endpoint.Url, access)
}
for _, remote := range cfg.Remotes() {
+ if remote == defaultRemote {
+ continue
+ }
remoteEndpoint := getAPIClient().Endpoints.RemoteEndpoint("download", remote)
remoteAccess := getAPIClient().Endpoints.AccessFor(remoteEndpoint.Url)
Print("Endpoint (%s)=%s (auth=%s)", remote, remoteEndpoint.Url, remoteAccess)
gf, extensions, uniqRemotes := readGitConfig(gitconfigs...)
c.extensions = extensions
c.remotes = make([]string, 0, len(uniqRemotes))
- for remote, isOrigin := range uniqRemotes {
- if isOrigin {
- continue
- }
+ for remote := range uniqRemotes {
c.remotes = append(c.remotes, remote)
}
)
const (
- Version = "2.5.0"
+ Version = "2.5.1"
)
func init() {
+git-lfs (2.5.1) stable; urgency=low
+
+ * New upstream version
+
+ -- Taylor Blau <me@ttaylorr.com> Thu, 2 Aug 2018 14:29:00 +0000
+
git-lfs (2.5.0) stable; urgency=low
* New upstream version
https://git-scm.com/docs/git-config#git-config-httplturlgt. To set this value
per-host: `git config --global lfs.https://github.com/.locksverify [true|false]`.
+* `lfs.<url>.contenttype`
+
+ Determines whether Git LFS should attempt to detect an appropriate HTTP
+ `Content-Type` header when uploading using the 'basic' upload adapter. If set
+ to false, the default header of `Content-Type: application/octet-stream` is
+ chosen instead. Default: 'true'.
+
* `lfs.skipdownloaderrors`
Causes Git LFS not to abort the smudge filter when a download error is
import (
"bufio"
+ "flag"
"fmt"
+ "io"
"io/ioutil"
"os"
"path/filepath"
"strings"
)
+func infof(w io.Writer, format string, a ...interface{}) {
+ if !*verbose {
+ return
+ }
+ fmt.Fprintf(w, format, a...)
+}
+
+func warnf(w io.Writer, format string, a ...interface{}) {
+ fmt.Fprintf(w, format, a...)
+}
+
func readManDir() (string, []os.FileInfo) {
rootDirs := []string{
"..",
}
}
- fmt.Fprintf(os.Stderr, "Failed to open man dir: %v\n", err)
+ warnf(os.Stderr, "Failed to open man dir: %v\n", err)
os.Exit(2)
return "", nil
}
+var (
+ verbose = flag.Bool("verbose", false, "Show verbose output.")
+)
+
// Reads all .ronn files & and converts them to string literals
// triggered by "go generate" comment
// Literals are inserted into a map using an init function, this means
// that there are no compilation errors if 'go generate' hasn't been run, just
// blank man files.
func main() {
- fmt.Fprintf(os.Stderr, "Converting man pages into code...\n")
+ flag.Parse()
+
+ infof(os.Stderr, "Converting man pages into code...\n")
rootDir, fs := readManDir()
manDir := filepath.Join(rootDir, "docs", "man")
out, err := os.Create(filepath.Join(rootDir, "commands", "mancontent_gen.go"))
if err != nil {
- fmt.Fprintf(os.Stderr, "Failed to create go file: %v\n", err)
+ warnf(os.Stderr, "Failed to create go file: %v\n", err)
os.Exit(2)
}
out.WriteString("package commands\n\nfunc init() {\n")
count := 0
for _, f := range fs {
if match := fileregex.FindStringSubmatch(f.Name()); match != nil {
- fmt.Fprintf(os.Stderr, "%v\n", f.Name())
+ infof(os.Stderr, "%v\n", f.Name())
cmd := match[1]
if len(cmd) == 0 {
// This is git-lfs.1.ronn
out.WriteString("ManPages[\"" + cmd + "\"] = `")
contentf, err := os.Open(filepath.Join(manDir, f.Name()))
if err != nil {
- fmt.Fprintf(os.Stderr, "Failed to open %v: %v\n", f.Name(), err)
+ warnf(os.Stderr, "Failed to open %v: %v\n", f.Name(), err)
os.Exit(2)
}
// Process the ronn to make it nicer as help text
}
}
out.WriteString("}\n")
- fmt.Fprintf(os.Stderr, "Successfully processed %d man pages.\n", count)
+ infof(os.Stderr, "Successfully processed %d man pages.\n", count)
}
return false
}
+// IsDownloadDeclinedError indicates that the upload operation failed because of
+// an HTTP 422 response code.
+func IsUnprocessableEntityError(err error) bool {
+ if e, ok := err.(interface {
+ UnprocessableEntityError() bool
+ }); ok {
+ return e.UnprocessableEntityError()
+ }
+ if parent := parentOf(err); parent != nil {
+ return IsUnprocessableEntityError(parent)
+ }
+ return false
+}
+
// IsRetriableError indicates the low level transfer had an error but the
// caller may retry the operation.
func IsRetriableError(err error) bool {
return downloadDeclinedError{newWrappedError(err, msg)}
}
+// Definitions for IsUnprocessableEntityError()
+
+type unprocessableEntityError struct {
+ *wrappedError
+}
+
+func (e unprocessableEntityError) UnprocessableEntityError() bool {
+ return true
+}
+
+func NewUnprocessableEntityError(err error) error {
+ return unprocessableEntityError{newWrappedError(err, "")}
+}
+
// Definitions for IsRetriableError()
type retriableError struct {
return errors.NewAuthError(err)
}
+ if res.StatusCode == 422 {
+ return errors.NewUnprocessableEntityError(err)
+ }
+
if res.StatusCode > 499 && res.StatusCode != 501 && res.StatusCode != 507 && res.StatusCode != 509 {
return errors.NewFatalError(err)
}
401: "Authorization error: %s\nCheck that you have proper access to the repository",
403: "Authorization error: %s\nCheck that you have proper access to the repository",
404: "Repository or object not found: %s\nCheck that it exists and that you have proper access to it",
+ 422: "Unprocessable entity: %s",
429: "Rate limit exceeded: %s",
500: "Server error: %s",
501: "Not Implemented: %s",
Name: git-lfs
-Version: 2.5.0
+Version: 2.5.1
Release: 1%{?dist}
Summary: Git extension for versioning large files
--- /dev/null
+#!/usr/bin/env bash
+
+. "$(dirname "$0")/testlib.sh"
+
+begin_test "content-type: is enabled by default"
+(
+ set -e
+
+ reponame="content-type-enabled-default"
+ setup_remote_repo "$reponame"
+ clone_repo "$reponame" "$reponame"
+
+ git lfs track "*.tar.gz"
+ printf "aaaaaaaaaa" > a.txt
+ tar -czf a.tar.gz a.txt
+ rm a.txt
+
+ git add .gitattributes a.tar.gz
+ git commit -m "initial commit"
+ GIT_CURL_VERBOSE=1 git push origin master 2>&1 | tee push.log
+
+ [ 1 -eq "$(grep -c "Content-Type: application/x-gzip" push.log)" ]
+)
+end_test
+
+begin_test "content-type: is disabled by configuration"
+(
+ set -e
+
+ reponame="content-type-disabled-by-configuration"
+ setup_remote_repo "$reponame"
+ clone_repo "$reponame" "$reponame"
+
+ git lfs track "*.tar.gz"
+ printf "aaaaaaaaaa" > a.txt
+ tar -czf a.tar.gz a.txt
+ rm a.txt
+
+ git add .gitattributes a.tar.gz
+ git commit -m "initial commit"
+ git config "lfs.$GITSERVER.contenttype" 0
+ GIT_CURL_VERBOSE=1 git push origin master 2>&1 | tee push.log
+
+ [ 0 -eq "$(grep -c "Content-Type: application/x-gzip" push.log)" ]
+)
+end_test
+
+begin_test "content-type: warning message"
+(
+ set -e
+
+ reponame="content-type-warning-message"
+ setup_remote_repo "$reponame"
+ clone_repo "$reponame" "$reponame"
+
+ git lfs track "*.txt"
+ printf "status-storage-422" > a.txt
+
+ git add .gitattributes a.txt
+ git commit -m "initial commit"
+ git push origin master 2>&1 | tee push.log
+
+ grep "info: Uploading failed due to unsupported Content-Type header(s)." push.log
+ grep "info: Consider disabling Content-Type detection with:" push.log
+ grep "info: $ git config lfs.contenttype false" push.log
+)
+end_test
)
end_test
+
+begin_test "env with multiple remotes and ref"
+(
+ set -e
+ reponame="env-multiple-remotes-ref"
+ mkdir $reponame
+ cd $reponame
+ git init
+ git remote add origin "$GITSERVER/env-origin-remote"
+ git remote add other "$GITSERVER/env-other-remote"
+
+ touch a.txt
+ git add a.txt
+ git commit -m "initial commit"
+
+ endpoint="$GITSERVER/env-origin-remote.git/info/lfs (auth=none)"
+ endpoint2="$GITSERVER/env-other-remote.git/info/lfs (auth=none)"
+ localwd=$(native_path "$TRASHDIR/$reponame")
+ localgit=$(native_path "$TRASHDIR/$reponame/.git")
+ localgitstore=$(native_path "$TRASHDIR/$reponame/.git")
+ lfsstorage=$(native_path "$TRASHDIR/$reponame/.git/lfs")
+ localmedia=$(native_path "$TRASHDIR/$reponame/.git/lfs/objects")
+ tempdir=$(native_path "$TRASHDIR/$reponame/.git/lfs/tmp")
+ envVars=$(printf "%s" "$(env | grep "^GIT")")
+ expected=$(printf '%s
+%s
+
+Endpoint=%s
+Endpoint (other)=%s
+LocalWorkingDir=%s
+LocalGitDir=%s
+LocalGitStorageDir=%s
+LocalMediaDir=%s
+LocalReferenceDirs=
+TempDir=%s
+ConcurrentTransfers=3
+TusTransfers=false
+BasicTransfersOnly=false
+SkipDownloadErrors=false
+FetchRecentAlways=false
+FetchRecentRefsDays=7
+FetchRecentCommitsDays=0
+FetchRecentRefsIncludeRemotes=true
+PruneOffsetDays=3
+PruneVerifyRemoteAlways=false
+PruneRemoteName=origin
+LfsStorageDir=%s
+AccessDownload=none
+AccessUpload=none
+DownloadTransfers=basic
+UploadTransfers=basic
+%s
+%s
+' "$(git lfs version)" "$(git version)" "$endpoint" "$endpoint2" "$localwd" "$localgit" "$localgitstore" "$localmedia" "$tempdir" "$lfsstorage" "$envVars" "$envInitConfig")
+ actual=$(git lfs env | grep -v "^GIT_EXEC_PATH=")
+ contains_same_elements "$expected" "$actual"
+)
+end_test
)
end_test
+begin_test "pull with multiple remotes"
+(
+ set -e
+ mkdir multiple
+ cd multiple
+ git init
+ git lfs install --local --skip-smudge
+
+ git remote add origin "$GITSERVER/t-pull"
+ git remote add bad-remote "invalid-url"
+ git pull origin master
+
+ contents="a"
+ contents_oid=$(calc_oid "$contents")
+
+ # LFS object not downloaded, pointer in working directory
+ refute_local_object "$contents_oid"
+ grep "$contents_oid" a.dat
+
+ # pull should default to origin instead of bad-remote
+ git lfs pull
+ echo "pulled!"
+
+ # LFS object downloaded and in working directory
+ assert_local_object "$contents_oid" 1
+ [ "0" = "$(grep -c "$contents_oid" a.dat)" ]
+ [ "a" = "$(cat a.dat)" ]
+)
+end_test
+
begin_test "pull: with missing object"
(
set -e
)
end_test
-begin_test "push: upload file with storage 422"
-(
- set -e
-
- push_fail_test "status-storage-422"
-)
-end_test
-
begin_test "push: upload file with storage 500"
(
set -e
"strconv"
"strings"
+ "github.com/git-lfs/git-lfs/config"
"github.com/git-lfs/git-lfs/errors"
"github.com/git-lfs/git-lfs/lfsapi"
"github.com/git-lfs/git-lfs/tools"
}
defer f.Close()
- if err := setContentTypeFor(req, f); err != nil {
+ if err := a.setContentTypeFor(req, f); err != nil {
return err
}
req = a.apiClient.LogRequest(req, "lfs.data.upload")
res, err := a.doHTTP(t, req)
if err != nil {
+ if errors.IsUnprocessableEntityError(err) {
+ // If we got an HTTP 422, we do _not_ want to retry the
+ // request later below, because it is likely that the
+ // implementing server does not support non-standard
+ // Content-Type headers.
+ //
+ // Instead, return immediately and wait for the
+ // *tq.TransferQueue to report an error message.
+ return err
+ }
+
// We're about to return a retriable error, meaning that this
// transfer will either be retried, or it will fail.
//
return verifyUpload(a.apiClient, a.remote, t)
}
+func (a *adapterBase) setContentTypeFor(req *http.Request, r io.ReadSeeker) error {
+ uc := config.NewURLConfig(a.apiClient.GitEnv())
+ disabled := !uc.Bool("lfs", req.URL.String(), "contenttype", true)
+ if len(req.Header.Get("Content-Type")) != 0 || disabled {
+ return nil
+ }
+
+ buffer := make([]byte, 512)
+ n, err := r.Read(buffer)
+ if err != nil && err != io.EOF {
+ return errors.Wrap(err, "content type detect")
+ }
+
+ contentType := http.DetectContentType(buffer[:n])
+ if _, err := r.Seek(0, 0); err != nil {
+ return errors.Wrap(err, "content type rewind")
+ }
+
+ if contentType == "" {
+ contentType = defaultContentType
+ }
+
+ req.Header.Set("Content-Type", contentType)
+ return nil
+}
+
// startCallbackReader is a reader wrapper which calls a function as soon as the
// first Read() call is made. This callback is only made once
type startCallbackReader struct {
return nil
})
}
-
-func setContentTypeFor(req *http.Request, r io.ReadSeeker) error {
- if len(req.Header.Get("Content-Type")) != 0 {
- return nil
- }
-
- buffer := make([]byte, 512)
- n, err := r.Read(buffer)
- if err != nil && err != io.EOF {
- return errors.Wrap(err, "content type detect")
- }
-
- contentType := http.DetectContentType(buffer[:n])
- if _, err := r.Seek(0, 0); err != nil {
- return errors.Wrap(err, "content type rewind")
- }
-
- if contentType == "" {
- contentType = defaultContentType
- }
-
- req.Header.Set("Content-Type", contentType)
- return nil
-}
package tq
import (
+ "fmt"
"os"
"sort"
"sync"
wait sync.WaitGroup
manifest *Manifest
rc *retryCounter
+
+ // unsupportedContentType indicates whether the transfer queue ever saw
+ // an HTTP 422 response indicating that their upload destination does
+ // not support Content-Type detection.
+ unsupportedContentType bool
}
// objects holds a set of objects.
// If the error wasn't retriable, OR the object has
// exceeded its retry budget, it will be NOT be sent to
// the retry channel, and the error will be reported
- // immediately.
- q.errorc <- res.Error
+ // immediately (unless the error is in response to a
+ // HTTP 422).
+ if errors.IsUnprocessableEntityError(res.Error) {
+ q.unsupportedContentType = true
+ } else {
+ q.errorc <- res.Error
+ }
q.wait.Done()
}
} else {
}
}
+var (
+ // contentTypeWarning is the message printed when a server returns an
+ // HTTP 422 at the end of a push.
+ contentTypeWarning = []string{
+ "Uploading failed due to unsupported Content-Type header(s).",
+ "Consider disabling Content-Type detection with:",
+ "",
+ " $ git config lfs.contenttype false",
+ }
+)
+
// Wait waits for the queue to finish processing all transfers. Once Wait is
// called, Add will no longer add transfers to the queue. Any failed
// transfers will be automatically retried once.
q.meter.Flush()
q.errorwait.Wait()
+
+ if q.unsupportedContentType {
+ for _, line := range contentTypeWarning {
+ fmt.Fprintf(os.Stderr, "info: %s\n", line)
+ }
+ }
}
// Watch returns a channel where the queue will write the value of each transfer
"FileVersion": {
"Major": 2,
"Minor": 5,
- "Patch": 0,
+ "Patch": 1,
"Build": 0
}
},
"FileDescription": "Git LFS",
"LegalCopyright": "GitHub, Inc. and Git LFS contributors",
"ProductName": "Git Large File Storage (LFS)",
- "ProductVersion": "2.5.0"
+ "ProductVersion": "2.5.1"
},
"IconPath": "script/windows-installer/git-lfs-logo.ico"
}