Update to go-git v5.1.0 (#11936)

Signed-off-by: Andrew Thornton <art27@cantab.net>

Co-authored-by: techknowlogick <techknowlogick@gitea.io>
This commit is contained in:
zeripath 2020-06-18 00:29:38 +01:00 committed by GitHub
parent 6bf78d2b57
commit 1426126690
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
76 changed files with 3134 additions and 556 deletions

View file

@ -101,7 +101,7 @@ is supported by go-git.
| http(s):// (smart) | ✔ |
| git:// | ✔ |
| ssh:// | ✔ |
| file:// | |
| file:// | partial | Warning: this is not pure Golang. This shells out to the `git` binary. |
| custom | ✔ |
| **other features** |
| gitignore | ✔ |

View file

@ -1,9 +1,9 @@
![go-git logo](https://cdn.rawgit.com/src-d/artwork/02036484/go-git/files/go-git-github-readme-header.png)
[![GoDoc](https://godoc.org/github.com/go-git/go-git/v5?status.svg)](https://godoc.org/github.com/src-d/go-git) [![Build Status](https://github.com/go-git/go-git/workflows/Test%20&%20Coverage/badge.svg)](https://github.com/go-git/go-git/actions) [![Go Report Card](https://goreportcard.com/badge/github.com/src-d/go-git)](https://goreportcard.com/report/github.com/src-d/go-git)
[![GoDoc](https://godoc.org/github.com/go-git/go-git/v5?status.svg)](https://pkg.go.dev/github.com/go-git/go-git/v5) [![Build Status](https://github.com/go-git/go-git/workflows/Test/badge.svg)](https://github.com/go-git/go-git/actions) [![Go Report Card](https://goreportcard.com/badge/github.com/go-git/go-git)](https://goreportcard.com/report/github.com/go-git/go-git)
*go-git* is a highly extensible git implementation library written in **pure Go**.
It can be used to manipulate git repositories at low level *(plumbing)* or high level *(porcelain)*, through an idiomatic Go API. It also supports several types of storage, such as in-memory filesystems, or custom implementations, thanks to the [`Storer`](https://godoc.org/github.com/go-git/go-git/v5/plumbing/storer) interface.
It can be used to manipulate git repositories at low level *(plumbing)* or high level *(porcelain)*, through an idiomatic Go API. It also supports several types of storage, such as in-memory filesystems, or custom implementations, thanks to the [`Storer`](https://pkg.go.dev/github.com/go-git/go-git/v5/plumbing/storer) interface.
It's being actively developed since 2015 and is being used extensively by [Keybase](https://keybase.io/blog/encrypted-git-for-everyone), [Gitea](https://gitea.io/en-us/) or [Pulumi](https://github.com/search?q=org%3Apulumi+go-git&type=Code), and by many other libraries and tools.
@ -12,7 +12,7 @@ Project Status
After the legal issues with the [`src-d`](https://github.com/src-d) organization, the lack of update for four months and the requirement to make a hard fork, the project is **now back to normality**.
The project is currently actively maintained by individual contributors, including several of the original authors, but also backed by a new company `gitsigth` where `go-git` is a critical component used at scale.
The project is currently actively maintained by individual contributors, including several of the original authors, but also backed by a new company, [gitsight](https://github.com/gitsight), where `go-git` is a critical component used at scale.
Comparison with git
@ -37,7 +37,7 @@ import "github.com/go-git/go-git" // with go modules disabled
Examples
--------
> Please note that the `CheckIfError` and `Info` functions used in the examples are from the [examples package](https://github.com/src-d/go-git/blob/master/_examples/common.go#L17) just to be used in the examples.
> Please note that the `CheckIfError` and `Info` functions used in the examples are from the [examples package](https://github.com/go-git/go-git/blob/master/_examples/common.go#L19) just to be used in the examples.
### Basic example

View file

@ -5,11 +5,16 @@ import (
"bytes"
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"sort"
"strconv"
"github.com/go-git/go-git/v5/internal/url"
format "github.com/go-git/go-git/v5/plumbing/format/config"
"github.com/mitchellh/go-homedir"
)
const (
@ -32,6 +37,16 @@ var (
ErrRemoteConfigEmptyName = errors.New("remote config: empty name")
)
// Scope defines the scope of a config file, such as local, global or system.
type Scope int
// Available ConfigScope's
const (
LocalScope Scope = iota
GlobalScope
SystemScope
)
// Config contains the repository configuration
// https://www.kernel.org/pub/software/scm/git/docs/git-config.html#FILES
type Config struct {
@ -46,6 +61,27 @@ type Config struct {
CommentChar string
}
User struct {
// Name is the personal name of the author and the commiter of a commit.
Name string
// Email is the email of the author and the commiter of a commit.
Email string
}
Author struct {
// Name is the personal name of the author of a commit.
Name string
// Email is the email of the author of a commit.
Email string
}
Committer struct {
// Name is the personal name of the commiter of a commit.
Name string
// Email is the email of the the commiter of a commit.
Email string
}
Pack struct {
// Window controls the size of the sliding window for delta
// compression. The default is 10. A value of 0 turns off
@ -82,6 +118,77 @@ func NewConfig() *Config {
return config
}
// ReadConfig reads a config file from a io.Reader.
func ReadConfig(r io.Reader) (*Config, error) {
b, err := ioutil.ReadAll(r)
if err != nil {
return nil, err
}
cfg := NewConfig()
if err = cfg.Unmarshal(b); err != nil {
return nil, err
}
return cfg, nil
}
// LoadConfig loads a config file from a given scope. The returned Config,
// contains exclusively information fom the given scope. If couldn't find a
// config file to the given scope, a empty one is returned.
func LoadConfig(scope Scope) (*Config, error) {
if scope == LocalScope {
return nil, fmt.Errorf("LocalScope should be read from the a ConfigStorer.")
}
files, err := Paths(scope)
if err != nil {
return nil, err
}
for _, file := range files {
f, err := os.Open(file)
if err != nil {
if os.IsNotExist(err) {
continue
}
return nil, err
}
defer f.Close()
return ReadConfig(f)
}
return NewConfig(), nil
}
// Paths returns the config file location for a given scope.
func Paths(scope Scope) ([]string, error) {
var files []string
switch scope {
case GlobalScope:
xdg := os.Getenv("XDG_CONFIG_HOME")
if xdg != "" {
files = append(files, filepath.Join(xdg, "git/config"))
}
home, err := homedir.Dir()
if err != nil {
return nil, err
}
files = append(files,
filepath.Join(home, ".gitconfig"),
filepath.Join(home, ".config/git/config"),
)
case SystemScope:
files = append(files, "/etc/gitconfig")
}
return files, nil
}
// Validate validates the fields and sets the default values.
func (c *Config) Validate() error {
for name, r := range c.Remotes {
@ -113,6 +220,9 @@ const (
branchSection = "branch"
coreSection = "core"
packSection = "pack"
userSection = "user"
authorSection = "author"
committerSection = "committer"
fetchKey = "fetch"
urlKey = "url"
bareKey = "bare"
@ -121,6 +231,8 @@ const (
windowKey = "window"
mergeKey = "merge"
rebaseKey = "rebase"
nameKey = "name"
emailKey = "email"
// DefaultPackWindow holds the number of previous objects used to
// generate deltas. The value 10 is the same used by git command.
@ -138,6 +250,7 @@ func (c *Config) Unmarshal(b []byte) error {
}
c.unmarshalCore()
c.unmarshalUser()
if err := c.unmarshalPack(); err != nil {
return err
}
@ -160,6 +273,20 @@ func (c *Config) unmarshalCore() {
c.Core.CommentChar = s.Options.Get(commentCharKey)
}
func (c *Config) unmarshalUser() {
s := c.Raw.Section(userSection)
c.User.Name = s.Options.Get(nameKey)
c.User.Email = s.Options.Get(emailKey)
s = c.Raw.Section(authorSection)
c.Author.Name = s.Options.Get(nameKey)
c.Author.Email = s.Options.Get(emailKey)
s = c.Raw.Section(committerSection)
c.Committer.Name = s.Options.Get(nameKey)
c.Committer.Email = s.Options.Get(emailKey)
}
func (c *Config) unmarshalPack() error {
s := c.Raw.Section(packSection)
window := s.Options.Get(windowKey)
@ -220,6 +347,7 @@ func (c *Config) unmarshalBranches() error {
// Marshal returns Config encoded as a git-config file.
func (c *Config) Marshal() ([]byte, error) {
c.marshalCore()
c.marshalUser()
c.marshalPack()
c.marshalRemotes()
c.marshalSubmodules()
@ -242,6 +370,35 @@ func (c *Config) marshalCore() {
}
}
func (c *Config) marshalUser() {
s := c.Raw.Section(userSection)
if c.User.Name != "" {
s.SetOption(nameKey, c.User.Name)
}
if c.User.Email != "" {
s.SetOption(emailKey, c.User.Email)
}
s = c.Raw.Section(authorSection)
if c.Author.Name != "" {
s.SetOption(nameKey, c.Author.Name)
}
if c.Author.Email != "" {
s.SetOption(emailKey, c.Author.Email)
}
s = c.Raw.Section(committerSection)
if c.Committer.Name != "" {
s.SetOption(nameKey, c.Committer.Name)
}
if c.Committer.Email != "" {
s.SetOption(emailKey, c.Committer.Email)
}
}
func (c *Config) marshalPack() {
s := c.Raw.Section(packSection)
if c.Pack.Window != DefaultPackWindow {

View file

@ -25,7 +25,7 @@ var (
// reference even if it isnt a fast-forward.
// eg.: "+refs/heads/*:refs/remotes/origin/*"
//
// https://git-scm.com/book/es/v2/Git-Internals-The-Refspec
// https://git-scm.com/book/en/v2/Git-Internals-The-Refspec
type RefSpec string
// Validate validates the RefSpec
@ -59,6 +59,11 @@ func (s RefSpec) IsDelete() bool {
return s[0] == refSpecSeparator[0]
}
// IsExactSHA1 returns true if the source is a SHA1 hash.
func (s RefSpec) IsExactSHA1() bool {
return plumbing.IsHash(s.Src())
}
// Src return the src side.
func (s RefSpec) Src() string {
spec := string(s)
@ -69,8 +74,8 @@ func (s RefSpec) Src() string {
} else {
start = 0
}
end := strings.Index(spec, refSpecSeparator)
end := strings.Index(spec, refSpecSeparator)
return spec[start:end]
}

View file

@ -10,6 +10,7 @@ require (
github.com/go-git/go-billy/v5 v5.0.0
github.com/go-git/go-git-fixtures/v4 v4.0.1
github.com/google/go-cmp v0.3.0
github.com/imdario/mergo v0.3.9
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99
github.com/jessevdk/go-flags v1.4.0
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd

View file

@ -22,6 +22,8 @@ github.com/go-git/go-git-fixtures/v4 v4.0.1 h1:q+IFMfLx200Q3scvt2hN79JsEzy4AmBTp
github.com/go-git/go-git-fixtures/v4 v4.0.1/go.mod h1:m+ICp2rF3jDhFgEZ/8yziagdT1C+ZpZcrJjappBCDSw=
github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/imdario/mergo v0.3.9 h1:UauaLniWCFHWd+Jp9oCEkTBj8VO/9DKg3PV3VCNMDIg=
github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA=

View file

@ -6,12 +6,12 @@ import (
"strings"
"time"
"golang.org/x/crypto/openpgp"
"github.com/go-git/go-git/v5/config"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/object"
"github.com/go-git/go-git/v5/plumbing/protocol/packp/sideband"
"github.com/go-git/go-git/v5/plumbing/transport"
"golang.org/x/crypto/openpgp"
)
// SubmoduleRescursivity defines how depth will affect any submodule recursive
@ -190,6 +190,9 @@ type PushOptions struct {
// Prune specify that remote refs that match given RefSpecs and that do
// not exist locally will be removed.
Prune bool
// Force allows the push to update a remote branch even when the local
// branch does not descend from it.
Force bool
}
// Validate validates the fields and sets the default values.
@ -375,7 +378,8 @@ type CommitOptions struct {
// All automatically stage files that have been modified and deleted, but
// new files you have not told Git about are not affected.
All bool
// Author is the author's signature of the commit.
// Author is the author's signature of the commit. If Author is empty the
// Name and Email is read from the config, and time.Now it's used as When.
Author *object.Signature
// Committer is the committer's signature of the commit. If Committer is
// nil the Author signature is used.
@ -392,7 +396,9 @@ type CommitOptions struct {
// Validate validates the fields and sets the default values.
func (o *CommitOptions) Validate(r *Repository) error {
if o.Author == nil {
return ErrMissingAuthor
if err := o.loadConfigAuthorAndCommitter(r); err != nil {
return err
}
}
if o.Committer == nil {
@ -413,6 +419,43 @@ func (o *CommitOptions) Validate(r *Repository) error {
return nil
}
func (o *CommitOptions) loadConfigAuthorAndCommitter(r *Repository) error {
cfg, err := r.ConfigScoped(config.SystemScope)
if err != nil {
return err
}
if o.Author == nil && cfg.Author.Email != "" && cfg.Author.Name != "" {
o.Author = &object.Signature{
Name: cfg.Author.Name,
Email: cfg.Author.Email,
When: time.Now(),
}
}
if o.Committer == nil && cfg.Committer.Email != "" && cfg.Committer.Name != "" {
o.Committer = &object.Signature{
Name: cfg.Committer.Name,
Email: cfg.Committer.Email,
When: time.Now(),
}
}
if o.Author == nil && cfg.User.Email != "" && cfg.User.Name != "" {
o.Author = &object.Signature{
Name: cfg.User.Name,
Email: cfg.User.Email,
When: time.Now(),
}
}
if o.Author == nil {
return ErrMissingAuthor
}
return nil
}
var (
ErrMissingName = errors.New("name field is required")
ErrMissingTagger = errors.New("tagger field is required")

View file

@ -0,0 +1,38 @@
package color
// TODO read colors from a github.com/go-git/go-git/plumbing/format/config.Config struct
// TODO implement color parsing, see https://github.com/git/git/blob/v2.26.2/color.c
// Colors. See https://github.com/git/git/blob/v2.26.2/color.h#L24-L53.
const (
Normal = ""
Reset = "\033[m"
Bold = "\033[1m"
Red = "\033[31m"
Green = "\033[32m"
Yellow = "\033[33m"
Blue = "\033[34m"
Magenta = "\033[35m"
Cyan = "\033[36m"
BoldRed = "\033[1;31m"
BoldGreen = "\033[1;32m"
BoldYellow = "\033[1;33m"
BoldBlue = "\033[1;34m"
BoldMagenta = "\033[1;35m"
BoldCyan = "\033[1;36m"
FaintRed = "\033[2;31m"
FaintGreen = "\033[2;32m"
FaintYellow = "\033[2;33m"
FaintBlue = "\033[2;34m"
FaintMagenta = "\033[2;35m"
FaintCyan = "\033[2;36m"
BgRed = "\033[41m"
BgGreen = "\033[42m"
BgYellow = "\033[43m"
BgBlue = "\033[44m"
BgMagenta = "\033[45m"
BgCyan = "\033[46m"
Faint = "\033[2m"
FaintItalic = "\033[2;3m"
Reverse = "\033[7m"
)

View file

@ -0,0 +1,97 @@
package diff
import "github.com/go-git/go-git/v5/plumbing/color"
// A ColorKey is a key into a ColorConfig map and also equal to the key in the
// diff.color subsection of the config. See
// https://github.com/git/git/blob/v2.26.2/diff.c#L83-L106.
type ColorKey string
// ColorKeys.
const (
Context ColorKey = "context"
Meta ColorKey = "meta"
Frag ColorKey = "frag"
Old ColorKey = "old"
New ColorKey = "new"
Commit ColorKey = "commit"
Whitespace ColorKey = "whitespace"
Func ColorKey = "func"
OldMoved ColorKey = "oldMoved"
OldMovedAlternative ColorKey = "oldMovedAlternative"
OldMovedDimmed ColorKey = "oldMovedDimmed"
OldMovedAlternativeDimmed ColorKey = "oldMovedAlternativeDimmed"
NewMoved ColorKey = "newMoved"
NewMovedAlternative ColorKey = "newMovedAlternative"
NewMovedDimmed ColorKey = "newMovedDimmed"
NewMovedAlternativeDimmed ColorKey = "newMovedAlternativeDimmed"
ContextDimmed ColorKey = "contextDimmed"
OldDimmed ColorKey = "oldDimmed"
NewDimmed ColorKey = "newDimmed"
ContextBold ColorKey = "contextBold"
OldBold ColorKey = "oldBold"
NewBold ColorKey = "newBold"
)
// A ColorConfig is a color configuration. A nil or empty ColorConfig
// corresponds to no color.
type ColorConfig map[ColorKey]string
// A ColorConfigOption sets an option on a ColorConfig.
type ColorConfigOption func(ColorConfig)
// WithColor sets the color for key.
func WithColor(key ColorKey, color string) ColorConfigOption {
return func(cc ColorConfig) {
cc[key] = color
}
}
// defaultColorConfig is the default color configuration. See
// https://github.com/git/git/blob/v2.26.2/diff.c#L57-L81.
var defaultColorConfig = ColorConfig{
Context: color.Normal,
Meta: color.Bold,
Frag: color.Cyan,
Old: color.Red,
New: color.Green,
Commit: color.Yellow,
Whitespace: color.BgRed,
Func: color.Normal,
OldMoved: color.BoldMagenta,
OldMovedAlternative: color.BoldBlue,
OldMovedDimmed: color.Faint,
OldMovedAlternativeDimmed: color.FaintItalic,
NewMoved: color.BoldCyan,
NewMovedAlternative: color.BoldYellow,
NewMovedDimmed: color.Faint,
NewMovedAlternativeDimmed: color.FaintItalic,
ContextDimmed: color.Faint,
OldDimmed: color.FaintRed,
NewDimmed: color.FaintGreen,
ContextBold: color.Bold,
OldBold: color.BoldRed,
NewBold: color.BoldGreen,
}
// NewColorConfig returns a new ColorConfig.
func NewColorConfig(options ...ColorConfigOption) ColorConfig {
cc := make(ColorConfig)
for key, value := range defaultColorConfig {
cc[key] = value
}
for _, option := range options {
option(cc)
}
return cc
}
// Reset returns the ANSI escape sequence to reset the color with key set from
// cc. If no color was set then no reset is needed so it returns the empty
// string.
func (cc ColorConfig) Reset(key ColorKey) string {
if cc[key] == "" {
return ""
}
return color.Reset
}

View file

@ -1,157 +1,158 @@
package diff
import (
"bytes"
"fmt"
"io"
"regexp"
"strconv"
"strings"
"github.com/go-git/go-git/v5/plumbing"
)
const (
diffInit = "diff --git a/%s b/%s\n"
// DefaultContextLines is the default number of context lines.
const DefaultContextLines = 3
chunkStart = "@@ -"
chunkMiddle = " +"
chunkEnd = " @@%s\n"
chunkCount = "%d,%d"
var (
splitLinesRegexp = regexp.MustCompile(`[^\n]*(\n|$)`)
noFilePath = "/dev/null"
aDir = "a/"
bDir = "b/"
operationChar = map[Operation]byte{
Add: '+',
Delete: '-',
Equal: ' ',
}
fPath = "--- %s\n"
tPath = "+++ %s\n"
binary = "Binary files %s and %s differ\n"
addLine = "+%s%s"
deleteLine = "-%s%s"
equalLine = " %s%s"
noNewLine = "\n\\ No newline at end of file\n"
oldMode = "old mode %o\n"
newMode = "new mode %o\n"
deletedFileMode = "deleted file mode %o\n"
newFileMode = "new file mode %o\n"
renameFrom = "from"
renameTo = "to"
renameFileMode = "rename %s %s\n"
indexAndMode = "index %s..%s %o\n"
indexNoMode = "index %s..%s\n"
DefaultContextLines = 3
operationColorKey = map[Operation]ColorKey{
Add: New,
Delete: Old,
Equal: Context,
}
)
// UnifiedEncoder encodes an unified diff into the provided Writer.
// There are some unsupported features:
// - Similarity index for renames
// - Sort hash representation
// UnifiedEncoder encodes an unified diff into the provided Writer. It does not
// support similarity index for renames or sorting hash representations.
type UnifiedEncoder struct {
io.Writer
// ctxLines is the count of unchanged lines that will appear
// surrounding a change.
ctxLines int
// contextLines is the count of unchanged lines that will appear surrounding
// a change.
contextLines int
buf bytes.Buffer
// colorConfig is the color configuration. The default is no color.
color ColorConfig
}
func NewUnifiedEncoder(w io.Writer, ctxLines int) *UnifiedEncoder {
return &UnifiedEncoder{ctxLines: ctxLines, Writer: w}
// NewUnifiedEncoder returns a new UnifiedEncoder that writes to w.
func NewUnifiedEncoder(w io.Writer, contextLines int) *UnifiedEncoder {
return &UnifiedEncoder{
Writer: w,
contextLines: contextLines,
}
}
// SetColor sets e's color configuration and returns e.
func (e *UnifiedEncoder) SetColor(colorConfig ColorConfig) *UnifiedEncoder {
e.color = colorConfig
return e
}
// Encode encodes patch.
func (e *UnifiedEncoder) Encode(patch Patch) error {
e.printMessage(patch.Message())
sb := &strings.Builder{}
if err := e.encodeFilePatch(patch.FilePatches()); err != nil {
return err
if message := patch.Message(); message != "" {
sb.WriteString(message)
if !strings.HasSuffix(message, "\n") {
sb.WriteByte('\n')
}
}
_, err := e.buf.WriteTo(e)
for _, filePatch := range patch.FilePatches() {
e.writeFilePatchHeader(sb, filePatch)
g := newHunksGenerator(filePatch.Chunks(), e.contextLines)
for _, hunk := range g.Generate() {
hunk.writeTo(sb, e.color)
}
}
_, err := e.Write([]byte(sb.String()))
return err
}
func (e *UnifiedEncoder) encodeFilePatch(filePatches []FilePatch) error {
for _, p := range filePatches {
f, t := p.Files()
if err := e.header(f, t, p.IsBinary()); err != nil {
return err
}
g := newHunksGenerator(p.Chunks(), e.ctxLines)
for _, c := range g.Generate() {
c.WriteTo(&e.buf)
}
func (e *UnifiedEncoder) writeFilePatchHeader(sb *strings.Builder, filePatch FilePatch) {
from, to := filePatch.Files()
if from == nil && to == nil {
return
}
isBinary := filePatch.IsBinary()
return nil
}
func (e *UnifiedEncoder) printMessage(message string) {
isEmpty := message == ""
hasSuffix := strings.HasSuffix(message, "\n")
if !isEmpty && !hasSuffix {
message += "\n"
}
e.buf.WriteString(message)
}
func (e *UnifiedEncoder) header(from, to File, isBinary bool) error {
var lines []string
switch {
case from == nil && to == nil:
return nil
case from != nil && to != nil:
hashEquals := from.Hash() == to.Hash()
fmt.Fprintf(&e.buf, diffInit, from.Path(), to.Path())
lines = append(lines,
fmt.Sprintf("diff --git a/%s b/%s", from.Path(), to.Path()),
)
if from.Mode() != to.Mode() {
fmt.Fprintf(&e.buf, oldMode+newMode, from.Mode(), to.Mode())
lines = append(lines,
fmt.Sprintf("old mode %o", from.Mode()),
fmt.Sprintf("new mode %o", to.Mode()),
)
}
if from.Path() != to.Path() {
fmt.Fprintf(&e.buf,
renameFileMode+renameFileMode,
renameFrom, from.Path(), renameTo, to.Path())
lines = append(lines,
fmt.Sprintf("rename from %s", from.Path()),
fmt.Sprintf("rename to %s", to.Path()),
)
}
if from.Mode() != to.Mode() && !hashEquals {
fmt.Fprintf(&e.buf, indexNoMode, from.Hash(), to.Hash())
lines = append(lines,
fmt.Sprintf("index %s..%s", from.Hash(), to.Hash()),
)
} else if !hashEquals {
fmt.Fprintf(&e.buf, indexAndMode, from.Hash(), to.Hash(), from.Mode())
lines = append(lines,
fmt.Sprintf("index %s..%s %o", from.Hash(), to.Hash(), from.Mode()),
)
}
if !hashEquals {
e.pathLines(isBinary, aDir+from.Path(), bDir+to.Path())
lines = e.appendPathLines(lines, "a/"+from.Path(), "b/"+to.Path(), isBinary)
}
case from == nil:
fmt.Fprintf(&e.buf, diffInit, to.Path(), to.Path())
fmt.Fprintf(&e.buf, newFileMode, to.Mode())
fmt.Fprintf(&e.buf, indexNoMode, plumbing.ZeroHash, to.Hash())
e.pathLines(isBinary, noFilePath, bDir+to.Path())
lines = append(lines,
fmt.Sprintf("diff --git a/%s b/%s", to.Path(), to.Path()),
fmt.Sprintf("new file mode %o", to.Mode()),
fmt.Sprintf("index %s..%s", plumbing.ZeroHash, to.Hash()),
)
lines = e.appendPathLines(lines, "/dev/null", "b/"+to.Path(), isBinary)
case to == nil:
fmt.Fprintf(&e.buf, diffInit, from.Path(), from.Path())
fmt.Fprintf(&e.buf, deletedFileMode, from.Mode())
fmt.Fprintf(&e.buf, indexNoMode, from.Hash(), plumbing.ZeroHash)
e.pathLines(isBinary, aDir+from.Path(), noFilePath)
lines = append(lines,
fmt.Sprintf("diff --git a/%s b/%s", from.Path(), from.Path()),
fmt.Sprintf("deleted file mode %o", from.Mode()),
fmt.Sprintf("index %s..%s", from.Hash(), plumbing.ZeroHash),
)
lines = e.appendPathLines(lines, "a/"+from.Path(), "/dev/null", isBinary)
}
return nil
sb.WriteString(e.color[Meta])
sb.WriteString(lines[0])
for _, line := range lines[1:] {
sb.WriteByte('\n')
sb.WriteString(line)
}
sb.WriteString(e.color.Reset(Meta))
sb.WriteByte('\n')
}
func (e *UnifiedEncoder) pathLines(isBinary bool, fromPath, toPath string) {
format := fPath + tPath
func (e *UnifiedEncoder) appendPathLines(lines []string, fromPath, toPath string, isBinary bool) []string {
if isBinary {
format = binary
return append(lines,
fmt.Sprintf("Binary files %s and %s differ", fromPath, toPath),
)
}
fmt.Fprintf(&e.buf, format, fromPath, toPath)
return append(lines,
fmt.Sprintf("--- %s", fromPath),
fmt.Sprintf("+++ %s", toPath),
)
}
type hunksGenerator struct {
@ -170,84 +171,84 @@ func newHunksGenerator(chunks []Chunk, ctxLines int) *hunksGenerator {
}
}
func (c *hunksGenerator) Generate() []*hunk {
for i, chunk := range c.chunks {
ls := splitLines(chunk.Content())
lsLen := len(ls)
func (g *hunksGenerator) Generate() []*hunk {
for i, chunk := range g.chunks {
lines := splitLines(chunk.Content())
nLines := len(lines)
switch chunk.Type() {
case Equal:
c.fromLine += lsLen
c.toLine += lsLen
c.processEqualsLines(ls, i)
g.fromLine += nLines
g.toLine += nLines
g.processEqualsLines(lines, i)
case Delete:
if lsLen != 0 {
c.fromLine++
if nLines != 0 {
g.fromLine++
}
c.processHunk(i, chunk.Type())
c.fromLine += lsLen - 1
c.current.AddOp(chunk.Type(), ls...)
g.processHunk(i, chunk.Type())
g.fromLine += nLines - 1
g.current.AddOp(chunk.Type(), lines...)
case Add:
if lsLen != 0 {
c.toLine++
if nLines != 0 {
g.toLine++
}
c.processHunk(i, chunk.Type())
c.toLine += lsLen - 1
c.current.AddOp(chunk.Type(), ls...)
g.processHunk(i, chunk.Type())
g.toLine += nLines - 1
g.current.AddOp(chunk.Type(), lines...)
}
if i == len(c.chunks)-1 && c.current != nil {
c.hunks = append(c.hunks, c.current)
if i == len(g.chunks)-1 && g.current != nil {
g.hunks = append(g.hunks, g.current)
}
}
return c.hunks
return g.hunks
}
func (c *hunksGenerator) processHunk(i int, op Operation) {
if c.current != nil {
func (g *hunksGenerator) processHunk(i int, op Operation) {
if g.current != nil {
return
}
var ctxPrefix string
linesBefore := len(c.beforeContext)
if linesBefore > c.ctxLines {
ctxPrefix = " " + c.beforeContext[linesBefore-c.ctxLines-1]
c.beforeContext = c.beforeContext[linesBefore-c.ctxLines:]
linesBefore = c.ctxLines
linesBefore := len(g.beforeContext)
if linesBefore > g.ctxLines {
ctxPrefix = g.beforeContext[linesBefore-g.ctxLines-1]
g.beforeContext = g.beforeContext[linesBefore-g.ctxLines:]
linesBefore = g.ctxLines
}
c.current = &hunk{ctxPrefix: strings.TrimSuffix(ctxPrefix, "\n")}
c.current.AddOp(Equal, c.beforeContext...)
g.current = &hunk{ctxPrefix: strings.TrimSuffix(ctxPrefix, "\n")}
g.current.AddOp(Equal, g.beforeContext...)
switch op {
case Delete:
c.current.fromLine, c.current.toLine =
c.addLineNumbers(c.fromLine, c.toLine, linesBefore, i, Add)
g.current.fromLine, g.current.toLine =
g.addLineNumbers(g.fromLine, g.toLine, linesBefore, i, Add)
case Add:
c.current.toLine, c.current.fromLine =
c.addLineNumbers(c.toLine, c.fromLine, linesBefore, i, Delete)
g.current.toLine, g.current.fromLine =
g.addLineNumbers(g.toLine, g.fromLine, linesBefore, i, Delete)
}
c.beforeContext = nil
g.beforeContext = nil
}
// addLineNumbers obtains the line numbers in a new chunk
func (c *hunksGenerator) addLineNumbers(la, lb int, linesBefore int, i int, op Operation) (cla, clb int) {
// addLineNumbers obtains the line numbers in a new chunk.
func (g *hunksGenerator) addLineNumbers(la, lb int, linesBefore int, i int, op Operation) (cla, clb int) {
cla = la - linesBefore
// we need to search for a reference for the next diff
switch {
case linesBefore != 0 && c.ctxLines != 0:
if lb > c.ctxLines {
clb = lb - c.ctxLines + 1
case linesBefore != 0 && g.ctxLines != 0:
if lb > g.ctxLines {
clb = lb - g.ctxLines + 1
} else {
clb = 1
}
case c.ctxLines == 0:
case g.ctxLines == 0:
clb = lb
case i != len(c.chunks)-1:
next := c.chunks[i+1]
case i != len(g.chunks)-1:
next := g.chunks[i+1]
if next.Type() == op || next.Type() == Equal {
// this diff will be into this chunk
clb = lb + 1
@ -257,34 +258,32 @@ func (c *hunksGenerator) addLineNumbers(la, lb int, linesBefore int, i int, op O
return
}
func (c *hunksGenerator) processEqualsLines(ls []string, i int) {
if c.current == nil {
c.beforeContext = append(c.beforeContext, ls...)
func (g *hunksGenerator) processEqualsLines(ls []string, i int) {
if g.current == nil {
g.beforeContext = append(g.beforeContext, ls...)
return
}
c.afterContext = append(c.afterContext, ls...)
if len(c.afterContext) <= c.ctxLines*2 && i != len(c.chunks)-1 {
c.current.AddOp(Equal, c.afterContext...)
c.afterContext = nil
g.afterContext = append(g.afterContext, ls...)
if len(g.afterContext) <= g.ctxLines*2 && i != len(g.chunks)-1 {
g.current.AddOp(Equal, g.afterContext...)
g.afterContext = nil
} else {
ctxLines := c.ctxLines
if ctxLines > len(c.afterContext) {
ctxLines = len(c.afterContext)
ctxLines := g.ctxLines
if ctxLines > len(g.afterContext) {
ctxLines = len(g.afterContext)
}
c.current.AddOp(Equal, c.afterContext[:ctxLines]...)
c.hunks = append(c.hunks, c.current)
g.current.AddOp(Equal, g.afterContext[:ctxLines]...)
g.hunks = append(g.hunks, g.current)
c.current = nil
c.beforeContext = c.afterContext[ctxLines:]
c.afterContext = nil
g.current = nil
g.beforeContext = g.afterContext[ctxLines:]
g.afterContext = nil
}
}
var splitLinesRE = regexp.MustCompile(`[^\n]*(\n|$)`)
func splitLines(s string) []string {
out := splitLinesRE.FindAllString(s, -1)
out := splitLinesRegexp.FindAllString(s, -1)
if out[len(out)-1] == "" {
out = out[:len(out)-1]
}
@ -302,44 +301,59 @@ type hunk struct {
ops []*op
}
func (c *hunk) WriteTo(buf *bytes.Buffer) {
buf.WriteString(chunkStart)
func (h *hunk) writeTo(sb *strings.Builder, color ColorConfig) {
sb.WriteString(color[Frag])
sb.WriteString("@@ -")
if c.fromCount == 1 {
fmt.Fprintf(buf, "%d", c.fromLine)
if h.fromCount == 1 {
sb.WriteString(strconv.Itoa(h.fromLine))
} else {
fmt.Fprintf(buf, chunkCount, c.fromLine, c.fromCount)
sb.WriteString(strconv.Itoa(h.fromLine))
sb.WriteByte(',')
sb.WriteString(strconv.Itoa(h.fromCount))
}
buf.WriteString(chunkMiddle)
sb.WriteString(" +")
if c.toCount == 1 {
fmt.Fprintf(buf, "%d", c.toLine)
if h.toCount == 1 {
sb.WriteString(strconv.Itoa(h.toLine))
} else {
fmt.Fprintf(buf, chunkCount, c.toLine, c.toCount)
sb.WriteString(strconv.Itoa(h.toLine))
sb.WriteByte(',')
sb.WriteString(strconv.Itoa(h.toCount))
}
fmt.Fprintf(buf, chunkEnd, c.ctxPrefix)
sb.WriteString(" @@")
sb.WriteString(color.Reset(Frag))
for _, d := range c.ops {
buf.WriteString(d.String())
if h.ctxPrefix != "" {
sb.WriteByte(' ')
sb.WriteString(color[Func])
sb.WriteString(h.ctxPrefix)
sb.WriteString(color.Reset(Func))
}
sb.WriteByte('\n')
for _, op := range h.ops {
op.writeTo(sb, color)
}
}
func (c *hunk) AddOp(t Operation, s ...string) {
ls := len(s)
func (h *hunk) AddOp(t Operation, ss ...string) {
n := len(ss)
switch t {
case Add:
c.toCount += ls
h.toCount += n
case Delete:
c.fromCount += ls
h.fromCount += n
case Equal:
c.toCount += ls
c.fromCount += ls
h.toCount += n
h.fromCount += n
}
for _, l := range s {
c.ops = append(c.ops, &op{l, t})
for _, s := range ss {
h.ops = append(h.ops, &op{s, t})
}
}
@ -348,20 +362,15 @@ type op struct {
t Operation
}
func (o *op) String() string {
var prefix, suffix string
switch o.t {
case Add:
prefix = addLine
case Delete:
prefix = deleteLine
case Equal:
prefix = equalLine
func (o *op) writeTo(sb *strings.Builder, color ColorConfig) {
colorKey := operationColorKey[o.t]
sb.WriteString(color[colorKey])
sb.WriteByte(operationChar[o.t])
if strings.HasSuffix(o.text, "\n") {
sb.WriteString(strings.TrimSuffix(o.text, "\n"))
} else {
sb.WriteString(o.text + "\n\\ No newline at end of file")
}
n := len(o.text)
if n > 0 && o.text[n-1] != '\n' {
suffix = noNewLine
}
return fmt.Sprintf(prefix, o.text, suffix)
sb.WriteString(color.Reset(colorKey))
sb.WriteByte('\n')
}

View file

@ -4,6 +4,7 @@ import (
"bytes"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/utils/ioutil"
)
// See https://github.com/jelmer/dulwich/blob/master/dulwich/pack.py and
@ -27,17 +28,20 @@ func GetDelta(base, target plumbing.EncodedObject) (plumbing.EncodedObject, erro
return getDelta(new(deltaIndex), base, target)
}
func getDelta(index *deltaIndex, base, target plumbing.EncodedObject) (plumbing.EncodedObject, error) {
func getDelta(index *deltaIndex, base, target plumbing.EncodedObject) (o plumbing.EncodedObject, err error) {
br, err := base.Reader()
if err != nil {
return nil, err
}
defer br.Close()
defer ioutil.CheckClose(br, &err)
tr, err := target.Reader()
if err != nil {
return nil, err
}
defer tr.Close()
defer ioutil.CheckClose(tr, &err)
bb := bufPool.Get().(*bytes.Buffer)
defer bufPool.Put(bb)

View file

@ -9,6 +9,7 @@ import (
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/storer"
"github.com/go-git/go-git/v5/utils/binary"
"github.com/go-git/go-git/v5/utils/ioutil"
)
// Encoder gets the data from the storage and write it into the writer in PACK
@ -80,7 +81,7 @@ func (e *Encoder) head(numEntries int) error {
)
}
func (e *Encoder) entry(o *ObjectToPack) error {
func (e *Encoder) entry(o *ObjectToPack) (err error) {
if o.WantWrite() {
// A cycle exists in this delta chain. This should only occur if a
// selected object representation disappeared during writing
@ -119,17 +120,22 @@ func (e *Encoder) entry(o *ObjectToPack) error {
}
e.zw.Reset(e.w)
defer ioutil.CheckClose(e.zw, &err)
or, err := o.Object.Reader()
if err != nil {
return err
}
defer ioutil.CheckClose(or, &err)
_, err = io.Copy(e.zw, or)
if err != nil {
return err
}
return e.zw.Close()
return nil
}
func (e *Encoder) writeBaseIfDelta(o *ObjectToPack) error {

View file

@ -10,6 +10,7 @@ import (
"github.com/go-git/go-git/v5/plumbing/cache"
"github.com/go-git/go-git/v5/plumbing/format/idxfile"
"github.com/go-git/go-git/v5/plumbing/storer"
"github.com/go-git/go-git/v5/utils/ioutil"
)
var (
@ -307,12 +308,14 @@ func (p *Packfile) getNextMemoryObject(h *ObjectHeader) (plumbing.EncodedObject,
return obj, nil
}
func (p *Packfile) fillRegularObjectContent(obj plumbing.EncodedObject) error {
func (p *Packfile) fillRegularObjectContent(obj plumbing.EncodedObject) (err error) {
w, err := obj.Writer()
if err != nil {
return err
}
defer ioutil.CheckClose(w, &err)
_, _, err = p.s.NextObject(w)
p.cachePut(obj)

View file

@ -4,11 +4,12 @@ import (
"bytes"
"errors"
"io"
"io/ioutil"
stdioutil "io/ioutil"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/cache"
"github.com/go-git/go-git/v5/plumbing/storer"
"github.com/go-git/go-git/v5/utils/ioutil"
)
var (
@ -283,7 +284,7 @@ func (p *Parser) resolveDeltas() error {
if !obj.IsDelta() && len(obj.Children) > 0 {
for _, child := range obj.Children {
if err := p.resolveObject(ioutil.Discard, child, content); err != nil {
if err := p.resolveObject(stdioutil.Discard, child, content); err != nil {
return err
}
}
@ -298,7 +299,7 @@ func (p *Parser) resolveDeltas() error {
return nil
}
func (p *Parser) get(o *objectInfo, buf *bytes.Buffer) error {
func (p *Parser) get(o *objectInfo, buf *bytes.Buffer) (err error) {
if !o.ExternalRef { // skip cache check for placeholder parents
b, ok := p.cache.Get(o.Offset)
if ok {
@ -310,17 +311,21 @@ func (p *Parser) get(o *objectInfo, buf *bytes.Buffer) error {
// If it's not on the cache and is not a delta we can try to find it in the
// storage, if there's one. External refs must enter here.
if p.storage != nil && !o.Type.IsDelta() {
e, err := p.storage.EncodedObject(plumbing.AnyObject, o.SHA1)
var e plumbing.EncodedObject
e, err = p.storage.EncodedObject(plumbing.AnyObject, o.SHA1)
if err != nil {
return err
}
o.Type = e.Type()
r, err := e.Reader()
var r io.ReadCloser
r, err = e.Reader()
if err != nil {
return err
}
defer ioutil.CheckClose(r, &err)
_, err = buf.ReadFrom(io.LimitReader(r, e.Size()))
return err
}

View file

@ -6,6 +6,7 @@ import (
"io"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/utils/ioutil"
)
// See https://github.com/git/git/blob/49fa3dc76179e04b0833542fa52d0f287a4955ac/delta.h
@ -16,17 +17,21 @@ import (
const deltaSizeMin = 4
// ApplyDelta writes to target the result of applying the modification deltas in delta to base.
func ApplyDelta(target, base plumbing.EncodedObject, delta []byte) error {
func ApplyDelta(target, base plumbing.EncodedObject, delta []byte) (err error) {
r, err := base.Reader()
if err != nil {
return err
}
defer ioutil.CheckClose(r, &err)
w, err := target.Writer()
if err != nil {
return err
}
defer ioutil.CheckClose(w, &err)
buf := bufPool.Get().(*bytes.Buffer)
defer bufPool.Put(buf)
buf.Reset()

View file

@ -71,3 +71,13 @@ type HashSlice []Hash
func (p HashSlice) Len() int { return len(p) }
func (p HashSlice) Less(i, j int) bool { return bytes.Compare(p[i][:], p[j][:]) < 0 }
func (p HashSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
// IsHash returns true if the given string is a valid hash.
func IsHash(s string) bool {
if len(s) != 40 {
return false
}
_, err := hex.DecodeString(s)
return err == nil
}

View file

@ -18,7 +18,7 @@ type Change struct {
To ChangeEntry
}
var empty = ChangeEntry{}
var empty ChangeEntry
// Action returns the kind of action represented by the change, an
// insertion, a deletion or a modification.
@ -27,9 +27,11 @@ func (c *Change) Action() (merkletrie.Action, error) {
return merkletrie.Action(0),
fmt.Errorf("malformed change: empty from and to")
}
if c.From == empty {
return merkletrie.Insert, nil
}
if c.To == empty {
return merkletrie.Delete, nil
}

View file

@ -78,21 +78,30 @@ func (c *Commit) Tree() (*Tree, error) {
// PatchContext returns the Patch between the actual commit and the provided one.
// Error will be return if context expires. Provided context must be non-nil.
//
// NOTE: Since version 5.1.0 the renames are correctly handled, the settings
// used are the recommended options DefaultDiffTreeOptions.
func (c *Commit) PatchContext(ctx context.Context, to *Commit) (*Patch, error) {
fromTree, err := c.Tree()
if err != nil {
return nil, err
}
toTree, err := to.Tree()
if err != nil {
return nil, err
var toTree *Tree
if to != nil {
toTree, err = to.Tree()
if err != nil {
return nil, err
}
}
return fromTree.PatchContext(ctx, toTree)
}
// Patch returns the Patch between the actual commit and the provided one.
//
// NOTE: Since version 5.1.0 the renames are correctly handled, the settings
// used are the recommended options DefaultDiffTreeOptions.
func (c *Commit) Patch(to *Commit) (*Patch, error) {
return c.PatchContext(context.Background(), to)
}

View file

@ -10,14 +10,62 @@ import (
// DiffTree compares the content and mode of the blobs found via two
// tree objects.
// DiffTree does not perform rename detection, use DiffTreeWithOptions
// instead to detect renames.
func DiffTree(a, b *Tree) (Changes, error) {
return DiffTreeContext(context.Background(), a, b)
}
// DiffTree compares the content and mode of the blobs found via two
// DiffTreeContext compares the content and mode of the blobs found via two
// tree objects. Provided context must be non-nil.
// An error will be return if context expires
// An error will be returned if context expires.
func DiffTreeContext(ctx context.Context, a, b *Tree) (Changes, error) {
return DiffTreeWithOptions(ctx, a, b, nil)
}
// DiffTreeOptions are the configurable options when performing a diff tree.
type DiffTreeOptions struct {
// DetectRenames is whether the diff tree will use rename detection.
DetectRenames bool
// RenameScore is the threshold to of similarity between files to consider
// that a pair of delete and insert are a rename. The number must be
// exactly between 0 and 100.
RenameScore uint
// RenameLimit is the maximum amount of files that can be compared when
// detecting renames. The number of comparisons that have to be performed
// is equal to the number of deleted files * the number of added files.
// That means, that if 100 files were deleted and 50 files were added, 5000
// file comparisons may be needed. So, if the rename limit is 50, the number
// of both deleted and added needs to be equal or less than 50.
// A value of 0 means no limit.
RenameLimit uint
// OnlyExactRenames performs only detection of exact renames and will not perform
// any detection of renames based on file similarity.
OnlyExactRenames bool
}
// DefaultDiffTreeOptions are the default and recommended options for the
// diff tree.
var DefaultDiffTreeOptions = &DiffTreeOptions{
DetectRenames: true,
RenameScore: 60,
RenameLimit: 0,
OnlyExactRenames: false,
}
// DiffTreeWithOptions compares the content and mode of the blobs found
// via two tree objects with the given options. The provided context
// must be non-nil.
// If no options are passed, no rename detection will be performed. The
// recommended options are DefaultDiffTreeOptions.
// An error will be returned if the context expires.
// This function will be deprecated and removed in v6 so the default
// behaviour of DiffTree is to detect renames.
func DiffTreeWithOptions(
ctx context.Context,
a, b *Tree,
opts *DiffTreeOptions,
) (Changes, error) {
from := NewTreeRootNode(a)
to := NewTreeRootNode(b)
@ -33,5 +81,18 @@ func DiffTreeContext(ctx context.Context, a, b *Tree) (Changes, error) {
return nil, err
}
return newChanges(merkletrieChanges)
changes, err := newChanges(merkletrieChanges)
if err != nil {
return nil, err
}
if opts == nil {
opts = new(DiffTreeOptions)
}
if opts.DetectRenames {
return DetectRenames(changes, opts)
}
return changes, nil
}

View file

@ -115,7 +115,7 @@ func fileContent(f *File) (content string, isBinary bool, err error) {
return
}
// textPatch is an implementation of fdiff.Patch interface
// Patch is an implementation of fdiff.Patch interface
type Patch struct {
message string
filePatches []fdiff.FilePatch

View file

@ -0,0 +1,813 @@
package object
import (
"errors"
"io"
"sort"
"strings"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/filemode"
"github.com/go-git/go-git/v5/utils/ioutil"
"github.com/go-git/go-git/v5/utils/merkletrie"
)
// DetectRenames detects the renames in the given changes on two trees with
// the given options. It will return the given changes grouping additions and
// deletions into modifications when possible.
// If options is nil, the default diff tree options will be used.
func DetectRenames(
changes Changes,
opts *DiffTreeOptions,
) (Changes, error) {
if opts == nil {
opts = DefaultDiffTreeOptions
}
detector := &renameDetector{
renameScore: int(opts.RenameScore),
renameLimit: int(opts.RenameLimit),
onlyExact: opts.OnlyExactRenames,
}
for _, c := range changes {
action, err := c.Action()
if err != nil {
return nil, err
}
switch action {
case merkletrie.Insert:
detector.added = append(detector.added, c)
case merkletrie.Delete:
detector.deleted = append(detector.deleted, c)
default:
detector.modified = append(detector.modified, c)
}
}
return detector.detect()
}
// renameDetector will detect and resolve renames in a set of changes.
// see: https://github.com/eclipse/jgit/blob/master/org.eclipse.jgit/src/org/eclipse/jgit/diff/RenameDetector.java
type renameDetector struct {
added []*Change
deleted []*Change
modified []*Change
renameScore int
renameLimit int
onlyExact bool
}
// detectExactRenames detects matches files that were deleted with files that
// were added where the hash is the same on both. If there are multiple targets
// the one with the most similar path will be chosen as the rename and the
// rest as either deletions or additions.
func (d *renameDetector) detectExactRenames() {
added := groupChangesByHash(d.added)
deletes := groupChangesByHash(d.deleted)
var uniqueAdds []*Change
var nonUniqueAdds [][]*Change
var addedLeft []*Change
for _, cs := range added {
if len(cs) == 1 {
uniqueAdds = append(uniqueAdds, cs[0])
} else {
nonUniqueAdds = append(nonUniqueAdds, cs)
}
}
for _, c := range uniqueAdds {
hash := changeHash(c)
deleted := deletes[hash]
if len(deleted) == 1 {
if sameMode(c, deleted[0]) {
d.modified = append(d.modified, &Change{From: deleted[0].From, To: c.To})
delete(deletes, hash)
} else {
addedLeft = append(addedLeft, c)
}
} else if len(deleted) > 1 {
bestMatch := bestNameMatch(c, deleted)
if bestMatch != nil && sameMode(c, bestMatch) {
d.modified = append(d.modified, &Change{From: bestMatch.From, To: c.To})
delete(deletes, hash)
var newDeletes = make([]*Change, 0, len(deleted)-1)
for _, d := range deleted {
if d != bestMatch {
newDeletes = append(newDeletes, d)
}
}
deletes[hash] = newDeletes
}
} else {
addedLeft = append(addedLeft, c)
}
}
for _, added := range nonUniqueAdds {
hash := changeHash(added[0])
deleted := deletes[hash]
if len(deleted) == 1 {
deleted := deleted[0]
bestMatch := bestNameMatch(deleted, added)
if bestMatch != nil && sameMode(deleted, bestMatch) {
d.modified = append(d.modified, &Change{From: deleted.From, To: bestMatch.To})
delete(deletes, hash)
for _, c := range added {
if c != bestMatch {
addedLeft = append(addedLeft, c)
}
}
} else {
addedLeft = append(addedLeft, added...)
}
} else if len(deleted) > 1 {
maxSize := len(deleted) * len(added)
if d.renameLimit > 0 && d.renameLimit < maxSize {
maxSize = d.renameLimit
}
matrix := make(similarityMatrix, 0, maxSize)
for delIdx, del := range deleted {
deletedName := changeName(del)
for addIdx, add := range added {
addedName := changeName(add)
score := nameSimilarityScore(addedName, deletedName)
matrix = append(matrix, similarityPair{added: addIdx, deleted: delIdx, score: score})
if len(matrix) >= maxSize {
break
}
}
if len(matrix) >= maxSize {
break
}
}
sort.Stable(matrix)
usedAdds := make(map[*Change]struct{})
usedDeletes := make(map[*Change]struct{})
for i := len(matrix) - 1; i >= 0; i-- {
del := deleted[matrix[i].deleted]
add := added[matrix[i].added]
if add == nil || del == nil {
// it was already matched
continue
}
usedAdds[add] = struct{}{}
usedDeletes[del] = struct{}{}
d.modified = append(d.modified, &Change{From: del.From, To: add.To})
added[matrix[i].added] = nil
deleted[matrix[i].deleted] = nil
}
for _, c := range added {
if _, ok := usedAdds[c]; !ok && c != nil {
addedLeft = append(addedLeft, c)
}
}
var newDeletes = make([]*Change, 0, len(deleted)-len(usedDeletes))
for _, c := range deleted {
if _, ok := usedDeletes[c]; !ok && c != nil {
newDeletes = append(newDeletes, c)
}
}
deletes[hash] = newDeletes
} else {
addedLeft = append(addedLeft, added...)
}
}
d.added = addedLeft
d.deleted = nil
for _, dels := range deletes {
d.deleted = append(d.deleted, dels...)
}
}
// detectContentRenames detects renames based on the similarity of the content
// in the files by building a matrix of pairs between sources and destinations
// and matching by the highest score.
// see: https://github.com/eclipse/jgit/blob/master/org.eclipse.jgit/src/org/eclipse/jgit/diff/SimilarityRenameDetector.java
func (d *renameDetector) detectContentRenames() error {
cnt := max(len(d.added), len(d.deleted))
if d.renameLimit > 0 && cnt > d.renameLimit {
return nil
}
srcs, dsts := d.deleted, d.added
matrix, err := buildSimilarityMatrix(srcs, dsts, d.renameScore)
if err != nil {
return err
}
renames := make([]*Change, 0, min(len(matrix), len(dsts)))
// Match rename pairs on a first come, first serve basis until
// we have looked at everything that is above the minimum score.
for i := len(matrix) - 1; i >= 0; i-- {
pair := matrix[i]
src := srcs[pair.deleted]
dst := dsts[pair.added]
if dst == nil || src == nil {
// It was already matched before
continue
}
renames = append(renames, &Change{From: src.From, To: dst.To})
// Claim destination and source as matched
dsts[pair.added] = nil
srcs[pair.deleted] = nil
}
d.modified = append(d.modified, renames...)
d.added = compactChanges(dsts)
d.deleted = compactChanges(srcs)
return nil
}
func (d *renameDetector) detect() (Changes, error) {
if len(d.added) > 0 && len(d.deleted) > 0 {
d.detectExactRenames()
if !d.onlyExact {
if err := d.detectContentRenames(); err != nil {
return nil, err
}
}
}
result := make(Changes, 0, len(d.added)+len(d.deleted)+len(d.modified))
result = append(result, d.added...)
result = append(result, d.deleted...)
result = append(result, d.modified...)
sort.Stable(result)
return result, nil
}
func bestNameMatch(change *Change, changes []*Change) *Change {
var best *Change
var bestScore int
cname := changeName(change)
for _, c := range changes {
score := nameSimilarityScore(cname, changeName(c))
if score > bestScore {
bestScore = score
best = c
}
}
return best
}
func nameSimilarityScore(a, b string) int {
aDirLen := strings.LastIndexByte(a, '/') + 1
bDirLen := strings.LastIndexByte(b, '/') + 1
dirMin := min(aDirLen, bDirLen)
dirMax := max(aDirLen, bDirLen)
var dirScoreLtr, dirScoreRtl int
if dirMax == 0 {
dirScoreLtr = 100
dirScoreRtl = 100
} else {
var dirSim int
for ; dirSim < dirMin; dirSim++ {
if a[dirSim] != b[dirSim] {
break
}
}
dirScoreLtr = dirSim * 100 / dirMax
if dirScoreLtr == 100 {
dirScoreRtl = 100
} else {
for dirSim = 0; dirSim < dirMin; dirSim++ {
if a[aDirLen-1-dirSim] != b[bDirLen-1-dirSim] {
break
}
}
dirScoreRtl = dirSim * 100 / dirMax
}
}
fileMin := min(len(a)-aDirLen, len(b)-bDirLen)
fileMax := max(len(a)-aDirLen, len(b)-bDirLen)
fileSim := 0
for ; fileSim < fileMin; fileSim++ {
if a[len(a)-1-fileSim] != b[len(b)-1-fileSim] {
break
}
}
fileScore := fileSim * 100 / fileMax
return (((dirScoreLtr + dirScoreRtl) * 25) + (fileScore * 50)) / 100
}
func changeName(c *Change) string {
if c.To != empty {
return c.To.Name
}
return c.From.Name
}
func changeHash(c *Change) plumbing.Hash {
if c.To != empty {
return c.To.TreeEntry.Hash
}
return c.From.TreeEntry.Hash
}
func changeMode(c *Change) filemode.FileMode {
if c.To != empty {
return c.To.TreeEntry.Mode
}
return c.From.TreeEntry.Mode
}
func sameMode(a, b *Change) bool {
return changeMode(a) == changeMode(b)
}
func groupChangesByHash(changes []*Change) map[plumbing.Hash][]*Change {
var result = make(map[plumbing.Hash][]*Change)
for _, c := range changes {
hash := changeHash(c)
result[hash] = append(result[hash], c)
}
return result
}
type similarityMatrix []similarityPair
func (m similarityMatrix) Len() int { return len(m) }
func (m similarityMatrix) Swap(i, j int) { m[i], m[j] = m[j], m[i] }
func (m similarityMatrix) Less(i, j int) bool {
if m[i].score == m[j].score {
if m[i].added == m[j].added {
return m[i].deleted < m[j].deleted
}
return m[i].added < m[j].added
}
return m[i].score < m[j].score
}
type similarityPair struct {
// index of the added file
added int
// index of the deleted file
deleted int
// similarity score
score int
}
func max(a, b int) int {
if a > b {
return a
}
return b
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
func buildSimilarityMatrix(srcs, dsts []*Change, renameScore int) (similarityMatrix, error) {
// Allocate for the worst-case scenario where every pair has a score
// that we need to consider. We might not need that many.
matrix := make(similarityMatrix, 0, len(srcs)*len(dsts))
srcSizes := make([]int64, len(srcs))
dstSizes := make([]int64, len(dsts))
dstTooLarge := make(map[int]bool)
// Consider each pair of files, if the score is above the minimum
// threshold we need to record that scoring in the matrix so we can
// later find the best matches.
outerLoop:
for srcIdx, src := range srcs {
if changeMode(src) != filemode.Regular {
continue
}
// Declare the from file and the similarity index here to be able to
// reuse it inside the inner loop. The reason to not initialize them
// here is so we can skip the initialization in case they happen to
// not be needed later. They will be initialized inside the inner
// loop if and only if they're needed and reused in subsequent passes.
var from *File
var s *similarityIndex
var err error
for dstIdx, dst := range dsts {
if changeMode(dst) != filemode.Regular {
continue
}
if dstTooLarge[dstIdx] {
continue
}
var to *File
srcSize := srcSizes[srcIdx]
if srcSize == 0 {
from, _, err = src.Files()
if err != nil {
return nil, err
}
srcSize = from.Size + 1
srcSizes[srcIdx] = srcSize
}
dstSize := dstSizes[dstIdx]
if dstSize == 0 {
_, to, err = dst.Files()
if err != nil {
return nil, err
}
dstSize = to.Size + 1
dstSizes[dstIdx] = dstSize
}
min, max := srcSize, dstSize
if dstSize < srcSize {
min = dstSize
max = srcSize
}
if int(min*100/max) < renameScore {
// File sizes are too different to be a match
continue
}
if s == nil {
s, err = fileSimilarityIndex(from)
if err != nil {
if err == errIndexFull {
continue outerLoop
}
return nil, err
}
}
if to == nil {
_, to, err = dst.Files()
if err != nil {
return nil, err
}
}
di, err := fileSimilarityIndex(to)
if err != nil {
if err == errIndexFull {
dstTooLarge[dstIdx] = true
}
return nil, err
}
contentScore := s.score(di, 10000)
// The name score returns a value between 0 and 100, so we need to
// convert it to the same range as the content score.
nameScore := nameSimilarityScore(src.From.Name, dst.To.Name) * 100
score := (contentScore*99 + nameScore*1) / 10000
if score < renameScore {
continue
}
matrix = append(matrix, similarityPair{added: dstIdx, deleted: srcIdx, score: score})
}
}
sort.Stable(matrix)
return matrix, nil
}
func compactChanges(changes []*Change) []*Change {
var result []*Change
for _, c := range changes {
if c != nil {
result = append(result, c)
}
}
return result
}
const (
keyShift = 32
maxCountValue = (1 << keyShift) - 1
)
var errIndexFull = errors.New("index is full")
// similarityIndex is an index structure of lines/blocks in one file.
// This structure can be used to compute an approximation of the similarity
// between two files.
// To save space in memory, this index uses a space efficient encoding which
// will not exceed 1MiB per instance. The index starts out at a smaller size
// (closer to 2KiB), but may grow as more distinct blocks withing the scanned
// file are discovered.
// see: https://github.com/eclipse/jgit/blob/master/org.eclipse.jgit/src/org/eclipse/jgit/diff/SimilarityIndex.java
type similarityIndex struct {
hashed uint64
// number of non-zero entries in hashes
numHashes int
growAt int
hashes []keyCountPair
hashBits int
}
func fileSimilarityIndex(f *File) (*similarityIndex, error) {
idx := newSimilarityIndex()
if err := idx.hash(f); err != nil {
return nil, err
}
sort.Stable(keyCountPairs(idx.hashes))
return idx, nil
}
func newSimilarityIndex() *similarityIndex {
return &similarityIndex{
hashBits: 8,
hashes: make([]keyCountPair, 1<<8),
growAt: shouldGrowAt(8),
}
}
func (i *similarityIndex) hash(f *File) error {
isBin, err := f.IsBinary()
if err != nil {
return err
}
r, err := f.Reader()
if err != nil {
return err
}
defer ioutil.CheckClose(r, &err)
return i.hashContent(r, f.Size, isBin)
}
func (i *similarityIndex) hashContent(r io.Reader, size int64, isBin bool) error {
var buf = make([]byte, 4096)
var ptr, cnt int
remaining := size
for 0 < remaining {
hash := 5381
var blockHashedCnt uint64
// Hash one line or block, whatever happens first
n := int64(0)
for {
if ptr == cnt {
ptr = 0
var err error
cnt, err = io.ReadFull(r, buf)
if err != nil && err != io.ErrUnexpectedEOF {
return err
}
if cnt == 0 {
return io.EOF
}
}
n++
c := buf[ptr] & 0xff
ptr++
// Ignore CR in CRLF sequence if it's text
if !isBin && c == '\r' && ptr < cnt && buf[ptr] == '\n' {
continue
}
blockHashedCnt++
if c == '\n' {
break
}
hash = (hash << 5) + hash + int(c)
if n >= 64 || n >= remaining {
break
}
}
i.hashed += blockHashedCnt
if err := i.add(hash, blockHashedCnt); err != nil {
return err
}
remaining -= n
}
return nil
}
// score computes the similarity score between this index and another one.
// A region of a file is defined as a line in a text file or a fixed-size
// block in a binary file. To prepare an index, each region in the file is
// hashed; the values and counts of hashes are retained in a sorted table.
// Define the similarity fraction F as the count of matching regions between
// the two files divided between the maximum count of regions in either file.
// The similarity score is F multiplied by the maxScore constant, yielding a
// range [0, maxScore]. It is defined as maxScore for the degenerate case of
// two empty files.
// The similarity score is symmetrical; i.e. a.score(b) == b.score(a).
func (i *similarityIndex) score(other *similarityIndex, maxScore int) int {
var maxHashed = i.hashed
if maxHashed < other.hashed {
maxHashed = other.hashed
}
if maxHashed == 0 {
return maxScore
}
return int(i.common(other) * uint64(maxScore) / maxHashed)
}
func (i *similarityIndex) common(dst *similarityIndex) uint64 {
srcIdx, dstIdx := 0, 0
if i.numHashes == 0 || dst.numHashes == 0 {
return 0
}
var common uint64
srcKey, dstKey := i.hashes[srcIdx].key(), dst.hashes[dstIdx].key()
for {
if srcKey == dstKey {
srcCnt, dstCnt := i.hashes[srcIdx].count(), dst.hashes[dstIdx].count()
if srcCnt < dstCnt {
common += srcCnt
} else {
common += dstCnt
}
srcIdx++
if srcIdx == len(i.hashes) {
break
}
srcKey = i.hashes[srcIdx].key()
dstIdx++
if dstIdx == len(dst.hashes) {
break
}
dstKey = dst.hashes[dstIdx].key()
} else if srcKey < dstKey {
// Region of src that is not in dst
srcIdx++
if srcIdx == len(i.hashes) {
break
}
srcKey = i.hashes[srcIdx].key()
} else {
// Region of dst that is not in src
dstIdx++
if dstIdx == len(dst.hashes) {
break
}
dstKey = dst.hashes[dstIdx].key()
}
}
return common
}
func (i *similarityIndex) add(key int, cnt uint64) error {
key = int(uint32(key)*0x9e370001 >> 1)
j := i.slot(key)
for {
v := i.hashes[j]
if v == 0 {
// It's an empty slot, so we can store it here.
if i.growAt <= i.numHashes {
if err := i.grow(); err != nil {
return err
}
j = i.slot(key)
continue
}
var err error
i.hashes[j], err = newKeyCountPair(key, cnt)
if err != nil {
return err
}
i.numHashes++
return nil
} else if v.key() == key {
// It's the same key, so increment the counter.
var err error
i.hashes[j], err = newKeyCountPair(key, v.count()+cnt)
if err != nil {
return err
}
return nil
} else if j+1 >= len(i.hashes) {
j = 0
} else {
j++
}
}
}
type keyCountPair uint64
func newKeyCountPair(key int, cnt uint64) (keyCountPair, error) {
if cnt > maxCountValue {
return 0, errIndexFull
}
return keyCountPair((uint64(key) << keyShift) | cnt), nil
}
func (p keyCountPair) key() int {
return int(p >> keyShift)
}
func (p keyCountPair) count() uint64 {
return uint64(p) & maxCountValue
}
func (i *similarityIndex) slot(key int) int {
// We use 31 - hashBits because the upper bit was already forced
// to be 0 and we want the remaining high bits to be used as the
// table slot.
return int(uint32(key) >> uint(31 - i.hashBits))
}
func shouldGrowAt(hashBits int) int {
return (1 << uint(hashBits)) * (hashBits - 3) / hashBits
}
func (i *similarityIndex) grow() error {
if i.hashBits == 30 {
return errIndexFull
}
old := i.hashes
i.hashBits++
i.growAt = shouldGrowAt(i.hashBits)
// TODO(erizocosmico): find a way to check if it will OOM and return
// errIndexFull instead.
i.hashes = make([]keyCountPair, 1<<uint(i.hashBits))
for _, v := range old {
if v != 0 {
j := i.slot(v.key())
for i.hashes[j] != 0 {
j++
if j >= len(i.hashes) {
j = 0
}
}
i.hashes[j] = v
}
}
return nil
}
type keyCountPairs []keyCountPair
func (p keyCountPairs) Len() int { return len(p) }
func (p keyCountPairs) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
func (p keyCountPairs) Less(i, j int) bool { return p[i] < p[j] }

View file

@ -304,29 +304,34 @@ func (t *Tree) buildMap() {
}
// Diff returns a list of changes between this tree and the provided one
func (from *Tree) Diff(to *Tree) (Changes, error) {
return DiffTree(from, to)
func (t *Tree) Diff(to *Tree) (Changes, error) {
return t.DiffContext(context.Background(), to)
}
// Diff returns a list of changes between this tree and the provided one
// Error will be returned if context expires
// Provided context must be non nil
func (from *Tree) DiffContext(ctx context.Context, to *Tree) (Changes, error) {
return DiffTreeContext(ctx, from, to)
// DiffContext returns a list of changes between this tree and the provided one
// Error will be returned if context expires. Provided context must be non nil.
//
// NOTE: Since version 5.1.0 the renames are correctly handled, the settings
// used are the recommended options DefaultDiffTreeOptions.
func (t *Tree) DiffContext(ctx context.Context, to *Tree) (Changes, error) {
return DiffTreeWithOptions(ctx, t, to, DefaultDiffTreeOptions)
}
// Patch returns a slice of Patch objects with all the changes between trees
// in chunks. This representation can be used to create several diff outputs.
func (from *Tree) Patch(to *Tree) (*Patch, error) {
return from.PatchContext(context.Background(), to)
func (t *Tree) Patch(to *Tree) (*Patch, error) {
return t.PatchContext(context.Background(), to)
}
// Patch returns a slice of Patch objects with all the changes between trees
// in chunks. This representation can be used to create several diff outputs.
// If context expires, an error will be returned
// Provided context must be non-nil
func (from *Tree) PatchContext(ctx context.Context, to *Tree) (*Patch, error) {
changes, err := DiffTreeContext(ctx, from, to)
// PatchContext returns a slice of Patch objects with all the changes between
// trees in chunks. This representation can be used to create several diff
// outputs. If context expires, an error will be returned. Provided context must
// be non-nil.
//
// NOTE: Since version 5.1.0 the renames are correctly handled, the settings
// used are the recommended options DefaultDiffTreeOptions.
func (t *Tree) PatchContext(ctx context.Context, to *Tree) (*Patch, error) {
changes, err := t.DiffContext(ctx, to)
if err != nil {
return nil, err
}

View file

@ -201,3 +201,11 @@ func (a *AdvRefs) addSymbolicRefs(s storer.ReferenceStorer) error {
func (a *AdvRefs) supportSymrefs() bool {
return a.Capabilities.Supports(capability.SymRef)
}
// IsEmpty returns true if doesn't contain any reference.
func (a *AdvRefs) IsEmpty() bool {
return a.Head == nil &&
len(a.References) == 0 &&
len(a.Peeled) == 0 &&
len(a.Shallows) == 0
}

View file

@ -175,6 +175,13 @@ func (s *session) AdvertisedReferences() (*packp.AdvRefs, error) {
}
}
// Some servers like jGit, announce capabilities instead of returning an
// packp message with a flush. This verifies that we received a empty
// adv-refs, even it contains capabilities.
if !s.isReceivePack && ar.IsEmpty() {
return nil, transport.ErrEmptyRemoteRepository
}
transport.FilterUnsupportedCapabilities(ar.Capabilities)
s.advRefs = ar
return ar, nil

View file

@ -243,11 +243,13 @@ func (s *rpSession) ReceivePack(ctx context.Context, req *packp.ReferenceUpdateR
//TODO: Implement 'atomic' update of references.
r := ioutil.NewContextReadCloser(ctx, req.Packfile)
if err := s.writePackfile(r); err != nil {
s.unpackErr = err
s.firstErr = err
return s.reportStatus(), err
if req.Packfile != nil {
r := ioutil.NewContextReadCloser(ctx, req.Packfile)
if err := s.writePackfile(r); err != nil {
s.unpackErr = err
s.firstErr = err
return s.reportStatus(), err
}
}
s.updateReferences(req)

View file

@ -29,6 +29,7 @@ var (
NoErrAlreadyUpToDate = errors.New("already up-to-date")
ErrDeleteRefNotSupported = errors.New("server does not support delete-refs")
ErrForceNeeded = errors.New("some refs were not updated")
ErrExactSHA1NotSupported = errors.New("server does not support exact SHA1 refspec")
)
const (
@ -122,6 +123,15 @@ func (r *Remote) PushContext(ctx context.Context, o *PushOptions) (err error) {
return ErrDeleteRefNotSupported
}
if o.Force {
for i := 0; i < len(o.RefSpecs); i++ {
rs := &o.RefSpecs[i]
if !rs.IsForceUpdate() {
o.RefSpecs[i] = config.RefSpec("+" + rs.String())
}
}
}
localRefs, err := r.references()
if err != nil {
return err
@ -303,6 +313,10 @@ func (r *Remote) fetch(ctx context.Context, o *FetchOptions) (sto storer.Referen
return nil, err
}
if err := r.isSupportedRefSpec(o.RefSpecs, ar); err != nil {
return nil, err
}
remoteRefs, err := ar.AllReferences()
if err != nil {
return nil, err
@ -546,6 +560,7 @@ func (r *Remote) addReferenceIfRefSpecMatches(rs config.RefSpec,
func (r *Remote) references() ([]*plumbing.Reference, error) {
var localRefs []*plumbing.Reference
iter, err := r.s.IterReferences()
if err != nil {
return nil, err
@ -701,6 +716,11 @@ func doCalculateRefs(
return err
}
if s.IsExactSHA1() {
ref := plumbing.NewHashReference(s.Dst(""), plumbing.NewHash(s.Src()))
return refs.SetReference(ref)
}
var matched bool
err = iter.ForEach(func(ref *plumbing.Reference) error {
if !s.Match(ref.Name()) {
@ -850,6 +870,26 @@ func (r *Remote) newUploadPackRequest(o *FetchOptions,
return req, nil
}
func (r *Remote) isSupportedRefSpec(refs []config.RefSpec, ar *packp.AdvRefs) error {
var containsIsExact bool
for _, ref := range refs {
if ref.IsExactSHA1() {
containsIsExact = true
}
}
if !containsIsExact {
return nil
}
if ar.Capabilities.Supports(capability.AllowReachableSHA1InWant) ||
ar.Capabilities.Supports(capability.AllowTipSHA1InWant) {
return nil
}
return ErrExactSHA1NotSupported
}
func buildSidebandIfSupported(l *capability.List, reader io.Reader, p sideband.Progress) io.Reader {
var t sideband.Type
@ -883,7 +923,7 @@ func (r *Remote) updateLocalReferenceStorage(
}
for _, ref := range fetchedRefs {
if !spec.Match(ref.Name()) {
if !spec.Match(ref.Name()) && !spec.IsExactSHA1() {
continue
}

View file

@ -13,7 +13,6 @@ import (
"strings"
"time"
"golang.org/x/crypto/openpgp"
"github.com/go-git/go-git/v5/config"
"github.com/go-git/go-git/v5/internal/revision"
"github.com/go-git/go-git/v5/plumbing"
@ -24,6 +23,8 @@ import (
"github.com/go-git/go-git/v5/storage"
"github.com/go-git/go-git/v5/storage/filesystem"
"github.com/go-git/go-git/v5/utils/ioutil"
"github.com/imdario/mergo"
"golang.org/x/crypto/openpgp"
"github.com/go-git/go-billy/v5"
"github.com/go-git/go-billy/v5/osfs"
@ -155,7 +156,7 @@ func setConfigWorktree(r *Repository, worktree, storage billy.Filesystem) error
return nil
}
cfg, err := r.Storer.Config()
cfg, err := r.Config()
if err != nil {
return err
}
@ -434,14 +435,56 @@ func cleanUpDir(path string, all bool) error {
return err
}
// Config return the repository config
// Config return the repository config. In a filesystem backed repository this
// means read the `.git/config`.
func (r *Repository) Config() (*config.Config, error) {
return r.Storer.Config()
}
// SetConfig marshall and writes the repository config. In a filesystem backed
// repository this means write the `.git/config`. This function should be called
// with the result of `Repository.Config` and never with the output of
// `Repository.ConfigScoped`.
func (r *Repository) SetConfig(cfg *config.Config) error {
return r.Storer.SetConfig(cfg)
}
// ConfigScoped returns the repository config, merged with requested scope and
// lower. For example if, config.GlobalScope is given the local and global config
// are returned merged in one config value.
func (r *Repository) ConfigScoped(scope config.Scope) (*config.Config, error) {
// TODO(mcuadros): v6, add this as ConfigOptions.Scoped
var err error
system := config.NewConfig()
if scope >= config.SystemScope {
system, err = config.LoadConfig(config.SystemScope)
if err != nil {
return nil, err
}
}
global := config.NewConfig()
if scope >= config.GlobalScope {
global, err = config.LoadConfig(config.GlobalScope)
if err != nil {
return nil, err
}
}
local, err := r.Storer.Config()
if err != nil {
return nil, err
}
_ = mergo.Merge(global, system)
_ = mergo.Merge(local, global)
return local, nil
}
// Remote return a remote if exists
func (r *Repository) Remote(name string) (*Remote, error) {
cfg, err := r.Storer.Config()
cfg, err := r.Config()
if err != nil {
return nil, err
}
@ -456,7 +499,7 @@ func (r *Repository) Remote(name string) (*Remote, error) {
// Remotes returns a list with all the remotes
func (r *Repository) Remotes() ([]*Remote, error) {
cfg, err := r.Storer.Config()
cfg, err := r.Config()
if err != nil {
return nil, err
}
@ -480,7 +523,7 @@ func (r *Repository) CreateRemote(c *config.RemoteConfig) (*Remote, error) {
remote := NewRemote(r.Storer, c)
cfg, err := r.Storer.Config()
cfg, err := r.Config()
if err != nil {
return nil, err
}
@ -511,7 +554,7 @@ func (r *Repository) CreateRemoteAnonymous(c *config.RemoteConfig) (*Remote, err
// DeleteRemote delete a remote from the repository and delete the config
func (r *Repository) DeleteRemote(name string) error {
cfg, err := r.Storer.Config()
cfg, err := r.Config()
if err != nil {
return err
}
@ -526,7 +569,7 @@ func (r *Repository) DeleteRemote(name string) error {
// Branch return a Branch if exists
func (r *Repository) Branch(name string) (*config.Branch, error) {
cfg, err := r.Storer.Config()
cfg, err := r.Config()
if err != nil {
return nil, err
}
@ -545,7 +588,7 @@ func (r *Repository) CreateBranch(c *config.Branch) error {
return err
}
cfg, err := r.Storer.Config()
cfg, err := r.Config()
if err != nil {
return err
}
@ -560,7 +603,7 @@ func (r *Repository) CreateBranch(c *config.Branch) error {
// DeleteBranch delete a Branch from the repository and delete the config
func (r *Repository) DeleteBranch(name string) error {
cfg, err := r.Storer.Config()
cfg, err := r.Config()
if err != nil {
return err
}
@ -835,7 +878,7 @@ func (r *Repository) cloneRefSpec(o *CloneOptions) []config.RefSpec {
}
func (r *Repository) setIsBare(isBare bool) error {
cfg, err := r.Storer.Config()
cfg, err := r.Config()
if err != nil {
return err
}
@ -851,7 +894,7 @@ func (r *Repository) updateRemoteConfigIfNeeded(o *CloneOptions, c *config.Remot
c.Fetch = r.cloneRefSpec(o)
cfg, err := r.Storer.Config()
cfg, err := r.Config()
if err != nil {
return err
}
@ -1541,7 +1584,7 @@ func (r *Repository) createNewObjectPack(cfg *RepackConfig) (h plumbing.Hash, er
return h, err
}
defer ioutil.CheckClose(wc, &err)
scfg, err := r.Storer.Config()
scfg, err := r.Config()
if err != nil {
return h, err
}

View file

@ -1,7 +1,6 @@
package filesystem
import (
stdioutil "io/ioutil"
"os"
"github.com/go-git/go-git/v5/config"
@ -14,29 +13,17 @@ type ConfigStorage struct {
}
func (c *ConfigStorage) Config() (conf *config.Config, err error) {
cfg := config.NewConfig()
f, err := c.dir.Config()
if err != nil {
if os.IsNotExist(err) {
return cfg, nil
return config.NewConfig(), nil
}
return nil, err
}
defer ioutil.CheckClose(f, &err)
b, err := stdioutil.ReadAll(f)
if err != nil {
return nil, err
}
if err = cfg.Unmarshal(b); err != nil {
return nil, err
}
return cfg, err
return config.ReadConfig(f)
}
func (c *ConfigStorage) SetConfig(cfg *config.Config) (err error) {

View file

@ -57,6 +57,9 @@ var (
// targeting a non-existing object. This usually means the repository
// is corrupt.
ErrSymRefTargetNotFound = errors.New("symbolic reference target not found")
// ErrIsDir is returned when a reference file is attempting to be read,
// but the path specified is a directory.
ErrIsDir = errors.New("reference path is a directory")
)
// Options holds configuration for the storage.
@ -926,6 +929,14 @@ func (d *DotGit) addRefFromHEAD(refs *[]*plumbing.Reference) error {
func (d *DotGit) readReferenceFile(path, name string) (ref *plumbing.Reference, err error) {
path = d.fs.Join(path, d.fs.Join(strings.Split(name, "/")...))
st, err := d.fs.Stat(path)
if err != nil {
return nil, err
}
if st.IsDir() {
return nil, ErrIsDir
}
f, err := d.fs.Open(path)
if err != nil {
return nil, err

View file

@ -408,6 +408,8 @@ func (s *ObjectStorage) getFromUnpacked(h plumbing.Hash) (obj plumbing.EncodedOb
return nil, err
}
defer ioutil.CheckClose(w, &err)
s.objectCache.Put(obj)
_, err = io.Copy(w, r)

View file

@ -35,7 +35,7 @@ func (s *Submodule) Config() *config.Submodule {
// Init initialize the submodule reading the recorded Entry in the index for
// the given submodule
func (s *Submodule) Init() error {
cfg, err := s.w.r.Storer.Config()
cfg, err := s.w.r.Config()
if err != nil {
return err
}

View file

@ -23,7 +23,7 @@ package merkletrie
// # Cases
//
// When comparing noders in both trees you will found yourself in
// When comparing noders in both trees you will find yourself in
// one of 169 possible cases, but if we ignore moves, we can
// simplify a lot the search space into the following table:
//
@ -256,17 +256,21 @@ import (
)
var (
// ErrCanceled is returned whenever the operation is canceled.
ErrCanceled = errors.New("operation canceled")
)
// DiffTree calculates the list of changes between two merkletries. It
// uses the provided hashEqual callback to compare noders.
func DiffTree(fromTree, toTree noder.Noder,
hashEqual noder.Equal) (Changes, error) {
func DiffTree(
fromTree,
toTree noder.Noder,
hashEqual noder.Equal,
) (Changes, error) {
return DiffTreeContext(context.Background(), fromTree, toTree, hashEqual)
}
// DiffTree calculates the list of changes between two merkletries. It
// DiffTreeContext calculates the list of changes between two merkletries. It
// uses the provided hashEqual callback to compare noders.
// Error will be returned if context expires
// Provided context must be non nil