diff --git a/GitCommands/FileAssociatedIconProvider.cs b/GitCommands/FileAssociatedIconProvider.cs index ab307f3606c..760373387bd 100644 --- a/GitCommands/FileAssociatedIconProvider.cs +++ b/GitCommands/FileAssociatedIconProvider.cs @@ -51,7 +51,7 @@ public FileAssociatedIconProvider() /// public Icon Get(string workingDirectory, string relativeFilePath) { - var extension = Path.GetExtension(relativeFilePath); + var extension = PathUtil.GetExtension(relativeFilePath); if (string.IsNullOrWhiteSpace(extension)) { return null; @@ -68,7 +68,7 @@ public Icon Get(string workingDirectory, string relativeFilePath) // extensions from the registry and using p/invokes and WinAPI, which have // significantly higher maintenance overhead. - var fullPath = Path.Combine(workingDirectory, relativeFilePath); + var fullPath = PathUtil.Combine(workingDirectory, relativeFilePath); if (!_fileSystem.File.Exists(fullPath)) { tempFile = CreateTempFile(Path.GetFileName(fullPath)); @@ -117,4 +117,4 @@ internal void ResetCache() LoadedFileIcons.Clear(); } } -} \ No newline at end of file +} diff --git a/GitCommands/Git/GitItemStatusFileExtensionComparer.cs b/GitCommands/Git/GitItemStatusFileExtensionComparer.cs index 989bd6859fe..1092e3cf55a 100644 --- a/GitCommands/Git/GitItemStatusFileExtensionComparer.cs +++ b/GitCommands/Git/GitItemStatusFileExtensionComparer.cs @@ -28,8 +28,8 @@ public override int Compare(GitItemStatus x, GitItemStatus y) var lhsPath = GetPrimarySortingPath(x); var rhsPath = GetPrimarySortingPath(y); - var lhsExt = Path.GetExtension(lhsPath); - var rhsExt = Path.GetExtension(rhsPath); + var lhsExt = PathUtil.GetExtension(lhsPath); + var rhsExt = PathUtil.GetExtension(rhsPath); var comparisonResult = StringComparer.InvariantCulture.Compare(lhsExt, rhsExt); if (comparisonResult == 0) diff --git a/GitCommands/GitRevisionInfoProvider.cs b/GitCommands/GitRevisionInfoProvider.cs index d3ecd98688b..b7792c5930a 100644 --- a/GitCommands/GitRevisionInfoProvider.cs +++ b/GitCommands/GitRevisionInfoProvider.cs @@ -59,9 +59,7 @@ IEnumerable YieldSubItems() { if (subItem is GitItem gitItem) { - gitItem.FileName = Path.Combine( - basePath, - gitItem.FileName ?? string.Empty); + gitItem.FileName = PathUtil.Combine(basePath, gitItem.FileName) ?? string.Empty; } yield return subItem; diff --git a/GitCommands/PathUtil.cs b/GitCommands/PathUtil.cs index bb2663bf87e..d63944f9b52 100644 --- a/GitCommands/PathUtil.cs +++ b/GitCommands/PathUtil.cs @@ -132,6 +132,54 @@ public static string NormalizePath([NotNull] this string path) } } + /// + /// Wrapper for Path.Combine + /// + /// + /// Similar to the .NET Core 2.1 variant, except that null is returned if Windows + /// invalid characters (that may be accepted in Git or other filesystems) + /// are in the paths instead of a possible path (the OS or file system will throw + /// if the paths are invalid). + /// + /// initial part + /// second part + /// path if it can be combined, null otherwise + [CanBeNull] + public static string Combine(string path1, string path2) + { + try + { + return Path.Combine(path1, path2); + } + catch (ArgumentException) + { + return null; + } + } + + /// + /// Wrapper for Path.GetExtension + /// + /// + /// for motivation. + /// + /// path to check + /// path if it can be combined, empty otherwise + [NotNull] + public static string GetExtension(string path) + { + try + { + return Path.GetExtension(path); + } + catch (ArgumentException) + { + // This could return part after a '.' using string commands, + // but wait for .NET Core with this edge case + return string.Empty; + } + } + [NotNull] public static string Resolve([NotNull] string path, string relativePath = "") { @@ -223,7 +271,7 @@ public static bool TryFindFullPath([NotNull] string fileName, out string fullPat foreach (var path in EnvironmentPathsProvider.GetEnvironmentValidPaths()) { - fullPath = Path.Combine(path, fileName); + fullPath = Combine(path, fileName); if (File.Exists(fullPath)) { return true; @@ -251,7 +299,7 @@ public static bool TryFindShellPath([NotNull] string shell, out string shellPath return true; } - shellPath = Path.Combine(AppSettings.GitBinDir, shell); + shellPath = Combine(AppSettings.GitBinDir, shell); if (File.Exists(shellPath)) { return true; @@ -369,7 +417,7 @@ string FindFileInEnvVarFolder(string environmentVariable, string location, strin return null; } - var path = Path.Combine(envVarFolder, location); + var path = Combine(envVarFolder, location); if (!Directory.Exists(path)) { return null; @@ -380,7 +428,7 @@ string FindFileInEnvVarFolder(string environmentVariable, string location, strin string FindFile(string location, string fileName1) { - string fullName = Path.Combine(location, fileName1); + string fullName = Combine(location, fileName1); if (File.Exists(fullName)) { return fullName; diff --git a/GitUI/AutoCompletion/CommitAutoCompleteProvider.cs b/GitUI/AutoCompletion/CommitAutoCompleteProvider.cs index ed3b59674f0..c0a2bee044c 100644 --- a/GitUI/AutoCompletion/CommitAutoCompleteProvider.cs +++ b/GitUI/AutoCompletion/CommitAutoCompleteProvider.cs @@ -41,7 +41,7 @@ public async Task> GetAutoCompleteWordsAsync(Cance { cancellationToken.ThrowIfCancellationRequested(); - var regex = GetRegexForExtension(Path.GetExtension(file.Name)); + var regex = GetRegexForExtension(PathUtil.GetExtension(file.Name)); if (regex != null) { @@ -94,7 +94,7 @@ private static Regex GetRegexForExtension(string extension) private static IEnumerable ReadOrInitializeAutoCompleteRegexes() { - var path = Path.Combine(AppSettings.ApplicationDataPath.Value, "AutoCompleteRegexes.txt"); + var path = PathUtil.Combine(AppSettings.ApplicationDataPath.Value, "AutoCompleteRegexes.txt"); if (File.Exists(path)) { diff --git a/GitUI/CommandsDialogs/FormBrowse.cs b/GitUI/CommandsDialogs/FormBrowse.cs index e686ca26cc3..8f063318409 100644 --- a/GitUI/CommandsDialogs/FormBrowse.cs +++ b/GitUI/CommandsDialogs/FormBrowse.cs @@ -2015,6 +2015,12 @@ public static void CopyFullPathToClipboard(FileStatusList diffFiles, GitModule m var fileNames = new StringBuilder(); foreach (var item in diffFiles.SelectedItems) { + var path = PathUtil.Combine(module.WorkingDir, item.Item.Name); + if (string.IsNullOrWhiteSpace(path)) + { + continue; + } + // Only use append line when multiple items are selected. // This to make it easier to use the text from clipboard when 1 file is selected. if (fileNames.Length > 0) @@ -2022,7 +2028,7 @@ public static void CopyFullPathToClipboard(FileStatusList diffFiles, GitModule m fileNames.AppendLine(); } - fileNames.Append(Path.Combine(module.WorkingDir, item.Item.Name).ToNativePath()); + fileNames.Append(path.ToNativePath()); } ClipboardUtil.TrySetText(fileNames.ToString()); @@ -2423,7 +2429,12 @@ public static void OpenContainingFolder(FileStatusList diffFiles, GitModule modu foreach (var item in diffFiles.SelectedItems) { - string filePath = Path.Combine(module.WorkingDir, item.Item.Name.ToNativePath()); + string filePath = PathUtil.Combine(module.WorkingDir, item.Item.Name.ToNativePath()); + if (string.IsNullOrWhiteSpace(filePath)) + { + continue; + } + FormBrowseUtil.ShowFileOrParentFolderInFileExplorer(filePath); } } diff --git a/GitUI/CommandsDialogs/FormResolveConflicts.cs b/GitUI/CommandsDialogs/FormResolveConflicts.cs index a8addd5ccdd..7541500f323 100644 --- a/GitUI/CommandsDialogs/FormResolveConflicts.cs +++ b/GitUI/CommandsDialogs/FormResolveConflicts.cs @@ -314,17 +314,17 @@ private bool TryMergeWithScript(string fileName, string baseFileName, string loc try { - string extension = Path.GetExtension(fileName).ToLower(); + string extension = PathUtil.GetExtension(fileName).ToLower(); if (extension.Length <= 1) { return false; } - string dir = Path.Combine(Path.GetDirectoryName(Application.ExecutablePath), "Diff-Scripts").EnsureTrailingPathSeparator(); + string dir = PathUtil.Combine(Path.GetDirectoryName(Application.ExecutablePath), "Diff-Scripts").EnsureTrailingPathSeparator(); if (Directory.Exists(dir)) { if (_mergeScripts.TryGetValue(extension, out var mergeScript) && - File.Exists(Path.Combine(dir, mergeScript))) + File.Exists(PathUtil.Combine(dir, mergeScript))) { if (MessageBox.Show(this, string.Format(_uskUseCustomMergeScript.Text, mergeScript), _uskUseCustomMergeScriptCaption.Text, MessageBoxButtons.YesNo, MessageBoxIcon.Question) == diff --git a/GitUI/CommandsDialogs/RevisionFileTreeController.cs b/GitUI/CommandsDialogs/RevisionFileTreeController.cs index 46804251de4..c6b1d0b8e48 100644 --- a/GitUI/CommandsDialogs/RevisionFileTreeController.cs +++ b/GitUI/CommandsDialogs/RevisionFileTreeController.cs @@ -120,7 +120,8 @@ public void LoadChildren(IGitItem item, TreeNodeCollection nodes, ImageList.Imag case GitObjectType.Blob: { - var extension = Path.GetExtension(gitItem.FileName); + var extension = PathUtil.GetExtension(gitItem.FileName); + if (string.IsNullOrWhiteSpace(extension)) { continue; @@ -128,8 +129,8 @@ public void LoadChildren(IGitItem item, TreeNodeCollection nodes, ImageList.Imag if (!imageCollection.ContainsKey(extension)) { - // a little optimisation - initialise the first time it is required - workingDir = workingDir ?? _getWorkingDir(); + // lazy - initialise the first time used + workingDir ??= _getWorkingDir(); var fileIcon = _iconProvider.Get(workingDir, gitItem.FileName); if (fileIcon == null) diff --git a/GitUI/CommandsDialogs/SettingsDialog/Pages/ChecklistSettingsPage.cs b/GitUI/CommandsDialogs/SettingsDialog/Pages/ChecklistSettingsPage.cs index d4be07a89ef..56516c0a5a6 100644 --- a/GitUI/CommandsDialogs/SettingsDialog/Pages/ChecklistSettingsPage.cs +++ b/GitUI/CommandsDialogs/SettingsDialog/Pages/ChecklistSettingsPage.cs @@ -555,8 +555,8 @@ private bool CheckGitExtensionRegistrySettings() null))) { // Check if shell extensions are installed - string path32 = Path.Combine(AppSettings.GetInstallDir(), CommonLogic.GitExtensionsShellEx32Name); - string path64 = Path.Combine(AppSettings.GetInstallDir(), CommonLogic.GitExtensionsShellEx64Name); + string path32 = PathUtil.Combine(AppSettings.GetInstallDir(), CommonLogic.GitExtensionsShellEx32Name); + string path64 = PathUtil.Combine(AppSettings.GetInstallDir(), CommonLogic.GitExtensionsShellEx64Name); if (!File.Exists(path32) || (IntPtr.Size == 8 && !File.Exists(path64))) { RenderSettingSet(ShellExtensionsRegistered, ShellExtensionsRegistered_Fix, _shellExtNoInstalled.Text); diff --git a/GitUI/CommandsDialogs/SettingsDialog/Pages/FormChooseTranslation.cs b/GitUI/CommandsDialogs/SettingsDialog/Pages/FormChooseTranslation.cs index fad6435af46..59841233c61 100644 --- a/GitUI/CommandsDialogs/SettingsDialog/Pages/FormChooseTranslation.cs +++ b/GitUI/CommandsDialogs/SettingsDialog/Pages/FormChooseTranslation.cs @@ -36,7 +36,7 @@ protected override void OnLoad(EventArgs e) foreach (string translation in translations) { - var imagePath = Path.Combine(Translator.GetTranslationDir(), translation + ".gif"); + var imagePath = PathUtil.Combine(Translator.GetTranslationDir(), translation + ".gif"); if (File.Exists(imagePath)) { var image = Image.FromFile(imagePath); diff --git a/GitUI/CommandsDialogs/SettingsDialog/Pages/FormFixHome.cs b/GitUI/CommandsDialogs/SettingsDialog/Pages/FormFixHome.cs index 042d73eafa2..5e2c8d38545 100644 --- a/GitUI/CommandsDialogs/SettingsDialog/Pages/FormFixHome.cs +++ b/GitUI/CommandsDialogs/SettingsDialog/Pages/FormFixHome.cs @@ -64,7 +64,7 @@ private static bool IsFixHome() try { if (!string.IsNullOrEmpty(candidate) && - File.Exists(Path.Combine(candidate, ".gitconfig"))) + File.Exists(PathUtil.Combine(candidate, ".gitconfig"))) { return true; } @@ -138,7 +138,7 @@ private void LoadSettings() try { string userHomeDir = Environment.GetEnvironmentVariable("HOME", EnvironmentVariableTarget.User); - if (!string.IsNullOrEmpty(userHomeDir) && File.Exists(Path.Combine(userHomeDir, ".gitconfig"))) + if (!string.IsNullOrEmpty(userHomeDir) && File.Exists(PathUtil.Combine(userHomeDir, ".gitconfig"))) { MessageBox.Show(this, string.Format(_gitconfigFoundHome.Text, userHomeDir), "Information", MessageBoxButtons.OK, MessageBoxIcon.Information); defaultHome.Checked = true; @@ -156,7 +156,7 @@ private void LoadSettings() { var path = Environment.GetEnvironmentVariable("HOMEDRIVE") + Environment.GetEnvironmentVariable("HOMEPATH"); - if (!string.IsNullOrEmpty(path) && File.Exists(Path.Combine(path, ".gitconfig"))) + if (!string.IsNullOrEmpty(path) && File.Exists(PathUtil.Combine(path, ".gitconfig"))) { MessageBox.Show(this, string.Format(_gitconfigFoundHomedrive.Text, path), "Information", MessageBoxButtons.OK, MessageBoxIcon.Information); defaultHome.Checked = true; @@ -173,7 +173,7 @@ private void LoadSettings() try { var path = Environment.GetEnvironmentVariable("USERPROFILE"); - if (!string.IsNullOrEmpty(path) && File.Exists(Path.Combine(path, ".gitconfig"))) + if (!string.IsNullOrEmpty(path) && File.Exists(PathUtil.Combine(path, ".gitconfig"))) { MessageBox.Show(this, string.Format(_gitconfigFoundUserprofile.Text, path), "Information", MessageBoxButtons.OK, MessageBoxIcon.Information); userprofileHome.Checked = true; @@ -190,7 +190,7 @@ private void LoadSettings() try { var path = Environment.GetFolderPath(Environment.SpecialFolder.Personal); - if (!string.IsNullOrEmpty(path) && File.Exists(Path.Combine(path, ".gitconfig"))) + if (!string.IsNullOrEmpty(path) && File.Exists(PathUtil.Combine(path, ".gitconfig"))) { MessageBox.Show(this, string.Format(_gitconfigFoundPersonalFolder.Text, Environment.GetFolderPath(Environment.SpecialFolder.Personal)), "Information", MessageBoxButtons.OK, MessageBoxIcon.Information); diff --git a/GitUI/Editor/FileViewer.cs b/GitUI/Editor/FileViewer.cs index b599fc6cbeb..1223f40b098 100644 --- a/GitUI/Editor/FileViewer.cs +++ b/GitUI/Editor/FileViewer.cs @@ -567,7 +567,7 @@ public Task ViewGitItemAsync(GitItemStatus file, [CanBeNull] Action openWithDiff { // File system access for other than Worktree, // to handle that git-status does not detect details for untracked (git-diff --no-index will not give info) - var fullPath = Path.Combine(Module.WorkingDir, file.Name); + var fullPath = PathUtil.Combine(Module.WorkingDir, file.Name); if (Directory.Exists(fullPath) && GitModule.IsValidGitWorkingDir(fullPath)) { isSubmodule = true; diff --git a/Plugins/Statistics/GitStatistics/LineCounter.cs b/Plugins/Statistics/GitStatistics/LineCounter.cs index 84dbd90585f..50d1c1cceea 100644 --- a/Plugins/Statistics/GitStatistics/LineCounter.cs +++ b/Plugins/Statistics/GitStatistics/LineCounter.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using GitCommands; namespace GitStatistics { @@ -53,7 +54,7 @@ IEnumerable GetFiles() { foreach (var file in filesToCheck) { - if (extensions.Contains(Path.GetExtension(file))) + if (extensions.Contains(PathUtil.GetExtension(file))) { FileInfo fileInfo; try @@ -98,4 +99,4 @@ void AddFile(FileInfo file) } } } -} \ No newline at end of file +} diff --git a/UnitTests/GitCommands.Tests/Helpers/PathUtilTest.cs b/UnitTests/GitCommands.Tests/Helpers/PathUtilTest.cs index 31d49b614f4..c25741f8706 100644 --- a/UnitTests/GitCommands.Tests/Helpers/PathUtilTest.cs +++ b/UnitTests/GitCommands.Tests/Helpers/PathUtilTest.cs @@ -188,6 +188,24 @@ public void NormalizePath(string path, string expected) PathUtil.NormalizePath(path).Should().Be(expected); } + [TestCase(@"C:\work\t.txt", "whatever", @"C:\work\t.txt\whatever")] + [TestCase(@"C:\wor""k\t.txt", "whatever", null)] + [TestCase(@"\\WSL$\Ubuntu\home\jack\.\work\", "whatever", @"\\WSL$\Ubuntu\home\jack\.\work\whatever")] + public void Combine(string path1, string path2, string expected) + { + PathUtil.Combine(path1, path2).Should().Be(expected); + } + + [TestCase(@"C:\work\t.txt", @".txt")] + [TestCase(@"C:\work\t.", @"")] + [TestCase(@"work/t.bmp", @".bmp")] + [TestCase(@"work""/t.bmp", @"")] + [TestCase(@"\\WSL$\Ubuntu\home\jack\.\work", @"")] + public void GetExtension(string path, string expected) + { + PathUtil.GetExtension(path).Should().Be(expected); + } + [TestCase(@"C:\WORK\GitExtensions\", @"C:\WORK\GitExtensions\")] [TestCase(@"\\my-pc\Work\GitExtensions\", @"\\my-pc\Work\GitExtensions\")] [TestCase(@"\\wsl$\Ubuntu\home\jack\work\", @"\\wsl$\Ubuntu\home\jack\work\")]