Skip to content
This repository was archived by the owner on Jun 21, 2023. It is now read-only.
Original file line number Diff line number Diff line change
Expand Up @@ -91,10 +91,12 @@ public PullRequestDetailViewModelDesigner()
public ReactiveCommand<Unit> Pull { get; }
public ReactiveCommand<Unit> Push { get; }
public ReactiveCommand<object> OpenOnGitHub { get; }
public ReactiveCommand<object> OpenFile { get; }
public ReactiveCommand<object> DiffFile { get; }
public ReactiveCommand<object> DiffFileWithWorkingDirectory { get; }
public ReactiveCommand<object> OpenFileInWorkingDirectory { get; }
public ReactiveCommand<object> ViewFile { get; }

public Task<Tuple<string, string>> ExtractDiffFiles(IPullRequestFileNode file)
public Task<string> ExtractFile(IPullRequestFileNode file, bool head)
{
return null;
}
Expand Down
62 changes: 40 additions & 22 deletions src/GitHub.App/Services/PullRequestService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
using System.Reactive;
using System.Collections.Generic;
using LibGit2Sharp;
using PullRequest = Octokit.PullRequest;
using System.Diagnostics;

namespace GitHub.Services
Expand Down Expand Up @@ -292,40 +291,40 @@ public IObservable<int> GetPullRequestForCurrentBranch(ILocalRepositoryModel rep
});
}

public IObservable<Tuple<string, string>> ExtractDiffFiles(
public IObservable<string> ExtractFile(
ILocalRepositoryModel repository,
IPullRequestModel pullRequest,
string fileName,
bool isPullRequestBranchCheckedOut)
bool head = true)
{
return Observable.Defer(async () =>
{
var repo = gitService.GetRepository(repository.LocalPath);
var remote = await gitClient.GetHttpRemote(repo, "origin");
string sha;

var baseSha = pullRequest.Base.Sha;
var headSha = pullRequest.Head.Sha;
var baseRef = pullRequest.Base.Ref;
string mergeBase = await gitClient.GetPullRequestMergeBase(repo, remote.Name, baseSha, headSha, baseRef, pullRequest.Number);
if (mergeBase == null)
if (head)
{
throw new FileNotFoundException($"Couldn't find merge base between {baseSha} and {headSha}.");
}

string left;
string right;
if (isPullRequestBranchCheckedOut)
{
right = Path.Combine(repository.LocalPath, fileName);
left = await ExtractToTempFile(repo, mergeBase, fileName, GetEncoding(right));
sha = pullRequest.Head.Sha;
}
else
{
left = await ExtractToTempFile(repo, mergeBase, fileName, Encoding.UTF8);
right = await ExtractToTempFile(repo, headSha, fileName, Encoding.UTF8);
sha = await gitClient.GetPullRequestMergeBase(
repo,
remote.Name,
pullRequest.Base.Sha,
pullRequest.Head.Sha,
pullRequest.Base.Ref,
pullRequest.Number);

if (sha == null)
{
throw new NotFoundException($"Couldn't find merge base between {pullRequest.Base.Sha} and {pullRequest.Head.Sha}.");
}
}

return Observable.Return(Tuple.Create(left, right));
var file = await ExtractToTempFile(repo, pullRequest.Number, sha, fileName, Encoding.UTF8);
return Observable.Return(file);
});
}

Expand Down Expand Up @@ -413,9 +412,27 @@ string CreateUniqueRemoteName(IRepository repo, string name)
return uniqueName;
}

async Task<string> ExtractToTempFile(IRepository repo, string commitSha, string fileName, Encoding encoding)
async Task<string> ExtractToTempFile(
IRepository repo,
int pullRequestNumber,
string commitSha,
string fileName,
Encoding encoding)
{
var contents = await gitClient.ExtractFile(repo, commitSha, fileName) ?? string.Empty;
string contents;

try
{
contents = await gitClient.ExtractFile(repo, commitSha, fileName) ?? string.Empty;
}
catch (FileNotFoundException)
{
var pullHeadRef = $"refs/pull/{pullRequestNumber}/head";
var remote = await gitClient.GetHttpRemote(repo, "origin");
await gitClient.Fetch(repo, remote.Name, commitSha, pullHeadRef);
contents = await gitClient.ExtractFile(repo, commitSha, fileName) ?? string.Empty;
}

return CreateTempFile(fileName, commitSha, contents, encoding);
}

Expand All @@ -427,6 +444,7 @@ static string CreateTempFile(string fileName, string commitSha, string contents,

Directory.CreateDirectory(tempDir);
File.WriteAllText(tempFile, contents, encoding);
File.SetAttributes(tempFile, FileAttributes.ReadOnly);
return tempFile;
}

