Skip to content

Commit 4f4819e

Browse files
committed
Introduce Repository.Blame
1 parent a3f95fc commit 4f4819e

File tree

12 files changed

+582
-0
lines changed

12 files changed

+582
-0
lines changed

LibGit2Sharp.Tests/BlameFixture.cs

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
using System;
2+
using System.Linq;
3+
using LibGit2Sharp.Tests.TestHelpers;
4+
using Xunit;
5+
6+
namespace LibGit2Sharp.Tests
7+
{
8+
public class BlameFixture : BaseFixture
9+
{
10+
private static void AssertCorrectHeadBlame(BlameHunkCollection blame)
11+
{
12+
Assert.Equal(1, blame.Count());
13+
Assert.Equal(0, blame[0].FinalStartLineNumber);
14+
Assert.Equal("schacon@gmail.com", blame[0].FinalSignature.Email);
15+
Assert.Equal("4a202b3", blame[0].FinalCommit.Id.ToString(7));
16+
17+
Assert.Equal(0, blame.HunkForLine(0).FinalStartLineNumber);
18+
Assert.Equal("schacon@gmail.com", blame.HunkForLine(0).FinalSignature.Email);
19+
Assert.Equal("4a202b3", blame.HunkForLine(0).FinalCommit.Id.ToString(7));
20+
}
21+
22+
[Fact]
23+
public void CanBlameSimply()
24+
{
25+
using (var repo = new Repository(BareTestRepoPath))
26+
{
27+
AssertCorrectHeadBlame(repo.Blame("README"));
28+
}
29+
}
30+
31+
[Fact]
32+
public void CanBlameFromADifferentCommit()
33+
{
34+
using (var repo = new Repository(MergedTestRepoWorkingDirPath))
35+
{
36+
// File doesn't exist at HEAD
37+
Assert.Throws<LibGit2SharpException>(() => repo.Blame("ancestor-only.txt"));
38+
39+
var blame = repo.Blame("ancestor-only.txt", new BlameOptions { StartingAt = "9107b30" });
40+
Assert.Equal(1, blame.Count());
41+
}
42+
}
43+
44+
[Fact]
45+
public void ValidatesLimits()
46+
{
47+
using (var repo = new Repository(BareTestRepoPath))
48+
{
49+
var blame = repo.Blame("README");
50+
51+
Assert.Throws<ArgumentOutOfRangeException>(() => blame[1]);
52+
Assert.Throws<ArgumentOutOfRangeException>(() => blame.HunkForLine(2));
53+
}
54+
}
55+
56+
[Fact]
57+
public void CanBlameFromVariousTypes()
58+
{
59+
using (var repo = new Repository(BareTestRepoPath))
60+
{
61+
AssertCorrectHeadBlame(repo.Blame("README", new BlameOptions {StartingAt = "HEAD" }));
62+
AssertCorrectHeadBlame(repo.Blame("README", new BlameOptions {StartingAt = repo.Head }));
63+
AssertCorrectHeadBlame(repo.Blame("README", new BlameOptions {StartingAt = repo.Head.Tip }));
64+
AssertCorrectHeadBlame(repo.Blame("README", new BlameOptions {StartingAt = repo.Branches["master"]}));
65+
}
66+
}
67+
68+
[Fact]
69+
public void CanStopBlame()
70+
{
71+
using (var repo = new Repository(BareTestRepoPath))
72+
{
73+
// $ git blame .\new.txt
74+
// 9fd738e8 (Scott Chacon 2010-05-24 10:19:19 -0700 1) my new file
75+
// (be3563a comes after 9fd738e8)
76+
var blame = repo.Blame("new.txt", new BlameOptions {StoppingAt = "be3563a"});
77+
Assert.True(blame[0].FinalCommit.Sha.StartsWith("be3563a"));
78+
}
79+
}
80+
}
81+
}

LibGit2Sharp.Tests/LibGit2Sharp.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
</Reference>
5959
</ItemGroup>
6060
<ItemGroup>
61+
<Compile Include="BlameFixture.cs" />
6162
<Compile Include="CheckoutFixture.cs" />
6263
<Compile Include="RefSpecFixture.cs" />
6364
<Compile Include="EqualityFixture.cs" />

