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

Commit e7ff5a4

Browse files
authored
Merge branch 'master' into donokuda/not-delete-nooooooo
2 parents 1995a37 + dffdf45 commit e7ff5a4

File tree

5 files changed

+272
-8
lines changed

5 files changed

+272
-8
lines changed

CONTRIBUTING.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,16 @@ This project adheres to the [Open Code of Conduct][code-of-conduct]. By particip
1313

1414
## Submitting a pull request
1515

16-
0. [Fork][] and clone the repository (see Build Instructions in the [README][readme])
17-
0. Create a new branch: `git checkout -b my-branch-name`
18-
0. Make your change, add tests, and make sure the tests still pass
19-
0. Push to your fork and [submit a pull request][pr]
20-
0. Pat your self on the back and wait for your pull request to be reviewed and merged.
16+
1. [Fork][] and clone the repository (see Build Instructions in the [README][readme])
17+
2. Create a new branch: `git checkout -b my-branch-name`
18+
3. Make your change, add tests, and make sure the tests still pass
19+
4. Push to your fork and [submit a pull request][pr]
20+
5. Pat your self on the back and wait for your pull request to be reviewed and merged.
2121

2222
Here are a few things you can do that will increase the likelihood of your pull request being accepted:
2323

24-
- Follow the existing code's style.
25-
- Write tests.
24+
- Follow the style/format of the existing code.
25+
- Write tests for your changes.
2626
- Keep your change as focused as possible. If there are multiple changes you would like to make that are not dependent upon each other, consider submitting them as separate pull requests.
2727
- Write a [good commit message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html).
2828

@@ -38,7 +38,7 @@ There are certain areas of the extension that are restricted in what they can do
3838
### Bug Reporting
3939

4040
Here are a few helpful tips when reporting a bug:
41-
- Verify the bug resides in the GitHub for Visual Studio extension
41+
- Verify that the bug resides in the GitHub for Visual Studio extension
4242
- A lot of functionality provided by this extension resides in the Team Explorer pane, alongside other non-GitHub tools to manage and collaborate on source code, including Visual Studio's Git support, which is owned by Microsoft.
4343
- If this bug not is related to the GitHub extension, visit the [Visual Studio support page](https://www.visualstudio.com/support/support-overview-vs) for help
4444
- Screenshots are very helpful in diagnosing bugs and understanding the state of the extension when it's experiencing problems. Please include them whenever possible.

src/GitHub.App/Services/GitHubContextService.cs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ public class GitHubContextService : IGitHubContextService
5858
static readonly Regex treeishCommitRegex = new Regex($"(?<commit>[a-z0-9]{{40}})(/(?<tree>.+))?", RegexOptions.Compiled);
5959
static readonly Regex treeishBranchRegex = new Regex($"(?<branch>master)(/(?<tree>.+))?", RegexOptions.Compiled);
6060

61+
static readonly Regex tempFileObjectishRegex = new Regex(@"\\TFSTemp\\[^\\]*[.](?<objectish>[a-z0-9]{8})[.][^.\\]*$", RegexOptions.Compiled);
62+
6163
[ImportingConstructor]
6264
public GitHubContextService(IGitHubServiceProvider serviceProvider, IGitService gitService)
6365
{
@@ -305,6 +307,55 @@ public bool TryOpenFile(string repositoryDir, GitHubContext context)
305307
}
306308
}
307309

