This commit is contained in:
Mustafa Gezen 2023-08-31 10:17:01 +02:00
parent 6d46a34c82
commit 96cca525d1
Signed by: mustafa
GPG Key ID: DCDF010D946438C1
40 changed files with 6647 additions and 1 deletions

View File

@ -220,6 +220,12 @@ def go_dependencies():
sum = "h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=",
version = "v4.0.0",
)
go_repository(
name = "com_github_bluekeyes_go_gitdiff",
importpath = "github.com/bluekeyes/go-gitdiff",
sum = "h1:AXrIoy/VEA9Baz2lhwMlpdzDJ/sKof6C9yTt1oqw4hQ=",
version = "v0.5.0",
)
go_repository(
name = "com_github_bmatcuk_doublestar_v4",
@ -1214,6 +1220,13 @@ def go_dependencies():
sum = "h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU=",
version = "v0.5.1",
)
go_repository(
name = "com_github_hashicorp_hcl",
importpath = "github.com/hashicorp/hcl",
sum = "h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=",
version = "v1.0.0",
)
go_repository(
name = "com_github_hashicorp_hcl_v2",
importpath = "github.com/hashicorp/hcl/v2",
@ -1621,6 +1634,13 @@ def go_dependencies():
sum = "h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=",
version = "v1.2.0",
)
go_repository(
name = "com_github_mitchellh_go_homedir",
importpath = "github.com/mitchellh/go-homedir",
sum = "h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=",
version = "v1.1.0",
)
go_repository(
name = "com_github_mitchellh_go_wordwrap",
importpath = "github.com/mitchellh/go-wordwrap",
@ -1962,6 +1982,12 @@ def go_dependencies():
sum = "h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=",
version = "v3.0.1",
)
go_repository(
name = "com_github_rocky_linux_srpmproc",
importpath = "github.com/rocky-linux/srpmproc",
sum = "h1:rVCtpFyrYI7kAj5XOCZEB4ZcBMfh/E2Vt01AxwJsfpE=",
version = "v0.5.0",
)
go_repository(
name = "com_github_rogpeppe_fastuuid",
@ -2099,6 +2125,12 @@ def go_dependencies():
sum = "h1:qLC7fQah7D6K1B0ujays3HV9gkFtllcxhzImRR7ArPQ=",
version = "v0.0.0-20180118202830-f09979ecbc72",
)
go_repository(
name = "com_github_spf13_afero",
importpath = "github.com/spf13/afero",
sum = "h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=",
version = "v1.1.2",
)
go_repository(
name = "com_github_spf13_cast",
@ -2113,6 +2145,12 @@ def go_dependencies():
sum = "h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=",
version = "v1.7.0",
)
go_repository(
name = "com_github_spf13_jwalterweatherman",
importpath = "github.com/spf13/jwalterweatherman",
sum = "h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=",
version = "v1.0.0",
)
go_repository(
name = "com_github_spf13_pflag",
@ -2120,6 +2158,13 @@ def go_dependencies():
sum = "h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=",
version = "v1.0.5",
)
go_repository(
name = "com_github_spf13_viper",
importpath = "github.com/spf13/viper",
sum = "h1:xVKxvI7ouOI5I+U9s2eeiUfMaWBVoXA3AWskkrqK0VM=",
version = "v1.7.0",
)
go_repository(
name = "com_github_stefanberger_go_pkcs11uri",
importpath = "github.com/stefanberger/go-pkcs11uri",
@ -2146,6 +2191,13 @@ def go_dependencies():
sum = "h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=",
version = "v1.8.4",
)
go_repository(
name = "com_github_subosito_gotenv",
importpath = "github.com/subosito/gotenv",
sum = "h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=",
version = "v1.2.0",
)
go_repository(
name = "com_github_syndtr_gocapability",
importpath = "github.com/syndtr/gocapability",
@ -3197,6 +3249,12 @@ def go_dependencies():
sum = "h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=",
version = "v0.9.1",
)
go_repository(
name = "in_gopkg_ini_v1",
importpath = "gopkg.in/ini.v1",
sum = "h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno=",
version = "v1.51.0",
)
go_repository(
name = "in_gopkg_square_go_jose_v2",

6
go.mod
View File

@ -26,6 +26,7 @@ require (
github.com/jarcoal/httpmock v1.3.1
github.com/pkg/errors v0.9.1
github.com/prometheus/client_golang v1.16.0
github.com/rocky-linux/srpmproc v0.5.0
github.com/sassoftware/go-rpmutils v0.2.0
github.com/stretchr/testify v1.8.4
github.com/temporalio/temporalite v0.3.1-0.20230117200252-2df426ad3426
@ -43,6 +44,7 @@ require (
golang.org/x/crypto v0.12.0
golang.org/x/mod v0.12.0
golang.org/x/oauth2 v0.11.0
google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d
google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d
google.golang.org/grpc v1.57.0
google.golang.org/protobuf v1.31.0
@ -82,6 +84,7 @@ require (
github.com/benbjohnson/clock v1.3.5 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/blang/semver/v4 v4.0.0 // indirect
github.com/bluekeyes/go-gitdiff v0.5.0 // indirect
github.com/bmatcuk/doublestar/v4 v4.6.0 // indirect
github.com/bufbuild/protocompile v0.6.0 // indirect
github.com/buger/jsonparser v1.1.1 // indirect
@ -258,7 +261,6 @@ require (
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
google.golang.org/api v0.138.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
@ -289,3 +291,5 @@ replace go.resf.org/peridot/tools/mothership/pb => ./bazel-bin/tools/mothership/
replace go.resf.org/peridot/third_party/bazel/src/main/protobuf => ./bazel-bin/third_party/bazel/src/main/protobuf/blaze_query_go_proto_/go.resf.org/peridot/third_party/bazel/src/main/protobuf
replace go.resf.org/peridot/tools/mothership/admin/pb => ./bazel-bin/tools/mothership/proto/admin/v1/mshipadminpb_go_proto_/go.resf.org/peridot/tools/mothership/admin/pb
replace google.golang.org/genproto/googleapis/longrunning => ./bazel-bin/third_party/googleapis/google/longrunning/longrunning_go_proto_/google.golang.org/genproto/googleapis/longrunning

4
go.sum
View File

@ -452,6 +452,8 @@ github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCS
github.com/bitly/go-hostpool v0.1.0 h1:XKmsF6k5el6xHG3WPJ8U0Ku/ye7njX7W81Ng7O2ioR0=
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
github.com/bluekeyes/go-gitdiff v0.5.0 h1:AXrIoy/VEA9Baz2lhwMlpdzDJ/sKof6C9yTt1oqw4hQ=
github.com/bluekeyes/go-gitdiff v0.5.0/go.mod h1:QpfYYO1E0fTVHVZAZKiRjtSGY9823iCdvGXBcEzHGbM=
github.com/bmatcuk/doublestar/v4 v4.0.2/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/bmatcuk/doublestar/v4 v4.6.0 h1:HTuxyug8GyFbRkrffIpzNCSK4luc0TY3wzXvzIZhEXc=
github.com/bmatcuk/doublestar/v4 v4.6.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
@ -964,6 +966,8 @@ github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ=
github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rocky-linux/srpmproc v0.5.0 h1:rVCtpFyrYI7kAj5XOCZEB4ZcBMfh/E2Vt01AxwJsfpE=
github.com/rocky-linux/srpmproc v0.5.0/go.mod h1:x8Z2wqhV2JqRnYMhYz3thOQkfsSWjJkyX8DVGDPOb48=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=

21
vendor/github.com/bluekeyes/go-gitdiff/LICENSE generated vendored Normal file
View File

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

19
vendor/github.com/bluekeyes/go-gitdiff/gitdiff/BUILD generated vendored Normal file
View File

@ -0,0 +1,19 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library")
go_library(
name = "gitdiff",
srcs = [
"apply.go",
"base85.go",
"binary.go",
"file_header.go",
"gitdiff.go",
"io.go",
"parser.go",
"patch_header.go",
"text.go",
],
importmap = "go.resf.org/peridot/vendor/github.com/bluekeyes/go-gitdiff/gitdiff",
importpath = "github.com/bluekeyes/go-gitdiff/gitdiff",
visibility = ["//visibility:public"],
)

454
vendor/github.com/bluekeyes/go-gitdiff/gitdiff/apply.go generated vendored Normal file
View File

@ -0,0 +1,454 @@
package gitdiff
import (
"errors"
"fmt"
"io"
"sort"
)
// Conflict indicates an apply failed due to a conflict between the patch and
// the source content.
//
// Users can test if an error was caused by a conflict by using errors.Is with
// an empty Conflict:
//
// if errors.Is(err, &Conflict{}) {
// // handle conflict
// }
//
type Conflict struct {
msg string
}
func (c *Conflict) Error() string {
return "conflict: " + c.msg
}
// Is implements error matching for Conflict. Passing an empty instance of
// Conflict always returns true.
func (c *Conflict) Is(other error) bool {
if other, ok := other.(*Conflict); ok {
return other.msg == "" || other.msg == c.msg
}
return false
}
// ApplyError wraps an error that occurs during patch application with
// additional location information, if it is available.
type ApplyError struct {
// Line is the one-indexed line number in the source data
Line int64
// Fragment is the one-indexed fragment number in the file
Fragment int
// FragmentLine is the one-indexed line number in the fragment
FragmentLine int
err error
}
// Unwrap returns the wrapped error.
func (e *ApplyError) Unwrap() error {
return e.err
}
func (e *ApplyError) Error() string {
return fmt.Sprintf("%v", e.err)
}
type lineNum int
type fragNum int
type fragLineNum int
// applyError creates a new *ApplyError wrapping err or augments the information
// in err with args if it is already an *ApplyError. Returns nil if err is nil.
func applyError(err error, args ...interface{}) error {
if err == nil {
return nil
}
e, ok := err.(*ApplyError)
if !ok {
if err == io.EOF {
err = io.ErrUnexpectedEOF
}
e = &ApplyError{err: err}
}
for _, arg := range args {
switch v := arg.(type) {
case lineNum:
e.Line = int64(v) + 1
case fragNum:
e.Fragment = int(v) + 1
case fragLineNum:
e.FragmentLine = int(v) + 1
}
}
return e
}
var (
errApplyInProgress = errors.New("gitdiff: incompatible apply in progress")
)
const (
applyInitial = iota
applyText
applyBinary
applyFile
)
// Apply is a convenience function that creates an Applier for src with default
// settings and applies the changes in f, writing the result to dst.
func Apply(dst io.Writer, src io.ReaderAt, f *File) error {
return NewApplier(src).ApplyFile(dst, f)
}
// Applier applies changes described in fragments to source data. If changes
// are described in multiple fragments, those fragments must be applied in
// order, usually by calling ApplyFile.
//
// By default, Applier operates in "strict" mode, where fragment content and
// positions must exactly match those of the source.
//
// If an error occurs while applying, methods on Applier return instances of
// *ApplyError that annotate the wrapped error with additional information
// when available. If the error is because of a conflict between a fragment and
// the source, the wrapped error will be a *Conflict.
//
// While an Applier can apply both text and binary fragments, only one fragment
// type can be used without resetting the Applier. The first fragment applied
// sets the type for the Applier. Mixing fragment types or mixing
// fragment-level and file-level applies results in an error.
type Applier struct {
src io.ReaderAt
lineSrc LineReaderAt
nextLine int64
applyType int
}
// NewApplier creates an Applier that reads data from src. If src is a
// LineReaderAt, it is used directly to apply text fragments.
func NewApplier(src io.ReaderAt) *Applier {
a := new(Applier)
a.Reset(src)
return a
}
// Reset resets the input and internal state of the Applier. If src is nil, the
// existing source is reused.
func (a *Applier) Reset(src io.ReaderAt) {
if src != nil {
a.src = src
if lineSrc, ok := src.(LineReaderAt); ok {
a.lineSrc = lineSrc
} else {
a.lineSrc = &lineReaderAt{r: src}
}
}
a.nextLine = 0
a.applyType = applyInitial
}
// ApplyFile applies the changes in all of the fragments of f and writes the
// result to dst.
func (a *Applier) ApplyFile(dst io.Writer, f *File) error {
if a.applyType != applyInitial {
return applyError(errApplyInProgress)
}
defer func() { a.applyType = applyFile }()
if f.IsBinary && len(f.TextFragments) > 0 {
return applyError(errors.New("binary file contains text fragments"))
}
if !f.IsBinary && f.BinaryFragment != nil {
return applyError(errors.New("text file contains binary fragment"))
}
switch {
case f.BinaryFragment != nil:
return a.ApplyBinaryFragment(dst, f.BinaryFragment)
case len(f.TextFragments) > 0:
frags := make([]*TextFragment, len(f.TextFragments))
copy(frags, f.TextFragments)
sort.Slice(frags, func(i, j int) bool {
return frags[i].OldPosition < frags[j].OldPosition
})
// TODO(bkeyes): consider merging overlapping fragments
// right now, the application fails if fragments overlap, but it should be
// possible to precompute the result of applying them in order
for i, frag := range frags {
if err := a.ApplyTextFragment(dst, frag); err != nil {
return applyError(err, fragNum(i))
}
}
}
return applyError(a.Flush(dst))
}
// ApplyTextFragment applies the changes in the fragment f and writes unwritten
// data before the start of the fragment and the result to dst. If multiple
// text fragments apply to the same source, ApplyTextFragment must be called in
// order of increasing start position. As a result, each fragment can be
// applied at most once before a call to Reset.
func (a *Applier) ApplyTextFragment(dst io.Writer, f *TextFragment) error {
if a.applyType != applyInitial && a.applyType != applyText {
return applyError(errApplyInProgress)
}
defer func() { a.applyType = applyText }()
// application code assumes fragment fields are consistent
if err := f.Validate(); err != nil {
return applyError(err)
}
// lines are 0-indexed, positions are 1-indexed (but new files have position = 0)
fragStart := f.OldPosition - 1
if fragStart < 0 {
fragStart = 0
}
fragEnd := fragStart + f.OldLines
start := a.nextLine
if fragStart < start {
return applyError(&Conflict{"fragment overlaps with an applied fragment"})
}
if f.OldPosition == 0 {
ok, err := isLen(a.src, 0)
if err != nil {
return applyError(err)
}
if !ok {
return applyError(&Conflict{"cannot create new file from non-empty src"})
}
}
preimage := make([][]byte, fragEnd-start)
n, err := a.lineSrc.ReadLinesAt(preimage, start)
switch {
case err == nil:
case err == io.EOF && n == len(preimage): // last line of frag has no newline character
default:
return applyError(err, lineNum(start+int64(n)))
}
// copy leading data before the fragment starts
for i, line := range preimage[:fragStart-start] {
if _, err := dst.Write(line); err != nil {
a.nextLine = start + int64(i)
return applyError(err, lineNum(a.nextLine))
}
}
preimage = preimage[fragStart-start:]
// apply the changes in the fragment
used := int64(0)
for i, line := range f.Lines {
if err := applyTextLine(dst, line, preimage, used); err != nil {
a.nextLine = fragStart + used
return applyError(err, lineNum(a.nextLine), fragLineNum(i))
}
if line.Old() {
used++
}
}
a.nextLine = fragStart + used
// new position of +0,0 mean a full delete, so check for leftovers
if f.NewPosition == 0 && f.NewLines == 0 {
var b [1][]byte
n, err := a.lineSrc.ReadLinesAt(b[:], a.nextLine)
if err != nil && err != io.EOF {
return applyError(err, lineNum(a.nextLine))
}
if n > 0 {
return applyError(&Conflict{"src still has content after full delete"}, lineNum(a.nextLine))
}
}
return nil
}
func applyTextLine(dst io.Writer, line Line, preimage [][]byte, i int64) (err error) {
if line.Old() && string(preimage[i]) != line.Line {
return &Conflict{"fragment line does not match src line"}
}
if line.New() {
_, err = io.WriteString(dst, line.Line)
}
return err
}
// Flush writes any data following the last applied fragment to dst.
func (a *Applier) Flush(dst io.Writer) (err error) {
switch a.applyType {
case applyInitial:
_, err = copyFrom(dst, a.src, 0)
case applyText:
_, err = copyLinesFrom(dst, a.lineSrc, a.nextLine)
case applyBinary:
// nothing to flush, binary apply "consumes" full source
}
return err
}
// ApplyBinaryFragment applies the changes in the fragment f and writes the
// result to dst. At most one binary fragment can be applied before a call to
// Reset.
func (a *Applier) ApplyBinaryFragment(dst io.Writer, f *BinaryFragment) error {
if a.applyType != applyInitial {
return applyError(errApplyInProgress)
}
defer func() { a.applyType = applyBinary }()
if f == nil {
return applyError(errors.New("nil fragment"))
}
switch f.Method {
case BinaryPatchLiteral:
if _, err := dst.Write(f.Data); err != nil {
return applyError(err)
}
case BinaryPatchDelta:
if err := applyBinaryDeltaFragment(dst, a.src, f.Data); err != nil {
return applyError(err)
}
default:
return applyError(fmt.Errorf("unsupported binary patch method: %v", f.Method))
}
return nil
}
func applyBinaryDeltaFragment(dst io.Writer, src io.ReaderAt, frag []byte) error {
srcSize, delta := readBinaryDeltaSize(frag)
if err := checkBinarySrcSize(src, srcSize); err != nil {
return err
}
dstSize, delta := readBinaryDeltaSize(delta)
for len(delta) > 0 {
op := delta[0]
if op == 0 {
return errors.New("invalid delta opcode 0")
}
var n int64
var err error
switch op & 0x80 {
case 0x80:
n, delta, err = applyBinaryDeltaCopy(dst, op, delta[1:], src)
case 0x00:
n, delta, err = applyBinaryDeltaAdd(dst, op, delta[1:])
}
if err != nil {
return err
}
dstSize -= n
}
if dstSize != 0 {
return errors.New("corrupt binary delta: insufficient or extra data")
}
return nil
}
// readBinaryDeltaSize reads a variable length size from a delta-encoded binary
// fragment, returing the size and the unused data. Data is encoded as:
//
// [[1xxxxxxx]...] [0xxxxxxx]
//
// in little-endian order, with 7 bits of the value per byte.
func readBinaryDeltaSize(d []byte) (size int64, rest []byte) {
shift := uint(0)
for i, b := range d {
size |= int64(b&0x7F) << shift
shift += 7
if b <= 0x7F {
return size, d[i+1:]
}
}
return size, nil
}
// applyBinaryDeltaAdd applies an add opcode in a delta-encoded binary
// fragment, returning the amount of data written and the usused part of the
// fragment. An add operation takes the form:
//
// [0xxxxxx][[data1]...]
//
// where the lower seven bits of the opcode is the number of data bytes
// following the opcode. See also pack-format.txt in the Git source.
func applyBinaryDeltaAdd(w io.Writer, op byte, delta []byte) (n int64, rest []byte, err error) {
size := int(op)
if len(delta) < size {
return 0, delta, errors.New("corrupt binary delta: incomplete add")
}
_, err = w.Write(delta[:size])
return int64(size), delta[size:], err
}
// applyBinaryDeltaCopy applies a copy opcode in a delta-encoded binary
// fragment, returing the amount of data written and the unused part of the
// fragment. A copy operation takes the form:
//
// [1xxxxxxx][offset1][offset2][offset3][offset4][size1][size2][size3]
//
// where the lower seven bits of the opcode determine which non-zero offset and
// size bytes are present in little-endian order: if bit 0 is set, offset1 is
// present, etc. If no offset or size bytes are present, offset is 0 and size
// is 0x10000. See also pack-format.txt in the Git source.
func applyBinaryDeltaCopy(w io.Writer, op byte, delta []byte, src io.ReaderAt) (n int64, rest []byte, err error) {
const defaultSize = 0x10000
unpack := func(start, bits uint) (v int64) {
for i := uint(0); i < bits; i++ {
mask := byte(1 << (i + start))
if op&mask > 0 {
if len(delta) == 0 {
err = errors.New("corrupt binary delta: incomplete copy")
return
}
v |= int64(delta[0]) << (8 * i)
delta = delta[1:]
}
}
return
}
offset := unpack(0, 4)
size := unpack(4, 3)
if err != nil {
return 0, delta, err
}
if size == 0 {
size = defaultSize
}
// TODO(bkeyes): consider pooling these buffers
b := make([]byte, size)
if _, err := src.ReadAt(b, offset); err != nil {
return 0, delta, err
}
_, err = w.Write(b)
return size, delta, err
}
func checkBinarySrcSize(r io.ReaderAt, size int64) error {
ok, err := isLen(r, size)
if err != nil {
return err
}
if !ok {
return &Conflict{"fragment src size does not match actual src size"}
}
return nil
}

View File

@ -0,0 +1,52 @@
package gitdiff
import (
"fmt"
)
var (
b85Table map[byte]byte
b85Alpha = []byte(
"0123456789" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "!#$%&()*+-;<=>?@^_`{|}~",
)
)
func init() {
b85Table = make(map[byte]byte)
for i, c := range b85Alpha {
b85Table[c] = byte(i)
}
}
// base85Decode decodes Base85-encoded data from src into dst. It uses the
// alphabet defined by base85.c in the Git source tree, which appears to be
// unique. src must contain at least len(dst) bytes of encoded data.
func base85Decode(dst, src []byte) error {
var v uint32
var n, ndst int
for i, b := range src {
if b, ok := b85Table[b]; ok {
v = 85*v + uint32(b)
n++
} else {
return fmt.Errorf("invalid base85 byte at index %d: 0x%X", i, src[i])
}
if n == 5 {
rem := len(dst) - ndst
for j := 0; j < 4 && j < rem; j++ {
dst[ndst] = byte(v >> 24)
ndst++
v <<= 8
}
v = 0
n = 0
}
}
if n > 0 {
return fmt.Errorf("base85 data terminated by underpadded sequence")
}
if ndst < len(dst) {
return fmt.Errorf("base85 data underrun: %d < %d", ndst, len(dst))
}
return nil
}

View File

@ -0,0 +1,179 @@
package gitdiff
import (
"bytes"
"compress/zlib"
"fmt"
"io"
"io/ioutil"
"strconv"
"strings"
)
func (p *parser) ParseBinaryFragments(f *File) (n int, err error) {
isBinary, hasData, err := p.ParseBinaryMarker()
if err != nil || !isBinary {
return 0, err
}
f.IsBinary = true
if !hasData {
return 0, nil
}
forward, err := p.ParseBinaryFragmentHeader()
if err != nil {
return 0, err
}
if forward == nil {
return 0, p.Errorf(0, "missing data for binary patch")
}
if err := p.ParseBinaryChunk(forward); err != nil {
return 0, err
}
f.BinaryFragment = forward
// valid for reverse to not exist, but it must be valid if present
reverse, err := p.ParseBinaryFragmentHeader()
if err != nil {
return 1, err
}
if reverse == nil {
return 1, nil
}
if err := p.ParseBinaryChunk(reverse); err != nil {
return 1, err
}
f.ReverseBinaryFragment = reverse
return 1, nil
}
func (p *parser) ParseBinaryMarker() (isBinary bool, hasData bool, err error) {
switch p.Line(0) {
case "GIT binary patch\n":
hasData = true
case "Binary files differ\n":
case "Files differ\n":
default:
return false, false, nil
}
if err = p.Next(); err != nil && err != io.EOF {
return false, false, err
}
return true, hasData, nil
}
func (p *parser) ParseBinaryFragmentHeader() (*BinaryFragment, error) {
parts := strings.SplitN(strings.TrimSuffix(p.Line(0), "\n"), " ", 2)
if len(parts) < 2 {
return nil, nil
}
frag := &BinaryFragment{}
switch parts[0] {
case "delta":
frag.Method = BinaryPatchDelta
case "literal":
frag.Method = BinaryPatchLiteral
default:
return nil, nil
}
var err error
if frag.Size, err = strconv.ParseInt(parts[1], 10, 64); err != nil {
nerr := err.(*strconv.NumError)
return nil, p.Errorf(0, "binary patch: invalid size: %v", nerr.Err)
}
if err := p.Next(); err != nil && err != io.EOF {
return nil, err
}
return frag, nil
}
func (p *parser) ParseBinaryChunk(frag *BinaryFragment) error {
// Binary fragments are encoded as a series of base85 encoded lines. Each
// line starts with a character in [A-Za-z] giving the number of bytes on
// the line, where A = 1 and z = 52, and ends with a newline character.
//
// The base85 encoding means each line is a multiple of 5 characters + 2
// additional characters for the length byte and the newline. The fragment
// ends with a blank line.
const (
shortestValidLine = "A00000\n"
maxBytesPerLine = 52
)
var data bytes.Buffer
buf := make([]byte, maxBytesPerLine)
for {
line := p.Line(0)
if line == "\n" {
break
}
if len(line) < len(shortestValidLine) || (len(line)-2)%5 != 0 {
return p.Errorf(0, "binary patch: corrupt data line")
}
byteCount, seq := int(line[0]), line[1:len(line)-1]
switch {
case 'A' <= byteCount && byteCount <= 'Z':
byteCount = byteCount - 'A' + 1
case 'a' <= byteCount && byteCount <= 'z':
byteCount = byteCount - 'a' + 27
default:
return p.Errorf(0, "binary patch: invalid length byte")
}
// base85 encodes every 4 bytes into 5 characters, with up to 3 bytes of end padding
maxByteCount := len(seq) / 5 * 4
if byteCount > maxByteCount || byteCount < maxByteCount-3 {
return p.Errorf(0, "binary patch: incorrect byte count")
}
if err := base85Decode(buf[:byteCount], []byte(seq)); err != nil {
return p.Errorf(0, "binary patch: %v", err)
}
data.Write(buf[:byteCount])
if err := p.Next(); err != nil {
if err == io.EOF {
return p.Errorf(0, "binary patch: unexpected EOF")
}
return err
}
}
if err := inflateBinaryChunk(frag, &data); err != nil {
return p.Errorf(0, "binary patch: %v", err)
}
// consume the empty line that ended the fragment
if err := p.Next(); err != nil && err != io.EOF {
return err
}
return nil
}
func inflateBinaryChunk(frag *BinaryFragment, r io.Reader) error {
zr, err := zlib.NewReader(r)
if err != nil {
return err
}
data, err := ioutil.ReadAll(zr)
if err != nil {
return err
}
if err := zr.Close(); err != nil {
return err
}
if int64(len(data)) != frag.Size {
return fmt.Errorf("%d byte fragment inflated to %d", frag.Size, len(data))
}
frag.Data = data
return nil
}

View File

@ -0,0 +1,470 @@
package gitdiff
import (
"fmt"
"io"
"os"
"strconv"
"strings"
"time"
)
const (
devNull = "/dev/null"
)
// ParseNextFileHeader finds and parses the next file header in the stream. If
// a header is found, it returns a file and all input before the header. It
// returns nil if no headers are found before the end of the input.
func (p *parser) ParseNextFileHeader() (*File, string, error) {
var preamble strings.Builder
var file *File
for {
// check for disconnected fragment headers (corrupt patch)
frag, err := p.ParseTextFragmentHeader()
if err != nil {
// not a valid header, nothing to worry about
goto NextLine
}
if frag != nil {
return nil, "", p.Errorf(-1, "patch fragment without file header: %s", frag.Header())
}
// check for a git-generated patch
file, err = p.ParseGitFileHeader()
if err != nil {
return nil, "", err
}
if file != nil {
return file, preamble.String(), nil
}
// check for a "traditional" patch
file, err = p.ParseTraditionalFileHeader()
if err != nil {
return nil, "", err
}
if file != nil {
return file, preamble.String(), nil
}
NextLine:
preamble.WriteString(p.Line(0))
if err := p.Next(); err != nil {
if err == io.EOF {
break
}
return nil, "", err
}
}
return nil, "", nil
}
func (p *parser) ParseGitFileHeader() (*File, error) {
const prefix = "diff --git "
if !strings.HasPrefix(p.Line(0), prefix) {
return nil, nil
}
header := p.Line(0)[len(prefix):]
defaultName, err := parseGitHeaderName(header)
if err != nil {
return nil, p.Errorf(0, "git file header: %v", err)
}
f := &File{}
for {
end, err := parseGitHeaderData(f, p.Line(1), defaultName)
if err != nil {
return nil, p.Errorf(1, "git file header: %v", err)
}
if err := p.Next(); err != nil {
if err == io.EOF {
break
}
return nil, err
}
if end {
break
}
}
if f.OldName == "" && f.NewName == "" {
if defaultName == "" {
return nil, p.Errorf(0, "git file header: missing filename information")
}
f.OldName = defaultName
f.NewName = defaultName
}
if (f.NewName == "" && !f.IsDelete) || (f.OldName == "" && !f.IsNew) {
return nil, p.Errorf(0, "git file header: missing filename information")
}
return f, nil
}
func (p *parser) ParseTraditionalFileHeader() (*File, error) {
const shortestValidFragHeader = "@@ -1 +1 @@\n"
const (
oldPrefix = "--- "
newPrefix = "+++ "
)
oldLine, newLine := p.Line(0), p.Line(1)
if !strings.HasPrefix(oldLine, oldPrefix) || !strings.HasPrefix(newLine, newPrefix) {
return nil, nil
}
// heuristic: only a file header if followed by a (probable) fragment header
if len(p.Line(2)) < len(shortestValidFragHeader) || !strings.HasPrefix(p.Line(2), "@@ -") {
return nil, nil
}
// advance past the first two lines so parser is after the header
// no EOF check needed because we know there are >=3 valid lines
if err := p.Next(); err != nil {
return nil, err
}
if err := p.Next(); err != nil {
return nil, err
}
oldName, _, err := parseName(oldLine[len(oldPrefix):], '\t', 0)
if err != nil {
return nil, p.Errorf(0, "file header: %v", err)
}
newName, _, err := parseName(newLine[len(newPrefix):], '\t', 0)
if err != nil {
return nil, p.Errorf(1, "file header: %v", err)
}
f := &File{}
switch {
case oldName == devNull || hasEpochTimestamp(oldLine):
f.IsNew = true
f.NewName = newName
case newName == devNull || hasEpochTimestamp(newLine):
f.IsDelete = true
f.OldName = oldName
default:
// if old name is a prefix of new name, use that instead
// this avoids picking variants like "file.bak" or "file~"
if strings.HasPrefix(newName, oldName) {
f.OldName = oldName
f.NewName = oldName
} else {
f.OldName = newName
f.NewName = newName
}
}
return f, nil
}
// parseGitHeaderName extracts a default file name from the Git file header
// line. This is required for mode-only changes and creation/deletion of empty
// files. Other types of patch include the file name(s) in the header data.
// If the names in the header do not match because the patch is a rename,
// return an empty default name.
func parseGitHeaderName(header string) (string, error) {
firstName, n, err := parseName(header, -1, 1)
if err != nil {
return "", err
}
if n < len(header) && (header[n] == ' ' || header[n] == '\t') {
n++
}
secondName, _, err := parseName(header[n:], -1, 1)
if err != nil {
return "", err
}
if firstName != secondName {
return "", nil
}
return firstName, nil
}
// parseGitHeaderData parses a single line of metadata from a Git file header.
// It returns true when header parsing is complete; in that case, line was the
// first line of non-header content.
func parseGitHeaderData(f *File, line, defaultName string) (end bool, err error) {
if len(line) > 0 && line[len(line)-1] == '\n' {
line = line[:len(line)-1]
}
for _, hdr := range []struct {
prefix string
end bool
parse func(*File, string, string) error
}{
{"@@ -", true, nil},
{"--- ", false, parseGitHeaderOldName},
{"+++ ", false, parseGitHeaderNewName},
{"old mode ", false, parseGitHeaderOldMode},
{"new mode ", false, parseGitHeaderNewMode},
{"deleted file mode ", false, parseGitHeaderDeletedMode},
{"new file mode ", false, parseGitHeaderCreatedMode},
{"copy from ", false, parseGitHeaderCopyFrom},
{"copy to ", false, parseGitHeaderCopyTo},
{"rename old ", false, parseGitHeaderRenameFrom},
{"rename new ", false, parseGitHeaderRenameTo},
{"rename from ", false, parseGitHeaderRenameFrom},
{"rename to ", false, parseGitHeaderRenameTo},
{"similarity index ", false, parseGitHeaderScore},
{"dissimilarity index ", false, parseGitHeaderScore},
{"index ", false, parseGitHeaderIndex},
} {
if strings.HasPrefix(line, hdr.prefix) {
if hdr.parse != nil {
err = hdr.parse(f, line[len(hdr.prefix):], defaultName)
}
return hdr.end, err
}
}
// unknown line indicates the end of the header
// this usually happens if the diff is empty
return true, nil
}
func parseGitHeaderOldName(f *File, line, defaultName string) error {
name, _, err := parseName(line, '\t', 1)
if err != nil {
return err
}
if f.OldName == "" && !f.IsNew {
f.OldName = name
return nil
}
return verifyGitHeaderName(name, f.OldName, f.IsNew, "old")
}
func parseGitHeaderNewName(f *File, line, defaultName string) error {
name, _, err := parseName(line, '\t', 1)
if err != nil {
return err
}
if f.NewName == "" && !f.IsDelete {
f.NewName = name
return nil
}
return verifyGitHeaderName(name, f.NewName, f.IsDelete, "new")
}
func parseGitHeaderOldMode(f *File, line, defaultName string) (err error) {
f.OldMode, err = parseMode(line)
return
}
func parseGitHeaderNewMode(f *File, line, defaultName string) (err error) {
f.NewMode, err = parseMode(line)
return
}
func parseGitHeaderDeletedMode(f *File, line, defaultName string) error {
f.IsDelete = true
f.OldName = defaultName
return parseGitHeaderOldMode(f, line, defaultName)
}
func parseGitHeaderCreatedMode(f *File, line, defaultName string) error {
f.IsNew = true
f.NewName = defaultName
return parseGitHeaderNewMode(f, line, defaultName)
}
func parseGitHeaderCopyFrom(f *File, line, defaultName string) (err error) {
f.IsCopy = true
f.OldName, _, err = parseName(line, -1, 0)
return
}
func parseGitHeaderCopyTo(f *File, line, defaultName string) (err error) {
f.IsCopy = true
f.NewName, _, err = parseName(line, -1, 0)
return
}
func parseGitHeaderRenameFrom(f *File, line, defaultName string) (err error) {
f.IsRename = true
f.OldName, _, err = parseName(line, -1, 0)
return
}
func parseGitHeaderRenameTo(f *File, line, defaultName string) (err error) {
f.IsRename = true
f.NewName, _, err = parseName(line, -1, 0)
return
}
func parseGitHeaderScore(f *File, line, defaultName string) error {
score, err := strconv.ParseInt(strings.TrimSuffix(line, "%"), 10, 32)
if err != nil {
nerr := err.(*strconv.NumError)
return fmt.Errorf("invalid score line: %v", nerr.Err)
}
if score <= 100 {
f.Score = int(score)
}
return nil
}
func parseGitHeaderIndex(f *File, line, defaultName string) error {
const sep = ".."
// note that git stops parsing if the OIDs are too long to be valid
// checking this requires knowing if the repository uses SHA1 or SHA256
// hashes, which we don't know, so we just skip that check
parts := strings.SplitN(line, " ", 2)
oids := strings.SplitN(parts[0], sep, 2)
if len(oids) < 2 {
return fmt.Errorf("invalid index line: missing %q", sep)
}
f.OldOIDPrefix, f.NewOIDPrefix = oids[0], oids[1]
if len(parts) > 1 {
return parseGitHeaderOldMode(f, parts[1], defaultName)
}
return nil
}
func parseMode(s string) (os.FileMode, error) {
mode, err := strconv.ParseInt(s, 8, 32)
if err != nil {
nerr := err.(*strconv.NumError)
return os.FileMode(0), fmt.Errorf("invalid mode line: %v", nerr.Err)
}
return os.FileMode(mode), nil
}
// parseName extracts a file name from the start of a string and returns the
// name and the index of the first character after the name. If the name is
// unquoted and term is non-negative, parsing stops at the first occurrence of
// term. Otherwise parsing of unquoted names stops at the first space or tab.
//
// If the name is exactly "/dev/null", no further processing occurs. Otherwise,
// if dropPrefix is greater than zero, that number of prefix components
// separated by forward slashes are dropped from the name and any duplicate
// slashes are collapsed.
func parseName(s string, term rune, dropPrefix int) (name string, n int, err error) {
if len(s) > 0 && s[0] == '"' {
name, n, err = parseQuotedName(s)
} else {
name, n, err = parseUnquotedName(s, term)
}
if err != nil {
return "", 0, err
}
if name == devNull {
return name, n, nil
}
return cleanName(name, dropPrefix), n, nil
}
func parseQuotedName(s string) (name string, n int, err error) {
for n = 1; n < len(s); n++ {
if s[n] == '"' && s[n-1] != '\\' {
n++
break
}
}
if n == 2 {
return "", 0, fmt.Errorf("missing name")
}
if name, err = strconv.Unquote(s[:n]); err != nil {
return "", 0, err
}
return name, n, err
}
func parseUnquotedName(s string, term rune) (name string, n int, err error) {
for n = 0; n < len(s); n++ {
if s[n] == '\n' {
break
}
if term >= 0 && rune(s[n]) == term {
break
}
if term < 0 && (s[n] == ' ' || s[n] == '\t') {
break
}
}
if n == 0 {
return "", 0, fmt.Errorf("missing name")
}
return s[:n], n, nil
}
// verifyGitHeaderName checks a parsed name against state set by previous lines
func verifyGitHeaderName(parsed, existing string, isNull bool, side string) error {
if existing != "" {
if isNull {
return fmt.Errorf("expected %s, but filename is set to %s", devNull, existing)
}
if existing != parsed {
return fmt.Errorf("inconsistent %s filename", side)
}
}
if isNull && parsed != devNull {
return fmt.Errorf("expected %s", devNull)
}
return nil
}
// cleanName removes double slashes and drops prefix segments.
func cleanName(name string, drop int) string {
var b strings.Builder
for i := 0; i < len(name); i++ {
if name[i] == '/' {
if i < len(name)-1 && name[i+1] == '/' {
continue
}
if drop > 0 {
drop--
b.Reset()
continue
}
}
b.WriteByte(name[i])
}
return b.String()
}
// hasEpochTimestamp returns true if the string ends with a POSIX-formatted
// timestamp for the UNIX epoch after a tab character. According to git, this
// is used by GNU diff to mark creations and deletions.
func hasEpochTimestamp(s string) bool {
const posixTimeLayout = "2006-01-02 15:04:05.9 -0700"
start := strings.IndexRune(s, '\t')
if start < 0 {
return false
}
ts := strings.TrimSuffix(s[start+1:], "\n")
// a valid timestamp can have optional ':' in zone specifier
// remove that if it exists so we have a single format
if ts[len(ts)-3] == ':' {
ts = ts[:len(ts)-3] + ts[len(ts)-2:]
}
t, err := time.Parse(posixTimeLayout, ts)
if err != nil {
return false
}
if !t.Equal(time.Unix(0, 0)) {
return false
}
return true
}

View File

@ -0,0 +1,199 @@
package gitdiff
import (
"errors"
"fmt"
"os"
)
// File describes changes to a single file. It can be either a text file or a
// binary file.
type File struct {
OldName string
NewName string
IsNew bool
IsDelete bool
IsCopy bool
IsRename bool
OldMode os.FileMode
NewMode os.FileMode
OldOIDPrefix string
NewOIDPrefix string
Score int
// TextFragments contains the fragments describing changes to a text file. It
// may be empty if the file is empty or if only the mode changes.
TextFragments []*TextFragment
// IsBinary is true if the file is a binary file. If the patch includes
// binary data, BinaryFragment will be non-nil and describe the changes to
// the data. If the patch is reversible, ReverseBinaryFragment will also be
// non-nil and describe the changes needed to restore the original file
// after applying the changes in BinaryFragment.
IsBinary bool
BinaryFragment *BinaryFragment
ReverseBinaryFragment *BinaryFragment
}
// TextFragment describes changed lines starting at a specific line in a text file.
type TextFragment struct {
Comment string
OldPosition int64
OldLines int64
NewPosition int64
NewLines int64
LinesAdded int64
LinesDeleted int64
LeadingContext int64
TrailingContext int64
Lines []Line
}
// Header returns the canonical header of this fragment.
func (f *TextFragment) Header() string {
return fmt.Sprintf("@@ -%d,%d +%d,%d @@ %s", f.OldPosition, f.OldLines, f.NewPosition, f.NewLines, f.Comment)
}
// Validate checks that the fragment is self-consistent and appliable. Validate
// returns an error if and only if the fragment is invalid.
func (f *TextFragment) Validate() error {
if f == nil {
return errors.New("nil fragment")
}
var (
oldLines, newLines int64
leadingContext, trailingContext int64
contextLines, addedLines, deletedLines int64
)