LibGit2Sharp/BlameHunk.cs

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
using System;
2+
using System.Diagnostics;
3+
using System.Globalization;
4+
using LibGit2Sharp.Core;
5+
using LibGit2Sharp.Core.Compat;
6+
7+
namespace LibGit2Sharp
8+
{
9+
/// <summary>
10+
/// A contiguous group of lines that have been traced to a single commit.
11+
/// </summary>
12+
[DebuggerDisplay("{DebuggerDisplay,nq}")]
13+
public class BlameHunk : IEquatable<BlameHunk>
14+
{
15+
private readonly IRepository repository;
16+
17+
private static readonly LambdaEqualityHelper<BlameHunk> equalityHelper =
18+
new LambdaEqualityHelper<BlameHunk>(x => x.LineCount,
19+
x => x.FinalStartLineNumber,
20+
x => x.FinalSignature,
21+
x => x.InitialStartLineNumber,
22+
x => x.InitialSignature,
23+
x => x.InitialCommit);
24+
25+
26+
internal BlameHunk(IRepository repository, GitBlameHunk rawHunk)
27+
{
28+
this.repository = repository;
29+
30+
finalCommit = new Lazy<Commit>(() => repository.Lookup<Commit>(rawHunk.FinalCommitId));
31+
origCommit = new Lazy<Commit>(() => repository.Lookup<Commit>(rawHunk.OrigCommitId));
32+
33+
if (rawHunk.OrigPath != IntPtr.Zero)
34+
{
35+
InitialPath = LaxUtf8Marshaler.FromNative(rawHunk.OrigPath);
36+
}
37+
LineCount = rawHunk.LinesInHunk;
38+
39+
// Libgit2's line numbers are 1-based
40+
FinalStartLineNumber = rawHunk.FinalStartLineNumber - 1;
41+
InitialStartLineNumber = rawHunk.OrigStartLineNumber - 1;
42+
43+
// Signature objects need to have ownership of their native pointers
44+
if (rawHunk.FinalSignature != IntPtr.Zero)
45+
{
46+
FinalSignature = new Signature(NativeMethods.git_signature_dup(rawHunk.FinalSignature));
47+
}
48+
if (rawHunk.OrigSignature != IntPtr.Zero)
49+
{
50+
InitialSignature = new Signature(NativeMethods.git_signature_dup(rawHunk.OrigSignature));
51+
}
52+
}
53+
54+
/// <summary>
55+
/// For easier mocking
56+
/// </summary>
57+
protected BlameHunk() { }
58+
59+
/// <summary>
60+
/// Determine if this hunk contains a given line.
61+
/// </summary>
62+
/// <param name="line">Line number to test</param>
63+
/// <returns>True if this hunk contains the given line.</returns>
64+
public virtual bool ContainsLine(int line)
65+
{
66+
return FinalStartLineNumber <= line && line < FinalStartLineNumber + LineCount;
67+
}
68+
69+
/// <summary>
70+
/// Number of lines in this hunk.
71+
/// </summary>
72+
public virtual int LineCount { get; private set; }
73+
74+
/// <summary>
75+
/// The line number where this hunk begins, as of <see cref="FinalCommit"/>
76+
/// </summary>
77+
public virtual int FinalStartLineNumber { get; private set; }
78+
79+
/// <summary>
80+
/// Signature of the most recent change to this hunk.
81+
/// </summary>
82+
public virtual Signature FinalSignature { get; private set; }
83+
84+
/// <summary>
85+
/// Commit which most recently changed this file.
86+
/// </summary>
87+
public virtual Commit FinalCommit { get { return finalCommit.Value; } }
88+
89+
/// <summary>
90+
/// Line number where this hunk begins, as of <see cref="FinalCommit"/>, in <see cref="InitialPath"/>.
91+
/// </summary>
92+
public virtual int InitialStartLineNumber { get; private set; }
93+
94+
/// <summary>
95+
/// Signature of the oldest-traced change to this hunk.
96+
/// </summary>
97+
public virtual Signature InitialSignature { get; private set; }
98+
99+
/// <summary>
100+
/// Commit to which the oldest change to this hunk has been traced.
101+
/// </summary>
102+
public virtual Commit InitialCommit { get { return origCommit.Value; } }
103+
104+
/// <summary>
105+
/// Path to the file where this hunk originated, as of <see cref="InitialCommit"/>.
106+
/// </summary>
107+
public virtual string InitialPath { get; private set; }
108+
109+
private string DebuggerDisplay
110+
{
111+
get
112+
{
113+
return string.Format(CultureInfo.InvariantCulture,
114+
"{0}-{1} ({2})",
115+
FinalStartLineNumber,
116+
FinalStartLineNumber+LineCount,
117+
FinalCommit.ToString().Substring(0,7));
118+
}
119+
}
120+
121+
private readonly Lazy<Commit> finalCommit;
122+
private readonly Lazy<Commit> origCommit;
123+
124+
/// <summary>
125+
/// Indicates whether the current object is equal to another object of the same type.
126+
/// </summary>
127+
/// <returns>
128+
/// true if the current object is equal to the <paramref name="other"/> parameter; otherwise, false.
129+
/// </returns>
130+
/// <param name="other">An object to compare with this object.</param>
131+
public bool Equals(BlameHunk other)
132+
{
133+
return equalityHelper.Equals(this, other);
134+
}
135+
136+
/// <summary>
137+
/// Determines whether the specified <see cref="Object"/> is equal to the current <see cref="BlameHunk"/>.
138+
/// </summary>
139+
/// <param name="obj">The <see cref="Object"/> to compare with the current <see cref="BlameHunk"/>.</param>
140+
/// <returns>True if the specified <see cref="Object"/> is equal to the current <see cref="BlameHunk"/>; otherwise, false.</returns>
141+
public override bool Equals(object obj)
142+
{
143+
return Equals(obj as BlameHunk);
144+
}
145+
146+
/// <summary>
147+
/// Returns the hash code for this instance.
148+
/// </summary>
149+
/// <returns>A 32-bit signed integer hash code.</returns>
150+
public override int GetHashCode()
151+
{
152+
return equalityHelper.GetHashCode();
153+
}
154+
155+
/// <summary>
156+
/// Tests if two <see cref="BlameHunk"/>s are equal.
157+
/// </summary>
158+
/// <param name="left">First hunk to compare.</param>
159+
/// <param name="right">Second hunk to compare.</param>
160+
/// <returns>True if the two objects are equal; false otherwise.</returns>
161+
public static bool operator ==(BlameHunk left, BlameHunk right)
162+
{
163+
return Equals(left, right);
164+
}
165+
166+
/// <summary>
167+
/// Tests if two <see cref="BlameHunk"/>s are unequal.
168+
/// </summary>
169+
/// <param name="left">First hunk to compare.</param>
170+
/// <param name="right">Second hunk to compare.</param>
171+
/// <returns>True if the two objects are different; false otherwise.</returns>
172+
public static bool operator !=(BlameHunk left, BlameHunk right)
173+
{
174+
return !Equals(left, right);
175+
}
176+
}
177+
}

