Skip to content
This repository was archived by the owner on Jun 21, 2023. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions src/GitHub.App/Services/GitHubContextService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ public class GitHubContextService : IGitHubContextService
static readonly Regex treeishCommitRegex = new Regex($"(?<commit>[a-z0-9]{{40}})(/(?<tree>.+))?", RegexOptions.Compiled);
static readonly Regex treeishBranchRegex = new Regex($"(?<branch>master)(/(?<tree>.+))?", RegexOptions.Compiled);

static readonly Regex tempFileObjectishRegex = new Regex(@"\\TFSTemp\\[^\\]*[.](?<objectish>[a-z0-9]{8})[.][^.\\]*$", RegexOptions.Compiled);

[ImportingConstructor]
public GitHubContextService(IGitHubServiceProvider serviceProvider, IGitService gitService)
{
Expand Down Expand Up @@ -305,6 +307,55 @@ public bool TryOpenFile(string repositoryDir, GitHubContext context)
}
}

/// <inheritdoc/>
public string FindObjectishForTFSTempFile(string tempFile)
{
var match = tempFileObjectishRegex.Match(tempFile);
if (match.Success)
{
return match.Groups["objectish"].Value;
Copy link
Contributor

Choose a reason for hiding this comment

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

Why did you name the group objectish?

Copy link
Contributor

Choose a reason for hiding this comment

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

I guess it's not you, that's just what the value is called in RepoExtensions.Lookup(...). Nvm me.

Copy link
Contributor

Choose a reason for hiding this comment

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

(first 8 chars of a blob SHA) ahh

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It started off being called blobish, but I stole the name from the Lookup method you found. 😄

}

return null;
}

/// <inheritdoc/>
public (string commitSha, string blobPath) ResolveBlobFromHistory(string repositoryDir, string objectish)
{
using (var repo = gitService.GetRepository(repositoryDir))
{
var blob = repo.Lookup<Blob>(objectish);
if (blob == null)
{
return (null, null);
}

foreach (var commit in repo.Commits)
{
var trees = new Stack<Tree>();
trees.Push(commit.Tree);
Copy link
Contributor

Choose a reason for hiding this comment

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

I now know what a Git blob sha is, I've yet to wrap my head around a Commit Tree.

Copy link
Contributor

Choose a reason for hiding this comment

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

I have to read the tests more


while (trees.Count > 0)
{
foreach (var treeEntry in trees.Pop())
{
if (treeEntry.Target == blob)
{
return (commit.Sha, treeEntry.Path);
}

if (treeEntry.TargetType == TreeEntryTargetType.Tree)
{
trees.Push((Tree)treeEntry.Target);
}
}
}
}

return (null, null);
}
}

/// <inheritdoc/>
public bool HasChangesInWorkingDirectory(string repositoryDir, string commitish, string path)
{
Expand Down
24 changes: 24 additions & 0 deletions src/GitHub.Exports/Services/IGitHubContextService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,30 @@ public interface IGitHubContextService
/// <returns>The resolved commit-ish, blob path and commit SHA for the blob. Path will be null if the commit-ish can be resolved but not the blob.</returns>
(string commitish, string path, string commitSha) ResolveBlob(string repositoryDir, GitHubContext context, string remoteName = "origin");

/// <summary>
/// Find the object-ish (first 8 chars of a blob SHA) from the path to historical blob created by Team Explorer.
/// </summary>
/// <remarks>
/// Team Explorer creates temporary blob files in the following format:
/// C:\Users\me\AppData\Local\Temp\TFSTemp\vctmp21996_181282.IOpenFromClipboardCommand.783ac965.cs
/// The object-ish appears immediately before the file extension and the path contains the folder "TFSTemp".
/// <remarks>
/// <param name="tempFile">The path to a possible Team Explorer temporary blob file.</param>
/// <returns>The target file's object-ish (blob SHA fragment) or null if the path isn't recognized as a Team Explorer blob file.</returns>
string FindObjectishForTFSTempFile(string tempFile);

/// <summary>
/// Find a tree entry in the commit log where a blob appears and return its commit SHA and path.
/// </summary>
/// <remarks>
/// Search back through the commit log for the first tree entry where a blob appears. This operation only takes
/// a fraction of a seond on the `github/VisualStudio` repository even if a tree entry casn't be found.
/// </remarks>
/// <param name="repositoryDir">The target repository directory.</param>
/// <param name="objectish">The fragment of a blob SHA to find.</param>
/// <returns>The commit SHA and blob path or null if the blob can't be found.</returns>
(string commitSha, string blobPath) ResolveBlobFromHistory(string repositoryDir, string objectish);

/// <summary>
/// Check if a file in the working directory has changed since a specified commit-ish.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.IO;
using System.ComponentModel.Composition;
using GitHub.Services;
using GitHub.Extensions;
Expand All @@ -10,6 +11,7 @@
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Text.Differencing;
using Microsoft.VisualStudio.TextManager.Interop;
using Task = System.Threading.Tasks.Task;

namespace github.202132.xyzmands
Expand Down Expand Up @@ -40,6 +42,8 @@ public class GoToSolutionOrPullRequestFileCommand : VsCommand, IGoToSolutionOrPu
readonly Lazy<IVsEditorAdaptersFactoryService> editorAdapter;
readonly Lazy<IPullRequestSessionManager> sessionManager;
readonly Lazy<IPullRequestEditorService> pullRequestEditorService;
readonly Lazy<ITeamExplorerContext> teamExplorerContext;
readonly Lazy<IGitHubContextService> gitHubContextService;
readonly Lazy<IStatusBarNotificationService> statusBar;
readonly Lazy<IUsageTracker> usageTracker;

Expand All @@ -49,13 +53,17 @@ public GoToSolutionOrPullRequestFileCommand(
Lazy<IVsEditorAdaptersFactoryService> editorAdapter,
Lazy<IPullRequestSessionManager> sessionManager,
Lazy<IPullRequestEditorService> pullRequestEditorService,
Lazy<ITeamExplorerContext> teamExplorerContext,
Lazy<IGitHubContextService> gitHubContextService,
Lazy<IStatusBarNotificationService> statusBar,
Lazy<IUsageTracker> usageTracker) : base(CommandSet, CommandId)
{
this.serviceProvider = serviceProvider;
this.editorAdapter = editorAdapter;
this.sessionManager = sessionManager;
this.pullRequestEditorService = pullRequestEditorService;
this.gitHubContextService = gitHubContextService;
this.teamExplorerContext = teamExplorerContext;
this.statusBar = statusBar;
this.usageTracker = usageTracker;

Expand Down Expand Up @@ -112,6 +120,11 @@ public override async Task Execute()
return;
}

if (TryNavigateFromHistoryFile(sourceView))
{
return;
}

var relativePath = sessionManager.Value.GetRelativePath(textView.TextBuffer);
if (relativePath == null)
{
Expand Down Expand Up @@ -189,6 +202,11 @@ void OnBeforeQueryStatus(object sender, EventArgs e)
return;
}
}

