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

Commit 6877282

Browse files
committed
Pre-fill PR title/description.
When there is only one commit in the branch (as compared to the target), pre-fill the new PR view with that commit title and description. If a PR template exists, then this takes precendence. Fixes #767 Fixes #997
1 parent 91e349d commit 6877282

File tree

9 files changed

+233
-12
lines changed

9 files changed

+233
-12
lines changed

src/GitHub.App/Services/GitClient.cs

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
using System;
2+
using System.Collections.Generic;
23
using System.ComponentModel.Composition;
34
using System.IO;
45
using System.Linq;
56
using System.Reactive.Linq;
67
using System.Threading.Tasks;
78
using GitHub.Extensions;
9+
using GitHub.Models;
810
using GitHub.Primitives;
911
using LibGit2Sharp;
1012
using NLog;
@@ -39,7 +41,6 @@ public GitClient(IGitHubCredentialProvider credentialProvider)
3941
public Task Pull(IRepository repository)
4042
{
4143
Guard.ArgumentNotNull(repository, nameof(repository));
42-
4344
return Task.Factory.StartNew(() =>
4445
{
4546
var signature = repository.Config.BuildSignature(DateTimeOffset.UtcNow);
@@ -450,6 +451,39 @@ public Task Fetch(IRepository repo, UriString cloneUrl, params string[] refspecs
450451
}
451452
}
452453

454+
public Task<IReadOnlyList<CommitMessage>> GetMessagesForUniqueCommits(
455+
IRepository repo,
456+
string baseBranch,
457+
string compareBranch,
458+
int maxCommits)
459+
{
460+
return Task.Factory.StartNew(() =>
461+
{
462+
var baseCommit = repo.Lookup<Commit>(baseBranch);
463+
var compareCommit = repo.Lookup<Commit>(compareBranch);
464+
if (baseCommit == null || compareCommit == null)
465+
{
466+
var missingBranch = baseCommit == null ? baseBranch : compareBranch;
467+
throw new NotFoundException(missingBranch);
468+
}
469+
470+
var mergeCommit = repo.ObjectDatabase.FindMergeBase(baseCommit, compareCommit);
471+
var commitFilter = new CommitFilter
472+
{
473+
IncludeReachableFrom = baseCommit,
474+
ExcludeReachableFrom = mergeCommit,
475+
};
476+
477+
var commits = repo.Commits
478+
.QueryBy(commitFilter)
479+
.Take(maxCommits)
480+
.Select(c => new CommitMessage(c.Message))
481+
.ToList();
482+
483+
return (IReadOnlyList<CommitMessage>)commits;
484+
});
485+
}
486+
453487
static bool IsCanonical(string s)
454488
{
455489
Guard.ArgumentNotEmptyString(s, nameof(s));

src/GitHub.App/Services/PullRequestService.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,19 @@ public IObservable<string> GetPullRequestTemplate(ILocalRepositoryModel reposito
8686
});
8787
}
8888

89+
public IObservable<IReadOnlyList<CommitMessage>> GetMessagesForUniqueCommits(
90+
ILocalRepositoryModel repository,
91+
string baseBranch,
92+
string compareBranch,
93+
int maxCommits)
94+
{
95+
return Observable.Defer(() =>
96+
{
97+
var repo = gitService.GetRepository(repository.LocalPath);
98+
return gitClient.GetMessagesForUniqueCommits(repo, baseBranch, compareBranch, maxCommits).ToObservable();
99+
});
100+
}
101+
89102
public IObservable<bool> IsWorkingDirectoryClean(ILocalRepositoryModel repository)
90103
{
91104
var repo = gitService.GetRepository(repository.LocalPath);

src/GitHub.App/ViewModels/PullRequestCreationViewModel.cs

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
using GitHub.Extensions;
1919
using System.Reactive.Disposables;
2020
using System.Reactive;
21+
using System.Threading.Tasks;
2122

2223
namespace GitHub.ViewModels
2324
{
@@ -70,8 +71,6 @@ public PullRequestCreationViewModel(IRepositoryHost repositoryHost, ILocalReposi
7071
});
7172

7273
SourceBranch = activeRepo.CurrentBranch;
73-
service.GetPullRequestTemplate(activeRepo)
74-
.Subscribe(x => Description = x ?? String.Empty, () => Description = Description ?? String.Empty);
7574

7675
this.WhenAnyValue(x => x.Branches)
7776
.WhereNotNull()
@@ -84,6 +83,31 @@ public PullRequestCreationViewModel(IRepositoryHost repositoryHost, ILocalReposi
8483

8584
SetupValidators();
8685

86+
var uniqueCommits = this.WhenAnyValue(
87+
x => x.SourceBranch,
88+
x => x.TargetBranch)
89+
.Where(x => x.Item1 != null && x.Item2 != null)
90+
.Select(branches =>
91+
{
92+
var baseBranch = branches.Item1.Name;
93+
var compareBranch = branches.Item2.Name;
94+
95+
// We only need to get max two commits for what we're trying to achieve here.
96+
// If there's no commits we want to block creation of the PR, if there's one commits
97+
// we wan't to use its commit message as the PR title/body and finally if there's more
98+
// than one we'll use the branch name for the title.
99+
return service.GetMessagesForUniqueCommits(activeRepo, baseBranch, compareBranch, maxCommits: 2)
100+
.Catch<IReadOnlyList<CommitMessage>, Exception>(ex =>
101+
{
102+
log.Warn("Could not load unique commits", ex);
103+
return Observable.Empty<IReadOnlyList<CommitMessage>>();
104+
});
105+
})
106+
.Switch()
107+
.ObserveOn(RxApp.MainThreadScheduler)
108+
.Replay(1)
109+
.RefCount();
110+
87111
var whenAnyValidationResultChanges = this.WhenAny(
88112
x => x.TitleValidator.ValidationResult,
89113
x => x.BranchValidator.ValidationResult,
@@ -119,6 +143,35 @@ public PullRequestCreationViewModel(IRepositoryHost repositoryHost, ILocalReposi
119143
this.WhenAnyValue(x => x.Initialized, x => x.GitHubRepository, x => x.Description, x => x.IsExecuting)
120144
.Select(x => !(x.Item1 && x.Item2 != null && x.Item3 != null && !x.Item4))
121145
.Subscribe(x => IsBusy = x);
146+
147+
Observable.CombineLatest(
148+
this.WhenAnyValue(x => x.SourceBranch),
149+
uniqueCommits,
150+
service.GetPullRequestTemplate(activeRepo).DefaultIfEmpty(string.Empty),
151+
(compare, commits, template) => new { compare, commits, template })
152+
.Subscribe(x =>
153+
{
154+
var prTitle = string.Empty;
155+
var prDescription = string.Empty;
156+
157+
if (x.commits.Count == 1)
158+
{
159+
prTitle = x.commits[0].Summary;
160+
prDescription = x.commits[0].Details;
161+
}
162+
else
163+
{
164+
prTitle = x.compare.Name.Humanize();
165+
}
166+
167+
if (!string.IsNullOrWhiteSpace(x.template))
168+
{
169+
prDescription = x.template;
170+
}
171+
172+
PRTitle = prTitle;
173+
Description = prDescription;
174+
});
122175
}
123176

124177
public override void Initialize(ViewWithData data = null)

src/GitHub.Exports.Reactive/Services/IGitClient.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
using System.Threading.Tasks;
33
using LibGit2Sharp;
44
using GitHub.Primitives;
5+
using System.Collections.Generic;
6+
using GitHub.Models;
57

68
namespace GitHub.Services
79
{
@@ -204,5 +206,21 @@ public interface IGitClient
204206
/// <param name="repository">The repository.</param>
205207
/// <returns></returns>
206208
Task<bool> IsHeadPushed(IRepository repo);
209+
210+
/// <summary>
211+
/// Gets the unique commits from <paramref name="compareBranch"/> to the merge base of
212+
/// <paramref name="baseBranch"/> and <paramref name="compareBranch"/> and returns their
213+
/// commit messages.
214+
/// </summary>
215+
/// <param name="repository">The repository.</param>
216+
/// <param name="baseBranch">The base branch to find a merge base with.</param>
217+
/// <param name="compareBranch">The compare branch to find a merge base with.</param>
218+
/// <param name="maxCommits">The maximum number of commits to return.</param>
219+
/// <returns>An enumerable of unique commits from the merge base to the compareBranch.</returns>
220+
Task<IReadOnlyList<CommitMessage>> GetMessagesForUniqueCommits(
221+
IRepository repo,
222+
string baseBranch,
223+
string compareBranch,
224+
int maxCommits);
207225
}
208226
}

src/GitHub.Exports.Reactive/Services/IPullRequestService.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
using System;
2+
using System.Collections.Generic;
23
using System.Reactive;
34
using System.Text;
5+
using System.Threading.Tasks;
46
using GitHub.Models;
57
using LibGit2Sharp;
68
using Octokit;
@@ -156,5 +158,21 @@ IObservable<string> ExtractFile(
156158
IObservable<Unit> RemoveUnusedRemotes(ILocalRepositoryModel repository);
157159

158160
IObservable<string> GetPullRequestTemplate(ILocalRepositoryModel repository);
161+
162+
/// <summary>
163+
/// Gets the unique commits from <paramref name="compareBranch"/> to the merge base of
164+
/// <paramref name="baseBranch"/> and <paramref name="compareBranch"/> and returns their
165+
/// commit messages.
166+
/// </summary>
167+
/// <param name="repository">The repository.</param>
168+
/// <param name="baseBranch">The base branch to find a merge base with.</param>
169+
/// <param name="compareBranch">The compare branch to find a merge base with.</param>
170+
/// <param name="maxCommits">The maximum number of commits to return.</param>
171+
/// <returns>An enumerable of unique commits from the merge base to the compareBranch.</returns>
172+
IObservable<IReadOnlyList<CommitMessage>> GetMessagesForUniqueCommits(
173+
ILocalRepositoryModel repository,
174+
string baseBranch,
175+
string compareBranch,
176+
int maxCommits);
159177
}
160178
}

src/GitHub.Exports/GitHub.Exports.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@
130130
<ItemGroup>
131131
<Compile Include="Exports\ExportForProcess.cs" />
132132
<Compile Include="GitHubLogicException.cs" />
133+
<Compile Include="Models\CommitMessage.cs" />
133134
<Compile Include="Models\DiffChangeType.cs" />
134135
<Compile Include="Models\DiffChunk.cs" />
135136
<Compile Include="Models\DiffLine.cs" />
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
using System;
2+
using System.Linq;
3+
4+
namespace GitHub.Models
5+
{
6+
public class CommitMessage : IEquatable<CommitMessage>
7+
{
8+
public string Summary { get; private set; }
9+
public string Details { get; private set; }
10+
public string FullMessage { get; private set; }
11+
12+
/// <summary>
13+
/// This is for mocking porpoises.
14+
/// http://cl.ly/image/0q2A2W0U3O2t
15+
/// </summary>
16+
public CommitMessage() { }
17+
18+
public CommitMessage(string fullMessage)
19+
{
20+
if (string.IsNullOrEmpty(fullMessage)) return;
21+
22+
var lines = fullMessage.Replace("\r\n", "\n").Split('\n');
23+
Summary = lines.FirstOrDefault();
24+
25+
FullMessage = fullMessage;
26+
var detailsLines = lines
27+
.Skip(1)
28+
.SkipWhile(string.IsNullOrEmpty)
29+
.ToList();
30+
Details = detailsLines.Any(x => !string.IsNullOrWhiteSpace(x))
31+
? string.Join(Environment.NewLine, detailsLines).Trim()
32+
: null;
33+
}
34+
35+
public bool Equals(CommitMessage other)
36+
{
37+
if (ReferenceEquals(other, null))
38+
{
39+
return false;
40+
}
41+
42+
return string.Equals(Summary, other.Summary)
43+
&& string.Equals(Details, other.Details);
44+
}
45+
46+
public override bool Equals(object obj)
47+
{
48+
return Equals(obj as CommitMessage);
49+
}
50+
51+
public override int GetHashCode()
52+
{
53+
return Tuple.Create(Summary, Details).GetHashCode();
54+
}
55+
}
56+
}

src/GitHub.Extensions/StringExtensions.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Diagnostics.CodeAnalysis;
34
using System.Globalization;
45
using System.IO;
56
using System.Linq;
67
using System.Text;
8+
using System.Text.RegularExpressions;
79

810
namespace GitHub.Extensions
911
{
@@ -196,5 +198,29 @@ public static Uri ToUriSafe(this string url)
196198
Uri.TryCreate(url, UriKind.Absolute, out uri);
197199
return uri;
198200
}
201+
202+
/// <summary>
203+
/// Returns an alphanumeric sentence cased string with dashes and underscores as spaces.
204+
/// </summary>
205+
/// <param name="s">The string to format.</param>
206+
[SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase")]
207+
public static string Humanize(this string s)
208+
{
209+
if (String.IsNullOrWhiteSpace(s))
210+
{
211+
return s;
212+
}
213+
214+
var matches = Regex.Matches(s, @"[a-zA-Z\d]{1,}", RegexOptions.None);
215+
216+
if (matches.Count == 0)
217+
{
218+
return s;
219+
}
220+
221+
var result = matches.Cast<Match>().Select(match => match.Value.ToLower(CultureInfo.InvariantCulture));
222+
var combined = String.Join(" ", result);
223+
return Char.ToUpper(combined[0], CultureInfo.InvariantCulture) + combined.Substring(1);
224+
}
199225
}
200226
}

src/UnitTests/GitHub.App/ViewModels/PullRequestCreationViewModelTests.cs

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ public void TargetBranchDisplayNameIncludesRepoOwnerWhenFork()
132132
[InlineData(8, "repo-name", "source-repo-owner", "master", true, false, "target-repo-owner", "master", "title", "description")]
133133
[InlineData(9, "repo-name", "source-repo-owner", "source-branch", false, false, "source-repo-owner", "target-branch", "title", null)]
134134
[InlineData(10, "repo-name", "source-repo-owner", "source-branch", false, false, "source-repo-owner", "master", "title", "description")]
135+
[InlineData(11, "repo-name", "source-repo-owner", "source-branch", false, false, "source-repo-owner", "master", null, null)]
135136
public async Task CreatingPRs(int testId,
136137
string repoName, string sourceRepoOwner, string sourceBranchName,
137138
bool repoIsFork, bool sourceBranchIsTracking,
@@ -151,30 +152,31 @@ public async Task CreatingPRs(int testId,
151152
var ms = data.ModelService;
152153

153154
var prservice = new PullRequestService(data.GitClient, data.GitService, data.ServiceProvider.GetOperatingSystem(), Substitute.For<IUsageTracker>());
154-
var vm = new PullRequestCreationViewModel(data.RepositoryHost, data.ActiveRepo, prservice, data.NotificationService);
155+
prservice.GetPullRequestTemplate(data.ActiveRepo).Returns(Observable.Empty<string>());
155156

157+
var vm = new PullRequestCreationViewModel(data.RepositoryHost, data.ActiveRepo, prservice, data.NotificationService);
156158
vm.Initialize();
157159

158-
// the user has to input this
159-
vm.PRTitle = title;
160-
161-
// this is optional
162-
if (body != null)
163-
vm.Description = body;
164-
165160
// the TargetBranch property gets set to whatever the repo default is (we assume master here),
166161
// so we only set it manually to emulate the user selecting a different target branch
167162
if (targetBranchName != "master")
168163
vm.TargetBranch = new BranchModel(targetBranchName, targetRepo);
169164

165+
if (title != null)
166+
vm.PRTitle = title;
167+
168+
// this is optional
169+
if (body != null)
170+
vm.Description = body;
171+
170172
await vm.CreatePullRequest.ExecuteAsync();
171173

172174
var unused2 = gitClient.Received().Push(l2repo, sourceBranchName, remote);
173175
if (!sourceBranchIsTracking)
174176
unused2 = gitClient.Received().SetTrackingBranch(l2repo, sourceBranchName, remote);
175177
else
176178
unused2 = gitClient.DidNotReceiveWithAnyArgs().SetTrackingBranch(Args.LibGit2Repo, Args.String, Args.String);
177-
var unused = ms.Received().CreatePullRequest(activeRepo, targetRepo, sourceBranch, targetBranch, title, body ?? String.Empty);
179+
var unused = ms.Received().CreatePullRequest(activeRepo, targetRepo, sourceBranch, targetBranch, title ?? "Source branch", body ?? String.Empty);
178180
}
179181

180182
[Fact]

0 commit comments

Comments
 (0)