LibGit2Sharp/BlameHunkCollection.cs

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
using System;
2+
using System.Collections;
3+
using System.Collections.Generic;
4+
using System.Linq;
5+
using LibGit2Sharp.Core;
6+
using LibGit2Sharp.Core.Handles;
7+
8+
namespace LibGit2Sharp
9+
{
10+
/// <summary>
11+
/// The result of a blame operation.
12+
/// </summary>
13+
public class BlameHunkCollection : IEnumerable<BlameHunk>
14+
{
15+
private readonly IRepository repo;
16+
private readonly List<BlameHunk> hunks = new List<BlameHunk>();
17+
18+
/// <summary>
19+
/// For easy mocking
20+
/// </summary>
21+
protected BlameHunkCollection() { }
22+
23+
internal BlameHunkCollection(Repository repo, RepositorySafeHandle repoHandle, string path, BlameOptions options)
24+
{
25+
this.repo = repo;
26+
27+
var rawopts = new GitBlameOptions
28+
{
29+
version = 1,
30+
flags = options.Strategy.ToGitBlameOptionFlags(),
31+
MinLine = (uint)options.MinLine,
32+
MaxLine = (uint)options.MaxLine,
33+
};
34+
if (options.StartingAt != null)
35+
{
36+
rawopts.NewestCommit = repo.Committish(options.StartingAt).Oid;
37+
}
38+
if (options.StoppingAt != null)
39+
{
40+
rawopts.OldestCommit = repo.Committish(options.StoppingAt).Oid;
41+
}
42+
43+
using (var blameHandle = Proxy.git_blame_file(repoHandle, path, rawopts))
44+
{
45+
var numHunks = NativeMethods.git_blame_get_hunk_count(blameHandle);
46+
for (uint i = 0; i < numHunks; ++i)
47+
{
48+
var rawHunk = Proxy.git_blame_get_hunk_byindex(blameHandle, i);
49+
hunks.Add(new BlameHunk(this.repo, rawHunk));
50+
}
51+
}
52+
}
53+
54+
/// <summary>
55+
/// Access blame hunks by index.
56+
/// </summary>
57+
/// <param name="idx">The index of the hunk to retrieve</param>
58+
/// <returns>The <see cref="BlameHunk"/> at the given index.</returns>
59+
public virtual BlameHunk this[int idx]
60+
{
61+
get { return hunks[idx]; }
62+
}
63+
64+
/// <summary>
65+
/// Access blame hunks by the file line.
66+
/// </summary>
67+
/// <param name="line">Line number to search for</param>
68+
/// <returns>The <see cref="BlameHunk"/> that contains the specified file line.</returns>
69+
public virtual BlameHunk HunkForLine(int line)
70+
{
71+
var hunk = hunks.FirstOrDefault(x => x.ContainsLine(line));
72+
if (hunk != null)
73+
{
74+
return hunk;
75+
}
76+
throw new ArgumentOutOfRangeException("line", "No hunk for that line");
77+
}
78+
79+
/// <summary>
80+
/// Returns an enumerator that iterates through a collection.
81+
/// </summary>
82+
/// <returns>
83+
/// An <see cref="T:System.Collections.IEnumerator"/> object that can be used to iterate through the collection.
84+
/// </returns>
85+
/// <filterpriority>2</filterpriority>
86+
public IEnumerator<BlameHunk> GetEnumerator()
87+
{
88+
return hunks.GetEnumerator();
89+
}
90+
IEnumerator IEnumerable.GetEnumerator()
91+
{
92+
return GetEnumerator();
93+
}
94+
}
95+
}

0 commit comments

Comments
 (0)