if (TryNavigateFromHistoryFileQueryStatus(sourceView))
{
return;
}
}
catch (Exception ex)
{
Expand All @@ -198,6 +216,65 @@ void OnBeforeQueryStatus(object sender, EventArgs e)
Visible = false;
}

// Set command Text/Visible properties and return true when active
bool TryNavigateFromHistoryFileQueryStatus(IVsTextView sourceView)
{
if (teamExplorerContext.Value.ActiveRepository?.LocalPath is string && // Check there is an active repo
FindObjectishForTFSTempFile(sourceView) is string) // Looks like a history file
{
// Navigate from history file is active
Text = "Open File in Solution";
Visible = true;
return true;
}

return false;
}

// Attempt navigation to historical file
bool TryNavigateFromHistoryFile(IVsTextView sourceView)
{
if (teamExplorerContext.Value.ActiveRepository?.LocalPath is string repositoryDir &&
FindObjectishForTFSTempFile(sourceView) is string objectish)
{
var (commitSha, blobPath) = gitHubContextService.Value.ResolveBlobFromHistory(repositoryDir, objectish);
if (blobPath is string)
{
var workingFile = Path.Combine(repositoryDir, blobPath);
VsShellUtilities.OpenDocument(serviceProvider, workingFile, VSConstants.LOGVIEWID.TextView_guid,
out IVsUIHierarchy hierarchy, out uint itemID, out IVsWindowFrame windowFrame, out IVsTextView targetView);

pullRequestEditorService.Value.NavigateToEquivalentPosition(sourceView, targetView);
return true;
}
}

return false;
}

// Find the blob SHA in a file name if any
string FindObjectishForTFSTempFile(IVsTextView sourceView)
{
return
FindPath(sourceView) is string path &&
gitHubContextService.Value.FindObjectishForTFSTempFile(path) is string objectish ?
objectish : null;
}

// See http://microsoft.public.vstudio.extensibility.narkive.com/agfoD1GO/full-pathname-of-file-shown-in-current-view-of-core-editor#post2
static string FindPath(IVsTextView textView)
{
ErrorHandler.ThrowOnFailure(textView.GetBuffer(out IVsTextLines buffer));
var userData = buffer as IVsUserData;
if (userData == null)
{
return null;
}

ErrorHandler.ThrowOnFailure(userData.GetData(typeof(IVsUserData).GUID, out object data));
return data as string;
}

ITextView FindActiveTextView(IDifferenceViewer diffViewer)
{
switch (diffViewer.ActiveViewType)
Expand Down
112 changes: 112 additions & 0 deletions test/GitHub.App.UnitTests/Services/GitHubContextServiceTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Linq;
using GitHub.Exports;
using GitHub.Services;
using NSubstitute;
Expand Down Expand Up @@ -382,6 +383,117 @@ public void ResolveBlob(string url, string commitish, string objectish, string e
}
}