Expand Down
34 changes: 25 additions & 9 deletions src/GitHub.App/ViewModels/PullRequestDetailViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,10 @@ public PullRequestDetailViewModel(
SubscribeOperationError(Push);

OpenOnGitHub = ReactiveCommand.Create();
OpenFile = ReactiveCommand.Create(this.WhenAnyValue(x => x.IsCheckedOut));
DiffFile = ReactiveCommand.Create();
DiffFileWithWorkingDirectory = ReactiveCommand.Create(this.WhenAnyValue(x => x.IsCheckedOut));
OpenFileInWorkingDirectory = ReactiveCommand.Create(this.WhenAnyValue(x => x.IsCheckedOut));
ViewFile = ReactiveCommand.Create();
}

/// <summary>
Expand Down Expand Up @@ -276,14 +278,25 @@ public IReadOnlyList<IPullRequestChangeNode> ChangedFilesTree
public ReactiveCommand<object> OpenOnGitHub { get; }

/// <summary>
/// Gets a command that opens a <see cref="IPullRequestFileNode"/>.
/// Gets a command that diffs an <see cref="IPullRequestFileNode"/> between BASE and HEAD.
/// </summary>
public ReactiveCommand<object> OpenFile { get; }
public ReactiveCommand<object> DiffFile { get; }

/// <summary>
/// Gets a command that diffs a <see cref="IPullRequestFileNode"/>.
/// Gets a command that diffs an <see cref="IPullRequestFileNode"/> between the version in
/// the working directory and HEAD.
/// </summary>
public ReactiveCommand<object> DiffFile { get; }
public ReactiveCommand<object> DiffFileWithWorkingDirectory { get; }

/// <summary>
/// Gets a command that opens an <see cref="IPullRequestFileNode"/> from disk.
/// </summary>
public ReactiveCommand<object> OpenFileInWorkingDirectory { get; }

/// <summary>
/// Gets a command that opens an <see cref="IPullRequestFileNode"/> as it appears in the PR.
/// </summary>
public ReactiveCommand<object> ViewFile { get; }

/// <summary>
/// Initializes the view model with new data.
Expand Down Expand Up @@ -404,14 +417,17 @@ public async Task Load(IPullRequestModel pullRequest)
}

/// <summary>
/// Gets the before and after files needed for viewing a diff.
/// Gets a file as it appears in the pull request head.
/// </summary>
/// <param name="file">The changed file.</param>
/// <returns>A tuple containing the full path to the before and after files.</returns>
public Task<Tuple<string, string>> ExtractDiffFiles(IPullRequestFileNode file)
/// <param name="head">
/// If true, gets the file at the PR head, otherwise gets the file at the PR merge base.
/// </param>
/// <returns>The path to a temporary file.</returns>
public Task<string> ExtractFile(IPullRequestFileNode file, bool head)
{
var path = Path.Combine(file.DirectoryPath, file.FileName);
return pullRequestsService.ExtractDiffFiles(Repository, model, path, IsCheckedOut).ToTask();
return pullRequestsService.ExtractFile(Repository, model, path).ToTask();
}

