diff --git a/CHANGES.md b/CHANGES.md index 3669bd560..1c35dddd5 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,6 +8,45 @@ - CI server: - @libgit2sharp: +## v0.15.0 - ([diff](https://github.com/libgit2/libgit2sharp/compare/v0.14.1...v0.15.0)) + +### Additions + + - Introduce ObjectDatabase.Archive() + - Introduce Repository.Blame() + - Introduce ObjectDatabase.CalculateHistoryDivergence() + - Add Configuration.Find(regexp) + - Add CommitFilter.FirstParentOnly + - Expose Configuration.BuildSignature() + - Add TreeDefinition.Add(string, TreeEntry) + - Make Remote expose its refspecs + +### Changes + + - Make Network.Fetch() accepts optional refspec + - Extend Network.Fetch() and ListReferences() to allow downloading from a url + - Allow Network.Push() to control packbuilder parallelism + - Expose Network.Push() progress reporting + - Extend RemoteUpdater to allow updation of refspecs + - Teach Index.RetrieveStatus to detect renames in index and workdir + - Teach NoteCollection to optionally build a Signature from configuration + - Add RewriteHistoryOptions.OnSucceeding and OnError + - Introduce Blob FilteringOptions + - Rename Blob.ContentAsText() as Blob.GetContentText() + - Rename Blob.ContentStream() as Blob.GetContentStream() + - Deprecate Blob.Content + - Teach Diff.Compare<> to detect renames and copies + - Split Patch and TreeChanges generation + - Deprecate ResetOptions in favor of ResetMode. + - Simplify OdbBackend.ReadPrefix() implementation + - Deprecate ObjectId.StartsWith(byte[], int) in favor of ObjectId.StartsWith(string) + - Update libgit2 binaries to libgit2/libgit2@96fb6a6 + +### Fixes + + - Fix building with Mono on OS X (#557) + - Make RetrieveStatus() reload on-disk index beforehand (#322 and #519) + ## v0.14.1 - ([diff](https://github.com/libgit2/libgit2sharp/compare/v0.14.0...v0.14.1)) ### Changes diff --git a/Lib/NativeBinaries/amd64/git2-11f8336.dll b/Lib/NativeBinaries/amd64/git2-11f8336.dll deleted file mode 100644 index d06ebea12..000000000 Binary files a/Lib/NativeBinaries/amd64/git2-11f8336.dll and /dev/null differ diff --git a/Lib/NativeBinaries/amd64/git2-11f8336.pdb b/Lib/NativeBinaries/amd64/git2-11f8336.pdb deleted file mode 100644 index 34989493f..000000000 Binary files a/Lib/NativeBinaries/amd64/git2-11f8336.pdb and /dev/null differ diff --git a/Lib/NativeBinaries/amd64/git2-65e9dc6.dll b/Lib/NativeBinaries/amd64/git2-65e9dc6.dll new file mode 100644 index 000000000..d548c861e Binary files /dev/null and b/Lib/NativeBinaries/amd64/git2-65e9dc6.dll differ diff --git a/Lib/NativeBinaries/amd64/git2-65e9dc6.pdb b/Lib/NativeBinaries/amd64/git2-65e9dc6.pdb new file mode 100644 index 000000000..4f3ebff6a Binary files /dev/null and b/Lib/NativeBinaries/amd64/git2-65e9dc6.pdb differ diff --git a/Lib/NativeBinaries/x86/git2-11f8336.dll b/Lib/NativeBinaries/x86/git2-11f8336.dll deleted file mode 100644 index 7d66ec112..000000000 Binary files a/Lib/NativeBinaries/x86/git2-11f8336.dll and /dev/null differ diff --git a/Lib/NativeBinaries/x86/git2-11f8336.pdb b/Lib/NativeBinaries/x86/git2-11f8336.pdb deleted file mode 100644 index def46eba0..000000000 Binary files a/Lib/NativeBinaries/x86/git2-11f8336.pdb and /dev/null differ diff --git a/Lib/NativeBinaries/x86/git2-65e9dc6.dll b/Lib/NativeBinaries/x86/git2-65e9dc6.dll new file mode 100644 index 000000000..040f87c75 Binary files /dev/null and b/Lib/NativeBinaries/x86/git2-65e9dc6.dll differ diff --git a/Lib/NativeBinaries/x86/git2-65e9dc6.pdb b/Lib/NativeBinaries/x86/git2-65e9dc6.pdb new file mode 100644 index 000000000..1653a478e Binary files /dev/null and b/Lib/NativeBinaries/x86/git2-65e9dc6.pdb differ diff --git a/LibGit2Sharp.Tests/ArchiveFixture.cs b/LibGit2Sharp.Tests/ArchiveFixture.cs new file mode 100644 index 000000000..e070db85d --- /dev/null +++ b/LibGit2Sharp.Tests/ArchiveFixture.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections; +using System.IO; +using LibGit2Sharp.Tests.TestHelpers; +using Xunit; + +namespace LibGit2Sharp.Tests +{ + public class ArchiveFixture : BaseFixture + { + [Fact] + public void CanArchiveATree() + { + using (var repo = new Repository(BareTestRepoPath)) + { + var tree = repo.Lookup("581f9824ecaf824221bd36edf5430f2739a7c4f5"); + + var archiver = new MockArchiver(); + + repo.ObjectDatabase.Archive(tree, archiver); + + var expected = new ArrayList + { + new { Path = "1", Sha = "7f76480d939dc401415927ea7ef25c676b8ddb8f" }, + new { Path = Path.Combine("1", "branch_file.txt"), Sha = "45b983be36b73c0788dc9cbcb76cbb80fc7bb057" }, + new { Path = "README", Sha = "a8233120f6ad708f843d861ce2b7228ec4e3dec6" }, + new { Path = "branch_file.txt", Sha = "45b983be36b73c0788dc9cbcb76cbb80fc7bb057" }, + new { Path = "new.txt", Sha = "a71586c1dfe8a71c6cbf6c129f404c5642ff31bd" }, + }; + Assert.Equal(expected, archiver.Files); + Assert.Null(archiver.ReceivedCommitSha); + Assert.InRange(archiver.ModificationTime, DateTimeOffset.UtcNow.Subtract(TimeSpan.FromMilliseconds(100)), DateTimeOffset.UtcNow); + } + } + + [Fact] + public void CanArchiveACommit() + { + using (var repo = new Repository(BareTestRepoPath)) + { + var commit = repo.Lookup("4c062a6361ae6959e06292c1fa5e2822d9c96345"); + + var archiver = new MockArchiver(); + + repo.ObjectDatabase.Archive(commit, archiver); + + var expected = new ArrayList + { + new { Path = "1", Sha = "7f76480d939dc401415927ea7ef25c676b8ddb8f" }, + new { Path = Path.Combine("1", "branch_file.txt"), Sha = "45b983be36b73c0788dc9cbcb76cbb80fc7bb057" }, + new { Path = "README", Sha = "a8233120f6ad708f843d861ce2b7228ec4e3dec6" }, + new { Path = "branch_file.txt", Sha = "45b983be36b73c0788dc9cbcb76cbb80fc7bb057" }, + new { Path = "new.txt", Sha = "a71586c1dfe8a71c6cbf6c129f404c5642ff31bd" }, + }; + Assert.Equal(expected, archiver.Files); + Assert.Equal(commit.Sha, archiver.ReceivedCommitSha); + Assert.Equal(commit.Committer.When, archiver.ModificationTime); + } + } + + [Fact] + public void ArchivingANullTreeOrCommitThrows() + { + using (var repo = new Repository(BareTestRepoPath)) + { + Assert.Throws(() => repo.ObjectDatabase.Archive((Commit)null, null)); + Assert.Throws(() => repo.ObjectDatabase.Archive((Tree)null, null)); + } + } + + #region MockArchiver + + private class MockArchiver : ArchiverBase + { + public readonly ArrayList Files = new ArrayList(); + public string ReceivedCommitSha; + public DateTimeOffset ModificationTime; + + #region Overrides of ArchiverBase + + public override void BeforeArchiving(Tree tree, ObjectId oid, DateTimeOffset modificationTime) + { + if (oid != null) + { + ReceivedCommitSha = oid.Sha; + } + ModificationTime = modificationTime; + } + + protected override void AddTreeEntry(string path, TreeEntry entry, DateTimeOffset modificationTime) + { + Files.Add(new { Path = path, entry.Target.Sha }); + } + + #endregion + } + + #endregion + } +} diff --git a/LibGit2Sharp.Tests/ArchiveTarFixture.cs b/LibGit2Sharp.Tests/ArchiveTarFixture.cs new file mode 100644 index 000000000..a59b23604 --- /dev/null +++ b/LibGit2Sharp.Tests/ArchiveTarFixture.cs @@ -0,0 +1,45 @@ +using System; +using System.IO; +using System.Text; +using LibGit2Sharp.Tests.TestHelpers; +using Xunit; + +namespace LibGit2Sharp.Tests +{ + public class ArchiveTarFixture : BaseFixture + { + [Fact] + public void CanArchiveACommitWithDirectoryAsTar() + { + var path = CloneBareTestRepo(); + using (var repo = new Repository(path)) + { + // This tests generates an archive of the bare test repo, and compares it with + // a pre-generated tar file. The expected tar file has been generated with the + // crlf filter active (on windows), so we need to make sure that even if the test + // is launched on linux then the files content has the crlf filter applied (not + // active by default). + var sb = new StringBuilder(); + sb.Append("* text eol=crlf\n"); + Touch(Path.Combine(repo.Info.Path, "info"), "attributes", sb.ToString()); + + var commit = repo.Lookup("4c062a6361ae6959e06292c1fa5e2822d9c96345"); + + var scd = BuildSelfCleaningDirectory(); + var archivePath = Path.Combine(scd.RootedDirectoryPath, Guid.NewGuid() + ".tar"); + Directory.CreateDirectory(scd.RootedDirectoryPath); + + repo.ObjectDatabase.Archive(commit, archivePath); + + using (var expectedStream = new StreamReader(Path.Combine(ResourcesDirectory.FullName, "expected_archives/commit_with_directory.tar"))) + using (var actualStream = new StreamReader(archivePath)) + { + string expected = expectedStream.ReadToEnd(); + string actual = actualStream.ReadToEnd(); + + Assert.Equal(expected, actual); + } + } + } + } +} diff --git a/LibGit2Sharp.Tests/AttributesFixture.cs b/LibGit2Sharp.Tests/AttributesFixture.cs index b9c2ba2f5..5092518b3 100644 --- a/LibGit2Sharp.Tests/AttributesFixture.cs +++ b/LibGit2Sharp.Tests/AttributesFixture.cs @@ -20,7 +20,7 @@ public void StagingHonorsTheAttributesFiles() } } - private static void AssertNormalization(Repository repo, string filename, bool shouldHaveBeenNormalized, string expectedSha) + private static void AssertNormalization(IRepository repo, string filename, bool shouldHaveBeenNormalized, string expectedSha) { var sb = new StringBuilder(); sb.Append("I'm going to be dynamically processed\r\n"); @@ -40,10 +40,10 @@ private static void AssertNormalization(Repository repo, string filename, bool s var blob = repo.Lookup(entry.Id); Assert.NotNull(blob); - Assert.Equal(!shouldHaveBeenNormalized, blob.ContentAsText().Contains("\r")); + Assert.Equal(!shouldHaveBeenNormalized, blob.GetContentText().Contains("\r")); } - private static void CreateAttributesFile(Repository repo) + private static void CreateAttributesFile(IRepository repo) { var sb = new StringBuilder(); sb.Append("* text=auto\n"); diff --git a/LibGit2Sharp.Tests/BlameFixture.cs b/LibGit2Sharp.Tests/BlameFixture.cs new file mode 100644 index 000000000..b0fcf77fe --- /dev/null +++ b/LibGit2Sharp.Tests/BlameFixture.cs @@ -0,0 +1,81 @@ +using System; +using System.Linq; +using LibGit2Sharp.Tests.TestHelpers; +using Xunit; + +namespace LibGit2Sharp.Tests +{ + public class BlameFixture : BaseFixture + { + private static void AssertCorrectHeadBlame(BlameHunkCollection blame) + { + Assert.Equal(1, blame.Count()); + Assert.Equal(0, blame[0].FinalStartLineNumber); + Assert.Equal("schacon@gmail.com", blame[0].FinalSignature.Email); + Assert.Equal("4a202b3", blame[0].FinalCommit.Id.ToString(7)); + + Assert.Equal(0, blame.HunkForLine(0).FinalStartLineNumber); + Assert.Equal("schacon@gmail.com", blame.HunkForLine(0).FinalSignature.Email); + Assert.Equal("4a202b3", blame.HunkForLine(0).FinalCommit.Id.ToString(7)); + } + + [Fact] + public void CanBlameSimply() + { + using (var repo = new Repository(BareTestRepoPath)) + { + AssertCorrectHeadBlame(repo.Blame("README")); + } + } + + [Fact] + public void CanBlameFromADifferentCommit() + { + using (var repo = new Repository(MergedTestRepoWorkingDirPath)) + { + // File doesn't exist at HEAD + Assert.Throws(() => repo.Blame("ancestor-only.txt")); + + var blame = repo.Blame("ancestor-only.txt", new BlameOptions { StartingAt = "9107b30" }); + Assert.Equal(1, blame.Count()); + } + } + + [Fact] + public void ValidatesLimits() + { + using (var repo = new Repository(BareTestRepoPath)) + { + var blame = repo.Blame("README"); + + Assert.Throws(() => blame[1]); + Assert.Throws(() => blame.HunkForLine(2)); + } + } + + [Fact] + public void CanBlameFromVariousTypes() + { + using (var repo = new Repository(BareTestRepoPath)) + { + AssertCorrectHeadBlame(repo.Blame("README", new BlameOptions {StartingAt = "HEAD" })); + AssertCorrectHeadBlame(repo.Blame("README", new BlameOptions {StartingAt = repo.Head })); + AssertCorrectHeadBlame(repo.Blame("README", new BlameOptions {StartingAt = repo.Head.Tip })); + AssertCorrectHeadBlame(repo.Blame("README", new BlameOptions {StartingAt = repo.Branches["master"]})); + } + } + + [Fact] + public void CanStopBlame() + { + using (var repo = new Repository(BareTestRepoPath)) + { + // $ git blame .\new.txt + // 9fd738e8 (Scott Chacon 2010-05-24 10:19:19 -0700 1) my new file + // (be3563a comes after 9fd738e8) + var blame = repo.Blame("new.txt", new BlameOptions {StoppingAt = "be3563a"}); + Assert.True(blame[0].FinalCommit.Sha.StartsWith("be3563a")); + } + } + } +} diff --git a/LibGit2Sharp.Tests/BlobFixture.cs b/LibGit2Sharp.Tests/BlobFixture.cs index 6d97a588d..765a0e7fa 100644 --- a/LibGit2Sharp.Tests/BlobFixture.cs +++ b/LibGit2Sharp.Tests/BlobFixture.cs @@ -1,4 +1,5 @@ -using System.IO; +using System; +using System.IO; using System.Linq; using System.Text; using LibGit2Sharp.Tests.TestHelpers; @@ -16,12 +17,33 @@ public void CanGetBlobAsText() { var blob = repo.Lookup("a8233120f6ad708f843d861ce2b7228ec4e3dec6"); - var text = blob.ContentAsText(); + var text = blob.GetContentText(); Assert.Equal("hey there\n", text); } } + [SkippableTheory] + [InlineData("false", "hey there\n")] + [InlineData("input", "hey there\n")] + [InlineData("true", "hey there\r\n")] + public void CanGetBlobAsFilteredText(string autocrlf, string expectedText) + { + SkipIfNotSupported(autocrlf); + + var path = CloneBareTestRepo(); + using (var repo = new Repository(path)) + { + repo.Config.Set("core.autocrlf", autocrlf); + + var blob = repo.Lookup("a8233120f6ad708f843d861ce2b7228ec4e3dec6"); + + var text = blob.GetContentText(new FilteringOptions("foo.txt")); + + Assert.Equal(expectedText, text); + } + } + [Theory] [InlineData("ascii", 4, "31 32 33 34")] [InlineData("utf-7", 4, "31 32 33 34")] @@ -45,15 +67,19 @@ public void CanGetBlobAsTextWithVariousEncodings(string encodingName, int expect var commit = repo.Commit("bom", Constants.Signature, Constants.Signature); var blob = (Blob)commit.Tree[bomFile].Target; - Assert.Equal(expectedContentBytes, blob.Content.Length); + Assert.Equal(expectedContentBytes, blob.Size); + using (var stream = blob.GetContentStream()) + { + Assert.Equal(expectedContentBytes, stream.Length); + } - var textDetected = blob.ContentAsText(); + var textDetected = blob.GetContentText(); Assert.Equal(content, textDetected); - var text = blob.ContentAsText(encoding); + var text = blob.GetContentText(encoding); Assert.Equal(content, text); - var utf7Chars = blob.ContentAsText(Encoding.UTF7).Select(c => ((int)c).ToString("X2")).ToArray(); + var utf7Chars = blob.GetContentText(Encoding.UTF7).Select(c => ((int)c).ToString("X2")).ToArray(); Assert.Equal(expectedUtf7Chars, string.Join(" ", utf7Chars)); } } @@ -79,44 +105,68 @@ public void CanLookUpBlob() } [Fact] - public void CanReadBlobContent() + public void CanReadBlobStream() { using (var repo = new Repository(BareTestRepoPath)) { var blob = repo.Lookup("a8233120f6ad708f843d861ce2b7228ec4e3dec6"); - byte[] bytes = blob.Content; - Assert.Equal(10, bytes.Length); - string content = Encoding.UTF8.GetString(bytes); - Assert.Equal("hey there\n", content); + var contentStream = blob.GetContentStream(); + Assert.Equal(blob.Size, contentStream.Length); + + using (var tr = new StreamReader(contentStream, Encoding.UTF8)) + { + string content = tr.ReadToEnd(); + Assert.Equal("hey there\n", content); + } } } - [Fact] - public void CanReadBlobStream() + [SkippableTheory] + [InlineData("false", "hey there\n")] + [InlineData("input", "hey there\n")] + [InlineData("true", "hey there\r\n")] + public void CanReadBlobFilteredStream(string autocrlf, string expectedContent) { - using (var repo = new Repository(BareTestRepoPath)) + SkipIfNotSupported(autocrlf); + + var path = CloneBareTestRepo(); + using (var repo = new Repository(path)) { + repo.Config.Set("core.autocrlf", autocrlf); + var blob = repo.Lookup("a8233120f6ad708f843d861ce2b7228ec4e3dec6"); - using (var tr = new StreamReader(blob.ContentStream, Encoding.UTF8)) + var contentStream = blob.GetContentStream(new FilteringOptions("foo.txt")); + Assert.Equal(expectedContent.Length, contentStream.Length); + + using (var tr = new StreamReader(contentStream, Encoding.UTF8)) { string content = tr.ReadToEnd(); - Assert.Equal("hey there\n", content); + + Assert.Equal(expectedContent, content); } } } - public static void CopyStream(Stream input, Stream output) + [Fact] + public void CanReadBlobFilteredStreamOfUnmodifiedBinary() { - // Reused from the following Stack Overflow post with permission - // of Jon Skeet (obtained on 25 Feb 2013) - // http://stackoverflow.com/questions/411592/how-do-i-save-a-stream-to-a-file/411605#411605 - var buffer = new byte[8*1024]; - int len; - while ((len = input.Read(buffer, 0, buffer.Length)) > 0) + var binaryContent = new byte[] { 0, 1, 2, 3, 4, 5 }; + + string path = CloneBareTestRepo(); + using (var repo = new Repository(path)) { - output.Write(buffer, 0, len); + using (var stream = new MemoryStream(binaryContent)) + { + Blob blob = repo.ObjectDatabase.CreateBlob(stream); + + using (var filtered = blob.GetContentStream(new FilteringOptions("foo.txt"))) + { + Assert.Equal(blob.Size, filtered.Length); + Assert.True(StreamEquals(stream, filtered)); + } + } } } @@ -143,7 +193,7 @@ public void CanStageAFileGeneratedFromABlobContentStream() var blob = repo.Lookup(entry.Id.Sha); - using (Stream stream = blob.ContentStream) + using (Stream stream = blob.GetContentStream()) using (Stream file = File.OpenWrite(Path.Combine(repo.Info.WorkingDirectory, "small.fromblob.txt"))) { CopyStream(stream, file); @@ -165,5 +215,11 @@ public void CanTellIfTheBlobContentLooksLikeBinary() Assert.Equal(false, blob.IsBinary); } } + + private static void SkipIfNotSupported(string autocrlf) + { + InconclusiveIf(() => autocrlf == "true" && IsRunningOnLinux(), "Non-Windows does not support core.autocrlf = true"); + InconclusiveIf(() => autocrlf == "input" && !IsRunningOnLinux(), "Windows does not support core.autocrlf = input"); + } } } diff --git a/LibGit2Sharp.Tests/BranchFixture.cs b/LibGit2Sharp.Tests/BranchFixture.cs index 3290f7523..63fcced78 100644 --- a/LibGit2Sharp.Tests/BranchFixture.cs +++ b/LibGit2Sharp.Tests/BranchFixture.cs @@ -30,7 +30,13 @@ public void CanCreateBranch(string name) Assert.Equal("refs/heads/" + name, newBranch.CanonicalName); Assert.NotNull(newBranch.Tip); Assert.Equal(committish, newBranch.Tip.Sha); - Assert.NotNull(repo.Branches.SingleOrDefault(p => p.Name == name)); + + // Note the call to String.Normalize(). This is because, on Mac OS X, the filesystem + // decomposes the UTF-8 characters on write, which results in a different set of bytes + // when they're read back: + // - from InlineData: C5-00-6E-00-67-00-73-00-74-00-72-00-F6-00-6D-00 + // - from filesystem: 41-00-0A-03-6E-00-67-00-73-00-74-00-72-00-6F-00-08-03-6D-00 + Assert.NotNull(repo.Branches.SingleOrDefault(p => p.Name.Normalize() == name)); AssertRefLogEntry(repo, newBranch.CanonicalName, newBranch.Tip.Id, @@ -42,6 +48,40 @@ public void CanCreateBranch(string name) } } + [Fact] + public void CanCreateAnUnbornBranch() + { + string path = CloneStandardTestRepo(); + using (var repo = new Repository(path)) + { + // No branch named orphan + Assert.Null(repo.Branches["orphan"]); + + // HEAD doesn't point to an unborn branch + Assert.False(repo.Info.IsHeadUnborn); + + // Let's move the HEAD to this branch to be created + repo.Refs.UpdateTarget("HEAD", "refs/heads/orphan"); + Assert.True(repo.Info.IsHeadUnborn); + + // The branch still doesn't exist + Assert.Null(repo.Branches["orphan"]); + + // Create a commit against HEAD + Commit c = repo.Commit("New initial root commit", Constants.Signature, Constants.Signature); + + // Ensure this commit has no parent + Assert.Equal(0, c.Parents.Count()); + + // The branch now exists... + Branch orphan = repo.Branches["orphan"]; + Assert.NotNull(orphan); + + // ...and points to that newly created commit + Assert.Equal(c, orphan.Tip); + } + } + [Fact] public void CanCreateBranchUsingAbbreviatedSha() { @@ -416,6 +456,7 @@ public void LookingOutABranchByNameWithBadParamsThrows() } } + [Fact] public void CanGetInformationFromUnbornBranch() { string repoPath = InitNewRepository(true); @@ -858,7 +899,7 @@ public void DetachedHeadIsNotATrackingBranch() string path = CloneStandardTestRepo(); using (var repo = new Repository(path)) { - repo.Reset(ResetOptions.Hard); + repo.Reset(ResetMode.Hard); repo.RemoveUntrackedFiles(); string headSha = repo.Head.Tip.Sha; diff --git a/LibGit2Sharp.Tests/CheckoutFixture.cs b/LibGit2Sharp.Tests/CheckoutFixture.cs index 9db8d36f8..f987bf6ea 100644 --- a/LibGit2Sharp.Tests/CheckoutFixture.cs +++ b/LibGit2Sharp.Tests/CheckoutFixture.cs @@ -1017,7 +1017,7 @@ public void CanCheckoutPathFromCurrentBranch(string fileName) /// a single file and two branches. /// /// Repository to populate - private void PopulateBasicRepository(Repository repo) + private void PopulateBasicRepository(IRepository repo) { // Generate a .gitignore file. string gitIgnoreFilePath = Touch(repo.Info.WorkingDirectory, ".gitignore", "bin"); @@ -1036,10 +1036,10 @@ private void PopulateBasicRepository(Repository repo) /// working directory matches the current Head commit. /// /// Repository whose current working directory should be operated on. - private void ResetAndCleanWorkingDirectory(Repository repo) + private void ResetAndCleanWorkingDirectory(IRepository repo) { // Reset the index and the working tree. - repo.Reset(ResetOptions.Hard); + repo.Reset(ResetMode.Hard); // Clean the working directory. repo.RemoveUntrackedFiles(); diff --git a/LibGit2Sharp.Tests/CloneFixture.cs b/LibGit2Sharp.Tests/CloneFixture.cs index 800ccc2f8..10e010348 100644 --- a/LibGit2Sharp.Tests/CloneFixture.cs +++ b/LibGit2Sharp.Tests/CloneFixture.cs @@ -116,7 +116,7 @@ public void CallsProgressCallbacks(string url) var scd = BuildSelfCleaningDirectory(); Repository.Clone(url, scd.DirectoryPath, - onTransferProgress: _ => { transferWasCalled = true; return 0; }, + onTransferProgress: _ => { transferWasCalled = true; return true; }, onCheckoutProgress: (a, b, c) => checkoutWasCalled = true); Assert.True(transferWasCalled); diff --git a/LibGit2Sharp.Tests/CommitAncestorFixture.cs b/LibGit2Sharp.Tests/CommitAncestorFixture.cs index c4d07a64e..9543e0f82 100644 --- a/LibGit2Sharp.Tests/CommitAncestorFixture.cs +++ b/LibGit2Sharp.Tests/CommitAncestorFixture.cs @@ -108,7 +108,7 @@ public void CannotFindCommonAncestorForSeveralCommmitsWithoutCommonAncestor() } } - private static Commit CreateOrphanedCommit(Repository repo) + private static Commit CreateOrphanedCommit(IRepository repo) { Commit random = repo.Head.Tip; diff --git a/LibGit2Sharp.Tests/CommitFixture.cs b/LibGit2Sharp.Tests/CommitFixture.cs index 66c89a895..f060ee0cc 100644 --- a/LibGit2Sharp.Tests/CommitFixture.cs +++ b/LibGit2Sharp.Tests/CommitFixture.cs @@ -29,7 +29,7 @@ public void CanCorrectlyCountCommitsWhenSwitchingToAnotherBranch() using (var repo = new Repository(path)) { // Hard reset and then remove untracked files - repo.Reset(ResetOptions.Hard); + repo.Reset(ResetMode.Hard); repo.RemoveUntrackedFiles(); repo.Checkout("test"); @@ -176,6 +176,18 @@ public void CanEnumerateCommitsWithReverseTopoSorting() } } + [Fact] + public void CanSimplifyByFirstParent() + { + AssertEnumerationOfCommits( + repo => new CommitFilter { Since = repo.Head, FirstParentOnly = true }, + new[] + { + "4c062a6", "be3563a", "9fd738e", + "4a202b3", "5b5b025", "8496071", + }); + } + [Fact] public void CanGetParentsCount() { @@ -246,7 +258,7 @@ public void CanEnumerateFromDetachedHead() using (var repoClone = new Repository(path)) { // Hard reset and then remove untracked files - repoClone.Reset(ResetOptions.Hard); + repoClone.Reset(ResetMode.Hard); repoClone.RemoveUntrackedFiles(); string headSha = repoClone.Head.Tip.Sha; @@ -400,7 +412,7 @@ public void CanEnumerateCommitsFromATagWhichPointsToATree() } } - private static void AssertEnumerationOfCommits(Func filterBuilder, IEnumerable abbrevIds) + private static void AssertEnumerationOfCommits(Func filterBuilder, IEnumerable abbrevIds) { using (var repo = new Repository(BareTestRepoPath)) { @@ -408,7 +420,7 @@ private static void AssertEnumerationOfCommits(Func fi } } - private static void AssertEnumerationOfCommitsInRepo(Repository repo, Func filterBuilder, IEnumerable abbrevIds) + private static void AssertEnumerationOfCommitsInRepo(IRepository repo, Func filterBuilder, IEnumerable abbrevIds) { ICommitLog commits = repo.Commits.QueryBy(filterBuilder(repo)); @@ -480,7 +492,7 @@ public void CanDirectlyAccessABlobOfTheCommit() var blob = commit["1/branch_file.txt"].Target as Blob; Assert.NotNull(blob); - Assert.Equal("hi\n", blob.ContentAsText()); + Assert.Equal("hi\n", blob.GetContentText()); } } @@ -507,20 +519,19 @@ public void DirectlyAccessingAnUnknownTreeEntryOfTheCommitReturnsNull() } } - [SkippableFact] + [Fact] public void CanCommitWithSignatureFromConfig() { string repoPath = InitNewRepository(); + string configPath = CreateConfigurationWithDummyUser(Constants.Signature); + var options = new RepositoryOptions { GlobalConfigurationLocation = configPath }; - using (var repo = new Repository(repoPath)) + using (var repo = new Repository(repoPath, options)) { string dir = repo.Info.Path; Assert.True(Path.IsPathRooted(dir)); Assert.True(Directory.Exists(dir)); - InconclusiveIf(() => !repo.Config.HasConfig(ConfigurationLevel.Global), - "No Git global configuration available"); - const string relativeFilepath = "new.txt"; string filePath = Touch(repo.Info.WorkingDirectory, relativeFilepath, "null"); repo.Index.Stage(relativeFilepath); @@ -535,12 +546,7 @@ public void CanCommitWithSignatureFromConfig() AssertBlobContent(repo.Head[relativeFilepath], "nulltoken\n"); AssertBlobContent(commit[relativeFilepath], "nulltoken\n"); - var name = repo.Config.Get("user.name"); - var email = repo.Config.Get("user.email"); - Assert.Equal(commit.Author.Name, name.Value); - Assert.Equal(commit.Author.Email, email.Value); - Assert.Equal(commit.Committer.Name, name.Value); - Assert.Equal(commit.Committer.Email, email.Value); + AssertCommitSignaturesAre(commit, Constants.Signature); } } @@ -550,7 +556,7 @@ public void CommitParentsAreMergeHeads() string path = CloneStandardTestRepo(); using (var repo = new Repository(path)) { - repo.Reset(ResetOptions.Hard, "c47800"); + repo.Reset(ResetMode.Hard, "c47800"); CreateAndStageANewFile(repo); @@ -694,7 +700,7 @@ public void CanCommitALittleBit() private static void AssertBlobContent(TreeEntry entry, string expectedContent) { Assert.Equal(TreeEntryTargetType.Blob, entry.TargetType); - Assert.Equal(expectedContent, ((Blob)(entry.Target)).ContentAsText()); + Assert.Equal(expectedContent, ((Blob)(entry.Target)).GetContentText()); } private static void AddCommitToRepo(string path) @@ -764,7 +770,7 @@ public void CanAmendACommitWithMoreThanOneParent() Assert.NotNull(mergedCommit); Assert.Equal(2, mergedCommit.Parents.Count()); - repo.Reset(ResetOptions.Soft, mergedCommit.Sha); + repo.Reset(ResetMode.Soft, mergedCommit.Sha); CreateAndStageANewFile(repo); const string commitMessage = "I'm rewriting the history!"; @@ -781,14 +787,14 @@ public void CanAmendACommitWithMoreThanOneParent() } } - private static void CreateAndStageANewFile(Repository repo) + private static void CreateAndStageANewFile(IRepository repo) { string relativeFilepath = string.Format("new-file-{0}.txt", Guid.NewGuid()); Touch(repo.Info.WorkingDirectory, relativeFilepath, "brand new content\n"); repo.Index.Stage(relativeFilepath); } - private static void AssertCommitHasBeenAmended(Repository repo, Commit amendedCommit, Commit originalCommit) + private static void AssertCommitHasBeenAmended(IRepository repo, Commit amendedCommit, Commit originalCommit) { Commit headCommit = repo.Head.Tip; Assert.Equal(amendedCommit, headCommit); diff --git a/LibGit2Sharp.Tests/ConfigurationFixture.cs b/LibGit2Sharp.Tests/ConfigurationFixture.cs index 8783cc4be..0e7774617 100644 --- a/LibGit2Sharp.Tests/ConfigurationFixture.cs +++ b/LibGit2Sharp.Tests/ConfigurationFixture.cs @@ -186,17 +186,17 @@ public void CanReadStringValue() } } - [SkippableFact] + [Fact] public void CanEnumerateGlobalConfig() { - using (var repo = new Repository(StandardTestRepoPath)) - { - InconclusiveIf(() => !repo.Config.HasConfig(ConfigurationLevel.Global), - "No Git global configuration available"); + string configPath = CreateConfigurationWithDummyUser(Constants.Signature); + var options = new RepositoryOptions { GlobalConfigurationLocation = configPath }; + using (var repo = new Repository(StandardTestRepoPath, options)) + { var entry = repo.Config.FirstOrDefault>(e => e.Key == "user.name"); Assert.NotNull(entry); - Assert.NotNull(entry.Value); + Assert.Equal(Constants.Signature.Name, entry.Value); } } @@ -223,6 +223,35 @@ public void CanEnumerateLocalConfigContainingAKeyWithNoValue() } } + [Fact] + public void CanFindInLocalConfig() + { + using (var repo = new Repository(StandardTestRepoPath)) + { + var matches = repo.Config.Find("unit"); + + Assert.NotNull(matches); + Assert.Equal(new[] { "unittests.intsetting", "unittests.longsetting" }, + matches.Select(m => m.Key).ToArray()); + } + } + + [Fact] + public void CanFindInGlobalConfig() + { + string configPath = CreateConfigurationWithDummyUser(Constants.Signature); + var options = new RepositoryOptions { GlobalConfigurationLocation = configPath }; + + using (var repo = new Repository(StandardTestRepoPath, options)) + { + var matches = repo.Config.Find(@"\.name", ConfigurationLevel.Global); + + Assert.NotNull(matches); + Assert.Equal(new[] { "user.name" }, + matches.Select(m => m.Key).ToArray()); + } + } + [Fact] public void CanSetBooleanValue() { diff --git a/LibGit2Sharp.Tests/ConflictFixture.cs b/LibGit2Sharp.Tests/ConflictFixture.cs index 5e6c3e7dd..deec945c6 100644 --- a/LibGit2Sharp.Tests/ConflictFixture.cs +++ b/LibGit2Sharp.Tests/ConflictFixture.cs @@ -13,7 +13,7 @@ public static IEnumerable ConflictData { get { - return new[] + return new List { new[] { "ancestor-and-ours.txt", "5dee68477001f447f50fa7ee7e6a818370b5c2fb", "dad0664ae617d36e464ec08ed969ff496432b075", null }, new[] { "ancestor-and-theirs.txt", "3aafd4d0bac33cc3c78c4c070f3966fb6e6f641a", null, "7b26cd5ac0ee68483ae4d5e1e00b064547ea8c9b" }, diff --git a/LibGit2Sharp.Tests/DiffBlobToBlobFixture.cs b/LibGit2Sharp.Tests/DiffBlobToBlobFixture.cs index 15bfc5f87..c0711a468 100644 --- a/LibGit2Sharp.Tests/DiffBlobToBlobFixture.cs +++ b/LibGit2Sharp.Tests/DiffBlobToBlobFixture.cs @@ -60,7 +60,7 @@ public void CanCompareTwoVersionsOfABlobWithADiffOfTwoHunks() } } - Blob CreateBinaryBlob(Repository repo) + Blob CreateBinaryBlob(IRepository repo) { string fullpath = Path.Combine(repo.Info.WorkingDirectory, "binary.bin"); diff --git a/LibGit2Sharp.Tests/DiffTreeToTargetFixture.cs b/LibGit2Sharp.Tests/DiffTreeToTargetFixture.cs index 37b3f669f..d58508ca8 100644 --- a/LibGit2Sharp.Tests/DiffTreeToTargetFixture.cs +++ b/LibGit2Sharp.Tests/DiffTreeToTargetFixture.cs @@ -8,7 +8,7 @@ namespace LibGit2Sharp.Tests { public class DiffTreeToTargetFixture : BaseFixture { - private static void SetUpSimpleDiffContext(Repository repo) + private static void SetUpSimpleDiffContext(IRepository repo) { var fullpath = Touch(repo.Info.WorkingDirectory, "file.txt", "hello\n"); @@ -43,9 +43,12 @@ public void CanCompareASimpleTreeAgainstTheWorkDir() { SetUpSimpleDiffContext(repo); - TreeChanges changes = repo.Diff.Compare(repo.Head.Tip.Tree, + var changes = repo.Diff.Compare(repo.Head.Tip.Tree, DiffTargets.WorkingDirectory); + Assert.Equal(1, changes.Modified.Count()); + var patch = repo.Diff.Compare(repo.Head.Tip.Tree, + DiffTargets.WorkingDirectory); var expected = new StringBuilder() .Append("diff --git a/file.txt b/file.txt\n") .Append("index ce01362..4f125e3 100644\n") @@ -56,7 +59,7 @@ public void CanCompareASimpleTreeAgainstTheWorkDir() .Append("+world\n") .Append("+!!!\n"); - Assert.Equal(expected.ToString(), changes.Patch); + Assert.Equal(expected.ToString(), patch); } } @@ -67,7 +70,7 @@ public void CanCompareAMoreComplexTreeAgainstTheWorkdir() { Tree tree = repo.Head.Tip.Tree; - TreeChanges changes = repo.Diff.Compare(tree, DiffTargets.WorkingDirectory); + var changes = repo.Diff.Compare(tree, DiffTargets.WorkingDirectory); Assert.NotNull(changes); Assert.Equal(6, changes.Count()); @@ -103,9 +106,12 @@ public void CanCompareASimpleTreeAgainstTheWorkDirAndTheIndex() { SetUpSimpleDiffContext(repo); - TreeChanges changes = repo.Diff.Compare(repo.Head.Tip.Tree, + var changes = repo.Diff.Compare(repo.Head.Tip.Tree, DiffTargets.Index | DiffTargets.WorkingDirectory); + Assert.Equal(1, changes.Modified.Count()); + var patch = repo.Diff.Compare(repo.Head.Tip.Tree, + DiffTargets.Index | DiffTargets.WorkingDirectory); var expected = new StringBuilder() .Append("diff --git a/file.txt b/file.txt\n") .Append("index ce01362..4f125e3 100644\n") @@ -116,7 +122,7 @@ public void CanCompareASimpleTreeAgainstTheWorkDirAndTheIndex() .Append("+world\n") .Append("+!!!\n"); - Assert.Equal(expected.ToString(), changes.Patch); + Assert.Equal(expected.ToString(), patch); } } @@ -158,8 +164,13 @@ public void ShowcaseTheDifferenceBetweenTheTwoKindOfComparison() FileStatus state = repo.Index.RetrieveStatus("file.txt"); Assert.Equal(FileStatus.Removed | FileStatus.Untracked, state); + var wrkDirToIdxToTree = repo.Diff.Compare(repo.Head.Tip.Tree, + DiffTargets.Index | DiffTargets.WorkingDirectory); + + Assert.Equal(1, wrkDirToIdxToTree.Deleted.Count()); + Assert.Equal(0, wrkDirToIdxToTree.Modified.Count()); - TreeChanges wrkDirToIdxToTree = repo.Diff.Compare(repo.Head.Tip.Tree, + var patch = repo.Diff.Compare(repo.Head.Tip.Tree, DiffTargets.Index | DiffTargets.WorkingDirectory); var expected = new StringBuilder() .Append("diff --git a/file.txt b/file.txt\n") @@ -170,9 +181,15 @@ public void ShowcaseTheDifferenceBetweenTheTwoKindOfComparison() .Append("@@ -1 +0,0 @@\n") .Append("-hello\n"); - Assert.Equal(expected.ToString(), wrkDirToIdxToTree.Patch); + Assert.Equal(expected.ToString(), patch); - TreeChanges wrkDirToTree = repo.Diff.Compare(repo.Head.Tip.Tree, + var wrkDirToTree = repo.Diff.Compare(repo.Head.Tip.Tree, + DiffTargets.WorkingDirectory); + + Assert.Equal(0, wrkDirToTree.Deleted.Count()); + Assert.Equal(1, wrkDirToTree.Modified.Count()); + + patch = repo.Diff.Compare(repo.Head.Tip.Tree, DiffTargets.WorkingDirectory); expected = new StringBuilder() .Append("diff --git a/file.txt b/file.txt\n") @@ -184,7 +201,7 @@ public void ShowcaseTheDifferenceBetweenTheTwoKindOfComparison() .Append("+world\n") .Append("+!!!\n"); - Assert.Equal(expected.ToString(), wrkDirToTree.Patch); + Assert.Equal(expected.ToString(), patch); } } @@ -207,9 +224,12 @@ public void CanCompareASimpleTreeAgainstTheIndex() { SetUpSimpleDiffContext(repo); - TreeChanges changes = repo.Diff.Compare(repo.Head.Tip.Tree, + var changes = repo.Diff.Compare(repo.Head.Tip.Tree, DiffTargets.Index); + Assert.Equal(1, changes.Modified.Count()); + var patch = repo.Diff.Compare(repo.Head.Tip.Tree, + DiffTargets.Index); var expected = new StringBuilder() .Append("diff --git a/file.txt b/file.txt\n") .Append("index ce01362..94954ab 100644\n") @@ -219,7 +239,7 @@ public void CanCompareASimpleTreeAgainstTheIndex() .Append(" hello\n") .Append("+world\n"); - Assert.Equal(expected.ToString(), changes.Patch); + Assert.Equal(expected.ToString(), patch); } } @@ -254,7 +274,7 @@ public void CanCompareAMoreComplexTreeAgainstTheIndex() { Tree tree = repo.Head.Tip.Tree; - TreeChanges changes = repo.Diff.Compare(tree, DiffTargets.Index); + var changes = repo.Diff.Compare(tree, DiffTargets.Index); Assert.NotNull(changes); Assert.Equal(3, changes.Count()); @@ -281,7 +301,7 @@ public void CanCompareASubsetofTheTreeAgainstTheIndex() { Tree tree = repo.Head.Tip.Tree; - TreeChanges changes = repo.Diff.Compare(tree, DiffTargets.Index, + var changes = repo.Diff.Compare(tree, DiffTargets.Index, new[] { "deleted_staged_file.txt", "1/branch_file.txt" }); Assert.NotNull(changes); @@ -305,11 +325,11 @@ public void CanCompareASubsetofTheTreeAgainstTheIndexWithLaxExplicitPathsValidat { Tree tree = repo.Head.Tip.Tree; - TreeChanges changes = repo.Diff.Compare(tree, DiffTargets.Index, + var changes = repo.Diff.Compare(tree, DiffTargets.Index, new[] { "deleted_staged_file.txt", "1/branch_file.txt", "I-do/not-exist" }, new ExplicitPathsOptions { ShouldFailOnUnmatchedPath = false }); AssertCanCompareASubsetOfTheTreeAgainstTheIndex(changes); - changes = repo.Diff.Compare(tree, DiffTargets.Index, + changes = repo.Diff.Compare(tree, DiffTargets.Index, new[] { "deleted_staged_file.txt", "1/branch_file.txt", "I-do/not-exist" }); AssertCanCompareASubsetOfTheTreeAgainstTheIndex(changes); } @@ -322,7 +342,7 @@ public void ComparingASubsetofTheTreeAgainstTheIndexWithStrictExplicitPathsValid { Tree tree = repo.Head.Tip.Tree; - Assert.Throws(() => repo.Diff.Compare(tree, DiffTargets.Index, + Assert.Throws(() => repo.Diff.Compare(tree, DiffTargets.Index, new[] { "deleted_staged_file.txt", "1/branch_file.txt", "I-do/not-exist" }, new ExplicitPathsOptions())); } } @@ -359,11 +379,10 @@ public void CanCopeWithEndOfFileNewlineChanges() File.AppendAllText(fullpath, "\n"); repo.Index.Stage("file.txt"); - TreeChanges changes = repo.Diff.Compare(repo.Head.Tip.Tree, DiffTargets.Index); + var changes = repo.Diff.Compare(repo.Head.Tip.Tree, DiffTargets.Index); Assert.Equal(1, changes.Modified.Count()); - Assert.Equal(1, changes.LinesAdded); - Assert.Equal(1, changes.LinesDeleted); + var patch = repo.Diff.Compare(repo.Head.Tip.Tree, DiffTargets.Index); var expected = new StringBuilder() .Append("diff --git a/file.txt b/file.txt\n") .Append("index 2e65efe..7898192 100644\n") @@ -374,7 +393,9 @@ public void CanCopeWithEndOfFileNewlineChanges() .Append("\\ No newline at end of file\n") .Append("+a\n"); - Assert.Equal(expected.ToString(), changes.Patch); + Assert.Equal(expected.ToString(), patch); + Assert.Equal(1, patch.LinesAdded); + Assert.Equal(1, patch.LinesDeleted); } } @@ -384,11 +405,11 @@ public void ComparingATreeInABareRepositoryAgainstTheWorkDirOrTheIndexThrows() using (var repo = new Repository(BareTestRepoPath)) { Assert.Throws( - () => repo.Diff.Compare(repo.Head.Tip.Tree, DiffTargets.WorkingDirectory)); + () => repo.Diff.Compare(repo.Head.Tip.Tree, DiffTargets.WorkingDirectory)); Assert.Throws( - () => repo.Diff.Compare(repo.Head.Tip.Tree, DiffTargets.Index)); + () => repo.Diff.Compare(repo.Head.Tip.Tree, DiffTargets.Index)); Assert.Throws( - () => repo.Diff.Compare(repo.Head.Tip.Tree, DiffTargets.WorkingDirectory | DiffTargets.Index)); + () => repo.Diff.Compare(repo.Head.Tip.Tree, DiffTargets.WorkingDirectory | DiffTargets.Index)); } } @@ -401,14 +422,13 @@ public void CanCompareANullTreeAgainstTheIndex() { SetUpSimpleDiffContext(repo); - TreeChanges changes = repo.Diff.Compare(null, + var changes = repo.Diff.Compare(null, DiffTargets.Index); Assert.Equal(1, changes.Count()); Assert.Equal(1, changes.Added.Count()); Assert.Equal("file.txt", changes.Added.Single().Path); - Assert.Equal(2, changes.Added.Single().LinesAdded); } } @@ -421,14 +441,13 @@ public void CanCompareANullTreeAgainstTheWorkdir() { SetUpSimpleDiffContext(repo); - TreeChanges changes = repo.Diff.Compare(null, + var changes = repo.Diff.Compare(null, DiffTargets.WorkingDirectory); Assert.Equal(1, changes.Count()); Assert.Equal(1, changes.Added.Count()); Assert.Equal("file.txt", changes.Added.Single().Path); - Assert.Equal(3, changes.Added.Single().LinesAdded); } } @@ -441,14 +460,13 @@ public void CanCompareANullTreeAgainstTheWorkdirAndTheIndex() { SetUpSimpleDiffContext(repo); - TreeChanges changes = repo.Diff.Compare(null, + var changes = repo.Diff.Compare(null, DiffTargets.WorkingDirectory | DiffTargets.Index); Assert.Equal(1, changes.Count()); Assert.Equal(1, changes.Added.Count()); Assert.Equal("file.txt", changes.Added.Single().Path); - Assert.Equal(3, changes.Added.Single().LinesAdded); } } } diff --git a/LibGit2Sharp.Tests/DiffTreeToTreeFixture.cs b/LibGit2Sharp.Tests/DiffTreeToTreeFixture.cs index 4ed88ea69..5ced715e0 100644 --- a/LibGit2Sharp.Tests/DiffTreeToTreeFixture.cs +++ b/LibGit2Sharp.Tests/DiffTreeToTreeFixture.cs @@ -19,10 +19,12 @@ public void ComparingATreeAgainstItselfReturnsNoDifference() { Tree tree = repo.Head.Tip.Tree; - TreeChanges changes = repo.Diff.Compare(tree, tree); + var changes = repo.Diff.Compare(tree, tree); + var patch = repo.Diff.Compare(tree, tree); Assert.Empty(changes); - Assert.Equal(string.Empty, changes.Patch); + Assert.Empty(patch); + Assert.Equal(String.Empty, patch); } } @@ -33,7 +35,7 @@ public void RetrievingANonExistentFileChangeReturnsNull() { Tree tree = repo.Head.Tip.Tree; - TreeChanges changes = repo.Diff.Compare(tree, tree); + var changes = repo.Diff.Compare(tree, tree); Assert.Null(changes["batman"]); } @@ -52,19 +54,20 @@ public void CanCompareACommitTreeAgainstItsParent() Tree commitTree = repo.Head.Tip.Tree; Tree parentCommitTree = repo.Head.Tip.Parents.Single().Tree; - TreeChanges changes = repo.Diff.Compare(parentCommitTree, commitTree); + var changes = repo.Diff.Compare(parentCommitTree, commitTree); Assert.Equal(1, changes.Count()); Assert.Equal(1, changes.Added.Count()); TreeEntryChanges treeEntryChanges = changes["1.txt"]; - Assert.False(treeEntryChanges.IsBinaryComparison); + + var patch = repo.Diff.Compare(parentCommitTree, commitTree); + Assert.False(patch["1.txt"].IsBinaryComparison); Assert.Equal("1.txt", treeEntryChanges.Path); Assert.Equal(ChangeKind.Added, treeEntryChanges.Status); Assert.Equal(treeEntryChanges, changes.Added.Single()); - Assert.Equal(1, treeEntryChanges.LinesAdded); Assert.Equal(Mode.Nonexistent, treeEntryChanges.OldMode); } @@ -88,7 +91,7 @@ public void CanCompareASubsetofTheTreeAgainstOneOfItsAncestor() Tree tree = repo.Head.Tip.Tree; Tree ancestor = repo.Lookup("9fd738e").Tree; - TreeChanges changes = repo.Diff.Compare(ancestor, tree, new[]{ "1" }); + var changes = repo.Diff.Compare(ancestor, tree, new[] { "1" }); Assert.NotNull(changes); Assert.Equal(1, changes.Count()); @@ -118,7 +121,7 @@ public void CanCompareACommitTreeAgainstATreeWithNoCommonAncestor() Tree commitTree = repo.Head.Tip.Tree; Tree commitTreeWithDifferentAncestor = repo.Branches["refs/remotes/origin/test"].Tip.Tree; - TreeChanges changes = repo.Diff.Compare(commitTreeWithDifferentAncestor, commitTree); + var changes = repo.Diff.Compare(commitTreeWithDifferentAncestor, commitTree); Assert.Equal(10, changes.Count()); Assert.Equal(9, changes.Added.Count()); @@ -128,9 +131,10 @@ public void CanCompareACommitTreeAgainstATreeWithNoCommonAncestor() Assert.Equal(new[] { "1.txt", subBranchFilePath, "README", "branch_file.txt", "deleted_staged_file.txt", "deleted_unstaged_file.txt", "modified_staged_file.txt", "modified_unstaged_file.txt", "new.txt" }, changes.Added.Select(x => x.Path).OrderBy(p => p, StringComparer.Ordinal).ToArray()); - Assert.Equal(9, changes.LinesAdded); - Assert.Equal(2, changes.LinesDeleted); - Assert.Equal(2, changes["readme.txt"].LinesDeleted); + var patch = repo.Diff.Compare(commitTreeWithDifferentAncestor, commitTree); + Assert.Equal(9, patch.LinesAdded); + Assert.Equal(2, patch.LinesDeleted); + Assert.Equal(2, patch["readme.txt"].LinesDeleted); } } @@ -142,11 +146,11 @@ public void CanCompareATreeAgainstAnotherTreeWithLaxExplicitPathsValidationAndNo Tree commitTree = repo.Head.Tip.Tree; Tree commitTreeWithDifferentAncestor = repo.Branches["refs/remotes/origin/test"].Tip.Tree; - TreeChanges changes = repo.Diff.Compare(commitTreeWithDifferentAncestor, commitTree, + var changes = repo.Diff.Compare(commitTreeWithDifferentAncestor, commitTree, new[] { "if-I-exist-this-test-is-really-unlucky.txt" }, new ExplicitPathsOptions { ShouldFailOnUnmatchedPath = false }); Assert.Equal(0, changes.Count()); - changes = repo.Diff.Compare(commitTreeWithDifferentAncestor, commitTree, + changes = repo.Diff.Compare(commitTreeWithDifferentAncestor, commitTree, new[] { "if-I-exist-this-test-is-really-unlucky.txt" }); Assert.Equal(0, changes.Count()); } @@ -161,7 +165,7 @@ public void ComparingATreeAgainstAnotherTreeWithStrictExplicitPathsValidationThr Tree commitTreeWithDifferentAncestor = repo.Branches["refs/remotes/origin/test"].Tip.Tree; Assert.Throws(() => - repo.Diff.Compare(commitTreeWithDifferentAncestor, commitTree, + repo.Diff.Compare(commitTreeWithDifferentAncestor, commitTree, new[] { "if-I-exist-this-test-is-really-unlucky.txt" }, new ExplicitPathsOptions())); } } @@ -184,23 +188,494 @@ public void ComparingATreeAgainstAnotherTreeWithStrictExplicitPathsValidationThr * $ git diff -M --shortstat f8d44d7..4be51d6 * 1 file changed, 1 insertion(+) */ - [Fact(Skip = "Not implemented in libgit2 yet.")] - public void CanDetectTheRenamingOfAModifiedFile() + [Fact] + public void DetectsTheRenamingOfAModifiedFileByDefault() { using (var repo = new Repository(StandardTestRepoPath)) { Tree rootCommitTree = repo.Lookup("f8d44d7").Tree; Tree commitTreeWithRenamedFile = repo.Lookup("4be51d6").Tree; - TreeChanges changes = repo.Diff.Compare(rootCommitTree, commitTreeWithRenamedFile); + var changes = repo.Diff.Compare(rootCommitTree, commitTreeWithRenamedFile); Assert.Equal(1, changes.Count()); Assert.Equal("super-file.txt", changes["super-file.txt"].Path); Assert.Equal("my-name-does-not-feel-right.txt", changes["super-file.txt"].OldPath); - //Assert.Equal(1, changes.FilesRenamed.Count()); + Assert.Equal(1, changes.Renamed.Count()); + } + } + + [Fact] + public void DetectsTheExactRenamingOfFilesByDefault() + { + SelfCleaningDirectory scd = BuildSelfCleaningDirectory(); + var path = Repository.Init(scd.DirectoryPath); + using (var repo = new Repository(path)) + { + const string originalPath = "original.txt"; + const string renamedPath = "renamed.txt"; + + Touch(repo.Info.WorkingDirectory, originalPath, "a\nb\nc\nd\n"); + + repo.Index.Stage(originalPath); + + Commit old = repo.Commit("Initial", Constants.Signature, Constants.Signature); + + repo.Index.Move(originalPath, renamedPath); + + Commit @new = repo.Commit("Updated", Constants.Signature, Constants.Signature); + + TreeChanges changes = repo.Diff.Compare(old.Tree, @new.Tree); + + Assert.Equal(1, changes.Count()); + Assert.Equal(1, changes.Renamed.Count()); + Assert.Equal("original.txt", changes.Renamed.Single().OldPath); + Assert.Equal("renamed.txt", changes.Renamed.Single().Path); + } + } + + [Fact(Skip = "Not supported by libgit2 as yet")] + public void RenameDetectionObeysConfigurationSetting() + { + // TODO: set the repo's diff.renames setting, and pass a structure that adjusts thresholds + } + + [Fact] + public void RenameThresholdsAreObeyed() + { + SelfCleaningDirectory scd = BuildSelfCleaningDirectory(); + var path = Repository.Init(scd.DirectoryPath); + using (var repo = new Repository(path)) + { + const string originalPath = "original.txt"; + const string renamedPath = "renamed.txt"; + + // 4 lines + Touch(repo.Info.WorkingDirectory, originalPath, "a\nb\nc\nd\n"); + repo.Index.Stage(originalPath); + + Commit old = repo.Commit("Initial", Constants.Signature, Constants.Signature); + + // 8 lines, 50% are from original file + Touch(repo.Info.WorkingDirectory, originalPath, "a\nb\nc\nd\ne\nf\ng\nh\n"); + repo.Index.Stage(originalPath); + repo.Index.Move(originalPath, renamedPath); + + Commit @new = repo.Commit("Updated", Constants.Signature, Constants.Signature); + + var compareOptions = new CompareOptions + { + Similarity = new SimilarityOptions + { + RenameDetectionMode = RenameDetectionMode.Renames, + }, + }; + + compareOptions.Similarity.RenameThreshold = 30; + TreeChanges changes = repo.Diff.Compare(old.Tree, @new.Tree, compareOptions: compareOptions); + Assert.True(changes.All(x => x.Status == ChangeKind.Renamed)); + + compareOptions.Similarity.RenameThreshold = 90; + changes = repo.Diff.Compare(old.Tree, @new.Tree, compareOptions: compareOptions); + Assert.False(changes.Any(x => x.Status == ChangeKind.Renamed)); + } + } + + [Fact] + public void ExactModeDetectsExactRenames() + { + SelfCleaningDirectory scd = BuildSelfCleaningDirectory(); + var path = Repository.Init(scd.DirectoryPath); + using (var repo = new Repository(path)) + { + const string originalPath = "original.txt"; + const string renamedPath = "renamed.txt"; + + Touch(repo.Info.WorkingDirectory, originalPath, "a\nb\nc\nd\n"); + + repo.Index.Stage(originalPath); + + Commit old = repo.Commit("Initial", Constants.Signature, Constants.Signature); + + repo.Index.Move(originalPath, renamedPath); + + Commit @new = repo.Commit("Updated", Constants.Signature, Constants.Signature); + + TreeChanges changes = repo.Diff.Compare(old.Tree, @new.Tree, + compareOptions: new CompareOptions + { + Similarity = SimilarityOptions.Exact, + }); + + Assert.Equal(1, changes.Count()); + Assert.Equal(1, changes.Renamed.Count()); + Assert.Equal("original.txt", changes.Renamed.Single().OldPath); + Assert.Equal("renamed.txt", changes.Renamed.Single().Path); + } + } + + [Fact] + public void ExactModeDetectsExactCopies() + { + SelfCleaningDirectory scd = BuildSelfCleaningDirectory(); + var path = Repository.Init(scd.DirectoryPath); + using (var repo = new Repository(path)) + { + const string originalPath = "original.txt"; + const string copiedPath = "copied.txt"; + var originalFullPath = Path.Combine(repo.Info.WorkingDirectory, originalPath); + var copiedFullPath = Path.Combine(repo.Info.WorkingDirectory, copiedPath); + + Touch(repo.Info.WorkingDirectory, originalPath, "a\nb\nc\nd\n"); + repo.Index.Stage(originalPath); + Commit old = repo.Commit("Initial", Constants.Signature, Constants.Signature); + + File.Copy(originalFullPath, copiedFullPath); + repo.Index.Stage(copiedPath); + + Commit @new = repo.Commit("Updated", Constants.Signature, Constants.Signature); + + TreeChanges changes = repo.Diff.Compare(old.Tree, @new.Tree, + compareOptions: new CompareOptions + { + Similarity = SimilarityOptions.Exact, + }); + + Assert.Equal(1, changes.Count()); + Assert.Equal(1, changes.Copied.Count()); + } + } + + [Fact] + public void ExactModeDoesntDetectRenamesWithEdits() + { + SelfCleaningDirectory scd = BuildSelfCleaningDirectory(); + var path = Repository.Init(scd.DirectoryPath); + using (var repo = new Repository(path)) + { + const string originalPath = "original.txt"; + const string renamedPath = "renamed.txt"; + + Touch(repo.Info.WorkingDirectory, originalPath, "a\nb\nc\nd\n"); + + repo.Index.Stage(originalPath); + + Commit old = repo.Commit("Initial", Constants.Signature, Constants.Signature); + + repo.Index.Move(originalPath, renamedPath); + File.AppendAllText(Path.Combine(repo.Info.WorkingDirectory, renamedPath), "e\nf\n"); + repo.Index.Stage(renamedPath); + + Commit @new = repo.Commit("Updated", Constants.Signature, Constants.Signature); + + TreeChanges changes = repo.Diff.Compare(old.Tree, @new.Tree, + compareOptions: new CompareOptions + { + Similarity = SimilarityOptions.Exact, + }); + + Assert.Equal(2, changes.Count()); + Assert.Equal(0, changes.Renamed.Count()); + Assert.Equal(1, changes.Added.Count()); + Assert.Equal(1, changes.Deleted.Count()); + } + } + + [Fact] + public void CanIncludeUnmodifiedEntriesWhenDetectingTheExactRenamingOfFilesWhenEnabled() + { + SelfCleaningDirectory scd = BuildSelfCleaningDirectory(); + var path = Repository.Init(scd.DirectoryPath); + using (var repo = new Repository(path)) + { + const string originalPath = "original.txt"; + const string copiedPath = "copied.txt"; + string originalFullPath = Path.Combine(repo.Info.WorkingDirectory, originalPath); + string copiedFullPath = Path.Combine(repo.Info.WorkingDirectory, copiedPath); + + Touch(repo.Info.WorkingDirectory, originalPath, "a\nb\nc\nd\n"); + + repo.Index.Stage(originalPath); + + Commit old = repo.Commit("Initial", Constants.Signature, Constants.Signature); + + File.Copy(originalFullPath, copiedFullPath); + repo.Index.Stage(copiedPath); + + Commit @new = repo.Commit("Updated", Constants.Signature, Constants.Signature); + + TreeChanges changes = repo.Diff.Compare(old.Tree, @new.Tree, + compareOptions: + new CompareOptions + { + Similarity = SimilarityOptions.CopiesHarder, + IncludeUnmodified = true, + }); + + Assert.Equal(2, changes.Count()); + Assert.Equal(1, changes.Unmodified.Count()); + Assert.Equal(1, changes.Copied.Count()); + Assert.Equal("original.txt", changes.Copied.Single().OldPath); + Assert.Equal("copied.txt", changes.Copied.Single().Path); + } + } + + [Fact] + public void CanNotDetectTheExactRenamingFilesWhenNotEnabled() + { + SelfCleaningDirectory scd = BuildSelfCleaningDirectory(); + var path = Repository.Init(scd.DirectoryPath); + using (var repo = new Repository(path)) + { + const string originalPath = "original.txt"; + const string renamedPath = "renamed.txt"; + + Touch(repo.Info.WorkingDirectory, originalPath, "a\nb\nc\nd\n"); + + repo.Index.Stage(originalPath); + + Commit old = repo.Commit("Initial", Constants.Signature, Constants.Signature); + + repo.Index.Move(originalPath, renamedPath); + + Commit @new = repo.Commit("Updated", Constants.Signature, Constants.Signature); + + TreeChanges changes = repo.Diff.Compare(old.Tree, @new.Tree, + compareOptions: + new CompareOptions + { + Similarity = SimilarityOptions.None, + }); + + Assert.Equal(2, changes.Count()); + Assert.Equal(0, changes.Renamed.Count()); } } + [Fact] + public void CanDetectTheExactCopyingOfNonModifiedFilesWhenEnabled() + { + SelfCleaningDirectory scd = BuildSelfCleaningDirectory(); + var path = Repository.Init(scd.DirectoryPath); + using (var repo = new Repository(path)) + { + const string originalPath = "original.txt"; + const string copiedPath = "copied.txt"; + string originalFullPath = Path.Combine(repo.Info.WorkingDirectory, originalPath); + string copiedFullPath = Path.Combine(repo.Info.WorkingDirectory, copiedPath); + + Touch(repo.Info.WorkingDirectory, originalPath, "a\nb\nc\nd\n"); + + repo.Index.Stage(originalPath); + + Commit old = repo.Commit("Initial", Constants.Signature, Constants.Signature); + + File.Copy(originalFullPath, copiedFullPath); + repo.Index.Stage(copiedPath); + + Commit @new = repo.Commit("Updated", Constants.Signature, Constants.Signature); + + TreeChanges changes = repo.Diff.Compare(old.Tree, @new.Tree, + compareOptions: + new CompareOptions + { + Similarity = SimilarityOptions.CopiesHarder, + }); + + Assert.Equal(1, changes.Count()); + Assert.Equal(1, changes.Copied.Count()); + Assert.Equal("original.txt", changes.Copied.Single().OldPath); + Assert.Equal("copied.txt", changes.Copied.Single().Path); + } + } + + [Fact] + public void CanNotDetectTheExactCopyingOfNonModifiedFilesWhenNotEnabled() + { + SelfCleaningDirectory scd = BuildSelfCleaningDirectory(); + var path = Repository.Init(scd.DirectoryPath); + using (var repo = new Repository(path)) + { + const string originalPath = "original.txt"; + const string copiedPath = "copied.txt"; + string originalFullPath = Path.Combine(repo.Info.WorkingDirectory, originalPath); + string copiedFullPath = Path.Combine(repo.Info.WorkingDirectory, copiedPath); + + Touch(repo.Info.WorkingDirectory, originalPath, "a\nb\nc\nd\n"); + + repo.Index.Stage(originalPath); + + Commit old = repo.Commit("Initial", Constants.Signature, Constants.Signature); + + File.Copy(originalFullPath, copiedFullPath); + repo.Index.Stage(copiedPath); + + Commit @new = repo.Commit("Updated", Constants.Signature, Constants.Signature); + + TreeChanges changes = repo.Diff.Compare(old.Tree, @new.Tree); + + Assert.Equal(1, changes.Count()); + Assert.Equal(0, changes.Copied.Count()); + } + } + + [Fact] + public void CanDetectTheExactCopyingOfModifiedFilesWhenEnabled() + { + SelfCleaningDirectory scd = BuildSelfCleaningDirectory(); + var path = Repository.Init(scd.DirectoryPath); + using (var repo = new Repository(path)) + { + const string originalPath = "original.txt"; + const string copiedPath = "copied.txt"; + string originalFullPath = Path.Combine(repo.Info.WorkingDirectory, originalPath); + string copiedFullPath = Path.Combine(repo.Info.WorkingDirectory, copiedPath); + + Touch(repo.Info.WorkingDirectory, originalPath, "a\nb\nc\nd\n"); + + repo.Index.Stage(originalPath); + + Commit old = repo.Commit("Initial", Constants.Signature, Constants.Signature); + + File.Copy(originalFullPath, copiedFullPath); + Touch(repo.Info.WorkingDirectory, originalPath, "e\n"); + + repo.Index.Stage(originalPath); + repo.Index.Stage(copiedPath); + + Commit @new = repo.Commit("Updated", Constants.Signature, Constants.Signature); + + TreeChanges changes = repo.Diff.Compare(old.Tree, @new.Tree, + compareOptions: + new CompareOptions + { + Similarity = SimilarityOptions.Copies, + }); + + Assert.Equal(2, changes.Count()); + Assert.Equal(1, changes.Copied.Count()); + Assert.Equal("original.txt", changes.Copied.Single().OldPath); + Assert.Equal("copied.txt", changes.Copied.Single().Path); + } + } + + [Fact] + public void CanNotDetectTheExactCopyingOfModifiedFilesWhenNotEnabled() + { + SelfCleaningDirectory scd = BuildSelfCleaningDirectory(); + var path = Repository.Init(scd.DirectoryPath); + using (var repo = new Repository(path)) + { + const string originalPath = "original.txt"; + const string copiedPath = "copied.txt"; + string originalFullPath = Path.Combine(repo.Info.WorkingDirectory, originalPath); + string copiedFullPath = Path.Combine(repo.Info.WorkingDirectory, copiedPath); + + Touch(repo.Info.WorkingDirectory, originalPath, "a\nb\nc\nd\n"); + + repo.Index.Stage(originalPath); + + Commit old = repo.Commit("Initial", Constants.Signature, Constants.Signature); + + File.Copy(originalFullPath, copiedFullPath); + File.AppendAllText(originalFullPath, "e\n"); + + repo.Index.Stage(originalPath); + repo.Index.Stage(copiedPath); + + Commit @new = repo.Commit("Updated", Constants.Signature, Constants.Signature); + + TreeChanges changes = repo.Diff.Compare(old.Tree, @new.Tree); + + Assert.Equal(2, changes.Count()); + Assert.Equal(0, changes.Copied.Count()); + } + } + + [Fact] + public void CanIncludeUnmodifiedEntriesWhenEnabled() + { + SelfCleaningDirectory scd = BuildSelfCleaningDirectory(); + var path = Repository.Init(scd.DirectoryPath); + using (var repo = new Repository(path)) + { + Touch(repo.Info.WorkingDirectory, "a.txt", "abc\ndef\n"); + Touch(repo.Info.WorkingDirectory, "b.txt", "abc\ndef\n"); + + repo.Index.Stage(new[] {"a.txt", "b.txt"}); + Commit old = repo.Commit("Initial", Constants.Signature, Constants.Signature); + + File.AppendAllText(Path.Combine(repo.Info.WorkingDirectory, "b.txt"), "ghi\njkl\n"); + repo.Index.Stage("b.txt"); + Commit @new = repo.Commit("Updated", Constants.Signature, Constants.Signature); + + TreeChanges changes = repo.Diff.Compare(old.Tree, @new.Tree, + compareOptions: new CompareOptions {IncludeUnmodified = true}); + + Assert.Equal(2, changes.Count()); + Assert.Equal(1, changes.Unmodified.Count()); + Assert.Equal(1, changes.Modified.Count()); + } + } + + [Fact] + public void CanDetectTheExactRenamingExactCopyingOfNonModifiedAndModifiedFilesWhenEnabled() + { + SelfCleaningDirectory scd = BuildSelfCleaningDirectory(); + var path = Repository.Init(scd.DirectoryPath); + using (var repo = new Repository(path)) + { + const string originalPath = "original.txt"; + const string renamedPath = "renamed.txt"; + const string originalPath2 = "original2.txt"; + const string copiedPath1 = "copied.txt"; + const string originalPath3 = "original3.txt"; + const string copiedPath2 = "copied2.txt"; + + Touch(repo.Info.WorkingDirectory, originalPath, "a\nb\nc\nd\n"); + Touch(repo.Info.WorkingDirectory, originalPath2, "1\n2\n3\n4\n"); + Touch(repo.Info.WorkingDirectory, originalPath3, "5\n6\n7\n8\n"); + + repo.Index.Stage(originalPath); + repo.Index.Stage(originalPath2); + repo.Index.Stage(originalPath3); + + Commit old = repo.Commit("Initial", Constants.Signature, Constants.Signature); + + var originalFullPath2 = Path.Combine(repo.Info.WorkingDirectory, originalPath2); + var originalFullPath3 = Path.Combine(repo.Info.WorkingDirectory, originalPath3); + var copiedFullPath1 = Path.Combine(repo.Info.WorkingDirectory, copiedPath1); + var copiedFullPath2 = Path.Combine(repo.Info.WorkingDirectory, copiedPath2); + File.Copy(originalFullPath2, copiedFullPath1); + File.Copy(originalFullPath3, copiedFullPath2); + File.AppendAllText(originalFullPath3, "9\n"); + + repo.Index.Stage(originalPath3); + repo.Index.Stage(copiedPath1); + repo.Index.Stage(copiedPath2); + repo.Index.Move(originalPath, renamedPath); + + Commit @new = repo.Commit("Updated", Constants.Signature, Constants.Signature); + + TreeChanges changes = repo.Diff.Compare(old.Tree, @new.Tree, + compareOptions: + new CompareOptions + { + Similarity = SimilarityOptions.CopiesHarder, + }); + + Assert.Equal(4, changes.Count()); + Assert.Equal(1, changes.Modified.Count()); + Assert.Equal(1, changes.Renamed.Count()); + Assert.Equal("original.txt", changes.Renamed.Single().OldPath); + Assert.Equal("renamed.txt", changes.Renamed.Single().Path); + Assert.Equal(2, changes.Copied.Count()); + Assert.Equal("original2.txt", changes.Copied.ElementAt(0).OldPath); + Assert.Equal("copied.txt", changes.Copied.ElementAt(0).Path); + Assert.Equal("original3.txt", changes.Copied.ElementAt(1).OldPath); + Assert.Equal("copied2.txt", changes.Copied.ElementAt(1).Path); + } + } /* * $ git diff f8d44d7..ec9e401 * diff --git a/numbers.txt b/numbers.txt @@ -236,18 +711,21 @@ public void CanCompareTwoVersionsOfAFileWithATrailingNewlineDeletion(int context Tree rootCommitTree = repo.Lookup("f8d44d7").Tree; Tree commitTreeWithUpdatedFile = repo.Lookup("ec9e401").Tree; - TreeChanges changes = repo.Diff.Compare(rootCommitTree, commitTreeWithUpdatedFile, - compareOptions: new CompareOptions { ContextLines = contextLines }); + var changes = repo.Diff.Compare(rootCommitTree, commitTreeWithUpdatedFile); Assert.Equal(1, changes.Count()); Assert.Equal(1, changes.Modified.Count()); - Assert.Equal(expectedPatchLength, changes.Patch.Length); - TreeEntryChanges treeEntryChanges = changes.Modified.Single(); + var patch = repo.Diff.Compare(rootCommitTree, commitTreeWithUpdatedFile, + compareOptions: new CompareOptions { ContextLines = contextLines }); + + Assert.Equal(expectedPatchLength, patch.Content.Length); - Assert.Equal(2, treeEntryChanges.LinesAdded); - Assert.Equal(1, treeEntryChanges.LinesDeleted); - Assert.Equal(expectedPatchLength, treeEntryChanges.Patch.Length); + ContentChanges contentChanges = patch["numbers.txt"]; + + Assert.Equal(2, contentChanges.LinesAdded); + Assert.Equal(1, contentChanges.LinesDeleted); + Assert.Equal(expectedPatchLength, contentChanges.Patch.Length); } } @@ -319,6 +797,7 @@ public void CanCompareTwoVersionsOfAFileWithADiffOfTwoHunks(int contextLines, in { ContextLines = contextLines, InterhunkLines = interhunkLines, + Similarity = SimilarityOptions.None, }; using (var repo = new Repository(StandardTestRepoPath)) @@ -326,23 +805,25 @@ public void CanCompareTwoVersionsOfAFileWithADiffOfTwoHunks(int contextLines, in Tree rootCommitTree = repo.Lookup("f8d44d7").Tree; Tree mergedCommitTree = repo.Lookup("7252fe2").Tree; - TreeChanges changes = repo.Diff.Compare(rootCommitTree, mergedCommitTree, compareOptions: compareOptions); + var changes = repo.Diff.Compare(rootCommitTree, mergedCommitTree, compareOptions: compareOptions); Assert.Equal(3, changes.Count()); Assert.Equal(1, changes.Modified.Count()); Assert.Equal(1, changes.Deleted.Count()); Assert.Equal(1, changes.Added.Count()); - TreeEntryChanges treeEntryChanges = changes["numbers.txt"]; + Assert.Equal(Mode.Nonexistent, changes["my-name-does-not-feel-right.txt"].Mode); - Assert.Equal(3, treeEntryChanges.LinesAdded); - Assert.Equal(1, treeEntryChanges.LinesDeleted); + var patch = repo.Diff.Compare(rootCommitTree, mergedCommitTree, compareOptions: compareOptions); - Assert.Equal(Mode.Nonexistent, changes["my-name-does-not-feel-right.txt"].Mode); + ContentChanges contentChanges = patch["numbers.txt"]; + + Assert.Equal(3, contentChanges.LinesAdded); + Assert.Equal(1, contentChanges.LinesDeleted); Assert.Equal(Expected("f8d44d7...7252fe2/numbers.txt-{0}-{1}.diff", contextLines, interhunkLines), - treeEntryChanges.Patch); + contentChanges.Patch); Assert.Equal(Expected("f8d44d7...7252fe2/full-{0}-{1}.diff", contextLines, interhunkLines), - changes.Patch); + patch); } } @@ -369,7 +850,11 @@ public void CanHandleTwoTreeEntryChangesWithTheSamePath() Tree treeNew = repo.ObjectDatabase.CreateTree(tdNew); - TreeChanges changes = repo.Diff.Compare(treeOld, treeNew); + var changes = repo.Diff.Compare(treeOld, treeNew, + compareOptions: new CompareOptions + { + Similarity = SimilarityOptions.None, + }); /* * $ git diff-tree -p 5c87b67 d5278d0 @@ -416,14 +901,14 @@ public void CanCompareATreeAgainstANullTree() { Tree tree = repo.Branches["refs/remotes/origin/test"].Tip.Tree; - TreeChanges changes = repo.Diff.Compare(tree, null); + var changes = repo.Diff.Compare(tree, null); Assert.Equal(1, changes.Count()); Assert.Equal(1, changes.Deleted.Count()); Assert.Equal("readme.txt", changes.Deleted.Single().Path); - changes = repo.Diff.Compare(null, tree); + changes = repo.Diff.Compare(null, tree); Assert.Equal(1, changes.Count()); Assert.Equal(1, changes.Added.Count()); @@ -437,7 +922,7 @@ public void ComparingTwoNullTreesReturnsAnEmptyTreeChanges() { using (var repo = new Repository(StandardTestRepoPath)) { - TreeChanges changes = repo.Diff.Compare(default(Tree), default(Tree)); + var changes = repo.Diff.Compare(default(Tree), default(Tree)); Assert.Equal(0, changes.Count()); } @@ -457,10 +942,13 @@ public void ComparingReliesOnProvidedConfigEntriesIfAny() // Recreate the file in the workdir without the executable bit string fullpath = Path.Combine(repo.Info.WorkingDirectory, file); File.Delete(fullpath); - File.WriteAllBytes(fullpath, ((Blob)(entry.Target)).Content); + using (var stream = ((Blob)(entry.Target)).GetContentStream()) + { + Touch(repo.Info.WorkingDirectory, file, stream); + } // Unset the local core.filemode, if any. - repo.Config.Unset("core.filemode", ConfigurationLevel.Local); + repo.Config.Unset("core.filemode"); } SelfCleaningDirectory scd = BuildSelfCleaningDirectory(); @@ -469,7 +957,7 @@ public void ComparingReliesOnProvidedConfigEntriesIfAny() using (var repo = new Repository(path, options)) { - TreeChanges changes = repo.Diff.Compare(new []{ file }); + var changes = repo.Diff.Compare(new[] { file }); Assert.Equal(1, changes.Count()); @@ -482,7 +970,7 @@ public void ComparingReliesOnProvidedConfigEntriesIfAny() using (var repo = new Repository(path, options)) { - TreeChanges changes = repo.Diff.Compare(new[] { file }); + var changes = repo.Diff.Compare(new[] { file }); Assert.Equal(0, changes.Count()); } @@ -503,7 +991,7 @@ private RepositoryOptions BuildFakeSystemConfigFilemodeOption( StringBuilder sb = new StringBuilder() .AppendFormat("[core]{0}", Environment.NewLine) .AppendFormat("filemode = {1}{0}", Environment.NewLine, value); - File.WriteAllText(options.SystemConfigurationLocation, sb.ToString()); + Touch("", options.SystemConfigurationLocation, sb.ToString()); return options; } @@ -535,11 +1023,21 @@ public void RetrievingDiffChangesMustAlwaysBeCaseSensitive() using (var repo = new Repository(repoPath)) { - var changes = repo.Diff.Compare(repo.Lookup(treeOldOid), repo.Lookup(treeNewOid)); + var changes = repo.Diff.Compare(repo.Lookup(treeOldOid), repo.Lookup(treeNewOid)); Assert.Equal(ChangeKind.Modified, changes["a.txt"].Status); Assert.Equal(ChangeKind.Modified, changes["A.TXT"].Status); } } + + [Fact] + public void CallingCompareWithAnUnsupportedGenericParamThrows() + { + using (var repo = new Repository(StandardTestRepoPath)) + { + Assert.Throws(() => repo.Diff.Compare(default(Tree), default(Tree))); + Assert.Throws(() => repo.Diff.Compare()); + } + } } } diff --git a/LibGit2Sharp.Tests/DiffWorkdirToIndexFixture.cs b/LibGit2Sharp.Tests/DiffWorkdirToIndexFixture.cs index 5232ef2a1..2b7c528c4 100644 --- a/LibGit2Sharp.Tests/DiffWorkdirToIndexFixture.cs +++ b/LibGit2Sharp.Tests/DiffWorkdirToIndexFixture.cs @@ -32,7 +32,7 @@ public void CanCompareTheWorkDirAgainstTheIndex() { using (var repo = new Repository(StandardTestRepoPath)) { - TreeChanges changes = repo.Diff.Compare(); + var changes = repo.Diff.Compare(); Assert.Equal(2, changes.Count()); Assert.Equal("deleted_unstaged_file.txt", changes.Deleted.Single().Path); @@ -49,10 +49,10 @@ public void CanCompareTheWorkDirAgainstTheIndexWithLaxUnmatchedExplicitPathsVali { Assert.Equal(currentStatus, repo.Index.RetrieveStatus(relativePath)); - TreeChanges changes = repo.Diff.Compare(new[] { relativePath }, false, new ExplicitPathsOptions { ShouldFailOnUnmatchedPath = false }); + var changes = repo.Diff.Compare(new[] { relativePath }, false, new ExplicitPathsOptions { ShouldFailOnUnmatchedPath = false }); Assert.Equal(0, changes.Count()); - changes = repo.Diff.Compare(new[] { relativePath }); + changes = repo.Diff.Compare(new[] { relativePath }); Assert.Equal(0, changes.Count()); } } @@ -66,7 +66,7 @@ public void ComparingTheWorkDirAgainstTheIndexWithStrictUnmatchedExplicitPathsVa { Assert.Equal(currentStatus, repo.Index.RetrieveStatus(relativePath)); - Assert.Throws(() => repo.Diff.Compare(new[] { relativePath }, false, new ExplicitPathsOptions())); + Assert.Throws(() => repo.Diff.Compare(new[] { relativePath }, false, new ExplicitPathsOptions())); } } @@ -81,7 +81,9 @@ public void CallbackForUnmatchedExplicitPathsIsCalledWhenSet(string relativePath { Assert.Equal(currentStatus, repo.Index.RetrieveStatus(relativePath)); - repo.Diff.Compare(new[] { relativePath }, false, new ExplicitPathsOptions { ShouldFailOnUnmatchedPath = false, + repo.Diff.Compare(new[] { relativePath }, false, new ExplicitPathsOptions + { + ShouldFailOnUnmatchedPath = false, OnUnmatchedPath = callback.OnUnmatchedPath }); Assert.True(callback.WasCalled); @@ -112,7 +114,10 @@ public void ComparingReliesOnProvidedConfigEntriesIfAny() // Recreate the file in the workdir without the executable bit string fullpath = Path.Combine(repo.Info.WorkingDirectory, file); File.Delete(fullpath); - File.WriteAllBytes(fullpath, ((Blob)(entry.Target)).Content); + using (var stream = ((Blob)(entry.Target)).GetContentStream()) + { + Touch(repo.Info.WorkingDirectory, file, stream); + } // Unset the local core.filemode, if any. repo.Config.Unset("core.filemode", ConfigurationLevel.Local); @@ -124,7 +129,7 @@ public void ComparingReliesOnProvidedConfigEntriesIfAny() using (var repo = new Repository(path, options)) { - TreeChanges changes = repo.Diff.Compare(new[] { file }); + var changes = repo.Diff.Compare(new[] { file }); Assert.Equal(1, changes.Count()); @@ -137,7 +142,7 @@ public void ComparingReliesOnProvidedConfigEntriesIfAny() using (var repo = new Repository(path, options)) { - TreeChanges changes = repo.Diff.Compare(new[] { file }); + var changes = repo.Diff.Compare(new[] { file }); Assert.Equal(0, changes.Count()); } @@ -168,7 +173,7 @@ public void CanCompareTheWorkDirAgainstTheIndexWithUntrackedFiles() { using (var repo = new Repository(StandardTestRepoPath)) { - TreeChanges changes = repo.Diff.Compare(null, true); + var changes = repo.Diff.Compare(null, true); Assert.Equal(3, changes.Count()); Assert.Equal("deleted_unstaged_file.txt", changes.Deleted.Single().Path); diff --git a/LibGit2Sharp.Tests/EqualityFixture.cs b/LibGit2Sharp.Tests/EqualityFixture.cs new file mode 100644 index 000000000..168d5fb99 --- /dev/null +++ b/LibGit2Sharp.Tests/EqualityFixture.cs @@ -0,0 +1,52 @@ +using LibGit2Sharp.Tests.TestHelpers; +using Xunit; + +namespace LibGit2Sharp.Tests +{ + public class EqualityFixture : BaseFixture + { + [Fact] + public void EqualityHelperCanTestNullInEquals() + { + var one = new ObjectWithEquality(); + var two = new ObjectWithEquality(); + var three = new ObjectWithEquality(ObjectId.Zero); + var four = new ObjectWithEquality(ObjectId.Zero); + + Assert.True(one.Equals(one)); + Assert.True(two.Equals(two)); + Assert.True(three.Equals(four)); + Assert.True(four.Equals(three)); + Assert.False(one.Equals(three)); + Assert.False(three.Equals(one)); + } + + [Fact] + public void EqualityHelperCanTestNullInHashCode() + { + var one = new ObjectWithEquality(); + var two = new ObjectWithEquality(); + var three = new ObjectWithEquality(ObjectId.Zero); + var four = new ObjectWithEquality(ObjectId.Zero); + + Assert.Equal(one.GetHashCode(), two.GetHashCode()); + Assert.Equal(three.GetHashCode(), four.GetHashCode()); + Assert.NotEqual(one.GetHashCode(), three.GetHashCode()); + } + + private class ObjectWithEquality : GitObject + { + private readonly ObjectId id; + + public ObjectWithEquality(ObjectId id = null) + { + this.id = id; + } + + public override ObjectId Id + { + get { return id; } + } + } + } +} diff --git a/LibGit2Sharp.Tests/FetchFixture.cs b/LibGit2Sharp.Tests/FetchFixture.cs index 182fd7f06..3e340bcdd 100644 --- a/LibGit2Sharp.Tests/FetchFixture.cs +++ b/LibGit2Sharp.Tests/FetchFixture.cs @@ -42,7 +42,7 @@ public void CanFetchIntoAnEmptyRepository(string url) } // Perform the actual fetch - repo.Network.Fetch(remote, onUpdateTips: expectedFetchState.RemoteUpdateTipsHandler); + repo.Network.Fetch(remote, new FetchOptions { OnUpdateTips = expectedFetchState.RemoteUpdateTipsHandler }); // Verify the expected expectedFetchState.CheckUpdatedReferences(repo); @@ -62,11 +62,14 @@ public void CanFetchIntoAnEmptyRepositoryWithCredentials() Remote remote = repo.Network.Remotes.Add(remoteName, Constants.PrivateRepoUrl); // Perform the actual fetch - repo.Network.Fetch(remote, credentials: new Credentials - { - Username = Constants.PrivateRepoUsername, - Password = Constants.PrivateRepoPassword - }); + repo.Network.Fetch(remote, new FetchOptions + { + Credentials = new Credentials + { + Username = Constants.PrivateRepoUsername, + Password = Constants.PrivateRepoPassword + } + }); } } @@ -94,7 +97,41 @@ public void CanFetchAllTagsIntoAnEmptyRepository(string url) } // Perform the actual fetch - repo.Network.Fetch(remote, TagFetchMode.All, onUpdateTips: expectedFetchState.RemoteUpdateTipsHandler); + repo.Network.Fetch(remote, new FetchOptions { + TagFetchMode = TagFetchMode.All, + OnUpdateTips = expectedFetchState.RemoteUpdateTipsHandler + }); + + // Verify the expected + expectedFetchState.CheckUpdatedReferences(repo); + } + } + + [Theory] + [InlineData("http://github.com/libgit2/TestGitRepository", "test-branch", "master")] + [InlineData("https://github.com/libgit2/TestGitRepository", "master", "master")] + [InlineData("git://github.com/libgit2/TestGitRepository.git", "master", "first-merge")] + public void CanFetchCustomRefSpecsIntoAnEmptyRepository(string url, string localBranchName, string remoteBranchName) + { + string repoPath = InitNewRepository(); + + using (var repo = new Repository(repoPath)) + { + Remote remote = repo.Network.Remotes.Add(remoteName, url); + + string refSpec = string.Format("refs/heads/{2}:refs/remotes/{0}/{1}", remoteName, localBranchName, remoteBranchName); + + // Set up structures for the expected results + // and verifying the RemoteUpdateTips callback. + TestRemoteInfo remoteInfo = TestRemoteInfo.TestRemoteInstance; + var expectedFetchState = new ExpectedFetchState(remoteName); + expectedFetchState.AddExpectedBranch(localBranchName, ObjectId.Zero, remoteInfo.BranchTips[remoteBranchName]); + + // Perform the actual fetch + repo.Network.Fetch(remote, new string[] { refSpec }, new FetchOptions { + TagFetchMode = TagFetchMode.None, + OnUpdateTips = expectedFetchState.RemoteUpdateTipsHandler + }); // Verify the expected expectedFetchState.CheckUpdatedReferences(repo); diff --git a/LibGit2Sharp.Tests/FetchHeadFixture.cs b/LibGit2Sharp.Tests/FetchHeadFixture.cs index 9714ff64a..0e929001c 100644 --- a/LibGit2Sharp.Tests/FetchHeadFixture.cs +++ b/LibGit2Sharp.Tests/FetchHeadFixture.cs @@ -38,7 +38,7 @@ public void CanIterateFetchHead(string url) using (var repo = new Repository(clonedRepoPath)) { - repo.Reset(ResetOptions.Hard, "HEAD~2"); + repo.Reset(ResetMode.Hard, "HEAD~2"); // Create a file, stage it, and commit it. const string filename = "b.txt"; diff --git a/LibGit2Sharp.Tests/FilterBranchFixture.cs b/LibGit2Sharp.Tests/FilterBranchFixture.cs index 20ebdb02a..efed5c0b6 100644 --- a/LibGit2Sharp.Tests/FilterBranchFixture.cs +++ b/LibGit2Sharp.Tests/FilterBranchFixture.cs @@ -10,6 +10,8 @@ namespace LibGit2Sharp.Tests public class FilterBranchFixture : BaseFixture { private readonly Repository repo; + private bool succeeding; + private Exception error; public FilterBranchFixture() { @@ -20,6 +22,7 @@ public FilterBranchFixture() public override void Dispose() { repo.Dispose(); + base.Dispose(); } [Fact] @@ -31,9 +34,13 @@ public void CanRewriteHistoryWithoutChangingCommitMetadata() // Noop header rewriter repo.Refs.RewriteHistory(new RewriteHistoryOptions { + OnError = OnError, + OnSucceeding = OnSucceeding, CommitHeaderRewriter = CommitRewriteInfo.From, }, commits); + AssertSucceedingButNotError(); + Assert.Equal(originalRefs, repo.Refs.ToList().OrderBy(r => r.CanonicalName)); Assert.Equal(commits, repo.Commits.QueryBy(new CommitFilter { Since = repo.Refs }).ToArray()); } @@ -47,9 +54,95 @@ public void CanRewriteHistoryWithoutChangingTrees() // Noop tree rewriter repo.Refs.RewriteHistory(new RewriteHistoryOptions { + OnError = OnError, + OnSucceeding = OnSucceeding, CommitTreeRewriter = TreeDefinition.From, }, commits); + AssertSucceedingButNotError(); + + Assert.Equal(originalRefs, repo.Refs.ToList().OrderBy(r => r.CanonicalName)); + Assert.Equal(commits, repo.Commits.QueryBy(new CommitFilter { Since = repo.Refs }).ToArray()); + } + + [Fact] + public void CanRollbackRewriteByThrowingInOnCompleting() + { + var originalRefs = repo.Refs.ToList().OrderBy(r => r.CanonicalName); + var commits = repo.Commits.QueryBy(new CommitFilter { Since = repo.Refs }).ToArray(); + + Assert.Throws( + () => + repo.Refs.RewriteHistory(new RewriteHistoryOptions + { + OnError = OnError, + OnSucceeding = + () => + { + succeeding = true; + throw new Exception(); + }, + CommitHeaderRewriter = + c => CommitRewriteInfo.From(c, message: "Rewritten " + c.Message), + }, commits) + ); + + AssertSucceedingButNotError(); + + Assert.Equal(originalRefs, repo.Refs.ToList().OrderBy(r => r.CanonicalName)); + Assert.Equal(commits, repo.Commits.QueryBy(new CommitFilter { Since = repo.Refs }).ToArray()); + } + + [Fact] + public void ErrorThrownInOnErrorTakesPrecedenceOverErrorDuringCommitHeaderRewriter() + { + var originalRefs = repo.Refs.ToList().OrderBy(r => r.CanonicalName); + var commits = repo.Commits.QueryBy(new CommitFilter { Since = repo.Refs }).ToArray(); + + var thrown = Assert.Throws( + () => + repo.Refs.RewriteHistory(new RewriteHistoryOptions + { + OnError = + ex => { throw new Exception("From OnError", ex); }, + OnSucceeding = OnSucceeding, + CommitHeaderRewriter = + c => { throw new Exception("From CommitHeaderRewriter"); }, + }, commits) + ); + + AssertSucceedingNotFired(); + Assert.Equal("From OnError", thrown.Message); + Assert.NotNull(thrown.InnerException); + Assert.Equal("From CommitHeaderRewriter", thrown.InnerException.Message); + + Assert.Equal(originalRefs, repo.Refs.ToList().OrderBy(r => r.CanonicalName)); + Assert.Equal(commits, repo.Commits.QueryBy(new CommitFilter { Since = repo.Refs }).ToArray()); + } + + [Fact] + public void ErrorThrownInOnErrorTakesPrecedenceOverErrorDuringCommitTreeRewriter() + { + var originalRefs = repo.Refs.ToList().OrderBy(r => r.CanonicalName); + var commits = repo.Commits.QueryBy(new CommitFilter { Since = repo.Refs }).ToArray(); + + var thrown = Assert.Throws( + () => + repo.Refs.RewriteHistory(new RewriteHistoryOptions + { + OnError = + ex => { throw new Exception("From OnError", ex); }, + OnSucceeding = OnSucceeding, + CommitTreeRewriter = + c => { throw new Exception("From CommitTreeRewriter"); }, + }, commits) + ); + + AssertSucceedingNotFired(); + Assert.Equal("From OnError", thrown.Message); + Assert.NotNull(thrown.InnerException); + Assert.Equal("From CommitTreeRewriter", thrown.InnerException.Message); + Assert.Equal(originalRefs, repo.Refs.ToList().OrderBy(r => r.CanonicalName)); Assert.Equal(commits, repo.Commits.QueryBy(new CommitFilter { Since = repo.Refs }).ToArray()); } @@ -60,11 +153,15 @@ public void CanRewriteAuthorOfCommits() var commits = repo.Commits.QueryBy(new CommitFilter { Since = repo.Refs }).ToArray(); repo.Refs.RewriteHistory(new RewriteHistoryOptions { + OnError = OnError, + OnSucceeding = OnSucceeding, CommitHeaderRewriter = c => CommitRewriteInfo.From(c, author: new Signature("Ben Straub", "me@example.com", c.Author.When)), }, commits); + AssertSucceedingButNotError(); + var nonBackedUpRefs = repo.Refs.Where(x => !x.CanonicalName.StartsWith("refs/original")); Assert.Empty(repo.Commits.QueryBy(new CommitFilter { Since = nonBackedUpRefs }) .Where(c => c.Author.Name != "Ben Straub")); @@ -84,10 +181,14 @@ public void CanRewriteAuthorOfCommitsOnlyBeingPointedAtByTags() repo.Refs.RewriteHistory(new RewriteHistoryOptions { + OnError = OnError, + OnSucceeding = OnSucceeding, CommitHeaderRewriter = c => CommitRewriteInfo.From(c, message: "Bam!"), }, commit); + AssertSucceedingButNotError(); + var lightweightTag = repo.Tags["so-lonely"]; Assert.Equal("Bam!\n", ((Commit)lightweightTag.Target).Message); @@ -100,14 +201,46 @@ public void CanRewriteTrees() { repo.Refs.RewriteHistory(new RewriteHistoryOptions { + OnError = OnError, + OnSucceeding = OnSucceeding, CommitTreeRewriter = c => TreeDefinition.From(c) .Remove("README"), }, repo.Head.Commits); + AssertSucceedingButNotError(); + Assert.True(repo.Head.Commits.All(c => c["README"] == null)); } + [Fact] + public void CanRewriteTreesByInjectingTreeEntry() + { + var commits = repo.Commits.QueryBy(new CommitFilter { Since = repo.Branches }).ToArray(); + + var currentReadme = repo.Head["README"]; + + repo.Refs.RewriteHistory(new RewriteHistoryOptions + { + OnError = OnError, + OnSucceeding = OnSucceeding, + CommitTreeRewriter = + c => c["README"] == null + ? TreeDefinition.From(c) + : TreeDefinition.From(c) + .Add("README", currentReadme), + }, commits); + + AssertSucceedingButNotError(); + + Assert.Equal(new Commit[0], + repo.Commits + .QueryBy(new CommitFilter {Since = repo.Branches}) + .Where(c => c["README"] != null + && c["README"].Target.Id != currentReadme.Target.Id) + .ToArray()); + } + // git log --graph --oneline --name-status --decorate // // * 4c062a6 (HEAD, master) directory was added @@ -189,19 +322,23 @@ public void CanRewriteTrees() [InlineData(new[] { "1" }, 6, "be3563a")] // If all trees are empty, master should be an orphan - [InlineData(new[] { "1", "branch_file.txt", "new.txt", "README" }, 0, null)] + [InlineData(new[] { "1", "branch_file.txt", "new.txt", "README" }, 0, null)] public void CanPruneEmptyCommits(string[] treeEntriesToRemove, int expectedCommitCount, string expectedHead) { Assert.Equal(7, repo.Head.Commits.Count()); repo.Refs.RewriteHistory(new RewriteHistoryOptions { + OnError = OnError, + OnSucceeding = OnSucceeding, PruneEmptyCommits = true, CommitTreeRewriter = c => TreeDefinition.From(c) .Remove(treeEntriesToRemove), }, repo.Head.Commits); + AssertSucceedingButNotError(); + Assert.Equal(expectedCommitCount, repo.Head.Commits.Count()); if (expectedHead == null) @@ -234,10 +371,14 @@ public void OnlyRewriteSelectedCommits() repo.Refs.RewriteHistory(new RewriteHistoryOptions { + OnError = OnError, + OnSucceeding = OnSucceeding, CommitHeaderRewriter = c => CommitRewriteInfo.From(c, author: Constants.Signature), }, parent); + AssertSucceedingButNotError(); + commit = repo.Branches["packed"].Tip; parent = commit.Parents.Single(); @@ -253,20 +394,29 @@ public void CanCustomizeTheNamespaceOfBackedUpRefs(string backupRefsNamespace) { repo.Refs.RewriteHistory(new RewriteHistoryOptions { + OnError = OnError, + OnSucceeding = OnSucceeding, CommitHeaderRewriter = c => CommitRewriteInfo.From(c, message: ""), }, repo.Head.Commits); + + AssertSucceedingButNotError(); + Assert.NotEmpty(repo.Refs.Where(x => x.CanonicalName.StartsWith("refs/original"))); Assert.Empty(repo.Refs.Where(x => x.CanonicalName.StartsWith("refs/rewritten"))); repo.Refs.RewriteHistory(new RewriteHistoryOptions { + OnError = OnError, + OnSucceeding = OnSucceeding, BackupRefsNamespace = backupRefsNamespace, CommitHeaderRewriter = c => CommitRewriteInfo.From(c, message: "abc"), }, repo.Head.Commits); + AssertSucceedingButNotError(); + Assert.NotEmpty(repo.Refs.Where(x => x.CanonicalName.StartsWith("refs/rewritten"))); } @@ -277,10 +427,12 @@ public void RefRewritingRollsBackOnFailure() const string backupNamespace = "refs/original/"; - Assert.Throws( + var ex = Assert.Throws( () => repo.Refs.RewriteHistory(new RewriteHistoryOptions { + OnError = OnError, + OnSucceeding = OnSucceeding, BackupRefsNamespace = backupNamespace, CommitHeaderRewriter = c => CommitRewriteInfo.From(c, message: ""), @@ -305,6 +457,9 @@ public void RefRewritingRollsBackOnFailure() }, repo.Lookup("6dcf9bf7541ee10456529833502442f385010c3d")) ); + AssertErrorFired(ex); + AssertSucceedingNotFired(); + // Ensure all the refs have been restored to their original targets var newRefs = repo.Refs.OrderBy(r => r.CanonicalName).ToArray(); Assert.Equal(newRefs, origRefs); @@ -324,10 +479,14 @@ public void DoesNotRewriteRefsThatDontChange() { repo.Refs.RewriteHistory(new RewriteHistoryOptions { + OnError = OnError, + OnSucceeding = OnSucceeding, CommitHeaderRewriter = c => CommitRewriteInfo.From(c, message: "abc"), }, repo.Lookup("c47800c")); + AssertSucceedingButNotError(); + Assert.Null(repo.Refs["refs/original/heads/packed-test"]); Assert.NotNull(repo.Refs["refs/original/heads/br2"]); @@ -345,20 +504,31 @@ public void CanNotOverWriteBackedUpReferences() repo.Refs.RewriteHistory(new RewriteHistoryOptions { + OnError = OnError, + OnSucceeding = OnSucceeding, CommitHeaderRewriter = c => CommitRewriteInfo.From(c, message: "abc"), }, repo.Head.Commits); + + AssertSucceedingButNotError(); + var originalRefs = repo.Refs.FromGlob("refs/original/*").OrderBy(r => r.CanonicalName).ToArray(); Assert.NotEmpty(originalRefs); - Assert.Throws( + var ex = Assert.Throws( () => repo.Refs.RewriteHistory(new RewriteHistoryOptions { + OnError = OnError, + OnSucceeding = OnSucceeding, CommitHeaderRewriter = c => CommitRewriteInfo.From(c, message: "def"), }, repo.Head.Commits) ); + + AssertErrorFired(ex); + AssertSucceedingNotFired(); + Assert.Equal("abc\n", repo.Head.Tip.Message); var newOriginalRefs = repo.Refs.FromGlob("refs/original/*").OrderBy(r => r.CanonicalName).ToArray(); @@ -372,15 +542,20 @@ public void CanNotOverWriteAnExistingReference() { var commits = repo.Commits.QueryBy(new CommitFilter { Since = repo.Refs }).ToArray(); - Assert.Throws( + var ex = Assert.Throws( () => repo.Refs.RewriteHistory(new RewriteHistoryOptions { + OnError = OnError, + OnSucceeding = OnSucceeding, TagNameRewriter = (n, a, t) => "test", }, commits) ); + AssertErrorFired(ex); + AssertSucceedingNotFired(); + Assert.Equal(0, repo.Refs.FromGlob("refs/original/*").Count()); } @@ -413,6 +588,8 @@ public void CanRewriteParents() repo.Refs.RewriteHistory(new RewriteHistoryOptions { + OnError = OnError, + OnSucceeding = OnSucceeding, CommitParentsRewriter = c => { @@ -423,6 +600,8 @@ public void CanRewriteParents() }, }, commitToRewrite); + AssertSucceedingButNotError(); + Assert.Contains(newParent, repo.Lookup("refs/heads/test~").Parents); Assert.True(hasBeenCalled); } @@ -455,6 +634,8 @@ public void CanRewriteParentWithRewrittenCommit() repo.Refs.RewriteHistory(new RewriteHistoryOptions { + OnError = OnError, + OnSucceeding = OnSucceeding, CommitHeaderRewriter = c => c.Id != newParent.Id @@ -467,6 +648,8 @@ public void CanRewriteParentWithRewrittenCommit() : new[] { newParent } }, commitToRewrite, newParent); + AssertSucceedingButNotError(); + var rewrittenParent = repo.Lookup("refs/heads/test~").Parents.Single(); Assert.Equal(newParent.Tree, rewrittenParent.Tree); Assert.NotEqual(newParent, rewrittenParent); @@ -479,10 +662,14 @@ public void WritesCorrectReflogMessagesForSimpleRewrites() repo.Refs.RewriteHistory(new RewriteHistoryOptions { + OnError = OnError, + OnSucceeding = OnSucceeding, CommitHeaderRewriter = c => CommitRewriteInfo.From(c, message: ""), }, repo.Head.Commits); + AssertSucceedingButNotError(); + Assert.Equal("filter-branch: rewrite", repo.Refs.Log(repo.Refs["refs/heads/master"]).First().Message); Assert.Equal("filter-branch: backup", repo.Refs.Log(repo.Refs["refs/original/heads/master"]).First().Message); } @@ -496,11 +683,15 @@ public void CanProvideNewNamesForTags() repo.Refs.RewriteHistory(new RewriteHistoryOptions { + OnError = OnError, + OnSucceeding = OnSucceeding, CommitHeaderRewriter = c => CommitRewriteInfo.From(c, message: ""), TagNameRewriter = TagNameRewriter, }, repo.Commits.QueryBy(new CommitFilter { Since = repo.Refs["refs/heads/test"] })); + AssertSucceedingButNotError(); + Assert.NotEqual(lwTarget, repo.Tags["lw_new_e90810b"].Target); Assert.NotEqual(e908Target, repo.Tags["e90810b_new_7b43849"].Target); Assert.NotEqual(testTarget, repo.Tags["test_new_b25fa35"].Target); @@ -517,11 +708,15 @@ public void CanRewriteSymbolicRefsPointingToTags() repo.Refs.RewriteHistory(new RewriteHistoryOptions { + OnError = OnError, + OnSucceeding = OnSucceeding, CommitHeaderRewriter = c => CommitRewriteInfo.From(c, author: Constants.Signature), TagNameRewriter = TagNameRewriter, }, repo.Lookup("e90810b8df")); + AssertSucceedingButNotError(); + // Ensure the initial tags don't exist anymore... Assert.Null(repo.Refs["refs/tags/one_tracker"]); Assert.Null(repo.Refs["refs/tags/another_tracker"]); @@ -555,12 +750,16 @@ public void HandlesNameRewritingOfChainedTags() // Rewrite the commit, renaming the tag repo.Refs.RewriteHistory(new RewriteHistoryOptions { + OnError = OnError, + OnSucceeding = OnSucceeding, BackupRefsNamespace = "refs/original/", CommitHeaderRewriter = c => CommitRewriteInfo.From(c, message: "Rewrote"), TagNameRewriter = TagNameRewriter, }, repo.Lookup("6dcf9bf")); + AssertSucceedingButNotError(); + // Verify the rewritten tag-annotation chain var newTagA = repo.Tags["lightweightA_new_d53d92e"]; Assert.NotNull(newTagA); @@ -602,5 +801,39 @@ private static string TagNameRewriter(string name, bool isAnnotated, string targ return name + (isAnnotated ? "_ann_" : "_new_") + t; } + + private Action OnSucceeding + { + get + { + succeeding = false; + return () => succeeding = true; + } + } + + private void AssertSucceedingButNotError() + { + Assert.True(succeeding); + Assert.Null(error); + } + + private void AssertSucceedingNotFired() + { + Assert.False(succeeding); + } + + private Action OnError + { + get + { + error = null; + return ex => error = ex; + } + } + + private void AssertErrorFired(Exception ex) + { + Assert.Equal(ex, error); + } } } diff --git a/LibGit2Sharp.Tests/IgnoreFixture.cs b/LibGit2Sharp.Tests/IgnoreFixture.cs index ef91bd179..0ad15a3f4 100644 --- a/LibGit2Sharp.Tests/IgnoreFixture.cs +++ b/LibGit2Sharp.Tests/IgnoreFixture.cs @@ -16,15 +16,15 @@ public void TemporaryRulesShouldApplyUntilCleared() { Touch(repo.Info.WorkingDirectory, "Foo.cs", "Bar"); - Assert.True(repo.Index.RetrieveStatus().Untracked.Contains("Foo.cs")); + Assert.True(repo.Index.RetrieveStatus().Untracked.Select(s => s.FilePath).Contains("Foo.cs")); repo.Ignore.AddTemporaryRules(new[] { "*.cs" }); - Assert.False(repo.Index.RetrieveStatus().Untracked.Contains("Foo.cs")); + Assert.False(repo.Index.RetrieveStatus().Untracked.Select(s => s.FilePath).Contains("Foo.cs")); repo.Ignore.ResetAllTemporaryRules(); - Assert.True(repo.Index.RetrieveStatus().Untracked.Contains("Foo.cs")); + Assert.True(repo.Index.RetrieveStatus().Untracked.Select(s => s.FilePath).Contains("Foo.cs")); } } diff --git a/LibGit2Sharp.Tests/IndexFixture.cs b/LibGit2Sharp.Tests/IndexFixture.cs index 5b6f6e4fa..f6c1690a2 100644 --- a/LibGit2Sharp.Tests/IndexFixture.cs +++ b/LibGit2Sharp.Tests/IndexFixture.cs @@ -244,5 +244,39 @@ public void StagingAFileWhenTheIndexIsLockedThrowsALockedFileException() Assert.Throws(() => repo.Index.Stage("newfile")); } } + + [Fact] + public void CanCopeWithExternalChangesToTheIndex() + { + SelfCleaningDirectory scd = BuildSelfCleaningDirectory(); + + Touch(scd.DirectoryPath, "a.txt", "a\n"); + Touch(scd.DirectoryPath, "b.txt", "b\n"); + + string path = Repository.Init(scd.DirectoryPath); + + using (var repoWrite = new Repository(path)) + using (var repoRead = new Repository(path)) + { + var writeStatus = repoWrite.Index.RetrieveStatus(); + Assert.True(writeStatus.IsDirty); + Assert.Equal(0, repoWrite.Index.Count); + + var readStatus = repoRead.Index.RetrieveStatus(); + Assert.True(readStatus.IsDirty); + Assert.Equal(0, repoRead.Index.Count); + + repoWrite.Index.Stage("*"); + repoWrite.Commit("message", Constants.Signature, Constants.Signature); + + writeStatus = repoWrite.Index.RetrieveStatus(); + Assert.False(writeStatus.IsDirty); + Assert.Equal(2, repoWrite.Index.Count); + + readStatus = repoRead.Index.RetrieveStatus(); + Assert.False(readStatus.IsDirty); + Assert.Equal(2, repoRead.Index.Count); + } + } } } diff --git a/LibGit2Sharp.Tests/LibGit2Sharp.Tests.csproj b/LibGit2Sharp.Tests/LibGit2Sharp.Tests.csproj index 791bbf1d2..966b94e9f 100644 --- a/LibGit2Sharp.Tests/LibGit2Sharp.Tests.csproj +++ b/LibGit2Sharp.Tests/LibGit2Sharp.Tests.csproj @@ -58,12 +58,18 @@ + + + + + + @@ -71,11 +77,12 @@ + - + diff --git a/LibGit2Sharp.Tests/MergeFixture.cs b/LibGit2Sharp.Tests/MergeFixture.cs index 96df06a59..7bb5d54dd 100644 --- a/LibGit2Sharp.Tests/MergeFixture.cs +++ b/LibGit2Sharp.Tests/MergeFixture.cs @@ -43,7 +43,7 @@ public void SoftResetARepoWithUnmergedEntriesThrows() var headCommit = repo.Head.Tip; var firstCommitParent = headCommit.Parents.First(); Assert.Throws( - () => repo.Reset(ResetOptions.Soft, firstCommitParent)); + () => repo.Reset(ResetMode.Soft, firstCommitParent)); } } diff --git a/LibGit2Sharp.Tests/MetaFixture.cs b/LibGit2Sharp.Tests/MetaFixture.cs index d99f1fa9d..b582dd4dd 100644 --- a/LibGit2Sharp.Tests/MetaFixture.cs +++ b/LibGit2Sharp.Tests/MetaFixture.cs @@ -4,19 +4,44 @@ using System.Linq; using System.Reflection; using System.Text; +using LibGit2Sharp.Tests.TestHelpers; using Xunit; +using Xunit.Extensions; namespace LibGit2Sharp.Tests { public class MetaFixture { + [Fact] + public void PublicTestMethodsAreFactsOrTheories() + { + var exceptions = new[] + { + "LibGit2Sharp.Tests.FilterBranchFixture.Dispose", + }; + + var fixtures = from t in Assembly.GetAssembly(typeof(MetaFixture)).GetExportedTypes() + where t.IsPublic && !t.IsNested + where t.Namespace != typeof(BaseFixture).Namespace // Exclude helpers + let methods = t.GetMethods(BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public) + from m in methods + where !m.GetCustomAttributes(typeof(FactAttribute), false) + .Concat(m.GetCustomAttributes(typeof(TheoryAttribute), false)) + .Any() + let name = t.FullName + "." + m.Name + where !exceptions.Contains(name) + select name; + + Assert.Equal("", string.Join(Environment.NewLine, fixtures.ToArray())); + } + // Related to https://github.com/libgit2/libgit2sharp/pull/251 [Fact] public void TypesInLibGit2DecoratedWithDebuggerDisplayMustFollowTheStandardImplPattern() { var typesWithDebuggerDisplayAndInvalidImplPattern = new List(); - IEnumerable libGit2SharpTypes = Assembly.GetAssembly(typeof(Repository)).GetExportedTypes() + IEnumerable libGit2SharpTypes = Assembly.GetAssembly(typeof(IRepository)).GetExportedTypes() .Where(t => t.GetCustomAttributes(typeof(DebuggerDisplayAttribute), false).Any()); foreach (Type type in libGit2SharpTypes) @@ -56,8 +81,8 @@ public void TypesInLibGit2SharpMustBeExtensibleInATestingContext() { var nonTestableTypes = new Dictionary>(); - IEnumerable libGit2SharpTypes = Assembly.GetAssembly(typeof(Repository)).GetExportedTypes() - .Where(t => !t.IsSealed && t.Namespace == typeof(Repository).Namespace); + IEnumerable libGit2SharpTypes = Assembly.GetAssembly(typeof(IRepository)).GetExportedTypes() + .Where(t => !t.IsSealed && t.Namespace == typeof(IRepository).Namespace); foreach (Type type in libGit2SharpTypes) { @@ -83,10 +108,28 @@ public void TypesInLibGit2SharpMustBeExtensibleInATestingContext() } } + [Fact] + public void LibGit2SharpInterfacesCoverAllPublicMembers() + { + var methodsMissingFromInterfaces = + from t in Assembly.GetAssembly(typeof(IRepository)).GetExportedTypes() + where !t.IsInterface + where t.GetInterfaces().Any(i => i.Namespace == typeof(IRepository).Namespace) + let interfaceTargetMethods = from i in t.GetInterfaces() + from im in t.GetInterfaceMap(i).TargetMethods + select im + from tm in t.GetMethods(BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Instance) + where !interfaceTargetMethods.Contains(tm) + select t.Name + " has extra method " + tm.Name; + + Assert.Equal("", string.Join(Environment.NewLine, + methodsMissingFromInterfaces.ToArray())); + } + [Fact] public void EnumsWithFlagsHaveMutuallyExclusiveValues() { - var flagsEnums = Assembly.GetAssembly(typeof(Repository)).GetExportedTypes() + var flagsEnums = Assembly.GetAssembly(typeof(IRepository)).GetExportedTypes() .Where(t => t.IsEnum && t.GetCustomAttributes(typeof(FlagsAttribute), false).Any()); var overlaps = from t in flagsEnums diff --git a/LibGit2Sharp.Tests/MockedRepositoryFixture.cs b/LibGit2Sharp.Tests/MockingFixture.cs similarity index 52% rename from LibGit2Sharp.Tests/MockedRepositoryFixture.cs rename to LibGit2Sharp.Tests/MockingFixture.cs index fd3044ac6..13376fae1 100644 --- a/LibGit2Sharp.Tests/MockedRepositoryFixture.cs +++ b/LibGit2Sharp.Tests/MockingFixture.cs @@ -7,11 +7,12 @@ namespace LibGit2Sharp.Tests { - // This fixture shows how one can mock the IRepository when writing an application against LibGit2Sharp. - // The application we want to test is simulated by the CommitCounter class (see below), which takes an IRepository, - // and whose role is to compute and return the number of commits in the given repository. - public class MockedRepositoryFixture : BaseFixture + // This fixture shows how one can mock various LibGit2Sharp APIs. + public class MockingFixture : BaseFixture { + // The application we want to test is simulated by the CommitCounter class (see below), which takes an IRepository, + // and whose role is to compute and return the number of commits in the given repository. + // In this test, we pass to CommitCounter a concrete instance of the Repository. It means we will end up calling the concrete Repository // during the test run. [Fact] @@ -29,7 +30,7 @@ public void CanCountCommitsWithConcreteRepository() [Fact] public void CanCountCommitsWithMockedRepository() { - var commitLog = Mock.Of(cl => cl.GetEnumerator() == FakeCommitLog(17)); + var commitLog = Mock.Of(cl => cl.GetEnumerator() == FakeCommitLog(17)); var repo = Mock.Of(r => r.Commits == commitLog); var commitCounter = new CommitCounter(repo); @@ -67,5 +68,42 @@ public int NumberOfCommits get { return repo.Commits.Count(); } } } + + [Fact] + public void CanFakeConfigurationBuildSignature() + { + var name = "name"; + var email = "email"; + var now = DateTimeOffset.UtcNow; + + var fakeConfig = new Mock(); + fakeConfig.Setup(c => c.BuildSignature(now)) + .Returns(t => new Signature(name, email, t)); + + var sig = fakeConfig.Object.BuildSignature(now); + Assert.Equal(name, sig.Name); + Assert.Equal(email, sig.Email); + Assert.Equal(now, sig.When); + } + + [Fact] + public void CanFakeEnumerationOfConfiguration() + { + var fakeConfig = new Mock(); + fakeConfig.Setup(c => c.GetEnumerator()).Returns(FakeEntries); + + Assert.Equal(2, fakeConfig.Object.Count()); + } + + private static IEnumerator> FakeEntries() + { + yield return FakeConfigurationEntry("foo", "bar", ConfigurationLevel.Local); + yield return FakeConfigurationEntry("baz", "quux", ConfigurationLevel.Global); + } + + private static ConfigurationEntry FakeConfigurationEntry(string key, string value, ConfigurationLevel level) + { + return new Mock>(key, value, level).Object; + } } } diff --git a/LibGit2Sharp.Tests/NetworkFixture.cs b/LibGit2Sharp.Tests/NetworkFixture.cs index 870633aa8..d31805fc0 100644 --- a/LibGit2Sharp.Tests/NetworkFixture.cs +++ b/LibGit2Sharp.Tests/NetworkFixture.cs @@ -22,7 +22,45 @@ public void CanListRemoteReferences(string url) using (var repo = new Repository(repoPath)) { Remote remote = repo.Network.Remotes.Add(remoteName, url); - IEnumerable references = repo.Network.ListReferences(remote); + IList references = repo.Network.ListReferences(remote).ToList(); + + foreach (var directReference in references) + { + // None of those references point to an existing + // object in this brand new repository + Assert.Null(directReference.Target); + } + + List> actualRefs = references. + Select(directRef => new Tuple(directRef.CanonicalName, directRef.TargetIdentifier)).ToList(); + + Assert.Equal(ExpectedRemoteRefs.Count, actualRefs.Count); + for (int i = 0; i < ExpectedRemoteRefs.Count; i++) + { + Assert.Equal(ExpectedRemoteRefs[i].Item2, actualRefs[i].Item2); + Assert.Equal(ExpectedRemoteRefs[i].Item1, actualRefs[i].Item1); + } + } + } + + [Theory] + [InlineData("http://github.com/libgit2/TestGitRepository")] + [InlineData("https://github.com/libgit2/TestGitRepository")] + [InlineData("git://github.com/libgit2/TestGitRepository.git")] + public void CanListRemoteReferencesFromUrl(string url) + { + string repoPath = InitNewRepository(); + + using (var repo = new Repository(repoPath)) + { + IList references = repo.Network.ListReferences(url).ToList(); + + foreach (var directReference in references) + { + // None of those references point to an existing + // object in this brand new repository + Assert.Null(directReference.Target); + } List> actualRefs = references. Select(directRef => new Tuple(directRef.CanonicalName, directRef.TargetIdentifier)).ToList(); diff --git a/LibGit2Sharp.Tests/NoteFixture.cs b/LibGit2Sharp.Tests/NoteFixture.cs index e24eb00af..b9c026074 100644 --- a/LibGit2Sharp.Tests/NoteFixture.cs +++ b/LibGit2Sharp.Tests/NoteFixture.cs @@ -150,6 +150,28 @@ public void CreatingANoteWhichAlreadyExistsOverwritesThePreviousNote() } } + [Fact] + public void CanAddANoteWithSignatureFromConfig() + { + string configPath = CreateConfigurationWithDummyUser(Constants.Signature); + var options = new RepositoryOptions { GlobalConfigurationLocation = configPath }; + string path = CloneBareTestRepo(); + + using (var repo = new Repository(path, options)) + { + var commit = repo.Lookup("9fd738e8f7967c078dceed8190330fc8648ee56a"); + var note = repo.Notes.Add(commit.Id, "I'm batman!\n", "batmobile"); + + var newNote = commit.Notes.Single(); + Assert.Equal(note, newNote); + + Assert.Equal("I'm batman!\n", newNote.Message); + Assert.Equal("batmobile", newNote.Namespace); + + AssertCommitSignaturesAre(repo.Lookup("refs/notes/batmobile"), Constants.Signature); + } + } + [Fact] public void CanCompareTwoUniqueNotes() { @@ -225,6 +247,28 @@ public void RemovingANonExistingNoteDoesntThrow() } } + [Fact] + public void CanRemoveANoteWithSignatureFromConfig() + { + string configPath = CreateConfigurationWithDummyUser(Constants.Signature); + RepositoryOptions options = new RepositoryOptions() { GlobalConfigurationLocation = configPath }; + string path = CloneBareTestRepo(); + + using (var repo = new Repository(path, options)) + { + var commit = repo.Lookup("8496071c1b46c854b31185ea97743be6a8774479"); + var notes = repo.Notes[commit.Id]; + + Assert.NotEmpty(notes); + + repo.Notes.Remove(commit.Id, repo.Notes.DefaultNamespace); + + Assert.Empty(notes); + + AssertCommitSignaturesAre(repo.Lookup("refs/notes/" + repo.Notes.DefaultNamespace), Constants.Signature); + } + } + [Fact] public void CanRetrieveTheListOfNotesForAGivenNamespace() { diff --git a/LibGit2Sharp.Tests/ObjectDatabaseFixture.cs b/LibGit2Sharp.Tests/ObjectDatabaseFixture.cs index 66e13571b..9913aefc8 100644 --- a/LibGit2Sharp.Tests/ObjectDatabaseFixture.cs +++ b/LibGit2Sharp.Tests/ObjectDatabaseFixture.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Linq; using System.Text; using LibGit2Sharp.Tests.TestHelpers; using Xunit; @@ -68,7 +69,7 @@ public void CanCreateABlobIntoTheDatabaseOfABareRepository() Assert.NotNull(blob); Assert.Equal("dc53d4c6b8684c21b0b57db29da4a2afea011565", blob.Sha); - Assert.Equal("I'm a new file\n", blob.ContentAsText()); + Assert.Equal("I'm a new file\n", blob.GetContentText()); var fetchedBlob = repo.Lookup(blob.Id); Assert.Equal(blob, fetchedBlob); @@ -313,7 +314,7 @@ public void CanCreateATreeContainingAGitLinkFromAnUntrackedSubmoduleInTheWorking var commitWithSubmodule = repo.ObjectDatabase.CreateCommit("Submodule!", Constants.Signature, Constants.Signature, tree, new[] { repo.Head.Tip }); - repo.Reset(ResetOptions.Soft, commitWithSubmodule); + repo.Reset(ResetMode.Soft, commitWithSubmodule); var submodule = repo.Submodules[submodulePath]; Assert.NotNull(submodule); @@ -417,5 +418,119 @@ public void CanEnumerateTheGitObjectsFromBareRepository() Assert.True(count >= 1683); } } + + [Theory] + [InlineData("\0Leading zero")] + [InlineData("Trailing zero\0")] + [InlineData("Zero \0inside")] + [InlineData("\0")] + [InlineData("\0\0\0")] + public void CreatingACommitWithMessageContainingZeroByteThrows(string message) + { + using (var repo = new Repository(BareTestRepoPath)) + { + Assert.Throws(() => repo.ObjectDatabase.CreateCommit( + message, Constants.Signature, Constants.Signature, repo.Head.Tip.Tree, Enumerable.Empty())); + } + } + + [Theory] + [InlineData("\0Leading zero")] + [InlineData("Trailing zero\0")] + [InlineData("Zero \0inside")] + [InlineData("\0")] + [InlineData("\0\0\0")] + public void CreatingATagAnnotationWithNameOrMessageContainingZeroByteThrows(string input) + { + using (var repo = new Repository(BareTestRepoPath)) + { + Assert.Throws(() => repo.ObjectDatabase.CreateTagAnnotation( + input, repo.Head.Tip, Constants.Signature, "message")); + Assert.Throws(() => repo.ObjectDatabase.CreateTagAnnotation( + "name", repo.Head.Tip, Constants.Signature, input)); + } + } + + [Fact] + public void CreatingATagAnnotationWithBadParametersThrows() + { + using (var repo = new Repository(BareTestRepoPath)) + { + Assert.Throws(() => repo.ObjectDatabase.CreateTagAnnotation( + null, repo.Head.Tip, Constants.Signature, "message")); + Assert.Throws(() => repo.ObjectDatabase.CreateTagAnnotation( + string.Empty, repo.Head.Tip, Constants.Signature, "message")); + Assert.Throws(() => repo.ObjectDatabase.CreateTagAnnotation( + "name", null, Constants.Signature, "message")); + Assert.Throws(() => repo.ObjectDatabase.CreateTagAnnotation( + "name", repo.Head.Tip, null, "message")); + Assert.Throws(() => repo.ObjectDatabase.CreateTagAnnotation( + "name", repo.Head.Tip, Constants.Signature, null)); + } + } + + [Fact] + public void CanCreateATagAnnotationWithAnEmptyMessage() + { + string path = CloneBareTestRepo(); + using (var repo = new Repository(path)) + { + var tagAnnotation = repo.ObjectDatabase.CreateTagAnnotation( + "name", repo.Head.Tip, Constants.Signature, string.Empty); + + Assert.Equal(string.Empty, tagAnnotation.Message); + } + } + + [Theory] + [InlineData("c47800c", "9fd738e", "5b5b025", 1, 2)] + [InlineData("9fd738e", "c47800c", "5b5b025", 2, 1)] + public void CanCalculateHistoryDivergence( + string sinceSha, string untilSha, + string expectedAncestorSha, int? expectedAheadBy, int? expectedBehindBy) + { + using (var repo = new Repository(BareTestRepoPath)) + { + var since = repo.Lookup(sinceSha); + var until = repo.Lookup(untilSha); + + HistoryDivergence div = repo.ObjectDatabase.CalculateHistoryDivergence(since, until); + + Assert.Equal(expectedAheadBy, div.AheadBy); + Assert.Equal(expectedBehindBy, div.BehindBy); + Assert.Equal(expectedAncestorSha, div.CommonAncestor.Id.ToString(7)); + } + } + + [Theory] + [InlineData("c47800c", "41bc8c6907", 3, 2)] + public void CanCalculateHistoryDivergenceWhenNoAncestorIsShared( + string sinceSha, string untilSha, + int? expectedAheadBy, int? expectedBehindBy) + { + using (var repo = new Repository(BareTestRepoPath)) + { + var since = repo.Lookup(sinceSha); + var until = repo.Lookup(untilSha); + + HistoryDivergence div = repo.ObjectDatabase.CalculateHistoryDivergence(since, until); + + Assert.Equal(expectedAheadBy, div.AheadBy); + Assert.Equal(expectedBehindBy, div.BehindBy); + Assert.Null(div.CommonAncestor); + } + } + + [Fact] + public void CalculatingHistoryDivergenceWithBadParamsThrows() + { + using (var repo = new Repository(BareTestRepoPath)) + { + Assert.Throws( + () => repo.ObjectDatabase.CalculateHistoryDivergence(repo.Head.Tip, null)); + Assert.Throws( + () => repo.ObjectDatabase.CalculateHistoryDivergence(null, repo.Head.Tip)); + } + } } } diff --git a/LibGit2Sharp.Tests/ObjectIdFixture.cs b/LibGit2Sharp.Tests/ObjectIdFixture.cs index 2e62addd2..e118cfdb8 100644 --- a/LibGit2Sharp.Tests/ObjectIdFixture.cs +++ b/LibGit2Sharp.Tests/ObjectIdFixture.cs @@ -138,23 +138,19 @@ public void TryParse(string maybeSha, bool isValidSha) } [Theory] - [InlineData(new byte[] { 0xde, 0xad, 0xbe }, 6, true)] - [InlineData(new byte[] { 0xde, 0xad }, 4, true)] - [InlineData(new byte[] { 0xde, 0xad }, 3, true)] - [InlineData(new byte[] { 0xde, 0xad }, 2, true)] - [InlineData(new byte[] { 0xde, 0xad }, 1, true)] - [InlineData(new byte[] { 0xde, 0xaf }, 3, true)] - [InlineData(new byte[] { 0xde, 0xff }, 2, true)] - [InlineData(new byte[] { 0xdf, 0xff }, 1, true)] - [InlineData(new byte[] { 0x98, 0x76 }, 4, false)] - [InlineData(new byte[] { 0x98, 0x76 }, 3, false)] - [InlineData(new byte[] { 0x98, 0x76 }, 2, false)] - [InlineData(new byte[] { 0x98, 0x76 }, 1, false)] - public void StartsWith(byte[] rawId, int len, bool expected) + [InlineData("d", true)] + [InlineData("dead", true)] + [InlineData("deadbe", true)] + [InlineData("DeAdBEE", true)] + [InlineData("deff", false)] + [InlineData("dfff", false)] + [InlineData("9876", false)] + [InlineData("This is not a valid short hexified sha!!!!!", false)] + public void StartsWith(string shortSha, bool expected) { var id = new ObjectId("deadbeef84650f067bd5703b6a59a8b3b3c99a09"); - Assert.Equal(expected, id.StartsWith(rawId, len)); + Assert.Equal(expected, id.StartsWith(shortSha)); } } } diff --git a/LibGit2Sharp.Tests/OdbBackendFixture.cs b/LibGit2Sharp.Tests/OdbBackendFixture.cs index 35d573329..1304d1097 100644 --- a/LibGit2Sharp.Tests/OdbBackendFixture.cs +++ b/LibGit2Sharp.Tests/OdbBackendFixture.cs @@ -12,7 +12,7 @@ public class OdbBackendFixture : BaseFixture { private const string content = "test\n"; - private static Commit AddCommitToRepo(Repository repo) + private static Commit AddCommitToRepo(IRepository repo) { string relativeFilepath = "test.txt"; Touch(repo.Info.WorkingDirectory, relativeFilepath, content); @@ -37,7 +37,7 @@ private static Commit AddCommitToRepo(Repository repo) return commit; } - private static void AssertGeneratedShas(Repository repo) + private static void AssertGeneratedShas(IRepository repo) { Commit commit = repo.Commits.Single(); Assert.Equal("1fe3126578fc4eca68c193e4a3a0a14a0704624d", commit.Sha); @@ -219,9 +219,9 @@ public override int Read(ObjectId oid, out Stream data, out ObjectType objectTyp return (int)ReturnCode.GIT_OK; } - public override int ReadPrefix(byte[] shortOid, int len, out byte[] oid, out Stream data, out ObjectType objectType) + public override int ReadPrefix(string shortSha, out ObjectId id, out Stream data, out ObjectType objectType) { - oid = null; + id = null; data = null; objectType = default(ObjectType); @@ -229,7 +229,7 @@ public override int ReadPrefix(byte[] shortOid, int len, out byte[] oid, out Str foreach (ObjectId objectId in m_objectIdToContent.Keys) { - if (!objectId.StartsWith(shortOid, len)) + if (!objectId.StartsWith(shortSha)) { continue; } @@ -254,7 +254,7 @@ public override int ReadPrefix(byte[] shortOid, int len, out byte[] oid, out Str return ret; } - oid = matchingKey.RawId; + id = matchingKey; return (int)ReturnCode.GIT_OK; } diff --git a/LibGit2Sharp.Tests/PushFixture.cs b/LibGit2Sharp.Tests/PushFixture.cs index d8d8a2311..a3d77704f 100644 --- a/LibGit2Sharp.Tests/PushFixture.cs +++ b/LibGit2Sharp.Tests/PushFixture.cs @@ -13,7 +13,7 @@ private void OnPushStatusError(PushStatusError pushStatusErrors) pushStatusErrors.Reference, pushStatusErrors.Message)); } - private void AssertPush(Action push) + private void AssertPush(Action push) { var scd = BuildSelfCleaningDirectory(); @@ -60,9 +60,20 @@ private void AssertPush(Action push) [Fact] public void CanPushABranchTrackingAnUpstreamBranch() { + bool packBuilderCalled = false; + Handlers.PackBuilderProgressHandler packBuilderCb = (x, y, z) => { packBuilderCalled = true; return true; }; + AssertPush(repo => repo.Network.Push(repo.Head)); AssertPush(repo => repo.Network.Push(repo.Branches["master"])); - AssertPush(repo => repo.Network.Push(repo.Network.Remotes["origin"], "HEAD", @"refs/heads/master", OnPushStatusError)); + + PushOptions options = new PushOptions() + { + OnPushStatusError = OnPushStatusError, + OnPackBuilderProgress = packBuilderCb, + }; + + AssertPush(repo => repo.Network.Push(repo.Network.Remotes["origin"], "HEAD", @"refs/heads/master", options)); + Assert.True(packBuilderCalled); } [Fact] @@ -77,5 +88,80 @@ public void PushingABranchThatDoesNotTrackAnUpstreamBranchThrows() repo.Network.Push(branch); })); } + + [Fact] + public void CanForcePush() + { + string remoteRepoPath = InitNewRepository(true); + + // Create a new repository + string localRepoPath = InitNewRepository(); + using (var localRepo = new Repository(localRepoPath)) + { + // Add a commit + Commit first = AddCommitToRepo(localRepo); + + Remote remote = localRepo.Network.Remotes.Add("origin", remoteRepoPath); + + localRepo.Branches.Update(localRepo.Head, + b => b.Remote = remote.Name, + b => b.UpstreamBranch = localRepo.Head.CanonicalName); + + // Push this commit + localRepo.Network.Push(localRepo.Head); + AssertRemoteHeadTipEquals(localRepo, first.Sha); + + UpdateTheRemoteRepositoryWithANewCommit(remoteRepoPath); + + // Add another commit + Commit second = AddCommitToRepo(localRepo); + + // Try to fast forward push this new commit + Assert.Throws(() => localRepo.Network.Push(localRepo.Head)); + + // Force push the new commit + string pushRefSpec = string.Format("+{0}:{0}", localRepo.Head.CanonicalName); + localRepo.Network.Push(localRepo.Network.Remotes.Single(), pushRefSpec); + + AssertRemoteHeadTipEquals(localRepo, second.Sha); + } + } + + private static void AssertRemoteHeadTipEquals(IRepository localRepo, string sha) + { + var remoteReferences = localRepo.Network.ListReferences(localRepo.Network.Remotes.Single()); + DirectReference remoteHead = remoteReferences.Single(r => r.CanonicalName == "HEAD"); + + Assert.Equal(sha, remoteHead.TargetIdentifier); + } + + private void UpdateTheRemoteRepositoryWithANewCommit(string remoteRepoPath) + { + // Perform a fresh clone of the upstream repository + var scd = BuildSelfCleaningDirectory(); + string clonedRepoPath = Repository.Clone(remoteRepoPath, scd.DirectoryPath); + + using (var clonedRepo = new Repository(clonedRepoPath)) + { + // Add a commit + AddCommitToRepo(clonedRepo); + + // Push this new commit toward an upstream repository + clonedRepo.Network.Push(clonedRepo.Head); + } + } + + private Commit AddCommitToRepo(IRepository repository) + { + + string random = Guid.NewGuid().ToString(); + string filename = random + ".txt"; + + Touch(repository.Info.WorkingDirectory, filename, random); + + repository.Index.Stage(filename); + + return repository.Commit("New commit", Constants.Signature, Constants.Signature); + } } } diff --git a/LibGit2Sharp.Tests/RefSpecFixture.cs b/LibGit2Sharp.Tests/RefSpecFixture.cs new file mode 100644 index 000000000..673585e40 --- /dev/null +++ b/LibGit2Sharp.Tests/RefSpecFixture.cs @@ -0,0 +1,195 @@ +using System; +using System.Linq; +using LibGit2Sharp.Tests.TestHelpers; +using Xunit; +using Xunit.Extensions; + +namespace LibGit2Sharp.Tests +{ + public class RefSpecFixture : BaseFixture + { + [Fact] + public void CanCountRefSpecs() + { + var path = CloneStandardTestRepo(); + using (var repo = new Repository(path)) + { + var remote = repo.Network.Remotes["origin"]; + Assert.Equal(1, remote.RefSpecs.Count()); + } + } + + [Fact] + public void CanIterateOverRefSpecs() + { + var path = CloneStandardTestRepo(); + using (var repo = new Repository(path)) + { + var remote = repo.Network.Remotes["origin"]; + int count = 0; + foreach (RefSpec refSpec in remote.RefSpecs) + { + Assert.NotNull(refSpec); + count++; + } + Assert.Equal(1, count); + } + } + + [Fact] + public void FetchAndPushRefSpecsComposeRefSpecs() + { + var path = CloneStandardTestRepo(); + using (var repo = new Repository(path)) + { + var remote = repo.Network.Remotes["origin"]; + + var totalRefSpecs = remote.FetchRefSpecs.Concat(remote.PushRefSpecs); + var orderedRefSpecs = remote.RefSpecs.OrderBy(r => r.Direction == RefSpecDirection.Fetch ? 0 : 1); + Assert.Equal(orderedRefSpecs, totalRefSpecs); + } + } + + [Fact] + public void CanReadRefSpecDetails() + { + var path = CloneStandardTestRepo(); + using (var repo = new Repository(path)) + { + var remote = repo.Network.Remotes["origin"]; + + RefSpec refSpec = remote.RefSpecs.First(); + Assert.NotNull(refSpec); + + Assert.Equal("refs/heads/*", refSpec.Source); + Assert.Equal("refs/remotes/origin/*", refSpec.Destination); + Assert.Equal(true, refSpec.ForceUpdate); + } + } + + [Theory] + [InlineData(new string[] { "+refs/tags/*:refs/tags/*" }, new string[] { "refs/heads/*:refs/remotes/test/*", "+refs/abc:refs/def" })] + [InlineData(new string[] { "+refs/abc/x:refs/def/x", "refs/def:refs/ghi" }, new string[0])] + [InlineData(new string[0], new string[] { "refs/ghi:refs/jkl/mno" })] + public void CanReplaceRefSpecs(string[] newFetchRefSpecs, string[] newPushRefSpecs) + { + var path = CloneStandardTestRepo(); + using (var repo = new Repository(path)) + { + var remote = repo.Network.Remotes["origin"]; + var oldRefSpecs = remote.RefSpecs.ToList(); + + var newRemote = repo.Network.Remotes.Update(remote, + r => r.FetchRefSpecs = newFetchRefSpecs, r => r.PushRefSpecs = newPushRefSpecs); + + Assert.Equal(oldRefSpecs, remote.RefSpecs.ToList()); + + var actualNewFetchRefSpecs = newRemote.RefSpecs + .Where(s => s.Direction == RefSpecDirection.Fetch) + .Select(r => r.Specification) + .ToArray(); + Assert.Equal(newFetchRefSpecs, actualNewFetchRefSpecs); + + var actualNewPushRefSpecs = newRemote.RefSpecs + .Where(s => s.Direction == RefSpecDirection.Push) + .Select(r => r.Specification) + .ToArray(); + Assert.Equal(newPushRefSpecs, actualNewPushRefSpecs); + } + } + + [Fact] + public void RemoteUpdaterSavesRefSpecsPermanently() + { + var fetchRefSpecs = new string[] { "refs/their/heads/*:refs/my/heads/*", "+refs/their/tag:refs/my/tag" }; + + var path = CloneStandardTestRepo(); + using (var repo = new Repository(path)) + { + var remote = repo.Network.Remotes["origin"]; + repo.Network.Remotes.Update(remote, r => r.FetchRefSpecs = fetchRefSpecs); + } + + using (var repo = new Repository(path)) + { + var remote = repo.Network.Remotes["origin"]; + var actualRefSpecs = remote.RefSpecs + .Where(r => r.Direction == RefSpecDirection.Fetch) + .Select(r => r.Specification) + .ToArray(); + Assert.Equal(fetchRefSpecs, actualRefSpecs); + } + } + + [Fact] + public void CanAddAndRemoveRefSpecs() + { + string newRefSpec = "+refs/heads/test:refs/heads/other-test"; + + var path = CloneStandardTestRepo(); + using (var repo = new Repository(path)) + { + var remote = repo.Network.Remotes["origin"]; + + remote = repo.Network.Remotes.Update(remote, + r => r.FetchRefSpecs.Add(newRefSpec), + r => r.PushRefSpecs.Add(newRefSpec)); + + Assert.Contains(newRefSpec, remote.FetchRefSpecs.Select(r => r.Specification)); + Assert.Contains(newRefSpec, remote.PushRefSpecs.Select(r => r.Specification)); + + remote = repo.Network.Remotes.Update(remote, + r => r.FetchRefSpecs.Remove(newRefSpec), + r => r.PushRefSpecs.Remove(newRefSpec)); + + Assert.DoesNotContain(newRefSpec, remote.FetchRefSpecs.Select(r => r.Specification)); + Assert.DoesNotContain(newRefSpec, remote.PushRefSpecs.Select(r => r.Specification)); + } + } + + [Fact] + public void CanClearRefSpecs() + { + var path = CloneStandardTestRepo(); + using (var repo = new Repository(path)) + { + var remote = repo.Network.Remotes["origin"]; + + // Push refspec does not exist in cloned repository + remote = repo.Network.Remotes.Update(remote, r => r.PushRefSpecs.Add("+refs/test:refs/test")); + + remote = repo.Network.Remotes.Update(remote, + r => r.FetchRefSpecs.Clear(), + r => r.PushRefSpecs.Clear()); + + Assert.Empty(remote.FetchRefSpecs); + Assert.Empty(remote.PushRefSpecs); + Assert.Empty(remote.RefSpecs); + } + } + + [Theory] + [InlineData("refs/test:refs//double-slash")] + [InlineData("refs/trailing-slash/:refs/test")] + [InlineData("refs/.dotfile:refs/test")] + [InlineData("refs/.:refs/dotdir")] + [InlineData("refs/asterix:refs/not-matching/*")] + [InlineData("refs/double/*/asterix/*:refs/double/*asterix/*")] + [InlineData("refs/ whitespace:refs/test")] + public void SettingInvalidRefSpecsThrows(string refSpec) + { + var path = CloneStandardTestRepo(); + using (var repo = new Repository(path)) + { + var remote = repo.Network.Remotes["origin"]; + var oldRefSpecs = remote.RefSpecs.Select(r => r.Specification).ToList(); + + Assert.Throws(() => + repo.Network.Remotes.Update(remote, r => r.FetchRefSpecs.Add(refSpec))); + + var newRemote = repo.Network.Remotes["origin"]; + Assert.Equal(oldRefSpecs, newRemote.RefSpecs.Select(r => r.Specification).ToList()); + } + } + } +} diff --git a/LibGit2Sharp.Tests/ReferenceFixture.cs b/LibGit2Sharp.Tests/ReferenceFixture.cs index 9c4a4add9..a7d2bb19b 100644 --- a/LibGit2Sharp.Tests/ReferenceFixture.cs +++ b/LibGit2Sharp.Tests/ReferenceFixture.cs @@ -110,7 +110,7 @@ public void CanAddASymbolicReferenceFromTheTargetReference() } } - private static void AssertSymbolicRef(SymbolicReference newRef, Repository repo, string expectedTargetName, string expectedName) + private static void AssertSymbolicRef(SymbolicReference newRef, IRepository repo, string expectedTargetName, string expectedName) { Assert.NotNull(newRef); Assert.Equal(expectedName, newRef.CanonicalName); diff --git a/LibGit2Sharp.Tests/ReflogFixture.cs b/LibGit2Sharp.Tests/ReflogFixture.cs index b73333fc7..9b3b92df4 100644 --- a/LibGit2Sharp.Tests/ReflogFixture.cs +++ b/LibGit2Sharp.Tests/ReflogFixture.cs @@ -32,7 +32,7 @@ public void CanReadReflog() } [Fact] - public void CannotReadReflogOnUnknownReference() + public void ReadingReflogOfInvalidReferenceNameThrows() { using (var repo = new Repository(StandardTestRepoWorkingDirPath)) { diff --git a/LibGit2Sharp.Tests/RepositoryFixture.cs b/LibGit2Sharp.Tests/RepositoryFixture.cs index 01fa8e76a..bc5656acf 100644 --- a/LibGit2Sharp.Tests/RepositoryFixture.cs +++ b/LibGit2Sharp.Tests/RepositoryFixture.cs @@ -186,13 +186,13 @@ public void CanFetchFromRemoteByName() } // Perform the actual fetch - repo.Fetch(remote.Name, onUpdateTips: expectedFetchState.RemoteUpdateTipsHandler); + repo.Fetch(remote.Name, new FetchOptions { OnUpdateTips = expectedFetchState.RemoteUpdateTipsHandler }); // Verify the expected state expectedFetchState.CheckUpdatedReferences(repo); // Now fetch the rest of the tags - repo.Fetch(remote.Name, tagFetchMode: TagFetchMode.All); + repo.Fetch(remote.Name, new FetchOptions { TagFetchMode = TagFetchMode.All }); // Verify that the "nearly-dangling" tag is now in the repo. Tag nearlyDanglingTag = repo.Tags["nearly-dangling"]; @@ -224,7 +224,7 @@ public void CreatingRepoWithBadParamsThrows() Assert.Throws(() => Repository.Init(null)); } - private static void AssertInitializedRepository(Repository repo, string expectedHeadTargetIdentifier) + private static void AssertInitializedRepository(IRepository repo, string expectedHeadTargetIdentifier) { Assert.NotNull(repo.Info.Path); Assert.False(repo.Info.IsHeadDetached); diff --git a/LibGit2Sharp.Tests/RepositoryOptionsFixture.cs b/LibGit2Sharp.Tests/RepositoryOptionsFixture.cs index 64acdd04f..0a816bc76 100644 --- a/LibGit2Sharp.Tests/RepositoryOptionsFixture.cs +++ b/LibGit2Sharp.Tests/RepositoryOptionsFixture.cs @@ -127,7 +127,7 @@ private string MeanwhileInAnotherDimensionAnEvilMastermindIsAtWork(string workin { Assert.Equal(Path.GetFullPath(newWorkdir) + Path.DirectorySeparatorChar, Path.GetFullPath(sneakyRepo.Info.WorkingDirectory)); - sneakyRepo.Reset(ResetOptions.Mixed, sneakyRepo.Head.Tip.Sha); + sneakyRepo.Reset(ResetMode.Mixed, sneakyRepo.Head.Tip.Sha); const string filename = "zomg.txt"; Touch(sneakyRepo.Info.WorkingDirectory, filename, "I'm being sneaked in!\n"); diff --git a/LibGit2Sharp.Tests/ResetHeadFixture.cs b/LibGit2Sharp.Tests/ResetHeadFixture.cs index 641c6b483..c0b36e98d 100644 --- a/LibGit2Sharp.Tests/ResetHeadFixture.cs +++ b/LibGit2Sharp.Tests/ResetHeadFixture.cs @@ -18,7 +18,7 @@ public void ResetANewlyInitializedRepositoryThrows(bool isBare) using (var repo = new Repository(repoPath)) { - Assert.Throws(() => repo.Reset(ResetOptions.Soft)); + Assert.Throws(() => repo.Reset(ResetMode.Soft)); } } @@ -30,7 +30,7 @@ public void SoftResetToTheHeadOfARepositoryDoesNotChangeTheTargetOfTheHead() { Branch oldHead = repo.Head; - repo.Reset(ResetOptions.Soft); + repo.Reset(ResetMode.Soft); Assert.Equal(oldHead, repo.Head); } @@ -44,7 +44,7 @@ public void SoftResetToAParentCommitChangesTheTargetOfTheHead() { var headCommit = repo.Head.Tip; var firstCommitParent = headCommit.Parents.First(); - repo.Reset(ResetOptions.Soft, firstCommitParent); + repo.Reset(ResetMode.Soft, firstCommitParent); Assert.Equal(firstCommitParent, repo.Head.Tip); } @@ -57,7 +57,7 @@ public void SoftResetSetsTheHeadToTheDereferencedCommitOfAChainedTag() using (var repo = new Repository(path)) { Tag tag = repo.Tags["test"]; - repo.Reset(ResetOptions.Soft, tag.CanonicalName); + repo.Reset(ResetMode.Soft, tag.CanonicalName); Assert.Equal("e90810b8df3e80c413d903f631643c716887138d", repo.Head.Tip.Sha); } } @@ -67,11 +67,11 @@ public void ResettingWithBadParamsThrows() { using (var repo = new Repository(BareTestRepoPath)) { - Assert.Throws(() => repo.Reset(ResetOptions.Soft, (string)null)); - Assert.Throws(() => repo.Reset(ResetOptions.Soft, (Commit)null)); - Assert.Throws(() => repo.Reset(ResetOptions.Soft, "")); - Assert.Throws(() => repo.Reset(ResetOptions.Soft, Constants.UnknownSha)); - Assert.Throws(() => repo.Reset(ResetOptions.Soft, repo.Head.Tip.Tree.Sha)); + Assert.Throws(() => repo.Reset(ResetMode.Soft, (string)null)); + Assert.Throws(() => repo.Reset(ResetMode.Soft, (Commit)null)); + Assert.Throws(() => repo.Reset(ResetMode.Soft, "")); + Assert.Throws(() => repo.Reset(ResetMode.Soft, Constants.UnknownSha)); + Assert.Throws(() => repo.Reset(ResetMode.Soft, repo.Head.Tip.Tree.Sha)); } } @@ -110,7 +110,7 @@ private void AssertSoftReset(Func branchIdentifierRetriever, boo Assert.Equal(branch.Tip.Sha, repo.Head.Tip.Sha); /* Reset --soft the Head to a tag through its canonical name */ - repo.Reset(ResetOptions.Soft, tag.CanonicalName); + repo.Reset(ResetMode.Soft, tag.CanonicalName); Assert.Equal(expectedHeadName, repo.Head.Name); Assert.Equal(tag.Target.Id, repo.Head.Tip.Id); @@ -122,7 +122,7 @@ private void AssertSoftReset(Func branchIdentifierRetriever, boo oldHeadId); /* Reset --soft the Head to a commit through its sha */ - repo.Reset(ResetOptions.Soft, branch.Tip.Sha); + repo.Reset(ResetMode.Soft, branch.Tip.Sha); Assert.Equal(expectedHeadName, repo.Head.Name); Assert.Equal(branch.Tip.Sha, repo.Head.Tip.Sha); @@ -135,7 +135,7 @@ private void AssertSoftReset(Func branchIdentifierRetriever, boo } } - private static void FeedTheRepository(Repository repo) + private static void FeedTheRepository(IRepository repo) { string fullPath = Touch(repo.Info.WorkingDirectory, "a.txt", "Hello\n"); repo.Index.Stage(fullPath); @@ -167,7 +167,7 @@ public void MixedResetRefreshesTheIndex() Tag tag = repo.Tags["mytag"]; - repo.Reset(ResetOptions.Mixed, tag.CanonicalName); + repo.Reset(ResetMode.Mixed, tag.CanonicalName); Assert.Equal(FileStatus.Modified, repo.Index.RetrieveStatus("a.txt")); @@ -183,7 +183,7 @@ public void MixedResetInABareRepositoryThrows() { using (var repo = new Repository(BareTestRepoPath)) { - Assert.Throws(() => repo.Reset(ResetOptions.Mixed)); + Assert.Throws(() => repo.Reset(ResetMode.Mixed)); } } @@ -192,7 +192,7 @@ public void HardResetInABareRepositoryThrows() { using (var repo = new Repository(BareTestRepoPath)) { - Assert.Throws(() => repo.Reset(ResetOptions.Hard)); + Assert.Throws(() => repo.Reset(ResetMode.Hard)); } } @@ -209,7 +209,7 @@ public void HardResetUpdatesTheContentOfTheWorkingDirectory() Assert.True(names.Count > 4); - repo.Reset(ResetOptions.Hard, "HEAD~3"); + repo.Reset(ResetMode.Hard, "HEAD~3"); names = new DirectoryInfo(repo.Info.WorkingDirectory).GetFileSystemInfos().Select(fsi => fsi.Name).ToList(); names.Sort(StringComparer.Ordinal); diff --git a/LibGit2Sharp.Tests/ResetIndexFixture.cs b/LibGit2Sharp.Tests/ResetIndexFixture.cs index 121da8c74..23e6a1cc4 100644 --- a/LibGit2Sharp.Tests/ResetIndexFixture.cs +++ b/LibGit2Sharp.Tests/ResetIndexFixture.cs @@ -93,7 +93,7 @@ public void CanResetTheIndexToTheContentOfACommitWithCommittishAsArgument() "deleted_unstaged_file.txt", "modified_staged_file.txt", "modified_unstaged_file.txt" }; Assert.Equal(expected.Length, newStatus.Where(IsStaged).Count()); - Assert.Equal(expected, newStatus.Removed); + Assert.Equal(expected, newStatus.Removed.Select(s => s.FilePath)); } } @@ -111,7 +111,7 @@ public void CanResetTheIndexToTheContentOfACommitWithCommitAsArgument() "deleted_unstaged_file.txt", "modified_staged_file.txt", "modified_unstaged_file.txt" }; Assert.Equal(expected.Length, newStatus.Where(IsStaged).Count()); - Assert.Equal(expected, newStatus.Removed); + Assert.Equal(expected, newStatus.Removed.Select(s => s.FilePath)); } } diff --git a/LibGit2Sharp.Tests/Resources/expected_archives/commit_with_directory.tar b/LibGit2Sharp.Tests/Resources/expected_archives/commit_with_directory.tar new file mode 100644 index 000000000..0933834c5 Binary files /dev/null and b/LibGit2Sharp.Tests/Resources/expected_archives/commit_with_directory.tar differ diff --git a/LibGit2Sharp.Tests/Resources/expected_archives/tree_with_directory.tar b/LibGit2Sharp.Tests/Resources/expected_archives/tree_with_directory.tar new file mode 100644 index 000000000..f25d0d973 Binary files /dev/null and b/LibGit2Sharp.Tests/Resources/expected_archives/tree_with_directory.tar differ diff --git a/LibGit2Sharp.Tests/ShadowCopyFixture.cs b/LibGit2Sharp.Tests/ShadowCopyFixture.cs new file mode 100644 index 000000000..c53366859 --- /dev/null +++ b/LibGit2Sharp.Tests/ShadowCopyFixture.cs @@ -0,0 +1,111 @@ +using System; +using System.IO; +using System.Reflection; +using System.Security; +using System.Security.Permissions; +using LibGit2Sharp.Tests.TestHelpers; +using Xunit; + +namespace LibGit2Sharp.Tests +{ + public class ShadowCopyFixture : BaseFixture + { + [SkippableFact] + public void CanProbeForNativeBinariesFromAShadowCopiedAssembly() + { + Type type = typeof(Wrapper); + Assembly assembly = type.Assembly; + + // Build a new domain which will shadow copy assemblies + string cachePath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + Directory.CreateDirectory(cachePath); + + var setup = new AppDomainSetup + { + ApplicationBase = Path.GetDirectoryName(new Uri(assembly.CodeBase).LocalPath), + ApplicationName = "ShadowWalker", + ShadowCopyFiles = "true", + CachePath = cachePath + }; + + setup.ShadowCopyDirectories = setup.ApplicationBase; + + AppDomain domain = AppDomain.CreateDomain( + setup.ApplicationName, + null, + setup, new PermissionSet(PermissionState.Unrestricted)); + + // Instantiate from the remote domain + var wrapper = (Wrapper)domain.CreateInstanceAndUnwrap(assembly.FullName, type.FullName); + + // Ensure that LibGit2Sharp correctly probes for the native binaries + // from the other domain + string repoPath = BuildSelfCleaningDirectory().DirectoryPath; + wrapper.CanInitANewRepositoryFromAShadowCopiedAssembly(repoPath); + + Assembly sourceAssembly = typeof(IRepository).Assembly; + + // Ensure both assemblies share the same escaped code base... + string cachedAssemblyEscapedCodeBase = wrapper.AssemblyEscapedCodeBase; + Assert.Equal(sourceAssembly.EscapedCodeBase, cachedAssemblyEscapedCodeBase); + + // ...but are currently loaded from different locations... + string cachedAssemblyLocation = wrapper.AssemblyLocation; + if (cachedAssemblyLocation.StartsWith("/private")) + { + // On OS X, sometimes you get /private/var/… instead of /var/…, but they map to the same place. + cachedAssemblyLocation = cachedAssemblyLocation.Substring("/private".Length); + } + Assert.NotEqual(sourceAssembly.Location, cachedAssemblyLocation); + + // ...that the assembly in the other domain is stored in the shadow copy cache... + string cachedAssembliesPath = Path.Combine(setup.CachePath, setup.ApplicationName); + Assert.True(cachedAssemblyLocation.StartsWith(cachedAssembliesPath)); + + // ...that this cache doesn't contain the `NativeBinaries` folder + string cachedAssemblyParentPath = Path.GetDirectoryName(cachedAssemblyLocation); + Assert.False(Directory.Exists(Path.Combine(cachedAssemblyParentPath, "NativeBinaries"))); + + // ...whereas `NativeBinaries` of course exists next to the source assembly + string sourceAssemblyParentPath = Path.GetDirectoryName(new Uri(sourceAssembly.EscapedCodeBase).LocalPath); + Assert.True(Directory.Exists(Path.Combine(sourceAssemblyParentPath, "NativeBinaries"))); + + AppDomain.Unload(domain); + } + + public class Wrapper : MarshalByRefObject + { + private readonly Assembly assembly; + public Wrapper() + { + assembly = typeof(IRepository).Assembly; + } + + public void CanInitANewRepositoryFromAShadowCopiedAssembly(string path) + { + var gitDirPath = Repository.Init(path); + + using (var repo = new Repository(gitDirPath)) + { + Assert.NotNull(repo); + } + } + + public string AssemblyLocation + { + get + { + return assembly.Location; + } + } + + public string AssemblyEscapedCodeBase + { + get + { + return assembly.EscapedCodeBase; + } + } + } + } +} diff --git a/LibGit2Sharp.Tests/SignatureFixture.cs b/LibGit2Sharp.Tests/SignatureFixture.cs new file mode 100644 index 000000000..978141837 --- /dev/null +++ b/LibGit2Sharp.Tests/SignatureFixture.cs @@ -0,0 +1,47 @@ +using System; +using LibGit2Sharp.Tests.TestHelpers; +using Xunit; +using Xunit.Extensions; + +namespace LibGit2Sharp.Tests +{ + public class SignatureFixture : BaseFixture + { + [Theory] + [InlineData("\0Leading zero")] + [InlineData("Trailing zero\0")] + [InlineData("Zero \0inside")] + [InlineData("\0")] + [InlineData("\0\0\0")] + public void CreatingASignatureWithANameContainingZerosThrows(string name) + { + Assert.Throws(() => new Signature(name, "me@there.com", DateTimeOffset.Now)); + } + + [Theory] + [InlineData("\0Leading@zero.com")] + [InlineData("Trailing@zero.com\0")] + [InlineData("Zero@\0inside.com")] + [InlineData("\0")] + [InlineData("\0\0\0")] + public void CreatingASignatureWithAnEmailContainingZerosThrows(string email) + { + Assert.Throws(() => new Signature("Me", email, DateTimeOffset.Now)); + } + + [Fact] + public void CreatingASignatureWithBadParamsThrows() + { + Assert.Throws(() => new Signature(null, "me@there.com", DateTimeOffset.Now)); + Assert.Throws(() => new Signature(string.Empty, "me@there.com", DateTimeOffset.Now)); + Assert.Throws(() => new Signature("Me", null, DateTimeOffset.Now)); + } + + [Fact] + public void CanCreateASignatureWithAnEmptyEmail() + { + var sig = new Signature("Me", string.Empty, DateTimeOffset.Now); + Assert.Equal(string.Empty, sig.Email); + } + } +} diff --git a/LibGit2Sharp.Tests/StatusFixture.cs b/LibGit2Sharp.Tests/StatusFixture.cs index f9a1c487e..b70ff0ae0 100644 --- a/LibGit2Sharp.Tests/StatusFixture.cs +++ b/LibGit2Sharp.Tests/StatusFixture.cs @@ -20,6 +20,42 @@ public void CanRetrieveTheStatusOfAFile() } } + [Theory] + [InlineData(StatusShowOption.IndexAndWorkDir, FileStatus.Untracked)] + [InlineData(StatusShowOption.WorkDirOnly, FileStatus.Untracked)] + [InlineData(StatusShowOption.IndexOnly, FileStatus.Nonexistent)] + public void CanLimitStatusToWorkDirOnly(StatusShowOption show, FileStatus expected) + { + var clone = CloneStandardTestRepo(); + + using (var repo = new Repository(clone)) + { + Touch(repo.Info.WorkingDirectory, "file.txt", "content"); + + RepositoryStatus status = repo.Index.RetrieveStatus(new StatusOptions() { Show = show }); + Assert.Equal(expected, status["file.txt"].State); + } + } + + [Theory] + [InlineData(StatusShowOption.IndexAndWorkDir, FileStatus.Added)] + [InlineData(StatusShowOption.WorkDirOnly, FileStatus.Nonexistent)] + [InlineData(StatusShowOption.IndexOnly, FileStatus.Added)] + public void CanLimitStatusToIndexOnly(StatusShowOption show, FileStatus expected) + { + var clone = CloneStandardTestRepo(); + + using (var repo = new Repository(clone)) + { + Touch(repo.Info.WorkingDirectory, "file.txt", "content"); + repo.Index.Stage("file.txt"); + + RepositoryStatus status = repo.Index.RetrieveStatus(new StatusOptions() { Show = show }); + Assert.Equal(expected, status["file.txt"].State); + } + } + + [Theory] [InlineData("file")] [InlineData("file.txt")] @@ -75,18 +111,18 @@ public void CanRetrieveTheStatusOfTheWholeWorkingDirectory() RepositoryStatus status = repo.Index.RetrieveStatus(); - Assert.Equal(FileStatus.Staged, status[file]); + Assert.Equal(FileStatus.Staged, status[file].State); Assert.NotNull(status); Assert.Equal(6, status.Count()); Assert.True(status.IsDirty); - Assert.Equal("new_untracked_file.txt", status.Untracked.Single()); - Assert.Equal("modified_unstaged_file.txt", status.Modified.Single()); - Assert.Equal("deleted_unstaged_file.txt", status.Missing.Single()); - Assert.Equal("new_tracked_file.txt", status.Added.Single()); - Assert.Equal(file, status.Staged.Single()); - Assert.Equal("deleted_staged_file.txt", status.Removed.Single()); + Assert.Equal("new_untracked_file.txt", status.Untracked.Select(s => s.FilePath).Single()); + Assert.Equal("modified_unstaged_file.txt", status.Modified.Select(s => s.FilePath).Single()); + Assert.Equal("deleted_unstaged_file.txt", status.Missing.Select(s => s.FilePath).Single()); + Assert.Equal("new_tracked_file.txt", status.Added.Select(s => s.FilePath).Single()); + Assert.Equal(file, status.Staged.Select(s => s.FilePath).Single()); + Assert.Equal("deleted_staged_file.txt", status.Removed.Select(s => s.FilePath).Single()); File.AppendAllText(Path.Combine(repo.Info.WorkingDirectory, file), "Tclem's favorite commit message: boom"); @@ -94,18 +130,113 @@ public void CanRetrieveTheStatusOfTheWholeWorkingDirectory() Assert.Equal(FileStatus.Staged | FileStatus.Modified, repo.Index.RetrieveStatus(file)); RepositoryStatus status2 = repo.Index.RetrieveStatus(); - Assert.Equal(FileStatus.Staged | FileStatus.Modified, status2[file]); + Assert.Equal(FileStatus.Staged | FileStatus.Modified, status2[file].State); Assert.NotNull(status2); Assert.Equal(6, status2.Count()); Assert.True(status2.IsDirty); - Assert.Equal("new_untracked_file.txt", status2.Untracked.Single()); - Assert.Equal(new[] { file, "modified_unstaged_file.txt" }, status2.Modified); - Assert.Equal("deleted_unstaged_file.txt", status2.Missing.Single()); - Assert.Equal("new_tracked_file.txt", status2.Added.Single()); - Assert.Equal(file, status2.Staged.Single()); - Assert.Equal("deleted_staged_file.txt", status2.Removed.Single()); + Assert.Equal("new_untracked_file.txt", status2.Untracked.Select(s => s.FilePath).Single()); + Assert.Equal(new[] { file, "modified_unstaged_file.txt" }, status2.Modified.Select(s => s.FilePath)); + Assert.Equal("deleted_unstaged_file.txt", status2.Missing.Select(s => s.FilePath).Single()); + Assert.Equal("new_tracked_file.txt", status2.Added.Select(s => s.FilePath).Single()); + Assert.Equal(file, status2.Staged.Select(s => s.FilePath).Single()); + Assert.Equal("deleted_staged_file.txt", status2.Removed.Select(s => s.FilePath).Single()); + } + } + + [Fact] + public void CanRetrieveTheStatusOfRenamedFilesInWorkDir() + { + string path = CloneStandardTestRepo(); + using (var repo = new Repository(path)) + { + Touch(repo.Info.WorkingDirectory, "old_name.txt", + "This is a file with enough data to trigger similarity matching.\r\n" + + "This is a file with enough data to trigger similarity matching.\r\n" + + "This is a file with enough data to trigger similarity matching.\r\n" + + "This is a file with enough data to trigger similarity matching.\r\n"); + + repo.Index.Stage("old_name.txt"); + + File.Move(Path.Combine(repo.Info.WorkingDirectory, "old_name.txt"), + Path.Combine(repo.Info.WorkingDirectory, "rename_target.txt")); + + RepositoryStatus status = repo.Index.RetrieveStatus( + new StatusOptions() + { + DetectRenamesInIndex = true, + DetectRenamesInWorkDir = true + }); + + Assert.Equal(FileStatus.Added | FileStatus.RenamedInWorkDir, status["rename_target.txt"].State); + Assert.Equal(100, status["rename_target.txt"].IndexToWorkDirRenameDetails.Similarity); + } + } + + [Fact] + public void CanRetrieveTheStatusOfRenamedFilesInIndex() + { + string path = CloneStandardTestRepo(); + using (var repo = new Repository(path)) + { + File.Move( + Path.Combine(repo.Info.WorkingDirectory, "1.txt"), + Path.Combine(repo.Info.WorkingDirectory, "rename_target.txt")); + + repo.Index.Stage("1.txt"); + repo.Index.Stage("rename_target.txt"); + + RepositoryStatus status = repo.Index.RetrieveStatus(); + + Assert.Equal(FileStatus.RenamedInIndex, status["rename_target.txt"].State); + Assert.Equal(100, status["rename_target.txt"].HeadToIndexRenameDetails.Similarity); + } + } + + [Fact] + public void CanDetectedVariousKindsOfRenaming() + { + string path = InitNewRepository(); + using (var repo = new Repository(path)) + { + Touch(repo.Info.WorkingDirectory, "file.txt", + "This is a file with enough data to trigger similarity matching.\r\n" + + "This is a file with enough data to trigger similarity matching.\r\n" + + "This is a file with enough data to trigger similarity matching.\r\n" + + "This is a file with enough data to trigger similarity matching.\r\n"); + + repo.Index.Stage("file.txt"); + repo.Commit("Initial commit", Constants.Signature, Constants.Signature); + + File.Move(Path.Combine(repo.Info.WorkingDirectory, "file.txt"), + Path.Combine(repo.Info.WorkingDirectory, "renamed.txt")); + + var opts = new StatusOptions + { + DetectRenamesInIndex = true, + DetectRenamesInWorkDir = true + }; + + RepositoryStatus status = repo.Index.RetrieveStatus(opts); + + // This passes as expected + Assert.Equal(FileStatus.RenamedInWorkDir, status.Single().State); + + repo.Index.Stage("file.txt"); + repo.Index.Stage("renamed.txt"); + + status = repo.Index.RetrieveStatus(opts); + + Assert.Equal(FileStatus.RenamedInIndex, status.Single().State); + + File.Move(Path.Combine(repo.Info.WorkingDirectory, "renamed.txt"), + Path.Combine(repo.Info.WorkingDirectory, "renamed_again.txt")); + + status = repo.Index.RetrieveStatus(opts); + + Assert.Equal(FileStatus.RenamedInWorkDir | FileStatus.RenamedInIndex, + status.Single().State); } } @@ -154,7 +285,7 @@ public void RetrievingTheStatusOfARepositoryReturnNativeFilePaths() Assert.Equal(relFilePath, statusEntry.FilePath); - Assert.Equal(statusEntry.FilePath, repoStatus.Added.Single()); + Assert.Equal(statusEntry.FilePath, repoStatus.Added.Select(s => s.FilePath).Single()); } } @@ -169,15 +300,15 @@ public void RetrievingTheStatusOfAnEmptyRepositoryHonorsTheGitIgnoreDirectives() Touch(repo.Info.WorkingDirectory, relativePath, "I'm going to be ignored!"); RepositoryStatus status = repo.Index.RetrieveStatus(); - Assert.Equal(new[] { relativePath }, status.Untracked); + Assert.Equal(new[] { relativePath }, status.Untracked.Select(s => s.FilePath)); Touch(repo.Info.WorkingDirectory, ".gitignore", "*.txt" + Environment.NewLine); RepositoryStatus newStatus = repo.Index.RetrieveStatus(); - Assert.Equal(".gitignore", newStatus.Untracked.Single()); + Assert.Equal(".gitignore", newStatus.Untracked.Select(s => s.FilePath).Single()); Assert.Equal(FileStatus.Ignored, repo.Index.RetrieveStatus(relativePath)); - Assert.Equal(new[] { relativePath }, newStatus.Ignored); + Assert.Equal(new[] { relativePath }, newStatus.Ignored.Select(s => s.FilePath)); } } @@ -223,7 +354,7 @@ public void RetrievingTheStatusOfTheRepositoryHonorsTheGitIgnoreDirectives() RepositoryStatus status = repo.Index.RetrieveStatus(); - Assert.Equal(new[]{relativePath, "new_untracked_file.txt"}, status.Untracked); + Assert.Equal(new[] { relativePath, "new_untracked_file.txt" }, status.Untracked.Select(s => s.FilePath)); Touch(repo.Info.WorkingDirectory, ".gitignore", "*.txt" + Environment.NewLine); @@ -263,10 +394,10 @@ public void RetrievingTheStatusOfTheRepositoryHonorsTheGitIgnoreDirectives() */ RepositoryStatus newStatus = repo.Index.RetrieveStatus(); - Assert.Equal(".gitignore", newStatus.Untracked.Single()); + Assert.Equal(".gitignore", newStatus.Untracked.Select(s => s.FilePath).Single()); Assert.Equal(FileStatus.Ignored, repo.Index.RetrieveStatus(relativePath)); - Assert.Equal(new[] { relativePath, "new_untracked_file.txt" }, newStatus.Ignored); + Assert.Equal(new[] { relativePath, "new_untracked_file.txt" }, newStatus.Ignored.Select(s => s.FilePath)); } } @@ -354,7 +485,7 @@ public void RetrievingTheStatusOfTheRepositoryHonorsTheGitIgnoreDirectivesThroug Assert.Equal(FileStatus.Ignored, repo.Index.RetrieveStatus("bin/what-about-me.txt")); RepositoryStatus newStatus = repo.Index.RetrieveStatus(); - Assert.Equal(new[] { "bin" + dirSep }, newStatus.Ignored); + Assert.Equal(new[] { "bin" + dirSep }, newStatus.Ignored.Select(s => s.FilePath)); var sb = new StringBuilder(); sb.AppendLine("bin/*"); @@ -366,8 +497,43 @@ public void RetrievingTheStatusOfTheRepositoryHonorsTheGitIgnoreDirectivesThroug newStatus = repo.Index.RetrieveStatus(); - Assert.Equal(new[] { "bin" + dirSep + "look-ma.txt" }, newStatus.Ignored); - Assert.True(newStatus.Untracked.Contains("bin" + dirSep + "what-about-me.txt" )); + Assert.Equal(new[] { "bin" + dirSep + "look-ma.txt" }, newStatus.Ignored.Select(s => s.FilePath)); + Assert.True(newStatus.Untracked.Select(s => s.FilePath).Contains("bin" + dirSep + "what-about-me.txt")); + } + } + + [Fact] + public void CanRetrieveStatusOfFilesInSubmodule() + { + var path = CloneSubmoduleTestRepo(); + using (var repo = new Repository(path)) + { + string[] expected = new string[] { + ".gitmodules", + "sm_changed_file", + "sm_changed_head", + "sm_changed_index", + "sm_changed_untracked_file", + "sm_missing_commits" + }; + + RepositoryStatus status = repo.Index.RetrieveStatus(); + Assert.Equal(expected, status.Modified.Select(x => x.FilePath).ToArray()); + } + } + + [Fact] + public void CanExcludeStatusOfFilesInSubmodule() + { + var path = CloneSubmoduleTestRepo(); + using (var repo = new Repository(path)) + { + string[] expected = new string[] { + ".gitmodules", + }; + + RepositoryStatus status = repo.Index.RetrieveStatus(new StatusOptions() { ExcludeSubmodules = true }); + Assert.Equal(expected, status.Modified.Select(x => x.FilePath).ToArray()); } } } diff --git a/LibGit2Sharp.Tests/TestHelpers/BaseFixture.cs b/LibGit2Sharp.Tests/TestHelpers/BaseFixture.cs index 1a7d42d00..3d0faa4e3 100644 --- a/LibGit2Sharp.Tests/TestHelpers/BaseFixture.cs +++ b/LibGit2Sharp.Tests/TestHelpers/BaseFixture.cs @@ -4,6 +4,7 @@ using System.Globalization; using System.IO; using System.Linq; +using System.Reflection; using System.Text; using System.Text.RegularExpressions; using LibGit2Sharp.Core; @@ -19,6 +20,8 @@ static BaseFixture() { // Do the set up in the static ctor so it only happens once SetUpTestEnvironment(); + + DirectoryHelper.DeleteSubdirectories(Constants.TemporaryReposPath); } public static string BareTestRepoPath { get; private set; } @@ -78,6 +81,14 @@ private static bool IsFileSystemCaseSensitiveInternal() return !isInsensitive; } + // Should match LibGit2Sharp.Core.NativeMethods.IsRunningOnLinux() + protected static bool IsRunningOnLinux() + { + // see http://mono-project.com/FAQ%3a_Technical#Mono_Platforms + var p = (int)Environment.OSVersion.Platform; + return (p == 4) || (p == 6) || (p == 128); + } + protected void CreateCorruptedDeadBeefHead(string repoPath) { const string deadbeef = "deadbeef"; @@ -170,6 +181,42 @@ protected static void InconclusiveIf(Func predicate, string message) throw new SkipException(message); } + protected void RequiresDotNetOrMonoGreaterThanOrEqualTo(Version minimumVersion) + { + Type type = Type.GetType("Mono.Runtime"); + + if (type == null) + { + // We're running on top of .Net + return; + } + + MethodInfo displayName = type.GetMethod("GetDisplayName", BindingFlags.NonPublic | BindingFlags.Static); + + if (displayName == null) + { + throw new InvalidOperationException("Cannot access Mono.RunTime.GetDisplayName() method."); + } + + var version = (string) displayName.Invoke(null, null); + + Version current; + + try + { + current = new Version(version.Split(' ')[0]); + } + catch (Exception e) + { + throw new Exception(string.Format("Cannot parse Mono version '{0}'.", version), e); + } + + InconclusiveIf(() => current < minimumVersion, + string.Format( + "Current Mono version is {0}. Minimum required version to run this test is {1}.", + current, minimumVersion)); + } + protected static void AssertValueInConfigFile(string configFilePath, string regex) { var text = File.ReadAllText(configFilePath); @@ -218,6 +265,40 @@ private static RepositoryOptions BuildFakeRepositoryOptions(SelfCleaningDirector }; } + /// + /// Creates a configuration file with user.name and user.email set to signature + /// + /// The configuration file will be removed automatically when the tests are finished + /// The signature to use for user.name and user.email + /// The path to the configuration file + protected string CreateConfigurationWithDummyUser(Signature signature) + { + SelfCleaningDirectory scd = BuildSelfCleaningDirectory(); + Directory.CreateDirectory(scd.DirectoryPath); + string configFilePath = Path.Combine(scd.DirectoryPath, "global-config"); + + using (Configuration config = new Configuration(configFilePath)) + { + config.Set("user.name", signature.Name, ConfigurationLevel.Global); + config.Set("user.email", signature.Email, ConfigurationLevel.Global); + } + + return configFilePath; + } + + /// + /// Asserts that the commit has been authored and committed by the specified signature + /// + /// The commit + /// The signature to compare author and commiter to + protected void AssertCommitSignaturesAre(Commit commit, Signature signature) + { + Assert.Equal(signature.Name, commit.Author.Name); + Assert.Equal(signature.Email, commit.Author.Email); + Assert.Equal(signature.Name, commit.Committer.Name); + Assert.Equal(signature.Email, commit.Committer.Email); + } + protected static string Touch(string parent, string file, string content = null, Encoding encoding = null) { string filePath = Path.Combine(parent, file); @@ -231,6 +312,25 @@ protected static string Touch(string parent, string file, string content = null, return filePath; } + protected static string Touch(string parent, string file, Stream stream) + { + Debug.Assert(stream != null); + + string filePath = Path.Combine(parent, file); + string dir = Path.GetDirectoryName(filePath); + Debug.Assert(dir != null); + + Directory.CreateDirectory(dir); + + using (var fs = File.Open(filePath, FileMode.Create)) + { + CopyStream(stream, fs); + fs.Flush(); + } + + return filePath; + } + protected string Expected(string filename) { return File.ReadAllText(Path.Combine(ResourcesDirectory.FullName, "expected/" + filename)); @@ -241,7 +341,7 @@ protected string Expected(string filenameFormat, params object[] args) return Expected(string.Format(CultureInfo.InvariantCulture, filenameFormat, args)); } - protected static void AssertRefLogEntry(Repository repo, string canonicalName, + protected static void AssertRefLogEntry(IRepository repo, string canonicalName, ObjectId to, string message, ObjectId @from = null, Signature committer = null) { @@ -262,9 +362,35 @@ protected static void AssertRefLogEntry(Repository repo, string canonicalName, } } - protected static void EnableRefLog(Repository repository, bool enable = true) + protected static void EnableRefLog(IRepository repository, bool enable = true) { repository.Config.Set("core.logAllRefUpdates", enable); } + + public static void CopyStream(Stream input, Stream output) + { + // Reused from the following Stack Overflow post with permission + // of Jon Skeet (obtained on 25 Feb 2013) + // http://stackoverflow.com/questions/411592/how-do-i-save-a-stream-to-a-file/411605#411605 + var buffer = new byte[8 * 1024]; + int len; + while ((len = input.Read(buffer, 0, buffer.Length)) > 0) + { + output.Write(buffer, 0, len); + } + } + + public static bool StreamEquals(Stream one, Stream two) + { + int onebyte, twobyte; + + while ((onebyte = one.ReadByte()) >= 0 && (twobyte = two.ReadByte()) >= 0) + { + if (onebyte != twobyte) + return false; + } + + return true; + } } } diff --git a/LibGit2Sharp.Tests/TestHelpers/ExpectedFetchState.cs b/LibGit2Sharp.Tests/TestHelpers/ExpectedFetchState.cs index 722b88730..d02b5913a 100644 --- a/LibGit2Sharp.Tests/TestHelpers/ExpectedFetchState.cs +++ b/LibGit2Sharp.Tests/TestHelpers/ExpectedFetchState.cs @@ -78,8 +78,8 @@ public void AddExpectedTag(string tagName, ObjectId oldId, TestRemoteInfo.Expect /// Name of reference being updated. /// Old ID of reference. /// New ID of reference. - /// 0 on success; a negative value to abort the process. - public int RemoteUpdateTipsHandler(string referenceName, ObjectId oldId, ObjectId newId) + /// True to continue, false to cancel. + public bool RemoteUpdateTipsHandler(string referenceName, ObjectId oldId, ObjectId newId) { // assert that we have not seen this reference before Assert.DoesNotContain(referenceName, ObservedReferenceUpdates.Keys); @@ -97,14 +97,14 @@ public int RemoteUpdateTipsHandler(string referenceName, ObjectId oldId, ObjectI Assert.Equal(referenceUpdate.NewId, newId); } - return 0; + return true; } /// /// Check that all expected references have been updated. /// /// Repository object whose state will be checked against expected state. - public void CheckUpdatedReferences(Repository repo) + public void CheckUpdatedReferences(IRepository repo) { // Verify the expected branches. // First, verify the expected branches have been created and diff --git a/LibGit2Sharp.Tests/TestHelpers/OdbHelper.cs b/LibGit2Sharp.Tests/TestHelpers/OdbHelper.cs index 748a9a7dc..b1ae071a9 100644 --- a/LibGit2Sharp.Tests/TestHelpers/OdbHelper.cs +++ b/LibGit2Sharp.Tests/TestHelpers/OdbHelper.cs @@ -5,7 +5,7 @@ namespace LibGit2Sharp.Tests.TestHelpers { public static class OdbHelper { - public static Blob CreateBlob(Repository repo, string content) + public static Blob CreateBlob(IRepository repo, string content) { using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(content))) { diff --git a/LibGit2Sharp.Tests/TreeDefinitionFixture.cs b/LibGit2Sharp.Tests/TreeDefinitionFixture.cs index d4fee346a..b43e6131d 100644 --- a/LibGit2Sharp.Tests/TreeDefinitionFixture.cs +++ b/LibGit2Sharp.Tests/TreeDefinitionFixture.cs @@ -91,9 +91,13 @@ private static Mode ToMode(string expectedAttributes) [Theory] [InlineData("README", "README_TOO")] [InlineData("README", "1/README")] + [InlineData("README", "1/2/README")] [InlineData("1/branch_file.txt", "1/another_one.txt")] [InlineData("1/branch_file.txt", "another_one.txt")] [InlineData("1/branch_file.txt", "1/2/another_one.txt")] + [InlineData("1/branch_file.txt", "1/2/3/another_one.txt")] + [InlineData("1", "2")] + [InlineData("1", "2/3")] public void CanAddAnExistingTreeEntryDefinition(string sourcePath, string targetPath) { using (var repo = new Repository(BareTestRepoPath)) @@ -132,6 +136,58 @@ public void CanAddAnExistingGitLinkTreeEntryDefinition() } } + [Theory] + [InlineData("README", "README_TOO")] + [InlineData("README", "1/README")] + [InlineData("README", "1/2/README")] + [InlineData("1/branch_file.txt", "1/another_one.txt")] + [InlineData("1/branch_file.txt", "another_one.txt")] + [InlineData("1/branch_file.txt", "1/2/another_one.txt")] + [InlineData("1/branch_file.txt", "1/2/3/another_one.txt")] + [InlineData("1", "2")] + [InlineData("1", "2/3")] + public void CanAddAnExistingTreeEntry(string sourcePath, string targetPath) + { + using (var repo = new Repository(BareTestRepoPath)) + { + var tree = repo.Head.Tip.Tree; + var td = TreeDefinition.From(tree); + Assert.Null(td[targetPath]); + + var te = tree[sourcePath]; + td.Add(targetPath, te); + + var fetched = td[targetPath]; + Assert.NotNull(fetched); + + Assert.Equal(te.Target.Id, fetched.TargetId); + } + } + + [Theory] + [InlineData("sm_from_td")] + [InlineData("1/sm_from_td")] + [InlineData("1/2/sm_from_td")] + public void CanAddAnExistingGitLinkTreeEntry(string targetPath) + { + const string sourcePath = "sm_unchanged"; + + using (var repo = new Repository(SubmoduleTestRepoWorkingDirPath)) + { + var tree = repo.Head.Tip.Tree; + var td = TreeDefinition.From(tree); + Assert.Null(td[targetPath]); + + var te = tree[sourcePath]; + td.Add(targetPath, te); + + var fetched = td[targetPath]; + Assert.NotNull(fetched); + + Assert.Equal(te.Target.Id, fetched.TargetId); + } + } + [Theory] [InlineData("a8233120f6ad708f843d861ce2b7228ec4e3dec6", "README_TOO")] [InlineData("a8233120f6ad708f843d861ce2b7228ec4e3dec6", "1/README")] diff --git a/LibGit2Sharp.Tests/TreeFixture.cs b/LibGit2Sharp.Tests/TreeFixture.cs index a32419657..ced6f2788 100644 --- a/LibGit2Sharp.Tests/TreeFixture.cs +++ b/LibGit2Sharp.Tests/TreeFixture.cs @@ -200,5 +200,28 @@ public void CanRetrieveTreeEntryPath() Assert.NotSame(anotherInstance, anInstance); } } + + [Fact] + public void CanParseSymlinkTreeEntries() + { + var path = CloneBareTestRepo(); + + using (var repo = new Repository(path)) + { + Blob linkContent = OdbHelper.CreateBlob(repo, "1/branch_file.txt"); + + var td = TreeDefinition.From(repo.Head.Tip) + .Add("A symlink", linkContent, Mode.SymbolicLink); + + Tree t = repo.ObjectDatabase.CreateTree(td); + + var te = t["A symlink"]; + + Assert.NotNull(te); + + Assert.Equal(Mode.SymbolicLink, te.Mode); + Assert.Equal(linkContent, te.Target); + } + } } } diff --git a/LibGit2Sharp/ArchiverBase.cs b/LibGit2Sharp/ArchiverBase.cs new file mode 100644 index 000000000..8268c1075 --- /dev/null +++ b/LibGit2Sharp/ArchiverBase.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.IO; + +namespace LibGit2Sharp +{ + /// + /// The archiving method needs to be passed an inheritor of this class, which will then be used + /// to provide low-level archiving facilities (tar, zip, ...). + /// + /// + /// + /// + public abstract class ArchiverBase + { + /// + /// Override this method to perform operations before the archiving of each entry of the tree takes place. + /// + /// The tree that will be archived + /// The ObjectId of the commit being archived, or null if there is no commit. + /// The modification time that will be used for the files in the archive. + public virtual void BeforeArchiving(Tree tree, ObjectId oid, DateTimeOffset modificationTime) + { } + + /// + /// Override this method to perform operations after the archiving of each entry of the tree took place. + /// + /// The tree that was archived + /// The ObjectId of the commit being archived, or null if there is no commit. + /// The modification time that was used for the files in the archive. + public virtual void AfterArchiving(Tree tree, ObjectId oid, DateTimeOffset modificationTime) + { } + + internal void OrchestrateArchiving(Tree tree, ObjectId oid, DateTimeOffset modificationTime) + { + BeforeArchiving(tree, oid, modificationTime); + + ArchiveTree(tree, "", modificationTime); + + AfterArchiving(tree, oid, modificationTime); + } + + private void ArchiveTree(IEnumerable tree, string path, DateTimeOffset modificationTime) + { + foreach (var entry in tree) + { + AddTreeEntry(Path.Combine(path, entry.Name), entry, modificationTime); + + // Recurse if we have subtrees + if (entry.Mode == Mode.Directory) + { + ArchiveTree((Tree)entry.Target, Path.Combine(path, entry.Name), modificationTime); + } + } + } + + /// + /// Implements the archiving of a TreeEntry in a given format. + /// + /// The path of the entry in the archive. + /// The entry to archive. + /// The datetime the entry was last modified. + protected abstract void AddTreeEntry(string path, TreeEntry entry, DateTimeOffset modificationTime); + } +} diff --git a/LibGit2Sharp/BlameHunk.cs b/LibGit2Sharp/BlameHunk.cs new file mode 100644 index 000000000..de532ee3d --- /dev/null +++ b/LibGit2Sharp/BlameHunk.cs @@ -0,0 +1,173 @@ +using System; +using System.Diagnostics; +using System.Globalization; +using LibGit2Sharp.Core; +using LibGit2Sharp.Core.Compat; + +namespace LibGit2Sharp +{ + /// + /// A contiguous group of lines that have been traced to a single commit. + /// + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public class BlameHunk : IEquatable + { + private static readonly LambdaEqualityHelper equalityHelper = + new LambdaEqualityHelper(x => x.LineCount, + x => x.FinalStartLineNumber, + x => x.FinalSignature, + x => x.InitialStartLineNumber, + x => x.InitialSignature, + x => x.InitialCommit); + + + internal BlameHunk(IRepository repository, GitBlameHunk rawHunk) + { + finalCommit = new Lazy(() => repository.Lookup(rawHunk.FinalCommitId)); + origCommit = new Lazy(() => repository.Lookup(rawHunk.OrigCommitId)); + + if (rawHunk.OrigPath != IntPtr.Zero) + { + InitialPath = LaxUtf8Marshaler.FromNative(rawHunk.OrigPath); + } + LineCount = rawHunk.LinesInHunk; + + // Libgit2's line numbers are 1-based + FinalStartLineNumber = rawHunk.FinalStartLineNumber - 1; + InitialStartLineNumber = rawHunk.OrigStartLineNumber - 1; + + // Signature objects need to have ownership of their native pointers + if (rawHunk.FinalSignature != IntPtr.Zero) + { + FinalSignature = new Signature(NativeMethods.git_signature_dup(rawHunk.FinalSignature)); + } + if (rawHunk.OrigSignature != IntPtr.Zero) + { + InitialSignature = new Signature(NativeMethods.git_signature_dup(rawHunk.OrigSignature)); + } + } + + /// + /// For easier mocking + /// + protected BlameHunk() { } + + /// + /// Determine if this hunk contains a given line. + /// + /// Line number to test + /// True if this hunk contains the given line. + public virtual bool ContainsLine(int line) + { + return FinalStartLineNumber <= line && line < FinalStartLineNumber + LineCount; + } + + /// + /// Number of lines in this hunk. + /// + public virtual int LineCount { get; private set; } + + /// + /// The line number where this hunk begins, as of + /// + public virtual int FinalStartLineNumber { get; private set; } + + /// + /// Signature of the most recent change to this hunk. + /// + public virtual Signature FinalSignature { get; private set; } + + /// + /// Commit which most recently changed this file. + /// + public virtual Commit FinalCommit { get { return finalCommit.Value; } } + + /// + /// Line number where this hunk begins, as of , in . + /// + public virtual int InitialStartLineNumber { get; private set; } + + /// + /// Signature of the oldest-traced change to this hunk. + /// + public virtual Signature InitialSignature { get; private set; } + + /// + /// Commit to which the oldest change to this hunk has been traced. + /// + public virtual Commit InitialCommit { get { return origCommit.Value; } } + + /// + /// Path to the file where this hunk originated, as of . + /// + public virtual string InitialPath { get; private set; } + + private string DebuggerDisplay + { + get + { + return string.Format(CultureInfo.InvariantCulture, + "{0}-{1} ({2})", + FinalStartLineNumber, + FinalStartLineNumber+LineCount-1, + FinalCommit.ToString().Substring(0,7)); + } + } + + private readonly Lazy finalCommit; + private readonly Lazy origCommit; + + /// + /// Indicates whether the current object is equal to another object of the same type. + /// + /// + /// true if the current object is equal to the parameter; otherwise, false. + /// + /// An object to compare with this object. + public bool Equals(BlameHunk other) + { + return equalityHelper.Equals(this, other); + } + + /// + /// Determines whether the specified is equal to the current . + /// + /// The to compare with the current . + /// True if the specified is equal to the current ; otherwise, false. + public override bool Equals(object obj) + { + return Equals(obj as BlameHunk); + } + + /// + /// Returns the hash code for this instance. + /// + /// A 32-bit signed integer hash code. + public override int GetHashCode() + { + return equalityHelper.GetHashCode(); + } + + /// + /// Tests if two s are equal. + /// + /// First hunk to compare. + /// Second hunk to compare. + /// True if the two objects are equal; false otherwise. + public static bool operator ==(BlameHunk left, BlameHunk right) + { + return Equals(left, right); + } + + /// + /// Tests if two s are unequal. + /// + /// First hunk to compare. + /// Second hunk to compare. + /// True if the two objects are different; false otherwise. + public static bool operator !=(BlameHunk left, BlameHunk right) + { + return !Equals(left, right); + } + } +} diff --git a/LibGit2Sharp/BlameHunkCollection.cs b/LibGit2Sharp/BlameHunkCollection.cs new file mode 100644 index 000000000..18a3e24ba --- /dev/null +++ b/LibGit2Sharp/BlameHunkCollection.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using LibGit2Sharp.Core; +using LibGit2Sharp.Core.Handles; + +namespace LibGit2Sharp +{ + /// + /// The result of a blame operation. + /// + public class BlameHunkCollection : IEnumerable + { + private readonly IRepository repo; + private readonly List hunks = new List(); + + /// + /// For easy mocking + /// + protected BlameHunkCollection() { } + + internal BlameHunkCollection(Repository repo, RepositorySafeHandle repoHandle, string path, BlameOptions options) + { + this.repo = repo; + + var rawopts = new GitBlameOptions + { + version = 1, + flags = options.Strategy.ToGitBlameOptionFlags(), + MinLine = (uint)options.MinLine, + MaxLine = (uint)options.MaxLine, + }; + if (options.StartingAt != null) + { + rawopts.NewestCommit = repo.Committish(options.StartingAt).Oid; + } + if (options.StoppingAt != null) + { + rawopts.OldestCommit = repo.Committish(options.StoppingAt).Oid; + } + + using (var blameHandle = Proxy.git_blame_file(repoHandle, path, rawopts)) + { + var numHunks = NativeMethods.git_blame_get_hunk_count(blameHandle); + for (uint i = 0; i < numHunks; ++i) + { + var rawHunk = Proxy.git_blame_get_hunk_byindex(blameHandle, i); + hunks.Add(new BlameHunk(this.repo, rawHunk)); + } + } + } + + /// + /// Access blame hunks by index. + /// + /// The index of the hunk to retrieve + /// The at the given index. + public virtual BlameHunk this[int idx] + { + get { return hunks[idx]; } + } + + /// + /// Access blame hunks by the file line. + /// + /// Line number to search for + /// The that contains the specified file line. + public virtual BlameHunk HunkForLine(int line) + { + var hunk = hunks.FirstOrDefault(x => x.ContainsLine(line)); + if (hunk != null) + { + return hunk; + } + throw new ArgumentOutOfRangeException("line", "No hunk for that line"); + } + + /// + /// Returns an enumerator that iterates through a collection. + /// + /// + /// An object that can be used to iterate through the collection. + /// + /// 2 + public IEnumerator GetEnumerator() + { + return hunks.GetEnumerator(); + } + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } +} diff --git a/LibGit2Sharp/BlameOptions.cs b/LibGit2Sharp/BlameOptions.cs new file mode 100644 index 000000000..1fa41a1d2 --- /dev/null +++ b/LibGit2Sharp/BlameOptions.cs @@ -0,0 +1,64 @@ +using System; + +namespace LibGit2Sharp +{ + /// + /// Strategy used for blaming. + /// + public enum BlameStrategy + { + /// + /// Track renames of the file, but no block movement. + /// + Default, + + // Track copies within the same file. (NOT SUPPORTED IN LIBGIT2 YET) + //TrackCopiesSameFile, + + // Track movement across files within the same commit. (NOT SUPPORTED IN LIBGIT2 YET) + //TrackCopiesSameCommitMoves, + + // Track copies across files within the same commit. (NOT SUPPORTED IN LIBGIT2 YET) + //TrackCopiesSameCommitCopies, + + // Track copies across all files in all commits. (NOT SUPPORTED IN LIBGIT2 YET) + //TrackCopiesAnyCommitCopies + } + + /// + /// Optional adjustments to the behavior of blame. + /// + public sealed class BlameOptions + { + /// + /// Strategy to use to determine the blame for each line. + /// The default is . + /// + public BlameStrategy Strategy { get; set; } + + /// + /// Latest commitish to consider (the starting point). + /// If null, blame will use HEAD. + /// + public object StartingAt { get; set; } + + /// + /// Oldest commitish to consider (the stopping point). + /// If null, blame will continue until all the lines have been blamed, + /// or until a commit with no parents is reached. + /// + public object StoppingAt { get; set; } + + /// + /// First text line in the file to blame (lines start at 1). + /// If this is set to 0, the blame begins at line 1. + /// + public int MinLine { get; set; } + + /// + /// Last text line in the file to blame (lines start at 1). + /// If this is set to 0, blame ends with the last line in the file. + /// + public int MaxLine { get; set; } + } +} diff --git a/LibGit2Sharp/Blob.cs b/LibGit2Sharp/Blob.cs index fd17d203c..88c5f3b10 100644 --- a/LibGit2Sharp/Blob.cs +++ b/LibGit2Sharp/Blob.cs @@ -26,7 +26,7 @@ internal Blob(Repository repo, ObjectId id) } /// - /// Gets the size in bytes of the contents of a blob + /// Gets the size in bytes of the raw content of a blob. /// public virtual int Size { get { return (int)lazySize.Value; } } @@ -38,6 +38,7 @@ internal Blob(Repository repo, ObjectId id) /// /// Gets the blob content in a array. /// + [Obsolete("This property will be removed in the next release. Please use one of the GetContentStream() overloads instead.")] public virtual byte[] Content { get @@ -49,11 +50,31 @@ public virtual byte[] Content /// /// Gets the blob content in a . /// + public virtual Stream GetContentStream() + { + return Proxy.git_blob_rawcontent_stream(repo.Handle, Id, Size); + } + + /// + /// Gets the blob content in a as it would be + /// checked out to the working directory. + /// Parameter controlling content filtering behavior + /// + public virtual Stream GetContentStream(FilteringOptions filteringOptions) + { + Ensure.ArgumentNotNull(filteringOptions, "filteringOptions"); + return Proxy.git_blob_filtered_content_stream(repo.Handle, Id, filteringOptions.HintPath, false); + } + + /// + /// Gets the blob content in a . + /// + [Obsolete("This property will be removed in the next release. Please use one of the GetContentStream() overloads instead.")] public virtual Stream ContentStream { get { - return Proxy.git_blob_rawcontent_stream(repo.Handle, Id, Size); + return GetContentStream(); } } } diff --git a/LibGit2Sharp/BlobExtensions.cs b/LibGit2Sharp/BlobExtensions.cs index a00208ec6..138cbdf9c 100644 --- a/LibGit2Sharp/BlobExtensions.cs +++ b/LibGit2Sharp/BlobExtensions.cs @@ -18,11 +18,62 @@ public static class BlobExtensions /// The blob for which the content will be returned. /// The encoding of the text. (default: detected or UTF8) /// Blob content as text. + [Obsolete("This method will be removed in the next release. Please use one of the GetContentText() overloads instead.")] public static string ContentAsText(this Blob blob, Encoding encoding = null) + { + return GetContentText(blob, encoding); + } + + /// + /// Gets the blob content as it would be checked out to the + /// working directory, decoded with the specified encoding, + /// or according to byte order marks, with UTF8 as fallback, + /// if is null. + /// + /// The blob for which the content will be returned. + /// Parameter controlling content filtering behavior + /// The encoding of the text. (default: detected or UTF8) + /// Blob content as text. + [Obsolete("This method will be removed in the next release. Please use one of the GetContentText() overloads instead.")] + public static string ContentAsText(this Blob blob, FilteringOptions filteringOptions, Encoding encoding = null) + { + return GetContentText(blob, filteringOptions, encoding); + } + + /// + /// Gets the blob content decoded with the specified encoding, + /// or according to byte order marks, with UTF8 as fallback, + /// if is null. + /// + /// The blob for which the content will be returned. + /// The encoding of the text. (default: detected or UTF8) + /// Blob content as text. + public static string GetContentText(this Blob blob, Encoding encoding = null) + { + Ensure.ArgumentNotNull(blob, "blob"); + + using (var reader = new StreamReader(blob.GetContentStream(), encoding ?? Encoding.UTF8, encoding == null)) + { + return reader.ReadToEnd(); + } + } + + /// + /// Gets the blob content as it would be checked out to the + /// working directory, decoded with the specified encoding, + /// or according to byte order marks, with UTF8 as fallback, + /// if is null. + /// + /// The blob for which the content will be returned. + /// Parameter controlling content filtering behavior + /// The encoding of the text. (default: detected or UTF8) + /// Blob content as text. + public static string GetContentText(this Blob blob, FilteringOptions filteringOptions, Encoding encoding = null) { Ensure.ArgumentNotNull(blob, "blob"); + Ensure.ArgumentNotNull(filteringOptions, "filteringOptions"); - using (var reader = new StreamReader(blob.ContentStream, encoding ?? Encoding.UTF8, encoding == null)) + using (var reader = new StreamReader(blob.GetContentStream(filteringOptions), encoding ?? Encoding.UTF8, encoding == null)) { return reader.ReadToEnd(); } diff --git a/LibGit2Sharp/BranchCollection.cs b/LibGit2Sharp/BranchCollection.cs index 575bc2b59..3c02ee1d4 100644 --- a/LibGit2Sharp/BranchCollection.cs +++ b/LibGit2Sharp/BranchCollection.cs @@ -91,9 +91,8 @@ private Branch BuildFromReferenceName(string canonicalName) /// An object that can be used to iterate through the collection. public virtual IEnumerator GetEnumerator() { - return Proxy.git_branch_foreach(repo.Handle, GitBranchType.GIT_BRANCH_LOCAL | GitBranchType.GIT_BRANCH_REMOTE, branchToCanoncialName) - .Select(n => this[n]) - .GetEnumerator(); + return Proxy.git_branch_iterator(repo, GitBranchType.GIT_BRANCH_LOCAL | GitBranchType.GIT_BRANCH_REMOTE) + .ToList().GetEnumerator(); } /// @@ -203,21 +202,6 @@ private static bool LooksLikeABranchName(string referenceName) referenceName.LooksLikeRemoteTrackingBranch(); } - private static string branchToCanoncialName(IntPtr namePtr, GitBranchType branchType) - { - string shortName = Utf8Marshaler.FromNative(namePtr); - - switch (branchType) - { - case GitBranchType.GIT_BRANCH_LOCAL: - return ShortToLocalName(shortName); - case GitBranchType.GIT_BRANCH_REMOTE: - return ShortToRemoteName(shortName); - default: - return shortName; - } - } - private string DebuggerDisplay { get diff --git a/LibGit2Sharp/BranchTrackingDetails.cs b/LibGit2Sharp/BranchTrackingDetails.cs index 0acce526e..ad7153c88 100644 --- a/LibGit2Sharp/BranchTrackingDetails.cs +++ b/LibGit2Sharp/BranchTrackingDetails.cs @@ -1,5 +1,4 @@ -using LibGit2Sharp.Core; -using LibGit2Sharp.Core.Compat; +using System; namespace LibGit2Sharp { @@ -8,10 +7,7 @@ namespace LibGit2Sharp /// public class BranchTrackingDetails { - private readonly Repository repo; - private readonly Branch branch; - private readonly Lazy> aheadBehind; - private readonly Lazy commonAncestor; + private readonly HistoryDivergence historyDivergence; /// /// Needed for mocking purposes. @@ -21,11 +17,13 @@ protected BranchTrackingDetails() internal BranchTrackingDetails(Repository repo, Branch branch) { - this.repo = repo; - this.branch = branch; + if (!branch.IsTracking || branch.Tip == null || branch.TrackedBranch.Tip == null) + { + historyDivergence = new NullHistoryDivergence(); + return; + } - aheadBehind = new Lazy>(ResolveAheadBehind); - commonAncestor = new Lazy(ResolveCommonAncestor); + historyDivergence = repo.ObjectDatabase.CalculateHistoryDivergence(branch.Tip, branch.TrackedBranch.Tip); } /// @@ -37,7 +35,7 @@ internal BranchTrackingDetails(Repository repo, Branch branch) /// public virtual int? AheadBy { - get { return aheadBehind.Value.Item1; } + get { return historyDivergence.AheadBy; } } /// @@ -49,7 +47,7 @@ public virtual int? AheadBy /// public virtual int? BehindBy { - get { return aheadBehind.Value.Item2; } + get { return historyDivergence.BehindBy; } } /// @@ -61,29 +59,7 @@ public virtual int? BehindBy /// public virtual Commit CommonAncestor { - get { return commonAncestor.Value; } - } - - private Tuple ResolveAheadBehind() - { - return branch.IsTracking - ? Proxy.git_graph_ahead_behind(repo.Handle, branch.TrackedBranch.Tip, branch.Tip) - : new Tuple(null, null); - } - - private Commit ResolveCommonAncestor() - { - if (!branch.IsTracking) - { - return null; - } - - if (branch.Tip == null || branch.TrackedBranch.Tip == null) - { - return null; - } - - return repo.Commits.FindCommonAncestor(branch.Tip, branch.TrackedBranch.Tip); + get { return historyDivergence.CommonAncestor; } } } } diff --git a/LibGit2Sharp/Changes.cs b/LibGit2Sharp/Changes.cs deleted file mode 100644 index 0a1a5f014..000000000 --- a/LibGit2Sharp/Changes.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System.Diagnostics; -using System.Globalization; -using System.Text; - -namespace LibGit2Sharp -{ - /// - /// Base class for changes. - /// - [DebuggerDisplay("{DebuggerDisplay,nq}")] - public abstract class Changes - { - private readonly StringBuilder patchBuilder = new StringBuilder(); - - internal void AppendToPatch(string patch) - { - patchBuilder.Append(patch); - } - - /// - /// The number of lines added. - /// - public virtual int LinesAdded { get; internal set; } - - /// - /// The number of lines deleted. - /// - public virtual int LinesDeleted { get; internal set; } - - /// - /// The patch corresponding to these changes. - /// - public virtual string Patch - { - get { return patchBuilder.ToString(); } - } - - /// - /// Determines if at least one side of the comparison holds binary content. - /// - public virtual bool IsBinaryComparison { get; protected set; } - - private string DebuggerDisplay - { - get - { - return string.Format(CultureInfo.InvariantCulture, - @"{{+{0}, -{1}}}", LinesAdded, LinesDeleted); - } - } - } -} diff --git a/LibGit2Sharp/CheckoutCallbacks.cs b/LibGit2Sharp/CheckoutCallbacks.cs index 56a0c8ad9..fa8817b81 100644 --- a/LibGit2Sharp/CheckoutCallbacks.cs +++ b/LibGit2Sharp/CheckoutCallbacks.cs @@ -86,9 +86,9 @@ private void OnGitCheckoutProgress(IntPtr str, UIntPtr completedSteps, UIntPtr t if (onCheckoutProgress != null) { // Convert null strings into empty strings. - string path = (str != IntPtr.Zero) ? Utf8Marshaler.FromNative(str) : string.Empty; + FilePath path = LaxFilePathMarshaler.FromNative(str) ?? FilePath.Empty; - onCheckoutProgress(path, (int)completedSteps, (int)totalSteps); + onCheckoutProgress(path.Native, (int)completedSteps, (int)totalSteps); } } @@ -100,14 +100,14 @@ private int OnGitCheckoutNotify( IntPtr workdirPtr, IntPtr payloadPtr) { - int result = 0; + bool result = true; if (this.onCheckoutNotify != null) { - string path = (pathPtr != IntPtr.Zero) ? FilePathMarshaler.FromNative(pathPtr).Native : string.Empty; - result = onCheckoutNotify(path, why) ? 0 : 1; + FilePath path = LaxFilePathMarshaler.FromNative(pathPtr) ?? FilePath.Empty; + result = onCheckoutNotify(path.Native, why); } - return result; + return Proxy.ConvertResultToCancelFlag(result); } } } diff --git a/LibGit2Sharp/CommitFilter.cs b/LibGit2Sharp/CommitFilter.cs index 43d26bbb9..5021c1260 100644 --- a/LibGit2Sharp/CommitFilter.cs +++ b/LibGit2Sharp/CommitFilter.cs @@ -16,6 +16,7 @@ public CommitFilter() { SortBy = CommitSortStrategies.Time; Since = "HEAD"; + FirstParentOnly = false; } /// @@ -57,6 +58,11 @@ internal IList UntilList get { return ToList(Until); } } + /// + /// Whether to limit the walk to each commit's first parent, instead of all of them + /// + public bool FirstParentOnly { get; set; } + private static IList ToList(object obj) { var list = new List(); diff --git a/LibGit2Sharp/CommitLog.cs b/LibGit2Sharp/CommitLog.cs index 939cb6f1d..2c59d6a5b 100644 --- a/LibGit2Sharp/CommitLog.cs +++ b/LibGit2Sharp/CommitLog.cs @@ -11,17 +11,11 @@ namespace LibGit2Sharp /// /// A log of commits in a /// - public class CommitLog : IQueryableCommitLog + public sealed class CommitLog : IQueryableCommitLog { private readonly Repository repo; private readonly CommitFilter queryFilter; - /// - /// Needed for mocking purposes. - /// - protected CommitLog() - { } - /// /// Initializes a new instance of the class. /// The commits will be enumerated according in reverse chronological order. @@ -46,7 +40,7 @@ internal CommitLog(Repository repo, CommitFilter queryFilter) /// /// Gets the current sorting strategy applied when enumerating the log /// - public virtual CommitSortStrategies SortedBy + public CommitSortStrategies SortedBy { get { return queryFilter.SortBy; } } @@ -57,7 +51,7 @@ public virtual CommitSortStrategies SortedBy /// Returns an enumerator that iterates through the log. /// /// An object that can be used to iterate through the log. - public virtual IEnumerator GetEnumerator() + public IEnumerator GetEnumerator() { return new CommitEnumerator(repo, queryFilter); } @@ -78,7 +72,7 @@ IEnumerator IEnumerable.GetEnumerator() /// /// The options used to control which commits will be returned. /// A list of commits, ready to be enumerated. - public virtual ICommitLog QueryBy(CommitFilter filter) + public ICommitLog QueryBy(CommitFilter filter) { Ensure.ArgumentNotNull(filter, "filter"); Ensure.ArgumentNotNull(filter.Since, "filter.Since"); @@ -93,7 +87,7 @@ public virtual ICommitLog QueryBy(CommitFilter filter) /// The first . /// The second . /// The common ancestor or null if none found. - public virtual Commit FindCommonAncestor(Commit first, Commit second) + public Commit FindCommonAncestor(Commit first, Commit second) { Ensure.ArgumentNotNull(first, "first"); Ensure.ArgumentNotNull(second, "second"); @@ -108,7 +102,7 @@ public virtual Commit FindCommonAncestor(Commit first, Commit second) /// /// The s for which to find the common ancestor. /// The common ancestor or null if none found. - public virtual Commit FindCommonAncestor(IEnumerable commits) + public Commit FindCommonAncestor(IEnumerable commits) { Ensure.ArgumentNotNull(commits, "commits"); @@ -160,6 +154,7 @@ public CommitEnumerator(Repository repo, CommitFilter filter) Sort(filter.SortBy); Push(filter.SinceList); Hide(filter.UntilList); + FirstParentOnly(filter.FirstParentOnly); } #region IEnumerator Members @@ -210,7 +205,7 @@ private void Dispose(bool disposing) private void InternalHidePush(IList identifier, HidePushSignature hidePush) { - IEnumerable oids = RetrieveCommitOids(identifier).TakeWhile(o => o != null); + IEnumerable oids = repo.Committishes(identifier).TakeWhile(o => o != null); foreach (ObjectId actedOn in oids) { @@ -238,96 +233,12 @@ private void Sort(CommitSortStrategies options) Proxy.git_revwalk_sorting(handle, options); } - private ObjectId DereferenceToCommit(string identifier) - { - var options = LookUpOptions.DereferenceResultToCommit; - - if (!AllowOrphanReference(identifier)) - { - options |= LookUpOptions.ThrowWhenNoGitObjectHasBeenFound; - } - - // TODO: Should we check the type? Git-log allows TagAnnotation oid as parameter. But what about Blobs and Trees? - GitObject commit = repo.Lookup(identifier, GitObjectType.Any, options); - - return commit != null ? commit.Id : null; - } - - private bool AllowOrphanReference(string identifier) + private void FirstParentOnly(bool firstParent) { - return string.Equals(identifier, "HEAD", StringComparison.Ordinal) - || string.Equals(identifier, repo.Head.CanonicalName, StringComparison.Ordinal); - } - - private IEnumerable RetrieveCommitOids(object identifier) - { - if (identifier is string) - { - yield return DereferenceToCommit(identifier as string); - yield break; - } - - if (identifier is ObjectId) - { - yield return DereferenceToCommit(((ObjectId)identifier).Sha); - yield break; - } - - if (identifier is Commit) + if (firstParent) { - yield return ((Commit)identifier).Id; - yield break; + Proxy.git_revwalk_simplify_first_parent(handle); } - - if (identifier is TagAnnotation) - { - yield return DereferenceToCommit(((TagAnnotation)identifier).Target.Id.Sha); - yield break; - } - - if (identifier is Tag) - { - yield return DereferenceToCommit(((Tag)identifier).Target.Id.Sha); - yield break; - } - - if (identifier is Branch) - { - var branch = (Branch)identifier; - if (branch.Tip == null && branch.IsCurrentRepositoryHead) - { - yield return null; - yield break; - } - - Ensure.GitObjectIsNotNull(branch.Tip, branch.CanonicalName); - - yield return branch.Tip.Id; - yield break; - } - - if (identifier is Reference) - { - yield return DereferenceToCommit(((Reference)identifier).CanonicalName); - yield break; - } - - if (identifier is IEnumerable) - { - var enumerable = (IEnumerable)identifier; - - foreach (object entry in enumerable) - { - foreach (ObjectId oid in RetrieveCommitOids(entry)) - { - yield return oid; - } - } - - yield break; - } - - throw new LibGit2SharpException(string.Format(CultureInfo.InvariantCulture, "Unexpected kind of identifier '{0}'.", identifier)); } } } diff --git a/LibGit2Sharp/CompareOptions.cs b/LibGit2Sharp/CompareOptions.cs index a9c03265d..09f47a2ec 100644 --- a/LibGit2Sharp/CompareOptions.cs +++ b/LibGit2Sharp/CompareOptions.cs @@ -1,3 +1,5 @@ +using System; + namespace LibGit2Sharp { /// @@ -12,7 +14,6 @@ public CompareOptions() { ContextLines = 3; InterhunkLines = 0; - SkipPatchBuilding = false; } /// @@ -28,9 +29,13 @@ public CompareOptions() public int InterhunkLines { get; set; } /// - /// Flag to skip patch building. May be used if only file name and status required. - /// (Default = false) + /// Options for rename detection. If null, the `diff.renames` configuration setting is used. + /// + public SimilarityOptions Similarity { get; set; } + + /// + /// Include "unmodified" entries in the results. /// - internal bool SkipPatchBuilding { get; set; } + public bool IncludeUnmodified { get; set; } } } diff --git a/LibGit2Sharp/Configuration.cs b/LibGit2Sharp/Configuration.cs index ed69ad344..1e70437b8 100644 --- a/LibGit2Sharp/Configuration.cs +++ b/LibGit2Sharp/Configuration.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Globalization; using System.IO; +using System.Linq; using System.Runtime.InteropServices; using LibGit2Sharp.Core; using LibGit2Sharp.Core.Handles; @@ -235,6 +236,23 @@ public virtual void Set(string key, T value, ConfigurationLevel level = Confi } } + /// + /// Find configuration entries matching . + /// + /// A regular expression. + /// The configuration file into which the key should be searched for. + /// Matching entries. + public virtual IEnumerable> Find(string regexp, + ConfigurationLevel level = ConfigurationLevel.Local) + { + Ensure.ArgumentNotNullOrEmptyString(regexp, "regexp"); + + using (ConfigurationSafeHandle h = RetrieveConfigurationHandle(level, true)) + { + return Proxy.git_config_iterator_glob(h, regexp, BuildConfigEntry).ToList(); + } + } + private ConfigurationSafeHandle RetrieveConfigurationHandle(ConfigurationLevel level, bool throwIfStoreHasNotBeenFound) { ConfigurationSafeHandle handle = null; @@ -266,7 +284,11 @@ private static Action GetUpdater(Act { typeof(string), GetUpdater(Proxy.git_config_set_string) }, }; - IEnumerator> IEnumerable>.GetEnumerator() + /// + /// Returns an enumerator that iterates through the configuration entries. + /// + /// An object that can be used to iterate through the configuration entries. + public virtual IEnumerator> GetEnumerator() { return BuildConfigEntries().GetEnumerator(); } @@ -278,17 +300,41 @@ System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() private IEnumerable> BuildConfigEntries() { - return Proxy.git_config_foreach(configHandle, entryPtr => - { - var entry = (GitConfigEntry)Marshal.PtrToStructure(entryPtr, typeof(GitConfigEntry)); + return Proxy.git_config_foreach(configHandle, BuildConfigEntry); + } + + private static ConfigurationEntry BuildConfigEntry(IntPtr entryPtr) + { + var entry = (GitConfigEntry)Marshal.PtrToStructure(entryPtr, typeof(GitConfigEntry)); - return new ConfigurationEntry(Utf8Marshaler.FromNative(entry.namePtr), - Utf8Marshaler.FromNative(entry.valuePtr), - (ConfigurationLevel)entry.level); - }); + return new ConfigurationEntry(LaxUtf8Marshaler.FromNative(entry.namePtr), + LaxUtf8Marshaler.FromNative(entry.valuePtr), + (ConfigurationLevel)entry.level); + } + + /// + /// Builds a based on current configuration. + /// + /// Name is populated from the user.name setting, and is "unknown" if unspecified. + /// Email is populated from the user.email setting, and is built from + /// and if unspecified. + /// + /// + /// The same escalation logic than in git.git will be used when looking for the key in the config files: + /// - local: the Git file in the current repository + /// - global: the Git file specific to the current interactive user (usually in `$HOME/.gitconfig`) + /// - xdg: another Git file specific to the current interactive user (usually in `$HOME/.config/git/config`) + /// - system: the system-wide Git file + /// + /// + /// The timestamp to use for the . + /// The signature. + public virtual Signature BuildSignature(DateTimeOffset now) + { + return BuildSignature(now, false); } - internal Signature BuildSignatureFromGlobalConfiguration(DateTimeOffset now, bool shouldThrowIfNotFound) + internal Signature BuildSignature(DateTimeOffset now, bool shouldThrowIfNotFound) { var name = Get("user.name"); var email = Get("user.email"); diff --git a/LibGit2Sharp/ConfigurationEntry.cs b/LibGit2Sharp/ConfigurationEntry.cs index 2a46373ea..7456be3c4 100644 --- a/LibGit2Sharp/ConfigurationEntry.cs +++ b/LibGit2Sharp/ConfigurationEntry.cs @@ -37,7 +37,7 @@ protected ConfigurationEntry() /// The option name /// The option value /// The origin store - internal ConfigurationEntry(string key, T value, ConfigurationLevel level) + protected internal ConfigurationEntry(string key, T value, ConfigurationLevel level) { Key = key; Value = value; diff --git a/LibGit2Sharp/ConfigurationExtensions.cs b/LibGit2Sharp/ConfigurationExtensions.cs index 6aa676b72..5e79a2898 100644 --- a/LibGit2Sharp/ConfigurationExtensions.cs +++ b/LibGit2Sharp/ConfigurationExtensions.cs @@ -63,7 +63,7 @@ public static ConfigurationEntry Get(this Configuration config, string fir { Ensure.ArgumentNotNullOrEmptyString(firstKeyPart, "firstKeyPart"); Ensure.ArgumentNotNullOrEmptyString(secondKeyPart, "secondKeyPart"); - Ensure.ArgumentNotNullOrEmptyString(thirdKeyPart, "secondKeyPart"); + Ensure.ArgumentNotNullOrEmptyString(thirdKeyPart, "thirdKeyPart"); return config.Get(new[] { firstKeyPart, secondKeyPart, thirdKeyPart }); } diff --git a/LibGit2Sharp/ContentChanges.cs b/LibGit2Sharp/ContentChanges.cs index 55e6f58c1..01fb41751 100644 --- a/LibGit2Sharp/ContentChanges.cs +++ b/LibGit2Sharp/ContentChanges.cs @@ -1,4 +1,6 @@ using System; +using System.Diagnostics; +using System.Globalization; using System.Text; using LibGit2Sharp.Core; @@ -7,8 +9,12 @@ namespace LibGit2Sharp /// /// Holds the changes between two s. /// - public class ContentChanges : Changes + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public class ContentChanges { + private readonly StringBuilder patchBuilder = new StringBuilder(); + private bool isBinaryComparison; + /// /// Needed for mocking purposes. /// @@ -23,6 +29,43 @@ internal ContentChanges(Repository repo, Blob oldBlob, Blob newBlob, GitDiffOpti options, FileCallback, HunkCallback, LineCallback); } + internal ContentChanges(bool isBinaryComparison) + { + this.isBinaryComparison = isBinaryComparison; + } + + internal void AppendToPatch(string patch) + { + patchBuilder.Append(patch); + } + + /// + /// The number of lines added. + /// + public virtual int LinesAdded { get; internal set; } + + /// + /// The number of lines deleted. + /// + public virtual int LinesDeleted { get; internal set; } + + /// + /// The patch corresponding to these changes. + /// + public virtual string Patch + { + get { return patchBuilder.ToString(); } + } + + /// + /// Determines if at least one side of the comparison holds binary content. + /// + public virtual bool IsBinaryComparison + { + get { return isBinaryComparison; } + private set { isBinaryComparison = value; } + } + private int FileCallback(GitDiffDelta delta, float progress, IntPtr payload) { IsBinaryComparison = delta.IsBinary(); @@ -37,34 +80,34 @@ private int FileCallback(GitDiffDelta delta, float progress, IntPtr payload) return 0; } - private int HunkCallback(GitDiffDelta delta, GitDiffRange range, IntPtr header, UIntPtr headerlen, IntPtr payload) + private int HunkCallback(GitDiffDelta delta, GitDiffHunk hunk, IntPtr payload) { - string decodedContent = Utf8Marshaler.FromNative(header, (int)headerlen); + string decodedContent = LaxUtf8Marshaler.FromBuffer(hunk.Header, (int)hunk.HeaderLen); AppendToPatch(decodedContent); return 0; } - private int LineCallback(GitDiffDelta delta, GitDiffRange range, GitDiffLineOrigin lineorigin, IntPtr content, UIntPtr contentlen, IntPtr payload) + private int LineCallback(GitDiffDelta delta, GitDiffHunk hunk, GitDiffLine line, IntPtr payload) { - string decodedContent = Utf8Marshaler.FromNative(content, (int)contentlen); + string decodedContent = LaxUtf8Marshaler.FromNative(line.content, (int)line.contentLen); string prefix; - switch (lineorigin) + switch (line.lineOrigin) { case GitDiffLineOrigin.GIT_DIFF_LINE_ADDITION: LinesAdded++; - prefix = Encoding.ASCII.GetString(new[] { (byte)lineorigin }); + prefix = Encoding.ASCII.GetString(new[] { (byte)line.lineOrigin }); break; case GitDiffLineOrigin.GIT_DIFF_LINE_DELETION: LinesDeleted++; - prefix = Encoding.ASCII.GetString(new[] { (byte)lineorigin }); + prefix = Encoding.ASCII.GetString(new[] { (byte)line.lineOrigin }); break; case GitDiffLineOrigin.GIT_DIFF_LINE_CONTEXT: - prefix = Encoding.ASCII.GetString(new[] { (byte)lineorigin }); + prefix = Encoding.ASCII.GetString(new[] { (byte)line.lineOrigin }); break; default: @@ -76,5 +119,14 @@ private int LineCallback(GitDiffDelta delta, GitDiffRange range, GitDiffLineOrig AppendToPatch(decodedContent); return 0; } + + private string DebuggerDisplay + { + get + { + return string.Format(CultureInfo.InvariantCulture, + @"{{+{0}, -{1}}}", LinesAdded, LinesDeleted); + } + } } } diff --git a/LibGit2Sharp/Core/EncodingMarshaler.cs b/LibGit2Sharp/Core/EncodingMarshaler.cs new file mode 100644 index 000000000..7cc879222 --- /dev/null +++ b/LibGit2Sharp/Core/EncodingMarshaler.cs @@ -0,0 +1,162 @@ +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Text; + +namespace LibGit2Sharp.Core +{ + internal abstract class EncodingMarshaler : ICustomMarshaler + { + private readonly Encoding encoding; + + protected EncodingMarshaler(Encoding encoding) + { + this.encoding = encoding; + } + + #region ICustomMarshaler + + public void CleanUpManagedData(object managedObj) + { + } + + public virtual void CleanUpNativeData(IntPtr pNativeData) + { + Cleanup(pNativeData); + } + + public int GetNativeDataSize() + { + // Not a value type + return -1; + } + + public virtual IntPtr MarshalManagedToNative(Object managedObj) + { + if (managedObj == null) + { + return IntPtr.Zero; + } + + var str = managedObj as string; + + if (str == null) + { + throw new MarshalDirectiveException( + string.Format("{0} must be used on a string.", GetType().Name)); + } + + return FromManaged(encoding, str); + } + + public virtual Object MarshalNativeToManaged(IntPtr pNativeData) + { + return FromNative(encoding, pNativeData); + } + + #endregion + + public static unsafe IntPtr FromManaged(Encoding encoding, String value) + { + if (value == null) + { + return IntPtr.Zero; + } + + int length = encoding.GetByteCount(value); + var buffer = (byte*)Marshal.AllocHGlobal(length + 1).ToPointer(); + + if (length > 0) + { + fixed (char* pValue = value) + { + encoding.GetBytes(pValue, value.Length, buffer, length); + } + } + + buffer[length] = 0; + + return new IntPtr(buffer); + } + + public static void Cleanup(IntPtr pNativeData) + { + if (pNativeData == IntPtr.Zero) + { + return; + } + + Marshal.FreeHGlobal(pNativeData); + } + + public static unsafe string FromNative(Encoding encoding, IntPtr pNativeData) + { + if (pNativeData == IntPtr.Zero) + { + return null; + } + + var start = (byte*)pNativeData; + byte* walk = start; + + // Find the end of the string + while (*walk != 0) + { + walk++; + } + + if (walk == start) + { + return String.Empty; + } + + return new String((sbyte*)pNativeData.ToPointer(), 0, (int)(walk - start), encoding); + } + + public static unsafe string FromNative(Encoding encoding, IntPtr pNativeData, int length) + { + if (pNativeData == IntPtr.Zero) + { + return null; + } + + if (length == 0) + { + return String.Empty; + } + + return new String((sbyte*)pNativeData.ToPointer(), 0, length, encoding); + } + + public static string FromBuffer(Encoding encoding, byte[] buffer) + { + if (buffer == null) + { + return null; + } + + int length = 0; + int stop = buffer.Length; + + while (length < stop && + 0 != buffer[length]) + { + length++; + } + + return FromBuffer(encoding, buffer, length); + } + + public static string FromBuffer(Encoding encoding, byte[] buffer, int length) + { + Debug.Assert(buffer != null); + + if (length == 0) + { + return String.Empty; + } + + return encoding.GetString(buffer, 0, length); + } + } +} diff --git a/LibGit2Sharp/Core/Ensure.cs b/LibGit2Sharp/Core/Ensure.cs index 3629d1362..368ffe9eb 100644 --- a/LibGit2Sharp/Core/Ensure.cs +++ b/LibGit2Sharp/Core/Ensure.cs @@ -39,6 +39,37 @@ public static void ArgumentNotNullOrEmptyString(string argumentValue, string arg } } + /// + /// Checks a string argument to ensure it doesn't contain a zero byte. + /// + /// The argument value to check. + /// The name of the argument. + public static void ArgumentDoesNotContainZeroByte(string argumentValue, string argumentName) + { + if (string.IsNullOrEmpty(argumentValue)) + { + return; + } + + int zeroPos = -1; + for (var i = 0; i < argumentValue.Length; i++) + { + if (argumentValue[i] == '\0') + { + zeroPos = i; + break; + } + } + + if (zeroPos == -1) + { + return; + } + + throw new ArgumentException( + string.Format("Zero bytes ('\\0') are not allowed. A zero byte has been found at position {0}.", zeroPos), argumentName); + } + private static readonly Dictionary> GitErrorsToLibGit2SharpExceptions = new Dictionary> @@ -65,7 +96,7 @@ private static void HandleError(int result) } else { - errorMessage = Utf8Marshaler.FromNative(error.Message); + errorMessage = LaxUtf8Marshaler.FromNative(error.Message); } Func exceptionBuilder; diff --git a/LibGit2Sharp/Core/FilePathMarshaler.cs b/LibGit2Sharp/Core/FilePathMarshaler.cs index 5c7f72247..9c5df4b2e 100644 --- a/LibGit2Sharp/Core/FilePathMarshaler.cs +++ b/LibGit2Sharp/Core/FilePathMarshaler.cs @@ -1,6 +1,6 @@ using System; -using System.Globalization; using System.Runtime.InteropServices; +using System.Text; namespace LibGit2Sharp.Core { @@ -12,13 +12,13 @@ namespace LibGit2Sharp.Core /// Use this marshaler for return values, for example: /// [return: MarshalAs(UnmanagedType.CustomMarshaler, /// MarshalCookie = UniqueId.UniqueIdentifier, - /// MarshalTypeRef = typeof(FilePathNoCleanupMarshaler))] + /// MarshalTypeRef = typeof(LaxFilePathNoCleanupMarshaler))] /// - internal class FilePathNoCleanupMarshaler : FilePathMarshaler + internal class LaxFilePathNoCleanupMarshaler : LaxFilePathMarshaler { - private static readonly FilePathNoCleanupMarshaler staticInstance = new FilePathNoCleanupMarshaler(); + private static readonly LaxFilePathNoCleanupMarshaler staticInstance = new LaxFilePathNoCleanupMarshaler(); - public static new ICustomMarshaler GetInstance(String cookie) + public new static ICustomMarshaler GetInstance(String cookie) { return staticInstance; } @@ -44,38 +44,20 @@ public override void CleanUpNativeData(IntPtr pNativeData) /// internal static extern int git_index_open(out IndexSafeHandle index, /// [MarshalAs(UnmanagedType.CustomMarshaler, /// MarshalCookie = UniqueId.UniqueIdentifier, - /// MarshalTypeRef = typeof(FilePathMarshaler))] FilePath indexpath); + /// MarshalTypeRef = typeof(StrictFilePathMarshaler))] FilePath indexpath); /// - internal class FilePathMarshaler : ICustomMarshaler + internal class StrictFilePathMarshaler : StrictUtf8Marshaler { - private static readonly FilePathMarshaler staticInstance = new FilePathMarshaler(); + private static readonly StrictFilePathMarshaler staticInstance = new StrictFilePathMarshaler(); - public static ICustomMarshaler GetInstance(String cookie) + public new static ICustomMarshaler GetInstance(String cookie) { return staticInstance; } #region ICustomMarshaler - public void CleanUpManagedData(Object managedObj) - { - } - - public virtual void CleanUpNativeData(IntPtr pNativeData) - { - if (IntPtr.Zero != pNativeData) - { - Marshal.FreeHGlobal(pNativeData); - } - } - - public int GetNativeDataSize() - { - // Not a value type - return -1; - } - - public IntPtr MarshalManagedToNative(Object managedObj) + public override IntPtr MarshalManagedToNative(Object managedObj) { if (null == managedObj) { @@ -86,59 +68,57 @@ public IntPtr MarshalManagedToNative(Object managedObj) if (null == filePath) { - var expectedType = typeof(FilePath); - var actualType = managedObj.GetType(); - throw new MarshalDirectiveException( - string.Format(CultureInfo.InvariantCulture, - "FilePathMarshaler must be used on a FilePath. Expected '{0}' from '{1}'; received '{2}' from '{3}'.", - expectedType.FullName, expectedType.Assembly.Location, - actualType.FullName, actualType.Assembly.Location)); + string.Format("{0} must be used on a FilePath.", GetType().Name)); } return FromManaged(filePath); } - public Object MarshalNativeToManaged(IntPtr pNativeData) - { - return FromNative(pNativeData); - } - #endregion public static IntPtr FromManaged(FilePath filePath) { - if (null == filePath) + if (filePath == null) { return IntPtr.Zero; } - return Utf8Marshaler.FromManaged(filePath.Posix); + return StrictUtf8Marshaler.FromManaged(filePath.Posix); } + } - public static FilePath FromNative(IntPtr pNativeData) + /// + /// This marshaler is to be used for capturing a UTF-8 string allocated by libgit2 and + /// converting it to a managed FilePath instance. The marshaler will free the native pointer + /// after conversion. + /// + internal class LaxFilePathMarshaler : LaxUtf8Marshaler + { + private static readonly LaxFilePathMarshaler staticInstance = new LaxFilePathMarshaler(); + + public new static ICustomMarshaler GetInstance(String cookie) { - if (IntPtr.Zero == pNativeData) - { - return null; - } + return staticInstance; + } - if (0 == Marshal.ReadByte(pNativeData)) - { - return FilePath.Empty; - } + #region ICustomMarshaler - return Utf8Marshaler.FromNative(pNativeData); + public override Object MarshalNativeToManaged(IntPtr pNativeData) + { + return FromNative(pNativeData); } - public static FilePath FromNative(IntPtr pNativeData, int length) + #endregion + + public new static FilePath FromNative(IntPtr pNativeData) { - if (0 == length) - { - return FilePath.Empty; - } + return LaxUtf8Marshaler.FromNative(pNativeData); + } - return Utf8Marshaler.FromNative(pNativeData, length); + public new static FilePath FromBuffer(byte[] buffer) + { + return LaxUtf8Marshaler.FromBuffer(buffer); } } } diff --git a/LibGit2Sharp/Core/GitBlame.cs b/LibGit2Sharp/Core/GitBlame.cs new file mode 100644 index 000000000..392a1b998 --- /dev/null +++ b/LibGit2Sharp/Core/GitBlame.cs @@ -0,0 +1,79 @@ +using System; +using System.Runtime.InteropServices; + +namespace LibGit2Sharp.Core +{ + [Flags] + internal enum GitBlameOptionFlags + { + /// + /// Normal blame, the default + /// + GIT_BLAME_NORMAL = 0, + + /// + /// Track lines that have moved within a file (like `git blame -M`). + /// + GIT_BLAME_TRACK_COPIES_SAME_FILE = (1 << 0), + + /** Track lines that have moved across files in the same commit (like `git blame -C`). + * NOT IMPLEMENTED. */ + GIT_BLAME_TRACK_COPIES_SAME_COMMIT_MOVES = (1 << 1), + + /// + /// Track lines that have been copied from another file that exists in the + /// same commit (like `git blame -CC`). Implies SAME_FILE. + /// + GIT_BLAME_TRACK_COPIES_SAME_COMMIT_COPIES = (1 << 2), + + /// + /// Track lines that have been copied from another file that exists in *any* + /// commit (like `git blame -CCC`). Implies SAME_COMMIT_COPIES. + /// + GIT_BLAME_TRACK_COPIES_ANY_COMMIT_COPIES = (1 << 3), + } + + [StructLayout(LayoutKind.Sequential)] + internal class GitBlameOptions + { + public uint version = 1; + public GitBlameOptionFlags flags; + public UInt16 MinMatchCharacters; + public GitOid NewestCommit; + public GitOid OldestCommit; + public uint MinLine; + public uint MaxLine; + } + + [StructLayout(LayoutKind.Sequential)] + internal class GitBlameHunk + { + public ushort LinesInHunk; + + public GitOid FinalCommitId; + public ushort FinalStartLineNumber; + public IntPtr FinalSignature; + + public GitOid OrigCommitId; + public IntPtr OrigPath; + public ushort OrigStartLineNumber; + public IntPtr OrigSignature; + + public byte Boundary; + } + + internal static class BlameStrategyExtensions + { + public static GitBlameOptionFlags ToGitBlameOptionFlags(this BlameStrategy strategy) + { + switch (strategy) + { + case BlameStrategy.Default: + return GitBlameOptionFlags.GIT_BLAME_NORMAL; + + default: + throw new NotSupportedException(string.Format("{0} is not supported at this time", strategy)); + } + } + } +} diff --git a/LibGit2Sharp/Core/GitBuf.cs b/LibGit2Sharp/Core/GitBuf.cs new file mode 100644 index 000000000..09860fdc3 --- /dev/null +++ b/LibGit2Sharp/Core/GitBuf.cs @@ -0,0 +1,18 @@ +using System; +using System.Runtime.InteropServices; + +namespace LibGit2Sharp.Core.Handles +{ + [StructLayout(LayoutKind.Sequential)] + internal class GitBuf : IDisposable + { + public IntPtr ptr; + public UIntPtr asize; + public UIntPtr size; + + public void Dispose() + { + Proxy.git_buf_free(this); + } + } +} diff --git a/LibGit2Sharp/Core/GitCheckoutOpts.cs b/LibGit2Sharp/Core/GitCheckoutOpts.cs index eedec66ad..a1d562f98 100644 --- a/LibGit2Sharp/Core/GitCheckoutOpts.cs +++ b/LibGit2Sharp/Core/GitCheckoutOpts.cs @@ -118,6 +118,9 @@ internal struct GitCheckoutOpts :IDisposable public IntPtr baseline; public IntPtr target_directory; + public IntPtr our_label; + public IntPtr their_label; + public void Dispose() { if (paths == null) diff --git a/LibGit2Sharp/Core/GitCloneOptions.cs b/LibGit2Sharp/Core/GitCloneOptions.cs index 5382b60a2..669d15d21 100644 --- a/LibGit2Sharp/Core/GitCloneOptions.cs +++ b/LibGit2Sharp/Core/GitCloneOptions.cs @@ -9,21 +9,12 @@ internal class GitCloneOptions public uint Version = 1; public GitCheckoutOpts CheckoutOpts; - public IntPtr InitOptions; + public GitRemoteCallbacks RemoteCallbacks; + public int Bare; - public NativeMethods.git_transfer_progress_callback TransferProgressCallback; - public IntPtr TransferProgressPayload; + public int IgnoreCertErrors; public IntPtr RemoteName; - public IntPtr PushUrl; - public IntPtr FetchSpec; - public IntPtr PushSpec; - - public NativeMethods.git_cred_acquire_cb CredAcquireCallback; - public IntPtr CredAcquirePayload; - - public IntPtr Transport; - public GitRemoteCallbacks RemoteCallbacks; - public int RemoteAutotag; + public IntPtr CheckoutBranch; } } diff --git a/LibGit2Sharp/Core/GitDiff.cs b/LibGit2Sharp/Core/GitDiff.cs index 57e03a010..d0d9bdd9b 100644 --- a/LibGit2Sharp/Core/GitDiff.cs +++ b/LibGit2Sharp/Core/GitDiff.cs @@ -7,117 +7,157 @@ namespace LibGit2Sharp.Core internal enum GitDiffOptionFlags { /// - /// Normal diff, the default. + /// Normal diff, the default /// GIT_DIFF_NORMAL = 0, + /* + * Options controlling which files will be in the diff + */ + /// - /// Reverse the sides of the diff. + /// Reverse the sides of the diff /// GIT_DIFF_REVERSE = (1 << 0), /// - /// Treat all files as text, disabling binary attributes and detection. + /// Include ignored files in the diff /// - GIT_DIFF_FORCE_TEXT = (1 << 1), + GIT_DIFF_INCLUDE_IGNORED = (1 << 1), /// - /// Ignore all whitespace. + /// Even with GIT_DIFF_INCLUDE_IGNORED, an entire ignored directory + /// will be marked with only a single entry in the diff; this flag + /// adds all files under the directory as IGNORED entries, too. /// - GIT_DIFF_IGNORE_WHITESPACE = (1 << 2), + GIT_DIFF_RECURSE_IGNORED_DIRS = (1 << 2), /// - /// Ignore changes in amount of whitespace. + /// Include untracked files in the diff /// - GIT_DIFF_IGNORE_WHITESPACE_CHANGE = (1 << 3), + GIT_DIFF_INCLUDE_UNTRACKED = (1 << 3), /// - /// Ignore whitespace at end of line. + /// Even with GIT_DIFF_INCLUDE_UNTRACKED, an entire untracked + /// directory will be marked with only a single entry in the diff + /// (a la what core Git does in `git status`); this flag adds *all* + /// files under untracked directories as UNTRACKED entries, too. /// - GIT_DIFF_IGNORE_WHITESPACE_EOL = (1 << 4), + GIT_DIFF_RECURSE_UNTRACKED_DIRS = (1 << 4), /// - /// Exclude submodules from the diff completely. + /// Include unmodified files in the diff /// - GIT_DIFF_IGNORE_SUBMODULES = (1 << 5), + GIT_DIFF_INCLUDE_UNMODIFIED = (1 << 5), /// - /// Use the "patience diff" algorithm (currently unimplemented). + /// Normally, a type change between files will be converted into a + /// DELETED record for the old and an ADDED record for the new; this + /// options enabled the generation of TYPECHANGE delta records. /// - GIT_DIFF_PATIENCE = (1 << 6), + GIT_DIFF_INCLUDE_TYPECHANGE = (1 << 6), /// - /// Include ignored files in the diff list. + /// Even with GIT_DIFF_INCLUDE_TYPECHANGE, blob->tree changes still + /// generally show as a DELETED blob. This flag tries to correctly + /// label blob->tree transitions as TYPECHANGE records with new_file's + /// mode set to tree. Note: the tree SHA will not be available. /// - GIT_DIFF_INCLUDE_IGNORED = (1 << 7), + GIT_DIFF_INCLUDE_TYPECHANGE_TREES = (1 << 7), /// - /// Include untracked files in the diff list. + /// Ignore file mode changes /// - GIT_DIFF_INCLUDE_UNTRACKED = (1 << 8), + GIT_DIFF_IGNORE_FILEMODE = (1 << 8), /// - /// Include unmodified files in the diff list. + /// Treat all submodules as unmodified /// - GIT_DIFF_INCLUDE_UNMODIFIED = (1 << 9), + GIT_DIFF_IGNORE_SUBMODULES = (1 << 9), /// - /// Even with the GIT_DIFF_INCLUDE_UNTRACKED flag, when an untracked - /// directory is found, only a single entry for the directory is added - /// to the diff list; with this flag, all files under the directory will - /// be included, too. + /// Use case insensitive filename comparisons /// - GIT_DIFF_RECURSE_UNTRACKED_DIRS = (1 << 10), + GIT_DIFF_IGNORE_CASE = (1 << 10), /// - /// If the pathspec is set in the diff options, this flags means to - /// apply it as an exact match instead of as an fnmatch pattern. + /// If the pathspec is set in the diff options, this flags means to + /// apply it as an exact match instead of as an fnmatch pattern. /// - GIT_DIFF_DISABLE_PATHSPEC_MATCH = (1 << 11), + GIT_DIFF_DISABLE_PATHSPEC_MATCH = (1 << 12), /// - /// Use case insensitive filename comparisons. + /// Disable updating of the `binary` flag in delta records. This is + /// useful when iterating over a diff if you don't need hunk and data + /// callbacks and want to avoid having to load file completely. /// - GIT_DIFF_DELTAS_ARE_ICASE = (1 << 12), + GIT_DIFF_SKIP_BINARY_CHECK = (1 << 13), /// - /// When generating patch text, include the content of untracked files. + /// When diff finds an untracked directory, to match the behavior of + /// core Git, it scans the contents for IGNORED and UNTRACKED files. + /// If *all* contents are IGNORED, then the directory is IGNORED; if + /// any contents are not IGNORED, then the directory is UNTRACKED. + /// This is extra work that may not matter in many cases. This flag + /// turns off that scan and immediately labels an untracked directory + /// as UNTRACKED (changing the behavior to not match core Git). /// - GIT_DIFF_INCLUDE_UNTRACKED_CONTENT = (1 << 13), + GIT_DIFF_ENABLE_FAST_UNTRACKED_DIRS = (1 << 14), + + /* + * Options controlling how output will be generated + */ /// - /// Disable updating of the `binary` flag in delta records. This is - /// useful when iterating over a diff if you don't need hunk and data - /// callbacks and want to avoid having to load file completely. + /// Treat all files as text, disabling binary attributes and detection /// - GIT_DIFF_SKIP_BINARY_CHECK = (1 << 14), + GIT_DIFF_FORCE_TEXT = (1 << 20), /// - /// Normally, a type change between files will be converted into a - /// DELETED record for the old and an ADDED record for the new; this - /// options enabled the generation of TYPECHANGE delta records. + /// Treat all files as binary, disabling text diffs /// - GIT_DIFF_INCLUDE_TYPECHANGE = (1 << 15), + GIT_DIFF_FORCE_BINARY = (1 << 21), /// - /// Even with GIT_DIFF_INCLUDE_TYPECHANGE, blob->tree changes still - /// generally show as a DELETED blob. This flag tries to correctly - /// label blob->tree transitions as TYPECHANGE records with new_file's - /// mode set to tree. Note: the tree SHA will not be available. + /// Ignore all whitespace /// - GIT_DIFF_INCLUDE_TYPECHANGE_TREES = (1 << 16), + GIT_DIFF_IGNORE_WHITESPACE = (1 << 22), /// - /// Ignore file mode changes. + /// Ignore changes in amount of whitespace /// - GIT_DIFF_IGNORE_FILEMODE = (1 << 17), + GIT_DIFF_IGNORE_WHITESPACE_CHANGE = (1 << 23), /// - /// Even with GIT_DIFF_INCLUDE_IGNORED, an entire ignored directory - /// will be marked with only a single entry in the diff list; this flag - /// adds all files under the directory as IGNORED entries, too. + /// Ignore whitespace at end of line + /// + GIT_DIFF_IGNORE_WHITESPACE_EOL = (1 << 24), + + /// + /// When generating patch text, include the content of untracked + /// files. This automatically turns on GIT_DIFF_INCLUDE_UNTRACKED but + /// it does not turn on GIT_DIFF_RECURSE_UNTRACKED_DIRS. Add that + /// flag if you want the content of every single UNTRACKED file. /// - GIT_DIFF_RECURSE_IGNORED_DIRS = (1 << 18), + GIT_DIFF_SHOW_UNTRACKED_CONTENT = (1 << 25), + + /// + /// When generating output, include the names of unmodified files if + /// they are included in the git_diff. Normally these are skipped in + /// the formats that list files (e.g. name-only, name-status, raw). + /// Even with this, these will not be included in patch format. + /// + GIT_DIFF_SHOW_UNMODIFIED = (1 << 26), + + /// + /// Use the "patience diff" algorithm + /// + GIT_DIFF_PATIENCE = (1 << 28), + + /// + /// Take extra time to find minimal diff + /// + GIT_DIFF_MINIMAL = (1 << 29), } internal delegate int diff_notify_cb( @@ -131,19 +171,23 @@ internal class GitDiffOptions : IDisposable { public uint Version = 1; public GitDiffOptionFlags Flags; - public ushort ContextLines; - public ushort InterhunkLines; - // NB: These are char*s to UTF8 strings, finna marshal them by hand - public IntPtr OldPrefixString; - public IntPtr NewPrefixString; + /* options controlling which files are in the diff */ + public SubmoduleIgnore IgnoreSubmodules; public GitStrArrayIn PathSpec; - public Int64 MaxSize; - public diff_notify_cb NotifyCallback; public IntPtr NotifyPayload; + /* options controlling how to diff text is generated */ + + public ushort ContextLines; + public ushort InterhunkLines; + public ushort OidAbbrev; + public Int64 MaxSize; + public IntPtr OldPrefixString; + public IntPtr NewPrefixString; + public void Dispose() { if (PathSpec == null) @@ -176,20 +220,37 @@ internal class GitDiffFile [StructLayout(LayoutKind.Sequential)] internal class GitDiffDelta { - public GitDiffFile OldFile; - public GitDiffFile NewFile; public ChangeKind Status; - public uint Similarity; public uint Flags; + public ushort Similarity; + public ushort NumberOfFiles; + public GitDiffFile OldFile; + public GitDiffFile NewFile; } [StructLayout(LayoutKind.Sequential)] - internal class GitDiffRange + internal class GitDiffHunk { public int OldStart; public int OldLines; public int NewStart; public int NewLines; + public UIntPtr HeaderLen; + + [MarshalAsAttribute(UnmanagedType.ByValArray, SizeConst = 128)] + public byte[] Header; + } + + [StructLayout(LayoutKind.Sequential)] + internal class GitDiffLine + { + public GitDiffLineOrigin lineOrigin; + public int OldLineNo; + public int NewLineNo; + public int NumLines; + public UIntPtr contentLen; + public Int64 contentOffset; + public IntPtr content; } enum GitDiffLineOrigin : byte @@ -205,4 +266,70 @@ enum GitDiffLineOrigin : byte GIT_DIFF_LINE_HUNK_HDR = 0x48, //'H', GIT_DIFF_LINE_BINARY = 0x42, //'B', } + + enum GitDiffFormat + { + GIT_DIFF_FORMAT_PATCH = 1, // < full git diff + GIT_DIFF_FORMAT_PATCH_HEADER = 2, // < just the file headers of patch + GIT_DIFF_FORMAT_RAW = 3, // < like git diff --raw + GIT_DIFF_FORMAT_NAME_ONLY = 4, // < like git diff --name-only + GIT_DIFF_FORMAT_NAME_STATUS = 5, // < like git diff --name-status + } + + [Flags] + enum GitDiffFindFlags + { + GIT_DIFF_FIND_RENAMES = (1 << 0), + // consider old side of modified for renames? (`--break-rewrites=N`) + GIT_DIFF_FIND_RENAMES_FROM_REWRITES = (1 << 1), + + // look for copies? (a la `--find-copies`) + GIT_DIFF_FIND_COPIES = (1 << 2), + // consider unmodified as copy sources? (`--find-copies-harder`) + GIT_DIFF_FIND_COPIES_FROM_UNMODIFIED = (1 << 3), + + // mark large rewrites for split (`--break-rewrites=/M`) + GIT_DIFF_FIND_REWRITES = (1 << 4), + // actually split large rewrites into delete/add pairs + GIT_DIFF_BREAK_REWRITES = (1 << 5), + // mark rewrites for split and break into delete/add pairs + GIT_DIFF_FIND_AND_BREAK_REWRITES = + (GIT_DIFF_FIND_REWRITES | GIT_DIFF_BREAK_REWRITES), + + // find renames/copies for untracked items in working directory + GIT_DIFF_FIND_FOR_UNTRACKED = (1 << 6), + + // turn on all finding features + GIT_DIFF_FIND_ALL = (0x0ff), + + // measure similarity ignoring leading whitespace (default) + GIT_DIFF_FIND_IGNORE_LEADING_WHITESPACE = 0, + // measure similarity ignoring all whitespace + GIT_DIFF_FIND_IGNORE_WHITESPACE = (1 << 12), + // measure similarity including all data + GIT_DIFF_FIND_DONT_IGNORE_WHITESPACE = (1 << 13), + // measure similarity only by comparing SHAs (fast and cheap) + GIT_DIFF_FIND_EXACT_MATCH_ONLY = (1 << 14), + + // do not break rewrites unless they contribute to a rename + GIT_DIFF_BREAK_REWRITES_FOR_RENAMES_ONLY = (1 << 15), + + // Remove any UNMODIFIED deltas after find_similar is done. + GIT_DIFF_FIND_REMOVE_UNMODIFIED = (1 << 16), + } + + [StructLayout(LayoutKind.Sequential)] + internal class GitDiffFindOptions + { + public uint Version = 1; + public GitDiffFindFlags Flags; + public UInt16 RenameThreshold; + public UInt16 RenameFromRewriteThreshold; + public UInt16 CopyThreshold; + public UInt16 BreakRewriteThreshold; + public UIntPtr RenameLimit; + + // TODO + public IntPtr SimilarityMetric; + } } diff --git a/LibGit2Sharp/Core/GitPushOptions.cs b/LibGit2Sharp/Core/GitPushOptions.cs new file mode 100644 index 000000000..bf6058d67 --- /dev/null +++ b/LibGit2Sharp/Core/GitPushOptions.cs @@ -0,0 +1,12 @@ +using System; +using System.Runtime.InteropServices; + +namespace LibGit2Sharp.Core +{ + [StructLayout(LayoutKind.Sequential)] + internal class GitPushOptions + { + public int Version = 1; + public int PackbuilderDegreeOfParallelism; + } +} diff --git a/LibGit2Sharp/Core/GitRemoteCallbacks.cs b/LibGit2Sharp/Core/GitRemoteCallbacks.cs index b1b2f7bcc..7903f567a 100644 --- a/LibGit2Sharp/Core/GitRemoteCallbacks.cs +++ b/LibGit2Sharp/Core/GitRemoteCallbacks.cs @@ -15,6 +15,10 @@ internal struct GitRemoteCallbacks internal NativeMethods.remote_completion_callback completion; + internal NativeMethods.git_cred_acquire_cb acquire_credentials; + + internal NativeMethods.git_transfer_progress_callback download_progress; + internal NativeMethods.remote_update_tips_callback update_tips; internal IntPtr payload; diff --git a/LibGit2Sharp/Core/GitRepositoryInitOptions.cs b/LibGit2Sharp/Core/GitRepositoryInitOptions.cs index da202b94e..c42bc83f4 100644 --- a/LibGit2Sharp/Core/GitRepositoryInitOptions.cs +++ b/LibGit2Sharp/Core/GitRepositoryInitOptions.cs @@ -28,7 +28,7 @@ public static GitRepositoryInitOptions BuildFrom(FilePath workdirPath, bool isBa { Debug.Assert(!isBare); - opts.WorkDirPath = FilePathMarshaler.FromManaged(workdirPath); + opts.WorkDirPath = StrictFilePathMarshaler.FromManaged(workdirPath); } if (isBare) @@ -41,12 +41,7 @@ public static GitRepositoryInitOptions BuildFrom(FilePath workdirPath, bool isBa public void Dispose() { - if (WorkDirPath == IntPtr.Zero) - { - return; - } - - Marshal.FreeHGlobal(WorkDirPath); + EncodingMarshaler.Cleanup(WorkDirPath); WorkDirPath = IntPtr.Zero; } } diff --git a/LibGit2Sharp/Core/GitStatusEntry.cs b/LibGit2Sharp/Core/GitStatusEntry.cs new file mode 100644 index 000000000..2e13a186a --- /dev/null +++ b/LibGit2Sharp/Core/GitStatusEntry.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; + +namespace LibGit2Sharp.Core +{ + /// + /// A status entry from libgit2. + /// + [StructLayout(LayoutKind.Sequential)] + internal class GitStatusEntry + { + /// + /// Calculated status of a filepath in the working directory considering the current and the . + /// + public FileStatus Status; + + /// + /// The difference between the and . + /// + public IntPtr HeadToIndexPtr; + + /// + /// The difference between the and the working directory. + /// + public IntPtr IndexToWorkDirPtr; + } +} diff --git a/LibGit2Sharp/Core/GitStatusOptions.cs b/LibGit2Sharp/Core/GitStatusOptions.cs new file mode 100644 index 000000000..5d02ebfe9 --- /dev/null +++ b/LibGit2Sharp/Core/GitStatusOptions.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; + +namespace LibGit2Sharp.Core +{ + [StructLayout(LayoutKind.Sequential)] + internal class GitStatusOptions : IDisposable + { + public uint Version = 1; + + public GitStatusShow Show; + public GitStatusOptionFlags Flags; + + GitStrArrayIn PathSpec; + + public void Dispose() + { + if (PathSpec == null) + { + return; + } + + PathSpec.Dispose(); + } + } + + internal enum GitStatusShow + { + IndexAndWorkDir = 0, + IndexOnly = 1, + WorkDirOnly = 2, + } + + [Flags] + internal enum GitStatusOptionFlags + { + IncludeUntracked = (1 << 0), + IncludeIgnored = (1 << 1), + IncludeUnmodified = (1 << 2), + ExcludeSubmodules = (1 << 3), + RecurseUntrackedDirs = (1 << 4), + DisablePathspecMatch = (1 << 5), + RecurseIgnoredDirs = (1 << 6), + RenamesHeadToIndex = (1 << 7), + RenamesIndexToWorkDir = (1 << 8), + SortCaseSensitively = (1 << 9), + SortCaseInsensitively = (1 << 10), + RenamesFromRewrites = (1 << 11), + } +} diff --git a/LibGit2Sharp/Core/GitStrArrayIn.cs b/LibGit2Sharp/Core/GitStrArrayIn.cs index 6ec4256c6..9abfcf3f9 100644 --- a/LibGit2Sharp/Core/GitStrArrayIn.cs +++ b/LibGit2Sharp/Core/GitStrArrayIn.cs @@ -1,8 +1,5 @@ using System; -using System.Collections.Generic; -using System.Linq; using System.Runtime.InteropServices; -using System.Text; namespace LibGit2Sharp.Core { @@ -12,23 +9,33 @@ internal class GitStrArrayIn : IDisposable public IntPtr strings; public uint size; + public static GitStrArrayIn BuildFrom(string[] strings) + { + return BuildFrom(strings, StrictUtf8Marshaler.FromManaged); + } + public static GitStrArrayIn BuildFrom(FilePath[] paths) { - var nbOfPaths = paths.Length; - var pathPtrs = new IntPtr[nbOfPaths]; + return BuildFrom(paths, StrictFilePathMarshaler.FromManaged); + } + + private static GitStrArrayIn BuildFrom(T[] input, Func marshaler) + { + var count = input.Length; + var pointers = new IntPtr[count]; - for (int i = 0; i < nbOfPaths; i++) + for (int i = 0; i < count; i++) { - var s = paths[i].Posix; - pathPtrs[i] = FilePathMarshaler.FromManaged(s); + var item = input[i]; + pointers[i] = marshaler(item); } - int dim = IntPtr.Size * nbOfPaths; + int dim = IntPtr.Size * count; IntPtr arrayPtr = Marshal.AllocHGlobal(dim); - Marshal.Copy(pathPtrs, 0, arrayPtr, nbOfPaths); + Marshal.Copy(pointers, 0, arrayPtr, count); - return new GitStrArrayIn { strings = arrayPtr, size = (uint)nbOfPaths }; + return new GitStrArrayIn { strings = arrayPtr, size = (uint)count }; } public void Dispose() @@ -38,14 +45,14 @@ public void Dispose() return; } - var nbOfPaths = (int)size; + var count = (int)size; - var pathPtrs = new IntPtr[nbOfPaths]; - Marshal.Copy(strings, pathPtrs, 0, nbOfPaths); + var pointers = new IntPtr[count]; + Marshal.Copy(strings, pointers, 0, count); - for (int i = 0; i < nbOfPaths; i++) + for (int i = 0; i < count; i++) { - Marshal.FreeHGlobal(pathPtrs[i]); + EncodingMarshaler.Cleanup(pointers[i]); } Marshal.FreeHGlobal(strings); diff --git a/LibGit2Sharp/Core/GitTransferProgress.cs b/LibGit2Sharp/Core/GitTransferProgress.cs index 29456c154..911028501 100644 --- a/LibGit2Sharp/Core/GitTransferProgress.cs +++ b/LibGit2Sharp/Core/GitTransferProgress.cs @@ -12,6 +12,9 @@ internal struct GitTransferProgress public uint total_objects; public uint indexed_objects; public uint received_objects; + public uint local_objects; + public uint total_deltas; + public uint indexed_deltas; public UIntPtr received_bytes; } } diff --git a/LibGit2Sharp/Core/Handles/BlameSafeHandle.cs b/LibGit2Sharp/Core/Handles/BlameSafeHandle.cs new file mode 100644 index 000000000..e8218e3da --- /dev/null +++ b/LibGit2Sharp/Core/Handles/BlameSafeHandle.cs @@ -0,0 +1,11 @@ +namespace LibGit2Sharp.Core.Handles +{ + internal class BlameSafeHandle : SafeHandleBase + { + protected override bool ReleaseHandleImpl() + { + Proxy.git_blame_free(handle); + return true; + } + } +} diff --git a/LibGit2Sharp/Core/Handles/BranchIteratorSafeHandle.cs b/LibGit2Sharp/Core/Handles/BranchIteratorSafeHandle.cs new file mode 100644 index 000000000..6764bee66 --- /dev/null +++ b/LibGit2Sharp/Core/Handles/BranchIteratorSafeHandle.cs @@ -0,0 +1,11 @@ +namespace LibGit2Sharp.Core.Handles +{ + internal class BranchIteratorSafeHandle : SafeHandleBase + { + protected override bool ReleaseHandleImpl() + { + Proxy.git_branch_iterator_free(handle); + return true; + } + } +} diff --git a/LibGit2Sharp/Core/Handles/ConfigurationIteratorSafeHandle.cs b/LibGit2Sharp/Core/Handles/ConfigurationIteratorSafeHandle.cs new file mode 100644 index 000000000..0d2cb6ab6 --- /dev/null +++ b/LibGit2Sharp/Core/Handles/ConfigurationIteratorSafeHandle.cs @@ -0,0 +1,11 @@ +namespace LibGit2Sharp.Core.Handles +{ + internal class ConfigurationIteratorSafeHandle : SafeHandleBase + { + protected override bool ReleaseHandleImpl() + { + Proxy.git_config_iterator_free(handle); + return true; + } + } +} diff --git a/LibGit2Sharp/Core/Handles/DiffListSafeHandle.cs b/LibGit2Sharp/Core/Handles/DiffSafeHandle.cs similarity index 59% rename from LibGit2Sharp/Core/Handles/DiffListSafeHandle.cs rename to LibGit2Sharp/Core/Handles/DiffSafeHandle.cs index 26d697ac2..fe117cba1 100644 --- a/LibGit2Sharp/Core/Handles/DiffListSafeHandle.cs +++ b/LibGit2Sharp/Core/Handles/DiffSafeHandle.cs @@ -1,10 +1,10 @@ namespace LibGit2Sharp.Core.Handles { - internal class DiffListSafeHandle : SafeHandleBase + internal class DiffSafeHandle : SafeHandleBase { protected override bool ReleaseHandleImpl() { - Proxy.git_diff_list_free(handle); + Proxy.git_diff_free(handle); return true; } } diff --git a/LibGit2Sharp/Core/Handles/StatusEntrySafeHandle.cs b/LibGit2Sharp/Core/Handles/StatusEntrySafeHandle.cs new file mode 100644 index 000000000..ef8433529 --- /dev/null +++ b/LibGit2Sharp/Core/Handles/StatusEntrySafeHandle.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; + +namespace LibGit2Sharp.Core.Handles +{ + internal class StatusEntrySafeHandle : NotOwnedSafeHandleBase + { + public StatusEntrySafeHandle() + : base() + { + } + + public StatusEntrySafeHandle(IntPtr handle) + : base() + { + this.SetHandle(handle); + } + + public GitStatusEntry MarshalAsGitStatusEntry() + { + return (GitStatusEntry)Marshal.PtrToStructure(handle, typeof(GitStatusEntry)); + } + } +} diff --git a/LibGit2Sharp/Core/Handles/StatusListSafeHandle.cs b/LibGit2Sharp/Core/Handles/StatusListSafeHandle.cs new file mode 100644 index 000000000..74316a3fa --- /dev/null +++ b/LibGit2Sharp/Core/Handles/StatusListSafeHandle.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace LibGit2Sharp.Core.Handles +{ + internal class StatusListSafeHandle : SafeHandleBase + { + protected override bool ReleaseHandleImpl() + { + Proxy.git_status_list_free(handle); + return true; + } + } +} diff --git a/LibGit2Sharp/Core/HistoryRewriter.cs b/LibGit2Sharp/Core/HistoryRewriter.cs index 0923e7fed..67dd9902f 100644 --- a/LibGit2Sharp/Core/HistoryRewriter.cs +++ b/LibGit2Sharp/Core/HistoryRewriter.cs @@ -7,7 +7,7 @@ namespace LibGit2Sharp.Core { internal class HistoryRewriter { - private readonly Repository repo; + private readonly IRepository repo; private readonly HashSet targetedCommits; private readonly Dictionary objectMap = new Dictionary(); @@ -18,7 +18,7 @@ internal class HistoryRewriter private readonly RewriteHistoryOptions options; public HistoryRewriter( - Repository repo, + IRepository repo, IEnumerable commitsToRewrite, RewriteHistoryOptions options) { @@ -36,23 +36,24 @@ public HistoryRewriter( public void Execute() { - // Find out which refs lead to at least one the commits - var refsToRewrite = repo.Refs.ReachableFrom(targetedCommits).ToList(); + var success = false; + try + { + // Find out which refs lead to at least one the commits + var refsToRewrite = repo.Refs.ReachableFrom(targetedCommits).ToList(); - var filter = new CommitFilter - { - Since = refsToRewrite, - SortBy = CommitSortStrategies.Reverse | CommitSortStrategies.Topological - }; + var filter = new CommitFilter + { + Since = refsToRewrite, + SortBy = CommitSortStrategies.Reverse | CommitSortStrategies.Topological + }; - var commits = repo.Commits.QueryBy(filter); - foreach (var commit in commits) - { - RewriteCommit(commit); - } + var commits = repo.Commits.QueryBy(filter); + foreach (var commit in commits) + { + RewriteCommit(commit); + } - try - { // Ordering matters. In the case of `A -> B -> commit`, we need to make sure B is rewritten // before A. foreach (var reference in refsToRewrite.OrderBy(ReferenceDepth)) @@ -61,12 +62,28 @@ public void Execute() RewriteReference(reference); } + + success = true; + if (options.OnSucceeding != null) + { + options.OnSucceeding(); + } } - catch (Exception) + catch (Exception ex) { - foreach (var action in rollbackActions) + try + { + if (!success && options.OnError != null) + { + options.OnError(ex); + } + } + finally { - action(); + foreach (var action in rollbackActions) + { + action(); + } } throw; diff --git a/LibGit2Sharp/Core/LambdaEqualityHelper.cs b/LibGit2Sharp/Core/LambdaEqualityHelper.cs index 55ae7bcaf..120e705de 100644 --- a/LibGit2Sharp/Core/LambdaEqualityHelper.cs +++ b/LibGit2Sharp/Core/LambdaEqualityHelper.cs @@ -47,7 +47,8 @@ public int GetHashCode(T instance) { foreach (Func accessor in equalityContributorAccessors) { - hashCode = (hashCode*397) ^ accessor(instance).GetHashCode(); + object item = accessor(instance); + hashCode = (hashCode*397) ^ (item != null ? item.GetHashCode() : 0); } } diff --git a/LibGit2Sharp/Core/NativeDllName.cs b/LibGit2Sharp/Core/NativeDllName.cs index 03663892b..6f3a79b72 100644 --- a/LibGit2Sharp/Core/NativeDllName.cs +++ b/LibGit2Sharp/Core/NativeDllName.cs @@ -2,6 +2,6 @@ namespace LibGit2Sharp.Core { internal static class NativeDllName { - public const string Name = "git2-11f8336"; + public const string Name = "git2-65e9dc6"; } } diff --git a/LibGit2Sharp/Core/NativeMethods.cs b/LibGit2Sharp/Core/NativeMethods.cs index 9fc8ba878..40b7d3459 100644 --- a/LibGit2Sharp/Core/NativeMethods.cs +++ b/LibGit2Sharp/Core/NativeMethods.cs @@ -88,6 +88,7 @@ public static string ProcessorArchitecture } } + // Should match LibGit2Sharp.Tests.TestHelpers.BaseFixture.IsRunningOnLinux() private static bool IsRunningOnLinux() { // see http://mono-project.com/FAQ%3a_Technical#Mono_Platforms @@ -106,17 +107,34 @@ internal static extern void giterr_set_str( [DllImport(libgit2)] internal static extern void giterr_set_oom(); + [DllImport(libgit2)] + internal static extern UInt32 git_blame_get_hunk_count(BlameSafeHandle blame); + + [DllImport(libgit2)] + internal static extern IntPtr git_blame_get_hunk_byindex( + BlameSafeHandle blame, UInt32 index); + + [DllImport(libgit2)] + internal static extern int git_blame_file( + out BlameSafeHandle blame, + RepositorySafeHandle repo, + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictFilePathMarshaler))] FilePath path, + GitBlameOptions options); + + [DllImport(libgit2)] + internal static extern int git_blame_free(IntPtr blame); + [DllImport(libgit2)] internal static extern int git_blob_create_fromdisk( ref GitOid id, RepositorySafeHandle repo, - [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(FilePathMarshaler))] FilePath path); + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictFilePathMarshaler))] FilePath path); [DllImport(libgit2)] internal static extern int git_blob_create_fromworkdir( ref GitOid id, RepositorySafeHandle repo, - [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(FilePathMarshaler))] FilePath relative_path); + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictFilePathMarshaler))] FilePath relative_path); internal delegate int source_callback( IntPtr content, @@ -127,10 +145,17 @@ internal delegate int source_callback( internal static extern int git_blob_create_fromchunks( ref GitOid oid, RepositorySafeHandle repositoryPtr, - [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(FilePathMarshaler))] FilePath hintpath, + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictFilePathMarshaler))] FilePath hintpath, source_callback fileCallback, IntPtr data); + [DllImport(libgit2)] + internal static extern int git_blob_filtered_content( + GitBuf buf, + GitObjectSafeHandle blob, + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictFilePathMarshaler))] FilePath as_path, + [MarshalAs(UnmanagedType.Bool)] bool check_for_binary_data); + [DllImport(libgit2)] internal static extern IntPtr git_blob_rawcontent(GitObjectSafeHandle blob); @@ -141,7 +166,7 @@ internal static extern int git_blob_create_fromchunks( internal static extern int git_branch_create( out ReferenceSafeHandle ref_out, RepositorySafeHandle repo, - [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(Utf8Marshaler))] string branch_name, + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string branch_name, GitObjectSafeHandle target, // TODO: GitCommitSafeHandle? [MarshalAs(UnmanagedType.Bool)] bool force); @@ -155,32 +180,44 @@ internal delegate int branch_foreach_callback( IntPtr payload); [DllImport(libgit2)] - internal static extern int git_branch_foreach( + internal static extern void git_branch_iterator_free( + IntPtr iterator); + + [DllImport(libgit2)] + internal static extern int git_branch_iterator_new( + out BranchIteratorSafeHandle iter_out, RepositorySafeHandle repo, - GitBranchType branch_type, - branch_foreach_callback branch_cb, - IntPtr payload); + GitBranchType branch_type); [DllImport(libgit2)] internal static extern int git_branch_move( out ReferenceSafeHandle ref_out, ReferenceSafeHandle reference, - [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(Utf8Marshaler))] string new_branch_name, + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string new_branch_name, [MarshalAs(UnmanagedType.Bool)] bool force); + [DllImport(libgit2)] + internal static extern int git_branch_next( + out ReferenceSafeHandle ref_out, + out GitBranchType type_out, + BranchIteratorSafeHandle iter); + [DllImport(libgit2)] internal static extern int git_branch_remote_name( byte[] remote_name_out, UIntPtr buffer_size, RepositorySafeHandle repo, - [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(Utf8Marshaler))] string canonical_branch_name); + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string canonical_branch_name); [DllImport(libgit2)] internal static extern int git_branch_upstream_name( byte[] tracking_branch_name_out, // NB: This is more properly a StringBuilder, but it's UTF8 UIntPtr buffer_size, RepositorySafeHandle repo, - [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(Utf8Marshaler))] string referenceName); + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string referenceName); + + [DllImport(libgit2)] + internal static extern void git_buf_free(GitBuf buf); [DllImport(libgit2)] internal static extern int git_checkout_tree( @@ -197,8 +234,8 @@ internal static extern int git_checkout_index( [DllImport(libgit2)] internal static extern int git_clone( out RepositorySafeHandle repo, - [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(Utf8Marshaler))] string origin_url, - [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(FilePathMarshaler))] FilePath workdir_path, + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string origin_url, + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictFilePathMarshaler))] FilePath workdir_path, GitCloneOptions opts); [DllImport(libgit2)] @@ -211,21 +248,21 @@ internal static extern int git_clone( internal static extern int git_commit_create_from_oids( out GitOid id, RepositorySafeHandle repo, - [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(Utf8Marshaler))] string updateRef, + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string updateRef, SignatureSafeHandle author, SignatureSafeHandle committer, - [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(Utf8Marshaler))] string encoding, - [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(Utf8Marshaler))] string message, + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string encoding, + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string message, ref GitOid tree, int parentCount, [MarshalAs(UnmanagedType.LPArray)] [In] IntPtr[] parents); [DllImport(libgit2)] - [return : MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(Utf8NoCleanupMarshaler))] + [return : MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(LaxUtf8NoCleanupMarshaler))] internal static extern string git_commit_message(GitObjectSafeHandle commit); [DllImport(libgit2)] - [return: MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(Utf8NoCleanupMarshaler))] + [return: MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(LaxUtf8NoCleanupMarshaler))] internal static extern string git_commit_message_encoding(GitObjectSafeHandle commit); [DllImport(libgit2)] @@ -256,12 +293,12 @@ internal static extern int git_commit_create_from_oids( internal static extern int git_config_get_entry( out GitConfigEntryHandle entry, ConfigurationSafeHandle cfg, - [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(Utf8Marshaler))] string name); + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string name); [DllImport(libgit2)] internal static extern int git_config_add_file_ondisk( ConfigurationSafeHandle cfg, - [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(FilePathMarshaler))] FilePath path, + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictFilePathMarshaler))] FilePath path, uint level, bool force); @@ -277,41 +314,41 @@ internal static extern int git_config_open_level( [DllImport(libgit2)] internal static extern int git_config_parse_bool( [MarshalAs(UnmanagedType.Bool)] out bool value, - [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(Utf8Marshaler))] string valueToParse); + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string valueToParse); [DllImport(libgit2)] internal static extern int git_config_parse_int32( [MarshalAs(UnmanagedType.I4)] out int value, - [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(Utf8Marshaler))] string valueToParse); + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string valueToParse); [DllImport(libgit2)] internal static extern int git_config_parse_int64( [MarshalAs(UnmanagedType.I8)] out long value, - [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(Utf8Marshaler))] string valueToParse); + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string valueToParse); [DllImport(libgit2)] internal static extern int git_config_set_bool( ConfigurationSafeHandle cfg, - [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(Utf8Marshaler))] string name, + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string name, [MarshalAs(UnmanagedType.Bool)] bool value); [DllImport(libgit2)] internal static extern int git_config_set_int32( ConfigurationSafeHandle cfg, - [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(Utf8Marshaler))] string name, + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string name, int value); [DllImport(libgit2)] internal static extern int git_config_set_int64( ConfigurationSafeHandle cfg, - [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(Utf8Marshaler))] string name, + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string name, long value); [DllImport(libgit2)] internal static extern int git_config_set_string( ConfigurationSafeHandle cfg, - [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(Utf8Marshaler))] string name, - [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(Utf8Marshaler))] string value); + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string name, + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string value); internal delegate int config_foreach_callback( IntPtr entry, @@ -323,10 +360,24 @@ internal static extern int git_config_foreach( config_foreach_callback callback, IntPtr payload); - // Ordinarily we would decorate the `url` parameter with the Utf8Marshaler like we do everywhere + [DllImport(libgit2)] + internal static extern int git_config_iterator_glob_new( + out ConfigurationIteratorSafeHandle iter, + ConfigurationSafeHandle cfg, + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string regexp); + + [DllImport(libgit2)] + internal static extern int git_config_next( + out IntPtr entry, + ConfigurationIteratorSafeHandle iter); + + [DllImport(libgit2)] + internal static extern void git_config_iterator_free(IntPtr iter); + + // Ordinarily we would decorate the `url` parameter with the StrictUtf8Marshaler like we do everywhere // else, but apparently doing a native->managed callback with the 64-bit version of CLR 2.0 can // sometimes vomit when using a custom IMarshaler. So yeah, don't do that. If you need the url, - // call Utf8Marshaler.FromNative manually. See the discussion here: + // call StrictUtf8Marshaler.FromNative manually. See the discussion here: // http://social.msdn.microsoft.com/Forums/en-US/netfx64bit/thread/1eb746c6-d695-4632-8a9e-16c4fa98d481 internal delegate int git_cred_acquire_cb( out IntPtr cred, @@ -338,15 +389,15 @@ internal delegate int git_cred_acquire_cb( [DllImport(libgit2)] internal static extern int git_cred_userpass_plaintext_new( out IntPtr cred, - [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof (Utf8Marshaler))] string username, - [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof (Utf8Marshaler))] string password); + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof (StrictUtf8Marshaler))] string username, + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof (StrictUtf8Marshaler))] string password); [DllImport(libgit2)] - internal static extern void git_diff_list_free(IntPtr diff); + internal static extern void git_diff_free(IntPtr diff); [DllImport(libgit2)] internal static extern int git_diff_tree_to_tree( - out DiffListSafeHandle diff, + out DiffSafeHandle diff, RepositorySafeHandle repo, GitObjectSafeHandle oldTree, GitObjectSafeHandle newTree, @@ -354,7 +405,7 @@ internal static extern int git_diff_tree_to_tree( [DllImport(libgit2)] internal static extern int git_diff_tree_to_index( - out DiffListSafeHandle diff, + out DiffSafeHandle diff, RepositorySafeHandle repo, GitObjectSafeHandle oldTree, IndexSafeHandle index, @@ -362,19 +413,19 @@ internal static extern int git_diff_tree_to_index( [DllImport(libgit2)] internal static extern int git_diff_merge( - DiffListSafeHandle onto, - DiffListSafeHandle from); + DiffSafeHandle onto, + DiffSafeHandle from); [DllImport(libgit2)] internal static extern int git_diff_index_to_workdir( - out DiffListSafeHandle diff, + out DiffSafeHandle diff, RepositorySafeHandle repo, IndexSafeHandle index, GitDiffOptions options); [DllImport(libgit2)] internal static extern int git_diff_tree_to_workdir( - out DiffListSafeHandle diff, + out DiffSafeHandle diff, RepositorySafeHandle repo, GitObjectSafeHandle oldTree, GitDiffOptions options); @@ -386,52 +437,54 @@ internal delegate int git_diff_file_cb( internal delegate int git_diff_hunk_cb( GitDiffDelta delta, - GitDiffRange range, - IntPtr header, - UIntPtr headerLen, + GitDiffHunk hunk, IntPtr payload); - internal delegate int git_diff_data_cb( + internal delegate int git_diff_line_cb( GitDiffDelta delta, - GitDiffRange range, - GitDiffLineOrigin lineOrigin, - IntPtr content, - UIntPtr contentLen, + GitDiffHunk hunk, + GitDiffLine line, IntPtr payload); [DllImport(libgit2)] - internal static extern int git_diff_print_patch( - DiffListSafeHandle diff, - git_diff_data_cb printCallback, + internal static extern int git_diff_print( + DiffSafeHandle diff, + GitDiffFormat format, + git_diff_line_cb printCallback, IntPtr payload); [DllImport(libgit2)] internal static extern int git_diff_blobs( GitObjectSafeHandle oldBlob, - [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(FilePathMarshaler))] FilePath old_as_path, + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictFilePathMarshaler))] FilePath old_as_path, GitObjectSafeHandle newBlob, - [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(FilePathMarshaler))] FilePath new_as_path, + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictFilePathMarshaler))] FilePath new_as_path, GitDiffOptions options, git_diff_file_cb fileCallback, git_diff_hunk_cb hunkCallback, - git_diff_data_cb lineCallback, + git_diff_line_cb lineCallback, IntPtr payload); [DllImport(libgit2)] internal static extern int git_diff_foreach( - DiffListSafeHandle diff, + DiffSafeHandle diff, git_diff_file_cb fileCallback, git_diff_hunk_cb hunkCallback, - git_diff_data_cb lineCallback, + git_diff_line_cb lineCallback, IntPtr payload); + [DllImport(libgit2)] + internal static extern int git_diff_find_similar( + DiffSafeHandle diff, + GitDiffFindOptions options); + [DllImport(libgit2)] internal static extern int git_graph_ahead_behind(out UIntPtr ahead, out UIntPtr behind, RepositorySafeHandle repo, ref GitOid one, ref GitOid two); [DllImport(libgit2)] internal static extern int git_ignore_add_rule( RepositorySafeHandle repo, - [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof (Utf8Marshaler))] string rules); + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof (StrictUtf8Marshaler))] string rules); [DllImport(libgit2)] internal static extern int git_ignore_clear_internal_rules(RepositorySafeHandle repo); @@ -440,12 +493,12 @@ internal static extern int git_ignore_add_rule( internal static extern int git_ignore_path_is_ignored( out int ignored, RepositorySafeHandle repo, - [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(FilePathMarshaler))] FilePath path); + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictFilePathMarshaler))] FilePath path); [DllImport(libgit2)] internal static extern int git_index_add_bypath( IndexSafeHandle index, - [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(FilePathMarshaler))] FilePath path); + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictFilePathMarshaler))] FilePath path); [DllImport(libgit2)] internal static extern int git_index_add( @@ -458,7 +511,7 @@ internal static extern int git_index_conflict_get( out IndexEntrySafeHandle ours, out IndexEntrySafeHandle theirs, IndexSafeHandle index, - [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(FilePathMarshaler))] FilePath path); + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictFilePathMarshaler))] FilePath path); [DllImport(libgit2)] internal static extern UIntPtr git_index_entrycount(IndexSafeHandle index); @@ -470,7 +523,7 @@ internal static extern int git_index_conflict_get( internal static extern int git_index_find( out UIntPtr pos, IndexSafeHandle index, - [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(FilePathMarshaler))] FilePath path); + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictFilePathMarshaler))] FilePath path); [DllImport(libgit2)] internal static extern void git_index_free(IntPtr index); @@ -481,7 +534,7 @@ internal static extern int git_index_find( [DllImport(libgit2)] internal static extern IndexEntrySafeHandle git_index_get_bypath( IndexSafeHandle index, - [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(FilePathMarshaler))] FilePath path, + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictFilePathMarshaler))] FilePath path, int stage); [DllImport(libgit2)] @@ -490,12 +543,15 @@ internal static extern IndexEntrySafeHandle git_index_get_bypath( [DllImport(libgit2)] internal static extern int git_index_open( out IndexSafeHandle index, - [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(FilePathMarshaler))] FilePath indexpath); + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictFilePathMarshaler))] FilePath indexpath); + + [DllImport(libgit2)] + internal static extern int git_index_read(IndexSafeHandle index, bool force); [DllImport(libgit2)] internal static extern int git_index_remove_bypath( IndexSafeHandle index, - [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(FilePathMarshaler))] FilePath path); + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictFilePathMarshaler))] FilePath path); [DllImport(libgit2)] internal static extern int git_index_write(IndexSafeHandle index); @@ -514,7 +570,7 @@ internal static extern int git_merge_base( internal static extern int git_message_prettify( byte[] message_out, // NB: This is more properly a StringBuilder, but it's UTF8 UIntPtr buffer_size, - [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(Utf8Marshaler))] string message, + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string message, bool strip_comments); [DllImport(libgit2)] @@ -523,16 +579,16 @@ internal static extern int git_note_create( RepositorySafeHandle repo, SignatureSafeHandle author, SignatureSafeHandle committer, - [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(Utf8Marshaler))] string notes_ref, + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string notes_ref, ref GitOid oid, - [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(Utf8Marshaler))] string note, + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string note, int force); [DllImport(libgit2)] internal static extern void git_note_free(IntPtr note); [DllImport(libgit2)] - [return: MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(Utf8NoCleanupMarshaler))] + [return: MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(LaxUtf8NoCleanupMarshaler))] internal static extern string git_note_message(NoteSafeHandle note); [DllImport(libgit2)] @@ -542,20 +598,20 @@ internal static extern int git_note_create( internal static extern int git_note_read( out NoteSafeHandle note, RepositorySafeHandle repo, - [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(Utf8Marshaler))] string notes_ref, + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string notes_ref, ref GitOid oid); [DllImport(libgit2)] internal static extern int git_note_remove( RepositorySafeHandle repo, - [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(Utf8Marshaler))] string notes_ref, + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string notes_ref, SignatureSafeHandle author, SignatureSafeHandle committer, ref GitOid oid); [DllImport(libgit2)] internal static extern int git_note_default_ref( - [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(Utf8NoCleanupMarshaler))] out string notes_ref, + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(LaxUtf8NoCleanupMarshaler))] out string notes_ref, RepositorySafeHandle repo); internal delegate int git_note_foreach_cb( @@ -566,7 +622,7 @@ internal delegate int git_note_foreach_cb( [DllImport(libgit2)] internal static extern int git_note_foreach( RepositorySafeHandle repo, - [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(Utf8Marshaler))] string notes_ref, + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string notes_ref, git_note_foreach_cb cb, IntPtr payload); @@ -613,10 +669,25 @@ internal static extern int git_object_peel( [DllImport(libgit2)] internal static extern int git_push_new(out PushSafeHandle push, RemoteSafeHandle remote); + /* Push network progress notification function */ + internal delegate int git_push_transfer_progress(uint current, uint total, UIntPtr bytes, IntPtr payload); + internal delegate int git_packbuilder_progress(int stage, uint current, uint total, IntPtr payload); + + [DllImport(libgit2)] + internal static extern int git_push_set_callbacks( + PushSafeHandle push, + git_packbuilder_progress pack_progress_cb, + IntPtr pack_progress_cb_payload, + git_push_transfer_progress transfer_progress_cb, + IntPtr transfer_progress_cb_payload); + + [DllImport(libgit2)] + internal static extern int git_push_set_options(PushSafeHandle push, GitPushOptions options); + [DllImport(libgit2)] internal static extern int git_push_add_refspec( PushSafeHandle push, - [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(Utf8Marshaler))] string pushRefSpec); + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string pushRefSpec); [DllImport(libgit2)] internal static extern int git_push_finish(PushSafeHandle push); @@ -645,7 +716,7 @@ internal delegate int push_status_foreach_cb( internal static extern int git_reference_create( out ReferenceSafeHandle reference, RepositorySafeHandle repo, - [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(Utf8Marshaler))] string name, + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string name, ref GitOid oid, [MarshalAs(UnmanagedType.Bool)] bool force); @@ -653,8 +724,8 @@ internal static extern int git_reference_create( internal static extern int git_reference_symbolic_create( out ReferenceSafeHandle reference, RepositorySafeHandle repo, - [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(Utf8Marshaler))] string name, - [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(Utf8Marshaler))] string target, + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string name, + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string target, [MarshalAs(UnmanagedType.Bool)] bool force); [DllImport(libgit2)] @@ -667,7 +738,7 @@ internal delegate int ref_glob_callback( [DllImport(libgit2)] internal static extern int git_reference_foreach_glob( RepositorySafeHandle repo, - [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(Utf8Marshaler))] string glob, + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string glob, ref_glob_callback callback, IntPtr payload); @@ -676,16 +747,16 @@ internal static extern int git_reference_foreach_glob( [DllImport(libgit2)] internal static extern int git_reference_is_valid_name( - [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(Utf8Marshaler))] string refname); + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string refname); [DllImport(libgit2)] internal static extern int git_reference_lookup( out ReferenceSafeHandle reference, RepositorySafeHandle repo, - [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(Utf8Marshaler))] string name); + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string name); [DllImport(libgit2)] - [return: MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(Utf8NoCleanupMarshaler))] + [return: MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(LaxUtf8NoCleanupMarshaler))] internal static extern string git_reference_name(ReferenceSafeHandle reference); [DllImport(libgit2)] @@ -695,7 +766,7 @@ internal static extern int git_reference_lookup( internal static extern int git_reference_rename( out ReferenceSafeHandle ref_out, ReferenceSafeHandle reference, - [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(Utf8Marshaler))] string newName, + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string newName, [MarshalAs(UnmanagedType.Bool)] bool force); [DllImport(libgit2)] @@ -705,10 +776,10 @@ internal static extern int git_reference_rename( internal static extern int git_reference_symbolic_set_target( out ReferenceSafeHandle ref_out, ReferenceSafeHandle reference, - [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(Utf8Marshaler))] string target); + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string target); [DllImport(libgit2)] - [return: MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(Utf8NoCleanupMarshaler))] + [return: MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(LaxUtf8NoCleanupMarshaler))] internal static extern string git_reference_symbolic_target(ReferenceSafeHandle reference); [DllImport(libgit2)] @@ -721,7 +792,8 @@ internal static extern void git_reflog_free( [DllImport(libgit2)] internal static extern int git_reflog_read( out ReflogSafeHandle ref_out, - ReferenceSafeHandle reference); + RepositorySafeHandle repo, + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string name); [DllImport(libgit2)] internal static extern UIntPtr git_reflog_entrycount @@ -749,13 +821,13 @@ internal static extern int git_reflog_append( ReflogSafeHandle reflog, ref GitOid id, SignatureSafeHandle committer, - [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(Utf8Marshaler))] string msg); + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string msg); [DllImport(libgit2)] internal static extern int git_reflog_write(ReflogSafeHandle reflog); [DllImport(libgit2)] - [return: MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(Utf8NoCleanupMarshaler))] + [return: MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(LaxUtf8NoCleanupMarshaler))] internal static extern string git_reflog_entry_message(SafeHandle entry); [DllImport(libgit2)] @@ -763,7 +835,28 @@ internal static extern int git_refspec_rtransform( byte[] target, UIntPtr outlen, GitRefSpecHandle refSpec, - [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(Utf8Marshaler))] string name); + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string name); + + [DllImport(libgit2)] + [return: MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(LaxUtf8NoCleanupMarshaler))] + internal static extern string git_refspec_string( + GitRefSpecHandle refSpec); + + [DllImport(libgit2)] + internal static extern RefSpecDirection git_refspec_direction(GitRefSpecHandle refSpec); + + [DllImport(libgit2)] + [return: MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(LaxUtf8NoCleanupMarshaler))] + internal static extern string git_refspec_dst( + GitRefSpecHandle refSpec); + + [DllImport(libgit2)] + [return: MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(LaxUtf8NoCleanupMarshaler))] + internal static extern string git_refspec_src( + GitRefSpecHandle refSpec); + + [DllImport(libgit2)] + internal static extern bool git_refspec_force(GitRefSpecHandle refSpec); [DllImport(libgit2)] internal static extern int git_remote_autotag(RemoteSafeHandle remote); @@ -775,17 +868,22 @@ internal static extern int git_refspec_rtransform( internal static extern int git_remote_create( out RemoteSafeHandle remote, RepositorySafeHandle repo, - [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(Utf8Marshaler))] string name, - [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(Utf8Marshaler))] string url); + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string name, + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string url); + + [DllImport(libgit2)] + internal static extern int git_remote_create_inmemory( + out RemoteSafeHandle remote, + RepositorySafeHandle repo, + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string refspec, + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string url); [DllImport(libgit2)] internal static extern void git_remote_disconnect(RemoteSafeHandle remote); [DllImport(libgit2)] internal static extern int git_remote_download( - RemoteSafeHandle remote, - git_transfer_progress_callback progress_cb, - IntPtr payload); + RemoteSafeHandle remote); [DllImport(libgit2)] internal static extern void git_remote_free(IntPtr remote); @@ -793,36 +891,39 @@ internal static extern int git_remote_download( [DllImport(libgit2)] internal static extern GitRefSpecHandle git_remote_get_refspec(RemoteSafeHandle remote, UIntPtr n); + [DllImport(libgit2)] + internal static extern UIntPtr git_remote_refspec_count(RemoteSafeHandle remote); + + [DllImport(libgit2)] + internal static extern int git_remote_set_fetch_refspecs(RemoteSafeHandle remote, GitStrArrayIn array); + + [DllImport(libgit2)] + internal static extern int git_remote_set_push_refspecs(RemoteSafeHandle remote, GitStrArrayIn array); + [DllImport(libgit2)] internal static extern int git_remote_is_valid_name( - [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(Utf8Marshaler))] string remote_name); + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string remote_name); [DllImport(libgit2)] internal static extern int git_remote_load( out RemoteSafeHandle remote, RepositorySafeHandle repo, - [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(Utf8Marshaler))] string name); + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string name); internal delegate int git_headlist_cb(ref GitRemoteHead remoteHeadPtr, IntPtr payload); [DllImport(libgit2)] - internal static extern int git_remote_ls(RemoteSafeHandle remote, git_headlist_cb headlist_cb, IntPtr payload); + internal static extern int git_remote_ls(out IntPtr heads, out UIntPtr size, RemoteSafeHandle remote); [DllImport(libgit2)] - [return: MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(Utf8NoCleanupMarshaler))] + [return: MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(LaxUtf8NoCleanupMarshaler))] internal static extern string git_remote_name(RemoteSafeHandle remote); [DllImport(libgit2)] internal static extern int git_remote_save(RemoteSafeHandle remote); [DllImport(libgit2)] - internal static extern void git_remote_set_cred_acquire_cb( - RemoteSafeHandle remote, - git_cred_acquire_cb cred_acquire_cb, - IntPtr payload); - - [DllImport(libgit2)] - [return: MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(Utf8NoCleanupMarshaler))] + [return: MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(LaxUtf8NoCleanupMarshaler))] internal static extern string git_remote_url(RemoteSafeHandle remote); [DllImport(libgit2)] @@ -836,9 +937,9 @@ internal static extern int git_remote_set_callbacks( [DllImport(libgit2)] internal static extern int git_remote_add_fetch( RemoteSafeHandle remote, - [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof (Utf8Marshaler))] string refspec); + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof (StrictUtf8Marshaler))] string refspec); - internal delegate void remote_progress_callback(IntPtr str, int len, IntPtr data); + internal delegate int remote_progress_callback(IntPtr str, int len, IntPtr data); internal delegate int remote_completion_callback(RemoteCompletionType type, IntPtr data); @@ -855,9 +956,9 @@ internal delegate int remote_update_tips_callback( internal static extern int git_repository_discover( byte[] repository_path, // NB: This is more properly a StringBuilder, but it's UTF8 UIntPtr size, - [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(FilePathMarshaler))] FilePath start_path, + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictFilePathMarshaler))] FilePath start_path, [MarshalAs(UnmanagedType.Bool)] bool across_fs, - [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(FilePathMarshaler))] FilePath ceiling_dirs); + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictFilePathMarshaler))] FilePath ceiling_dirs); internal delegate int git_repository_fetchhead_foreach_cb( IntPtr remote_name, @@ -887,7 +988,7 @@ internal static extern int git_repository_fetchhead_foreach( [DllImport(libgit2)] internal static extern int git_repository_init_ext( out RepositorySafeHandle repository, - [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(FilePathMarshaler))] FilePath path, + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictFilePathMarshaler))] FilePath path, GitRepositoryInitOptions options); [DllImport(libgit2)] @@ -900,7 +1001,7 @@ internal static extern int git_repository_init_ext( internal static extern int git_repository_is_shallow(RepositorySafeHandle repo); [DllImport(libgit2)] - internal static extern int git_repository_merge_cleanup(RepositorySafeHandle repo); + internal static extern int git_repository_state_cleanup(RepositorySafeHandle repo); internal delegate int git_repository_mergehead_foreach_cb( ref GitOid oid, @@ -924,17 +1025,17 @@ internal static extern int git_repository_message( [DllImport(libgit2)] internal static extern int git_repository_open( out RepositorySafeHandle repository, - [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(FilePathMarshaler))] FilePath path); + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictFilePathMarshaler))] FilePath path); [DllImport(libgit2)] internal static extern int git_repository_open_ext( NullRepositorySafeHandle repository, - [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(FilePathMarshaler))] FilePath path, + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictFilePathMarshaler))] FilePath path, RepositoryOpenFlags flags, - [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(FilePathMarshaler))] FilePath ceilingDirs); + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictFilePathMarshaler))] FilePath ceilingDirs); [DllImport(libgit2)] - [return: MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(FilePathNoCleanupMarshaler))] + [return: MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(LaxFilePathNoCleanupMarshaler))] internal static extern FilePath git_repository_path(RepositorySafeHandle repository); [DllImport(libgit2)] @@ -950,7 +1051,7 @@ internal static extern void git_repository_set_index( [DllImport(libgit2)] internal static extern int git_repository_set_workdir( RepositorySafeHandle repository, - [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(FilePathMarshaler))] FilePath workdir, + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictFilePathMarshaler))] FilePath workdir, bool update_gitlink); [DllImport(libgit2)] @@ -958,21 +1059,21 @@ internal static extern int git_repository_state( RepositorySafeHandle repository); [DllImport(libgit2)] - [return: MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(FilePathNoCleanupMarshaler))] + [return: MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(LaxFilePathNoCleanupMarshaler))] internal static extern FilePath git_repository_workdir(RepositorySafeHandle repository); [DllImport(libgit2)] internal static extern int git_reset( RepositorySafeHandle repo, GitObjectSafeHandle target, - ResetOptions reset_type); + ResetMode reset_type); [DllImport(libgit2)] internal static extern int git_revparse_ext( out GitObjectSafeHandle obj, out ReferenceSafeHandle reference, RepositorySafeHandle repo, - [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(Utf8Marshaler))] string spec); + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string spec); [DllImport(libgit2)] internal static extern void git_revwalk_free(IntPtr walker); @@ -995,23 +1096,29 @@ internal static extern int git_revparse_ext( [DllImport(libgit2)] internal static extern void git_revwalk_sorting(RevWalkerSafeHandle walk, CommitSortStrategies sort); + [DllImport(libgit2)] + internal static extern void git_revwalk_simplify_first_parent(RevWalkerSafeHandle walk); + [DllImport(libgit2)] internal static extern void git_signature_free(IntPtr signature); [DllImport(libgit2)] internal static extern int git_signature_new( out SignatureSafeHandle signature, - [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(Utf8Marshaler))] string name, - [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(Utf8Marshaler))] string email, + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string name, + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string email, long time, int offset); + [DllImport(libgit2)] + internal static extern IntPtr git_signature_dup(IntPtr sig); + [DllImport(libgit2)] internal static extern int git_stash_save( out GitOid id, RepositorySafeHandle repo, SignatureSafeHandle stasher, - [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(Utf8Marshaler))] string message, + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string message, StashModifiers flags); internal delegate int git_stash_cb( @@ -1033,21 +1140,33 @@ internal static extern int git_stash_foreach( internal static extern int git_status_file( out FileStatus statusflags, RepositorySafeHandle repo, - [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(FilePathMarshaler))] FilePath filepath); + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictFilePathMarshaler))] FilePath filepath); - internal delegate int git_status_cb( - IntPtr path, - uint statusflags, - IntPtr payload); [DllImport(libgit2)] - internal static extern int git_status_foreach(RepositorySafeHandle repo, git_status_cb cb, IntPtr payload); + internal static extern int git_status_list_new( + out StatusListSafeHandle git_status_list, + RepositorySafeHandle repo, + GitStatusOptions options); + + [DllImport(libgit2)] + internal static extern int git_status_list_entrycount( + StatusListSafeHandle statusList); + + [DllImport(libgit2)] + internal static extern StatusEntrySafeHandle git_status_byindex( + StatusListSafeHandle list, + UIntPtr idx); + + [DllImport(libgit2)] + internal static extern void git_status_list_free( + IntPtr statusList); [DllImport(libgit2)] internal static extern int git_submodule_lookup( out SubmoduleSafeHandle reference, RepositorySafeHandle repo, - [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(FilePathMarshaler))] FilePath name); + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictFilePathMarshaler))] FilePath name); internal delegate int submodule_callback( IntPtr sm, @@ -1070,12 +1189,12 @@ internal static extern int git_submodule_save( SubmoduleSafeHandle submodule); [DllImport(libgit2)] - [return: MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(Utf8NoCleanupMarshaler))] + [return: MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(LaxUtf8NoCleanupMarshaler))] internal static extern string git_submodule_path( SubmoduleSafeHandle submodule); [DllImport(libgit2)] - [return: MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(Utf8NoCleanupMarshaler))] + [return: MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(LaxUtf8NoCleanupMarshaler))] internal static extern string git_submodule_url( SubmoduleSafeHandle submodule); @@ -1116,19 +1235,19 @@ internal static extern int git_submodule_status( internal static extern int git_tag_annotation_create( out GitOid oid, RepositorySafeHandle repo, - [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(Utf8Marshaler))] string name, + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string name, GitObjectSafeHandle target, SignatureSafeHandle signature, - [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(Utf8Marshaler))] string message); + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string message); [DllImport(libgit2)] internal static extern int git_tag_create( out GitOid oid, RepositorySafeHandle repo, - [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(Utf8Marshaler))] string name, + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string name, GitObjectSafeHandle target, SignatureSafeHandle signature, - [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(Utf8Marshaler))] string message, + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string message, [MarshalAs(UnmanagedType.Bool)] bool force); @@ -1136,7 +1255,7 @@ internal static extern int git_tag_create( internal static extern int git_tag_create_lightweight( out GitOid oid, RepositorySafeHandle repo, - [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(Utf8Marshaler))] string name, + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string name, GitObjectSafeHandle target, [MarshalAs(UnmanagedType.Bool)] bool force); @@ -1144,14 +1263,14 @@ internal static extern int git_tag_create_lightweight( [DllImport(libgit2)] internal static extern int git_tag_delete( RepositorySafeHandle repo, - [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(Utf8Marshaler))] string tagName); + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string tagName); [DllImport(libgit2)] - [return: MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(Utf8NoCleanupMarshaler))] + [return: MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(LaxUtf8NoCleanupMarshaler))] internal static extern string git_tag_message(GitObjectSafeHandle tag); [DllImport(libgit2)] - [return: MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(Utf8NoCleanupMarshaler))] + [return: MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(LaxUtf8NoCleanupMarshaler))] internal static extern string git_tag_name(GitObjectSafeHandle tag); [DllImport(libgit2)] @@ -1181,7 +1300,7 @@ internal static extern int git_tag_delete( internal static extern int git_tree_entry_bypath( out TreeEntrySafeHandle_Owned tree, GitObjectSafeHandle root, - [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(FilePathMarshaler))] FilePath treeentry_path); + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictFilePathMarshaler))] FilePath treeentry_path); [DllImport(libgit2)] internal static extern void git_tree_entry_free(IntPtr treeEntry); @@ -1190,7 +1309,7 @@ internal static extern int git_tree_entry_bypath( internal static extern OidSafeHandle git_tree_entry_id(SafeHandle entry); [DllImport(libgit2)] - [return: MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(Utf8NoCleanupMarshaler))] + [return: MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(LaxUtf8NoCleanupMarshaler))] internal static extern string git_tree_entry_name(SafeHandle entry); [DllImport(libgit2)] @@ -1206,7 +1325,7 @@ internal static extern int git_tree_entry_bypath( internal static extern int git_treebuilder_insert( IntPtr entry_out, TreeBuilderSafeHandle builder, - [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(Utf8Marshaler))] string treeentry_name, + [MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string treeentry_name, ref GitOid id, uint attributes); diff --git a/LibGit2Sharp/Core/PackbuilderCallbacks.cs b/LibGit2Sharp/Core/PackbuilderCallbacks.cs new file mode 100644 index 000000000..7c496654c --- /dev/null +++ b/LibGit2Sharp/Core/PackbuilderCallbacks.cs @@ -0,0 +1,38 @@ +using System; +using LibGit2Sharp.Handlers; + +namespace LibGit2Sharp.Core +{ + internal class PackbuilderCallbacks + { + private readonly PackBuilderProgressHandler onPackBuilderProgress; + + /// S + /// Constructor to set up the native callback given managed delegate. + /// + /// The delegate that the git_packbuilder_progress will call. + internal PackbuilderCallbacks(PackBuilderProgressHandler onPackBuilderProgress) + { + this.onPackBuilderProgress = onPackBuilderProgress; + } + + /// + /// Generates a delegate that matches the native git_packbuilder_progress function's signature and wraps the delegate. + /// + /// A delegate method with a signature that matches git_transfer_progress_callback. + internal NativeMethods.git_packbuilder_progress GenerateCallback() + { + if (onPackBuilderProgress == null) + { + return null; + } + + return new PackbuilderCallbacks(onPackBuilderProgress).OnGitPackBuilderProgress; + } + + private int OnGitPackBuilderProgress(int stage, uint current, uint total, IntPtr payload) + { + return Proxy.ConvertResultToCancelFlag(onPackBuilderProgress((PackBuilderStage)stage, (int)current, (int)total)); + } + } +} diff --git a/LibGit2Sharp/Core/Proxy.cs b/LibGit2Sharp/Core/Proxy.cs index cf9dfe2b3..18cc88b95 100644 --- a/LibGit2Sharp/Core/Proxy.cs +++ b/LibGit2Sharp/Core/Proxy.cs @@ -7,7 +7,6 @@ using System.Threading; using LibGit2Sharp.Core.Compat; using LibGit2Sharp.Core.Handles; -using LibGit2Sharp.Handlers; // ReSharper disable InconsistentNaming namespace LibGit2Sharp.Core @@ -35,6 +34,36 @@ public static void giterr_set_str(GitErrorCategory error_class, String errorStri #endregion + #region git_blame_ + + public static BlameSafeHandle git_blame_file( + RepositorySafeHandle repo, + FilePath path, + GitBlameOptions options) + { + using (ThreadAffinity()) + { + BlameSafeHandle handle; + int res = NativeMethods.git_blame_file(out handle, repo, path, options); + Ensure.ZeroResult(res); + return handle; + } + } + + public static GitBlameHunk git_blame_get_hunk_byindex(BlameSafeHandle blame, uint idx) + { + GitBlameHunk hunk = new GitBlameHunk(); + Marshal.PtrToStructure(NativeMethods.git_blame_get_hunk_byindex(blame, idx), hunk); + return hunk; + } + + public static void git_blame_free(IntPtr blame) + { + NativeMethods.git_blame_free(blame); + } + + #endregion + #region git_blob_ public static ObjectId git_blob_create_fromchunks(RepositorySafeHandle repo, FilePath hintpath, NativeMethods.source_callback fileCallback) @@ -73,6 +102,20 @@ public static ObjectId git_blob_create_fromfile(RepositorySafeHandle repo, FileP } } + public static UnmanagedMemoryStream git_blob_filtered_content_stream(RepositorySafeHandle repo, ObjectId id, FilePath path, bool check_for_binary_data) + { + var buf = new GitBuf(); + var handle = new ObjectSafeWrapper(id, repo).ObjectPtr; + + return new RawContentStream(handle, h => + { + Ensure.ZeroResult(NativeMethods.git_blob_filtered_content(buf, h, path, check_for_binary_data)); + return buf.ptr; + }, + h => (long)buf.size, + new[] { buf }); + } + public static byte[] git_blob_rawcontent(RepositorySafeHandle repo, ObjectId id, int size) { using (var obj = new ObjectSafeWrapper(id, repo)) @@ -85,7 +128,8 @@ public static byte[] git_blob_rawcontent(RepositorySafeHandle repo, ObjectId id, public static UnmanagedMemoryStream git_blob_rawcontent_stream(RepositorySafeHandle repo, ObjectId id, Int64 size) { - return new RawContentStream(id, repo, NativeMethods.git_blob_rawcontent, size); + var handle = new ObjectSafeWrapper(id, repo).ObjectPtr; + return new RawContentStream(handle, NativeMethods.git_blob_rawcontent, h => size); } public static Int64 git_blob_rawsize(GitObjectSafeHandle obj) @@ -127,12 +171,28 @@ public static void git_branch_delete(ReferenceSafeHandle reference) } } - public static ICollection git_branch_foreach( - RepositorySafeHandle repo, - GitBranchType branch_type, - Func resultSelector) + public static IEnumerable git_branch_iterator(Repository repo, GitBranchType branchType) + { + return git_iterator( + (out BranchIteratorSafeHandle iter_out) => + NativeMethods.git_branch_iterator_new(out iter_out, repo.Handle, branchType), + (BranchIteratorSafeHandle iter, out ReferenceSafeHandle ref_out, out int res) => + { + GitBranchType type_out; + res = NativeMethods.git_branch_next(out ref_out, out type_out, iter); + return new { BranchType = type_out }; + }, + (handle, payload) => + { + var reference = Reference.BuildFromPtr(handle, repo); + return new Branch(repo, reference, reference.CanonicalName); + } + ); + } + + public static void git_branch_iterator_free(IntPtr iter) { - return git_foreach(resultSelector, c => NativeMethods.git_branch_foreach(repo, branch_type, (x, y, p) => c(x, y, p), IntPtr.Zero)); + NativeMethods.git_branch_iterator_free(iter); } public static ReferenceSafeHandle git_branch_move(ReferenceSafeHandle reference, string new_branch_name, bool force) @@ -158,7 +218,7 @@ public static string git_branch_remote_name(RepositorySafeHandle repo, string ca int res = NativeMethods.git_branch_remote_name(buffer, (UIntPtr)buffer.Length, repo, canonical_branch_name); Ensure.Int32Result(res); - return Utf8Marshaler.Utf8FromBuffer(buffer) ?? string.Empty; + return LaxUtf8Marshaler.FromBuffer(buffer) ?? string.Empty; } } @@ -182,12 +242,21 @@ public static string git_branch_upstream_name(RepositorySafeHandle handle, strin buffer, (UIntPtr)buffer.Length, handle, canonicalReferenceName); Ensure.Int32Result(res); - return Utf8Marshaler.Utf8FromBuffer(buffer); + return LaxUtf8Marshaler.FromBuffer(buffer); } } #endregion + #region git_buf_ + + public static void git_buf_free(GitBuf buf) + { + NativeMethods.git_buf_free(buf); + } + + #endregion + #region git_checkout_ public static void git_checkout_tree( @@ -335,17 +404,17 @@ public static bool git_config_delete(ConfigurationSafeHandle config, string name } } - public static string git_config_find_global() + public static FilePath git_config_find_global() { return ConvertPath(NativeMethods.git_config_find_global); } - public static string git_config_find_system() + public static FilePath git_config_find_system() { return ConvertPath(NativeMethods.git_config_find_system); } - public static string git_config_find_xdg() + public static FilePath git_config_find_xdg() { return ConvertPath(NativeMethods.git_config_find_xdg); } @@ -377,8 +446,8 @@ public static ConfigurationEntry git_config_get_entry(ConfigurationSafeHan GitConfigEntry entry = handle.MarshalAsGitConfigEntry(); - return new ConfigurationEntry(Utf8Marshaler.FromNative(entry.namePtr), - (T)configurationParser[typeof(T)](Utf8Marshaler.FromNative(entry.valuePtr)), + return new ConfigurationEntry(LaxUtf8Marshaler.FromNative(entry.namePtr), + (T)configurationParser[typeof(T)](LaxUtf8Marshaler.FromNative(entry.valuePtr)), (ConfigurationLevel)entry.level); } @@ -491,6 +560,31 @@ public static ICollection git_config_foreach( return git_foreach(resultSelector, c => NativeMethods.git_config_foreach(config, (e, p) => c(e, p), IntPtr.Zero)); } + public static IEnumerable> git_config_iterator_glob( + ConfigurationSafeHandle config, + string regexp, + Func> resultSelector) + { + return git_iterator( + (out ConfigurationIteratorSafeHandle iter) => + NativeMethods.git_config_iterator_glob_new(out iter, config, regexp), + (ConfigurationIteratorSafeHandle iter, out SafeHandleBase handle, out int res) => + { + handle = null; + + IntPtr entry; + res = NativeMethods.git_config_next(out entry, iter); + return new { EntryPtr = entry }; + }, + (handle, payload) => resultSelector(payload.EntryPtr) + ); + } + + public static void git_config_iterator_free(IntPtr iter) + { + NativeMethods.git_config_iterator_free(iter); + } + #endregion #region git_diff_ @@ -502,7 +596,7 @@ public static void git_diff_blobs( GitDiffOptions options, NativeMethods.git_diff_file_cb fileCallback, NativeMethods.git_diff_hunk_cb hunkCallback, - NativeMethods.git_diff_data_cb lineCallback) + NativeMethods.git_diff_line_cb lineCallback) { using (ThreadAffinity()) using (var osw1 = new ObjectSafeWrapper(oldBlob, repo, true)) @@ -517,19 +611,19 @@ public static void git_diff_blobs( } public static void git_diff_foreach( - DiffListSafeHandle diff, + DiffSafeHandle diff, NativeMethods.git_diff_file_cb fileCallback, NativeMethods.git_diff_hunk_cb hunkCallback, - NativeMethods.git_diff_data_cb dataCallback) + NativeMethods.git_diff_line_cb lineCallback) { using (ThreadAffinity()) { - int res = NativeMethods.git_diff_foreach(diff, fileCallback, hunkCallback, dataCallback, IntPtr.Zero); + int res = NativeMethods.git_diff_foreach(diff, fileCallback, hunkCallback, lineCallback, IntPtr.Zero); Ensure.ZeroResult(res); } } - public static DiffListSafeHandle git_diff_tree_to_index( + public static DiffSafeHandle git_diff_tree_to_index( RepositorySafeHandle repo, IndexSafeHandle index, ObjectId oldTree, @@ -538,7 +632,7 @@ public static DiffListSafeHandle git_diff_tree_to_index( using (ThreadAffinity()) using (var osw = new ObjectSafeWrapper(oldTree, repo, true)) { - DiffListSafeHandle diff; + DiffSafeHandle diff; int res = NativeMethods.git_diff_tree_to_index(out diff, repo, osw.ObjectPtr, index, options); Ensure.ZeroResult(res); @@ -546,12 +640,12 @@ public static DiffListSafeHandle git_diff_tree_to_index( } } - public static void git_diff_list_free(IntPtr diff) + public static void git_diff_free(IntPtr diff) { - NativeMethods.git_diff_list_free(diff); + NativeMethods.git_diff_free(diff); } - public static void git_diff_merge(DiffListSafeHandle onto, DiffListSafeHandle from) + public static void git_diff_merge(DiffSafeHandle onto, DiffSafeHandle from) { using (ThreadAffinity()) { @@ -560,16 +654,17 @@ public static void git_diff_merge(DiffListSafeHandle onto, DiffListSafeHandle fr } } - public static void git_diff_print_patch(DiffListSafeHandle diff, NativeMethods.git_diff_data_cb printCallback) + public static void git_diff_print(DiffSafeHandle diff, NativeMethods.git_diff_line_cb printCallback) { using (ThreadAffinity()) { - int res = NativeMethods.git_diff_print_patch(diff, printCallback, IntPtr.Zero); + int res = NativeMethods.git_diff_print(diff, GitDiffFormat.GIT_DIFF_FORMAT_PATCH, + printCallback, IntPtr.Zero); Ensure.ZeroResult(res); } } - public static DiffListSafeHandle git_diff_tree_to_tree( + public static DiffSafeHandle git_diff_tree_to_tree( RepositorySafeHandle repo, ObjectId oldTree, ObjectId newTree, @@ -579,7 +674,7 @@ public static DiffListSafeHandle git_diff_tree_to_tree( using (var osw1 = new ObjectSafeWrapper(oldTree, repo, true)) using (var osw2 = new ObjectSafeWrapper(newTree, repo, true)) { - DiffListSafeHandle diff; + DiffSafeHandle diff; int res = NativeMethods.git_diff_tree_to_tree(out diff, repo, osw1.ObjectPtr, osw2.ObjectPtr, options); Ensure.ZeroResult(res); @@ -587,14 +682,14 @@ public static DiffListSafeHandle git_diff_tree_to_tree( } } - public static DiffListSafeHandle git_diff_index_to_workdir( + public static DiffSafeHandle git_diff_index_to_workdir( RepositorySafeHandle repo, IndexSafeHandle index, GitDiffOptions options) { using (ThreadAffinity()) { - DiffListSafeHandle diff; + DiffSafeHandle diff; int res = NativeMethods.git_diff_index_to_workdir(out diff, repo, index, options); Ensure.ZeroResult(res); @@ -602,7 +697,7 @@ public static DiffListSafeHandle git_diff_index_to_workdir( } } - public static DiffListSafeHandle git_diff_tree_to_workdir( + public static DiffSafeHandle git_diff_tree_to_workdir( RepositorySafeHandle repo, ObjectId oldTree, GitDiffOptions options) @@ -610,7 +705,7 @@ public static DiffListSafeHandle git_diff_tree_to_workdir( using (ThreadAffinity()) using (var osw = new ObjectSafeWrapper(oldTree, repo, true)) { - DiffListSafeHandle diff; + DiffSafeHandle diff; int res = NativeMethods.git_diff_tree_to_workdir(out diff, repo, osw.ObjectPtr, options); Ensure.ZeroResult(res); @@ -618,6 +713,15 @@ public static DiffListSafeHandle git_diff_tree_to_workdir( } } + public static void git_diff_find_similar(DiffSafeHandle diff, GitDiffFindOptions options) + { + using (ThreadAffinity()) + { + int res = NativeMethods.git_diff_find_similar(diff, options); + Ensure.ZeroResult(res); + } + } + #endregion #region git_graph_ @@ -719,9 +823,9 @@ public static Conflict git_index_conflict_get( Ensure.ZeroResult(res); return new Conflict( - IndexEntry.BuildFromPtr(repo, ancestor), - IndexEntry.BuildFromPtr(repo, ours), - IndexEntry.BuildFromPtr(repo, theirs)); + IndexEntry.BuildFromPtr(ancestor), + IndexEntry.BuildFromPtr(ours), + IndexEntry.BuildFromPtr(theirs)); } public static int git_index_entrycount(IndexSafeHandle index) @@ -776,6 +880,15 @@ public static IndexSafeHandle git_index_open(FilePath indexpath) } } + public static void git_index_read(IndexSafeHandle index) + { + using (ThreadAffinity()) + { + int res = NativeMethods.git_index_read(index, false); + Ensure.ZeroResult(res); + } + } + public static void git_index_remove_bypath(IndexSafeHandle index, FilePath path) { using (ThreadAffinity()) @@ -846,7 +959,7 @@ public static string git_message_prettify(string message) int res = NativeMethods.git_message_prettify(buffer, (UIntPtr)buffer.Length, message, false); Ensure.Int32Result(res); - return Utf8Marshaler.Utf8FromBuffer(buffer) ?? string.Empty; + return LaxUtf8Marshaler.FromBuffer(buffer) ?? string.Empty; } } @@ -1102,6 +1215,27 @@ public static PushSafeHandle git_push_new(RemoteSafeHandle remote) } } + public static void git_push_set_callbacks( + PushSafeHandle push, + NativeMethods.git_push_transfer_progress pushTransferProgress, + NativeMethods.git_packbuilder_progress packBuilderProgress) + { + using (ThreadAffinity()) + { + int res = NativeMethods.git_push_set_callbacks(push, packBuilderProgress, IntPtr.Zero, pushTransferProgress, IntPtr.Zero); + Ensure.ZeroResult(res); + } + } + + public static void git_push_set_options(PushSafeHandle push, GitPushOptions options) + { + using (ThreadAffinity()) + { + int res = NativeMethods.git_push_set_options(push, options); + Ensure.ZeroResult(res); + } + } + public static void git_push_status_foreach(PushSafeHandle push, NativeMethods.push_status_foreach_cb status_cb) { using (ThreadAffinity()) @@ -1285,13 +1419,13 @@ public static void git_reflog_free(IntPtr reflog) NativeMethods.git_reflog_free(reflog); } - public static ReflogSafeHandle git_reflog_read(ReferenceSafeHandle reference) + public static ReflogSafeHandle git_reflog_read(RepositorySafeHandle repo, string canonicalName) { using (ThreadAffinity()) { ReflogSafeHandle reflog_out; - int res = NativeMethods.git_reflog_read(out reflog_out, reference); + int res = NativeMethods.git_reflog_read(out reflog_out, repo, canonicalName); Ensure.ZeroResult(res); return reflog_out; @@ -1359,10 +1493,35 @@ public static string git_refspec_rtransform(GitRefSpecHandle refSpecPtr, string int res = NativeMethods.git_refspec_rtransform(buffer, (UIntPtr)buffer.Length, refSpecPtr, name); Ensure.ZeroResult(res); - return Utf8Marshaler.Utf8FromBuffer(buffer) ?? string.Empty; + return LaxUtf8Marshaler.FromBuffer(buffer) ?? string.Empty; } } + public static string git_refspec_string(GitRefSpecHandle refSpec) + { + return NativeMethods.git_refspec_string(refSpec); + } + + public static string git_refspec_src(GitRefSpecHandle refSpec) + { + return NativeMethods.git_refspec_src(refSpec); + } + + public static string git_refspec_dst(GitRefSpecHandle refSpec) + { + return NativeMethods.git_refspec_dst(refSpec); + } + + public static RefSpecDirection git_refspec_direction(GitRefSpecHandle refSpec) + { + return NativeMethods.git_refspec_direction(refSpec); + } + + public static bool git_refspec_force(GitRefSpecHandle refSpec) + { + return NativeMethods.git_refspec_force(refSpec); + } + #endregion #region git_remote_ @@ -1384,6 +1543,18 @@ public static RemoteSafeHandle git_remote_create(RepositorySafeHandle repo, stri } } + public static RemoteSafeHandle git_remote_create_inmemory(RepositorySafeHandle repo, string url, string refspec) + { + using (ThreadAffinity()) + { + RemoteSafeHandle handle; + int res = NativeMethods.git_remote_create_inmemory(out handle, repo, url, refspec); + Ensure.ZeroResult(res); + + return handle; + } + } + public static void git_remote_connect(RemoteSafeHandle remote, GitDirection direction) { using (ThreadAffinity()) @@ -1406,13 +1577,60 @@ public static GitRefSpecHandle git_remote_get_refspec(RemoteSafeHandle remote, i return NativeMethods.git_remote_get_refspec(remote, (UIntPtr)n); } - public static void git_remote_download(RemoteSafeHandle remote, TransferProgressHandler onTransferProgress) + public static int git_remote_refspec_count(RemoteSafeHandle remote) + { + return (int)NativeMethods.git_remote_refspec_count(remote); + } + + public static IList git_remote_get_fetch_refspecs(RemoteSafeHandle remote) + { + using (ThreadAffinity()) + { + UnSafeNativeMethods.git_strarray arr; + int res = UnSafeNativeMethods.git_remote_get_fetch_refspecs(out arr, remote); + Ensure.ZeroResult(res); + + return Libgit2UnsafeHelper.BuildListOf(arr); + } + } + + public static IList git_remote_get_push_refspecs(RemoteSafeHandle remote) + { + using (ThreadAffinity()) + { + UnSafeNativeMethods.git_strarray arr; + int res = UnSafeNativeMethods.git_remote_get_push_refspecs(out arr, remote); + Ensure.ZeroResult(res); + + return Libgit2UnsafeHelper.BuildListOf(arr); + } + } + + public static void git_remote_set_fetch_refspecs(RemoteSafeHandle remote, IEnumerable refSpecs) { using (ThreadAffinity()) + using (GitStrArrayIn array = GitStrArrayIn.BuildFrom(refSpecs.ToArray())) { - NativeMethods.git_transfer_progress_callback cb = TransferCallbacks.GenerateCallback(onTransferProgress); + int res = NativeMethods.git_remote_set_fetch_refspecs(remote, array); + Ensure.ZeroResult(res); + } + } + + public static void git_remote_set_push_refspecs(RemoteSafeHandle remote, IEnumerable refSpecs) + { + using (ThreadAffinity()) + using (GitStrArrayIn array = GitStrArrayIn.BuildFrom(refSpecs.ToArray())) + { + int res = NativeMethods.git_remote_set_push_refspecs(remote, array); + Ensure.ZeroResult(res); + } + } - int res = NativeMethods.git_remote_download(remote, cb, IntPtr.Zero); + public static void git_remote_download(RemoteSafeHandle remote) + { + using (ThreadAffinity()) + { + int res = NativeMethods.git_remote_download(remote); Ensure.ZeroResult(res); } } @@ -1442,13 +1660,34 @@ public static IList git_remote_list(RepositorySafeHandle repo) } } - public static void git_remote_ls(RemoteSafeHandle remote, NativeMethods.git_headlist_cb headlist_cb) + public static IEnumerable git_remote_ls(Repository repository, RemoteSafeHandle remote) { + var refs = new List(); + IntPtr heads; + UIntPtr size; + using (ThreadAffinity()) { - int res = NativeMethods.git_remote_ls(remote, headlist_cb, IntPtr.Zero); + int res = NativeMethods.git_remote_ls(out heads, out size, remote); Ensure.ZeroResult(res); } + + var grheads = Libgit2UnsafeHelper.RemoteLsHelper(heads, size); + + foreach (var remoteHead in grheads) + { + // The name pointer should never be null - if it is, + // this indicates a bug somewhere (libgit2, server, etc). + if (remoteHead.NamePtr == IntPtr.Zero) + { + throw new InvalidOperationException("Not expecting null value for reference name."); + } + + string name = LaxUtf8Marshaler.FromNative(remoteHead.NamePtr); + refs.Add(new DirectReference(name, repository, remoteHead.Oid)); + } + + return refs; } public static RemoteSafeHandle git_remote_load(RepositorySafeHandle repo, string name, bool throwsIfNotFound) @@ -1505,11 +1744,6 @@ public static void git_remote_set_callbacks(RemoteSafeHandle remote, ref GitRemo } } - public static void git_remote_set_cred_acquire_cb(RemoteSafeHandle remote, NativeMethods.git_cred_acquire_cb cred_acquire_cb, IntPtr payload) - { - NativeMethods.git_remote_set_cred_acquire_cb(remote, cred_acquire_cb, payload); - } - public static void git_remote_update_tips(RemoteSafeHandle remote) { using (ThreadAffinity()) @@ -1547,7 +1781,7 @@ public static ICollection git_repository_fetchhead_foreach( c => NativeMethods.git_repository_fetchhead_foreach( repo, (IntPtr w, IntPtr x, ref GitOid y, bool z, IntPtr p) - => c(Utf8Marshaler.FromNative(w), Utf8Marshaler.FromNative(x), y, z, p), IntPtr.Zero), + => c(LaxUtf8Marshaler.FromNative(w), LaxUtf8Marshaler.FromNative(x), y, z, p), IntPtr.Zero), GitErrorCode.NotFound); } @@ -1604,11 +1838,11 @@ public static bool git_repository_is_shallow(RepositorySafeHandle repo) return RepositoryStateChecker(repo, NativeMethods.git_repository_is_shallow); } - public static void git_repository_merge_cleanup(RepositorySafeHandle repo) + public static void git_repository_state_cleanup(RepositorySafeHandle repo) { using (ThreadAffinity()) { - int res = NativeMethods.git_repository_merge_cleanup(repo); + int res = NativeMethods.git_repository_state_cleanup(repo); Ensure.ZeroResult(res); } } @@ -1645,7 +1879,7 @@ public static string git_repository_message(RepositorySafeHandle repo) throw new LibGit2SharpException("Repository message file changed as we were reading it"); } - return Utf8Marshaler.Utf8FromBuffer(buf); + return LaxUtf8Marshaler.FromBuffer(buf); } } @@ -1745,7 +1979,7 @@ public static FilePath git_repository_workdir(RepositorySafeHandle repo) public static void git_reset( RepositorySafeHandle repo, ObjectId committishId, - ResetOptions resetKind) + ResetMode resetKind) { using (ThreadAffinity()) using (var osw = new ObjectSafeWrapper(committishId, repo)) @@ -1867,6 +2101,11 @@ public static void git_revwalk_sorting(RevWalkerSafeHandle walker, CommitSortStr NativeMethods.git_revwalk_sorting(walker, options); } + public static void git_revwalk_simplify_first_parent(RevWalkerSafeHandle walker) + { + NativeMethods.git_revwalk_simplify_first_parent(walker); + } + #endregion #region git_signature_ @@ -1965,9 +2204,35 @@ public static FileStatus git_status_file(RepositorySafeHandle repo, FilePath pat } } - public static ICollection git_status_foreach(RepositorySafeHandle repo, Func resultSelector) + public static StatusListSafeHandle git_status_list_new(RepositorySafeHandle repo, GitStatusOptions options) + { + using (ThreadAffinity()) + { + StatusListSafeHandle handle; + int res = NativeMethods.git_status_list_new(out handle, repo, options); + Ensure.ZeroResult(res); + return handle; + } + } + + public static int git_status_list_entrycount(StatusListSafeHandle list) { - return git_foreach(resultSelector, c => NativeMethods.git_status_foreach(repo, (x, y, p) => c(x, y, p), IntPtr.Zero)); + using (ThreadAffinity()) + { + int res = NativeMethods.git_status_list_entrycount(list); + Ensure.Int32Result(res); + return res; + } + } + + public static StatusEntrySafeHandle git_status_byindex(StatusListSafeHandle list, long idx) + { + return NativeMethods.git_status_byindex(list, (UIntPtr)idx); + } + + public static void git_status_list_free(IntPtr statusList) + { + NativeMethods.git_status_list_free(statusList); } #endregion @@ -2385,6 +2650,68 @@ private static ICollection git_foreach( } } + private delegate int IteratorNew(out THandle iter); + + private delegate TPayload IteratorNext(TIterator iter, out THandle next, out int res); + + private static THandle git_iterator_new(IteratorNew newFunc) + where THandle : SafeHandleBase + { + THandle iter; + Ensure.ZeroResult(newFunc(out iter)); + return iter; + } + + private static IEnumerable git_iterator_next( + TIterator iter, + IteratorNext nextFunc, + Func resultSelector) + where THandle : SafeHandleBase + { + while (true) + { + var next = default(THandle); + try + { + int res; + var payload = nextFunc(iter, out next, out res); + + if (res == (int)GitErrorCode.IterOver) + { + yield break; + } + + Ensure.ZeroResult(res); + yield return resultSelector(next, payload); + } + finally + { + if (next != null) + next.SafeDispose(); + } + } + } + + private static IEnumerable git_iterator( + IteratorNew newFunc, + IteratorNext nextFunc, + Func resultSelector + ) + where TIterator : SafeHandleBase + where THandle : SafeHandleBase + { + using (ThreadAffinity()) + { + using (var iter = git_iterator_new(newFunc)) + { + foreach (var next in git_iterator_next(iter, nextFunc, resultSelector)) + { + yield return next; + } + } + } + } + private static unsafe class Libgit2UnsafeHelper { public static IList BuildListOf(UnSafeNativeMethods.git_strarray strArray) @@ -2398,7 +2725,7 @@ public static IList BuildListOf(UnSafeNativeMethods.git_strarray strArra var list = new List(numberOfEntries); for (uint i = 0; i < numberOfEntries; i++) { - var name = Utf8Marshaler.FromNative((IntPtr)gitStrArray->strings[i]); + var name = LaxUtf8Marshaler.FromNative((IntPtr)gitStrArray->strings[i]); list.Add(name); } @@ -2409,6 +2736,19 @@ public static IList BuildListOf(UnSafeNativeMethods.git_strarray strArra UnSafeNativeMethods.git_strarray_free(ref strArray); } } + + public static IList RemoteLsHelper(IntPtr heads, UIntPtr size) + { + var rawHeads = (IntPtr*) heads; + var count = (int) size; + + var list = new List(count); + for (int i = 0; i < count; i++) + { + list.Add((GitRemoteHead)Marshal.PtrToStructure(rawHeads[i], typeof (GitRemoteHead))); + } + return list; + } } private static bool RepositoryStateChecker(RepositorySafeHandle repo, Func checker) @@ -2422,7 +2762,7 @@ private static bool RepositoryStateChecker(RepositorySafeHandle repo, Func pathRetriever) + private static FilePath ConvertPath(Func pathRetriever) { using (ThreadAffinity()) { @@ -2437,7 +2777,7 @@ private static string ConvertPath(Func pathRetriever) Ensure.ZeroResult(result); - return Utf8Marshaler.Utf8FromBuffer(buffer); + return LaxFilePathMarshaler.FromBuffer(buffer); } } @@ -2478,6 +2818,19 @@ public void Dispose() { typeof(bool), value => git_config_parse_bool(value) }, { typeof(string), value => value }, }; + + /// + /// Helper method for consistent conversion of return value on + /// Callbacks that support cancellation from bool to native type. + /// True indicates that function should continue, false indicates + /// user wants to cancel. + /// + /// + /// + internal static int ConvertResultToCancelFlag(bool result) + { + return result ? 0 : -1; + } } } // ReSharper restore InconsistentNaming diff --git a/LibGit2Sharp/Core/PushTransferProgressCallbacks.cs b/LibGit2Sharp/Core/PushTransferProgressCallbacks.cs new file mode 100644 index 000000000..2d40bf687 --- /dev/null +++ b/LibGit2Sharp/Core/PushTransferProgressCallbacks.cs @@ -0,0 +1,38 @@ +using System; +using LibGit2Sharp.Handlers; + +namespace LibGit2Sharp.Core +{ + internal class PushTransferCallbacks + { + private readonly PushTransferProgressHandler onPushTransferProgress; + + /// + /// Constructor to set up the native callback given managed delegate. + /// + /// The delegate that the git_transfer_progress_callback will call. + internal PushTransferCallbacks(PushTransferProgressHandler onPushTransferProgress) + { + this.onPushTransferProgress = onPushTransferProgress; + } + + /// + /// Generates a delegate that matches the native git_transfer_progress_callback function's signature and wraps the delegate. + /// + /// A delegate method with a signature that matches git_transfer_progress_callback. + internal NativeMethods.git_push_transfer_progress GenerateCallback() + { + if (onPushTransferProgress == null) + { + return null; + } + + return new PushTransferCallbacks(onPushTransferProgress).OnGitTransferProgress; + } + + private int OnGitTransferProgress(uint current, uint total, UIntPtr bytes, IntPtr payload) + { + return Proxy.ConvertResultToCancelFlag(onPushTransferProgress((int)current, (int)total, (long)bytes)); + } + } +} diff --git a/LibGit2Sharp/Core/RawContentStream.cs b/LibGit2Sharp/Core/RawContentStream.cs index 825b09763..d02fe8b81 100644 --- a/LibGit2Sharp/Core/RawContentStream.cs +++ b/LibGit2Sharp/Core/RawContentStream.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using LibGit2Sharp.Core.Handles; @@ -6,25 +7,62 @@ namespace LibGit2Sharp.Core { internal class RawContentStream : UnmanagedMemoryStream { - private readonly ObjectSafeWrapper wrapper; + private readonly GitObjectSafeHandle handle; + private readonly ICollection linkedResources; - internal RawContentStream(ObjectId id, RepositorySafeHandle repo, - Func bytePtrProvider, long length) - : this(new ObjectSafeWrapper(id, repo), bytePtrProvider, length) + internal unsafe RawContentStream( + GitObjectSafeHandle handle, + Func bytePtrProvider, + Func sizeProvider, + ICollection linkedResources = null) + : base((byte*)Wrap(handle, bytePtrProvider, linkedResources).ToPointer(), + Wrap(handle, sizeProvider, linkedResources)) { + this.handle = handle; + this.linkedResources = linkedResources; } - unsafe RawContentStream(ObjectSafeWrapper wrapper, - Func bytePtrProvider, long length) - : base((byte*)bytePtrProvider(wrapper.ObjectPtr).ToPointer(), length) + private static T Wrap( + GitObjectSafeHandle handle, + Func provider, + IEnumerable linkedResources) { - this.wrapper = wrapper; + T value; + + try + { + value = provider(handle); + } + catch + { + Dispose(handle, linkedResources); + throw; + } + + return value; + } + + private static void Dispose( + GitObjectSafeHandle handle, + IEnumerable linkedResources) + { + handle.SafeDispose(); + + if (linkedResources == null) + { + return; + } + + foreach (IDisposable linkedResource in linkedResources) + { + linkedResource.Dispose(); + } } protected override void Dispose(bool disposing) { base.Dispose(disposing); - wrapper.SafeDispose(); + Dispose(handle, linkedResources); } - } + } } diff --git a/LibGit2Sharp/Core/StringExtensions.cs b/LibGit2Sharp/Core/StringExtensions.cs new file mode 100644 index 000000000..2d557e23f --- /dev/null +++ b/LibGit2Sharp/Core/StringExtensions.cs @@ -0,0 +1,12 @@ +using System; + +namespace LibGit2Sharp.Core +{ + internal static class StringExtensions + { + public static int OctalToInt32(this string octal) + { + return Convert.ToInt32(octal, 8); + } + } +} diff --git a/LibGit2Sharp/Core/TarWriter.cs b/LibGit2Sharp/Core/TarWriter.cs new file mode 100644 index 000000000..f86f56a18 --- /dev/null +++ b/LibGit2Sharp/Core/TarWriter.cs @@ -0,0 +1,447 @@ +/* + * Source: http://code.google.com/p/tar-cs/ + * + * BSD License + * + * Copyright (c) 2009, Vladimir Vasiltsov + * 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. + * * Names of its contributors may not 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. + */ + +using System; +using System.IO; +using System.Linq; +using System.Net; +using System.Text; + +namespace LibGit2Sharp.Core +{ + internal class TarWriter : IDisposable + { + private readonly Stream outStream; + + /// + /// Writes tar (see GNU tar) archive to a stream + /// + /// stream to write archive to + public TarWriter(Stream writeStream) + { + outStream = writeStream; + } + + protected Stream OutStream + { + get { return outStream; } + } + + #region IDisposable Members + + public void Dispose() + { + AlignTo512(0, true); + AlignTo512(0, true); + + GC.SuppressFinalize(this); + } + + #endregion + + public void Write( + FilePath filePath, + Stream data, + DateTimeOffset modificationTime, + int mode, + string userId, + string groupId, + char typeflag, + string userName, + string groupName, + string deviceMajorNumber, + string deviceMinorNumber, + string entrySha, + bool isLink) + { + FileNameExtendedHeader fileNameExtendedHeader = FileNameExtendedHeader.Parse(filePath.Posix, entrySha); + LinkExtendedHeader linkExtendedHeader = ParseLink(isLink, data, entrySha); + + WriteExtendedHeader(fileNameExtendedHeader, linkExtendedHeader, entrySha, modificationTime); + + // Note: in case of links, we won't add a content, but the size in the header will still be != 0. It seems strange, but it seem to be what git.git is doing? + WriteHeader(fileNameExtendedHeader.Name, fileNameExtendedHeader.Prefix, modificationTime, (data != null) ? data.Length : 0, mode, + userId, groupId, typeflag, linkExtendedHeader.Link, userName, groupName, deviceMajorNumber, deviceMinorNumber); + + // folders have no data, and so do links + if (data != null && !isLink) + { + WriteContent(data.Length, data, OutStream); + } + AlignTo512((data != null) ? data.Length : 0, false); + } + + protected void WriteContent(long count, Stream data, Stream dest) + { + var buffer = new byte[1024]; + + while (count > 0 && count > buffer.Length) + { + int bytesRead = data.Read(buffer, 0, buffer.Length); + if (bytesRead < 0) + throw new IOException("TarWriter unable to read from provided stream"); + + dest.Write(buffer, 0, bytesRead); + count -= bytesRead; + } + if (count > 0) + { + int bytesRead = data.Read(buffer, 0, (int)count); + if (bytesRead < 0) + throw new IOException("TarWriter unable to read from provided stream"); + if (bytesRead == 0) + { + while (count > 0) + { + dest.WriteByte(0); + --count; + } + } + else + dest.Write(buffer, 0, bytesRead); + } + } + + protected void AlignTo512(long size, bool acceptZero) + { + size = size % 512; + if (size == 0 && !acceptZero) return; + while (size < 512) + { + OutStream.WriteByte(0); + size++; + } + } + + protected void WriteHeader( + string fileName, + string namePrefix, + DateTimeOffset lastModificationTime, + long count, + int mode, + string userId, + string groupId, + char typeflag, + string link, + string userName, + string groupName, + string deviceMajorNumber, + string deviceMinorNumber) + { + var tarHeader = new UsTarHeader(fileName, namePrefix, lastModificationTime, count, mode, + userId, groupId, typeflag, link, userName, groupName, deviceMajorNumber, deviceMinorNumber); + var header = tarHeader.GetHeaderValue(); + OutStream.Write(header, 0, header.Length); + } + + private LinkExtendedHeader ParseLink(bool isLink, Stream data, string entrySha) + { + if (!isLink) + { + return new LinkExtendedHeader(string.Empty, string.Empty, false); + } + + using (var dest = new MemoryStream()) + { + WriteContent(data.Length, data, dest); + dest.Seek(0, SeekOrigin.Begin); + + using (var linkStream = new StreamReader(dest)) + { + string link = linkStream.ReadToEnd(); + + if (data.Length > 100) + { + return new LinkExtendedHeader(link, string.Format("see %s.paxheader{0}", entrySha), true); + } + + return new LinkExtendedHeader(link, link, false); + } + } + } + + private void WriteExtendedHeader(FileNameExtendedHeader fileNameExtendedHeader, LinkExtendedHeader linkExtendedHeader, string entrySha, + DateTimeOffset modificationTime) + { + string extHeader = string.Empty; + + if (fileNameExtendedHeader.NeedsExtendedHeaderEntry) + { + extHeader += BuildKeyValueExtHeader("path", fileNameExtendedHeader.InitialPath); + } + + if (linkExtendedHeader.NeedsExtendedHeaderEntry) + { + extHeader += BuildKeyValueExtHeader("linkpath", linkExtendedHeader.InitialLink); + } + + if (string.IsNullOrEmpty(extHeader)) + { + return; + } + + using (var stream = new MemoryStream(Encoding.ASCII.GetBytes(extHeader))) + { + Write(string.Format("{0}.paxheader", entrySha), stream, modificationTime, "666".OctalToInt32(), + "0", "0", 'x', "root", "root", "0", "0", entrySha, false); + } + } + + private string BuildKeyValueExtHeader(string key, string value) + { + // "%u %s=%s\n" + int len = key.Length + value.Length + 3; + for (int i = len; i > 9; i /= 10) + len++; + + return string.Format("{0} {1}={2}\n", len, key, value); + } + + /// + /// UsTar header implementation. + /// + private class UsTarHeader + { + private readonly string mode; + private readonly long size; + private readonly string unixTime; + private const string magic = "ustar"; + private const string version = "00"; + private readonly string userName; + private readonly string groupName; + private readonly string userId; + private readonly string groupId; + private readonly char typeflag; + private readonly string link; + private readonly string deviceMajorNumber; + private readonly string deviceMinorNumber; + private readonly string namePrefix; + private readonly string fileName; + + public UsTarHeader( + string fileName, + string namePrefix, + DateTimeOffset lastModificationTime, + long size, + int mode, + string userId, + string groupId, + char typeflag, + string link, + string userName, + string groupName, + string deviceMajorNumber, + string deviceMinorNumber) + { + #region Length validations + + if (userName.Length > 32) + { + throw new ArgumentException("ustar userName cannot be longer than 32 characters.", "userName"); + } + if (groupName.Length > 32) + { + throw new ArgumentException("ustar groupName cannot be longer than 32 characters.", "groupName"); + } + if (userId.Length > 7) + { + throw new ArgumentException("ustar userId cannot be longer than 7 characters.", "userId"); + } + if (groupId.Length > 7) + { + throw new ArgumentException("ustar groupId cannot be longer than 7 characters.", "groupId"); + } + if (deviceMajorNumber.Length > 7) + { + throw new ArgumentException("ustar deviceMajorNumber cannot be longer than 7 characters.", "deviceMajorNumber"); + } + if (deviceMinorNumber.Length > 7) + { + throw new ArgumentException("ustar deviceMinorNumber cannot be longer than 7 characters.", "deviceMinorNumber"); + } + if (link.Length > 100) + { + throw new ArgumentException("ustar link cannot be longer than 100 characters.", "link"); + } + + #endregion + + this.mode = Convert.ToString(mode, 8).PadLeft(7, '0'); + this.size = size; + unixTime = Convert.ToString(lastModificationTime.ToSecondsSinceEpoch(), 8).PadLeft(11, '0'); + this.userId = userId.PadLeft(7, '0'); + this.groupId = userId.PadLeft(7, '0'); + this.userName = userName; + this.groupName = groupName; + this.typeflag = typeflag; + this.link = link; + this.deviceMajorNumber = deviceMajorNumber.PadLeft(7, '0'); + this.deviceMinorNumber = deviceMinorNumber.PadLeft(7, '0'); + + this.fileName = fileName; + this.namePrefix = namePrefix; + } + + public byte[] GetHeaderValue() + { + var buffer = new byte[512]; + + // Fill header + Encoding.ASCII.GetBytes(fileName.PadRight(100, '\0')).CopyTo(buffer, 0); + Encoding.ASCII.GetBytes(mode).CopyTo(buffer, 100); + Encoding.ASCII.GetBytes(userId).CopyTo(buffer, 108); + Encoding.ASCII.GetBytes(groupId).CopyTo(buffer, 116); + Encoding.ASCII.GetBytes(Convert.ToString(size, 8).PadLeft(11, '0')).CopyTo(buffer, 124); + Encoding.ASCII.GetBytes(unixTime).CopyTo(buffer, 136); + buffer[156] = Convert.ToByte(typeflag); + Encoding.ASCII.GetBytes(link).CopyTo(buffer, 157); + + Encoding.ASCII.GetBytes(magic).CopyTo(buffer, 257); // Mark header as ustar + Encoding.ASCII.GetBytes(version).CopyTo(buffer, 263); + Encoding.ASCII.GetBytes(userName).CopyTo(buffer, 265); + Encoding.ASCII.GetBytes(groupName).CopyTo(buffer, 297); + Encoding.ASCII.GetBytes(deviceMajorNumber).CopyTo(buffer, 329); + Encoding.ASCII.GetBytes(deviceMinorNumber).CopyTo(buffer, 337); + Encoding.ASCII.GetBytes(namePrefix).CopyTo(buffer, 345); + + if (size >= 0x1FFFFFFFF) + { + byte[] bytes = BitConverter.GetBytes(IPAddress.HostToNetworkOrder(size)); + SetMarker(AlignTo12(bytes)).CopyTo(buffer, 124); + } + + string checksum = CalculateChecksum(buffer); + Encoding.ASCII.GetBytes(checksum).CopyTo(buffer, 148); + Encoding.ASCII.GetBytes("\0").CopyTo(buffer, 155); + + return buffer; + } + + private string CalculateChecksum(byte[] buf) + { + Encoding.ASCII.GetBytes(new string(' ', 8)).CopyTo(buf, 148); + + long headerChecksum = buf.Aggregate(0, (current, b) => current + b); + + return Convert.ToString(headerChecksum, 8).PadLeft(7, '0'); + } + + private static byte[] SetMarker(byte[] bytes) + { + bytes[0] |= 0x80; + return bytes; + } + + private static byte[] AlignTo12(byte[] bytes) + { + var retVal = new byte[12]; + bytes.CopyTo(retVal, 12 - bytes.Length); + return retVal; + } + } + + private class FileNameExtendedHeader + { + private readonly string prefix; + private readonly string name; + private readonly string initialPath; + private readonly bool needsExtendedHeaderEntry; + + private FileNameExtendedHeader(string initialPath, string prefix, string name, bool needsExtendedHeaderEntry) + { + this.initialPath = initialPath; + this.prefix = prefix; + this.name = name; + this.needsExtendedHeaderEntry = needsExtendedHeaderEntry; + } + + public bool NeedsExtendedHeaderEntry + { + get { return needsExtendedHeaderEntry; } + } + + public string Name + { + get { return name; } + } + + public string Prefix + { + get { return prefix; } + } + + public string InitialPath + { + get { return initialPath; } + } + + /// + /// Logic taken from https://github.com/git/git/blob/master/archive-tar.c + /// + public static FileNameExtendedHeader Parse(string posixPath, string entrySha) + { + if (posixPath.Length > 100) + { + // Need to increment by one because while loop decrements first before testing for path separator + int position = Math.Min(156, posixPath.Length); + + while (--position > 0 && !Equals('/', posixPath[position])) + { } + + int remaining = posixPath.Length - position - 1; + if (remaining < 100 && position > 0) + { + return new FileNameExtendedHeader(posixPath, posixPath.Substring(0, position), posixPath.Substring(position, posixPath.Length - position), false); + } + + return new FileNameExtendedHeader(posixPath, string.Empty, string.Format("{0}.data", entrySha), true); + } + + return new FileNameExtendedHeader(posixPath, string.Empty, posixPath, false); + } + } + + private class LinkExtendedHeader + { + private readonly string initialLink; + private readonly string link; + private readonly bool needsExtendedHeaderEntry; + + public LinkExtendedHeader(string initialLink, string link, bool needsExtendedHeaderEntry) + { + this.initialLink = initialLink; + this.link = link; + this.needsExtendedHeaderEntry = needsExtendedHeaderEntry; + } + + public string InitialLink + { + get { return initialLink; } + } + + public bool NeedsExtendedHeaderEntry + { + get { return needsExtendedHeaderEntry; } + } + + public string Link + { + get { return link; } + } + } + } +} diff --git a/LibGit2Sharp/Core/UnSafeNativeMethods.cs b/LibGit2Sharp/Core/UnSafeNativeMethods.cs index 3d692d9d9..be17f4397 100644 --- a/LibGit2Sharp/Core/UnSafeNativeMethods.cs +++ b/LibGit2Sharp/Core/UnSafeNativeMethods.cs @@ -20,6 +20,12 @@ internal static unsafe class UnSafeNativeMethods [DllImport(libgit2)] internal static extern void git_strarray_free(ref git_strarray array); + [DllImport(libgit2)] + internal static extern int git_remote_get_fetch_refspecs(out git_strarray array, RemoteSafeHandle remote); + + [DllImport(libgit2)] + internal static extern int git_remote_get_push_refspecs(out git_strarray array, RemoteSafeHandle remote); + #region Nested type: git_strarray internal struct git_strarray diff --git a/LibGit2Sharp/Core/Utf8Marshaler.cs b/LibGit2Sharp/Core/Utf8Marshaler.cs index ad199db7b..8a480816f 100644 --- a/LibGit2Sharp/Core/Utf8Marshaler.cs +++ b/LibGit2Sharp/Core/Utf8Marshaler.cs @@ -12,11 +12,11 @@ namespace LibGit2Sharp.Core /// Use this marshaler for return values, for example: /// [return: MarshalAs(UnmanagedType.CustomMarshaler, /// MarshalCookie = UniqueId.UniqueIdentifier, - /// MarshalTypeRef = typeof(Utf8NoCleanupMarshaler))] + /// MarshalTypeRef = typeof(LaxUtf8NoCleanupMarshaler))] /// - internal class Utf8NoCleanupMarshaler : Utf8Marshaler + internal class LaxUtf8NoCleanupMarshaler : LaxUtf8Marshaler { - private static readonly Utf8NoCleanupMarshaler staticInstance = new Utf8NoCleanupMarshaler(); + private static readonly LaxUtf8NoCleanupMarshaler staticInstance = new LaxUtf8NoCleanupMarshaler(); public new static ICustomMarshaler GetInstance(String cookie) { @@ -44,11 +44,21 @@ public override void CleanUpNativeData(IntPtr pNativeData) /// internal static extern int git_tag_delete(RepositorySafeHandle repo, /// [MarshalAs(UnmanagedType.CustomMarshaler, /// MarshalCookie = UniqueId.UniqueIdentifier, - /// MarshalTypeRef = typeof(Utf8Marshaler))] String tagName); + /// MarshalTypeRef = typeof(StrictUtf8Marshaler))] String tagName); /// - internal class Utf8Marshaler : ICustomMarshaler + internal class StrictUtf8Marshaler : EncodingMarshaler { - private static readonly Utf8Marshaler staticInstance = new Utf8Marshaler(); + private static readonly StrictUtf8Marshaler staticInstance; + private static readonly Encoding encoding; + + static StrictUtf8Marshaler() + { + encoding = new UTF8Encoding(false, true); + staticInstance = new StrictUtf8Marshaler(); + } + + public StrictUtf8Marshaler() : base(encoding) + { } public static ICustomMarshaler GetInstance(String cookie) { @@ -57,132 +67,67 @@ public static ICustomMarshaler GetInstance(String cookie) #region ICustomMarshaler - public void CleanUpManagedData(Object managedObj) + public override Object MarshalNativeToManaged(IntPtr pNativeData) { + throw new InvalidOperationException( + string.Format("{0} cannot be used to retrieve data from libgit2.", GetType().Name)); } - public virtual void CleanUpNativeData(IntPtr pNativeData) - { - if (IntPtr.Zero != pNativeData) - { - Marshal.FreeHGlobal(pNativeData); - } - } + #endregion - public int GetNativeDataSize() + public static IntPtr FromManaged(String value) { - // Not a value type - return -1; + return FromManaged(encoding, value); } + } - public IntPtr MarshalManagedToNative(Object managedObj) - { - if (null == managedObj) - { - return IntPtr.Zero; - } + /// + /// This marshaler is to be used for capturing a UTF-8 string allocated by libgit2 and + /// converting it to a managed String instance. The marshaler will free the native pointer + /// after conversion. + /// + internal class LaxUtf8Marshaler : EncodingMarshaler + { + private static readonly LaxUtf8Marshaler staticInstance = new LaxUtf8Marshaler(); - String str = managedObj as String; + private static readonly Encoding encoding = new UTF8Encoding(false, false); - if (null == str) - { - throw new MarshalDirectiveException("Utf8Marshaler must be used on a string."); - } + public LaxUtf8Marshaler() : base(encoding) + { } - return FromManaged(str); + public static ICustomMarshaler GetInstance(String cookie) + { + return staticInstance; } - public Object MarshalNativeToManaged(IntPtr pNativeData) + #region ICustomMarshaler + + public override IntPtr MarshalManagedToNative(object managedObj) { - return FromNative(pNativeData); + throw new InvalidOperationException( + string.Format("{0} cannot be used to pass data to libgit2.", GetType().Name)); } #endregion - public static unsafe IntPtr FromManaged(String value) + public static string FromNative(IntPtr pNativeData) { - if (null == value) - { - return IntPtr.Zero; - } - - int length = Encoding.UTF8.GetByteCount(value); - byte* buffer = (byte*)Marshal.AllocHGlobal(length + 1).ToPointer(); - - if (length > 0) - { - fixed (char* pValue = value) - { - Encoding.UTF8.GetBytes(pValue, value.Length, buffer, length); - } - } - - buffer[length] = 0; - - return new IntPtr(buffer); + return FromNative(encoding, pNativeData); } - public static unsafe String FromNative(IntPtr pNativeData) + public static string FromNative(IntPtr pNativeData, int length) { - if (IntPtr.Zero == pNativeData) - { - return null; - } - - byte* start = (byte*)pNativeData; - byte* walk = start; - - // Find the end of the string - while (*walk != 0) - { - walk++; - } - - if (walk == start) - { - return String.Empty; - } - - return new String((sbyte*)pNativeData.ToPointer(), 0, (int)(walk - start), Encoding.UTF8); + return FromNative(encoding, pNativeData, length); } - public static unsafe String FromNative(IntPtr pNativeData, int length) + public static string FromBuffer(byte[] buffer) { - if (IntPtr.Zero == pNativeData) - { - return null; - } - - if (0 == length) - { - return String.Empty; - } - - return new String((sbyte*)pNativeData.ToPointer(), 0, length, Encoding.UTF8); + return FromBuffer(encoding, buffer); } - public static String Utf8FromBuffer(byte[] buffer) + public static string FromBuffer(byte[] buffer, int length) { - if (null == buffer) - { - return null; - } - - int length = 0; - int stop = buffer.Length; - - while (length < stop && - 0 != buffer[length]) - { - length++; - } - - if (0 == length) - { - return String.Empty; - } - - return Encoding.UTF8.GetString(buffer, 0, length); + return FromBuffer(encoding, buffer, length); } } } diff --git a/LibGit2Sharp/Diff.cs b/LibGit2Sharp/Diff.cs index 29034b001..3dafa58aa 100644 --- a/LibGit2Sharp/Diff.cs +++ b/LibGit2Sharp/Diff.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Text; using LibGit2Sharp.Core; @@ -33,7 +34,7 @@ private static GitDiffOptions BuildOptions(DiffModifiers diffOptions, FilePath[] { options.Flags |= GitDiffOptionFlags.GIT_DIFF_INCLUDE_UNTRACKED | GitDiffOptionFlags.GIT_DIFF_RECURSE_UNTRACKED_DIRS | - GitDiffOptionFlags.GIT_DIFF_INCLUDE_UNTRACKED_CONTENT; + GitDiffOptionFlags.GIT_DIFF_SHOW_UNTRACKED_CONTENT; } if (diffOptions.HasFlag(DiffModifiers.IncludeIgnored)) @@ -42,7 +43,10 @@ private static GitDiffOptions BuildOptions(DiffModifiers diffOptions, FilePath[] GitDiffOptionFlags.GIT_DIFF_RECURSE_IGNORED_DIRS; } - if (diffOptions.HasFlag(DiffModifiers.IncludeUnmodified)) + if (diffOptions.HasFlag(DiffModifiers.IncludeUnmodified) || compareOptions.IncludeUnmodified || + (compareOptions.Similarity != null && + (compareOptions.Similarity.RenameDetectionMode == RenameDetectionMode.CopiesHarder || + compareOptions.Similarity.RenameDetectionMode == RenameDetectionMode.Exact))) { options.Flags |= GitDiffOptionFlags.GIT_DIFF_INCLUDE_UNMODIFIED; } @@ -77,6 +81,39 @@ internal Diff(Repository repo) this.repo = repo; } + private static readonly IDictionary> HandleRetrieverDispatcher = BuildHandleRetrieverDispatcher(); + + private static IDictionary> BuildHandleRetrieverDispatcher() + { + return new Dictionary> + { + { DiffTargets.Index, IndexToTree }, + { DiffTargets.WorkingDirectory, WorkdirToTree }, + { DiffTargets.Index | DiffTargets.WorkingDirectory, WorkdirAndIndexToTree }, + }; + } + + private static readonly IDictionary> ChangesBuilders = new Dictionary> + { + { typeof(Patch), diff => new Patch(diff) }, + { typeof(TreeChanges), diff => new TreeChanges(diff) }, + }; + + /// + /// Show changes between two s. + /// + /// The you want to compare from. + /// The you want to compare to. + /// Additional options to define comparison behavior. + /// A containing the changes between the and the . + public virtual ContentChanges Compare(Blob oldBlob, Blob newBlob, CompareOptions compareOptions = null) + { + using (GitDiffOptions options = BuildOptions(DiffModifiers.None, compareOptions: compareOptions)) + { + return new ContentChanges(repo, oldBlob, newBlob, options); + } + } + /// /// Show changes between two s. /// @@ -87,10 +124,20 @@ internal Diff(Repository repo) /// If set, the passed will be treated as explicit paths. /// Use these options to determine how unmatched explicit paths should be handled. /// - /// Additional options to define comparison behavior. + /// Additional options to define patch generation behavior. /// A containing the changes between the and the . - public virtual TreeChanges Compare(Tree oldTree, Tree newTree, IEnumerable paths = null, ExplicitPathsOptions explicitPathsOptions = null, CompareOptions compareOptions = null) + public virtual T Compare(Tree oldTree, Tree newTree, IEnumerable paths = null, ExplicitPathsOptions explicitPathsOptions = null, + CompareOptions compareOptions = null) where T : class { + Func builder; + + if (!ChangesBuilders.TryGetValue(typeof (T), out builder)) + { + throw new LibGit2SharpException(string.Format(CultureInfo.InvariantCulture, + "Unexpected type '{0}' passed to Compare. Supported values are either '{1}' or '{2}'.", typeof (T), + typeof (TreeChanges), typeof (Patch))); + } + var comparer = TreeToTree(repo); ObjectId oldTreeId = oldTree != null ? oldTree.Id : null; ObjectId newTreeId = newTree != null ? newTree.Id : null; @@ -107,38 +154,19 @@ public virtual TreeChanges Compare(Tree oldTree, Tree newTree, IEnumerable - /// Show changes between two s. - /// - /// The you want to compare from. - /// The you want to compare to. - /// Additional options to define comparison behavior. - /// A containing the changes between the and the . - public virtual ContentChanges Compare(Blob oldBlob, Blob newBlob, CompareOptions compareOptions = null) - { - using (GitDiffOptions options = BuildOptions(DiffModifiers.None, compareOptions: compareOptions)) + using (DiffSafeHandle diff = BuildDiffList(oldTreeId, newTreeId, comparer, + diffOptions, paths, explicitPathsOptions, compareOptions)) { - return new ContentChanges(repo, oldBlob, newBlob, options); + return (T)builder(diff); } } - private readonly IDictionary> handleRetrieverDispatcher = BuildHandleRetrieverDispatcher(); - - private static IDictionary> BuildHandleRetrieverDispatcher() - { - return new Dictionary> - { - { DiffTargets.Index, IndexToTree }, - { DiffTargets.WorkingDirectory, WorkdirToTree }, - { DiffTargets.Index | DiffTargets.WorkingDirectory, WorkdirAndIndexToTree }, - }; - } - /// /// Show changes between a and the Index, the Working Directory, or both. + /// + /// The level of diff performed can be specified by passing either a + /// or type as the generic parameter. + /// /// /// The to compare from. /// The targets to compare to. @@ -147,15 +175,28 @@ private static IDictionary will be treated as explicit paths. /// Use these options to determine how unmatched explicit paths should be handled. /// - /// Additional options to define comparison behavior. - /// A containing the changes between the and the selected target. - public virtual TreeChanges Compare(Tree oldTree, DiffTargets diffTargets, IEnumerable paths = null, ExplicitPathsOptions explicitPathsOptions = null, CompareOptions compareOptions = null) + /// Additional options to define patch generation behavior. + /// Can be either a if you are only interested in the list of files modified, added, ..., or + /// a if you want the actual patch content for the whole diff and for individual files. + /// A containing the changes between the and the selected target. + public virtual T Compare(Tree oldTree, DiffTargets diffTargets, IEnumerable paths = null, + ExplicitPathsOptions explicitPathsOptions = null, CompareOptions compareOptions = null) where T : class { - var comparer = handleRetrieverDispatcher[diffTargets](repo); + Func builder; + + if (!ChangesBuilders.TryGetValue(typeof (T), out builder)) + { + throw new LibGit2SharpException(string.Format(CultureInfo.InvariantCulture, + "Unexpected type '{0}' passed to Compare. Supported values are either '{1}' or '{2}'.", typeof (T), + typeof (TreeChanges), typeof (Patch))); + } + + var comparer = HandleRetrieverDispatcher[diffTargets](repo); ObjectId oldTreeId = oldTree != null ? oldTree.Id : null; - DiffModifiers diffOptions = diffTargets.HasFlag(DiffTargets.WorkingDirectory) ? - DiffModifiers.IncludeUntracked : DiffModifiers.None; + DiffModifiers diffOptions = diffTargets.HasFlag(DiffTargets.WorkingDirectory) + ? DiffModifiers.IncludeUntracked + : DiffModifiers.None; if (explicitPathsOptions != null) { @@ -168,11 +209,19 @@ public virtual TreeChanges Compare(Tree oldTree, DiffTargets diffTargets, IEnume } } - return BuildTreeChangesFromComparer(oldTreeId, null, comparer, diffOptions, paths, explicitPathsOptions, compareOptions); + using (DiffSafeHandle diff = BuildDiffList(oldTreeId, null, comparer, + diffOptions, paths, explicitPathsOptions, compareOptions)) + { + return (T)builder(diff); + } } /// /// Show changes between the working directory and the index. + /// + /// The level of diff performed can be specified by passing either a + /// or type as the generic parameter. + /// /// /// The list of paths (either files or directories) that should be compared. /// If true, include untracked files from the working dir as additions. Otherwise ignore them. @@ -180,16 +229,28 @@ public virtual TreeChanges Compare(Tree oldTree, DiffTargets diffTargets, IEnume /// If set, the passed will be treated as explicit paths. /// Use these options to determine how unmatched explicit paths should be handled. /// - /// Additional options to define comparison behavior. - /// A containing the changes between the working directory and the index. - public virtual TreeChanges Compare(IEnumerable paths = null, bool includeUntracked = false, ExplicitPathsOptions explicitPathsOptions = null, CompareOptions compareOptions = null) + /// Additional options to define patch generation behavior. + /// Can be either a if you are only interested in the list of files modified, added, ..., or + /// a if you want the actual patch content for the whole diff and for individual files. + /// A containing the changes between the working directory and the index. + public virtual T Compare(IEnumerable paths = null, bool includeUntracked = false, ExplicitPathsOptions explicitPathsOptions = null, + CompareOptions compareOptions = null) where T : class { - return Compare(includeUntracked ? DiffModifiers.IncludeUntracked : DiffModifiers.None, paths, explicitPathsOptions, compareOptions); + return Compare(includeUntracked ? DiffModifiers.IncludeUntracked : DiffModifiers.None, paths, explicitPathsOptions); } - internal virtual TreeChanges Compare(DiffModifiers diffOptions, IEnumerable paths = null, - ExplicitPathsOptions explicitPathsOptions = null, CompareOptions compareOptions = null) + internal virtual T Compare(DiffModifiers diffOptions, IEnumerable paths = null, + ExplicitPathsOptions explicitPathsOptions = null, CompareOptions compareOptions = null) where T : class { + Func builder; + + if (!ChangesBuilders.TryGetValue(typeof (T), out builder)) + { + throw new LibGit2SharpException(string.Format(CultureInfo.InvariantCulture, + "Unexpected type '{0}' passed to Compare. Supported values are either '{1}' or '{2}'.", typeof (T), + typeof (TreeChanges), typeof (Patch))); + } + var comparer = WorkdirToIndex(repo); if (explicitPathsOptions != null) @@ -203,10 +264,14 @@ internal virtual TreeChanges Compare(DiffModifiers diffOptions, IEnumerable { - DiffListSafeHandle diff = null, diff2 = null; + DiffSafeHandle diff = null, diff2 = null; try { @@ -256,26 +321,103 @@ private static TreeComparisonHandleRetriever IndexToTree(Repository repo) return (oh, nh, o) => Proxy.git_diff_tree_to_index(repo.Handle, repo.Index.Handle, oh, o); } - private TreeChanges BuildTreeChangesFromComparer( - ObjectId oldTreeId, ObjectId newTreeId, TreeComparisonHandleRetriever comparisonHandleRetriever, - DiffModifiers diffOptions, IEnumerable paths = null, ExplicitPathsOptions explicitPathsOptions = null, CompareOptions compareOptions = null) + private DiffSafeHandle BuildDiffList(ObjectId oldTreeId, ObjectId newTreeId, TreeComparisonHandleRetriever comparisonHandleRetriever, + DiffModifiers diffOptions, IEnumerable paths, ExplicitPathsOptions explicitPathsOptions, + CompareOptions compareOptions) { var matchedPaths = new MatchedPathsAggregator(); var filePaths = repo.ToFilePaths(paths); using (GitDiffOptions options = BuildOptions(diffOptions, filePaths, matchedPaths, compareOptions)) - using (DiffListSafeHandle diffList = comparisonHandleRetriever(oldTreeId, newTreeId, options)) { - if (explicitPathsOptions != null) + var diffList = comparisonHandleRetriever(oldTreeId, newTreeId, options); + + try + { + if (explicitPathsOptions != null) + { + DispatchUnmatchedPaths(explicitPathsOptions, filePaths, matchedPaths); + } + } + catch { - DispatchUnmatchedPaths(explicitPathsOptions, filePaths, matchedPaths); + diffList.Dispose(); + throw; } - bool skipPatchBuilding = (compareOptions != null) && compareOptions.SkipPatchBuilding; - return new TreeChanges(diffList, skipPatchBuilding); + DetectRenames(diffList, compareOptions); + + return diffList; } } + private void DetectRenames(DiffSafeHandle diffList, CompareOptions compareOptions) + { + var similarityOptions = (compareOptions == null) ? null : compareOptions.Similarity; + if (similarityOptions == null || + similarityOptions.RenameDetectionMode == RenameDetectionMode.Default) + { + Proxy.git_diff_find_similar(diffList, null); + return; + } + + if (similarityOptions.RenameDetectionMode == RenameDetectionMode.None) + { + return; + } + + var opts = new GitDiffFindOptions + { + RenameThreshold = (ushort)similarityOptions.RenameThreshold, + RenameFromRewriteThreshold = (ushort)similarityOptions.RenameFromRewriteThreshold, + CopyThreshold = (ushort)similarityOptions.CopyThreshold, + BreakRewriteThreshold = (ushort)similarityOptions.BreakRewriteThreshold, + RenameLimit = (UIntPtr)similarityOptions.RenameLimit, + }; + + switch (similarityOptions.RenameDetectionMode) + { + case RenameDetectionMode.Exact: + opts.Flags = GitDiffFindFlags.GIT_DIFF_FIND_EXACT_MATCH_ONLY | + GitDiffFindFlags.GIT_DIFF_FIND_RENAMES | + GitDiffFindFlags.GIT_DIFF_FIND_COPIES | + GitDiffFindFlags.GIT_DIFF_FIND_COPIES_FROM_UNMODIFIED; + break; + case RenameDetectionMode.Renames: + opts.Flags = GitDiffFindFlags.GIT_DIFF_FIND_RENAMES; + break; + case RenameDetectionMode.Copies: + opts.Flags = GitDiffFindFlags.GIT_DIFF_FIND_RENAMES | + GitDiffFindFlags.GIT_DIFF_FIND_COPIES; + break; + case RenameDetectionMode.CopiesHarder: + opts.Flags = GitDiffFindFlags.GIT_DIFF_FIND_RENAMES | + GitDiffFindFlags.GIT_DIFF_FIND_COPIES | + GitDiffFindFlags.GIT_DIFF_FIND_COPIES_FROM_UNMODIFIED; + break; + } + + if (!compareOptions.IncludeUnmodified) + { + opts.Flags |= GitDiffFindFlags.GIT_DIFF_FIND_REMOVE_UNMODIFIED; + } + + switch (similarityOptions.WhitespaceMode) + { + case WhitespaceMode.DontIgnoreWhitespace: + opts.Flags |= GitDiffFindFlags.GIT_DIFF_FIND_DONT_IGNORE_WHITESPACE; + break; + case WhitespaceMode.IgnoreLeadingWhitespace: + opts.Flags |= GitDiffFindFlags.GIT_DIFF_FIND_IGNORE_LEADING_WHITESPACE; + break; + case WhitespaceMode.IgnoreAllWhitespace: + opts.Flags |= GitDiffFindFlags.GIT_DIFF_FIND_IGNORE_WHITESPACE; + break; + } + + Proxy.git_diff_find_similar(diffList, opts); + } + private static void DispatchUnmatchedPaths(ExplicitPathsOptions explicitPathsOptions, IEnumerable filePaths, IEnumerable matchedPaths) diff --git a/LibGit2Sharp/DirectReference.cs b/LibGit2Sharp/DirectReference.cs index ca02b7bc4..1111a2919 100644 --- a/LibGit2Sharp/DirectReference.cs +++ b/LibGit2Sharp/DirectReference.cs @@ -15,7 +15,7 @@ public class DirectReference : Reference protected DirectReference() { } - internal DirectReference(string canonicalName, Repository repo, ObjectId targetId) + internal DirectReference(string canonicalName, IRepository repo, ObjectId targetId) : base(canonicalName, targetId.Sha) { targetBuilder = new Lazy(() => repo.Lookup(targetId)); diff --git a/LibGit2Sharp/FetchOptions.cs b/LibGit2Sharp/FetchOptions.cs new file mode 100644 index 000000000..47f161e8b --- /dev/null +++ b/LibGit2Sharp/FetchOptions.cs @@ -0,0 +1,46 @@ +using LibGit2Sharp.Handlers; + +namespace LibGit2Sharp +{ + /// + /// Collection of parameters controlling Fetch behavior. + /// + public sealed class FetchOptions + { + /// + /// Specifies the tag-following behavior of the fetch operation. + /// + /// If not set, the fetch operation will follow the default behavior for the + /// based on the remote's configuration. + /// + /// If neither this property nor the remote `tagopt` configuration is set, + /// this will default to (i.e. tags that point to objects + /// retrieved during this fetch will be retrieved as well). + /// + public TagFetchMode? TagFetchMode { get; set; } + + /// + /// Delegate that progress updates of the network transfer portion of fetch + /// will be reported through. + /// + public ProgressHandler OnProgress { get; set; } + + /// + /// Delegate that updates of remote tracking branches will be reported through. + /// + public UpdateTipsHandler OnUpdateTips { get; set; } + + /// + /// Callback method that transfer progress will be reported through. + /// + /// Reports the client's state regarding the received and processed (bytes, objects) from the server. + /// + /// + public TransferProgressHandler OnTransferProgress { get; set; } + + /// + /// Credentials to use for username/password authentication. + /// + public Credentials Credentials { get; set; } + } +} diff --git a/LibGit2Sharp/FileStatus.cs b/LibGit2Sharp/FileStatus.cs index bf6975d0d..08fa73f60 100644 --- a/LibGit2Sharp/FileStatus.cs +++ b/LibGit2Sharp/FileStatus.cs @@ -36,7 +36,7 @@ public enum FileStatus /// /// The renaming of a file has been promoted from the working directory to the Index. A previous version exists in the Head. /// - Renamed = (1 << 3), /* GIT_STATUS_INDEX_RENAMED */ + RenamedInIndex = (1 << 3), /* GIT_STATUS_INDEX_RENAMED */ /// /// A change in type for a file has been promoted from the working directory to the Index. A previous version exists in the Head. @@ -63,6 +63,11 @@ public enum FileStatus /// TypeChanged = (1 << 10), /* GIT_STATUS_WT_TYPECHANGE */ + /// + /// The file has been renamed in the working directory. The previous version at the previous name exists in the Index. + /// + RenamedInWorkDir = (1 << 11), /* GIT_STATUS_WT_RENAMED */ + /// /// The file is but its name and/or path matches an exclude pattern in a gitignore file. /// diff --git a/LibGit2Sharp/FilteringOptions.cs b/LibGit2Sharp/FilteringOptions.cs new file mode 100644 index 000000000..f06d5a2a4 --- /dev/null +++ b/LibGit2Sharp/FilteringOptions.cs @@ -0,0 +1,27 @@ +using LibGit2Sharp.Core; + +namespace LibGit2Sharp +{ + /// + /// Allows callers to specify how blob content filters will be applied. + /// + public sealed class FilteringOptions + { + /// + /// Initializes a new instance of the class. + /// + /// The path that a file would be checked out as + public FilteringOptions(string hintPath) + { + Ensure.ArgumentNotNull(hintPath, "hintPath"); + + this.HintPath = hintPath; + } + + /// + /// The path to "hint" to the filters will be used to apply + /// attributes. + /// + public string HintPath { get; private set; } + } +} diff --git a/LibGit2Sharp/Handlers.cs b/LibGit2Sharp/Handlers.cs index 4d8ceefe4..17e822ab6 100644 --- a/LibGit2Sharp/Handlers.cs +++ b/LibGit2Sharp/Handlers.cs @@ -10,7 +10,8 @@ /// text reported by the server. /// Text can be chunked at arbitrary increments (i.e. can be composed /// of a partial line of text). - public delegate void ProgressHandler(string serverProgressOutput); + /// True to continue, false to cancel. + public delegate bool ProgressHandler(string serverProgressOutput); /// /// Delegate definition to handle UpdateTips callback. @@ -18,20 +19,33 @@ /// Name of the updated reference. /// Old ID of the reference. /// New ID of the reference. - /// Return negative integer to cancel. - public delegate int UpdateTipsHandler(string referenceName, ObjectId oldId, ObjectId newId); + /// True to continue, false to cancel. + public delegate bool UpdateTipsHandler(string referenceName, ObjectId oldId, ObjectId newId); /// - /// Delegate definition to handle Completion callback. + /// Delegate definition for transfer progress callback. /// - public delegate int CompletionHandler(RemoteCompletionType remoteCompletionType); + /// The object containing progress information. + /// True to continue, false to cancel. + public delegate bool TransferProgressHandler(TransferProgress progress); /// - /// Delegate definition for transfer progress callback. + /// Delegate definition for callback reporting push network progress. /// - /// The object containing progress information. - /// Return negative integer to cancel. - public delegate int TransferProgressHandler(TransferProgress progress); + /// The current number of objects sent to server. + /// The total number of objects to send to the server. + /// The number of bytes sent to the server. + /// True to continue, false to cancel. + public delegate bool PushTransferProgressHandler(int current, int total, long bytes); + + /// + /// Delegate definition for callback reporting pack builder progress. + /// + /// The current stage progress is being reported for. + /// The current number of objects processed in this this stage. + /// The total number of objects to process for the current stage. + /// True to continue, false to cancel. + public delegate bool PackBuilderProgressHandler(PackBuilderStage stage, int current, int total); /// /// Delegate definition to handle reporting errors when updating references on the remote. @@ -63,4 +77,20 @@ /// /// The unmatched path. public delegate void UnmatchedPathHandler(string unmatchedPath); + + /// + /// The stages of pack building. + /// + public enum PackBuilderStage + { + /// + /// Counting stage. + /// + Counting, + + /// + /// Deltafying stage. + /// + Deltafying + } } diff --git a/LibGit2Sharp/HistoryDivergence.cs b/LibGit2Sharp/HistoryDivergence.cs new file mode 100644 index 000000000..fa7960719 --- /dev/null +++ b/LibGit2Sharp/HistoryDivergence.cs @@ -0,0 +1,81 @@ +using LibGit2Sharp.Core; +using LibGit2Sharp.Core.Compat; + +namespace LibGit2Sharp +{ + /// + /// Holds information about the potential ancestor + /// and distance from it and two specified s. + /// + public class HistoryDivergence + { + private readonly Lazy commonAncestor; + + /// + /// Needed for mocking purposes. + /// + protected HistoryDivergence() + { } + + internal HistoryDivergence(Repository repo, Commit one, Commit another) + { + commonAncestor = new Lazy(() => repo.Commits.FindCommonAncestor(one, another)); + Tuple div = Proxy.git_graph_ahead_behind(repo.Handle, another, one); + + One = one; + Another = another; + AheadBy = div.Item1; + BehindBy = div.Item2; + } + + /// + /// Gets the being used as a reference. + /// + public virtual Commit One { get; private set; } + + /// + /// Gets the being compared against . + /// + public virtual Commit Another { get; private set; } + + /// + /// Gets the number of commits that are reachable from , + /// but not from . + /// + /// This property will return null when + /// and do not share a common ancestor. + /// + /// + public virtual int? AheadBy { get; private set; } + + /// + /// Gets the number of commits that are reachable from , + /// but not from . + /// + /// This property will return null when + /// and do not share a common ancestor. + /// + /// + public virtual int? BehindBy { get; private set; } + + /// + /// Returns the best possible common ancestor of + /// and or null if none found. + /// + public virtual Commit CommonAncestor + { + get + { + return commonAncestor.Value; + } + } + } + + internal class NullHistoryDivergence : HistoryDivergence + { + public override Commit CommonAncestor + { + get { return null; } + } + } +} diff --git a/LibGit2Sharp/IRepository.cs b/LibGit2Sharp/IRepository.cs index 763b7bbfc..557547e12 100644 --- a/LibGit2Sharp/IRepository.cs +++ b/LibGit2Sharp/IRepository.cs @@ -170,8 +170,17 @@ public interface IRepository : IDisposable /// /// Flavor of reset operation to perform. /// The target commit object. + [Obsolete("This method will be removed in the next release. Please use Reset(ResetMode, Commit) instead.")] void Reset(ResetOptions resetOptions, Commit commit); + /// + /// Sets the current to the specified commit and optionally resets the and + /// the content of the working tree to match. + /// + /// Flavor of reset operation to perform. + /// The target commit object. + void Reset(ResetMode resetMode, Commit commit); + /// /// Replaces entries in the with entries from the specified commit. /// @@ -193,9 +202,27 @@ public interface IRepository : IDisposable /// IEnumerable MergeHeads { get; } + /// + /// Manipulate the currently ignored files. + /// + Ignore Ignore { get; } + /// /// Provides access to network functionality for a repository. /// Network Network { get; } + + /// + /// Lookup and enumerate stashes in the repository. + /// + StashCollection Stashes { get; } + + /// + /// Find where each line of a file originated. + /// + /// Path of the file to blame. + /// Specifies optional parameters; if null, the defaults are used. + /// The blame for the file. + BlameHunkCollection Blame(string path, BlameOptions options = null); } } diff --git a/LibGit2Sharp/Index.cs b/LibGit2Sharp/Index.cs index d077f0e7f..1e7031e60 100644 --- a/LibGit2Sharp/Index.cs +++ b/LibGit2Sharp/Index.cs @@ -5,7 +5,6 @@ using System.Globalization; using System.IO; using System.Linq; -using System.Runtime.InteropServices; using LibGit2Sharp.Core; using LibGit2Sharp.Core.Compat; using LibGit2Sharp.Core.Handles; @@ -81,7 +80,7 @@ public virtual IndexEntry this[string path] Ensure.ArgumentNotNullOrEmptyString(path, "path"); IndexEntrySafeHandle entryHandle = Proxy.git_index_get_bypath(handle, path, 0); - return IndexEntry.BuildFromPtr(repo, entryHandle); + return IndexEntry.BuildFromPtr(entryHandle); } } @@ -90,7 +89,7 @@ private IndexEntry this[int index] get { IndexEntrySafeHandle entryHandle = Proxy.git_index_get_byindex(handle, (UIntPtr)index); - return IndexEntry.BuildFromPtr(repo, entryHandle); + return IndexEntry.BuildFromPtr(entryHandle); } } @@ -156,8 +155,7 @@ public virtual void Stage(IEnumerable paths, ExplicitPathsOptions explic { Ensure.ArgumentNotNull(paths, "paths"); - var compareOptions = new CompareOptions { SkipPatchBuilding = true }; - TreeChanges changes = repo.Diff.Compare(DiffModifiers.IncludeUntracked | DiffModifiers.IncludeIgnored, paths, explicitPathsOptions, compareOptions); + var changes = repo.Diff.Compare(DiffModifiers.IncludeUntracked | DiffModifiers.IncludeIgnored, paths, explicitPathsOptions); foreach (var treeEntryChanges in changes) { @@ -214,8 +212,7 @@ public virtual void Unstage(IEnumerable paths, ExplicitPathsOptions expl if (repo.Info.IsHeadUnborn) { - var compareOptions = new CompareOptions { SkipPatchBuilding = true }; - TreeChanges changes = repo.Diff.Compare(null, DiffTargets.Index, paths, explicitPathsOptions, compareOptions); + var changes = repo.Diff.Compare(null, DiffTargets.Index, paths, explicitPathsOptions); Reset(changes); } @@ -350,8 +347,7 @@ public virtual void Remove(string path, bool removeFromWorkingDirectory = true, public virtual void Remove(IEnumerable paths, bool removeFromWorkingDirectory = true, ExplicitPathsOptions explicitPathsOptions = null) { var pathsList = paths.ToList(); - var compareOptions = new CompareOptions { SkipPatchBuilding = true }; - TreeChanges changes = repo.Diff.Compare(DiffModifiers.IncludeUnmodified | DiffModifiers.IncludeUntracked, pathsList, explicitPathsOptions, compareOptions); + var changes = repo.Diff.Compare(DiffModifiers.IncludeUnmodified | DiffModifiers.IncludeUntracked, pathsList, explicitPathsOptions); var pathsTodelete = pathsList.Where(p => Directory.Exists(Path.Combine(repo.Info.WorkingDirectory, p))).ToList(); @@ -502,10 +498,13 @@ public virtual FileStatus RetrieveStatus(string filePath) /// /// Retrieves the state of all files in the working directory, comparing them against the staging area and the latest commmit. /// + /// If set, the options that control the status investigation. /// A holding the state of all the files. - public virtual RepositoryStatus RetrieveStatus() + public virtual RepositoryStatus RetrieveStatus(StatusOptions options = null) { - return new RepositoryStatus(repo); + ReloadFromDisk(); + + return new RepositoryStatus(repo, options); } internal void Reset(TreeChanges changes) @@ -552,11 +551,16 @@ private void ReplaceIndexEntryWith(TreeEntryChanges treeEntryChanges) { Mode = (uint)treeEntryChanges.OldMode, oid = treeEntryChanges.OldOid.Oid, - Path = FilePathMarshaler.FromManaged(treeEntryChanges.OldPath), + Path = StrictFilePathMarshaler.FromManaged(treeEntryChanges.OldPath), }; Proxy.git_index_add(handle, indexEntry); - Marshal.FreeHGlobal(indexEntry.Path); + EncodingMarshaler.Cleanup(indexEntry.Path); + } + + internal void ReloadFromDisk() + { + Proxy.git_index_read(handle); } private string DebuggerDisplay diff --git a/LibGit2Sharp/IndexEntry.cs b/LibGit2Sharp/IndexEntry.cs index c76a5e51d..3b8e2f330 100644 --- a/LibGit2Sharp/IndexEntry.cs +++ b/LibGit2Sharp/IndexEntry.cs @@ -35,7 +35,7 @@ public class IndexEntry : IEquatable /// public virtual ObjectId Id { get; private set; } - internal static IndexEntry BuildFromPtr(Repository repo, IndexEntrySafeHandle handle) + internal static IndexEntry BuildFromPtr(IndexEntrySafeHandle handle) { if (handle == null || handle.IsZero) { @@ -44,7 +44,7 @@ internal static IndexEntry BuildFromPtr(Repository repo, IndexEntrySafeHandle ha GitIndexEntry entry = handle.MarshalAsGitIndexEntry(); - var path = FilePathMarshaler.FromNative(entry.Path); + FilePath path = LaxFilePathMarshaler.FromNative(entry.Path); return new IndexEntry { diff --git a/LibGit2Sharp/LibGit2Sharp.csproj b/LibGit2Sharp/LibGit2Sharp.csproj index 1ff1ce6fa..07e07b454 100644 --- a/LibGit2Sharp/LibGit2Sharp.csproj +++ b/LibGit2Sharp/LibGit2Sharp.csproj @@ -56,7 +56,11 @@ + + + + @@ -64,7 +68,6 @@ - @@ -72,6 +75,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + @@ -82,6 +109,7 @@ + @@ -103,11 +131,11 @@ + - @@ -117,6 +145,8 @@ + + @@ -137,6 +167,7 @@ + @@ -176,7 +207,7 @@ - + @@ -193,7 +224,7 @@ - + diff --git a/LibGit2Sharp/MatchedPathsAggregator.cs b/LibGit2Sharp/MatchedPathsAggregator.cs index aa9a9f1c5..ecae6f2c7 100644 --- a/LibGit2Sharp/MatchedPathsAggregator.cs +++ b/LibGit2Sharp/MatchedPathsAggregator.cs @@ -19,7 +19,7 @@ internal class MatchedPathsAggregator : IEnumerable internal int OnGitDiffNotify(IntPtr diffListSoFar, IntPtr deltaToAdd, IntPtr matchedPathspec, IntPtr payload) { // Convert null strings into empty strings. - var path = (matchedPathspec != IntPtr.Zero) ? FilePathMarshaler.FromNative(matchedPathspec) : FilePath.Empty; + var path = LaxFilePathMarshaler.FromNative(matchedPathspec) ?? FilePath.Empty; if (matchedPaths.Contains(path)) { diff --git a/LibGit2Sharp/Network.cs b/LibGit2Sharp/Network.cs index c57c28ef9..a94c01e43 100644 --- a/LibGit2Sharp/Network.cs +++ b/LibGit2Sharp/Network.cs @@ -54,6 +54,12 @@ public virtual RemoteCollection Remotes /// /// List references in a repository. + /// + /// When the remote tips are ahead of the local ones, the retrieved + /// s may point to non existing + /// s in the local repository. In that + /// case, will return null. + /// /// /// The to list from. /// The references in the repository. @@ -61,32 +67,70 @@ public virtual IEnumerable ListReferences(Remote remote) { Ensure.ArgumentNotNull(remote, "remote"); - List directReferences = new List(); using (RemoteSafeHandle remoteHandle = Proxy.git_remote_load(repository.Handle, remote.Name, true)) { Proxy.git_remote_connect(remoteHandle, GitDirection.Fetch); + return Proxy.git_remote_ls(repository, remoteHandle); + } + } - NativeMethods.git_headlist_cb cb = (ref GitRemoteHead remoteHead, IntPtr payload) => - { - // The name pointer should never be null - if it is, - // this indicates a bug somewhere (libgit2, server, etc). - if (remoteHead.NamePtr == IntPtr.Zero) - { - Proxy.giterr_set_str(GitErrorCategory.Invalid, "Not expecting null value for reference name."); - return -1; - } + /// + /// List references in a remote repository. + /// + /// When the remote tips are ahead of the local ones, the retrieved + /// s may point to non existing + /// s in the local repository. In that + /// case, will return null. + /// + /// + /// The url to list from. + /// The references in the remote repository. + public virtual IEnumerable ListReferences(string url) + { + Ensure.ArgumentNotNull(url, "url"); - ObjectId oid = remoteHead.Oid; - string name = Utf8Marshaler.FromNative(remoteHead.NamePtr); - directReferences.Add(new DirectReference(name, this.repository, oid)); + using (RemoteSafeHandle remoteHandle = Proxy.git_remote_create_inmemory(repository.Handle, null, url)) + { + Proxy.git_remote_connect(remoteHandle, GitDirection.Fetch); + return Proxy.git_remote_ls(repository, remoteHandle); + } + } - return 0; - }; + static void DoFetch(RemoteSafeHandle remoteHandle, FetchOptions options) + { + if (options == null) + { + options = new FetchOptions(); + } - Proxy.git_remote_ls(remoteHandle, cb); + if (options.TagFetchMode.HasValue) + { + Proxy.git_remote_set_autotag(remoteHandle, options.TagFetchMode.Value); } - return directReferences; + var callbacks = new RemoteCallbacks(options); + GitRemoteCallbacks gitCallbacks = callbacks.GenerateCallbacks(); + + // It is OK to pass the reference to the GitCallbacks directly here because libgit2 makes a copy of + // the data in the git_remote_callbacks structure. If, in the future, libgit2 changes its implementation + // to store a reference to the git_remote_callbacks structure this would introduce a subtle bug + // where the managed layer could move the git_remote_callbacks to a different location in memory, + // but libgit2 would still reference the old address. + // + // Also, if GitRemoteCallbacks were a class instead of a struct, we would need to guard against + // GC occuring in between setting the remote callbacks and actual usage in one of the functions afterwords. + Proxy.git_remote_set_callbacks(remoteHandle, ref gitCallbacks); + + try + { + Proxy.git_remote_connect(remoteHandle, GitDirection.Fetch); + Proxy.git_remote_download(remoteHandle); + Proxy.git_remote_update_tips(remoteHandle); + } + finally + { + Proxy.git_remote_disconnect(remoteHandle); + } } /// @@ -95,73 +139,94 @@ public virtual IEnumerable ListReferences(Remote remote) /// The remote to fetch /// Optional parameter indicating what tags to download. /// Progress callback. Corresponds to libgit2 progress callback. - /// Completion callback. Corresponds to libgit2 completion callback. /// UpdateTips callback. Corresponds to libgit2 update_tips callback. /// Callback method that transfer progress will be reported through. /// Reports the client's state regarding the received and processed (bytes, objects) from the server. /// Credentials to use for username/password authentication. + [Obsolete("This overload will be removed in the next release. Please use Fetch(Remote, FetchOptions) instead.")] public virtual void Fetch( Remote remote, TagFetchMode? tagFetchMode = null, ProgressHandler onProgress = null, - CompletionHandler onCompletion = null, UpdateTipsHandler onUpdateTips = null, TransferProgressHandler onTransferProgress = null, Credentials credentials = null) { - Ensure.ArgumentNotNull(remote, "remote"); + Fetch(remote, new FetchOptions + { + TagFetchMode = tagFetchMode, + OnProgress = onProgress, + OnUpdateTips = onUpdateTips, + OnTransferProgress = onTransferProgress, + Credentials = credentials + }); + } - // We need to keep a reference to the git_cred_acquire_cb callback around - // so it will not be garbage collected before we are done with it. - // Note that we also have a GC.KeepAlive call at the end of the method. - NativeMethods.git_cred_acquire_cb credentialCallback = null; + /// + /// Fetch from the . + /// + /// The remote to fetch + public virtual void Fetch(Remote remote) + { + // This overload is required as long as the obsolete overload exists. + // Otherwise, Fetch(Remote) is ambiguous. + Fetch(remote, (FetchOptions)null); + } + + /// + /// Fetch from the . + /// + /// The remote to fetch + /// controlling fetch behavior + public virtual void Fetch(Remote remote, FetchOptions options = null) + { + Ensure.ArgumentNotNull(remote, "remote"); using (RemoteSafeHandle remoteHandle = Proxy.git_remote_load(repository.Handle, remote.Name, true)) { - var callbacks = new RemoteCallbacks(onProgress, onCompletion, onUpdateTips); - GitRemoteCallbacks gitCallbacks = callbacks.GenerateCallbacks(); + DoFetch(remoteHandle, options); + } + } - if (tagFetchMode.HasValue) - { - Proxy.git_remote_set_autotag(remoteHandle, tagFetchMode.Value); - } + /// + /// Fetch from the , using custom refspecs. + /// + /// The remote to fetch + /// Refspecs to use, replacing the remote's fetch refspecs + /// controlling fetch behavior + public virtual void Fetch(Remote remote, IEnumerable refspecs, FetchOptions options = null) + { + Ensure.ArgumentNotNull(remote, "remote"); + Ensure.ArgumentNotNull(refspecs, "refspecs"); - if (credentials != null) - { - credentialCallback = (out IntPtr cred, IntPtr url, IntPtr username_from_url, uint types, IntPtr payload) => - NativeMethods.git_cred_userpass_plaintext_new(out cred, credentials.Username, credentials.Password); + using (RemoteSafeHandle remoteHandle = Proxy.git_remote_load(repository.Handle, remote.Name, true)) + { + Proxy.git_remote_set_fetch_refspecs(remoteHandle, refspecs); - Proxy.git_remote_set_cred_acquire_cb( - remoteHandle, - credentialCallback, - IntPtr.Zero); - } + DoFetch(remoteHandle, options); + } + } - // It is OK to pass the reference to the GitCallbacks directly here because libgit2 makes a copy of - // the data in the git_remote_callbacks structure. If, in the future, libgit2 changes its implementation - // to store a reference to the git_remote_callbacks structure this would introduce a subtle bug - // where the managed layer could move the git_remote_callbacks to a different location in memory, - // but libgit2 would still reference the old address. - // - // Also, if GitRemoteCallbacks were a class instead of a struct, we would need to guard against - // GC occuring in between setting the remote callbacks and actual usage in one of the functions afterwords. - Proxy.git_remote_set_callbacks(remoteHandle, ref gitCallbacks); + /// + /// Fetch from a url with a set of fetch refspecs + /// + /// The url to fetch from + /// The list of resfpecs to use + /// controlling fetch behavior + public virtual void Fetch( + string url, + IEnumerable refspecs, + FetchOptions options = null) + { + Ensure.ArgumentNotNull(url, "url"); + Ensure.ArgumentNotNull(refspecs, "refspecs"); - try - { - Proxy.git_remote_connect(remoteHandle, GitDirection.Fetch); - Proxy.git_remote_download(remoteHandle, onTransferProgress); - Proxy.git_remote_update_tips(remoteHandle); - } - finally - { - Proxy.git_remote_disconnect(remoteHandle); - } - } + using (RemoteSafeHandle remoteHandle = Proxy.git_remote_create_inmemory(repository.Handle, null, url)) + { + Proxy.git_remote_set_fetch_refspecs(remoteHandle, refspecs); - // To be safe, make sure the credential callback is kept until - // alive until at least this point. - GC.KeepAlive(credentialCallback); + DoFetch(remoteHandle, options); + } } /// @@ -170,21 +235,19 @@ public virtual void Fetch( /// The to push to. /// The source objectish to push. /// The reference to update on the remote. - /// Handler for reporting failed push updates. - /// Credentials to use for user/pass authentication + /// controlling push behavior public virtual void Push( Remote remote, string objectish, string destinationSpec, - PushStatusErrorHandler onPushStatusError, - Credentials credentials = null) + PushOptions pushOptions = null) { Ensure.ArgumentNotNull(remote, "remote"); Ensure.ArgumentNotNull(objectish, "objectish"); Ensure.ArgumentNotNullOrEmptyString(destinationSpec, destinationSpec); Push(remote, string.Format(CultureInfo.InvariantCulture, - "{0}:{1}", objectish, destinationSpec), onPushStatusError, credentials); + "{0}:{1}", objectish, destinationSpec), pushOptions); } /// @@ -192,18 +255,16 @@ public virtual void Push( /// /// The to push to. /// The pushRefSpec to push. - /// Handler for reporting failed push updates. - /// Credentials to use for user/pass authentication + /// controlling push behavior public virtual void Push( Remote remote, string pushRefSpec, - PushStatusErrorHandler onPushStatusError, - Credentials credentials = null) + PushOptions pushOptions = null) { Ensure.ArgumentNotNull(remote, "remote"); Ensure.ArgumentNotNullOrEmptyString(pushRefSpec, "pushRefSpec"); - Push(remote, new string[] { pushRefSpec }, onPushStatusError, credentials); + Push(remote, new string[] { pushRefSpec }, pushOptions); } /// @@ -211,21 +272,22 @@ public virtual void Push( /// /// The to push to. /// The pushRefSpecs to push. - /// Handler for reporting failed push updates. - /// Credentials to use for user/pass authentication + /// controlling push behavior public virtual void Push( Remote remote, IEnumerable pushRefSpecs, - PushStatusErrorHandler onPushStatusError, - Credentials credentials = null) + PushOptions pushOptions = null) { Ensure.ArgumentNotNull(remote, "remote"); Ensure.ArgumentNotNull(pushRefSpecs, "pushRefSpecs"); - // We need to keep a reference to the git_cred_acquire_cb callback around - // so it will not be garbage collected before we are done with it. - // Note that we also have a GC.KeepAlive call at the end of the method. - NativeMethods.git_cred_acquire_cb credentialCallback = null; + // The following local variables are protected from garbage collection + // by a GC.KeepAlive call at the end of the method. Otherwise, + // random crashes during push progress reporting could occur. + PushTransferCallbacks pushTransferCallbacks; + PackbuilderCallbacks packBuilderCallbacks; + NativeMethods.git_push_transfer_progress pushProgress; + NativeMethods.git_packbuilder_progress packBuilderProgress; // Return early if there is nothing to push. if (!pushRefSpecs.Any()) @@ -233,21 +295,19 @@ public virtual void Push( return; } - PushCallbacks pushStatusUpdates = new PushCallbacks(onPushStatusError); + if (pushOptions == null) + { + pushOptions = new PushOptions(); + } + + PushCallbacks pushStatusUpdates = new PushCallbacks(pushOptions.OnPushStatusError); // Load the remote. using (RemoteSafeHandle remoteHandle = Proxy.git_remote_load(repository.Handle, remote.Name, true)) { - if (credentials != null) - { - credentialCallback = (out IntPtr cred, IntPtr url, IntPtr username_from_url, uint types, IntPtr payload) => - NativeMethods.git_cred_userpass_plaintext_new(out cred, credentials.Username, credentials.Password); - - Proxy.git_remote_set_cred_acquire_cb( - remoteHandle, - credentialCallback, - IntPtr.Zero); - } + var callbacks = new RemoteCallbacks(null, null, null, pushOptions.Credentials); + GitRemoteCallbacks gitCallbacks = callbacks.GenerateCallbacks(); + Proxy.git_remote_set_callbacks(remoteHandle, ref gitCallbacks); try { @@ -256,6 +316,21 @@ public virtual void Push( // Perform the actual push. using (PushSafeHandle pushHandle = Proxy.git_push_new(remoteHandle)) { + pushTransferCallbacks = new PushTransferCallbacks(pushOptions.OnPushTransferProgress); + packBuilderCallbacks = new PackbuilderCallbacks(pushOptions.OnPackBuilderProgress); + + pushProgress = pushTransferCallbacks.GenerateCallback(); + packBuilderProgress = packBuilderCallbacks.GenerateCallback(); + + Proxy.git_push_set_callbacks(pushHandle, pushProgress, packBuilderProgress); + + // Set push options. + Proxy.git_push_set_options(pushHandle, + new GitPushOptions() + { + PackbuilderDegreeOfParallelism = pushOptions.PackbuilderDegreeOfParallelism + }); + // Add refspecs. foreach (string pushRefSpec in pushRefSpecs) { @@ -280,9 +355,10 @@ public virtual void Push( } } - // To be safe, make sure the credential callback is kept - // alive until at least this point. - GC.KeepAlive(credentialCallback); + GC.KeepAlive(pushProgress); + GC.KeepAlive(packBuilderProgress); + GC.KeepAlive(pushTransferCallbacks); + GC.KeepAlive(packBuilderCallbacks); } /// @@ -317,8 +393,8 @@ public int Callback(IntPtr referenceNamePtr, IntPtr msgPtr, IntPtr payload) // that there was an error. if (msgPtr != IntPtr.Zero) { - string referenceName = Utf8Marshaler.FromNative(referenceNamePtr); - string msg = Utf8Marshaler.FromNative(msgPtr); + string referenceName = LaxUtf8Marshaler.FromNative(referenceNamePtr); + string msg = LaxUtf8Marshaler.FromNative(msgPtr); onError(new PushStatusError(referenceName, msg)); } diff --git a/LibGit2Sharp/NetworkExtensions.cs b/LibGit2Sharp/NetworkExtensions.cs index 1d4671ae9..6016aafa4 100644 --- a/LibGit2Sharp/NetworkExtensions.cs +++ b/LibGit2Sharp/NetworkExtensions.cs @@ -16,16 +16,14 @@ public static class NetworkExtensions /// /// The being worked with. /// The branch to push. - /// Handler for reporting failed push updates. - /// Credentials to use for user/pass authentication. + /// controlling push behavior /// Throws if either the Remote or the UpstreamBranchCanonicalName is not set. public static void Push( this Network network, Branch branch, - PushStatusErrorHandler onPushStatusError = null, - Credentials credentials = null) + PushOptions pushOptions = null) { - network.Push(new[] { branch }, onPushStatusError, credentials); + network.Push(new[] { branch }, pushOptions); } /// @@ -33,14 +31,12 @@ public static void Push( /// /// The being worked with. /// The branches to push. - /// Handler for reporting failed push updates. - /// Credentials to use for user/pass authentication. + /// controlling push behavior /// Throws if either the Remote or the UpstreamBranchCanonicalName is not set. public static void Push( this Network network, IEnumerable branches, - PushStatusErrorHandler onPushStatusError = null, - Credentials credentials = null) + PushOptions pushOptions = null) { var enumeratedBranches = branches as IList ?? branches.ToList(); @@ -55,72 +51,8 @@ public static void Push( foreach (var branch in enumeratedBranches) { - network.Push(branch.Remote, string.Format("{0}:{1}", branch.CanonicalName, branch.UpstreamBranchCanonicalName), onPushStatusError, credentials); + network.Push(branch.Remote, string.Format("{0}:{1}", branch.CanonicalName, branch.UpstreamBranchCanonicalName), pushOptions); } } - - /// - /// Push the objectish to the destination reference on the . - /// - /// The being worked with. - /// The to push to. - /// The source objectish to push. - /// The reference to update on the remote. - /// Credentials to use for user/pass authentication - /// Results of the push operation. - public static PushResult Push( - this Network network, - Remote remote, - string objectish, - string destinationSpec, - Credentials credentials = null) - { - Ensure.ArgumentNotNull(remote, "remote"); - Ensure.ArgumentNotNull(objectish, "objectish"); - Ensure.ArgumentNotNullOrEmptyString(destinationSpec, "destinationSpec"); - - return network.Push(remote, string.Format(CultureInfo.InvariantCulture, - "{0}:{1}", objectish, destinationSpec), credentials); - } - - /// - /// Push specified reference to the . - /// - /// The being worked with. - /// The to push to. - /// The pushRefSpec to push. - /// Credentials to use for user/pass authentication - /// Results of the push operation. - public static PushResult Push(this Network network, Remote remote, string pushRefSpec, Credentials credentials = null) - { - Ensure.ArgumentNotNull(remote, "remote"); - Ensure.ArgumentNotNullOrEmptyString(pushRefSpec, "pushRefSpec"); - - return network.Push(remote, new[] { pushRefSpec }, credentials); - } - - /// - /// Push specified references to the . - /// - /// The being worked with. - /// The to push to. - /// The pushRefSpecs to push. - /// Credentials to use for user/pass authentication - /// Results of the push operation. - public static PushResult Push(this Network network, Remote remote, IEnumerable pushRefSpecs, Credentials credentials = null) - { - Ensure.ArgumentNotNull(remote, "remote"); - Ensure.ArgumentNotNull(pushRefSpecs, "pushRefSpecs"); - - var failedRemoteUpdates = new List(); - - network.Push( - remote, - pushRefSpecs, - failedRemoteUpdates.Add, - credentials); - - return new PushResult(failedRemoteUpdates); - } } } diff --git a/LibGit2Sharp/NoteCollection.cs b/LibGit2Sharp/NoteCollection.cs index 2cda44f10..0541d23d4 100644 --- a/LibGit2Sharp/NoteCollection.cs +++ b/LibGit2Sharp/NoteCollection.cs @@ -16,7 +16,7 @@ namespace LibGit2Sharp [DebuggerDisplay("{DebuggerDisplay,nq}")] public class NoteCollection : IEnumerable { - private readonly Repository repo; + internal readonly Repository repo; private readonly Lazy defaultNamespace; /// diff --git a/LibGit2Sharp/NoteCollectionExtensions.cs b/LibGit2Sharp/NoteCollectionExtensions.cs new file mode 100644 index 000000000..3a2f78e22 --- /dev/null +++ b/LibGit2Sharp/NoteCollectionExtensions.cs @@ -0,0 +1,40 @@ +using System; + +namespace LibGit2Sharp +{ + /// + /// Provides helper overloads to a . + /// + public static class NoteCollectionExtensions + { + /// + /// Creates or updates a on the specified object, and for the given namespace. + /// Both the Author and Committer will be guessed from the Git configuration. An exception will be raised if no configuration is reachable. + /// + /// The + /// The target , for which the note will be created. + /// The note message. + /// The namespace on which the note will be created. It can be either a canonical namespace or an abbreviated namespace ('refs/notes/myNamespace' or just 'myNamespace'). + /// The note which was just saved. + public static Note Add(this NoteCollection collection, ObjectId targetId, string message, string @namespace) + { + Signature author = collection.repo.Config.BuildSignature(DateTimeOffset.Now, true); + + return collection.Add(targetId, message, author, author, @namespace); + } + + /// + /// Deletes the note on the specified object, and for the given namespace. + /// Both the Author and Committer will be guessed from the Git configuration. An exception will be raised if no configuration is reachable. + /// + /// The + /// The target , for which the note will be created. + /// The namespace on which the note will be removed. It can be either a canonical namespace or an abbreviated namespace ('refs/notes/myNamespace' or just 'myNamespace'). + public static void Remove(this NoteCollection collection, ObjectId targetId, string @namespace) + { + Signature author = collection.repo.Config.BuildSignature(DateTimeOffset.Now, true); + + collection.Remove(targetId, author, author, @namespace); + } + } +} diff --git a/LibGit2Sharp/ObjectDatabase.cs b/LibGit2Sharp/ObjectDatabase.cs index b53f1fdeb..1fe0a890b 100644 --- a/LibGit2Sharp/ObjectDatabase.cs +++ b/LibGit2Sharp/ObjectDatabase.cs @@ -7,6 +7,7 @@ using System.Runtime.InteropServices; using LibGit2Sharp.Core; using LibGit2Sharp.Core.Handles; +using LibGit2Sharp.Handlers; namespace LibGit2Sharp { @@ -198,6 +199,7 @@ public virtual Commit CreateCommit(string message, Signature author, Signature c internal Commit CreateCommit(string message, Signature author, Signature committer, Tree tree, IEnumerable parents, string referenceName) { Ensure.ArgumentNotNull(message, "message"); + Ensure.ArgumentDoesNotContainZeroByte(message, "message"); Ensure.ArgumentNotNull(author, "author"); Ensure.ArgumentNotNull(committer, "committer"); Ensure.ArgumentNotNull(tree, "tree"); @@ -221,11 +223,59 @@ internal Commit CreateCommit(string message, Signature author, Signature committ /// The created . public virtual TagAnnotation CreateTagAnnotation(string name, GitObject target, Signature tagger, string message) { + Ensure.ArgumentNotNullOrEmptyString(name, "name"); + Ensure.ArgumentNotNull(message, "message"); + Ensure.ArgumentNotNull(target, "target"); + Ensure.ArgumentNotNull(tagger, "tagger"); + Ensure.ArgumentDoesNotContainZeroByte(name, "name"); + Ensure.ArgumentDoesNotContainZeroByte(message, "message"); + string prettifiedMessage = Proxy.git_message_prettify(message); ObjectId tagId = Proxy.git_tag_annotation_create(repo.Handle, name, target, tagger, prettifiedMessage); return repo.Lookup(tagId); } + + /// + /// Archive the given commit. + /// + /// The commit. + /// The archiver to use. + public virtual void Archive(Commit commit, ArchiverBase archiver) + { + Ensure.ArgumentNotNull(commit, "commit"); + Ensure.ArgumentNotNull(archiver, "archiver"); + + archiver.OrchestrateArchiving(commit.Tree, commit.Id, commit.Committer.When); + } + + /// + /// Archive the given tree. + /// + /// The tree. + /// The archiver to use. + public virtual void Archive(Tree tree, ArchiverBase archiver) + { + Ensure.ArgumentNotNull(tree, "tree"); + Ensure.ArgumentNotNull(archiver, "archiver"); + + archiver.OrchestrateArchiving(tree, null, DateTimeOffset.UtcNow); + } + + /// + /// Returns the merge base (best common ancestor) of the given commits + /// and the distance between each of these commits and this base. + /// + /// The being used as a reference. + /// The being compared against . + /// A instance of . + public virtual HistoryDivergence CalculateHistoryDivergence(Commit one, Commit another) + { + Ensure.ArgumentNotNull(one, "one"); + Ensure.ArgumentNotNull(another, "another"); + + return new HistoryDivergence(repo, one, another); + } } } diff --git a/LibGit2Sharp/ObjectDatabaseExtensions.cs b/LibGit2Sharp/ObjectDatabaseExtensions.cs new file mode 100644 index 000000000..1ed70be6c --- /dev/null +++ b/LibGit2Sharp/ObjectDatabaseExtensions.cs @@ -0,0 +1,40 @@ +using System.IO; + +namespace LibGit2Sharp +{ + /// + /// Provides helper overloads to a . + /// + public static class ObjectDatabaseExtensions + { + /// + /// Create a TAR archive of the given tree. + /// + /// The object database. + /// The tree. + /// The archive path. + public static void Archive(this ObjectDatabase odb, Tree tree, string archivePath) + { + using (var output = new FileStream(archivePath, FileMode.Create)) + using (var archiver = new TarArchiver(output)) + { + odb.Archive(tree, archiver); + } + } + + /// + /// Create a TAR archive of the given commit. + /// + /// The object database. + /// commit. + /// The archive path. + public static void Archive(this ObjectDatabase odb, Commit commit, string archivePath) + { + using (var output = new FileStream(archivePath, FileMode.Create)) + using (var archiver = new TarArchiver(output)) + { + odb.Archive(commit, archiver); + } + } + } +} diff --git a/LibGit2Sharp/ObjectId.cs b/LibGit2Sharp/ObjectId.cs index 3818347ec..00454347a 100644 --- a/LibGit2Sharp/ObjectId.cs +++ b/LibGit2Sharp/ObjectId.cs @@ -37,8 +37,13 @@ public sealed class ObjectId : IEquatable /// The oid. internal ObjectId(GitOid oid) { + if (oid.Id == null || oid.Id.Length != rawSize) + { + throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, "A non null array of {0} bytes is expected.", rawSize), "oid"); + } + this.oid = oid; - sha = ToString(oid.Id); + sha = ToString(oid.Id, oid.Id.Length * 2); } /// @@ -226,18 +231,13 @@ private static byte[] BuildReverseHexDigits() return bytes; } - private static string ToString(byte[] id) + internal static string ToString(byte[] id, int lengthInNibbles) { - if (id == null || id.Length != rawSize) - { - throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, "A non null array of {0} bytes is expected.", rawSize), "id"); - } - // Inspired from http://stackoverflow.com/questions/623104/c-byte-to-hex-string/3974535#3974535 - var c = new char[HexSize]; + var c = new char[lengthInNibbles]; - for (int i = 0; i < HexSize; i++) + for (int i = 0; i < (lengthInNibbles & -2); i++) { int index0 = i >> 1; var b = ((byte)(id[index0] >> 4)); @@ -247,6 +247,13 @@ private static string ToString(byte[] id) c[i] = hexDigits[b]; } + if ((lengthInNibbles & 1) == 1) + { + int index0 = lengthInNibbles >> 1; + var b = ((byte)(id[index0] >> 4)); + c[lengthInNibbles - 1] = hexDigits[b]; + } + return new string(c); } @@ -305,6 +312,7 @@ private static bool LooksValid(string objectId, bool throwIfInvalid) /// The byte array to compare the against. /// The number of nibbles from /// + [Obsolete("This method will be removed in the next release. Please use one of the StartsWith(string) overload instead.")] public bool StartsWith(byte[] rawId, int len) { Ensure.ArgumentNotNull(rawId, "rawId"); @@ -344,5 +352,21 @@ public bool StartsWith(byte[] rawId, int len) return match; } + + /// + /// Determine whether matches the hexified + /// representation of the first nibbles of this instance. + /// + /// Comparison is made in a case insensitive-manner. + /// + /// + /// True if this instance starts with , + /// false otherwise. + public bool StartsWith(string shortSha) + { + Ensure.ArgumentNotNullOrEmptyString(shortSha, "shortSha"); + + return Sha.StartsWith(shortSha, StringComparison.OrdinalIgnoreCase); + } } } diff --git a/LibGit2Sharp/OdbBackend.cs b/LibGit2Sharp/OdbBackend.cs index 03e9f3a5d..1862d26b9 100644 --- a/LibGit2Sharp/OdbBackend.cs +++ b/LibGit2Sharp/OdbBackend.cs @@ -62,9 +62,8 @@ public abstract int Read( /// Requests that this backend read an object. The object ID may not be complete (may be a prefix). /// public abstract int ReadPrefix( - byte[] shortOid, - int prefixLen, - out byte[] oid, + string shortSha, + out ObjectId oid, out Stream data, out ObjectType objectType); @@ -298,19 +297,12 @@ private unsafe static int ReadPrefix( try { - // The length of short_oid is described in characters (40 per full ID) vs. bytes (20) - // which is what we care about. - var oidLen = (int)len; + var shortSha = ObjectId.ToString(short_oid.Id, (int) len); - // Ensure we allocate enough space to cope with odd-sized prefix - int arraySize = (oidLen + 1) >> 1; - var shortOidArray = new byte[arraySize]; - Array.Copy(short_oid.Id, shortOidArray, arraySize); - - byte[] oid; + ObjectId oid; ObjectType objectType; - int toReturn = odbBackend.ReadPrefix(shortOidArray, oidLen, out oid, out dataStream, out objectType); + int toReturn = odbBackend.ReadPrefix(shortSha, out oid, out dataStream, out objectType); if (toReturn != 0) { @@ -325,7 +317,7 @@ private unsafe static int ReadPrefix( return (int)GitErrorCode.Error; } - out_oid.Id = oid; + out_oid.Id = oid.RawId; len_p = new UIntPtr((ulong)memoryStream.Capacity); type_p = objectType.ToGitObjectType(); diff --git a/LibGit2Sharp/OrphanedHeadException.cs b/LibGit2Sharp/OrphanedHeadException.cs deleted file mode 100644 index a314d6df7..000000000 --- a/LibGit2Sharp/OrphanedHeadException.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; -using System.Runtime.Serialization; - -namespace LibGit2Sharp -{ - /// - /// The exception that is thrown when a operation requiring an existing - /// branch is performed against an unborn branch. - /// - [Serializable] - [Obsolete("This type will be removed in the next release. Please use UnbornBranchException instead.")] - public class OrphanedHeadException : UnbornBranchException - { - /// - /// Initializes a new instance of the class. - /// - public OrphanedHeadException() - { - } - - /// - /// Initializes a new instance of the class with a specified error message. - /// - /// A message that describes the error. - public OrphanedHeadException(string message) - : base(message) - { - } - - /// - /// Initializes a new instance of the class with a specified error message and a reference to the inner exception that is the cause of this exception. - /// - /// The error message that explains the reason for the exception. - /// The exception that is the cause of the current exception. If the parameter is not a null reference, the current exception is raised in a catch block that handles the inner exception. - public OrphanedHeadException(string message, Exception innerException) - : base(message, innerException) - { - } - - /// - /// Initializes a new instance of the class with a serialized data. - /// - /// The that holds the serialized object data about the exception being thrown. - /// The that contains contextual information about the source or destination. - protected OrphanedHeadException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - } - } -} diff --git a/LibGit2Sharp/Patch.cs b/LibGit2Sharp/Patch.cs new file mode 100644 index 000000000..9f227275c --- /dev/null +++ b/LibGit2Sharp/Patch.cs @@ -0,0 +1,175 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.Text; +using LibGit2Sharp.Core; +using LibGit2Sharp.Core.Handles; + +namespace LibGit2Sharp +{ + /// + /// Holds the patch between two trees. + /// The individual patches for each file can be accessed through the indexer of this class. + /// Building a patch is an expensive operation. If you only need to know which files have been added, + /// deleted, modified, ..., then consider using a simpler . + /// + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public class Patch : IEnumerable + { + private readonly StringBuilder fullPatchBuilder = new StringBuilder(); + + private readonly IDictionary changes = new Dictionary(); + private int linesAdded; + private int linesDeleted; + + /// + /// Needed for mocking purposes. + /// + protected Patch() + { } + + internal Patch(DiffSafeHandle diff) + { + Proxy.git_diff_foreach(diff, FileCallback, null, null); + + Proxy.git_diff_print(diff, PrintCallBack); + } + + private int FileCallback(GitDiffDelta delta, float progress, IntPtr payload) + { + AddFileChange(delta); + return 0; + } + + private void AddFileChange(GitDiffDelta delta) + { + var newFilePath = LaxFilePathMarshaler.FromNative(delta.NewFile.Path); + + changes.Add(newFilePath, new ContentChanges(delta.IsBinary())); + } + + private int PrintCallBack(GitDiffDelta delta, GitDiffHunk hunk, GitDiffLine line, IntPtr payload) + { + string patchPart = LaxUtf8Marshaler.FromNative(line.content, (int)line.contentLen); + var filePath = LaxFilePathMarshaler.FromNative(delta.NewFile.Path); + + ContentChanges currentChange = this[filePath]; + string prefix = string.Empty; + + switch (line.lineOrigin) + { + case GitDiffLineOrigin.GIT_DIFF_LINE_CONTEXT: + prefix = " "; + break; + + case GitDiffLineOrigin.GIT_DIFF_LINE_ADDITION: + linesAdded++; + currentChange.LinesAdded++; + prefix = "+"; + break; + + case GitDiffLineOrigin.GIT_DIFF_LINE_DELETION: + linesDeleted++; + currentChange.LinesDeleted++; + prefix = "-"; + break; + } + + string formatedOutput = string.Concat(prefix, patchPart); + + fullPatchBuilder.Append(formatedOutput); + this[filePath].AppendToPatch(formatedOutput); + + return 0; + } + + #region IEnumerable Members + + /// + /// Returns an enumerator that iterates through the collection. + /// + /// An object that can be used to iterate through the collection. + public virtual IEnumerator GetEnumerator() + { + return changes.Values.GetEnumerator(); + } + + /// + /// Returns an enumerator that iterates through the collection. + /// + /// An object that can be used to iterate through the collection. + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + #endregion + + /// + /// Gets the corresponding to the specified . + /// + public virtual ContentChanges this[string path] + { + get { return this[(FilePath)path]; } + } + + private ContentChanges this[FilePath path] + { + get + { + ContentChanges contentChanges; + if (changes.TryGetValue(path, out contentChanges)) + { + return contentChanges; + } + + return null; + } + } + + /// + /// The total number of lines added in this diff. + /// + public virtual int LinesAdded + { + get { return linesAdded; } + } + + /// + /// The total number of lines added in this diff. + /// + public virtual int LinesDeleted + { + get { return linesDeleted; } + } + + /// + /// The full patch file of this diff. + /// + public virtual string Content + { + get { return fullPatchBuilder.ToString(); } + } + + /// + /// Implicit operator for string conversion. + /// + /// . + /// The patch content as string. + public static implicit operator string(Patch patch) + { + return patch.fullPatchBuilder.ToString(); + } + + private string DebuggerDisplay + { + get + { + return string.Format(CultureInfo.InvariantCulture, + "+{0} -{1}", linesAdded, linesDeleted); + } + } + } +} diff --git a/LibGit2Sharp/Properties/AssemblyInfo.cs b/LibGit2Sharp/Properties/AssemblyInfo.cs index 1e2626b5d..4fac73f6b 100644 --- a/LibGit2Sharp/Properties/AssemblyInfo.cs +++ b/LibGit2Sharp/Properties/AssemblyInfo.cs @@ -42,5 +42,5 @@ // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("0.14.1")] -[assembly: AssemblyFileVersion("0.14.1")] +[assembly: AssemblyVersion("0.15.0")] +[assembly: AssemblyFileVersion("0.15.0")] diff --git a/LibGit2Sharp/PushOptions.cs b/LibGit2Sharp/PushOptions.cs new file mode 100644 index 000000000..db998ff29 --- /dev/null +++ b/LibGit2Sharp/PushOptions.cs @@ -0,0 +1,43 @@ +using LibGit2Sharp.Handlers; + +namespace LibGit2Sharp +{ + /// + /// Collection of parameters controlling Push behavior. + /// + public sealed class PushOptions + { + /// + /// The to authenticate with during the push. + /// + public Credentials Credentials { get; set; } + + /// + /// If the transport being used to push to the remote requires the creation + /// of a pack file, this controls the number of worker threads used by + /// the packbuilder when creating that pack file to be sent to the remote. + /// The default is 0, which indicates that the packbuilder will auto-detect + /// the number of threads to create. + /// + public int PackbuilderDegreeOfParallelism { get; set; } + + /// + /// Delegate to report errors when updating references on the remote. + /// + public PushStatusErrorHandler OnPushStatusError { get; set; } + + /// + /// Delegate that progress updates of the network transfer portion of push + /// will be reported through. The frequency of progress updates will not + /// be more than once every 0.5 seconds (in general). + /// + public PushTransferProgressHandler OnPushTransferProgress { get; set; } + + /// + /// Delegate that progress updates of the pack building portion of push + /// will be reported through. The frequency of progress updates will not + /// be more than once every 0.5 seconds (in general). + /// + public PackBuilderProgressHandler OnPackBuilderProgress { get; set; } + } +} diff --git a/LibGit2Sharp/RefSpec.cs b/LibGit2Sharp/RefSpec.cs new file mode 100644 index 000000000..5534bd547 --- /dev/null +++ b/LibGit2Sharp/RefSpec.cs @@ -0,0 +1,76 @@ +using System; +using System.Diagnostics; +using System.Globalization; +using LibGit2Sharp.Core; +using LibGit2Sharp.Core.Handles; + +namespace LibGit2Sharp +{ + /// + /// A push or fetch reference specification + /// + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public class RefSpec + { + private RefSpec(string refSpec, RefSpecDirection direction, string source, string destination, bool forceUpdate) + { + Ensure.ArgumentNotNullOrEmptyString(refSpec, "refSpec"); + Ensure.ArgumentNotNull(source, "source"); + Ensure.ArgumentNotNull(destination, "destination"); + + Specification = refSpec; + Direction = direction; + Source = source; + Destination = destination; + ForceUpdate = forceUpdate; + } + + /// + /// Needed for mocking purposes. + /// + protected RefSpec() + { } + + internal static RefSpec BuildFromPtr(GitRefSpecHandle handle) + { + Ensure.ArgumentNotNull(handle, "handle"); + + return new RefSpec(Proxy.git_refspec_string(handle), Proxy.git_refspec_direction(handle), + Proxy.git_refspec_src(handle), Proxy.git_refspec_dst(handle), Proxy.git_refspec_force(handle)); + } + + /// + /// Gets the pattern describing the mapping between remote and local references + /// + public virtual string Specification { get; private set; } + + /// + /// Indicates whether this is intended to be used during a Push or Fetch operation + /// + public virtual RefSpecDirection Direction { get; private set; } + + /// + /// The source reference specifier + /// + public virtual string Source { get; private set; } + + /// + /// The target reference specifier + /// + public virtual string Destination { get; private set; } + + /// + /// Indicates whether the destination will be force-updated if fast-forwarding is not possible + /// + public virtual bool ForceUpdate { get; private set; } + + private string DebuggerDisplay + { + get + { + return string.Format(CultureInfo.InvariantCulture, + "{0}", Specification); + } + } + } +} diff --git a/LibGit2Sharp/RefSpecCollection.cs b/LibGit2Sharp/RefSpecCollection.cs new file mode 100644 index 000000000..81d4b70c8 --- /dev/null +++ b/LibGit2Sharp/RefSpecCollection.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Diagnostics; +using System.Globalization; +using LibGit2Sharp.Core; +using LibGit2Sharp.Core.Handles; +using LibGit2Sharp.Core.Compat; + +namespace LibGit2Sharp +{ + /// + /// The collection of s in a + /// + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public class RefSpecCollection : IEnumerable + { + readonly IList refspecs; + + /// + /// Needed for mocking purposes. + /// + protected RefSpecCollection() + { } + + internal RefSpecCollection(RemoteSafeHandle handle) + { + Ensure.ArgumentNotNull(handle, "handle"); + + refspecs = RetrieveRefSpecs(handle); + } + + static IList RetrieveRefSpecs(RemoteSafeHandle remoteHandle) + { + int count = Proxy.git_remote_refspec_count(remoteHandle); + List refSpecs = new List(); + + for (int i = 0; i < count; i++) + { + using (GitRefSpecHandle handle = Proxy.git_remote_get_refspec(remoteHandle, i)) + { + refSpecs.Add(RefSpec.BuildFromPtr(handle)); + } + } + + return refSpecs; + + } + + /// + /// Returns an enumerator that iterates through the collection. + /// + /// An object that can be used to iterate through the collection. + public virtual IEnumerator GetEnumerator() + { + return refspecs.GetEnumerator(); + } + + /// + /// Returns an enumerator that iterates through the collection. + /// + /// An object that can be used to iterate through the collection. + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + private string DebuggerDisplay + { + get + { + return string.Format(CultureInfo.InvariantCulture, + "Count = {0}", this.Count()); + } + } + } +} diff --git a/LibGit2Sharp/RefSpecDirection.cs b/LibGit2Sharp/RefSpecDirection.cs new file mode 100644 index 000000000..9239b7bd9 --- /dev/null +++ b/LibGit2Sharp/RefSpecDirection.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace LibGit2Sharp +{ + /// + /// Indicates whether a refspec is a push refspec or a fetch refspec + /// + public enum RefSpecDirection + { + /// + /// Indicates that the refspec is a fetch refspec + /// + Fetch, + + /// + /// Indicates that the refspec is a push refspec + /// + Push + } +} diff --git a/LibGit2Sharp/ReferenceCollection.cs b/LibGit2Sharp/ReferenceCollection.cs index edd041a5a..27c24d324 100644 --- a/LibGit2Sharp/ReferenceCollection.cs +++ b/LibGit2Sharp/ReferenceCollection.cs @@ -267,7 +267,7 @@ public virtual IEnumerable FromGlob(string pattern) { Ensure.ArgumentNotNullOrEmptyString(pattern, "pattern"); - return Proxy.git_reference_foreach_glob(repo.Handle, pattern, Utf8Marshaler.FromNative) + return Proxy.git_reference_foreach_glob(repo.Handle, pattern, LaxUtf8Marshaler.FromNative) .Select(n => this[n]); } diff --git a/LibGit2Sharp/ReflogCollection.cs b/LibGit2Sharp/ReflogCollection.cs index 03c178cc4..08f233669 100644 --- a/LibGit2Sharp/ReflogCollection.cs +++ b/LibGit2Sharp/ReflogCollection.cs @@ -36,6 +36,12 @@ internal ReflogCollection(Repository repo, string canonicalName) Ensure.ArgumentNotNullOrEmptyString(canonicalName, "canonicalName"); Ensure.ArgumentNotNull(repo, "repo"); + if (!repo.Refs.IsValidName(canonicalName)) + { + throw new InvalidSpecificationException( + string.Format("The given reference name '{0}' is not valid", canonicalName)); + } + this.repo = repo; this.canonicalName = canonicalName; } @@ -53,8 +59,7 @@ public IEnumerator GetEnumerator() { var entries = new List(); - using (ReferenceSafeHandle reference = Proxy.git_reference_lookup(repo.Handle, canonicalName, true)) - using (ReflogSafeHandle reflog = Proxy.git_reflog_read(reference)) + using (ReflogSafeHandle reflog = Proxy.git_reflog_read(repo.Handle, canonicalName)) { var entriesCount = Proxy.git_reflog_entrycount(reflog); @@ -103,8 +108,7 @@ internal virtual void Append(ObjectId target, string reflogMessage, Signature co return; } - using (ReferenceSafeHandle reference = Proxy.git_reference_lookup(repo.Handle, canonicalName, true)) - using (ReflogSafeHandle reflog = Proxy.git_reflog_read(reference)) + using (ReflogSafeHandle reflog = Proxy.git_reflog_read(repo.Handle, canonicalName)) { string prettifiedMessage = Proxy.git_message_prettify(reflogMessage); Proxy.git_reflog_append(reflog, target, committer, prettifiedMessage); @@ -122,7 +126,7 @@ internal virtual void Append(ObjectId target, string reflogMessage, Signature co /// the message associated with the new . internal void Append(ObjectId target, string reflogMessage) { - Signature author = repo.Config.BuildSignatureFromGlobalConfiguration(DateTimeOffset.Now, false); + Signature author = repo.Config.BuildSignature(DateTimeOffset.Now, false); Append(target, reflogMessage, author); } } diff --git a/LibGit2Sharp/Remote.cs b/LibGit2Sharp/Remote.cs index 7feb1357c..4524e77aa 100644 --- a/LibGit2Sharp/Remote.cs +++ b/LibGit2Sharp/Remote.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Linq; using System.Diagnostics; using System.Globalization; using LibGit2Sharp.Core; @@ -15,7 +17,9 @@ public class Remote : IEquatable private static readonly LambdaEqualityHelper equalityHelper = new LambdaEqualityHelper(x => x.Name, x => x.Url); - private readonly Repository repository; + internal readonly Repository repository; + + private readonly RefSpecCollection refSpecs; /// /// Needed for mocking purposes. @@ -23,21 +27,18 @@ public class Remote : IEquatable protected Remote() { } - private Remote(Repository repository, string name, string url, TagFetchMode tagFetchMode) + private Remote(RemoteSafeHandle handle, Repository repository) { this.repository = repository; - Name = name; - Url = url; - TagFetchMode = tagFetchMode; + Name = Proxy.git_remote_name(handle); + Url = Proxy.git_remote_url(handle); + TagFetchMode = Proxy.git_remote_autotag(handle); + refSpecs = new RefSpecCollection(handle); } internal static Remote BuildFromPtr(RemoteSafeHandle handle, Repository repo) { - string name = Proxy.git_remote_name(handle); - string url = Proxy.git_remote_url(handle); - TagFetchMode tagFetchMode = Proxy.git_remote_autotag(handle); - - var remote = new Remote(repo, name, url, tagFetchMode); + var remote = new Remote(handle, repo); return remote; } @@ -57,6 +58,29 @@ internal static Remote BuildFromPtr(RemoteSafeHandle handle, Repository repo) /// public virtual TagFetchMode TagFetchMode { get; private set; } + /// + /// Gets the list of s defined for this + /// + public virtual IEnumerable RefSpecs { get { return refSpecs; } } + + /// + /// Gets the list of s defined for this + /// that are intended to be used during a Fetch operation + /// + public virtual IEnumerable FetchRefSpecs + { + get { return refSpecs.Where(r => r.Direction == RefSpecDirection.Fetch); } + } + + /// + /// Gets the list of s defined for this + /// that are intended to be used during a Push operation + /// + public virtual IEnumerable PushRefSpecs + { + get { return refSpecs.Where(r => r.Direction == RefSpecDirection.Push); } + } + /// /// Transform a reference to its source reference using the 's default fetchspec. /// diff --git a/LibGit2Sharp/RemoteCallbacks.cs b/LibGit2Sharp/RemoteCallbacks.cs index 3427e634a..65330882c 100644 --- a/LibGit2Sharp/RemoteCallbacks.cs +++ b/LibGit2Sharp/RemoteCallbacks.cs @@ -12,11 +12,25 @@ namespace LibGit2Sharp /// internal class RemoteCallbacks { - internal RemoteCallbacks(ProgressHandler onProgress = null, CompletionHandler onCompletion = null, UpdateTipsHandler onUpdateTips = null) + internal RemoteCallbacks( + ProgressHandler onProgress = null, + TransferProgressHandler onDownloadProgress = null, + UpdateTipsHandler onUpdateTips = null, + Credentials credentials = null) { Progress = onProgress; - Completion = onCompletion; + DownloadTransferProgress = onDownloadProgress; UpdateTips = onUpdateTips; + Credentials = credentials; + } + + internal RemoteCallbacks(FetchOptions fetchOptions) + { + Ensure.ArgumentNotNull(fetchOptions, "fetchOptions"); + Progress = fetchOptions.OnProgress; + DownloadTransferProgress = fetchOptions.OnTransferProgress; + UpdateTips = fetchOptions.OnUpdateTips; + Credentials = fetchOptions.Credentials; } #region Delegates @@ -32,12 +46,18 @@ internal RemoteCallbacks(ProgressHandler onProgress = null, CompletionHandler on private readonly UpdateTipsHandler UpdateTips; /// - /// Completion callback. Corresponds to libgit2 Completion callback. + /// Managed delegate to be called in response to a git_transfer_progress_callback callback from libgit2. + /// This will in turn call the user provided delegate. /// - private readonly CompletionHandler Completion; + private readonly TransferProgressHandler DownloadTransferProgress; #endregion + /// + /// The credentials to use for the credential callback. + /// + Credentials Credentials; + internal GitRemoteCallbacks GenerateCallbacks() { var callbacks = new GitRemoteCallbacks {version = 1}; @@ -52,9 +72,14 @@ internal GitRemoteCallbacks GenerateCallbacks() callbacks.update_tips = GitUpdateTipsHandler; } - if (Completion != null) + if (Credentials != null) { - callbacks.completion = GitCompletionHandler; + callbacks.acquire_credentials = GitCredentialHandler; + } + + if (DownloadTransferProgress != null) + { + callbacks.download_progress = GitDownloadTransferProgressHandler; } return callbacks; @@ -70,15 +95,20 @@ internal GitRemoteCallbacks GenerateCallbacks() /// IntPtr to string from libgit2 /// length of string /// IntPtr to optional payload passed back to the callback. - private void GitProgressHandler(IntPtr str, int len, IntPtr data) + /// 0 on success; a negative value to abort the process. + private int GitProgressHandler(IntPtr str, int len, IntPtr data) { ProgressHandler onProgress = Progress; + bool shouldContinue = true; + if (onProgress != null) { - string message = Utf8Marshaler.FromNative(str, len); - onProgress(message); + string message = LaxUtf8Marshaler.FromNative(str, len); + shouldContinue = onProgress(message); } + + return Proxy.ConvertResultToCancelFlag(shouldContinue); } /// @@ -94,36 +124,38 @@ private void GitProgressHandler(IntPtr str, int len, IntPtr data) private int GitUpdateTipsHandler(IntPtr str, ref GitOid oldId, ref GitOid newId, IntPtr data) { UpdateTipsHandler onUpdateTips = UpdateTips; - int result = 0; + bool shouldContinue = true; if (onUpdateTips != null) { - string refName = Utf8Marshaler.FromNative(str); - result = onUpdateTips(refName, oldId, newId); + string refName = LaxUtf8Marshaler.FromNative(str); + shouldContinue = onUpdateTips(refName, oldId, newId); } - return result; + return Proxy.ConvertResultToCancelFlag(shouldContinue); } /// - /// Handler for libgit2 completion callback. Converts values - /// received from libgit2 callback to more suitable types - /// and calls delegate provided by LibGit2Sharp consumer. + /// The delegate with the signature that matches the native git_transfer_progress_callback function's signature. /// - /// Which operation completed. - /// IntPtr to optional payload passed back to the callback. - /// 0 on success; a negative value to abort the process. - private int GitCompletionHandler(RemoteCompletionType remoteCompletionType, IntPtr data) + /// structure containing progress information. + /// Payload data. + /// the result of the wrapped + private int GitDownloadTransferProgressHandler(ref GitTransferProgress progress, IntPtr payload) { - CompletionHandler completion = Completion; - int result = 0; + bool shouldContinue = true; - if (completion != null) + if (DownloadTransferProgress != null) { - result = completion(remoteCompletionType); + shouldContinue = DownloadTransferProgress(new TransferProgress(progress)); } - return result; + return Proxy.ConvertResultToCancelFlag(shouldContinue); + } + + private int GitCredentialHandler(out IntPtr cred, IntPtr url, IntPtr username_from_url, uint types, IntPtr payload) + { + return NativeMethods.git_cred_userpass_plaintext_new(out cred, Credentials.Username, Credentials.Password); } #endregion diff --git a/LibGit2Sharp/RemoteUpdater.cs b/LibGit2Sharp/RemoteUpdater.cs index c0814e7c8..74b989a3e 100644 --- a/LibGit2Sharp/RemoteUpdater.cs +++ b/LibGit2Sharp/RemoteUpdater.cs @@ -1,5 +1,8 @@ -using LibGit2Sharp.Core; +using System; +using System.Collections.Generic; +using LibGit2Sharp.Core; using LibGit2Sharp.Core.Handles; +using LibGit2Sharp.Core.Compat; namespace LibGit2Sharp { @@ -10,6 +13,8 @@ public class RemoteUpdater { private readonly Repository repo; private readonly Remote remote; + private readonly UpdatingCollection fetchRefSpecs; + private readonly UpdatingCollection pushRefSpecs; /// /// Needed for mocking purposes. @@ -24,6 +29,43 @@ internal RemoteUpdater(Repository repo, Remote remote) this.repo = repo; this.remote = remote; + + fetchRefSpecs = new UpdatingCollection(GetFetchRefSpecs, SetFetchRefSpecs); + pushRefSpecs = new UpdatingCollection(GetPushRefSpecs, SetPushRefSpecs); + } + + private IEnumerable GetFetchRefSpecs() + { + using (RemoteSafeHandle remoteHandle = Proxy.git_remote_load(repo.Handle, remote.Name, true)) + { + return Proxy.git_remote_get_fetch_refspecs(remoteHandle); + } + } + + private void SetFetchRefSpecs(IEnumerable value) + { + using (RemoteSafeHandle remoteHandle = Proxy.git_remote_load(repo.Handle, remote.Name, true)) + { + Proxy.git_remote_set_fetch_refspecs(remoteHandle, value); + Proxy.git_remote_save(remoteHandle); + } + } + + private IEnumerable GetPushRefSpecs() + { + using (RemoteSafeHandle remoteHandle = Proxy.git_remote_load(repo.Handle, remote.Name, true)) + { + return Proxy.git_remote_get_push_refspecs(remoteHandle); + } + } + + private void SetPushRefSpecs(IEnumerable value) + { + using (RemoteSafeHandle remoteHandle = Proxy.git_remote_load(repo.Handle, remote.Name, true)) + { + Proxy.git_remote_set_push_refspecs(remoteHandle, value); + Proxy.git_remote_save(remoteHandle); + } } /// @@ -40,5 +82,106 @@ public virtual TagFetchMode TagFetchMode } } } + + /// + /// Sets the list of s defined for this that are intended to + /// be used during a Fetch operation + /// + /// Changing the list updates the . + public virtual ICollection FetchRefSpecs + { + get { return fetchRefSpecs; } + set { fetchRefSpecs.ReplaceAll(value); } + } + + /// + /// Sets or gets the list of s defined for this that are intended to + /// be used during a Push operation + /// + /// Changing the list updates the . + public virtual ICollection PushRefSpecs + { + get { return pushRefSpecs; } + set { pushRefSpecs.ReplaceAll(value); } + } + + private class UpdatingCollection : ICollection + { + private readonly Lazy> list; + private readonly Action> setter; + + public UpdatingCollection(Func> getter, + Action> setter) + { + list = new Lazy>(() => new List(getter())); + this.setter = setter; + } + + public void Add(T item) + { + list.Value.Add(item); + Save(); + } + + public void Clear() + { + list.Value.Clear(); + Save(); + } + + public bool Contains(T item) + { + return list.Value.Contains(item); + } + + public void CopyTo(T[] array, int arrayIndex) + { + list.Value.CopyTo(array, arrayIndex); + } + + public int Count + { + get { return list.Value.Count; } + } + + public bool IsReadOnly + { + get { return false; } + } + + public bool Remove(T item) + { + if (!list.Value.Remove(item)) + { + return false; + } + + Save(); + return true; + } + + public IEnumerator GetEnumerator() + { + return list.Value.GetEnumerator(); + } + + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + return list.Value.GetEnumerator(); + } + + public void ReplaceAll(IEnumerable newValues) + { + Ensure.ArgumentNotNull(newValues, "newValues"); + list.Value.Clear(); + list.Value.AddRange(newValues); + Save(); + } + + private void Save() + { + setter(list.Value); + } + } } } diff --git a/LibGit2Sharp/RenameDetails.cs b/LibGit2Sharp/RenameDetails.cs new file mode 100644 index 000000000..bfc470589 --- /dev/null +++ b/LibGit2Sharp/RenameDetails.cs @@ -0,0 +1,116 @@ +using System; +using System.Diagnostics; +using LibGit2Sharp.Core; + +namespace LibGit2Sharp +{ + /// + /// Holds the rename details of a particular file. + /// + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public class RenameDetails : IEquatable + { + private readonly string oldFilePath; + private readonly string newFilePath; + private readonly int similarity; + + private static readonly LambdaEqualityHelper equalityHelper = + new LambdaEqualityHelper(x => x.OldFilePath, x => x.NewFilePath, x => x.Similarity); + + /// + /// Needed for mocking purposes. + /// + protected RenameDetails() + { } + + internal RenameDetails(string oldFilePath, string newFilePath, int similarity) + { + this.oldFilePath = oldFilePath; + this.newFilePath = newFilePath; + this.similarity = similarity; + } + + /// + /// Gets the relative filepath to the working directory of the old file (the rename source). + /// + public virtual string OldFilePath + { + get { return oldFilePath; } + } + + /// + /// Gets the relative filepath to the working directory of the new file (the rename target). + /// + public virtual string NewFilePath + { + get { return newFilePath; } + } + + /// + /// Gets the similarity between the old file an the new file (0-100). + /// + public virtual int Similarity + { + get { return similarity; } + } + + /// + /// Determines whether the specified is equal to the current . + /// + /// The to compare with the current . + /// True if the specified is equal to the current ; otherwise, false. + public override bool Equals(object obj) + { + return Equals(obj as RenameDetails); + } + + /// + /// Determines whether the specified is equal to the current . + /// + /// The to compare with the current . + /// True if the specified is equal to the current ; otherwise, false. + public bool Equals(RenameDetails other) + { + return equalityHelper.Equals(this, other); + } + + /// + /// Returns the hash code for this instance. + /// + /// A 32-bit signed integer hash code. + public override int GetHashCode() + { + return equalityHelper.GetHashCode(this); + } + + /// + /// Tests if two are equal. + /// + /// First to compare. + /// Second to compare. + /// True if the two objects are equal; false otherwise. + public static bool operator ==(RenameDetails left, RenameDetails right) + { + return Equals(left, right); + } + + /// + /// Tests if two are different. + /// + /// First to compare. + /// Second to compare. + /// True if the two objects are different; false otherwise. + public static bool operator !=(RenameDetails left, RenameDetails right) + { + return !Equals(left, right); + } + + private string DebuggerDisplay + { + get + { + return string.Format("{0} -> {1} [{2}%]", OldFilePath, NewFilePath, Similarity); + } + } + } +} diff --git a/LibGit2Sharp/Repository.cs b/LibGit2Sharp/Repository.cs index d9399302b..b3df311fc 100644 --- a/LibGit2Sharp/Repository.cs +++ b/LibGit2Sharp/Repository.cs @@ -560,10 +560,12 @@ public static string Clone(string sourceUrl, string workdirPath, { CheckoutCallbacks checkoutCallbacks = CheckoutCallbacks.GenerateCheckoutCallbacks(onCheckoutProgress, null); + var callbacks = new RemoteCallbacks(null, onTransferProgress, null, credentials); + GitRemoteCallbacks gitCallbacks = callbacks.GenerateCallbacks(); + var cloneOpts = new GitCloneOptions { Bare = bare ? 1 : 0, - TransferProgressCallback = TransferCallbacks.GenerateCallback(onTransferProgress), CheckoutOpts = { version = 1, @@ -573,28 +575,29 @@ public static string Clone(string sourceUrl, string workdirPath, ? CheckoutStrategy.GIT_CHECKOUT_SAFE_CREATE : CheckoutStrategy.GIT_CHECKOUT_NONE }, + RemoteCallbacks = gitCallbacks, }; - if (credentials != null) - { - cloneOpts.CredAcquireCallback = - (out IntPtr cred, IntPtr url, IntPtr username_from_url, uint types, IntPtr payload) => - NativeMethods.git_cred_userpass_plaintext_new(out cred, credentials.Username, credentials.Password); - } - FilePath repoPath; using (RepositorySafeHandle repo = Proxy.git_clone(sourceUrl, workdirPath, cloneOpts)) { repoPath = Proxy.git_repository_path(repo); } - // To be safe, make sure the credential callback is kept until - // alive until at least this point. - GC.KeepAlive(cloneOpts.CredAcquireCallback); - return repoPath.Native; } + /// + /// Find where each line of a file originated. + /// + /// Path of the file to blame. + /// Specifies optional parameters; if null, the defaults are used. + /// The blame for the file. + public BlameHunkCollection Blame(string path, BlameOptions options = null) + { + return new BlameHunkCollection(this, Handle, path, options ?? new BlameOptions()); + } + /// /// Checkout the specified , reference or SHA. /// @@ -798,11 +801,23 @@ private void CheckoutTree( /// /// Flavor of reset operation to perform. /// The target commit object. + [Obsolete("This method will be removed in the next release. Please use Reset(ResetMode, Commit) instead.")] public void Reset(ResetOptions resetOptions, Commit commit) + { + Reset((ResetMode)resetOptions, commit); + } + + /// + /// Sets the current to the specified commit and optionally resets the and + /// the content of the working tree to match. + /// + /// Flavor of reset operation to perform. + /// The target commit object. + public void Reset(ResetMode resetMode, Commit commit) { Ensure.ArgumentNotNull(commit, "commit"); - Proxy.git_reset(handle, commit.Id, resetOptions); + Proxy.git_reset(handle, commit.Id, resetMode); Refs.Log(Refs.Head).Append(commit.Id, string.Format("reset: moving to {0}", commit.Sha)); } @@ -849,7 +864,7 @@ public void Reset(Commit commit, IEnumerable paths = null, ExplicitPaths Ensure.ArgumentNotNull(commit, "commit"); - TreeChanges changes = Diff.Compare(commit.Tree, DiffTargets.Index, paths, explicitPathsOptions); + var changes = Diff.Compare(commit.Tree, DiffTargets.Index, paths, explicitPathsOptions); Index.Reset(changes); } @@ -879,7 +894,7 @@ public Commit Commit(string message, Signature author, Signature committer, bool Commit result = ObjectDatabase.CreateCommit(message, author, committer, tree, parents, "HEAD"); - Proxy.git_repository_merge_cleanup(handle); + Proxy.git_repository_state_cleanup(handle); // Insert reflog entry LogCommit(result, amendPreviousCommit, isHeadOrphaned, parents.Count() > 1); diff --git a/LibGit2Sharp/RepositoryExtensions.cs b/LibGit2Sharp/RepositoryExtensions.cs index f00e69719..57688121f 100644 --- a/LibGit2Sharp/RepositoryExtensions.cs +++ b/LibGit2Sharp/RepositoryExtensions.cs @@ -1,7 +1,9 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Globalization; using System.IO; +using System.Linq; using LibGit2Sharp.Core; using LibGit2Sharp.Handlers; @@ -145,13 +147,26 @@ public static Branch CreateBranch(this IRepository repository, string branchName /// The being worked with. /// Flavor of reset operation to perform. /// A revparse spec for the target commit object. + [Obsolete("This method will be removed in the next release. Please use Reset(this IRepository, ResetMode, string) instead.")] public static void Reset(this IRepository repository, ResetOptions resetOptions, string committish = "HEAD") + { + repository.Reset((ResetMode) resetOptions, committish); + } + + /// + /// Sets the current to the specified commit and optionally resets the and + /// the content of the working tree to match. + /// + /// The being worked with. + /// Flavor of reset operation to perform. + /// A revparse spec for the target commit object. + public static void Reset(this IRepository repository, ResetMode resetMode, string committish = "HEAD") { Ensure.ArgumentNotNullOrEmptyString(committish, "committish"); Commit commit = LookUpCommit(repository, committish); - repository.Reset(resetOptions, commit); + repository.Reset(resetMode, commit); } /// @@ -197,7 +212,7 @@ private static Commit LookUpCommit(IRepository repository, string committish) /// The generated . public static Commit Commit(this IRepository repository, string message, bool amendPreviousCommit = false) { - Signature author = repository.Config.BuildSignatureFromGlobalConfiguration(DateTimeOffset.Now, true); + Signature author = repository.Config.BuildSignature(DateTimeOffset.Now, true); return repository.Commit(message, author, amendPreviousCommit); } @@ -215,7 +230,7 @@ public static Commit Commit(this IRepository repository, string message, bool am /// The generated . public static Commit Commit(this IRepository repository, string message, Signature author, bool amendPreviousCommit = false) { - Signature committer = repository.Config.BuildSignatureFromGlobalConfiguration(DateTimeOffset.Now, true); + Signature committer = repository.Config.BuildSignature(DateTimeOffset.Now, true); return repository.Commit(message, author, committer, amendPreviousCommit); } @@ -227,15 +242,14 @@ public static Commit Commit(this IRepository repository, string message, Signatu /// The name of the to fetch from. /// Optional parameter indicating what tags to download. /// Progress callback. Corresponds to libgit2 progress callback. - /// Completion callback. Corresponds to libgit2 completion callback. /// UpdateTips callback. Corresponds to libgit2 update_tips callback. /// Callback method that transfer progress will be reported through. /// Reports the client's state regarding the received and processed (bytes, objects) from the server. /// Credentials to use for username/password authentication. + [Obsolete("This overload will be removed in the next release. Please use Fetch(Remote, FetchOptions) instead.")] public static void Fetch(this IRepository repository, string remoteName, TagFetchMode tagFetchMode = TagFetchMode.Auto, ProgressHandler onProgress = null, - CompletionHandler onCompletion = null, UpdateTipsHandler onUpdateTips = null, TransferProgressHandler onTransferProgress = null, Credentials credentials = null) @@ -244,8 +258,41 @@ public static void Fetch(this IRepository repository, string remoteName, Ensure.ArgumentNotNullOrEmptyString(remoteName, "remoteName"); Remote remote = repository.Network.Remotes.RemoteForName(remoteName, true); - repository.Network.Fetch(remote, tagFetchMode, onProgress, onCompletion, onUpdateTips, - onTransferProgress, credentials); + repository.Network.Fetch(remote, new FetchOptions + { + TagFetchMode = tagFetchMode, + OnProgress = onProgress, + OnUpdateTips = onUpdateTips, + OnTransferProgress = onTransferProgress, + Credentials = credentials + }); + } + + /// + /// Fetch from the specified remote. + /// + /// The being worked with. + /// The name of the to fetch from. + public static void Fetch(this IRepository repository, string remoteName) + { + // This overload is required as long as the obsolete overload exists. + // Otherwise, Fetch(string) is ambiguous. + Fetch(repository, remoteName, (FetchOptions)null); + } + + /// + /// Fetch from the specified remote. + /// + /// The being worked with. + /// The name of the to fetch from. + /// controlling fetch behavior + public static void Fetch(this IRepository repository, string remoteName, FetchOptions options = null) + { + Ensure.ArgumentNotNull(repository, "repository"); + Ensure.ArgumentNotNullOrEmptyString(remoteName, "remoteName"); + + Remote remote = repository.Network.Remotes.RemoteForName(remoteName, true); + repository.Network.Fetch(remote, options); } /// @@ -307,5 +354,114 @@ internal static string BuildRelativePathFrom(this Repository repo, string path) return normalizedPath.Substring(repo.Info.WorkingDirectory.Length); } + + private static ObjectId DereferenceToCommit(Repository repo, string identifier) + { + var options = LookUpOptions.DereferenceResultToCommit; + + if (!AllowOrphanReference(repo, identifier)) + { + options |= LookUpOptions.ThrowWhenNoGitObjectHasBeenFound; + } + + // TODO: Should we check the type? Git-log allows TagAnnotation oid as parameter. But what about Blobs and Trees? + GitObject commit = repo.Lookup(identifier, GitObjectType.Any, options); + + return commit != null ? commit.Id : null; + } + + private static bool AllowOrphanReference(IRepository repo, string identifier) + { + return string.Equals(identifier, "HEAD", StringComparison.Ordinal) + || string.Equals(identifier, repo.Head.CanonicalName, StringComparison.Ordinal); + } + + /// + /// Dereferences the passed identifier to a commit. If the identifier is enumerable, all items are dereferenced. + /// + /// Repository to search + /// Committish to dereference + /// If true, allow thrown exceptions to propagate. If false, exceptions will be swallowed and null returned. + /// A series of commit s which identify commit objects. + internal static IEnumerable Committishes(this Repository repo, object identifier, bool throwIfNotFound = false) + { + ObjectId singleReturnValue = null; + + if (identifier is string) + { + singleReturnValue = DereferenceToCommit(repo, identifier as string); + } + + if (identifier is ObjectId) + { + singleReturnValue = DereferenceToCommit(repo, ((ObjectId) identifier).Sha); + } + + if (identifier is Commit) + { + singleReturnValue = ((Commit) identifier).Id; + } + + if (identifier is TagAnnotation) + { + singleReturnValue = DereferenceToCommit(repo, ((TagAnnotation) identifier).Target.Id.Sha); + } + + if (identifier is Tag) + { + singleReturnValue = DereferenceToCommit(repo, ((Tag) identifier).Target.Id.Sha); + } + + if (identifier is Branch) + { + var branch = (Branch) identifier; + if (branch.Tip != null || !branch.IsCurrentRepositoryHead) + { + Ensure.GitObjectIsNotNull(branch.Tip, branch.CanonicalName); + singleReturnValue = branch.Tip.Id; + } + } + + if (identifier is Reference) + { + singleReturnValue = DereferenceToCommit(repo, ((Reference) identifier).CanonicalName); + } + + if (singleReturnValue != null) + { + yield return singleReturnValue; + yield break; + } + + if (identifier is IEnumerable) + { + foreach (object entry in (IEnumerable)identifier) + { + foreach (ObjectId oid in Committishes(repo, entry)) + { + yield return oid; + } + } + + yield break; + } + + if (throwIfNotFound) + { + throw new LibGit2SharpException(string.Format(CultureInfo.InvariantCulture, "Unexpected kind of identifier '{0}'.", identifier)); + } + yield return null; + } + + /// + /// Dereference the identifier to a commit. If the identifier is enumerable, dereference the first element. + /// + /// The to search + /// Committish to dereference + /// An for a commit object. + internal static ObjectId Committish(this Repository repo, object identifier) + { + return repo.Committishes(identifier, true).First(); + } } } diff --git a/LibGit2Sharp/RepositoryInformation.cs b/LibGit2Sharp/RepositoryInformation.cs index 3fd789221..12751906f 100644 --- a/LibGit2Sharp/RepositoryInformation.cs +++ b/LibGit2Sharp/RepositoryInformation.cs @@ -37,7 +37,7 @@ internal RepositoryInformation(Repository repo, bool isBare) /// /// Gets the normalized path to the working directory. /// - /// Is the repository is bare, null is returned. + /// If the repository is bare, null is returned. /// /// public virtual string WorkingDirectory { get; private set; } @@ -60,16 +60,6 @@ public virtual bool IsHeadDetached get { return Proxy.git_repository_head_detached(repo.Handle); } } - /// - /// Indicates whether the Head points to a reference which doesn't exist. - /// - [Obsolete("This property will be removed in the next release. Please use IsHeadUnborn instead.")] - public virtual bool IsHeadOrphaned - { - get { return IsHeadUnborn; } - } - - /// /// Indicates whether the Head points to a reference which doesn't exist. /// diff --git a/LibGit2Sharp/RepositoryStatus.cs b/LibGit2Sharp/RepositoryStatus.cs index 443978653..74f3c3781 100644 --- a/LibGit2Sharp/RepositoryStatus.cs +++ b/LibGit2Sharp/RepositoryStatus.cs @@ -4,8 +4,10 @@ using System.Diagnostics; using System.Globalization; using System.Linq; +using System.Runtime.InteropServices; using LibGit2Sharp.Core; using LibGit2Sharp.Core.Compat; +using LibGit2Sharp.Core.Handles; namespace LibGit2Sharp { @@ -17,20 +19,22 @@ namespace LibGit2Sharp public class RepositoryStatus : IEnumerable { private readonly ICollection statusEntries; - private readonly List added = new List(); - private readonly List staged = new List(); - private readonly List removed = new List(); - private readonly List missing = new List(); - private readonly List modified = new List(); - private readonly List untracked = new List(); - private readonly List ignored = new List(); + private readonly List added = new List(); + private readonly List staged = new List(); + private readonly List removed = new List(); + private readonly List missing = new List(); + private readonly List modified = new List(); + private readonly List untracked = new List(); + private readonly List ignored = new List(); + private readonly List renamedInIndex = new List(); + private readonly List renamedInWorkDir = new List(); private readonly bool isDirty; - private readonly IDictionary> dispatcher = Build(); + private readonly IDictionary> dispatcher = Build(); - private static IDictionary> Build() + private static IDictionary> Build() { - return new Dictionary> + return new Dictionary> { { FileStatus.Untracked, (rs, s) => rs.untracked.Add(s) }, { FileStatus.Modified, (rs, s) => rs.modified.Add(s) }, @@ -38,7 +42,9 @@ private static IDictionary> Build() { FileStatus.Added, (rs, s) => rs.added.Add(s) }, { FileStatus.Staged, (rs, s) => rs.staged.Add(s) }, { FileStatus.Removed, (rs, s) => rs.removed.Add(s) }, + { FileStatus.RenamedInIndex, (rs, s) => rs.renamedInIndex.Add(s) }, { FileStatus.Ignored, (rs, s) => rs.ignored.Add(s) }, + { FileStatus.RenamedInWorkDir, (rs, s) => rs.renamedInWorkDir.Add(s) } }; } @@ -48,34 +54,118 @@ private static IDictionary> Build() protected RepositoryStatus() { } - internal RepositoryStatus(Repository repo) + internal RepositoryStatus(Repository repo, StatusOptions options) { - statusEntries = Proxy.git_status_foreach(repo.Handle, StateChanged); - isDirty = statusEntries.Any(entry => entry.State != FileStatus.Ignored); + statusEntries = new List(); + + using (GitStatusOptions coreOptions = CreateStatusOptions(options ?? new StatusOptions())) + using (StatusListSafeHandle list = Proxy.git_status_list_new(repo.Handle, coreOptions)) + { + int count = Proxy.git_status_list_entrycount(list); + + for (int i = 0; i < count; i++) + { + StatusEntrySafeHandle e = Proxy.git_status_byindex(list, i); + GitStatusEntry entry = e.MarshalAsGitStatusEntry(); + + GitDiffDelta deltaHeadToIndex = null; + GitDiffDelta deltaIndexToWorkDir = null; + + if (entry.HeadToIndexPtr != IntPtr.Zero) + { + deltaHeadToIndex = (GitDiffDelta)Marshal.PtrToStructure(entry.HeadToIndexPtr, typeof(GitDiffDelta)); + } + if (entry.IndexToWorkDirPtr != IntPtr.Zero) + { + deltaIndexToWorkDir = (GitDiffDelta)Marshal.PtrToStructure(entry.IndexToWorkDirPtr, typeof(GitDiffDelta)); + } + + AddStatusEntryForDelta(entry.Status, deltaHeadToIndex, deltaIndexToWorkDir); + } + + isDirty = statusEntries.Any(entry => entry.State != FileStatus.Ignored); + } + } + + private static GitStatusOptions CreateStatusOptions(StatusOptions options) + { + var coreOptions = new GitStatusOptions + { + Version = 1, + Show = (GitStatusShow)options.Show, + Flags = + GitStatusOptionFlags.IncludeIgnored | + GitStatusOptionFlags.IncludeUntracked | + GitStatusOptionFlags.RecurseUntrackedDirs, + }; + + if (options.DetectRenamesInIndex) + { + coreOptions.Flags |= + GitStatusOptionFlags.RenamesHeadToIndex | + GitStatusOptionFlags.RenamesFromRewrites; + } + + if (options.DetectRenamesInWorkDir) + { + coreOptions.Flags |= + GitStatusOptionFlags.RenamesIndexToWorkDir | + GitStatusOptionFlags.RenamesFromRewrites; + } + + if (options.ExcludeSubmodules) + { + coreOptions.Flags |= + GitStatusOptionFlags.ExcludeSubmodules; + } + + return coreOptions; } - private StatusEntry StateChanged(IntPtr filePathPtr, uint state) + private void AddStatusEntryForDelta(FileStatus gitStatus, GitDiffDelta deltaHeadToIndex, GitDiffDelta deltaIndexToWorkDir) { - var filePath = FilePathMarshaler.FromNative(filePathPtr); - var gitStatus = (FileStatus)state; + RenameDetails headToIndexRenameDetails = null; + RenameDetails indexToWorkDirRenameDetails = null; + + if ((gitStatus & FileStatus.RenamedInIndex) == FileStatus.RenamedInIndex) + { + headToIndexRenameDetails = new RenameDetails( + LaxFilePathMarshaler.FromNative(deltaHeadToIndex.OldFile.Path).Native, + LaxFilePathMarshaler.FromNative(deltaHeadToIndex.NewFile.Path).Native, + (int)deltaHeadToIndex.Similarity); + } + + if ((gitStatus & FileStatus.RenamedInWorkDir) == FileStatus.RenamedInWorkDir) + { + indexToWorkDirRenameDetails = new RenameDetails( + LaxFilePathMarshaler.FromNative(deltaIndexToWorkDir.OldFile.Path).Native, + LaxFilePathMarshaler.FromNative(deltaIndexToWorkDir.NewFile.Path).Native, + (int)deltaIndexToWorkDir.Similarity); + } + + var filePath = (deltaIndexToWorkDir != null) ? + LaxFilePathMarshaler.FromNative(deltaIndexToWorkDir.NewFile.Path).Native : + LaxFilePathMarshaler.FromNative(deltaHeadToIndex.NewFile.Path).Native; + + StatusEntry statusEntry = new StatusEntry(filePath, gitStatus, headToIndexRenameDetails, indexToWorkDirRenameDetails); - foreach (KeyValuePair> kvp in dispatcher) + foreach (KeyValuePair> kvp in dispatcher) { if (!gitStatus.HasFlag(kvp.Key)) { continue; } - kvp.Value(this, filePath.Native); + kvp.Value(this, statusEntry); } - return new StatusEntry(filePath.Native, gitStatus); + statusEntries.Add(statusEntry); } /// - /// Gets the for the specified relative path. + /// Gets the for the specified relative path. /// - public virtual FileStatus this[string path] + public virtual StatusEntry this[string path] { get { @@ -87,10 +177,10 @@ public virtual FileStatus this[string path] if (entries.Count == 0) { - return FileStatus.Nonexistent; + return new StatusEntry(path, FileStatus.Nonexistent); } - return entries.Single().State; + return entries.Single(); } } @@ -115,7 +205,7 @@ IEnumerator IEnumerable.GetEnumerator() /// /// List of files added to the index, which are not in the current commit /// - public virtual IEnumerable Added + public virtual IEnumerable Added { get { return added; } } @@ -123,7 +213,7 @@ public virtual IEnumerable Added /// /// List of files added to the index, which are already in the current commit with different content /// - public virtual IEnumerable Staged + public virtual IEnumerable Staged { get { return staged; } } @@ -131,7 +221,7 @@ public virtual IEnumerable Staged /// /// List of files removed from the index but are existent in the current commit /// - public virtual IEnumerable Removed + public virtual IEnumerable Removed { get { return removed; } } @@ -139,7 +229,7 @@ public virtual IEnumerable Removed /// /// List of files existent in the index but are missing in the working directory /// - public virtual IEnumerable Missing + public virtual IEnumerable Missing { get { return missing; } } @@ -147,7 +237,7 @@ public virtual IEnumerable Missing /// /// List of files with unstaged modifications. A file may be modified and staged at the same time if it has been modified after adding. /// - public virtual IEnumerable Modified + public virtual IEnumerable Modified { get { return modified; } } @@ -155,7 +245,7 @@ public virtual IEnumerable Modified /// /// List of files existing in the working directory but are neither tracked in the index nor in the current commit. /// - public virtual IEnumerable Untracked + public virtual IEnumerable Untracked { get { return untracked; } } @@ -163,11 +253,27 @@ public virtual IEnumerable Untracked /// /// List of files existing in the working directory that are ignored. /// - public virtual IEnumerable Ignored + public virtual IEnumerable Ignored { get { return ignored; } } + /// + /// List of files that were renamed and staged. + /// + public virtual IEnumerable RenamedInIndex + { + get { return renamedInIndex; } + } + + /// + /// List of files that were renamed in the working directory but have not been staged. + /// + public virtual IEnumerable RenamedInWorkDir + { + get { return renamedInWorkDir; } + } + /// /// True if the index or the working directory has been altered since the last commit. False otherwise. /// diff --git a/LibGit2Sharp/ResetMode.cs b/LibGit2Sharp/ResetMode.cs new file mode 100644 index 000000000..f15028918 --- /dev/null +++ b/LibGit2Sharp/ResetMode.cs @@ -0,0 +1,26 @@ +namespace LibGit2Sharp +{ + /// + /// Specifies the kind of operation that should perform. + /// + public enum ResetMode + { + /// + /// Moves the branch pointed to by HEAD to the specified commit object. + /// + Soft = 1, + + /// + /// Moves the branch pointed to by HEAD to the specified commit object and resets the index + /// to the tree recorded by the commit. + /// + Mixed, + + /// + /// Moves the branch pointed to by HEAD to the specified commit object, resets the index + /// to the tree recorded by the commit and updates the working directory to match the content + /// of the index. + /// + Hard, + } +} diff --git a/LibGit2Sharp/ResetOptions.cs b/LibGit2Sharp/ResetOptions.cs index 8ff26eff0..d294adb17 100644 --- a/LibGit2Sharp/ResetOptions.cs +++ b/LibGit2Sharp/ResetOptions.cs @@ -1,8 +1,11 @@ +using System; + namespace LibGit2Sharp { /// /// Specifies the kind of operation that should perform. /// + [Obsolete("This enumeration will be removed in the next release. Please use ResetMode instead.")] public enum ResetOptions { /// diff --git a/LibGit2Sharp/RewriteHistoryOptions.cs b/LibGit2Sharp/RewriteHistoryOptions.cs index ad1d16109..031839c38 100644 --- a/LibGit2Sharp/RewriteHistoryOptions.cs +++ b/LibGit2Sharp/RewriteHistoryOptions.cs @@ -49,5 +49,33 @@ public RewriteHistoryOptions() /// Empty commits should be removed while rewriting. /// public bool PruneEmptyCommits { get; set; } + + /// + /// Action to exectute after rewrite succeeds, + /// but before it is finalized. + /// + /// An exception thrown here will rollback the operation. + /// This is useful to inspect the new state of the repository + /// and throw if you need to adjust and try again. + /// + /// + public Action OnSucceeding { get; set; } + + /// + /// Action to execute if an error occurred during rewrite, + /// before rollback of rewrite progress. + /// Does not fire for exceptions thrown in . + /// + /// This is useful to inspect the state of the repository + /// at the time of the exception for troubleshooting. + /// It is not meant to be used for general error handling; + /// for that use try/catch. + /// + /// + /// An exception thrown here will replace the original exception. + /// You may want to pass the callback exception as an innerException. + /// + /// + public Action OnError { get; set; } } } diff --git a/LibGit2Sharp/Signature.cs b/LibGit2Sharp/Signature.cs index ae3566961..d4a1f3745 100644 --- a/LibGit2Sharp/Signature.cs +++ b/LibGit2Sharp/Signature.cs @@ -22,8 +22,8 @@ internal Signature(IntPtr signaturePtr) var handle = new GitSignature(); Marshal.PtrToStructure(signaturePtr, handle); - name = Utf8Marshaler.FromNative(handle.Name); - email = Utf8Marshaler.FromNative(handle.Email); + name = LaxUtf8Marshaler.FromNative(handle.Name); + email = LaxUtf8Marshaler.FromNative(handle.Email); when = Epoch.ToDateTimeOffset(handle.When.Time, handle.When.Offset); } @@ -35,6 +35,11 @@ internal Signature(IntPtr signaturePtr) /// The when. public Signature(string name, string email, DateTimeOffset when) { + Ensure.ArgumentNotNullOrEmptyString(name, "name"); + Ensure.ArgumentNotNull(email, "email"); + Ensure.ArgumentDoesNotContainZeroByte(name, "name"); + Ensure.ArgumentDoesNotContainZeroByte(email, "email"); + this.name = name; this.email = email; this.when = when; diff --git a/LibGit2Sharp/SimilarityOptions.cs b/LibGit2Sharp/SimilarityOptions.cs new file mode 100644 index 000000000..13d26abf2 --- /dev/null +++ b/LibGit2Sharp/SimilarityOptions.cs @@ -0,0 +1,164 @@ +namespace LibGit2Sharp +{ + /// + /// Represents a mode for handling whitespace while detecting renames and copies. + /// + public enum WhitespaceMode + { + /// + /// Don't consider leading whitespace when comparing files + /// + IgnoreLeadingWhitespace, + + /// + /// Don't consider any whitespace when comparing files + /// + IgnoreAllWhitespace, + + /// + /// Include all whitespace when comparing files + /// + DontIgnoreWhitespace, + } + + /// + /// Represents a mode for detecting renames and copies. + /// + public enum RenameDetectionMode + { + /// + /// Obey the user's `diff.renames` configuration setting + /// + Default, + + /// + /// Attempt no rename or copy detection + /// + None, + + /// + /// Detect exact renames and copies (compare SHA hashes only) + /// + Exact, + + /// + /// Detect fuzzy renames (use similarity metric) + /// + Renames, + + /// + /// Detect renames and copies + /// + Copies, + + /// + /// Detect renames, and include unmodified files when looking for copies + /// + CopiesHarder, + } + + /// + /// Options for handling file similarity + /// + public sealed class SimilarityOptions + { + /// + /// Initializes a new instance of the class. + /// + public SimilarityOptions() + { + RenameDetectionMode = RenameDetectionMode.Default; + WhitespaceMode = WhitespaceMode.IgnoreLeadingWhitespace; + RenameThreshold = 50; + RenameFromRewriteThreshold = 50; + CopyThreshold = 50; + BreakRewriteThreshold = 60; + RenameLimit = 200; + } + + /// + /// Get a instance that does no rename detection + /// + public static SimilarityOptions None + { + get { return new SimilarityOptions {RenameDetectionMode = RenameDetectionMode.None}; } + } + + /// + /// Get a instance that detects renames + /// + public static SimilarityOptions Renames + { + get { return new SimilarityOptions {RenameDetectionMode = RenameDetectionMode.Renames}; } + } + + /// + /// Get a instance that detects exact renames only + /// + public static SimilarityOptions Exact + { + get { return new SimilarityOptions {RenameDetectionMode = RenameDetectionMode.Exact}; } + } + + /// + /// Get a instance that detects renames and copies + /// + public static SimilarityOptions Copies + { + get { return new SimilarityOptions {RenameDetectionMode = RenameDetectionMode.Copies}; } + } + + /// + /// Get a instance that detects renames, and includes unmodified files when detecting copies + /// + public static SimilarityOptions CopiesHarder + { + get { return new SimilarityOptions {RenameDetectionMode = RenameDetectionMode.CopiesHarder}; } + } + + /// + /// Get a instance that obeys the user's `diff.renames` setting + /// + public static SimilarityOptions Default + { + get { return new SimilarityOptions {RenameDetectionMode = RenameDetectionMode.Default}; } + } + + /// + /// The mode for detecting renames and copies + /// + public RenameDetectionMode RenameDetectionMode { get; set; } + + /// + /// The mode for handling whitespace when comparing files + /// + public WhitespaceMode WhitespaceMode { get; set; } + + /// + /// Similarity in order to consider a rename + /// + public int RenameThreshold { get; set; } + + /// + /// Similarity of a modified file in order to be eligible as a rename source + /// + public int RenameFromRewriteThreshold { get; set; } + + /// + /// Similarity to consider a file a copy + /// + public int CopyThreshold { get; set; } + + /// + /// Similarity to split modify into an add/delete pair + /// + public int BreakRewriteThreshold { get; set; } + + /// + /// Maximum similarity sources to examine for a file + /// + public int RenameLimit { get; set; } + + // TODO: custom metric + } +} diff --git a/LibGit2Sharp/StatusEntry.cs b/LibGit2Sharp/StatusEntry.cs index a11d57be2..a049d3164 100644 --- a/LibGit2Sharp/StatusEntry.cs +++ b/LibGit2Sharp/StatusEntry.cs @@ -12,9 +12,11 @@ public class StatusEntry : IEquatable { private readonly string filePath; private readonly FileStatus state; + private readonly RenameDetails headToIndexRenameDetails; + private readonly RenameDetails indexToWorkDirRenameDetails; private static readonly LambdaEqualityHelper equalityHelper = - new LambdaEqualityHelper(x => x.FilePath, x => x.State); + new LambdaEqualityHelper(x => x.FilePath, x => x.State, x => x.HeadToIndexRenameDetails, x => x.IndexToWorkDirRenameDetails); /// /// Needed for mocking purposes. @@ -22,10 +24,12 @@ public class StatusEntry : IEquatable protected StatusEntry() { } - internal StatusEntry(string filePath, FileStatus state) + internal StatusEntry(string filePath, FileStatus state, RenameDetails headToIndexRenameDetails = null, RenameDetails indexToWorkDirRenameDetails = null) { this.filePath = filePath; this.state = state; + this.headToIndexRenameDetails = headToIndexRenameDetails; + this.indexToWorkDirRenameDetails = indexToWorkDirRenameDetails; } /// @@ -37,13 +41,29 @@ public virtual FileStatus State } /// - /// Gets the relative filepath to the working directory of the file. + /// Gets the relative new filepath to the working directory of the file. /// public virtual string FilePath { get { return filePath; } } + /// + /// Gets the rename details from the HEAD to the Index, if this contains + /// + public virtual RenameDetails HeadToIndexRenameDetails + { + get { return headToIndexRenameDetails; } + } + + /// + /// Gets the rename details from the Index to the working directory, if this contains + /// + public virtual RenameDetails IndexToWorkDirRenameDetails + { + get { return indexToWorkDirRenameDetails; } + } + /// /// Determines whether the specified is equal to the current . /// @@ -97,7 +117,19 @@ public override int GetHashCode() private string DebuggerDisplay { - get { return string.Format("{0}: {1}", State, FilePath); } + get + { + if ((State & FileStatus.RenamedInIndex) == FileStatus.RenamedInIndex || + (State & FileStatus.RenamedInWorkDir) == FileStatus.RenamedInWorkDir) + { + string oldFilePath = ((State & FileStatus.RenamedInIndex) == FileStatus.RenamedInIndex) ? + HeadToIndexRenameDetails.OldFilePath : IndexToWorkDirRenameDetails.OldFilePath; + + return string.Format("{0}: {1} -> {2}", State, oldFilePath, FilePath); + } + + return string.Format("{0}: {1}", State, FilePath); + } } } } diff --git a/LibGit2Sharp/StatusOptions.cs b/LibGit2Sharp/StatusOptions.cs new file mode 100644 index 000000000..13963711a --- /dev/null +++ b/LibGit2Sharp/StatusOptions.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace LibGit2Sharp +{ + /// + /// Flags controlling what files are reported by status. + /// + public enum StatusShowOption + { + /// + /// Both the index and working directory are examined for changes + /// + IndexAndWorkDir = 0, + + /// + /// Only the index is examined for changes + /// + IndexOnly = 1, + + /// + /// Only the working directory is examined for changes + /// + WorkDirOnly = 2 + } + + /// + /// Options controlling the status behavior. + /// + public sealed class StatusOptions + { + /// + /// Initializes a new instance of the class. + /// By default, both the index and the working directory will be scanned + /// for status, and renames will be detected from changes staged in the + /// index only. + /// + public StatusOptions() + { + DetectRenamesInIndex = true; + } + + /// + /// Which files should be scanned and returned + /// + public StatusShowOption Show { get; set; } + + /// + /// Examine the staged changes for renames. + /// + public bool DetectRenamesInIndex { get; set; } + + /// + /// Examine unstaged changes in the working directory for renames. + /// + public bool DetectRenamesInWorkDir { get; set; } + + /// + /// Exclude submodules from being scanned for status + /// + public bool ExcludeSubmodules { get; set; } + } +} diff --git a/LibGit2Sharp/SubmoduleCollection.cs b/LibGit2Sharp/SubmoduleCollection.cs index 18649c764..c762c1098 100644 --- a/LibGit2Sharp/SubmoduleCollection.cs +++ b/LibGit2Sharp/SubmoduleCollection.cs @@ -54,7 +54,7 @@ public virtual Submodule this[string name] /// An object that can be used to iterate through the collection. public IEnumerator GetEnumerator() { - return Proxy.git_submodule_foreach(repo.Handle, (h, n) => Utf8Marshaler.FromNative(n)) + return Proxy.git_submodule_foreach(repo.Handle, (h, n) => LaxUtf8Marshaler.FromNative(n)) .Select(n => this[n]) .GetEnumerator(); } diff --git a/LibGit2Sharp/SubmoduleIgnore.cs b/LibGit2Sharp/SubmoduleIgnore.cs index cae886da4..e98407295 100644 --- a/LibGit2Sharp/SubmoduleIgnore.cs +++ b/LibGit2Sharp/SubmoduleIgnore.cs @@ -9,22 +9,26 @@ public enum SubmoduleIgnore /// /// Reset to the last saved ignore rule. /// - Default = -1, + Reset = -1, + /// /// Any change or untracked == dirty /// - None = 0, + None = 1, + /// /// Dirty if tracked files change /// - Untracked = 1, + Untracked = 2, + /// /// Only dirty if HEAD moved /// - Dirty = 2, + Dirty = 3, + /// /// Never dirty /// - All = 3, + All = 4, } } diff --git a/LibGit2Sharp/TarArchiver.cs b/LibGit2Sharp/TarArchiver.cs new file mode 100644 index 000000000..f6fe75203 --- /dev/null +++ b/LibGit2Sharp/TarArchiver.cs @@ -0,0 +1,88 @@ +using System; +using System.IO; +using System.Text; +using LibGit2Sharp.Core; + +namespace LibGit2Sharp +{ + /// + /// Logic for tar archiving (not the actual tar format, but the overal logic related to tar+git) is taken + /// from https://github.com/git/git/blob/master/archive-tar.c. + /// + internal class TarArchiver : ArchiverBase, IDisposable + { + private readonly TarWriter writer; + + public TarArchiver(FileStream output) + { + writer = new TarWriter(output); + } + + #region Overrides of ArchiverBase + + public override void BeforeArchiving(Tree tree, ObjectId oid, DateTimeOffset modificationTime) + { + if (oid == null) + { + return; + } + + // Store the sha in the pax_global_header + using (var stream = new MemoryStream(Encoding.ASCII.GetBytes(string.Format("52 comment={0}\n", oid.Sha)))) + { + writer.Write("pax_global_header", stream, modificationTime, "666".OctalToInt32(), + "0", "0", 'g', "root", "root", "0", "0", oid.Sha, false); + } + } + + protected override void AddTreeEntry(string path, TreeEntry entry, DateTimeOffset modificationTime) + { + switch (entry.Mode) + { + case Mode.GitLink: + case Mode.Directory: + writer.Write(path + "/", null, modificationTime, "775".OctalToInt32(), + "0", "0", '5', "root", "root", "0", "0", entry.TargetId.Sha, false); + break; + case Mode.ExecutableFile: + case Mode.NonExecutableFile: + case Mode.NonExecutableGroupWritableFile: + var blob = ((Blob) entry.Target); + + WriteStream(path, entry, modificationTime, + () => blob.IsBinary ? blob.GetContentStream() : blob.GetContentStream(new FilteringOptions(path))); + break; + case Mode.SymbolicLink: + using (Stream contentStream = ((Blob)entry.Target).GetContentStream(new FilteringOptions(path))) + { + writer.Write(path, contentStream, modificationTime, "777".OctalToInt32(), + "0", "0", '2', "root", "root", "0", "0", entry.TargetId.Sha, true); + } + break; + default: + throw new InvalidOperationException(string.Format("Unsupported file mode: {0} (sha1: {1}).", entry.Mode, entry.TargetId.Sha)); + } + } + + private void WriteStream(string path, TreeEntry entry, DateTimeOffset modificationTime, Func streamer) + { + using (Stream contentStream = streamer()) + { + writer.Write(path, contentStream, modificationTime, + (entry.Mode == Mode.ExecutableFile) ? "775".OctalToInt32() : "664".OctalToInt32(), + "0", "0", '0', "root", "root", "0", "0", entry.TargetId.Sha, false); + } + } + + #endregion + + #region Implementation of IDisposable + + public void Dispose() + { + writer.Dispose(); + } + + #endregion + } +} diff --git a/LibGit2Sharp/TransferCallbacks.cs b/LibGit2Sharp/TransferCallbacks.cs deleted file mode 100644 index 46497ba6b..000000000 --- a/LibGit2Sharp/TransferCallbacks.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System; -using LibGit2Sharp.Core; -using LibGit2Sharp.Handlers; - -namespace LibGit2Sharp -{ - /// - /// Class to handle the mapping between libgit2 git_transfer_progress_callback function and - /// a corresponding . Generates a delegate that - /// wraps the delegate with a delegate that matches - /// the git_transfer_progress_callback signature. - /// - internal class TransferCallbacks - { - /// - /// Managed delegate to be called in response to a git_transfer_progress_callback callback from libgit2. - /// - private readonly TransferProgressHandler onTransferProgress; - - /// - /// Constructor to set up the native callback given managed delegate. - /// - /// The delegate that the git_transfer_progress_callback will call. - private TransferCallbacks(TransferProgressHandler onTransferProgress) - { - this.onTransferProgress = onTransferProgress; - } - - /// - /// Generates a delegate that matches the native git_transfer_progress_callback function's signature and wraps the delegate. - /// - /// The delegate to call in responde to a the native git_transfer_progress_callback callback. - /// A delegate method with a signature that matches git_transfer_progress_callback. - internal static NativeMethods.git_transfer_progress_callback GenerateCallback(TransferProgressHandler onTransferProgress) - { - if (onTransferProgress == null) - { - return null; - } - - return new TransferCallbacks(onTransferProgress).OnGitTransferProgress; - } - - /// - /// The delegate with the signature that matches the native git_transfer_progress_callback function's signature. - /// - /// structure containing progress information. - /// Payload data. - /// the result of the wrapped - private int OnGitTransferProgress(ref GitTransferProgress progress, IntPtr payload) - { - return onTransferProgress(new TransferProgress(progress)); - } - } -} diff --git a/LibGit2Sharp/TreeChanges.cs b/LibGit2Sharp/TreeChanges.cs index 5f79f59e7..cb3affdaa 100644 --- a/LibGit2Sharp/TreeChanges.cs +++ b/LibGit2Sharp/TreeChanges.cs @@ -4,7 +4,6 @@ using System.Diagnostics; using System.Globalization; using System.Linq; -using System.Text; using LibGit2Sharp.Core; using LibGit2Sharp.Core.Handles; @@ -13,6 +12,7 @@ namespace LibGit2Sharp /// /// Holds the result of a diff between two trees. /// Changes at the granularity of the file can be obtained through the different sub-collections , and . + /// To obtain the actual patch of the diff, use the class when calling Compare.. /// [DebuggerDisplay("{DebuggerDisplay,nq}")] public class TreeChanges : IEnumerable @@ -23,22 +23,22 @@ public class TreeChanges : IEnumerable private readonly List modified = new List(); private readonly List typeChanged = new List(); private readonly List unmodified = new List(); - private int linesAdded; - private int linesDeleted; + private readonly List renamed = new List(); + private readonly List copied = new List(); private readonly IDictionary> fileDispatcher = Build(); - private readonly StringBuilder fullPatchBuilder = new StringBuilder(); - private static IDictionary> Build() { return new Dictionary> { - { ChangeKind.Modified, (de, d) => de.modified.Add(d) }, - { ChangeKind.Deleted, (de, d) => de.deleted.Add(d) }, - { ChangeKind.Added, (de, d) => de.added.Add(d) }, + { ChangeKind.Modified, (de, d) => de.modified.Add(d) }, + { ChangeKind.Deleted, (de, d) => de.deleted.Add(d) }, + { ChangeKind.Added, (de, d) => de.added.Add(d) }, { ChangeKind.TypeChanged, (de, d) => de.typeChanged.Add(d) }, - { ChangeKind.Unmodified, (de, d) => de.unmodified.Add(d) }, + { ChangeKind.Unmodified, (de, d) => de.unmodified.Add(d) }, + { ChangeKind.Renamed, (de, d) => de.renamed.Add(d) }, + { ChangeKind.Copied, (de, d) => de.copied.Add(d) }, }; } @@ -48,23 +48,9 @@ private static IDictionary> Bu protected TreeChanges() { } - internal TreeChanges(DiffListSafeHandle diff, bool skipPatchBuilding = false) + internal TreeChanges(DiffSafeHandle diff) { - Proxy.git_diff_foreach(diff, FileCallback, null, DataCallback); - - if (!skipPatchBuilding) - { - Proxy.git_diff_print_patch(diff, PrintCallBack); - } - } - - private int DataCallback(GitDiffDelta delta, GitDiffRange range, GitDiffLineOrigin lineOrigin, IntPtr content, UIntPtr contentLen, IntPtr payload) - { - var filePath = FilePathMarshaler.FromNative(delta.NewFile.Path); - - AddLineChange(this[filePath], lineOrigin); - - return 0; + Proxy.git_diff_foreach(diff, FileCallback, null, null); } private int FileCallback(GitDiffDelta delta, float progress, IntPtr payload) @@ -73,38 +59,11 @@ private int FileCallback(GitDiffDelta delta, float progress, IntPtr payload) return 0; } - private int PrintCallBack(GitDiffDelta delta, GitDiffRange range, GitDiffLineOrigin lineorigin, IntPtr content, UIntPtr contentlen, IntPtr payload) - { - string formattedoutput = Utf8Marshaler.FromNative(content, (int)contentlen); - var filePath = FilePathMarshaler.FromNative(delta.NewFile.Path); - - fullPatchBuilder.Append(formattedoutput); - this[filePath].AppendToPatch(formattedoutput); - - return 0; - } - - private void AddLineChange(Changes currentChange, GitDiffLineOrigin lineOrigin) - { - switch (lineOrigin) - { - case GitDiffLineOrigin.GIT_DIFF_LINE_ADDITION: - linesAdded++; - currentChange.LinesAdded++; - break; - - case GitDiffLineOrigin.GIT_DIFF_LINE_DELETION: - linesDeleted++; - currentChange.LinesDeleted++; - break; - } - } - private void AddFileChange(GitDiffDelta delta) { - var newFilePath = FilePathMarshaler.FromNative(delta.NewFile.Path); + var newFilePath = LaxFilePathMarshaler.FromNative(delta.NewFile.Path); - var oldFilePath = FilePathMarshaler.FromNative(delta.OldFile.Path); + var oldFilePath = LaxFilePathMarshaler.FromNative(delta.OldFile.Path); var newMode = (Mode)delta.NewFile.Mode; var oldMode = (Mode)delta.OldFile.Mode; var newOid = delta.NewFile.Oid; @@ -115,13 +74,13 @@ private void AddFileChange(GitDiffDelta delta) delta.Status = ChangeKind.Added; } - var diffFile = new TreeEntryChanges(newFilePath, newMode, newOid, delta.Status, oldFilePath, oldMode, oldOid, delta.IsBinary()); + var diffFile = new TreeEntryChanges(newFilePath, newMode, newOid, delta.Status, oldFilePath, oldMode, oldOid); fileDispatcher[delta.Status](this, diffFile); changes.Add(newFilePath, diffFile); } - #region IEnumerable Members + #region IEnumerable Members /// /// Returns an enumerator that iterates through the collection. @@ -198,27 +157,27 @@ public virtual IEnumerable TypeChanged } /// - /// The total number of lines added in this diff. + /// List of which have been renamed /// - public virtual int LinesAdded + public virtual IEnumerable Renamed { - get { return linesAdded; } + get { return renamed; } } /// - /// The total number of lines added in this diff. + /// List of which have been copied /// - public virtual int LinesDeleted + public virtual IEnumerable Copied { - get { return linesDeleted; } + get { return copied; } } /// - /// The full patch file of this diff. + /// List of which are unmodified /// - public virtual string Patch + public virtual IEnumerable Unmodified { - get { return fullPatchBuilder.ToString(); } + get { return unmodified; } } private string DebuggerDisplay @@ -226,9 +185,9 @@ private string DebuggerDisplay get { return string.Format(CultureInfo.InvariantCulture, - "+{0} ~{2} -{1} \u00B1{3}", - Added.Count(), Deleted.Count(), - Modified.Count(), TypeChanged.Count()); + "+{0} ~{1} -{2} \u00B1{3} R{4} C{5}", + Added.Count(), Modified.Count(), Deleted.Count(), + TypeChanged.Count(), Renamed.Count(), Copied.Count()); } } } diff --git a/LibGit2Sharp/TreeDefinition.cs b/LibGit2Sharp/TreeDefinition.cs index bd29a20ee..63bbb0caf 100644 --- a/LibGit2Sharp/TreeDefinition.cs +++ b/LibGit2Sharp/TreeDefinition.cs @@ -29,7 +29,7 @@ public static TreeDefinition From(Tree tree) foreach (TreeEntry treeEntry in tree) { - td.AddEntry(treeEntry.Name, TreeEntryDefinition.From(treeEntry)); + td.Add(treeEntry.Name, treeEntry); } return td; @@ -137,6 +137,21 @@ public virtual TreeDefinition Add(string targetTreeEntryPath, TreeEntryDefinitio return this; } + /// + /// Adds or replaces a , built from the provided , at the specified location. + /// + /// The path within this . + /// The to be stored at the described location. + /// The current . + public virtual TreeDefinition Add(string targetTreeEntryPath, TreeEntry treeEntry) + { + Ensure.ArgumentNotNull(treeEntry, "treeEntry"); + + TreeEntryDefinition ted = TreeEntryDefinition.From(treeEntry); + + return Add(targetTreeEntryPath, ted); + } + /// /// Adds or replaces a , dynamically built from the provided , at the specified location. /// diff --git a/LibGit2Sharp/TreeEntryChanges.cs b/LibGit2Sharp/TreeEntryChanges.cs index 4ce5547d4..62e76f04c 100644 --- a/LibGit2Sharp/TreeEntryChanges.cs +++ b/LibGit2Sharp/TreeEntryChanges.cs @@ -8,7 +8,7 @@ namespace LibGit2Sharp /// Holds the changes between two versions of a tree entry. /// [DebuggerDisplay("{DebuggerDisplay,nq}")] - public class TreeEntryChanges : Changes + public class TreeEntryChanges { /// /// Needed for mocking purposes. @@ -16,7 +16,7 @@ public class TreeEntryChanges : Changes protected TreeEntryChanges() { } - internal TreeEntryChanges(FilePath path, Mode mode, ObjectId oid, ChangeKind status, FilePath oldPath, Mode oldMode, ObjectId oldOid, bool isBinaryComparison) + internal TreeEntryChanges(FilePath path, Mode mode, ObjectId oid, ChangeKind status, FilePath oldPath, Mode oldMode, ObjectId oldOid) { Path = path.Native; Mode = mode; @@ -25,7 +25,6 @@ internal TreeEntryChanges(FilePath path, Mode mode, ObjectId oid, ChangeKind sta OldPath = oldPath.Native; OldMode = oldMode; OldOid = oldOid; - IsBinaryComparison = isBinaryComparison; } /// diff --git a/LibGit2Sharp/UnbornBranchException.cs b/LibGit2Sharp/UnbornBranchException.cs index 0ee5e4afa..5899321ea 100644 --- a/LibGit2Sharp/UnbornBranchException.cs +++ b/LibGit2Sharp/UnbornBranchException.cs @@ -11,7 +11,7 @@ namespace LibGit2Sharp public class UnbornBranchException : LibGit2SharpException { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// public UnbornBranchException() { diff --git a/LibGit2Sharp/libgit2_hash.txt b/LibGit2Sharp/libgit2_hash.txt index f3f5d3a01..845caffcf 100644 --- a/LibGit2Sharp/libgit2_hash.txt +++ b/LibGit2Sharp/libgit2_hash.txt @@ -1 +1 @@ -11f8336ec93ea3a270c9fe80c4bbb68aa4729423 +65e9dc659a013817d1adb3cdc1ed43e697cfbc54 diff --git a/UpdateLibgit2ToSha.ps1 b/UpdateLibgit2ToSha.ps1 index ff5974943..2bafd6aca 100644 --- a/UpdateLibgit2ToSha.ps1 +++ b/UpdateLibgit2ToSha.ps1 @@ -178,8 +178,8 @@ namespace LibGit2Sharp.Core } "@ - sc -Encoding ASCII .\Libgit2sharp\Core\NativeDllName.cs $dllNameClass - sc -Encoding ASCII libgit2sharp\libgit2_hash.txt $sha + sc -Encoding ASCII (Join-Path $libgit2sharpDirectory "Libgit2sharp\Core\NativeDllName.cs") $dllNameClass + sc -Encoding ASCII (Join-Path $libgit2sharpDirectory "Libgit2sharp\libgit2_hash.txt") $sha Write-Output "Done!" } diff --git a/acknowledgments.md b/acknowledgments.md new file mode 100644 index 000000000..a04ce2847 --- /dev/null +++ b/acknowledgments.md @@ -0,0 +1,2 @@ +LibGit2Sharp is making use of the following OSS projects: +- [tar-cs](http://code.google.com/p/tar-cs/) project by Vladimir Vasiltsov and is used under the [BSD license](http://code.google.com/p/tar-cs/source/browse/trunk/COPYING) diff --git a/build.libgit2sharp.sh b/build.libgit2sharp.sh index 2cc44c8c5..151f76b24 100755 --- a/build.libgit2sharp.sh +++ b/build.libgit2sharp.sh @@ -1,27 +1,28 @@ #!/bin/bash -PREVIOUS_LD=$LD_LIBRARY_PATH - LIBGIT2SHA=`cat ./LibGit2Sharp/libgit2_hash.txt` SHORTSHA=${LIBGIT2SHA:0:7} -rm -rf cmake-build -mkdir cmake-build && cd cmake-build +rm -rf libgit2/build +mkdir libgit2/build +pushd libgit2/build +export _BINPATH=`pwd` -cmake -DCMAKE_BUILD_TYPE:STRING=RelWithDebInfo -DTHREADSAFE:BOOL=ON -DBUILD_CLAR:BOOL=OFF -DCMAKE_INSTALL_PREFIX=./libgit2-bin -DLIBGIT2_FILENAME=git2-$SHORTSHA ../libgit2 -cmake --build . --target install +cmake -DCMAKE_BUILD_TYPE:STRING=RelWithDebInfo \ + -DTHREADSAFE:BOOL=ON \ + -DBUILD_CLAR:BOOL=OFF \ + -DLIBGIT2_FILENAME=git2-$SHORTSHA \ + -DCMAKE_OSX_ARCHITECTURES="i386;x86_64" \ + .. +cmake --build . -LD_LIBRARY_PATH=$PWD/libgit2-bin/lib:$LD_LIBRARY_PATH -export LD_LIBRARY_PATH +export LD_LIBRARY_PATH=$_BINPATH:$LD_LIBRARY_PATH +export DYLD_LIBRARY_PATH=$_BINPATH:$DYLD_LIBRARY_PATH -cd .. +popd +echo $DYLD_LIBRARY_PATH echo $LD_LIBRARY_PATH xbuild CI-build.msbuild /t:Deploy -EXIT_CODE=$? - -LD_LIBRARY_PATH=$PREVIOUS_LD -export LD_LIBRARY_PATH - -exit $EXIT_CODE +exit $? diff --git a/libgit2 b/libgit2 index 11f8336ec..65e9dc659 160000 --- a/libgit2 +++ b/libgit2 @@ -1 +1 @@ -Subproject commit 11f8336ec93ea3a270c9fe80c4bbb68aa4729423 +Subproject commit 65e9dc659a013817d1adb3cdc1ed43e697cfbc54