Merge remote-tracking branch 'GIT2GO/master' into master

This commit is contained in:
Michael Boulton 2020-08-18 14:20:08 +01:00
commit d6df76dca9
No known key found for this signature in database
GPG Key ID: 8A62CA0BE2E0197E
10 changed files with 687 additions and 24 deletions

53
.github/workflows/backport.yml vendored Normal file
View File

@ -0,0 +1,53 @@
name: Backport to older releases
on:
push:
branches:
- master
jobs:
backport:
strategy:
fail-fast: false
matrix:
branch: [ 'release-0.28', 'release-0.27' ]
name: Backport change to branch ${{ matrix.branch }}
runs-on: ubuntu-20.04
steps:
- name: Check out code
uses: actions/checkout@v1
with:
fetch-depth: 0
- name: Create a cherry-pick PR
run: |
if ! git diff --quiet HEAD^ HEAD -- vendor/libgit2; then
echo '::warning::Skipping cherry-pick since it is a vendored libgit2 bump'
exit 0
fi
BRANCH_NAME="cherry-pick-${{ github.run_id }}-${{ matrix.branch }}"
# Setup usernames and authentication
git config --global user.name "${{ github.actor }}"
git config --global user.email "${{ github.actor }}@users.noreply.github.com"
cat <<- EOF > $HOME/.netrc
machine github.com
login ${{ github.actor }}
password ${{ secrets.GITHUB_TOKEN }}
machine api.github.com
login ${{ github.actor }}
password ${{ secrets.GITHUB_TOKEN }}
EOF
chmod 600 $HOME/.netrc
# Create the cherry-pick commit and create the PR for it.
git checkout "${{ matrix.branch }}"
git switch -c "${BRANCH_NAME}"
git cherry-pick -x "${{ github.sha }}"
git push --set-upstream origin "${BRANCH_NAME}"
GITHUB_TOKEN="${{ secrets.GITHUB_TOKEN }}" gh pr create \
--base "${{ matrix.branch }}" \
--title "$(git --no-pager show --format="%s" --no-patch HEAD)" \
--body "$(git --no-pager show --format="%b" --no-patch HEAD)"

View File

@ -46,10 +46,10 @@ jobs:
strategy:
fail-fast: false
matrix:
go: [ '1.11', '1.12', '1.13' ]
go: [ '1.11', '1.12', '1.13', '1.14' ]
name: Go ${{ matrix.go }}
runs-on: ubuntu-18.04
runs-on: ubuntu-20.04
steps:
- name: Set up Go
@ -71,13 +71,13 @@ jobs:
fail-fast: false
name: Go (dynamic)
runs-on: ubuntu-18.04
runs-on: ubuntu-20.04
steps:
- name: Set up Go
uses: actions/setup-go@v1
with:
go-version: '1.13'
go-version: '1.14'
id: go
- name: Check out code into the Go module directory
uses: actions/checkout@v1
@ -87,3 +87,47 @@ jobs:
make build-libgit2-dynamic
- name: Test
run: make test-dynamic
build-system-dynamic:
strategy:
fail-fast: false
name: Go (system-wide, dynamic)
runs-on: ubuntu-20.04
steps:
- name: Set up Go
uses: actions/setup-go@v1
with:
go-version: '1.14'
id: go
- name: Check out code into the Go module directory
uses: actions/checkout@v1
- name: Build libgit2
run: |
git submodule update --init
sudo ./script/build-libgit2.sh --dynamic --system
- name: Test
run: make test
build-system-static:
strategy:
fail-fast: false
name: Go (system-wide, static)
runs-on: ubuntu-20.04
steps:
- name: Set up Go
uses: actions/setup-go@v1
with:
go-version: '1.14'
id: go
- name: Check out code into the Go module directory
uses: actions/checkout@v1
- name: Build libgit2
run: |
git submodule update --init
sudo ./script/build-libgit2.sh --static --system
- name: Test
run: go test --count=1 --tags "static,system_libgit2" ./...

28
.github/workflows/tag.yml vendored Normal file
View File