/// <summary>
Expand Down
13 changes: 4 additions & 9 deletions src/GitHub.Exports.Reactive/Services/IPullRequestService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -121,23 +121,18 @@ IObservable<IPullRequestModel> CreatePullRequest(IRepositoryHost host,
IObservable<int> GetPullRequestForCurrentBranch(ILocalRepositoryModel repository);

/// <summary>
/// Gets the left and right files for a diff.
/// Gets a file as it appears in a pull request.
/// </summary>
/// <param name="repository">The repository.</param>
/// <param name="modelService">A model service to use as a cache if the file is not fetched.</param>
/// <param name="pullRequest">The pull request details.</param>
/// <param name="fileName">The filename relative to the repository root.</param>
/// <param name="fileSha">The SHA of the file in the pull request.</param>
/// <param name="isPullRequestBranchCheckedOut">
/// Whether the pull request branch is currently checked out. If so the right file returned
/// will be the path to the file in the working directory.
/// </param>
/// <param name="head">If true, gets the file at the PR head, otherwise gets the file at the PR base.</param>
/// <returns>The paths of the left and right files for the diff.</returns>
IObservable<Tuple<string, string>> ExtractDiffFiles(
IObservable<string> ExtractFile(
ILocalRepositoryModel repository,
IPullRequestModel pullRequest,
string fileName,
bool isPullRequestBranchCheckedOut);
bool head = true);

/// <summary>
/// Remotes all unused remotes that were created by GitHub for Visual Studio to track PRs
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,21 +152,35 @@ public interface IPullRequestDetailViewModel : IViewModel, IHasLoading, IHasBusy
ReactiveCommand<object> OpenOnGitHub { get; }

/// <summary>
/// Gets a command that opens a <see cref="IPullRequestFileNode"/>.
/// Gets a command that diffs an <see cref="IPullRequestFileNode"/> between BASE and HEAD.
/// </summary>
ReactiveCommand<object> OpenFile { get; }
ReactiveCommand<object> DiffFile { get; }

/// <summary>
/// Gets a command that diffs a <see cref="IPullRequestFileNode"/>.
/// Gets a command that diffs an <see cref="IPullRequestFileNode"/> between the version in
/// the working directory and HEAD.
/// </summary>
ReactiveCommand<object> DiffFile { get; }
ReactiveCommand<object> DiffFileWithWorkingDirectory { get; }

/// <summary>
/// Gets a command that opens an <see cref="IPullRequestFileNode"/> from disk.
/// </summary>
ReactiveCommand<object> OpenFileInWorkingDirectory { get; }

/// <summary>
/// Gets a command that opens an <see cref="IPullRequestFileNode"/> as it appears in the PR.
/// </summary>
ReactiveCommand<object> ViewFile { get; }

/// <summary>
/// Gets the before and after files needed for viewing a diff.
/// Gets a file as it appears in the pull request.
/// </summary>
/// <param name="file">The changed file.</param>
/// <returns>A tuple containing the full path to the before and after files.</returns>
Task<Tuple<string, string>> ExtractDiffFiles(IPullRequestFileNode file);
/// <param name="head">
/// If true, gets the file at the PR head, otherwise gets the file at the PR merge base.
/// </param>
/// <returns>The path to a temporary file.</returns>
Task<string> ExtractFile(IPullRequestFileNode file, bool head);

/// <summary>
/// Gets the full path to a file in the working directory.
Expand Down
32 changes: 25 additions & 7 deletions src/GitHub.VisualStudio.UI/Resources.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 9 additions & 3 deletions src/GitHub.VisualStudio.UI/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -360,8 +360,8 @@
<data name="CompareFileAsDefaultAction" xml:space="preserve">
<value>Compare File as Default Action</value>
</data>
<data name="OpenFile" xml:space="preserve">
<value>Open File</value>
<data name="ViewFile" xml:space="preserve">
<value>View File</value>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Users could easily miss the fact that the "Working Directory" commands are writable.
Maybe change this label to, "View File (read-only)"?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similarly with "Compare File (read-only)" above.

</data>
<data name="OpenFileAsDefaultAction" xml:space="preserve">
<value>Open File as Default Action</value>
Expand All @@ -383,4 +383,10 @@

[Don't show this again](dont-show-again)</value>
</data>
</root>
<data name="CompareFileWithWorkingDirectory" xml:space="preserve">
<value>Compare With Working Directory</value>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think, "Compare with Working Directory" might look better.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Visual Studio seems to mainly use title casing even on articles and prepositions, for example "Edit -> Insert File As Text", "Edit -> Bookmark -> Previous Bookmark In Document" (although it is inconsistent - it uses e.g. "Attach to Process"). I agree though, it does look better to me the way you suggested...

</data>
<data name="OpenFileInWorkingDirectory" xml:space="preserve">
<value>Open File In Working Directory</value>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, maybe change this to "Open File in Working Directory".

</data>
</root>
4 changes: 3 additions & 1 deletion src/GitHub.VisualStudio/UI/Views/PullRequestDetailView.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,9 @@
DataContext and update the CommandParameter to the changed file -->
<ContextMenu x:Key="FileContextMenu">
<MenuItem Header="{x:Static prop:Resources.CompareFile}" Command="{Binding DiffFile}"/>
<MenuItem Header="{x:Static prop:Resources.OpenFile}" Command="{Binding OpenFile}"/>
<MenuItem Header="{x:Static prop:Resources.ViewFile}" Command="{Binding ViewFile}"/>
<MenuItem Header="{x:Static prop:Resources.CompareFileWithWorkingDirectory}" Command="{Binding DiffFileWithWorkingDirectory}"/>
<MenuItem Header="{x:Static prop:Resources.OpenFileInWorkingDirectory}" Command="{Binding OpenFileInWorkingDirectory}"/>
</ContextMenu>
</Grid.Resources>

Expand Down
Loading