-
Notifications
You must be signed in to change notification settings - Fork 100
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Skyline: Create ImageComparer tool as an easier way to compare modifi… (
#3296) * Skyline: Create ImageComparer tool as an easier way to compare modified tutorial screenshots in Git
- Loading branch information
1 parent
5948bd0
commit b21b512
Showing
31 changed files
with
2,540 additions
and
2 deletions.
There are no files selected for viewing
33 changes: 33 additions & 0 deletions
33
pwiz_tools/Skyline/Executables/DevTools/ImageComparer/App.config
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
<?xml version="1.0" encoding="utf-8" ?> | ||
<configuration> | ||
<configSections> | ||
<sectionGroup name="userSettings" type="System.Configuration.UserSettingsGroup, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" > | ||
<section name="ImageComparer.Properties.Settings" type="System.Configuration.ClientSettingsSection, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" allowExeDefinition="MachineToLocalUser" requirePermission="false" /> | ||
</sectionGroup> | ||
</configSections> | ||
<startup> | ||
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" /> | ||
</startup> | ||
<userSettings> | ||
<ImageComparer.Properties.Settings> | ||
<setting name="FormLocation" serializeAs="String"> | ||
<value>0, 0</value> | ||
</setting> | ||
<setting name="FormSize" serializeAs="String"> | ||
<value>0, 0</value> | ||
</setting> | ||
<setting name="FormMaximized" serializeAs="String"> | ||
<value>False</value> | ||
</setting> | ||
<setting name="ManualSize" serializeAs="String"> | ||
<value>False</value> | ||
</setting> | ||
<setting name="OldImageSource" serializeAs="String"> | ||
<value>0</value> | ||
</setting> | ||
<setting name="LastOpenFolder" serializeAs="String"> | ||
<value /> | ||
</setting> | ||
</ImageComparer.Properties.Settings> | ||
</userSettings> | ||
</configuration> |
145 changes: 145 additions & 0 deletions
145
pwiz_tools/Skyline/Executables/DevTools/ImageComparer/FileSaver.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,145 @@ | ||
/* | ||
* Original author: Nicholas Shulman <nicksh .at. u.washington.edu>, | ||
* MacCoss Lab, Department of Genome Sciences, UW | ||
* | ||
* Copyright 2024 University of Washington - Seattle, WA | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
using System; | ||
using System.Diagnostics; | ||
using System.IO; | ||
using System.Runtime.InteropServices; | ||
using System.Text; | ||
|
||
namespace ImageComparer | ||
{ | ||
public sealed class FileSaver : IDisposable | ||
{ | ||
public const string TEMP_PREFIX = "~SK"; | ||
/// <summary> | ||
/// Construct an instance of <see cref="FileSaver"/> to manage saving to a temporary | ||
/// file, and then renaming to the final destination. | ||
/// </summary> | ||
/// <param name="fileName">File path to the final destination</param> | ||
/// <throws>IOException</throws> | ||
public FileSaver(string fileName) | ||
{ | ||
RealName = Path.GetFullPath(fileName); | ||
|
||
string dirName = Path.GetDirectoryName(RealName)!; | ||
string tempName = GetTempFileName(dirName, TEMP_PREFIX); | ||
// If the directory name is returned, then starting path was bogus. | ||
if (!Equals(dirName, tempName)) | ||
SafeName = tempName; | ||
} | ||
|
||
public string SafeName { get; private set; } | ||
|
||
public string RealName { get; private set; } | ||
|
||
public bool Commit() | ||
{ | ||
// This is where the file that got written is renamed to the desired file. | ||
// Dispose() will do any necessary temporary file clean-up. | ||
|
||
if (string.IsNullOrEmpty(SafeName)) | ||
return false; | ||
Commit(SafeName, RealName); | ||
Dispose(); | ||
|
||
return true; | ||
} | ||
|
||
private static void Commit(string pathTemp, string pathDestination) | ||
{ | ||
try | ||
{ | ||
string backupFile = GetBackupFileName(pathDestination); | ||
File.Delete(backupFile); | ||
|
||
// First try replacing the destination file, if it exists | ||
if (File.Exists(pathDestination)) | ||
{ | ||
File.Replace(pathTemp, pathDestination, backupFile, true); | ||
File.Delete(backupFile); | ||
return; | ||
} | ||
} | ||
catch (FileNotFoundException) | ||
{ | ||
} | ||
|
||
// Or just move, if it does not. | ||
File.Move(pathTemp, pathDestination); | ||
} | ||
|
||
private static string GetBackupFileName(string pathDestination) | ||
{ | ||
string backupFile = FileSaver.TEMP_PREFIX + Path.GetFileName(pathDestination) + @".bak"; | ||
string dirName = Path.GetDirectoryName(pathDestination)!; | ||
if (!string.IsNullOrEmpty(dirName)) | ||
backupFile = Path.Combine(dirName, backupFile); | ||
// CONSIDER: Handle failure by trying a different name, or use a true temporary name? | ||
File.Delete(backupFile); | ||
return backupFile; | ||
} | ||
|
||
|
||
public void Dispose() | ||
{ | ||
// Get rid of the temporary file, if it still exists. | ||
if (!string.IsNullOrEmpty(SafeName)) | ||
{ | ||
try | ||
{ | ||
if (File.Exists(SafeName)) | ||
File.Delete(SafeName); | ||
} | ||
catch (Exception e) | ||
{ | ||
Trace.TraceWarning(@"Exception in FileSaver.Dispose: {0}", e); | ||
} | ||
// Make sure any further calls to Dispose() do nothing. | ||
SafeName = null; | ||
} | ||
} | ||
public string GetTempFileName(string basePath, string prefix) | ||
{ | ||
return GetTempFileName(basePath, prefix, 0); | ||
} | ||
|
||
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)] | ||
static extern uint GetTempFileName(string lpPathName, string lpPrefixString, | ||
uint uUnique, [Out] StringBuilder lpTempFileName); | ||
|
||
private static string GetTempFileName(string basePath, string prefix, uint unique) | ||
{ | ||
// 260 is MAX_PATH in Win32 windows.h header | ||
// 'sb' needs >0 size else GetTempFileName throws IndexOutOfRangeException. 260 is the most you'd want. | ||
StringBuilder sb = new StringBuilder(260); | ||
|
||
Directory.CreateDirectory(basePath); | ||
uint result = GetTempFileName(basePath, prefix, unique, sb); | ||
if (result == 0) | ||
{ | ||
var lastWin32Error = Marshal.GetLastWin32Error(); | ||
throw new IOException(string.Format("Error {0} GetTempFileName({1}, {2}, {3})", lastWin32Error, | ||
basePath, prefix, unique)); | ||
} | ||
|
||
return sb.ToString(); | ||
} | ||
} | ||
} |
174 changes: 174 additions & 0 deletions
174
pwiz_tools/Skyline/Executables/DevTools/ImageComparer/GitFileHelper.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,174 @@ | ||
/* | ||
* Original author: Brendan MacLean <brendanx .at. uw.edu>, | ||
* MacCoss Lab, Department of Genome Sciences, UW | ||
* | ||
* Copyright 2024 University of Washington - Seattle, WA | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Diagnostics; | ||
using System.IO; | ||
|
||
namespace ImageComparer | ||
{ | ||
internal static class GitFileHelper | ||
{ | ||
/// <summary> | ||
/// Retrieves the committed binary content of a file in a Git repository. | ||
/// </summary> | ||
/// <param name="fullPath">The fully qualified path of the file.</param> | ||
/// <returns>The committed binary content of the file.</returns> | ||
public static byte[] GetGitFileBinaryContent(string fullPath) | ||
{ | ||
return RunGitCommand(GetPathInfo(fullPath), "show HEAD:{RelativePath}", process => | ||
{ | ||
using var memoryStream = new MemoryStream(); | ||
process.StandardOutput.BaseStream.CopyTo(memoryStream); | ||
return memoryStream.ToArray(); | ||
}); | ||
} | ||
|
||
/// <summary> | ||
/// Gets a list of changed file paths under a specific directory. | ||
/// </summary> | ||
/// <param name="directoryPath">The fully qualified directory path.</param> | ||
/// <returns>An enumerable of file paths that have been modified, added, or deleted.</returns> | ||
public static IEnumerable<string> GetChangedFilePaths(string directoryPath) | ||
{ | ||
var output = RunGitCommand(GetPathInfo(directoryPath), "status --porcelain \"{RelativePath}\""); | ||
|
||
using var reader = new StringReader(output); | ||
while (reader.ReadLine() is { } line) | ||
{ | ||
// 'git status --porcelain' format: XY path/to/file | ||
var filePath = line.Substring(3).Replace('/', Path.DirectorySeparatorChar); | ||
yield return Path.Combine(GetPathInfo(directoryPath).Root, filePath); | ||
} | ||
} | ||
|
||
/// <summary> | ||
/// Reverts a file to its state in the HEAD commit. | ||
/// </summary> | ||
/// <param name="fullPath">The fully qualified path of the file to revert.</param> | ||
public static void RevertFileToHead(string fullPath) | ||
{ | ||
RunGitCommand(GetPathInfo(fullPath), "checkout HEAD -- \"{RelativePath}\""); | ||
} | ||
|
||
/// <summary> | ||
/// Executes a Git command in the specified repository and returns the standard output as a string. | ||
/// </summary> | ||
/// <param name="pathInfo">The PathInfo object for the target path.</param> | ||
/// <param name="commandTemplate">The Git command template with placeholders (e.g., {RelativePath}).</param> | ||
/// <param name="processOutput">A function that takes a running process and returns the necessary output</param> | ||
/// <returns>Output of type T from the process.</returns> | ||
private static T RunGitCommand<T>(PathInfo pathInfo, string commandTemplate, Func<Process, T> processOutput) | ||
{ | ||
var command = commandTemplate.Replace("{RelativePath}", pathInfo.RelativePath); | ||
var processInfo = new ProcessStartInfo("git", command) | ||
{ | ||
RedirectStandardOutput = true, | ||
RedirectStandardError = true, | ||
UseShellExecute = false, | ||
CreateNoWindow = true, | ||
WorkingDirectory = pathInfo.Root | ||
}; | ||
|
||
using var process = new Process(); | ||
process.StartInfo = processInfo; | ||
process.Start(); | ||
|
||
var output = processOutput(process); | ||
var error = process.StandardError.ReadToEnd(); | ||
process.WaitForExit(); | ||
|
||
if (process.ExitCode != 0) | ||
{ | ||
throw new Exception($"Git error: {error}"); | ||
} | ||
|
||
return output; | ||
} | ||
|
||
private static string RunGitCommand(PathInfo pathInfo, string commandTemplate) | ||
{ | ||
return RunGitCommand(pathInfo, commandTemplate, process => process.StandardOutput.ReadToEnd()); | ||
} | ||
|
||
/// <summary> | ||
/// Retrieves the root directory of the Git repository containing the given directory. | ||
/// </summary> | ||
/// <param name="startPath">The directory or file path to start searching from.</param> | ||
/// <returns>The root directory of the Git repository, or null if not found.</returns> | ||
private static string GetGitRepositoryRoot(string startPath) | ||
{ | ||
var currentDirectory = File.Exists(startPath) | ||
? Path.GetDirectoryName(startPath) | ||
: startPath; | ||
|
||
while (!string.IsNullOrEmpty(currentDirectory)) | ||
{ | ||
if (Directory.Exists(Path.Combine(currentDirectory, ".git"))) | ||
{ | ||
return currentDirectory; | ||
} | ||
|
||
currentDirectory = Directory.GetParent(currentDirectory)?.FullName; | ||
} | ||
|
||
return null; // No Git repository found | ||
} | ||
|
||
/// <summary> | ||
/// Retrieves path information for a given file or directory, including the repository root and relative path. | ||
/// </summary> | ||
/// <param name="fullPath">The fully qualified file or directory path.</param> | ||
/// <returns>A PathInfo object containing the repository root and the relative path.</returns> | ||
private static PathInfo GetPathInfo(string fullPath) | ||
{ | ||
if (!File.Exists(fullPath) && !Directory.Exists(fullPath)) | ||
{ | ||
throw new FileNotFoundException($"The path '{fullPath}' does not exist."); | ||
} | ||
|
||
var repoRoot = GetGitRepositoryRoot(fullPath); | ||
if (repoRoot == null) | ||
{ | ||
throw new InvalidOperationException($"The path '{fullPath}' is not part of a Git repository."); | ||
} | ||
|
||
var relativePath = GetRelativePath(repoRoot, fullPath).Replace(Path.DirectorySeparatorChar, '/'); | ||
return new PathInfo { Root = repoRoot, RelativePath = relativePath }; | ||
} | ||
|
||
private static string GetRelativePath(string basePath, string fullPath) | ||
{ | ||
if (!basePath.EndsWith(Path.DirectorySeparatorChar.ToString())) | ||
basePath += Path.DirectorySeparatorChar; | ||
if (fullPath.ToLowerInvariant().StartsWith(basePath.ToLowerInvariant())) | ||
{ | ||
return fullPath.Substring(basePath.Length); | ||
} | ||
return fullPath; | ||
} | ||
|
||
private struct PathInfo | ||
{ | ||
public string Root { get; set; } | ||
public string RelativePath { get; set; } | ||
} | ||
} | ||
|
||
} |
Oops, something went wrong.