public class TheResolveBlobFromCommitsMethod
{
[Test]
public void FlatTree()
{
var objectish = "12345678";
var expectCommitSha = "2434215c5489db2bfa2e5249144a3bc532465f97";
var expectBlobPath = "Class1.cs";
var repositoryDir = "repositoryDir";
var blob = Substitute.For<Blob>();
var treeEntry = CreateTreeEntry(TreeEntryTargetType.Blob, blob, expectBlobPath);
var commit = CreateCommit(expectCommitSha, treeEntry);
var repository = CreateRepository(commit);
repository.Lookup<Blob>(objectish).Returns(blob);
var target = CreateGitHubContextService(repositoryDir, repository);

var (commitSha, blobPath) = target.ResolveBlobFromHistory(repositoryDir, objectish);

Assert.That((commitSha, blobPath), Is.EqualTo((expectCommitSha, expectBlobPath)));
}

[Test]
public void NestedTree()
{
var objectish = "12345678";
var expectCommitSha = "2434215c5489db2bfa2e5249144a3bc532465f97";
var expectBlobPath = @"AnnotateFileTests\Class1.cs";
var repositoryDir = "repositoryDir";
var blob = Substitute.For<Blob>();
var blobTreeEntry = CreateTreeEntry(TreeEntryTargetType.Blob, blob, expectBlobPath);
var childTree = CreateTree(blobTreeEntry);
var treeTreeEntry = CreateTreeEntry(TreeEntryTargetType.Tree, childTree, "AnnotateFileTests");
var commit = CreateCommit(expectCommitSha, treeTreeEntry);
var repository = CreateRepository(commit);
repository.Lookup<Blob>(objectish).Returns(blob);
var target = CreateGitHubContextService(repositoryDir, repository);

var (commitSha, blobPath) = target.ResolveBlobFromHistory(repositoryDir, objectish);

Assert.That((commitSha, blobPath), Is.EqualTo((expectCommitSha, expectBlobPath)));
}

[Test]
public void MissingBlob()
{
var objectish = "12345678";
var repositoryDir = "repositoryDir";
var treeEntry = Substitute.For<TreeEntry>();
var repository = CreateRepository();
var target = CreateGitHubContextService(repositoryDir, repository);

var (commitSha, blobPath) = target.ResolveBlobFromHistory(repositoryDir, objectish);

Assert.That((commitSha, blobPath), Is.EqualTo((null as string, null as string)));
}

static IRepository CreateRepository(params Commit[] commits)
{
var repository = Substitute.For<IRepository>();
var enumerator = commits.ToList().GetEnumerator();
repository.Commits.GetEnumerator().Returns(enumerator);
return repository;
}

static Commit CreateCommit(string sha, params TreeEntry[] treeEntries)
{
var commit = Substitute.For<Commit>();
commit.Sha.Returns(sha);
var tree = CreateTree(treeEntries);
commit.Tree.Returns(tree);
return commit;
}

static TreeEntry CreateTreeEntry(TreeEntryTargetType targetType, GitObject target, string path)
{
var treeEntry = Substitute.For<TreeEntry>();
treeEntry.TargetType.Returns(targetType);
treeEntry.Target.Returns(target);
treeEntry.Path.Returns(path);
return treeEntry;
}

static Tree CreateTree(params TreeEntry[] treeEntries)
{
var tree = Substitute.For<Tree>();
var enumerator = treeEntries.ToList().GetEnumerator();
tree.GetEnumerator().Returns(enumerator);
return tree;
}
}

public class TheFindBlobShaForTextViewMethod
{
[TestCase(@"C:\Users\me\AppData\Local\Temp\TFSTemp\vctmp21996_181282.IOpenFromClipboardCommand.783ac965.cs", "783ac965")]
[TestCase(@"\TFSTemp\File.12345678.ext", "12345678")]
[TestCase(@"\TFSTemp\File.abcdefab.ext", "abcdefab")]
[TestCase(@"\TFSTemp\.12345678.", "12345678")]
[TestCase(@"\TFSTemp\File.ABCDEFAB.ext", null)]
[TestCase(@"\TFSTemp\File.1234567.ext", null)]
[TestCase(@"\TFSTemp\File.123456789.ext", null)]
[TestCase(@"\TFSTemp\File.12345678.ext\\", null)]
public void FindObjectishForTFSTempFile(string path, string expectObjectish)
{
var target = CreateGitHubContextService();

var objectish = target.FindObjectishForTFSTempFile(path);

Assert.That(objectish, Is.EqualTo(expectObjectish));
}
}

static GitHubContextService CreateGitHubContextService(string repositoryDir = null, IRepository repository = null)
{
var sp = Substitute.For<IGitHubServiceProvider>();
Expand Down