using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.RegularExpressions; using GitCommands.Utils; using JetBrains.Annotations; namespace GitCommands { public static class PathUtil { private static readonly IEnvironmentAbstraction EnvironmentAbstraction = new EnvironmentAbstraction(); private static readonly IEnvironmentPathsProvider EnvironmentPathsProvider = new EnvironmentPathsProvider(EnvironmentAbstraction); public static readonly char PosixDirectorySeparatorChar = '/'; public static readonly char NativeDirectorySeparatorChar = Path.DirectorySeparatorChar; /// <summary>Replaces native path separator with posix path separator.</summary> [NotNull] public static string ToPosixPath([NotNull] this string path) { return path.Replace(NativeDirectorySeparatorChar, PosixDirectorySeparatorChar); } /// <summary>Replaces '\' with '/'.</summary> [NotNull] public static string ToNativePath([NotNull] this string path) { return path.Replace(PosixDirectorySeparatorChar, NativeDirectorySeparatorChar); } /// <summary> /// Removes any trailing path separator character from the end of <paramref name="dirPath"/>. /// </summary> [ContractAnnotation("dirPath:null=>null")] [ContractAnnotation("dirPath:notnull=>notnull")] public static string RemoveTrailingPathSeparator([CanBeNull] this string dirPath) { if (dirPath?.Length > 0 && (dirPath[dirPath.Length - 1] == NativeDirectorySeparatorChar || dirPath[dirPath.Length - 1] == PosixDirectorySeparatorChar)) { return dirPath.Substring(0, dirPath.Length - 1); } return dirPath; } /// <summary> /// Code guideline: "A directory path should always end with / or \. /// Better use Path.Combine instead of Setting.PathSeparator" /// /// This method can be used to add (or keep) a trailing path separator character to a directory path. /// </summary> [ContractAnnotation("dirPath:null=>null")] [ContractAnnotation("dirPath:notnull=>notnull")] public static string EnsureTrailingPathSeparator([CanBeNull] this string dirPath) { if (!dirPath.IsNullOrEmpty() && dirPath[dirPath.Length - 1] != NativeDirectorySeparatorChar && dirPath[dirPath.Length - 1] != PosixDirectorySeparatorChar) { dirPath += NativeDirectorySeparatorChar; } return dirPath; } public static bool IsLocalFile([NotNull] string fileName) { return !Regex.IsMatch(fileName, @"^(\w+):\/\/([\S]+)"); } /// <summary> /// A naive way to check whether the given path is a URL by checking /// whether it starts with either 'http', 'ssh' or 'git'. /// </summary> /// <param name="path">A path to check.</param> /// <returns><see langword="true"/> if the given path starts with 'http', 'ssh' or 'git'; otherwise <see langword="false"/>.</returns> [ContractAnnotation("path:null=>false")] [Pure] public static bool IsUrl(string path) { return !string.IsNullOrEmpty(path) && (path.StartsWith("http", StringComparison.CurrentCultureIgnoreCase) || path.StartsWith("git", StringComparison.CurrentCultureIgnoreCase) || path.StartsWith("ssh", StringComparison.CurrentCultureIgnoreCase)); } [NotNull] public static string GetFileName([NotNull] string fileName) { var pathSeparators = new[] { NativeDirectorySeparatorChar, PosixDirectorySeparatorChar }; var pos = fileName.LastIndexOfAny(pathSeparators); if (pos != -1) { fileName = fileName.Substring(pos + 1); } return fileName; } [NotNull] public static string GetDirectoryName([NotNull] string fileName) { var pathSeparators = new[] { NativeDirectorySeparatorChar, PosixDirectorySeparatorChar }; var pos = fileName.LastIndexOfAny(pathSeparators); if (pos != -1) { if (pos == 0 && fileName[0] == PosixDirectorySeparatorChar) { return fileName.Length == 1 ? "" : PosixDirectorySeparatorChar.ToString(); } fileName = fileName.Substring(0, pos); } if (fileName.Length == 2 && char.IsLetter(fileName[0]) && fileName[1] == Path.VolumeSeparatorChar) { return ""; } return fileName; } [ContractAnnotation("=>false,posixPath:null")] [ContractAnnotation("=>true,posixPath:notnull")] public static bool TryConvertWindowsPathToPosix([NotNull] string path, out string posixPath) { var directoryInfo = new DirectoryInfo(path); if (!directoryInfo.Exists) { posixPath = null; return false; } posixPath = "/" + directoryInfo.FullName.ToPosixPath().Remove(1, 1); return true; } [NotNull] public static string GetRepositoryName([CanBeNull] string repositoryUrl) { string name = ""; if (repositoryUrl != null) { const string standardRepositorySuffix = ".git"; string path = repositoryUrl.TrimEnd('\\', '/'); if (path.EndsWith(standardRepositorySuffix)) { path = path.Substring(0, path.Length - standardRepositorySuffix.Length); } if (path.Contains("\\") || path.Contains("/")) { name = path.Substring(path.LastIndexOfAny(new[] { '\\', '/' }) + 1); } } return name; } [ContractAnnotation("=>false,fullPath:null")] [ContractAnnotation("=>true,fullPath:notnull")] public static bool TryFindFullPath([NotNull] string fileName, out string fullPath) { try { if (File.Exists(fileName)) { fullPath = Path.GetFullPath(fileName); return true; } foreach (var path in EnvironmentPathsProvider.GetEnvironmentValidPaths()) { fullPath = Path.Combine(path, fileName); if (File.Exists(fullPath)) { return true; } } } catch { // do nothing } fullPath = null; return false; } [ContractAnnotation("=>false,shellPath:null")] [ContractAnnotation("=>true,shellPath:notnull")] public static bool TryFindShellPath([NotNull] string shell, out string shellPath) { try { shellPath = Path.Combine(EnvironmentAbstraction.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "Git", shell); if (File.Exists(shellPath)) { return true; } shellPath = Path.Combine(AppSettings.GitBinDir, shell); if (File.Exists(shellPath)) { return true; } if (TryFindFullPath(shell, out shellPath)) { return true; } } catch { // do nothing } shellPath = null; return false; } [NotNull] public static string GetDisplayPath([NotNull] string path) { // TODO verify whether the user profile contains forwards/backwards slashes on other platforms var userProfile = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); var comparison = EnvUtils.RunningOnWindows() ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal; if (TryGetExactPath(path, out var exactPath)) { path = exactPath; } if (path.StartsWith(userProfile, comparison)) { var length = path.Length - userProfile.Length; if (path.EndsWith("/") || path.EndsWith("\\")) { length--; } return $"~{path.Substring(userProfile.Length, length)}"; } return path; } /// <summary> /// Gets the exact case used on the file system for an existing file or directory. /// </summary> /// <param name="path">A relative or absolute path.</param> /// <param name="exactPath">The full path using the correct case if the path exists. Otherwise, null.</param> /// <returns>True if the exact path was found. False otherwise.</returns> /// <remarks> /// This supports drive-lettered paths and UNC paths, but a UNC root /// will be returned in lowercase (e.g., \\server\share). /// </remarks> [ContractAnnotation("=>false,exactPath:null")] [ContractAnnotation("=>true,exactPath:notnull")] [ContractAnnotation("path:null=>false,exactPath:null")] public static bool TryGetExactPath(string path, out string exactPath) { if (!File.Exists(path) && !Directory.Exists(path)) { exactPath = null; return false; } var directory = new DirectoryInfo(path); var parts = new List<string>(); var parentDirectory = directory.Parent; while (parentDirectory != null) { var entry = parentDirectory.EnumerateFileSystemInfos(directory.Name).First(); parts.Add(entry.Name); directory = parentDirectory; parentDirectory = directory.Parent; } // Handle the root part (i.e., drive letter or UNC \\server\share). var root = directory.FullName; parts.Add(root.Contains(':') ? root.ToUpper() : root.ToLower()); parts.Reverse(); exactPath = Path.Combine(parts.ToArray()); return true; } [CanBeNull] public static string GetFileExtension(string fileName) { var index = fileName.LastIndexOf('.'); if (index != -1) { return fileName.Substring(index + 1); } return null; } [NotNull, ItemNotNull] public static IEnumerable<string> FindAncestors([NotNull] string path) { path = path.RemoveTrailingPathSeparator(); if (string.IsNullOrWhiteSpace(path)) { yield break; } while (true) { path = Path.GetDirectoryName(path); if (string.IsNullOrEmpty(path)) { yield break; } yield return path.EnsureTrailingPathSeparator(); } } } }