310+
/// <inheritdoc/>
311+
public string FindObjectishForTFSTempFile(string tempFile)
312+
{
313+
var match = tempFileObjectishRegex.Match(tempFile);
314+
if (match.Success)
315+
{
316+
return match.Groups["objectish"].Value;
317+
}
318+
319+
return null;
320+
}
321+
322+
/// <inheritdoc/>
323+
public (string commitSha, string blobPath) ResolveBlobFromHistory(string repositoryDir, string objectish)
324+
{
325+
using (var repo = gitService.GetRepository(repositoryDir))
326+
{
327+
var blob = repo.Lookup<Blob>(objectish);
328+
if (blob == null)
329+
{
330+
return (null, null);
331+
}
332+
333+
foreach (var commit in repo.Commits)
334+
{
335+
var trees = new Stack<Tree>();
336+
trees.Push(commit.Tree);
337+
338+
while (trees.Count > 0)
339+
{
340+
foreach (var treeEntry in trees.Pop())
341+
{
342+
if (treeEntry.Target == blob)
343+
{
344+
return (commit.Sha, treeEntry.Path);
345+
}
346+
347+
if (treeEntry.TargetType == TreeEntryTargetType.Tree)
348+
{
349+
trees.Push((Tree)treeEntry.Target);
350+
}
351+
}
352+
}
353+
}
354+
355+
return (null, null);
356+
}
357+
}
358+
308359
/// <inheritdoc/>
309360
public bool HasChangesInWorkingDirectory(string repositoryDir, string commitish, string path)
310361
{

src/GitHub.Exports/Services/IGitHubContextService.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,30 @@ public interface IGitHubContextService
7171
/// <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>
7272
(string commitish, string path, string commitSha) ResolveBlob(string repositoryDir, GitHubContext context, string remoteName = "origin");
7373

74+
/// <summary>
75+
/// Find the object-ish (first 8 chars of a blob SHA) from the path to historical blob created by Team Explorer.
76+
/// </summary>
77+
/// <remarks>
78+
/// Team Explorer creates temporary blob files in the following format:
79+
/// C:\Users\me\AppData\Local\Temp\TFSTemp\vctmp21996_181282.IOpenFromClipboardCommand.783ac965.cs
80+
/// The object-ish appears immediately before the file extension and the path contains the folder "TFSTemp".
81+
/// <remarks>
82+
/// <param name="tempFile">The path to a possible Team Explorer temporary blob file.</param>
83+
/// <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>
84+
string FindObjectishForTFSTempFile(string tempFile);
85+
86+
/// <summary>
87+
/// Find a tree entry in the commit log where a blob appears and return its commit SHA and path.
88+
/// </summary>
89+
/// <remarks>
90+
/// Search back through the commit log for the first tree entry where a blob appears. This operation only takes
91+
/// a fraction of a seond on the `github/VisualStudio` repository even if a tree entry casn't be found.
92+
/// </remarks>
93+
/// <param name="repositoryDir">The target repository directory.</param>
94+
/// <param name="objectish">The fragment of a blob SHA to find.</param>
95+
/// <returns>The commit SHA and blob path or null if the blob can't be found.</returns>
96+
(string commitSha, string blobPath) ResolveBlobFromHistory(string repositoryDir, string objectish);
97+
7498
/// <summary>
7599
/// Check if a file in the working directory has changed since a specified commit-ish.
76100
/// </summary>

src/GitHub.VisualStudio/Commands/GoToSolutionOrPullRequestFileCommand.cs

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.IO;
23
using System.ComponentModel.Composition;
34
using GitHub.Services;
45
using GitHub.Extensions;
@@ -10,6 +11,7 @@
1011
using Microsoft.VisualStudio.Shell.Interop;
1112
using Microsoft.VisualStudio.Text.Editor;
1213
using Microsoft.VisualStudio.Text.Differencing;
14+
using Microsoft.VisualStudio.TextManager.Interop;
1315
using Task = System.Threading.Tasks.Task;
1416

1517
namespace GitHub.Commands
@@ -40,6 +42,8 @@ public class GoToSolutionOrPullRequestFileCommand : VsCommand, IGoToSolutionOrPu
4042
readonly Lazy<IVsEditorAdaptersFactoryService> editorAdapter;
4143
readonly Lazy<IPullRequestSessionManager> sessionManager;
4244
readonly Lazy<IPullRequestEditorService> pullRequestEditorService;
45+
readonly Lazy<ITeamExplorerContext> teamExplorerContext;
46+
readonly Lazy<IGitHubContextService> gitHubContextService;
4347
readonly Lazy<IStatusBarNotificationService> statusBar;
4448
readonly Lazy<IUsageTracker> usageTracker;
4549

@@ -49,13 +53,17 @@ public GoToSolutionOrPullRequestFileCommand(
4953
Lazy<IVsEditorAdaptersFactoryService> editorAdapter,
5054
Lazy<IPullRequestSessionManager> sessionManager,
5155
Lazy<IPullRequestEditorService> pullRequestEditorService,
56+
Lazy<ITeamExplorerContext> teamExplorerContext,
57+
Lazy<IGitHubContextService> gitHubContextService,
5258
Lazy<IStatusBarNotificationService> statusBar,
5359
Lazy<IUsageTracker> usageTracker) : base(CommandSet, CommandId)
5460
{
5561
this.serviceProvider = serviceProvider;
5662
this.editorAdapter = editorAdapter;
5763
this.sessionManager = sessionManager;
5864
this.pullRequestEditorService = pullRequestEditorService;
65+
this.gitHubContextService = gitHubContextService;
66+
this.teamExplorerContext = teamExplorerContext;
5967
this.statusBar = statusBar;
6068
this.usageTracker = usageTracker;
6169

@@ -112,6 +120,11 @@ public override async Task Execute()
112120
return;
113121
}
114122

123+
if (TryNavigateFromHistoryFile(sourceView))
124+
{
125+
return;
126+
}
127+
115128
var relativePath = sessionManager.Value.GetRelativePath(textView.TextBuffer);
116129
if (relativePath == null)
117130
{
@@ -189,6 +202,11 @@ void OnBeforeQueryStatus(object sender, EventArgs e)
189202
return;
190203
}
191204
}
205+
206+
if (TryNavigateFromHistoryFileQueryStatus(sourceView))
207+
{
208+
return;
209+
}
192210
}
193211
catch (Exception ex)
194212
{
@@ -198,6 +216,65 @@ void OnBeforeQueryStatus(object sender, EventArgs e)
198216
Visible = false;
199217
}
200218

