Skip to content

Commit

Permalink
Skyline: Create ImageComparer tool as an easier way to compare modifi… (
Browse files Browse the repository at this point in the history
#3296)

* Skyline: Create ImageComparer tool as an easier way to compare modified tutorial screenshots in Git
  • Loading branch information
brendanx67 authored Dec 22, 2024
1 parent 5948bd0 commit b21b512
Show file tree
Hide file tree
Showing 31 changed files with 2,540 additions and 2 deletions.
33 changes: 33 additions & 0 deletions pwiz_tools/Skyline/Executables/DevTools/ImageComparer/App.config
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 pwiz_tools/Skyline/Executables/DevTools/ImageComparer/FileSaver.cs
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 pwiz_tools/Skyline/Executables/DevTools/ImageComparer/GitFileHelper.cs
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; }
}
}

}
Loading

0 comments on commit b21b512

Please sign in to comment.