Skip to content
This repository was archived by the owner on Jun 21, 2023. It is now read-only.

Commit b74380d

Browse files
authored
Merge pull request #1971 from github/fixes/1707-single-call-to-ReachableFrom
Fix slowness and non-determinism of GetLatestPushedSha (GitHub > Open on GitHub)
2 parents 21e6e3e + 75819e9 commit b74380d

File tree

7 files changed

+337
-22
lines changed

7 files changed

+337
-22
lines changed

src/GitHub.App/SampleData/GitServiceDesigner.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ namespace GitHub.SampleData
77
{
88
class GitServiceDesigner : IGitService
99
{
10-
public Task<string> GetLatestPushedSha(string path) => Task.FromResult<string>(null);
10+
public Task<string> GetLatestPushedSha(string path, string remote = "origin") => Task.FromResult<string>(null);
1111
public UriString GetRemoteUri(IRepository repo, string remote = "origin") => null;
1212
public IRepository GetRepository(string path) => null;
1313
public UriString GetUri(string path, string remote = "origin") => null;

src/GitHub.Exports/Services/GitService.cs

Lines changed: 48 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,14 @@ namespace GitHub.Services
1313
[PartCreationPolicy(CreationPolicy.NonShared)]
1414
public class GitService : IGitService
1515
{
16+
readonly IRepositoryFacade repositoryFacade;
17+
18+
[ImportingConstructor]
19+
public GitService(IRepositoryFacade repositoryFacade)
20+
{
21+
this.repositoryFacade = repositoryFacade;
22+
}
23+
1624
/// <summary>
1725
/// Returns the URL of the remote for the specified <see cref="repository"/>. If the repository
1826
/// is null or no remote named origin exists, this method returns null
@@ -56,8 +64,8 @@ public UriString GetUri(string path, string remote = "origin")
5664
/// <returns>An instance of <see cref="IRepositoryModel"/> or null</returns>
5765
public IRepository GetRepository(string path)
5866
{
59-
var repoPath = Repository.Discover(path);
60-
return repoPath == null ? null : new Repository(repoPath);
67+
var repoPath = repositoryFacade.Discover(path);
68+
return repoPath == null ? null : repositoryFacade.NewRepository(repoPath);
6169
}
6270

6371
/// <summary>
@@ -80,42 +88,70 @@ public UriString GetRemoteUri(IRepository repo, string remote = "origin")
8088
/// <remarks>
8189
/// This is equivalent to creating it via MEF with <see cref="CreationPolicy.NonShared"/>
8290
/// </remarks>
83-
public static IGitService GitServiceHelper => new GitService();
91+
public static IGitService GitServiceHelper => new GitService(new RepositoryFacade());
8492

8593
/// <summary>
8694
/// Finds the latest pushed commit of a file and returns the sha of that commit. Returns null when no commits have
8795
/// been found in any remote branches or the current local branch.
8896
/// </summary>
8997
/// <param name="path">The local path of a repository or a file inside a repository. This cannot be null.</param>
98+
/// <param name="remote">The remote name to look for</param>
9099
/// <returns></returns>
91-
public Task<string> GetLatestPushedSha(string path)
100+
public Task<string> GetLatestPushedSha(string path, string remote = "origin")
92101
{
93102
Guard.ArgumentNotNull(path, nameof(path));
94103

95-
return Task.Factory.StartNew(() =>
104+
return Task.Run(() =>
96105
{
97106
using (var repo = GetRepository(path))
98107
{
99108
if (repo != null)
100109
{
101-
if (repo.Head.IsTracking && repo.Head.Tip.Sha == repo.Head.TrackedBranch.Tip.Sha)
110+
// This is the common case where HEAD is tracking a remote branch
111+
var commonAncestor = repo.Head.TrackingDetails.CommonAncestor;
112+
if (commonAncestor != null)
102113
{
103-
return repo.Head.Tip.Sha;
114+
return commonAncestor.Sha;
104115
}
105116

106-
var remoteHeads = repo.Refs.Where(r => r.IsRemoteTrackingBranch).ToList();
107-
foreach (var c in repo.Commits)
117+
// This is the common case where a branch was forked from a local branch.
118+
// Use CommonAncestor because we don't want to search for a commit that only exists
119+
// locally or that has been added to the remote tracking branch since the fork.
120+
var commonAncestorShas = repo.Branches
121+
.Where(b => b.IsTracking)
122+
.Select(b => b.TrackingDetails.CommonAncestor?.Sha)
123+
.Where(s => s != null)
124+
.ToArray();
125+
126+
var sortByTopological = new CommitFilter { SortBy = CommitSortStrategies.Topological };
127+
foreach (var commit in repo.Commits.QueryBy(sortByTopological))
108128
{
109-
if (repo.Refs.ReachableFrom(remoteHeads, new[] { c }).Any())
129+
if (commonAncestorShas.Contains(commit.Sha))
110130
{
111-
return c.Sha;
131+
return commit.Sha;
112132
}
113133
}
134+
135+
// This is a less common case where a branch was forked from a branch
136+
// which has since had new commits added to it.
137+
var nearestCommonAncestor = repo.Branches
138+
.Where(b => b.IsRemote)
139+
.Select(b => b.Tip)
140+
.Distinct()
141+
.Select(c => repo.ObjectDatabase.CalculateHistoryDivergence(c, repo.Head.Tip))
142+
.Where(hd => hd.AheadBy != null)
143+
.OrderBy(hd => hd.BehindBy)
144+
.Select(hd => hd.CommonAncestor)
145+
.FirstOrDefault();
146+
if (nearestCommonAncestor != null)
147+
{
148+
return nearestCommonAncestor.Sha;
149+
}
114150
}
115151

116152
return null;
117153
}
118154
});
119155
}
120156
}
121-
}
157+
}

src/GitHub.Exports/Services/IGitService.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ public interface IGitService
2828
/// <param name="remote">The remote name to look for</param>
2929
/// <returns>A <see cref="UriString"/> representing the origin or null if none found.</returns>
3030
UriString GetUri(string path, string remote = "origin");
31-
31+
3232
/// <summary>
3333
/// Probes for a git repository and if one is found, returns a <see cref="IRepositoryModel"/> instance for the
3434
/// repository.
@@ -54,7 +54,8 @@ public interface IGitService
5454
/// been found in any remote branches or the current local branch.
5555
/// </summary>
5656
/// <param name="path">The local path of a repository or a file inside a repository. This cannot be null.</param>
57+
/// <param name="remote">The remote name to look for</param>
5758
/// <returns></returns>
58-
Task<string> GetLatestPushedSha(string path);
59+
Task<string> GetLatestPushedSha(string path, string remote = "origin");
5960
}
6061
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using LibGit2Sharp;
2+
3+
namespace GitHub.Services
4+
{
5+
/// <summary>
6+
/// Facade for <see cref="LibGit2Sharp.Repository"/> static methods.
7+
/// </summary>
8+
public interface IRepositoryFacade
9+
{
10+
IRepository NewRepository(string path);
11+
string Discover(string startingPath);
12+
}
13+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
using System.ComponentModel.Composition;
2+
using LibGit2Sharp;
3+
4+
namespace GitHub.Services
5+
{
6+
/// <summary>
7+
/// Facade for <see cref="LibGit2Sharp.Repository"/> static methods.
8+
/// </summary>
9+
[Export(typeof(IRepositoryFacade))]
10+
public class RepositoryFacade : IRepositoryFacade
11+
{
12+
public IRepository NewRepository(string path)
13+
{
14+
return new Repository(path);
15+
}
16+
17+
public string Discover(string startingPath)
18+
{
19+
return Repository.Discover(startingPath);
20+
}
21+
}
22+
}

0 commit comments

Comments
 (0)