219+
// Set command Text/Visible properties and return true when active
220+
bool TryNavigateFromHistoryFileQueryStatus(IVsTextView sourceView)
221+
{
222+
if (teamExplorerContext.Value.ActiveRepository?.LocalPath is string && // Check there is an active repo
223+
FindObjectishForTFSTempFile(sourceView) is string) // Looks like a history file
224+
{
225+
// Navigate from history file is active
226+
Text = "Open File in Solution";
227+
Visible = true;
228+
return true;
229+
}
230+
231+
return false;
232+
}
233+
234+
// Attempt navigation to historical file
235+
bool TryNavigateFromHistoryFile(IVsTextView sourceView)
236+
{
237+
if (teamExplorerContext.Value.ActiveRepository?.LocalPath is string repositoryDir &&
238+
FindObjectishForTFSTempFile(sourceView) is string objectish)
239+
{
240+
var (commitSha, blobPath) = gitHubContextService.Value.ResolveBlobFromHistory(repositoryDir, objectish);
241+
if (blobPath is string)
242+
{
243+
var workingFile = Path.Combine(repositoryDir, blobPath);
244+
VsShellUtilities.OpenDocument(serviceProvider, workingFile, VSConstants.LOGVIEWID.TextView_guid,
245+
out IVsUIHierarchy hierarchy, out uint itemID, out IVsWindowFrame windowFrame, out IVsTextView targetView);
246+
247+
pullRequestEditorService.Value.NavigateToEquivalentPosition(sourceView, targetView);
248+
return true;
249+
}
250+
}
251+
252+
return false;
253+
}
254+
255+
// Find the blob SHA in a file name if any
256+
string FindObjectishForTFSTempFile(IVsTextView sourceView)
257+
{
258+
return
259+
FindPath(sourceView) is string path &&
260+
gitHubContextService.Value.FindObjectishForTFSTempFile(path) is string objectish ?
261+
objectish : null;
262+
}
263+
264+
// See http://microsoft.public.vstudio.extensibility.narkive.com/agfoD1GO/full-pathname-of-file-shown-in-current-view-of-core-editor#post2
265+
static string FindPath(IVsTextView textView)
266+
{
267+
ErrorHandler.ThrowOnFailure(textView.GetBuffer(out IVsTextLines buffer));
268+
var userData = buffer as IVsUserData;
269+
if (userData == null)
270+
{
271+
return null;
272+
}
273+
274+
ErrorHandler.ThrowOnFailure(userData.GetData(typeof(IVsUserData).GUID, out object data));
275+
return data as string;
276+
}
277+
201278
ITextView FindActiveTextView(IDifferenceViewer diffViewer)
202279
{
203280
switch (diffViewer.ActiveViewType)

test/GitHub.App.UnitTests/Services/GitHubContextServiceTests.cs

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Linq;
23
using GitHub.Exports;
34
using GitHub.Services;
45
using NSubstitute;
@@ -382,6 +383,117 @@ public void ResolveBlob(string url, string commitish, string objectish, string e
382383
}
383384
}
384385