@ -0,0 +1,28 @@
name: Tag new releases
on:
push:
branches:
- master
- release-*
jobs:
tag-release:
name: Bump tag in ${{ github.ref }}
runs-on: ubuntu-20.04
steps:
- name: Check out code
uses: actions/checkout@v1
with:
fetch-depth: 0
- name: Bump version and push tag
id: bump-version
uses: anothrNick/github-tag-action@9aaabdb5e989894e95288328d8b17a6347217ae3
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
WITH_V: true
DEFAULT_BUMP: patch
TAG_CONTEXT: branch
RELEASE_BRANCHES: .*

View File

@ -31,7 +31,7 @@ The `master` branch follows the tip of libgit2 itself (with some lag) and as suc
### Which branch to send Pull requests to
If there's something version-specific that you'd want to contribute to, you can send them to the `release-${MAJOR}-${MINOR}` branches, which follow libgit2's releases.
If there's something version-specific that you'd want to contribute to, you can send them to the `release-${MAJOR}.${MINOR}` branches, which follow libgit2's releases.
Installing
----------

174
diff.go
View File

@ -3,6 +3,7 @@ package git
/*
#include <git2.h>
extern void _go_git_populate_apply_cb(git_apply_options *options);
extern int _go_git_diff_foreach(git_diff *diff, int eachFile, int eachHunk, int eachLine, void *payload);
extern void _go_git_setup_diff_notify_callbacks(git_diff_options* opts);
extern int _go_git_diff_blobs(git_blob *old, const char *old_path, git_blob *new, const char *new_path, git_diff_options *opts, int eachFile, int eachHunk, int eachLine, void *payload);
@ -550,7 +551,7 @@ const (
DiffFindRemoveUnmodified DiffFindOptionsFlag = C.GIT_DIFF_FIND_REMOVE_UNMODIFIED
)
//TODO implement git_diff_similarity_metric
// TODO implement git_diff_similarity_metric
type DiffFindOptions struct {
Flags DiffFindOptionsFlag
RenameThreshold uint16
@ -847,3 +848,174 @@ func DiffBlobs(oldBlob *Blob, oldAsPath string, newBlob *Blob, newAsPath string,
return nil
}
// ApplyHunkCallback is a callback that will be made per delta (file) when applying a patch.
type ApplyHunkCallback func(*DiffHunk) (apply bool, err error)
// ApplyDeltaCallback is a callback that will be made per hunk when applying a patch.
type ApplyDeltaCallback func(*DiffDelta) (apply bool, err error)
// ApplyOptions has 2 callbacks that are called for hunks or deltas
// If these functions return an error, abort the apply process immediately.
// If the first return value is true, the delta/hunk will be applied. If it is false, the delta/hunk will not be applied. In either case, the rest of the apply process will continue.
type ApplyOptions struct {
ApplyHunkCallback ApplyHunkCallback
ApplyDeltaCallback ApplyDeltaCallback
Flags uint
}
//export hunkApplyCallback
func hunkApplyCallback(_hunk *C.git_diff_hunk, _payload unsafe.Pointer) C.int {
opts, ok := pointerHandles.Get(_payload).(*ApplyOptions)
if !ok {
panic("invalid apply options payload")
}
if opts.ApplyHunkCallback == nil {
return 0
}
hunk := diffHunkFromC(_hunk)
apply, err := opts.ApplyHunkCallback(&hunk)
if err != nil {
if gitError, ok := err.(*GitError); ok {
return C.int(gitError.Code)
}
return -1
} else if apply {
return 0
} else {
return 1
}
}
//export deltaApplyCallback
func deltaApplyCallback(_delta *C.git_diff_delta, _payload unsafe.Pointer) C.int {
opts, ok := pointerHandles.Get(_payload).(*ApplyOptions)
if !ok {
panic("invalid apply options payload")
}
if opts.ApplyDeltaCallback == nil {
return 0
}
delta := diffDeltaFromC(_delta)
apply, err := opts.ApplyDeltaCallback(&delta)
if err != nil {
if gitError, ok := err.(*GitError); ok {
return C.int(gitError.Code)
}
return -1
} else if apply {
return 0
} else {
return 1
}
}
// DefaultApplyOptions returns default options for applying diffs or patches.
func DefaultApplyOptions() (*ApplyOptions, error) {
opts := C.git_apply_options{}
runtime.LockOSThread()
defer runtime.UnlockOSThread()
ecode := C.git_apply_options_init(&opts, C.GIT_APPLY_OPTIONS_VERSION)
if int(ecode) != 0 {
return nil, MakeGitError(ecode)
}
return applyOptionsFromC(&opts), nil
}
func (a *ApplyOptions) toC() *C.git_apply_options {
if a == nil {
return nil
}
opts := &C.git_apply_options{
version: C.GIT_APPLY_OPTIONS_VERSION,
flags: C.uint(a.Flags),
}
if a.ApplyDeltaCallback != nil || a.ApplyHunkCallback != nil {
C._go_git_populate_apply_cb(opts)
opts.payload = pointerHandles.Track(a)
}
return opts
}
func applyOptionsFromC(opts *C.git_apply_options) *ApplyOptions {
return &ApplyOptions{
Flags: uint(opts.flags),
}
}
// ApplyLocation represents the possible application locations for applying
// diffs.
type ApplyLocation int
const (
// ApplyLocationWorkdir applies the patch to the workdir, leaving the
// index untouched. This is the equivalent of `git apply` with no location
// argument.
ApplyLocationWorkdir ApplyLocation = C.GIT_APPLY_LOCATION_WORKDIR
// ApplyLocationIndex applies the patch to the index, leaving the working
// directory untouched. This is the equivalent of `git apply --cached`.
ApplyLocationIndex ApplyLocation = C.GIT_APPLY_LOCATION_INDEX
// ApplyLocationBoth applies the patch to both the working directory and
// the index. This is the equivalent of `git apply --index`.
ApplyLocationBoth ApplyLocation = C.GIT_APPLY_LOCATION_BOTH
)
// ApplyDiff appllies a Diff to the given repository, making changes directly
// in the working directory, the index, or both.
func (v *Repository) ApplyDiff(diff *Diff, location ApplyLocation, opts *ApplyOptions) error {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
cOpts := opts.toC()
ecode := C.git_apply(v.ptr, diff.ptr, C.git_apply_location_t(location), cOpts)
runtime.KeepAlive(v)
runtime.KeepAlive(diff)
runtime.KeepAlive(cOpts)
if ecode < 0 {
return MakeGitError(ecode)
}
return nil
}
// DiffFromBuffer reads the contents of a git patch file into a Diff object.
//
// The diff object produced is similar to the one that would be produced if you
// actually produced it computationally by comparing two trees, however there
// may be subtle differences. For example, a patch file likely contains
// abbreviated object IDs, so the object IDs in a git_diff_delta produced by
// this function will also be abbreviated.
//
// This function will only read patch files created by a git implementation, it
// will not read unified diffs produced by the diff program, nor any other
// types of patch files.
func DiffFromBuffer(buffer []byte, repo *Repository) (*Diff, error) {
var diff *C.git_diff
cBuffer := C.CBytes(buffer)
defer C.free(unsafe.Pointer(cBuffer))
runtime.LockOSThread()
defer runtime.UnlockOSThread()
ecode := C.git_diff_from_buffer(&diff, (*C.char)(cBuffer), C.size_t(len(buffer)))
if ecode < 0 {
return nil, MakeGitError(ecode)
}
runtime.KeepAlive(diff)
return newDiffFromC(diff, repo), nil
}

View File

@ -2,6 +2,9 @@ package git
import (
"errors"
"fmt"
"io/ioutil"
"path"
"strings"
"testing"
)
@ -236,3 +239,307 @@ func TestDiffBlobs(t *testing.T) {
t.Fatalf("Bad number of lines iterated")
}
}
func TestApplyDiffAddfile(t *testing.T) {
repo := createTestRepo(t)
defer cleanupTestRepo(t, repo)
seedTestRepo(t, repo)
addFirstFileCommit, addFileTree := addAndGetTree(t, repo, "file1", `hello`)
addSecondFileCommit, addSecondFileTree := addAndGetTree(t, repo, "file2", `hello2`)
diff, err := repo.DiffTreeToTree(addFileTree, addSecondFileTree, nil)
checkFatal(t, err)
t.Run("check does not apply to current tree because file exists", func(t *testing.T) {
err = repo.ResetToCommit(addSecondFileCommit, ResetHard, &CheckoutOpts{})
checkFatal(t, err)
err = repo.ApplyDiff(diff, ApplyLocationBoth, nil)
if err == nil {
t.Error("expecting applying patch to current repo to fail")
}
})
t.Run("check apply to correct commit", func(t *testing.T) {
err = repo.ResetToCommit(addFirstFileCommit, ResetHard, &CheckoutOpts{})
checkFatal(t, err)
err = repo.ApplyDiff(diff, ApplyLocationBoth, nil)
checkFatal(t, err)
t.Run("Check that diff only changed one file", func(t *testing.T) {
checkSecondFileStaged(t, repo)
index, err := repo.Index()
checkFatal(t, err)
defer index.Free()
newTreeOID, err := index.WriteTreeTo(repo)
checkFatal(t, err)
newTree, err := repo.LookupTree(newTreeOID)
checkFatal(t, err)
defer newTree.Free()
_, err = repo.CreateCommit("HEAD", signature(), signature(), fmt.Sprintf("patch apply"), newTree, addFirstFileCommit)
checkFatal(t, err)
})
t.Run("test applying patch produced the same diff", func(t *testing.T) {
head, err := repo.Head()
checkFatal(t, err)
commit, err := repo.LookupCommit(head.Target())
checkFatal(t, err)
tree, err := commit.Tree()
checkFatal(t, err)
newDiff, err := repo.DiffTreeToTree(addFileTree, tree, nil)
checkFatal(t, err)
raw1b, err := diff.ToBuf(DiffFormatPatch)
checkFatal(t, err)
raw2b, err := newDiff.ToBuf(DiffFormatPatch)
checkFatal(t, err)
raw1 := string(raw1b)
raw2 := string(raw2b)
if raw1 != raw2 {
t.Error("diffs should be the same")
}
})
})
t.Run("check convert to raw buffer and apply", func(t *testing.T) {
err = repo.ResetToCommit(addFirstFileCommit, ResetHard, &CheckoutOpts{})
checkFatal(t, err)
raw, err := diff.ToBuf(DiffFormatPatch)
checkFatal(t, err)
if len(raw) == 0 {
t.Error("empty diff created")
}
diff2, err := DiffFromBuffer(raw, repo)
checkFatal(t, err)
err = repo.ApplyDiff(diff2, ApplyLocationBoth, nil)
checkFatal(t, err)
})
t.Run("check apply callbacks work", func(t *testing.T) {
// reset the state and get new default options for test
resetAndGetOpts := func(t *testing.T) *ApplyOptions {
err = repo.ResetToCommit(addFirstFileCommit, ResetHard, &CheckoutOpts{})
checkFatal(t, err)
opts, err := DefaultApplyOptions()
checkFatal(t, err)
return opts
}
t.Run("Check hunk callback working applies patch", func(t *testing.T) {
opts := resetAndGetOpts(t)
called := false
opts.ApplyHunkCallback = func(hunk *DiffHunk) (apply bool, err error) {
called = true
return true, nil
}
err = repo.ApplyDiff(diff, ApplyLocationBoth, opts)
checkFatal(t, err)
if called == false {
t.Error("apply hunk callback was not called")
}
checkSecondFileStaged(t, repo)
})
t.Run("Check delta callback working applies patch", func(t *testing.T) {
opts := resetAndGetOpts(t)
called := false
opts.ApplyDeltaCallback = func(hunk *DiffDelta) (apply bool, err error) {
if hunk.NewFile.Path != "file2" {
t.Error("Unexpected delta in diff application")
}
called = true
return true, nil
}
err = repo.ApplyDiff(diff, ApplyLocationBoth, opts)
checkFatal(t, err)
if called == false {
t.Error("apply hunk callback was not called")
}
checkSecondFileStaged(t, repo)
})
t.Run("Check delta callback returning false does not apply patch", func(t *testing.T) {
opts := resetAndGetOpts(t)
called := false
opts.ApplyDeltaCallback = func(hunk *DiffDelta) (apply bool, err error) {
if hunk.NewFile.Path != "file2" {
t.Error("Unexpected hunk in diff application")
}
called = true
return false, nil
}
err = repo.ApplyDiff(diff, ApplyLocationBoth, opts)
checkFatal(t, err)
if called == false {
t.Error("apply hunk callback was not called")
}
checkNoFilesStaged(t, repo)
})
t.Run("Check hunk callback returning causes application to fail", func(t *testing.T) {
opts := resetAndGetOpts(t)
called := false
opts.ApplyHunkCallback = func(hunk *DiffHunk) (apply bool, err error) {
called = true
return false, errors.New("something happened")
}
err = repo.ApplyDiff(diff, ApplyLocationBoth, opts)
if err == nil {
t.Error("expected an error after trying to apply")
}
if called == false {
t.Error("apply hunk callback was not called")
}
checkNoFilesStaged(t, repo)
})
t.Run("Check delta callback returning causes application to fail", func(t *testing.T) {
opts := resetAndGetOpts(t)
called := false
opts.ApplyDeltaCallback = func(hunk *DiffDelta) (apply bool, err error) {
if hunk.NewFile.Path != "file2" {
t.Error("Unexpected delta in diff application")
}
called = true
return false, errors.New("something happened")
}
err = repo.ApplyDiff(diff, ApplyLocationBoth, opts)
if err == nil {
t.Error("expected an error after trying to apply")
}
if called == false {
t.Error("apply hunk callback was not called")
}
checkNoFilesStaged(t, repo)
})
})
}
// checkSecondFileStaged checks that there is a single file called "file2" uncommitted in the repo
func checkSecondFileStaged(t *testing.T, repo *Repository) {
opts := StatusOptions{
Show: StatusShowIndexAndWorkdir,
Flags: StatusOptIncludeUntracked,
}
statuses, err := repo.StatusList(&opts)
checkFatal(t, err)
count, err := statuses.EntryCount()
checkFatal(t, err)
if count != 1 {
t.Error("diff should affect exactly one file")
}
if count == 0 {
t.Fatal("no statuses, cannot continue test")
}
entry, err := statuses.ByIndex(0)
checkFatal(t, err)
if entry.Status != StatusIndexNew {
t.Error("status should be 'new' as file has been added between commits")
}
if entry.HeadToIndex.NewFile.Path != "file2" {
t.Error("new file should be 'file2")
}
return
}
// checkNoFilesStaged checks that there is a single file called "file2" uncommitted in the repo
func checkNoFilesStaged(t *testing.T, repo *Repository) {
opts := StatusOptions{
Show: StatusShowIndexAndWorkdir,
Flags: StatusOptIncludeUntracked,
}
statuses, err := repo.StatusList(&opts)
checkFatal(t, err)
count, err := statuses.EntryCount()
checkFatal(t, err)
if count != 0 {
t.Error("files changed unexpectedly")
}
}
// addAndGetTree creates a file and commits it, returning the commit and tree
func addAndGetTree(t *testing.T, repo *Repository, filename string, content string) (*Commit, *Tree) {
headCommit, err := headCommit(repo)
checkFatal(t, err)
defer headCommit.Free()
p := repo.Path()
p = strings.TrimSuffix(p, ".git")
p = strings.TrimSuffix(p, ".git/")
err = ioutil.WriteFile(path.Join(p, filename), []byte((content)), 0777)
checkFatal(t, err)
index, err := repo.Index()
checkFatal(t, err)
defer index.Free()
err = index.AddByPath(filename)
checkFatal(t, err)
newTreeOID, err := index.WriteTreeTo(repo)
checkFatal(t, err)
newTree, err := repo.LookupTree(newTreeOID)
checkFatal(t, err)
defer newTree.Free()
commitId, err := repo.CreateCommit("HEAD", signature(), signature(), fmt.Sprintf("add %s", filename), newTree, headCommit)
checkFatal(t, err)
commit, err := repo.LookupCommit(commitId)
checkFatal(t, err)
tree, err := commit.Tree()
checkFatal(t, err)
return commit, tree
}

View File

@ -6,33 +6,54 @@
set -e
if [ "$#" -eq "0" ]; then
echo "Usage: $0 <--dynamic|--static>">&2
usage() {
echo "Usage: $0 <--dynamic|--static> [--system]">&2
exit 1
}
if [ "$#" -eq "0" ]; then
usage
fi
ROOT="$(cd "$(dirname "$0")/.." && echo "${PWD}")"
VENDORED_PATH="${ROOT}/vendor/libgit2"
BUILD_SYSTEM=OFF
case "$1" in
--static)
BUILD_PATH="${ROOT}/static-build"
BUILD_SHARED_LIBS=OFF
;;
while [ $# -gt 0 ]; do
case "$1" in
--static)
BUILD_PATH="${ROOT}/static-build"
BUILD_SHARED_LIBS=OFF
;;
--dynamic)
BUILD_PATH="${ROOT}/dynamic-build"
BUILD_SHARED_LIBS=ON
;;
--dynamic)
BUILD_PATH="${ROOT}/dynamic-build"
BUILD_SHARED_LIBS=ON
;;
*)
echo "Usage: $0 <--dynamic|--static>">&2
exit 1
;;
esac
--system)
BUILD_SYSTEM=ON
;;
mkdir -p "${BUILD_PATH}/build" "${BUILD_PATH}/install/lib"
*)
usage
;;
esac
shift
done
if [ -z "${BUILD_SHARED_LIBS}" ]; then
usage
fi
if [ "${BUILD_SYSTEM}" = "ON" ]; then
BUILD_INSTALL_PREFIX="/usr"
else
BUILD_INSTALL_PREFIX="${BUILD_PATH}/install"
mkdir -p "${BUILD_PATH}/install/lib"
fi
mkdir -p "${BUILD_PATH}/build" &&
cd "${BUILD_PATH}/build" &&
cmake -DTHREADSAFE=ON \
-DBUILD_CLAR=OFF \
@ -40,7 +61,7 @@ cmake -DTHREADSAFE=ON \
-DREGEX_BACKEND=builtin \
-DCMAKE_C_FLAGS=-fPIC \
-DCMAKE_BUILD_TYPE="RelWithDebInfo" \
-DCMAKE_INSTALL_PREFIX="${BUILD_PATH}/install" \
-DCMAKE_INSTALL_PREFIX="${BUILD_INSTALL_PREFIX}" \
-DCMAKE_INSTALL_LIBDIR="lib" \
"${VENDORED_PATH}" &&

View File

@ -6,6 +6,7 @@ package git
import "C"
import (
"errors"
"runtime"
"unsafe"
)
@ -86,6 +87,9 @@ func (statusList *StatusList) ByIndex(index int) (StatusEntry, error) {
return StatusEntry{}, ErrInvalid
}
ptr := C.git_status_byindex(statusList.ptr, C.size_t(index))
if ptr == nil {
return StatusEntry{}, errors.New("index out of Bounds")
}
entry := statusEntryFromC(ptr)
runtime.KeepAlive(statusList)

View File

@ -61,3 +61,31 @@ func TestStatusList(t *testing.T) {
t.Fatal("Incorrect entry path: ", entry.IndexToWorkdir.NewFile.Path)
}
}
func TestStatusNothing(t *testing.T) {
t.Parallel()
repo := createTestRepo(t)
defer cleanupTestRepo(t, repo)
seedTestRepo(t, repo)
opts := &StatusOptions{
Show: StatusShowIndexAndWorkdir,
Flags: StatusOptIncludeUntracked | StatusOptRenamesHeadToIndex | StatusOptSortCaseSensitively,
}
statusList, err := repo.StatusList(opts)
checkFatal(t, err)
entryCount, err := statusList.EntryCount()
checkFatal(t, err)
if entryCount != 0 {
t.Fatal("expected no statuses in empty repo")
}
_, err = statusList.ByIndex(0)
if err == nil {
t.Error("expected error getting status by index")
}
}

View File

@ -6,6 +6,12 @@
typedef int (*gogit_submodule_cbk)(git_submodule *sm, const char *name, void *payload);
void _go_git_populate_apply_cb(git_apply_options *options)
{
options->delta_cb = (git_apply_delta_cb)deltaApplyCallback;
options->hunk_cb = (git_apply_hunk_cb)hunkApplyCallback;
}
void _go_git_buf_fill_null(git_buf *buf)
{
memset(buf->ptr, '\0', buf->asize*sizeof(char));