55using System . Linq ;
66using System . Text ;
77using LibGit2Sharp ;
8+ using Microsoft . VisualStudio ;
89using Microsoft . VisualStudio . Shell ;
910using Microsoft . VisualStudio . Text ;
1011using __VSDIFFSERVICEOPTIONS = Microsoft . VisualStudio . Shell . Interop . __VSDIFFSERVICEOPTIONS ;
12+ using __VSENUMPROJFLAGS = Microsoft . VisualStudio . Shell . Interop . __VSENUMPROJFLAGS ;
13+ using IEnumHierarchies = Microsoft . VisualStudio . Shell . Interop . IEnumHierarchies ;
1114using IVsDifferenceService = Microsoft . VisualStudio . Shell . Interop . IVsDifferenceService ;
15+ using IVsHierarchy = Microsoft . VisualStudio . Shell . Interop . IVsHierarchy ;
16+ using IVsProject = Microsoft . VisualStudio . Shell . Interop . IVsProject ;
17+ using IVsSolution = Microsoft . VisualStudio . Shell . Interop . IVsSolution ;
1218using SVsDifferenceService = Microsoft . VisualStudio . Shell . Interop . SVsDifferenceService ;
19+ using SVsSolution = Microsoft . VisualStudio . Shell . Interop . SVsSolution ;
1320
1421namespace GitDiffMargin . Git
1522{
@@ -26,10 +33,10 @@ public GitCommands(SVsServiceProvider serviceProvider)
2633
2734 private const int ContextLines = 0 ;
2835
29- public IEnumerable < HunkRangeInfo > GetGitDiffFor ( ITextDocument textDocument , ITextSnapshot snapshot )
36+ public IEnumerable < HunkRangeInfo > GetGitDiffFor ( ITextDocument textDocument , string originalPath , ITextSnapshot snapshot )
3037 {
3138 var filename = textDocument . FilePath ;
32- var repositoryPath = GetGitRepository ( Path . GetFullPath ( filename ) ) ;
39+ var repositoryPath = GetGitRepository ( Path . GetFullPath ( filename ) , ref originalPath ) ;
3340 if ( repositoryPath == null )
3441 yield break ;
3542
@@ -39,7 +46,7 @@ public IEnumerable<HunkRangeInfo> GetGitDiffFor(ITextDocument textDocument, ITex
3946 if ( workingDirectory == null )
4047 yield break ;
4148
42- var retrieveStatus = repo . RetrieveStatus ( filename ) ;
49+ var retrieveStatus = repo . RetrieveStatus ( originalPath ) ;
4350 if ( retrieveStatus == FileStatus . Nonexistent )
4451 {
4552 // this occurs if a file within the repository itself (not the working copy) is opened.
@@ -52,9 +59,13 @@ public IEnumerable<HunkRangeInfo> GetGitDiffFor(ITextDocument textDocument, ITex
5259 yield break ;
5360 }
5461
55- if ( retrieveStatus == FileStatus . Unaltered && ! textDocument . IsDirty )
62+ if ( retrieveStatus == FileStatus . Unaltered
63+ && ! textDocument . IsDirty
64+ && Path . GetFullPath ( filename ) == originalPath )
5665 {
57- // truly unaltered
66+ // Truly unaltered. The `IsDirty` check isn't valid for cases where the textDocument is a view of a
67+ // temporary copy of the file, since the temporary copy could have been made using unsaved changes
68+ // and still appear "not dirty".
5869 yield break ;
5970 }
6071
@@ -63,7 +74,7 @@ public IEnumerable<HunkRangeInfo> GetGitDiffFor(ITextDocument textDocument, ITex
6374
6475 using ( var currentContent = new MemoryStream ( content ) )
6576 {
66- var relativeFilepath = filename ;
77+ var relativeFilepath = originalPath ;
6778 if ( relativeFilepath . StartsWith ( workingDirectory , StringComparison . OrdinalIgnoreCase ) )
6879 relativeFilepath = relativeFilepath . Substring ( workingDirectory . Length ) ;
6980
@@ -139,20 +150,19 @@ private static byte[] GetCompleteContent(ITextDocument textDocument, ITextSnapsh
139150 return completeContent ;
140151 }
141152
142- public void StartExternalDiff ( ITextDocument textDocument )
153+ public void StartExternalDiff ( ITextDocument textDocument , string originalPath )
143154 {
144155 if ( textDocument == null || string . IsNullOrEmpty ( textDocument . FilePath ) ) return ;
145156
146157 var filename = textDocument . FilePath ;
147-
148- var repositoryPath = GetGitRepository ( Path . GetFullPath ( filename ) ) ;
158+ var repositoryPath = GetGitRepository ( Path . GetFullPath ( filename ) , ref originalPath ) ;
149159 if ( repositoryPath == null )
150160 return ;
151161
152162 using ( var repo = new Repository ( repositoryPath ) )
153163 {
154164 string workingDirectory = repo . Info . WorkingDirectory ;
155- string relativePath = Path . GetFullPath ( filename ) ;
165+ string relativePath = originalPath ;
156166 if ( relativePath . StartsWith ( workingDirectory , StringComparison . OrdinalIgnoreCase ) )
157167 relativePath = relativePath . Substring ( workingDirectory . Length ) ;
158168
@@ -174,7 +184,7 @@ public void StartExternalDiff(ITextDocument textDocument)
174184 IVsDifferenceService differenceService = _serviceProvider . GetService ( typeof ( SVsDifferenceService ) ) as IVsDifferenceService ;
175185 string leftFileMoniker = tempFileName ;
176186 // The difference service will automatically load the text from the file open in the editor, even if
177- // it has changed.
187+ // it has changed. Don't use the original path here.
178188 string rightFileMoniker = filename ;
179189
180190 string actualFilename = objectName ;
@@ -207,7 +217,7 @@ public void StartExternalDiff(ITextDocument textDocument)
207217 leftLabel = string . Format ( "{0}@{1}" , objectName , repo . Head . Tip . Sha . Substring ( 0 , 7 ) ) ;
208218 }
209219
210- string rightLabel = filename ;
220+ string rightLabel = originalPath ;
211221
212222 string inlineLabel = null ;
213223 string roles = null ;
@@ -220,26 +230,63 @@ public void StartExternalDiff(ITextDocument textDocument)
220230 }
221231
222232 /// <inheritdoc/>
223- public bool IsGitRepository ( string path )
233+ public bool TryGetOriginalPath ( string path , out string originalPath )
224234 {
225- return GetGitRepository ( path ) != null ;
235+ originalPath = null ;
236+ if ( GetGitRepository ( path , ref originalPath ) == null )
237+ {
238+ originalPath = path ;
239+ return false ;
240+ }
241+
242+ return true ;
226243 }
227244
228245 /// <inheritdoc/>
229- public string GetGitRepository ( string path )
246+ public bool IsGitRepository ( string path , string originalPath )
247+ {
248+ return GetGitRepository ( path , originalPath ) != null ;
249+ }
250+
251+ /// <inheritdoc/>
252+ public string GetGitRepository ( string path , string originalPath )
253+ {
254+ if ( originalPath == null )
255+ throw new ArgumentNullException ( nameof ( originalPath ) ) ;
256+
257+ return GetGitRepository ( path , ref originalPath ) ;
258+ }
259+
260+ private string GetGitRepository ( string path , ref string originalPath )
230261 {
231- if ( ! Directory . Exists ( path ) && ! File . Exists ( path ) )
262+ if ( originalPath == null )
263+ {
264+ originalPath = path ;
265+ if ( ! Directory . Exists ( path ) && ! File . Exists ( path ) )
266+ return null ;
267+
268+ var discoveredPath = Repository . Discover ( Path . GetFullPath ( path ) ) ;
269+ if ( discoveredPath != null )
270+ return discoveredPath ;
271+
272+ originalPath = AdjustPath ( path ) ;
273+ if ( originalPath == path )
274+ return null ;
275+ }
276+
277+ if ( ! Directory . Exists ( path ) && ! File . Exists ( originalPath ) )
232278 return null ;
233279
234- var discoveredPath = Repository . Discover ( Path . GetFullPath ( path ) ) ;
235- // https://github.com/libgit2/libgit2sharp/issues/818#issuecomment-54760613
236- return discoveredPath ;
280+ return Repository . Discover ( Path . GetFullPath ( originalPath ) ) ;
237281 }
238282
239283 /// <inheritdoc/>
240- public string GetGitWorkingCopy ( string path )
284+ public string GetGitWorkingCopy ( string path , string originalPath )
241285 {
242- var repositoryPath = GetGitRepository ( path ) ;
286+ if ( originalPath == null )
287+ throw new ArgumentNullException ( nameof ( originalPath ) ) ;
288+
289+ var repositoryPath = GetGitRepository ( path , originalPath ) ;
243290 if ( repositoryPath == null )
244291 return null ;
245292
@@ -282,5 +329,87 @@ static bool HasPreamble(string file, Encoding encoding)
282329
283330 return true ;
284331 }
332+
333+ private string AdjustPath ( string fullPath )
334+ {
335+ // Right now the only adjustment is for CPS-based project systems which open their project files in a
336+ // temporary location. There are several of these, such as .csproj, .vbproj, .shproj, and .fsproj, and more
337+ // could appear in the future.
338+ if ( ! fullPath . EndsWith ( "proj" , StringComparison . Ordinal ) )
339+ {
340+ return fullPath ;
341+ }
342+
343+ // CPS will open the file in %TEMP%\{random name}\{ProjectFileName}
344+ string directoryName = Path . GetDirectoryName ( fullPath ) ;
345+ if ( string . IsNullOrEmpty ( directoryName ) )
346+ return fullPath ;
347+
348+ directoryName = Path . GetDirectoryName ( directoryName ) ;
349+ if ( ! Path . GetTempPath ( ) . Equals ( directoryName + Path . DirectorySeparatorChar , StringComparison . OrdinalIgnoreCase ) )
350+ return fullPath ;
351+
352+ IVsSolution solution = _serviceProvider . GetService ( typeof ( SVsSolution ) ) as IVsSolution ;
353+ if ( solution == null )
354+ return fullPath ;
355+
356+ if ( ! ErrorHandler . Succeeded ( solution . GetProjectEnum ( ( uint ) __VSENUMPROJFLAGS . EPF_LOADEDINSOLUTION , Guid . Empty , out IEnumHierarchies ppenum ) )
357+ || ppenum == null )
358+ {
359+ return fullPath ;
360+ }
361+
362+ List < string > projectFiles = new List < string > ( ) ;
363+ IVsHierarchy [ ] hierarchies = new IVsHierarchy [ 1 ] ;
364+ while ( true )
365+ {
366+ int hr = ppenum . Next ( ( uint ) hierarchies . Length , hierarchies , out uint fetched ) ;
367+ if ( ! ErrorHandler . Succeeded ( hr ) )
368+ return fullPath ;
369+
370+ for ( uint i = 0 ; i < fetched ; i ++ )
371+ {
372+ if ( ! ( hierarchies [ 0 ] is IVsProject project ) )
373+ continue ;
374+
375+ if ( ! ErrorHandler . Succeeded ( project . GetMkDocument ( ( uint ) VSConstants . VSITEMID . Root , out string projectFilePath ) ) )
376+ continue ;
377+
378+ if ( ! Path . GetFileName ( projectFilePath ) . Equals ( Path . GetFileName ( fullPath ) , StringComparison . Ordinal ) )
379+ continue ;
380+
381+ projectFiles . Add ( projectFilePath ) ;
382+ }
383+
384+ if ( hr != VSConstants . S_OK )
385+ {
386+ // No more projects
387+ break ;
388+ }
389+ }
390+
391+ switch ( projectFiles . Count )
392+ {
393+ case 0 :
394+ // No matching project file found in solution
395+ return fullPath ;
396+
397+ case 1 :
398+ // Exactly one matching project file found in solution
399+ return projectFiles [ 0 ] ;
400+
401+ default :
402+ // Multiple project files found in solution; try to find one with a matching file size
403+ long desiredSize = new FileInfo ( fullPath ) . Length ;
404+ foreach ( var projectFilePath in projectFiles )
405+ {
406+ if ( File . Exists ( projectFilePath ) && new FileInfo ( projectFilePath ) . Length == desiredSize )
407+ return projectFilePath ;
408+ }
409+
410+ // No results found
411+ return fullPath ;
412+ }
413+ }
285414 }
286415}
0 commit comments