Skip to content

Commit 421d5a6

Browse files
committed
Add support for registering custom merge drivers
libgit2#2107
1 parent cb58177 commit 421d5a6

File tree

12 files changed

+861
-3
lines changed

12 files changed

+861
-3
lines changed
Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
using System;
2+
using System.IO;
3+
using System.Linq;
4+
using LibGit2Sharp.Tests.TestHelpers;
5+
using Xunit;
6+
7+
namespace LibGit2Sharp.Tests
8+
{
9+
public class MergeDriverFixture : BaseFixture
10+
{
11+
private const string MergeDriverName = "the-merge-driver";
12+
13+
[Fact]
14+
public void CanRegisterAndUnregisterTheSameMergeDriver()
15+
{
16+
var mergeDriver = new EmptyMergeDriver(MergeDriverName);
17+
18+
var registration = GlobalSettings.RegisterMergeDriver(mergeDriver);
19+
GlobalSettings.DeregisterMergeDriver(registration);
20+
21+
var secondRegistration = GlobalSettings.RegisterMergeDriver(mergeDriver);
22+
GlobalSettings.DeregisterMergeDriver(secondRegistration);
23+
}
24+
25+
[Fact]
26+
public void CanRegisterAndDeregisterAfterGarbageCollection()
27+
{
28+
var registration = GlobalSettings.RegisterMergeDriver(new EmptyMergeDriver(MergeDriverName));
29+
30+
GC.Collect();
31+
32+
GlobalSettings.DeregisterMergeDriver(registration);
33+
}
34+
35+
[Fact]
36+
public void SameMergeDriverIsEqual()
37+
{
38+
var mergeDriver = new EmptyMergeDriver(MergeDriverName);
39+
Assert.Equal(mergeDriver, mergeDriver);
40+
}
41+
42+
[Fact]
43+
public void InitCallbackNotMadeWhenMergeDriverNeverUsed()
44+
{
45+
bool called = false;
46+
void initializeCallback()
47+
{
48+
called = true;
49+
}
50+
51+
var driver = new FakeMergeDriver(MergeDriverName, initializeCallback);
52+
var registration = GlobalSettings.RegisterMergeDriver(driver);
53+
54+
try
55+
{
56+
Assert.False(called);
57+
}
58+
finally
59+
{
60+
GlobalSettings.DeregisterMergeDriver(registration);
61+
}
62+
}
63+
64+
[Fact]
65+
public void WhenMergingApplyIsCalledWhenThereIsAConflict()
66+
{
67+
string repoPath = InitNewRepository();
68+
bool called = false;
69+
70+
MergeDriverResult apply(MergeDriverSource source)
71+
{
72+
called = true;
73+
return new MergeDriverResult { Status = MergeStatus.Conflicts };
74+
}
75+
76+
var mergeDriver = new FakeMergeDriver(MergeDriverName, applyCallback: apply);
77+
var registration = GlobalSettings.RegisterMergeDriver(mergeDriver);
78+
79+
try
80+
{
81+
using (var repo = CreateTestRepository(repoPath))
82+
{
83+
string newFilePath = Touch(repo.Info.WorkingDirectory, Guid.NewGuid() + ".txt", "file1");
84+
var stageNewFile = new FileInfo(newFilePath);
85+
Commands.Stage(repo, newFilePath);
86+
repo.Commit("Commit", Constants.Signature, Constants.Signature);
87+
88+
var branch = repo.CreateBranch("second");
89+
90+
var id = Guid.NewGuid() + ".txt";
91+
newFilePath = Touch(repo.Info.WorkingDirectory, id, "file2");
92+
stageNewFile = new FileInfo(newFilePath);
93+
Commands.Stage(repo, newFilePath);
94+
repo.Commit("Commit in master", Constants.Signature, Constants.Signature);
95+
96+
Commands.Checkout(repo, branch.FriendlyName);
97+
98+
newFilePath = Touch(repo.Info.WorkingDirectory, id, "file3");
99+
stageNewFile = new FileInfo(newFilePath);
100+
Commands.Stage(repo, newFilePath);
101+
repo.Commit("Commit in second branch", Constants.Signature, Constants.Signature);
102+
103+
var result = repo.Merge("master", Constants.Signature, new MergeOptions { CommitOnSuccess = false });
104+
105+
Assert.True(called);
106+
}
107+
}
108+
finally
109+
{
110+
GlobalSettings.DeregisterMergeDriver(registration);
111+
}
112+
}
113+
114+
[Fact]
115+
public void MergeDriverCanFetchFileContents()
116+
{
117+
string repoPath = InitNewRepository();
118+
119+
MergeDriverResult apply(MergeDriverSource source)
120+
{
121+
var repos = source.Repository;
122+
var blob = repos.Lookup<Blob>(source.Theirs.Id);
123+
var content = blob.GetContentStream();
124+
return new MergeDriverResult { Status = MergeStatus.UpToDate, Content = content };
125+
}
126+
127+
var mergeDriver = new FakeMergeDriver(MergeDriverName, applyCallback: apply);
128+
var registration = GlobalSettings.RegisterMergeDriver(mergeDriver);
129+
130+
try
131+
{
132+
using (var repo = CreateTestRepository(repoPath))
133+
{
134+
string newFilePath = Touch(repo.Info.WorkingDirectory, Guid.NewGuid() + ".txt", "file1");
135+
var stageNewFile = new FileInfo(newFilePath);
136+
Commands.Stage(repo, newFilePath);
137+
repo.Commit("Commit", Constants.Signature, Constants.Signature);
138+
139+
var branch = repo.CreateBranch("second");
140+
141+
var id = Guid.NewGuid() + ".txt";
142+
newFilePath = Touch(repo.Info.WorkingDirectory, id, "file2");
143+
stageNewFile = new FileInfo(newFilePath);
144+
Commands.Stage(repo, newFilePath);
145+
repo.Commit("Commit in master", Constants.Signature, Constants.Signature);
146+
147+
Commands.Checkout(repo, branch.FriendlyName);
148+
149+
newFilePath = Touch(repo.Info.WorkingDirectory, id, "file3");
150+
stageNewFile = new FileInfo(newFilePath);
151+
Commands.Stage(repo, newFilePath);
152+
repo.Commit("Commit in second branch", Constants.Signature, Constants.Signature);
153+
154+
var result = repo.Merge("master", Constants.Signature, new MergeOptions { CommitOnSuccess = false });
155+
}
156+
}
157+
finally
158+
{
159+
GlobalSettings.DeregisterMergeDriver(registration);
160+
}
161+
}
162+
163+
[Fact]
164+
public void DoubleRegistrationFailsButDoubleDeregistrationDoesNot()
165+
{
166+
Assert.Empty(GlobalSettings.GetRegisteredMergeDrivers());
167+
168+
var mergeDriver = new EmptyMergeDriver(MergeDriverName);
169+
var registration = GlobalSettings.RegisterMergeDriver(mergeDriver);
170+
171+
Assert.Throws<EntryExistsException>(() => { GlobalSettings.RegisterMergeDriver(mergeDriver); });
172+
Assert.Single(GlobalSettings.GetRegisteredMergeDrivers());
173+
174+
Assert.True(registration.IsValid, "MergeDriverRegistration.IsValid should be true.");
175+
176+
GlobalSettings.DeregisterMergeDriver(registration);
177+
Assert.Empty(GlobalSettings.GetRegisteredMergeDrivers());
178+
179+
Assert.False(registration.IsValid, "MergeDriverRegistration.IsValid should be false.");
180+
181+
GlobalSettings.DeregisterMergeDriver(registration);
182+
Assert.Empty(GlobalSettings.GetRegisteredMergeDrivers());
183+
184+
Assert.False(registration.IsValid, "MergeDriverRegistration.IsValid should be false.");
185+
}
186+
187+
private static FileInfo CommitFileOnBranch(Repository repo, string branchName, String content)
188+
{
189+
var branch = repo.CreateBranch(branchName);
190+
Commands.Checkout(repo, branch.FriendlyName);
191+
192+
FileInfo expectedPath = StageNewFile(repo, content);
193+
repo.Commit("Commit", Constants.Signature, Constants.Signature);
194+
return expectedPath;
195+
}
196+
197+
private static FileInfo StageNewFile(IRepository repo, string contents = "null")
198+
{
199+
string newFilePath = Touch(repo.Info.WorkingDirectory, Guid.NewGuid() + ".txt", contents);
200+
var stageNewFile = new FileInfo(newFilePath);
201+
Commands.Stage(repo, newFilePath);
202+
return stageNewFile;
203+
}
204+
205+
private Repository CreateTestRepository(string path)
206+
{
207+
var repository = new Repository(path);
208+
CreateConfigurationWithDummyUser(repository, Constants.Identity);
209+
CreateAttributesFile(repository, "* merge=the-merge-driver");
210+
return repository;
211+
}
212+
213+
class EmptyMergeDriver : MergeDriver
214+
{
215+
public EmptyMergeDriver(string name)
216+
: base(name)
217+
{ }
218+
219+
protected override MergeDriverResult Apply(MergeDriverSource source)
220+
{
221+
throw new NotImplementedException();
222+
}
223+
224+
protected override void Initialize()
225+
{
226+
throw new NotImplementedException();
227+
}
228+
}
229+
230+
class FakeMergeDriver : MergeDriver
231+
{
232+
private readonly Action initCallback;
233+
private readonly Func<MergeDriverSource, MergeDriverResult> applyCallback;
234+
235+
public FakeMergeDriver(string name, Action initCallback = null, Func<MergeDriverSource, MergeDriverResult> applyCallback = null)
236+
: base(name)
237+
{
238+
this.initCallback = initCallback;
239+
this.applyCallback = applyCallback;
240+
}
241+
242+
protected override void Initialize()
243+
{
244+
initCallback?.Invoke();
245+
}
246+
247+
protected override MergeDriverResult Apply(MergeDriverSource source)
248+
{
249+
if (applyCallback != null)
250+
return applyCallback(source);
251+
return new MergeDriverResult { Status = MergeStatus.UpToDate };
252+
}
253+
}
254+
}
255+
}