386+
public class TheResolveBlobFromCommitsMethod
387+
{
388+
[Test]
389+
public void FlatTree()
390+
{
391+
var objectish = "12345678";
392+
var expectCommitSha = "2434215c5489db2bfa2e5249144a3bc532465f97";
393+
var expectBlobPath = "Class1.cs";
394+
var repositoryDir = "repositoryDir";
395+
var blob = Substitute.For<Blob>();
396+
var treeEntry = CreateTreeEntry(TreeEntryTargetType.Blob, blob, expectBlobPath);
397+
var commit = CreateCommit(expectCommitSha, treeEntry);
398+
var repository = CreateRepository(commit);
399+
repository.Lookup<Blob>(objectish).Returns(blob);
400+
var target = CreateGitHubContextService(repositoryDir, repository);
401+
402+
var (commitSha, blobPath) = target.ResolveBlobFromHistory(repositoryDir, objectish);
403+
404+
Assert.That((commitSha, blobPath), Is.EqualTo((expectCommitSha, expectBlobPath)));
405+
}
406+
407+
[Test]
408+
public void NestedTree()
409+
{
410+
var objectish = "12345678";
411+
var expectCommitSha = "2434215c5489db2bfa2e5249144a3bc532465f97";
412+
var expectBlobPath = @"AnnotateFileTests\Class1.cs";
413+
var repositoryDir = "repositoryDir";
414+
var blob = Substitute.For<Blob>();
415+
var blobTreeEntry = CreateTreeEntry(TreeEntryTargetType.Blob, blob, expectBlobPath);
416+
var childTree = CreateTree(blobTreeEntry);
417+
var treeTreeEntry = CreateTreeEntry(TreeEntryTargetType.Tree, childTree, "AnnotateFileTests");
418+
var commit = CreateCommit(expectCommitSha, treeTreeEntry);
419+
var repository = CreateRepository(commit);
420+
repository.Lookup<Blob>(objectish).Returns(blob);
421+
var target = CreateGitHubContextService(repositoryDir, repository);
422+
423+
var (commitSha, blobPath) = target.ResolveBlobFromHistory(repositoryDir, objectish);
424+
425+
Assert.That((commitSha, blobPath), Is.EqualTo((expectCommitSha, expectBlobPath)));
426+
}
427+
428+
[Test]
429+
public void MissingBlob()
430+
{
431+
var objectish = "12345678";
432+
var repositoryDir = "repositoryDir";
433+
var treeEntry = Substitute.For<TreeEntry>();
434+
var repository = CreateRepository();
435+
var target = CreateGitHubContextService(repositoryDir, repository);
436+
437+
var (commitSha, blobPath) = target.ResolveBlobFromHistory(repositoryDir, objectish);
438+
439+
Assert.That((commitSha, blobPath), Is.EqualTo((null as string, null as string)));
440+
}
441+
442+
static IRepository CreateRepository(params Commit[] commits)
443+
{
444+
var repository = Substitute.For<IRepository>();
445+
var enumerator = commits.ToList().GetEnumerator();
446+
repository.Commits.GetEnumerator().Returns(enumerator);
447+
return repository;
448+
}
449+
450+
static Commit CreateCommit(string sha, params TreeEntry[] treeEntries)
451+
{
452+
var commit = Substitute.For<Commit>();
453+
commit.Sha.Returns(sha);
454+
var tree = CreateTree(treeEntries);
455+
commit.Tree.Returns(tree);
456+
return commit;
457+
}
458+
459+
static TreeEntry CreateTreeEntry(TreeEntryTargetType targetType, GitObject target, string path)
460+
{
461+
var treeEntry = Substitute.For<TreeEntry>();
462+
treeEntry.TargetType.Returns(targetType);
463+
treeEntry.Target.Returns(target);
464+
treeEntry.Path.Returns(path);
465+
return treeEntry;
466+
}
467+
468+
static Tree CreateTree(params TreeEntry[] treeEntries)
469+
{
470+
var tree = Substitute.For<Tree>();
471+
var enumerator = treeEntries.ToList().GetEnumerator();
472+
tree.GetEnumerator().Returns(enumerator);
473+
return tree;
474+
}
475+
}
476+
477+
public class TheFindBlobShaForTextViewMethod
478+
{
479+
[TestCase(@"C:\Users\me\AppData\Local\Temp\TFSTemp\vctmp21996_181282.IOpenFromClipboardCommand.783ac965.cs", "783ac965")]
480+
[TestCase(@"\TFSTemp\File.12345678.ext", "12345678")]
481+
[TestCase(@"\TFSTemp\File.abcdefab.ext", "abcdefab")]
482+
[TestCase(@"\TFSTemp\.12345678.", "12345678")]
483+
[TestCase(@"\TFSTemp\File.ABCDEFAB.ext", null)]
484+
[TestCase(@"\TFSTemp\File.1234567.ext", null)]
485+
[TestCase(@"\TFSTemp\File.123456789.ext", null)]
486+
[TestCase(@"\TFSTemp\File.12345678.ext\\", null)]
487+
public void FindObjectishForTFSTempFile(string path, string expectObjectish)
488+
{
489+
var target = CreateGitHubContextService();
490+
491+
var objectish = target.FindObjectishForTFSTempFile(path);
492+
493+
Assert.That(objectish, Is.EqualTo(expectObjectish));
494+
}
495+
}
496+
385497
static GitHubContextService CreateGitHubContextService(string repositoryDir = null, IRepository repository = null)
386498
{
387499
var sp = Substitute.For<IGitHubServiceProvider>();

0 commit comments

Comments
 (0)