mirror of https://github.com/go-gitea/gitea
Update to go-git v5.1.0 (#11936)
Signed-off-by: Andrew Thornton <art27@cantab.net> Co-authored-by: techknowlogick <techknowlogick@gitea.io>pull/11932/head^2
parent
6bf78d2b57
commit
1426126690
@ -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" |
||||
) |
@ -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 |
||||
} |
@ -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] } |
7
vendor/github.com/go-git/go-git/v5/plumbing/transport/internal/common/common.go
generated
vendored
7
vendor/github.com/go-git/go-git/v5/plumbing/transport/internal/common/common.go
generated
vendored
@ -0,0 +1,12 @@ |
||||
version = 1 |
||||
|
||||
test_patterns = [ |
||||
"*_test.go" |
||||
] |
||||
|
||||
[[analyzers]] |
||||
name = "go" |
||||
enabled = true |
||||
|
||||
[analyzers.meta] |
||||
import_path = "github.com/imdario/mergo" |
@ -0,0 +1,33 @@ |
||||
#### joe made this: http://goel.io/joe |
||||
|
||||
#### go #### |
||||
# Binaries for programs and plugins |
||||
*.exe |
||||
*.dll |
||||
*.so |
||||
*.dylib |
||||
|
||||
# Test binary, build with `go test -c` |
||||
*.test |
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE |
||||
*.out |
||||
|
||||
# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 |
||||
.glide/ |
||||
|
||||
#### vim #### |
||||
# Swap |
||||
[._]*.s[a-v][a-z] |
||||
[._]*.sw[a-p] |
||||
[._]s[a-v][a-z] |
||||
[._]sw[a-p] |
||||
|
||||
# Session |
||||
Session.vim |
||||
|
||||
# Temporary |
||||
.netrwhist |
||||
*~ |
||||
# Auto-generated tag files |
||||
tags |
@ -0,0 +1,9 @@ |
||||
language: go |
||||
install: |
||||
- go get -t |
||||
- go get golang.org/x/tools/cmd/cover |
||||
- go get github.com/mattn/goveralls |
||||
script: |
||||
- go test -race -v ./... |
||||
after_script: |
||||
- $HOME/gopath/bin/goveralls -service=travis-ci -repotoken $COVERALLS_TOKEN |
@ -0,0 +1,46 @@ |
||||
# Contributor Covenant Code of Conduct |
||||
|
||||
## Our Pledge |
||||
|
||||
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. |
||||
|
||||
## Our Standards |
||||
|
||||
Examples of behavior that contributes to creating a positive environment include: |
||||
|
||||
* Using welcoming and inclusive language |
||||
* Being respectful of differing viewpoints and experiences |
||||
* Gracefully accepting constructive criticism |
||||
* Focusing on what is best for the community |
||||
* Showing empathy towards other community members |
||||
|
||||
Examples of unacceptable behavior by participants include: |
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or advances |
||||
* Trolling, insulting/derogatory comments, and personal or political attacks |
||||
* Public or private harassment |
||||
* Publishing others' private information, such as a physical or electronic address, without explicit permission |
||||
* Other conduct which could reasonably be considered inappropriate in a professional setting |
||||
|
||||
## Our Responsibilities |
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. |
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. |
||||
|
||||
## Scope |
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. |
||||
|
||||
## Enforcement |
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at i@dario.im. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. |
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. |
||||
|
||||
## Attribution |
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] |
||||
|
||||
[homepage]: http://contributor-covenant.org |
||||
[version]: http://contributor-covenant.org/version/1/4/ |
@ -0,0 +1,28 @@ |
||||
Copyright (c) 2013 Dario Castañé. All rights reserved. |
||||
Copyright (c) 2012 The Go Authors. All rights reserved. |
||||
|
||||
Redistribution and use in source and binary forms, with or without |
||||
modification, are permitted provided that the following conditions are |
||||
met: |
||||
|
||||
* Redistributions of source code must retain the above copyright |
||||
notice, this list of conditions and the following disclaimer. |
||||
* Redistributions in binary form must reproduce the above |
||||
copyright notice, this list of conditions and the following disclaimer |
||||
in the documentation and/or other materials provided with the |
||||
distribution. |
||||
* Neither the name of Google Inc. nor the names of its |
||||
contributors may be used to endorse or promote products derived from |
||||
this software without specific prior written permission. |
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
@ -0,0 +1,238 @@ |
||||
# Mergo |
||||
|
||||
A helper to merge structs and maps in Golang. Useful for configuration default values, avoiding messy if-statements. |
||||
|
||||
Also a lovely [comune](http://en.wikipedia.org/wiki/Mergo) (municipality) in the Province of Ancona in the Italian region of Marche. |
||||
|
||||
## Status |
||||
|
||||
It is ready for production use. [It is used in several projects by Docker, Google, The Linux Foundation, VMWare, Shopify, etc](https://github.com/imdario/mergo#mergo-in-the-wild). |
||||
|
||||
[![GoDoc][3]][4] |
||||
[![GoCard][5]][6] |
||||
[![Build Status][1]][2] |
||||
[![Coverage Status][7]][8] |
||||
[![Sourcegraph][9]][10] |
||||
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fimdario%2Fmergo.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Fimdario%2Fmergo?ref=badge_shield) |
||||
|
||||
[1]: https://travis-ci.org/imdario/mergo.png |
||||
[2]: https://travis-ci.org/imdario/mergo |
||||
[3]: https://godoc.org/github.com/imdario/mergo?status.svg |
||||
[4]: https://godoc.org/github.com/imdario/mergo |
||||
[5]: https://goreportcard.com/badge/imdario/mergo |
||||
[6]: https://goreportcard.com/report/github.com/imdario/mergo |
||||
[7]: https://coveralls.io/repos/github/imdario/mergo/badge.svg?branch=master |
||||
[8]: https://coveralls.io/github/imdario/mergo?branch=master |
||||
[9]: https://sourcegraph.com/github.com/imdario/mergo/-/badge.svg |
||||
[10]: https://sourcegraph.com/github.com/imdario/mergo?badge |
||||
|
||||
### Latest release |
||||
|
||||
[Release v0.3.7](https://github.com/imdario/mergo/releases/tag/v0.3.7). |
||||
|
||||
### Important note |
||||
|
||||
Please keep in mind that in [0.3.2](//github.com/imdario/mergo/releases/tag/0.3.2) Mergo changed `Merge()`and `Map()` signatures to support [transformers](#transformers). An optional/variadic argument has been added, so it won't break existing code. |
||||
|
||||
If you were using Mergo **before** April 6th 2015, please check your project works as intended after updating your local copy with ```go get -u github.com/imdario/mergo```. I apologize for any issue caused by its previous behavior and any future bug that Mergo could cause (I hope it won't!) in existing projects after the change (release 0.2.0). |
||||
|
||||
### Donations |
||||
|
||||
If Mergo is useful to you, consider buying me a coffee, a beer or making a monthly donation so I can keep building great free software. :heart_eyes: |
||||
|
||||
<a href='https://ko-fi.com/B0B58839' target='_blank'><img height='36' style='border:0px;height:36px;' src='https://az743702.vo.msecnd.net/cdn/kofi1.png?v=0' border='0' alt='Buy Me a Coffee at ko-fi.com' /></a> |
||||
[![Beerpay](https://beerpay.io/imdario/mergo/badge.svg)](https://beerpay.io/imdario/mergo) |
||||
[![Beerpay](https://beerpay.io/imdario/mergo/make-wish.svg)](https://beerpay.io/imdario/mergo) |
||||
<a href="https://liberapay.com/dario/donate"><img alt="Donate using Liberapay" src="https://liberapay.com/assets/widgets/donate.svg"></a> |
||||
|
||||
### Mergo in the wild |
||||
|
||||
- [moby/moby](https://github.com/moby/moby) |
||||
- [kubernetes/kubernetes](https://github.com/kubernetes/kubernetes) |
||||
- [vmware/dispatch](https://github.com/vmware/dispatch) |
||||
- [Shopify/themekit](https://github.com/Shopify/themekit) |
||||
- [imdario/zas](https://github.com/imdario/zas) |
||||
- [matcornic/hermes](https://github.com/matcornic/hermes) |
||||
- [OpenBazaar/openbazaar-go](https://github.com/OpenBazaar/openbazaar-go) |
||||
- [kataras/iris](https://github.com/kataras/iris) |
||||
- [michaelsauter/crane](https://github.com/michaelsauter/crane) |
||||
- [go-task/task](https://github.com/go-task/task) |
||||
- [sensu/uchiwa](https://github.com/sensu/uchiwa) |
||||
- [ory/hydra](https://github.com/ory/hydra) |
||||
- [sisatech/vcli](https://github.com/sisatech/vcli) |
||||
- [dairycart/dairycart](https://github.com/dairycart/dairycart) |
||||
- [projectcalico/felix](https://github.com/projectcalico/felix) |
||||
- [resin-os/balena](https://github.com/resin-os/balena) |
||||
- [go-kivik/kivik](https://github.com/go-kivik/kivik) |
||||
- [Telefonica/govice](https://github.com/Telefonica/govice) |
||||
- [supergiant/supergiant](supergiant/supergiant) |
||||
- [SergeyTsalkov/brooce](https://github.com/SergeyTsalkov/brooce) |
||||
- [soniah/dnsmadeeasy](https://github.com/soniah/dnsmadeeasy) |
||||
- [ohsu-comp-bio/funnel](https://github.com/ohsu-comp-bio/funnel) |
||||
- [EagerIO/Stout](https://github.com/EagerIO/Stout) |
||||
- [lynndylanhurley/defsynth-api](https://github.com/lynndylanhurley/defsynth-api) |
||||
- [russross/canvasassignments](https://github.com/russross/canvasassignments) |
||||
- [rdegges/cryptly-api](https://github.com/rdegges/cryptly-api) |
||||
- [casualjim/exeggutor](https://github.com/casualjim/exeggutor) |
||||
- [divshot/gitling](https://github.com/divshot/gitling) |
||||
- [RWJMurphy/gorl](https://github.com/RWJMurphy/gorl) |
||||
- [andrerocker/deploy42](https://github.com/andrerocker/deploy42) |
||||
- [elwinar/rambler](https://github.com/elwinar/rambler) |
||||
- [tmaiaroto/gopartman](https://github.com/tmaiaroto/gopartman) |
||||
- [jfbus/impressionist](https://github.com/jfbus/impressionist) |
||||
- [Jmeyering/zealot](https://github.com/Jmeyering/zealot) |
||||
- [godep-migrator/rigger-host](https://github.com/godep-migrator/rigger-host) |
||||
- [Dronevery/MultiwaySwitch-Go](https://github.com/Dronevery/MultiwaySwitch-Go) |
||||
- [thoas/picfit](https://github.com/thoas/picfit) |
||||
- [mantasmatelis/whooplist-server](https://github.com/mantasmatelis/whooplist-server) |
||||
- [jnuthong/item_search](https://github.com/jnuthong/item_search) |
||||
- [bukalapak/snowboard](https://github.com/bukalapak/snowboard) |
||||
|
||||
## Installation |
||||
|
||||
go get github.com/imdario/mergo |
||||
|
||||
// use in your .go code |
||||
import ( |
||||
"github.com/imdario/mergo" |
||||
) |
||||
|
||||
## Usage |
||||
|
||||
You can only merge same-type structs with exported fields initialized as zero value of their type and same-types maps. Mergo won't merge unexported (private) fields but will do recursively any exported one. It won't merge empty structs value as [they are not considered zero values](https://golang.org/ref/spec#The_zero_value) either. Also maps will be merged recursively except for structs inside maps (because they are not addressable using Go reflection). |
||||
|
||||
```go |
||||
if err := mergo.Merge(&dst, src); err != nil { |
||||
// ... |
||||
} |
||||
``` |
||||
|
||||
Also, you can merge overwriting values using the transformer `WithOverride`. |
||||
|
||||
```go |
||||
if err := mergo.Merge(&dst, src, mergo.WithOverride); err != nil { |
||||
// ... |
||||
} |
||||
``` |
||||
|
||||
Additionally, you can map a `map[string]interface{}` to a struct (and otherwise, from struct to map), following the same restrictions as in `Merge()`. Keys are capitalized to find each corresponding exported field. |
||||
|
||||
```go |
||||
if err := mergo.Map(&dst, srcMap); err != nil { |
||||
// ... |
||||
} |
||||
``` |
||||
|
||||
Warning: if you map a struct to map, it won't do it recursively. Don't expect Mergo to map struct members of your struct as `map[string]interface{}`. They will be just assigned as values. |
||||
|
||||
More information and examples in [godoc documentation](http://godoc.org/github.com/imdario/mergo). |
||||
|
||||
### Nice example |
||||
|
||||
```go |
||||
package main |
||||
|
||||
import ( |
||||
"fmt" |
||||
"github.com/imdario/mergo" |
||||
) |
||||
|
||||
type Foo struct { |
||||
A string |
||||
B int64 |
||||
} |
||||
|
||||
func main() { |
||||
src := Foo{ |
||||
A: "one", |
||||
B: 2, |
||||
} |
||||
dest := Foo{ |
||||
A: "two", |
||||
} |
||||
mergo.Merge(&dest, src) |
||||
fmt.Println(dest) |
||||
// Will print |
||||
// {two 2} |
||||
} |
||||
``` |
||||
|
||||
Note: if test are failing due missing package, please execute: |
||||
|
||||
go get gopkg.in/yaml.v2 |
||||
|
||||
### Transformers |
||||
|
||||
Transformers allow to merge specific types differently than in the default behavior. In other words, now you can customize how some types are merged. For example, `time.Time` is a struct; it doesn't have zero value but IsZero can return true because it has fields with zero value. How can we merge a non-zero `time.Time`? |
||||
|
||||
```go |
||||
package main |
||||
|
||||
import ( |
||||
"fmt" |
||||
"github.com/imdario/mergo" |
||||
"reflect" |
||||
"time" |
||||
) |
||||
|
||||
type timeTransfomer struct { |
||||
} |
||||
|
||||
func (t timeTransfomer) Transformer(typ reflect.Type) func(dst, src reflect.Value) error { |
||||
if typ == reflect.TypeOf(time.Time{}) { |
||||
return func(dst, src reflect.Value) error { |
||||
if dst.CanSet() { |
||||
isZero := dst.MethodByName("IsZero") |
||||
result := isZero.Call([]reflect.Value{}) |
||||
if result[0].Bool() { |
||||
dst.Set(src) |
||||
} |
||||
} |
||||
return nil |
||||
} |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
type Snapshot struct { |
||||
Time time.Time |
||||
// ... |
||||
} |
||||
|
||||
func main() { |
||||
src := Snapshot{time.Now()} |
||||
dest := Snapshot{} |
||||
mergo.Merge(&dest, src, mergo.WithTransformers(timeTransfomer{})) |
||||
fmt.Println(dest) |
||||
// Will print |
||||
// { 2018-01-12 01:15:00 +0000 UTC m=+0.000000001 } |
||||
} |
||||
``` |
||||
|
||||
|
||||
## Contact me |
||||
|
||||
If I can help you, you have an idea or you are using Mergo in your projects, don't hesitate to drop me a line (or a pull request): [@im_dario](https://twitter.com/im_dario) |
||||
|
||||
## About |
||||
|
||||
Written by [Dario Castañé](http://dario.im). |
||||
|
||||
## Top Contributors |
||||
|
||||
[![0](https://sourcerer.io/fame/imdario/imdario/mergo/images/0)](https://sourcerer.io/fame/imdario/imdario/mergo/links/0) |
||||
[![1](https://sourcerer.io/fame/imdario/imdario/mergo/images/1)](https://sourcerer.io/fame/imdario/imdario/mergo/links/1) |
||||
[![2](https://sourcerer.io/fame/imdario/imdario/mergo/images/2)](https://sourcerer.io/fame/imdario/imdario/mergo/links/2) |
||||
[![3](https://sourcerer.io/fame/imdario/imdario/mergo/images/3)](https://sourcerer.io/fame/imdario/imdario/mergo/links/3) |
||||
[![4](https://sourcerer.io/fame/imdario/imdario/mergo/images/4)](https://sourcerer.io/fame/imdario/imdario/mergo/links/4) |
||||
[![5](https://sourcerer.io/fame/imdario/imdario/mergo/images/5)](https://sourcerer.io/fame/imdario/imdario/mergo/links/5) |
||||
[![6](https://sourcerer.io/fame/imdario/imdario/mergo/images/6)](https://sourcerer.io/fame/imdario/imdario/mergo/links/6) |
||||
[![7](https://sourcerer.io/fame/imdario/imdario/mergo/images/7)](https://sourcerer.io/fame/imdario/imdario/mergo/links/7) |
||||
|
||||
|
||||
## License |
||||
|
||||
[BSD 3-Clause](http://opensource.org/licenses/BSD-3-Clause) license, as [Go language](http://golang.org/LICENSE). |
||||
|
||||
|
||||
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fimdario%2Fmergo.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2Fimdario%2Fmergo?ref=badge_large) |
@ -0,0 +1,44 @@ |
||||
// Copyright 2013 Dario Castañé. All rights reserved.
|
||||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
/* |
||||
Package mergo merges same-type structs and maps by setting default values in zero-value fields. |
||||
|
||||
Mergo won't merge unexported (private) fields but will do recursively any exported one. It also won't merge structs inside maps (because they are not addressable using Go reflection). |
||||
|
||||
Usage |
||||
|
||||
From my own work-in-progress project: |
||||
|
||||
type networkConfig struct { |
||||
Protocol string |
||||
Address string |
||||
ServerType string `json: "server_type"` |
||||
Port uint16 |
||||
} |
||||
|
||||
type FssnConfig struct { |
||||
Network networkConfig |
||||
} |
||||
|
||||
var fssnDefault = FssnConfig { |
||||
networkConfig { |
||||
"tcp", |
||||
"127.0.0.1", |
||||
"http", |
||||
31560, |
||||
}, |
||||
} |
||||
|
||||
// Inside a function [...]
|
||||
|
||||
if err := mergo.Merge(&config, fssnDefault); err != nil { |
||||
log.Fatal(err) |
||||
} |
||||
|
||||
// More code [...]
|
||||
|
||||
*/ |
||||
package mergo |
@ -0,0 +1,176 @@ |
||||
// Copyright 2014 Dario Castañé. All rights reserved.
|
||||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Based on src/pkg/reflect/deepequal.go from official
|
||||
// golang's stdlib.
|
||||
|
||||
package mergo |
||||
|
||||
import ( |
||||
"fmt" |
||||
"reflect" |
||||
"unicode" |
||||
"unicode/utf8" |
||||
) |
||||
|
||||
func changeInitialCase(s string, mapper func(rune) rune) string { |
||||
if s == "" { |
||||
return s |
||||
} |
||||
r, n := utf8.DecodeRuneInString(s) |
||||
return string(mapper(r)) + s[n:] |
||||
} |
||||
|
||||
func isExported(field reflect.StructField) bool { |
||||
r, _ := utf8.DecodeRuneInString(field.Name) |
||||
return r >= 'A' && r <= 'Z' |
||||
} |
||||
|
||||
// Traverses recursively both values, assigning src's fields values to dst.
|
||||
// The map argument tracks comparisons that have already been seen, which allows
|
||||
// short circuiting on recursive types.
|
||||
func deepMap(dst, src reflect.Value, visited map[uintptr]*visit, depth int, config *Config) (err error) { |
||||
overwrite := config.Overwrite |
||||
if dst.CanAddr() { |
||||
addr := dst.UnsafeAddr() |
||||
h := 17 * addr |
||||
seen := visited[h] |
||||
typ := dst.Type() |
||||
for p := seen; p != nil; p = p.next { |
||||
if p.ptr == addr && p.typ == typ { |
||||
return nil |
||||
} |
||||
} |
||||
// Remember, remember...
|
||||
visited[h] = &visit{addr, typ, seen} |
||||
} |
||||
zeroValue := reflect.Value{} |
||||
switch dst.Kind() { |
||||
case reflect.Map: |
||||
dstMap := dst.Interface().(map[string]interface{}) |
||||
for i, n := 0, src.NumField(); i < n; i++ { |
||||
srcType := src.Type() |
||||
field := srcType.Field(i) |
||||
if !isExported(field) { |
||||
continue |
||||
} |
||||
fieldName := field.Name |
||||
fieldName = changeInitialCase(fieldName, unicode.ToLower) |
||||
if v, ok := dstMap[fieldName]; !ok || (isEmptyValue(reflect.ValueOf(v)) || overwrite) { |
||||
dstMap[fieldName] = src.Field(i).Interface() |
||||
} |
||||
} |
||||
case reflect.Ptr: |
||||
if dst.IsNil() { |
||||
v := reflect.New(dst.Type().Elem()) |
||||
dst.Set(v) |
||||
} |
||||
dst = dst.Elem() |
||||
fallthrough |
||||
case reflect.Struct: |
||||
srcMap := src.Interface().(map[string]interface{}) |
||||
for key := range srcMap { |
||||
config.overwriteWithEmptyValue = true |
||||
srcValue := srcMap[key] |
||||
fieldName := changeInitialCase(key, unicode.ToUpper) |
||||
dstElement := dst.FieldByName(fieldName) |
||||
if dstElement == zeroValue { |
||||
// We discard it because the field doesn't exist.
|
||||
continue |
||||
} |
||||
srcElement := reflect.ValueOf(srcValue) |
||||
dstKind := dstElement.Kind() |
||||
srcKind := srcElement.Kind() |
||||
if srcKind == reflect.Ptr && dstKind != reflect.Ptr { |
||||
srcElement = srcElement.Elem() |
||||
srcKind = reflect.TypeOf(srcElement.Interface()).Kind() |
||||
} else if dstKind == reflect.Ptr { |
||||
// Can this work? I guess it can't.
|
||||
if srcKind != reflect.Ptr && srcElement.CanAddr() { |
||||
srcPtr := srcElement.Addr() |
||||
srcElement = reflect.ValueOf(srcPtr) |
||||
srcKind = reflect.Ptr |
||||
} |
||||
} |
||||
|
||||
if !srcElement.IsValid() { |
||||
continue |
||||
} |
||||
if srcKind == dstKind { |
||||
if _, err = deepMerge(dstElement, srcElement, visited, depth+1, config); err != nil { |
||||
return |
||||
} |
||||
} else if dstKind == reflect.Interface && dstElement.Kind() == reflect.Interface { |
||||
if _, err = deepMerge(dstElement, srcElement, visited, depth+1, config); err != nil { |
||||
return |
||||
} |
||||
} else if srcKind == reflect.Map { |
||||
if err = deepMap(dstElement, srcElement, visited, depth+1, config); err != nil { |
||||
return |
||||
} |
||||
} else { |
||||
return fmt.Errorf("type mismatch on %s field: found %v, expected %v", fieldName, srcKind, dstKind) |
||||
} |
||||
} |
||||
} |
||||
return |
||||
} |
||||
|
||||
// Map sets fields' values in dst from src.
|
||||
// src can be a map with string keys or a struct. dst must be the opposite:
|
||||
// if src is a map, dst must be a valid pointer to struct. If src is a struct,
|
||||
// dst must be map[string]interface{}.
|
||||
// It won't merge unexported (private) fields and will do recursively
|
||||
// any exported field.
|
||||
// If dst is a map, keys will be src fields' names in lower camel case.
|
||||
// Missing key in src that doesn't match a field in dst will be skipped. This
|
||||
// doesn't apply if dst is a map.
|
||||
// This is separated method from Merge because it is cleaner and it keeps sane
|
||||
// semantics: merging equal types, mapping different (restricted) types.
|
||||
func Map(dst, src interface{}, opts ...func(*Config)) error { |
||||
return _map(dst, src, opts...) |
||||
} |
||||
|
||||
// MapWithOverwrite will do the same as Map except that non-empty dst attributes will be overridden by
|
||||
// non-empty src attribute values.
|
||||
// Deprecated: Use Map(…) with WithOverride
|
||||
func MapWithOverwrite(dst, src interface{}, opts ...func(*Config)) error { |
||||
return _map(dst, src, append(opts, WithOverride)...) |
||||
} |
||||
|
||||
func _map(dst, src interface{}, opts ...func(*Config)) error { |
||||
var ( |
||||
vDst, vSrc reflect.Value |
||||
err error |
||||
) |
||||
config := &Config{} |
||||
|
||||
for _, opt := range opts { |
||||
opt(config) |
||||
} |
||||
|
||||
if vDst, vSrc, err = resolveValues(dst, src); err != nil { |
||||
return err |
||||
} |
||||
// To be friction-less, we redirect equal-type arguments
|
||||
// to deepMerge. Only because arguments can be anything.
|
||||
if vSrc.Kind() == vDst.Kind() { |
||||
_, err := deepMerge(vDst, vSrc, make(map[uintptr]*visit), 0, config) |
||||
return err |
||||
} |
||||
switch vSrc.Kind() { |
||||
case reflect.Struct: |
||||
if vDst.Kind() != reflect.Map { |
||||
return ErrExpectedMapAsDestination |
||||
} |
||||
case reflect.Map: |
||||
if vDst.Kind() != reflect.Struct { |
||||
return ErrExpectedStructAsDestination |
||||
} |
||||
default: |
||||
return ErrNotSupported |
||||
} |
||||
return deepMap(vDst, vSrc, make(map[uintptr]*visit), 0, config) |
||||
} |
@ -0,0 +1,338 @@ |
||||
// Copyright 2013 Dario Castañé. All rights reserved.
|
||||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Based on src/pkg/reflect/deepequal.go from official
|
||||
// golang's stdlib.
|
||||
|
||||
package mergo |
||||
|
||||
import ( |
||||
"fmt" |
||||
"reflect" |
||||
"unsafe" |
||||
) |
||||
|
||||
func hasExportedField(dst reflect.Value) (exported bool) { |
||||
for i, n := 0, dst.NumField(); i < n; i++ { |
||||
field := dst.Type().Field(i) |
||||
if isExportedComponent(&field) { |
||||
return true |
||||
} |
||||
} |
||||
return |
||||
} |
||||
|
||||
func isExportedComponent(field *reflect.StructField) bool { |
||||
name := field.Name |
||||
pkgPath := field.PkgPath |
||||
if len(pkgPath) > 0 { |
||||
return false |
||||
} |
||||
c := name[0] |
||||
if 'a' <= c && c <= 'z' || c == '_' { |
||||
return false |
||||
} |
||||
return true |
||||
} |
||||
|
||||
type Config struct { |
||||
Overwrite bool |
||||
AppendSlice bool |
||||
TypeCheck bool |
||||
Transformers Transformers |
||||
overwriteWithEmptyValue bool |
||||
overwriteSliceWithEmptyValue bool |
||||
} |
||||
|
||||
type Transformers interface { |
||||
Transformer(reflect.Type) func(dst, src reflect.Value) error |
||||
} |
||||
|
||||
// Traverses recursively both values, assigning src's fields values to dst.
|
||||
// The map argument tracks comparisons that have already been seen, which allows
|
||||
// short circuiting on recursive types.
|
||||
func deepMerge(dstIn, src reflect.Value, visited map[uintptr]*visit, depth int, config *Config) (dst reflect.Value, err error) { |
||||
dst = dstIn |
||||
overwrite := config.Overwrite |
||||
typeCheck := config.TypeCheck |
||||
overwriteWithEmptySrc := config.overwriteWithEmptyValue |
||||
overwriteSliceWithEmptySrc := config.overwriteSliceWithEmptyValue |
||||
|
||||
if !src.IsValid() { |
||||
return |
||||
} |
||||
|
||||
if dst.CanAddr() { |
||||
addr := dst.UnsafeAddr() |
||||
h := 17 * addr |
||||
seen := visited[h] |
||||
typ := dst.Type() |
||||
for p := seen; p != nil; p = p.next { |
||||
if p.ptr == addr && p.typ == typ { |
||||
return dst, nil |
||||
} |
||||
} |
||||
// Remember, remember...
|
||||
visited[h] = &visit{addr, typ, seen} |
||||
} |
||||
|
||||
if config.Transformers != nil && !isEmptyValue(dst) { |
||||
if fn := config.Transformers.Transformer(dst.Type()); fn != nil { |
||||
err = fn(dst, src) |
||||
return |
||||
} |
||||
} |
||||
|
||||
if dst.IsValid() && src.IsValid() && src.Type() != dst.Type() { |
||||
err = fmt.Errorf("cannot append two different types (%s, %s)", src.Kind(), dst.Kind()) |
||||
return |
||||
} |
||||
|
||||
switch dst.Kind() { |
||||
case reflect.Struct: |
||||
if hasExportedField(dst) { |
||||
dstCp := reflect.New(dst.Type()).Elem() |
||||
for i, n := 0, dst.NumField(); i < n; i++ { |
||||
dstField := dst.Field(i) |
||||
structField := dst.Type().Field(i) |
||||
// copy un-exported struct fields
|
||||
if !isExportedComponent(&structField) { |
||||
rf := dstCp.Field(i) |
||||
rf = reflect.NewAt(rf.Type(), unsafe.Pointer(rf.UnsafeAddr())).Elem() //nolint:gosec
|
||||
dstRF := dst.Field(i) |
||||
if !dst.Field(i).CanAddr() { |
||||
continue |
||||
} |
||||
|
||||
dstRF = reflect.NewAt(dstRF.Type(), unsafe.Pointer(dstRF.UnsafeAddr())).Elem() //nolint:gosec
|
||||
rf.Set(dstRF) |
||||
continue |
||||
} |
||||
dstField, err = deepMerge(dstField, src.Field(i), visited, depth+1, config) |
||||
if err != nil { |
||||
return |
||||
} |
||||
dstCp.Field(i).Set(dstField) |
||||
} |
||||
|
||||
if dst.CanSet() { |
||||
dst.Set(dstCp) |
||||
} else { |
||||
dst = dstCp |
||||
} |
||||
return |
||||
} else { |
||||
if (isReflectNil(dst) || overwrite) && (!isEmptyValue(src) || overwriteWithEmptySrc) { |
||||
dst = src |
||||
} |
||||
} |
||||
|
||||
case reflect.Map: |
||||
if dst.IsNil() && !src.IsNil() { |
||||
if dst.CanSet() { |
||||
dst.Set(reflect.MakeMap(dst.Type())) |
||||
} else { |
||||
dst = src |
||||
return |
||||
} |
||||
} |
||||
for _, key := range src.MapKeys() { |
||||
srcElement := src.MapIndex(key) |
||||
dstElement := dst.MapIndex(key) |
||||
if !srcElement.IsValid() { |
||||
continue |
||||
} |
||||
if dst.MapIndex(key).IsValid() { |
||||
k := dstElement.Interface() |
||||
dstElement = reflect.ValueOf(k) |
||||
} |
||||
if isReflectNil(srcElement) { |
||||
if overwrite || isReflectNil(dstElement) { |
||||
dst.SetMapIndex(key, srcElement) |
||||
} |
||||
continue |
||||
} |
||||
if !srcElement.CanInterface() { |
||||
continue |
||||
} |
||||
|
||||
if srcElement.CanInterface() { |
||||
srcElement = reflect.ValueOf(srcElement.Interface()) |
||||
if dstElement.IsValid() { |
||||
dstElement = reflect.ValueOf(dstElement.Interface()) |
||||
} |
||||
} |
||||
dstElement, err = deepMerge(dstElement, srcElement, visited, depth+1, config) |
||||
if err != nil { |
||||
return |
||||
} |
||||
dst.SetMapIndex(key, dstElement) |
||||
|
||||
} |
||||
case reflect.Slice: |
||||
newSlice := dst |
||||
if (!isEmptyValue(src) || overwriteWithEmptySrc || overwriteSliceWithEmptySrc) && (overwrite || isEmptyValue(dst)) && !config.AppendSlice { |
||||
if typeCheck && src.Type() != dst.Type() { |
||||
return dst, fmt.Errorf("cannot override two slices with different type (%s, %s)", src.Type(), dst.Type()) |
||||
} |
||||
newSlice = src |
||||
} else if config.AppendSlice { |
||||
if typeCheck && src.Type() != dst.Type() { |
||||
err = fmt.Errorf("cannot append two slice with different type (%s, %s)", src.Type(), dst.Type()) |
||||
return |
||||
} |
||||
newSlice = reflect.AppendSlice(dst, src) |
||||
} |
||||
if dst.CanSet() { |
||||
dst.Set(newSlice) |
||||
} else { |
||||
dst = newSlice |
||||
} |
||||
case reflect.Ptr, reflect.Interface: |
||||
if isReflectNil(src) { |
||||
break |
||||
} |
||||
|
||||
if dst.Kind() != reflect.Ptr && src.Type().AssignableTo(dst.Type()) { |
||||
if dst.IsNil() || overwrite { |
||||
if overwrite || isEmptyValue(dst) { |
||||
if dst.CanSet() { |
||||
dst.Set(src) |
||||
} else { |
||||
dst = src |
||||
} |
||||
} |
||||
} |
||||
break |
||||
} |
||||
|
||||
if src.Kind() != reflect.Interface { |
||||
if dst.IsNil() || (src.Kind() != reflect.Ptr && overwrite) { |
||||
if dst.CanSet() && (overwrite || isEmptyValue(dst)) { |
||||
dst.Set(src) |
||||
} |
||||
} else if src.Kind() == reflect.Ptr { |
||||
if dst, err = deepMerge(dst.Elem(), src.Elem(), visited, depth+1, config); err != nil { |
||||
return |
||||
} |
||||
dst = dst.Addr() |
||||
} else if dst.Elem().Type() == src.Type() { |
||||
if dst, err = deepMerge(dst.Elem(), src, visited, depth+1, config); err != nil { |
||||
return |
||||
} |
||||
} else { |
||||
return dst, ErrDifferentArgumentsTypes |
||||
} |
||||
break |
||||
} |
||||
if dst.IsNil() || overwrite { |
||||
if (overwrite || isEmptyValue(dst)) && (overwriteWithEmptySrc || !isEmptyValue(src)) { |
||||
if dst.CanSet() { |
||||
dst.Set(src) |
||||
} else { |
||||
dst = src |
||||
} |
||||
} |
||||
} else if _, err = deepMerge(dst.Elem(), src.Elem(), visited, depth+1, config); err != nil { |
||||
return |
||||
} |
||||
default: |
||||
overwriteFull := (!isEmptyValue(src) || overwriteWithEmptySrc) && (overwrite || isEmptyValue(dst)) |
||||
if overwriteFull { |
||||
if dst.CanSet() { |
||||
dst.Set(src) |
||||
} else { |
||||
dst = src |
||||
} |
||||
} |
||||
} |
||||
|
||||
return |
||||
} |
||||
|
||||
// Merge will fill any empty for value type attributes on the dst struct using corresponding
|
||||
// src attributes if they themselves are not empty. dst and src must be valid same-type structs
|
||||
// and dst must be a pointer to struct.
|
||||
// It won't merge unexported (private) fields and will do recursively any exported field.
|
||||
func Merge(dst, src interface{}, opts ...func(*Config)) error { |
||||
return merge(dst, src, opts...) |
||||
} |
||||
|
||||
// MergeWithOverwrite will do the same as Merge except that non-empty dst attributes will be overridden by
|
||||
// non-empty src attribute values.
|
||||
// Deprecated: use Merge(…) with WithOverride
|
||||
func MergeWithOverwrite(dst, src interface{}, opts ...func(*Config)) error { |
||||
return merge(dst, src, append(opts, WithOverride)...) |
||||
} |
||||
|
||||
// WithTransformers adds transformers to merge, allowing to customize the merging of some types.
|
||||
func WithTransformers(transformers Transformers) func(*Config) { |
||||
return func(config *Config) { |
||||
config.Transformers = transformers |
||||
} |
||||
} |
||||
|
||||
// WithOverride will make merge override non-empty dst attributes with non-empty src attributes values.
|
||||
func WithOverride(config *Config) { |
||||
config.Overwrite = true |
||||
} |
||||
|
||||
// WithOverwriteWithEmptyValue will make merge override non empty dst attributes with empty src attributes values.
|
||||
func WithOverwriteWithEmptyValue(config *Config) { |
||||
config.overwriteWithEmptyValue = true |
||||
} |
||||
|
||||
// WithOverrideEmptySlice will make merge override empty dst slice with empty src slice.
|
||||
func WithOverrideEmptySlice(config *Config) { |
||||
config.overwriteSliceWithEmptyValue = true |
||||
} |
||||
|
||||
// WithAppendSlice will make merge append slices instead of overwriting it.
|
||||
func WithAppendSlice(config *Config) { |
||||
config.AppendSlice = true |
||||
} |
||||
|
||||
// WithTypeCheck will make merge check types while overwriting it (must be used with WithOverride).
|
||||
func WithTypeCheck(config *Config) { |
||||
config.TypeCheck = true |
||||
} |
||||
|
||||
func merge(dst, src interface{}, opts ...func(*Config)) error { |
||||
var ( |
||||
vDst, vSrc reflect.Value |
||||
err error |
||||
) |
||||
|
||||
config := &Config{} |
||||
|
||||
for _, opt := range opts { |
||||
opt(config) |
||||
} |
||||
|
||||
if vDst, vSrc, err = resolveValues(dst, src); err != nil { |
||||
return err |
||||
} |
||||
if !vDst.CanSet() { |
||||
return fmt.Errorf("cannot set dst, needs reference") |
||||
} |
||||
if vDst.Type() != vSrc.Type() { |
||||
return ErrDifferentArgumentsTypes |
||||
} |
||||
_, err = deepMerge(vDst, vSrc, make(map[uintptr]*visit), 0, config) |
||||
return err |
||||
} |
||||
|
||||
// IsReflectNil is the reflect value provided nil
|
||||
func isReflectNil(v reflect.Value) bool { |
||||
k := v.Kind() |
||||
switch k { |
||||
case reflect.Interface, reflect.Slice, reflect.Chan, reflect.Func, reflect.Map, reflect.Ptr: |
||||
// Both interface and slice are nil if first word is 0.
|
||||
// Both are always bigger than a word; assume flagIndir.
|
||||
return v.IsNil() |
||||
default: |
||||
return false |
||||
} |
||||
} |
@ -0,0 +1,97 @@ |
||||
// Copyright 2013 Dario Castañé. All rights reserved.
|
||||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Based on src/pkg/reflect/deepequal.go from official
|
||||
// golang's stdlib.
|
||||
|
||||
package mergo |
||||
|
||||
import ( |
||||
"errors" |
||||
"reflect" |
||||
) |
||||
|
||||
// Errors reported by Mergo when it finds invalid arguments.
|
||||
var ( |
||||
ErrNilArguments = errors.New("src and dst must not be nil") |
||||
ErrDifferentArgumentsTypes = errors.New("src and dst must be of same type") |
||||
ErrNotSupported = errors.New("only structs and maps are supported") |
||||
ErrExpectedMapAsDestination = errors.New("dst was expected to be a map") |
||||
ErrExpectedStructAsDestination = errors.New("dst was expected to be a struct") |
||||
) |
||||
|
||||
// During deepMerge, must keep track of checks that are
|
||||
// in progress. The comparison algorithm assumes that all
|
||||
// checks in progress are true when it reencounters them.
|
||||
// Visited are stored in a map indexed by 17 * a1 + a2;
|
||||
type visit struct { |
||||
ptr uintptr |
||||
typ reflect.Type |
||||
next *visit |
||||
} |
||||
|
||||
// From src/pkg/encoding/json/encode.go.
|
||||
func isEmptyValue(v reflect.Value) bool { |
||||
switch v.Kind() { |
||||
case reflect.Array, reflect.Map, reflect.Slice, reflect.String: |
||||
return v.Len() == 0 |
||||
case reflect.Bool: |
||||
return !v.Bool() |
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: |
||||
return v.Int() == 0 |
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: |
||||
return v.Uint() == 0 |
||||
case reflect.Float32, reflect.Float64: |
||||
return v.Float() == 0 |
||||
case reflect.Interface, reflect.Ptr: |
||||
if v.IsNil() { |
||||
return true |
||||
} |
||||
return isEmptyValue(v.Elem()) |
||||
case reflect.Func: |
||||
return v.IsNil() |
||||
case reflect.Invalid: |
||||
return true |
||||
} |
||||
return false |
||||
} |
||||
|
||||
func resolveValues(dst, src interface{}) (vDst, vSrc reflect.Value, err error) { |
||||
if dst == nil || src == nil { |
||||
err = ErrNilArguments |
||||
return |
||||
} |
||||
vDst = reflect.ValueOf(dst).Elem() |
||||
if vDst.Kind() != reflect.Struct && vDst.Kind() != reflect.Map { |
||||
err = ErrNotSupported |
||||
return |
||||
} |
||||
vSrc = reflect.ValueOf(src) |
||||
// We check if vSrc is a pointer to dereference it.
|
||||
if vSrc.Kind() == reflect.Ptr { |
||||
vSrc = vSrc.Elem() |
||||
} |
||||
return |
||||
} |
||||
|
||||
// Traverses recursively both values, assigning src's fields values to dst.
|
||||
// The map argument tracks comparisons that have already been seen, which allows
|
||||
// short circuiting on recursive types.
|
||||
func deeper(dst, src reflect.Value, visited map[uintptr]*visit, depth int) (err error) { |
||||
if dst.CanAddr() { |
||||
addr := dst.UnsafeAddr() |
||||
h := 17 * addr |
||||
seen := visited[h] |
||||
typ := dst.Type() |
||||
for p := seen; p != nil; p = p.next { |
||||
if p.ptr == addr && p.typ == typ { |
||||
return nil |
||||
} |
||||
} |
||||
// Remember, remember...
|
||||
visited[h] = &visit{addr, typ, seen} |
||||
} |
||||
return // TODO refactor
|
||||
} |
@ -0,0 +1,27 @@ |
||||
// Copyright 2020 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Recreate a getsystemcfg syscall handler instead of
|
||||
// using the one provided by x/sys/unix to avoid having
|
||||
// the dependency between them. (See golang.org/issue/32102)
|
||||
// Morever, this file will be used during the building of
|
||||
// gccgo's libgo and thus must not used a CGo method.
|
||||
|
||||
// +build aix
|
||||
// +build gccgo
|
||||
|
||||
package cpu |
||||
|
||||
import ( |
||||
"syscall" |
||||
) |
||||
|
||||
//extern getsystemcfg
|
||||
func gccgoGetsystemcfg(label uint32) (r uint64) |
||||
|
||||
func callgetsystemcfg(label int) (r1 uintptr, e1 syscall.Errno) { |
||||
r1 = uintptr(gccgoGetsystemcfg(uint32(label))) |
||||
e1 = syscall.GetErrno() |
||||
return |
||||
} |
Loading…
Reference in new issue