LibGit2Sharp/Core/GitBuf.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,12 @@ public void Dispose()
1515
Proxy.git_buf_dispose(this);
1616
}
1717
}
18+
19+
[StructLayout(LayoutKind.Sequential)]
20+
unsafe struct git_buf
21+
{
22+
public IntPtr ptr;
23+
public UIntPtr asize;
24+
public UIntPtr size;
25+
}
1826
}

LibGit2Sharp/Core/GitErrorCategory.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ internal enum GitErrorCategory
3636
Filesystem,
3737
Patch,
3838
Worktree,
39-
Sha1
39+
Sha1,
40+
MergeDriver
4041
}
4142
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
using System;
2+
using System.Runtime.InteropServices;
3+
4+
namespace LibGit2Sharp.Core
5+
{
6+
[StructLayout(LayoutKind.Sequential)]
7+
internal struct GitMergeDriver
8+
{
9+
/** The `version` should be set to `GIT_MERGE_DRIVER_VERSION`. */
10+
public uint version;
11+
12+
/** Called when the merge driver is first used for any file. */
13+
[MarshalAs(UnmanagedType.FunctionPtr)]
14+
public git_merge_driver_init_fn initialize;
15+
16+
/** Called when the merge driver is unregistered from the system. */
17+
[MarshalAs(UnmanagedType.FunctionPtr)]
18+
public git_merge_driver_shutdown_fn shutdown;
19+
20+
/**
21+
* Called to merge the contents of a conflict. If this function
22+
* returns `GIT_PASSTHROUGH` then the default (`text`) merge driver
23+
* will instead be invoked. If this function returns
24+
* `GIT_EMERGECONFLICT` then the file will remain conflicted.
25+
*/
26+
[MarshalAs(UnmanagedType.FunctionPtr)]
27+
public git_merge_driver_apply_fn apply;
28+
29+
internal delegate int git_merge_driver_init_fn(IntPtr merge_driver);
30+
internal delegate void git_merge_driver_shutdown_fn(IntPtr merge_driver);
31+
32+
/** Called when the merge driver is invoked due to a file level merge conflict. */
33+
internal delegate int git_merge_driver_apply_fn(
34+
IntPtr merge_driver,
35+
IntPtr path_out,
36+
UIntPtr mode_out,
37+
IntPtr merged_out,
38+
IntPtr driver_name,
39+
IntPtr merge_driver_source
40+
);
41+
}
42+
43+
/// <summary>
44+
/// The file source being merged
45+
/// </summary>
46+
[StructLayout(LayoutKind.Sequential)]
47+
internal unsafe struct git_merge_driver_source
48+
{
49+
public git_repository* repository;
50+
readonly char *default_driver;
51+
readonly IntPtr file_opts;
52+
53+
public git_index_entry* ancestor;
54+
public git_index_entry* ours;
55+
public git_index_entry* theirs;
56+
}
57+
}

LibGit2Sharp/Core/NativeMethods.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,9 @@ internal static extern unsafe int git_branch_upstream_name(
360360
[DllImport(libgit2, CallingConvention = CallingConvention.Cdecl)]
361361
internal static extern void git_buf_dispose(GitBuf buf);
362362

363+
[DllImport(libgit2, CallingConvention = CallingConvention.Cdecl)]
364+
internal static extern int git_buf_grow(IntPtr buf, uint target_size);
365+
363366
[DllImport(libgit2, CallingConvention = CallingConvention.Cdecl)]
364367
internal static extern unsafe int git_checkout_tree(
365368
git_repository* repo,
@@ -989,6 +992,16 @@ internal static extern unsafe int git_merge(
989992
ref GitMergeOpts merge_opts,
990993
ref GitCheckoutOpts checkout_opts);
991994

995+
[DllImport(libgit2, CallingConvention = CallingConvention.Cdecl)]
996+
internal static extern int git_merge_driver_register(
997+
[MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))] string name,
998+
IntPtr gitMergeDriver);
999+
1000+
[DllImport(libgit2, CallingConvention = CallingConvention.Cdecl)]
1001+
internal static extern int git_merge_driver_unregister(
1002+
[MarshalAs(UnmanagedType.CustomMarshaler, MarshalCookie = UniqueId.UniqueIdentifier, MarshalTypeRef = typeof(StrictUtf8Marshaler))]string name);
1003+
1004+
9921005
[DllImport(libgit2, CallingConvention = CallingConvention.Cdecl)]
9931006
internal static extern unsafe int git_merge_commits(
9941007
out git_index* index,

0 commit comments

Comments
 (0)