🎨 Reformat everything to standard

Standard:

- Use {} for blocks of GUI.Begin/End methods for explicit indentation
purposes
- Use explicit private
- Explicitly state serializable/nonserialized intent on fields in
serializable types
- Reorder methods/members by
- public enums
- public delegates
- const
- static fields
- instance fields
- constructors
- public static methods
- public instance methods
- public interface implementations
- private static methods
- private instance methods
- private interface implementations
- properties and indexers
- nested types
This commit is contained in:
Andreia Gaita 2017-01-11 19:22:02 +01:00
Родитель 849c9829ed
Коммит cd317784d0
34 изменённых файлов: 3299 добавлений и 3280 удалений

3
.gitignore поставляемый
Просмотреть файл

@ -0,0 +1,3 @@
GitHub.Unity.dll*
GitHub.Unity.pdb*
GitHub.Unity.mdb*

Просмотреть файл

@ -1,6 +1,4 @@
using System;
using UnityEditor;
using UnityEngine;
using UnityEditor;
namespace GitHub.Unity
{
@ -14,7 +12,7 @@ namespace GitHub.Unity
}
// we do this so we're guaranteed to run on the main thread, not the loader thread
static void Initialize()
private static void Initialize()
{
EditorApplication.update -= Initialize;
@ -31,4 +29,4 @@ namespace GitHub.Unity
Window.Initialize();
}
}
}
}

Просмотреть файл

@ -20,6 +20,8 @@
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<RunCodeAnalysis>false</RunCodeAnalysis>
<CodeAnalysisRuleSet>..\common\GitHub.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
@ -47,6 +49,7 @@
</ItemGroup>
<ItemGroup>
<Compile Include="EntryPoint.cs" />
<Compile Include="Guard.cs" />
<Compile Include="Installer.cs" />
<Compile Include="Localization.Designer.cs">
<AutoGen>True</AutoGen>
@ -71,6 +74,7 @@
<Compile Include="Tasks\GitSwitchBranchesTask.cs" />
<Compile Include="Tasks\GitTask.cs" />
<Compile Include="Tasks\ProcessTask.cs" />
<Compile Include="Tasks\TaskException.cs" />
<Compile Include="Tasks\Tasks.cs" />
<Compile Include="Tasks\TestTask.cs" />
<Compile Include="Utility.cs" />
@ -114,27 +118,9 @@
<EmbeddedResource Include="Icons\tracked-branch-indicator.png" />
</ItemGroup>
<ItemGroup>
<None Include="Tasks\EvaluateProjectConfigurationTask.cs.meta" />
<None Include="Tasks\FindGitTask.cs.meta" />
<None Include="Tasks\GitAddTask.cs.meta" />
<None Include="Tasks\GitBranchCreateTask.cs.meta" />
<None Include="Tasks\GitCommitTask.cs.meta" />
<None Include="Tasks\GitListBranchesTask.cs.meta" />
<None Include="Tasks\GitListRemotesTask.cs.meta" />
<None Include="Tasks\GitListUntrackedFilesTask.cs.meta" />
<None Include="Tasks\GitLogTask.cs.meta" />
<None Include="Tasks\GitStatusTask.cs.meta" />
<None Include="Tasks\GitSwitchBranchesTask.cs.meta" />
<None Include="Tasks\GitTask.cs.meta" />
<None Include="Tasks\ProcessTask.cs.meta" />
<None Include="Tasks\Tasks.cs.meta" />
<None Include="Tasks\TestTask.cs.meta" />
<None Include="Views\BranchesView.cs.meta" />
<None Include="Views\ChangesetTreeView.cs.meta" />
<None Include="Views\ChangesView.cs.meta" />
<None Include="Views\HistoryView.cs.meta" />
<None Include="Views\SettingsView.cs.meta" />
<None Include="Views\View.cs.meta" />
<None Include="..\common\GitHub.ruleset">
<Link>GitHub.ruleset</Link>
</None>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.

95
src/GitHub.Unity/Guard.cs Normal file
Просмотреть файл

@ -0,0 +1,95 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
namespace GitHub.Extensions
{
public static class Guard
{
public static void ArgumentNotNull(object value, string name)
{
if (value != null) return;
string message = String.Format(CultureInfo.InvariantCulture, "Failed Null Check on '{0}'", name);
#if DEBUG
if (!InUnitTestRunner)
{
Debug.Fail(message);
}
#endif
throw new ArgumentNullException(name, message);
}
public static void ArgumentNonNegative(int value, string name)
{
if (value > -1) return;
var message = String.Format(CultureInfo.InvariantCulture, "The value for '{0}' must be non-negative", name);
#if DEBUG
if (!InUnitTestRunner)
{
Debug.Fail(message);
}
#endif
throw new ArgumentException(message, name);
}
/// <summary>
/// Checks a string argument to ensure it isn't null or empty.
/// </summary>
/// <param name = "value">The argument value to check.</param>
/// <param name = "name">The name of the argument.</param>
public static void ArgumentNotEmptyString(string value, string name)
{
if (value?.Length > 0) return;
string message = String.Format(CultureInfo.InvariantCulture, "The value for '{0}' must not be empty", name);
#if DEBUG
if (!InUnitTestRunner)
{
Debug.Fail(message);
}
#endif
throw new ArgumentException(message, name);
}
public static void ArgumentInRange(int value, int minValue, string name)
{
if (value >= minValue) return;
string message = String.Format(CultureInfo.InvariantCulture,
"The value '{0}' for '{1}' must be greater than or equal to '{2}'",
value,
name,
minValue);
#if DEBUG
if (!InUnitTestRunner)
{
Debug.Fail(message);
}
#endif
throw new ArgumentOutOfRangeException(name, message);
}
public static void ArgumentInRange(int value, int minValue, int maxValue, string name)
{
if (value >= minValue && value <= maxValue) return;
string message = String.Format(CultureInfo.InvariantCulture,
"The value '{0}' for '{1}' must be greater than or equal to '{2}' and less than or equal to '{3}'",
value,
name,
minValue,
maxValue);
#if DEBUG
if (!InUnitTestRunner)
{
Debug.Fail(message);
}
#endif
throw new ArgumentOutOfRangeException(name, message);
}
// Borrowed from Splat.
public static bool InUnitTestRunner { get; set; }
}
}

Просмотреть файл

@ -1,21 +1,20 @@
using UnityEngine;
using UnityEditor;
using System;
using UnityEditor;
using UnityEngine;
namespace GitHub.Unity
{
class Installer : ScriptableObject
{
const string PackageName = "GitHub extensions";
const string
QueryTitle = "Embed " + PackageName + "?",
QueryMessage = "This package has no project dependencies and so can either run embedded in your Unity install or remain in your assets folder.\n\nWould you like to embed it?",
QueryOK = "Embed",
QueryCancel = "Cancel",
ErrorTitle = "Installer error",
ErrorMessage = "An error occured during installation:\n{0}",
ErrorOK = "OK";
private const string PackageName = "GitHub extensions";
private const string QueryTitle = "Embed " + PackageName + "?";
private const string QueryMessage =
"This package has no project dependencies and so can either run embedded in your Unity install or remain in your assets folder.\n\nWould you like to embed it?";
private const string QueryOK = "Embed";
private const string QueryCancel = "Cancel";
private const string ErrorTitle = "Installer error";
private const string ErrorMessage = "An error occured during installation:\n{0}";
private const string ErrorOK = "OK";
public static void Initialize()
{
@ -26,12 +25,12 @@ namespace GitHub.Unity
// Detect install path
string selfPath;
Installer instance = FindObjectOfType(typeof(Installer)) as Installer;
var instance = FindObjectOfType(typeof(Installer)) as Installer;
if (instance == null)
{
instance = CreateInstance<Installer>();
}
MonoScript script = MonoScript.FromScriptableObject(instance);
var script = MonoScript.FromScriptableObject(instance);
if (script == null)
{
selfPath = string.Empty;
@ -42,14 +41,14 @@ namespace GitHub.Unity
}
DestroyImmediate(instance);
if (string.IsNullOrEmpty(selfPath))
// If we cannot self-locate then forget the whole thing
if (string.IsNullOrEmpty(selfPath))
{
return;
}
if (EditorUtility.DisplayDialog(QueryTitle, QueryMessage, QueryOK, QueryCancel))
// Perform move
if (EditorUtility.DisplayDialog(QueryTitle, QueryMessage, QueryOK, QueryCancel))
{
MoveFrom(Application.dataPath + selfPath.Substring("Assets".Length, selfPath.LastIndexOf('/') - "Assets".Length));
}
@ -58,8 +57,7 @@ namespace GitHub.Unity
AssetDatabase.DeleteAsset(selfPath);
}
static void MoveFrom(string path)
private static void MoveFrom(string path)
{
try
{
@ -72,11 +70,9 @@ namespace GitHub.Unity
}
}
static void Failure(string error)
private static void Failure(string error)
{
EditorUtility.DisplayDialog(ErrorTitle, string.Format(ErrorMessage, error), ErrorOK);
EditorUtility.DisplayDialog(ErrorTitle, String.Format(ErrorMessage, error), ErrorOK);
}
}
}

Просмотреть файл

@ -1,14 +1,13 @@
using UnityEngine;
using UnityEditor;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
namespace GitHub.Unity
{
class ProjectWindowInterface : AssetPostprocessor
class ProjectWindowInterface : AssetPostprocessor
{
static List<GitStatusEntry> entries = new List<GitStatusEntry>();
static List<string> guids = new List<string>();
private static readonly List<GitStatusEntry> entries = new List<GitStatusEntry>();
private static readonly List<string> guids = new List<string>();
public static void Initialize()
{
@ -19,74 +18,61 @@ namespace GitHub.Unity
Tasks.ScheduleMainThread(() => Refresh());
}
static void OnPostprocessAllAssets(string[] imported, string[] deleted, string[] moveDestination, string[] moveSource)
private static void OnPostprocessAllAssets(string[] imported, string[] deleted, string[] moveDestination, string[] moveSource)
{
Refresh();
}
static void Refresh()
private static void Refresh()
{
GitStatusTask.Schedule();
}
static void OnStatusUpdate(GitStatus update)
private static void OnStatusUpdate(GitStatus update)
{
entries.Clear();
entries.AddRange(update.Entries);
guids.Clear();
for (int index = 0; index < entries.Count; ++index)
for (var index = 0; index < entries.Count; ++index)
{
string path = entries[index].ProjectPath;
var path = entries[index].ProjectPath;
guids.Add(string.IsNullOrEmpty(path) ? string.Empty : AssetDatabase.AssetPathToGUID(path));
}
EditorApplication.RepaintProjectWindow();
}
static void OnProjectWindowItemGUI(string guid, Rect itemRect)
private static void OnProjectWindowItemGUI(string guid, Rect itemRect)
{
if (Event.current.type != EventType.Repaint || string.IsNullOrEmpty(guid))
{
return;
}
int index = guids.IndexOf(guid);
var index = guids.IndexOf(guid);
if (index < 0)
{
return;
}
Texture2D texture = Styles.GetGitFileStatusIcon(entries[index].Status);
var texture = Styles.GetGitFileStatusIcon(entries[index].Status);
Rect rect;
if (itemRect.width > itemRect.height)
// End of row placement
if (itemRect.width > itemRect.height)
{
rect = new Rect(itemRect.xMax - texture.width, itemRect.y, texture.width, Mathf.Min(texture.height, EditorGUIUtility.singleLineHeight));
rect = new Rect(itemRect.xMax - texture.width, itemRect.y, texture.width,
Mathf.Min(texture.height, EditorGUIUtility.singleLineHeight));
}
else
// Corner placement
// TODO: Magic numbers that need reviewing. Make sure this works properly with long filenames and wordwrap.
else
{
float scale = itemRect.height / 90f;
Vector2
size = new Vector2(texture.width * scale, texture.height * scale),
offset = new Vector2(
itemRect.width * Mathf.Min(.4f * scale, .2f),
itemRect.height * Mathf.Min(.2f * scale, .2f)
);
rect = new Rect(
itemRect.center.x - size.x * .5f + offset.x,
itemRect.center.y - size.y * .5f + offset.y,
size.x,
size.y
);
var scale = itemRect.height / 90f;
var size = new Vector2(texture.width * scale, texture.height * scale);
var offset = new Vector2(itemRect.width * Mathf.Min(.4f * scale, .2f), itemRect.height * Mathf.Min(.2f * scale, .2f));
rect = new Rect(itemRect.center.x - size.x * .5f + offset.x, itemRect.center.y - size.y * .5f + offset.y, size.x, size.y);
}
GUI.DrawTexture(rect, texture, ScaleMode.ScaleToFit);

Просмотреть файл

@ -1,4 +1,5 @@
using System.Reflection;
using System.Resources;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
@ -8,12 +9,13 @@ using System.Runtime.InteropServices;
[assembly: AssemblyTitle("GitHub.Unity")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyCompany("GitHub")]
[assembly: AssemblyProduct("GitHub.Unity")]
[assembly: AssemblyCopyright("Copyright © 2017")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
@ -34,3 +36,5 @@ using System.Runtime.InteropServices;
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: NeutralResourcesLanguage("en-US")]

Просмотреть файл

@ -1,81 +1,38 @@
using UnityEngine;
using UnityEditor;
using System;
using System.IO;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using UnityEngine;
namespace GitHub.Unity
{
public class Settings : ScriptableObject
{
const string
SettingsParseError = "Failed to parse settings file at '{0}'",
RelativeSettingsPath = "{0}/ProjectSettings/{1}",
LocalSettingsName = "GitHub.local.json",
TeamSettingsName = "GitHub.json";
private const string SettingsParseError = "Failed to parse settings file at '{0}'";
private const string RelativeSettingsPath = "{0}/ProjectSettings/{1}";
private const string LocalSettingsName = "GitHub.local.json";
private const string TeamSettingsName = "GitHub.json";
private static Settings asset;
[SerializeField] List<string>
keys = new List<string>(),
teamKeys = new List<string>();
[SerializeField] List<object>
values = new List<object>(),
teamValues = new List<object>();
static Settings asset;
static string GetLocalPath()
{
return string.Format(RelativeSettingsPath, Utility.UnityProjectPath, LocalSettingsName);
}
static string GetTeamPath()
{
return string.Format(RelativeSettingsPath, Utility.UnityProjectPath, TeamSettingsName);
}
static IDictionary<string, object> LoadSettings(string path)
{
if (!File.Exists(path))
{
return null;
}
object parseResult;
IDictionary<string, object> settings;
if(!SimpleJson.TryDeserializeObject(File.ReadAllText(path), out parseResult) || (settings = parseResult as IDictionary<string, object>) == null)
{
Debug.LogErrorFormat(SettingsParseError, path);
return null;
}
return settings;
}
[SerializeField] private List<string> keys = new List<string>();
[SerializeField] private List<string> teamKeys = new List<string>();
[SerializeField] private List<object> teamValues = new List<object>();
[SerializeField] private List<object> values = new List<object>();
public static bool Reload()
{
Settings newAsset = CreateInstance<Settings>();
var newAsset = CreateInstance<Settings>();
IDictionary<string, object> settings = LoadSettings(GetLocalPath());
var settings = LoadSettings(GetLocalPath());
if (settings != null)
{
newAsset.keys.AddRange(settings.Keys);
newAsset.values.AddRange(settings.Values.Select(
v => (v is IList<object>) ?
(object)new List<string>(((IList<object>)v).Select(v2 => v2 as string))
:
(object)(v as string)
));
newAsset.values.AddRange(settings.Values.Select(v => v is IList<object>
? (object)new List<string>(((IList<object>)v).Select(v2 => v2 as string))
: (object)(v as string)));
}
settings = LoadSettings(GetTeamPath());
@ -83,12 +40,9 @@ namespace GitHub.Unity
if (settings != null)
{
newAsset.teamKeys.AddRange(settings.Keys);
newAsset.teamValues.AddRange(settings.Values.Select(
v => (v is IList<object>) ?
(object)new List<string>(((IList<object>)v).Select(v2 => v2 as string))
:
(object)(v as string)
));
newAsset.teamValues.AddRange(settings.Values.Select(v => v is IList<object>
? (object)new List<string>(((IList<object>)v).Select(v2 => v2 as string))
: (object)(v as string)));
}
asset = newAsset;
@ -96,7 +50,6 @@ namespace GitHub.Unity
return true;
}
public static bool Save()
{
if (asset == null)
@ -104,74 +57,47 @@ namespace GitHub.Unity
return false;
}
string path = GetLocalPath();
var path = GetLocalPath();
StreamWriter settings = File.CreateText(path);
settings.Write("{\n");
var settings = File.CreateText(path);
settings.WriteLine("{");
for (int index = 0; index < asset.keys.Count; ++index)
for (var index = 0; index < asset.keys.Count; ++index)
{
List<string> list = asset.values[index] as List<string>;
var list = asset.values[index] as List<string>;
if (list == null)
{
settings.Write("\t\"{0}\": \"{1}\",\n", Escape(asset.keys[index]), Escape((string)asset.values[index]));
settings.WriteLine("\t\"{0}\": \"{1}\",", Escape(asset.keys[index]), Escape((string)asset.values[index]));
}
else
{
settings.Write("\t\"{0}\":\n\t[\n", Escape(asset.keys[index]));
for (int listIndex = 0; listIndex < list.Count; ++listIndex)
settings.WriteLine("\t\"{0}\":\n\t[", Escape(asset.keys[index]));
for (var listIndex = 0; listIndex < list.Count; ++listIndex)
{
settings.Write("\t\t\"{0}\",\n", Escape(list[listIndex]));
settings.WriteLine("\t\t\"{0}\",", Escape(list[listIndex]));
}
settings.Write("\t],\n");
settings.WriteLine("\t],");
}
}
settings.Write("}\n");
settings.WriteLine("}");
settings.Close();
return true;
}
static string Escape(string unescaped)
{
StringBuilder builder = new StringBuilder(unescaped);
builder.Replace("\\", "\\\\");
builder.Replace("\"", "\\\"");
builder.Replace("\n", "\\n");
builder.Replace("\r", "\\r");
builder.Replace("\t", "\\t");
builder.Replace("\b", "\\b");
builder.Replace("\f", "\\f");
return builder.ToString();
}
static Settings GetAsset()
{
if (asset == null)
{
Reload();
}
return asset;
}
public static string Get(string key, string fallback = "")
{
Settings asset = GetAsset();
var asset = GetAsset();
if (asset == null)
{
return fallback;
}
int index = asset.teamKeys.IndexOf(key);
var index = asset.teamKeys.IndexOf(key);
if (index >= 0)
{
@ -188,10 +114,9 @@ namespace GitHub.Unity
return fallback;
}
public static bool Set(string key, string value, bool noSave = false)
{
Settings asset = GetAsset();
var asset = GetAsset();
if (asset == null)
{
@ -203,7 +128,7 @@ namespace GitHub.Unity
return false;
}
int index = asset.keys.IndexOf(key);
var index = asset.keys.IndexOf(key);
if (index >= 0)
{
@ -228,10 +153,9 @@ namespace GitHub.Unity
return true;
}
public static bool Unset(string key, bool noSave = false)
{
Settings asset = GetAsset();
var asset = GetAsset();
if (asset == null)
{
@ -243,7 +167,7 @@ namespace GitHub.Unity
return false;
}
int index = asset.keys.IndexOf(key);
var index = asset.keys.IndexOf(key);
if (index < 0)
{
@ -261,10 +185,9 @@ namespace GitHub.Unity
return true;
}
public static bool Rename(string key, string newKey, bool noSave = false)
{
Settings asset = GetAsset();
var asset = GetAsset();
if (asset == null)
{
@ -276,7 +199,7 @@ namespace GitHub.Unity
return false;
}
int index = asset.keys.IndexOf(key);
var index = asset.keys.IndexOf(key);
if (index < 0)
{
@ -293,156 +216,16 @@ namespace GitHub.Unity
return true;
}
static List<string> GetLocalList(string key)
{
Settings asset = GetAsset();
if (asset == null)
{
return null;
}
int index = asset.keys.IndexOf(key);
return index < 0 ? null : asset.values[index] as List<string>;
}
static List<string> GetTeamList(string key)
{
Settings asset = GetAsset();
if (asset == null)
{
return null;
}
int index = asset.teamKeys.IndexOf(key);
return index < 0 ? null : asset.teamValues[index] as List<string>;
}
public static int CountElements(string key)
{
List<string>
localList = GetLocalList(key),
teamList = GetTeamList(key);
return (localList == null ? 0 : teamList.Count) + (teamList == null ? 0 : teamList.Count);
}
public static int GetElementIndex(string key, string value)
{
List<string>
localList = GetLocalList(key),
teamList = GetTeamList(key);
int index = (teamList == null ? -1 : teamList.IndexOf(value));
if (index > -1)
{
return index + (localList == null ? 0 : localList.Count);
}
return localList == null ? -1 : localList.IndexOf(value);
}
public static string GetElement(string key, int index, string fallback = "")
{
List<string>
localList = GetLocalList(key),
teamList = GetTeamList(key);
if (index < 0)
{
return fallback;
}
if (localList != null && index < localList.Count)
{
return localList[index];
}
if (teamList != null && index < teamList.Count + (localList == null ? 0 : localList.Count))
{
return teamList[index];
}
return fallback;
}
public static bool SetElement(string key, int index, string value, bool noSave = false)
{
List<string> localList = GetLocalList(key);
if (localList == null || index >= localList.Count || index < 0)
{
return false;
}
localList[index] = value;
if (!noSave)
{
Save();
}
return true;
}
public static bool RemoveElement(string key, string value, bool noSave = false)
{
List<string> localList = GetLocalList(key);
if (localList == null)
{
return false;
}
localList.Remove(value);
if (!noSave)
{
Save();
}
return true;
}
public static bool RemoveElementAt(string key, int index, bool noSave = false)
{
List<string> localList = GetLocalList(key);
if (localList == null || index >= localList.Count || index < 0)
{
return false;
}
localList.RemoveAt(index);
if (!noSave)
{
Save();
}
return true;
}
public static bool AddElement(string key, string value, bool noSave = false)
{
Settings asset = GetAsset();
var asset = GetAsset();
if (asset == null)
{
return false;
}
int index = asset.keys.IndexOf(key);
var index = asset.keys.IndexOf(key);
List<string> list = null;
@ -466,5 +249,188 @@ namespace GitHub.Unity
return true;
}
public static bool RemoveElement(string key, string value, bool noSave = false)
{
var localList = GetLocalList(key);
if (localList == null)
{
return false;
}
localList.Remove(value);
if (!noSave)
{
Save();
}
return true;
}
public static bool RemoveElementAt(string key, int index, bool noSave = false)
{
var localList = GetLocalList(key);
if (localList == null || index >= localList.Count || index < 0)
{
return false;
}
localList.RemoveAt(index);
if (!noSave)
{
Save();
}
return true;
}
public static string GetElement(string key, int index, string fallback = "")
{
if (index < 0)
{
return fallback;
}
var localList = GetLocalList(key);
var teamList = GetTeamList(key);
if (localList != null && index < localList.Count)
{
return localList[index];
}
if (teamList != null && index < teamList.Count + (localList == null ? 0 : localList.Count))
{
return teamList[index];
}
return fallback;
}
public static int GetElementIndex(string key, string value)
{
var localList = GetLocalList(key);
var teamList = GetTeamList(key);
var index = teamList == null ? -1 : teamList.IndexOf(value);
if (index > -1)
{
return index + (localList == null ? 0 : localList.Count);
}
return localList == null ? -1 : localList.IndexOf(value);
}
public static bool SetElement(string key, int index, string value, bool noSave = false)
{
var localList = GetLocalList(key);
if (localList == null || index >= localList.Count || index < 0)
{
return false;
}
localList[index] = value;
if (!noSave)
{
Save();
}
return true;
}
public static int CountElements(string key)
{
var localList = GetLocalList(key);
var teamList = GetTeamList(key);
return (localList == null ? 0 : teamList.Count) + (teamList == null ? 0 : teamList.Count);
}
private static string GetLocalPath()
{
return String.Format(RelativeSettingsPath, Utility.UnityProjectPath, LocalSettingsName);
}
private static string GetTeamPath()
{
return String.Format(RelativeSettingsPath, Utility.UnityProjectPath, TeamSettingsName);
}
private static IDictionary<string, object> LoadSettings(string path)
{
if (!File.Exists(path))
{
return null;
}
object parseResult;
IDictionary<string, object> settings;
if (!SimpleJson.TryDeserializeObject(File.ReadAllText(path), out parseResult) ||
(settings = parseResult as IDictionary<string, object>) == null)
{
Debug.LogErrorFormat(SettingsParseError, path);
return null;
}
return settings;
}
private static string Escape(string unescaped)
{
var builder = new StringBuilder(unescaped);
builder.Replace("\\", "\\\\");
builder.Replace("\"", "\\\"");
builder.Replace("\n", "\\n");
builder.Replace("\r", "\\r");
builder.Replace("\t", "\\t");
builder.Replace("\b", "\\b");
builder.Replace("\f", "\\f");
return builder.ToString();
}
private static Settings GetAsset()
{
if (asset == null)
{
Reload();
}
return asset;
}
private static List<string> GetLocalList(string key)
{
var asset = GetAsset();
if (asset == null)
{
return null;
}
var index = asset.keys.IndexOf(key);
return index < 0 ? null : asset.values[index] as List<string>;
}
private static List<string> GetTeamList(string key)
{
var asset = GetAsset();
if (asset == null)
{
return null;
}
var index = asset.teamKeys.IndexOf(key);
return index < 0 ? null : asset.teamValues[index] as List<string>;
}
}
}

Просмотреть файл

@ -420,7 +420,7 @@ namespace GitHub.Unity
public static void Warning(string message)
{
GUILayout.BeginHorizontal(EditorStyles.helpBox);
GUILayout.Label(string.Format(WarningLabel, message), Styles.LongMessageStyle);
GUILayout.Label(String.Format(WarningLabel, message), Styles.LongMessageStyle);
GUILayout.FlexibleSpace();
GUILayout.EndHorizontal();
}

Просмотреть файл

@ -1,278 +1,208 @@
using UnityEngine;
using UnityEditor;
using System;
using System.IO;
using System.Threading;
using System.Linq;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using UnityEditor;
using UnityEditorInternal;
using UnityEngine;
using Object = UnityEngine.Object;
namespace GitHub.Unity
{
[Flags]
enum ProjectSettingsEvaluation
{
None = 0,
EditorSettingsMissing = 1 << 0,
BadVCSSettings = 1 << 1,
BinarySerialization = 1 << 2,
MixedSerialization = 1 << 3
None = 0,
EditorSettingsMissing = 1 << 0,
BadVCSSettings = 1 << 1,
BinarySerialization = 1 << 2,
MixedSerialization = 1 << 3
}
enum GitIgnoreRuleEffect
{
Require = 0,
Disallow = 1
}
abstract class ProjectConfigurationIssue
{}
class ProjectSettingsIssue : ProjectConfigurationIssue
{
public ProjectSettingsEvaluation Evaluation { get; protected set; }
public ProjectSettingsIssue(ProjectSettingsEvaluation evaluation)
{
Evaluation = evaluation;
}
public bool WasCaught(ProjectSettingsEvaluation evaluation)
{
return (Evaluation & evaluation) != 0;
}
}
public ProjectSettingsEvaluation Evaluation { get; protected set; }
}
class GitIgnoreException : ProjectConfigurationIssue
{
public Exception Exception { get; protected set; }
public GitIgnoreException(Exception exception)
{
Exception = exception;
}
}
public Exception Exception { get; protected set; }
}
class GitIgnoreIssue : ProjectConfigurationIssue
{
public string File { get; protected set; }
public string Line { get; protected set; }
public string Description { get; protected set; }
public GitIgnoreIssue(string file, string line, string description)
{
File = file;
Line = line;
Description = description;
}
}
public string File { get; protected set; }
public string Line { get; protected set; }
public string Description { get; protected set; }
}
struct GitIgnoreRule
{
const string
CountKey = "GitIgnoreRuleCount",
EffectKey = "GitIgnoreRule{0}Effect",
FileKey = "GitIgnoreRule{0}File",
LineKey = "GitIgnoreRule{0}Line",
TriggetTextKey = "GitIgnoreRule{0}TriggerText";
public static int Count
{
get
{
return Mathf.Max(0, int.Parse(Settings.Get(CountKey, "0")));
}
}
public GitIgnoreRuleEffect Effect { get; private set; }
public string FileString { get; private set; }
public string LineString { get; private set; }
public Regex File { get; private set; }
public Regex Line { get; private set; }
public string TriggerText { get; private set; }
private const string CountKey = "GitIgnoreRuleCount";
private const string EffectKey = "GitIgnoreRule{0}Effect";
private const string FileKey = "GitIgnoreRule{0}File";
private const string LineKey = "GitIgnoreRule{0}Line";
private const string TriggerTextKey = "GitIgnoreRule{0}TriggerText";
public static bool TryLoad(int index, out GitIgnoreRule result)
{
result = new GitIgnoreRule();
int effect;
if (!int.TryParse(Settings.Get(string.Format(EffectKey, index), "-1"), out effect) || effect < 0)
if (!int.TryParse(Settings.Get(String.Format(EffectKey, index), "-1"), out effect) || effect < 0)
{
return false;
}
result.Effect = (GitIgnoreRuleEffect)effect;
result.FileString = Settings.Get(string.Format(FileKey, index));
result.FileString = Settings.Get(String.Format(FileKey, index));
try
{
result.File = new Regex(result.FileString);
}
catch(ArgumentException e)
catch (ArgumentException e)
{
result.File = null;
}
result.LineString = Settings.Get(string.Format(LineKey, index));
result.LineString = Settings.Get(String.Format(LineKey, index));
try
{
result.Line = new Regex(result.LineString);
}
catch(ArgumentException e)
catch (ArgumentException e)
{
result.Line = null;
}
result.TriggerText = Settings.Get(string.Format(TriggetTextKey, index));
result.TriggerText = Settings.Get(String.Format(TriggerTextKey, index));
return true;
}
public static void Save(int index, GitIgnoreRuleEffect effect, string file, string line, string triggerText)
{
Settings.Set(string.Format(EffectKey, index), ((int)effect).ToString(), true);
Settings.Set(string.Format(FileKey, index), file, true);
Settings.Set(string.Format(LineKey, index), line, true);
Settings.Set(string.Format(TriggetTextKey, index), triggerText);
Settings.Set(String.Format(EffectKey, index), ((int)effect).ToString(), true);
Settings.Set(String.Format(FileKey, index), file, true);
Settings.Set(String.Format(LineKey, index), line, true);
Settings.Set(String.Format(TriggerTextKey, index), triggerText);
}
public static void New()
{
Save(Count, GitIgnoreRuleEffect.Require, "", "", "");
Settings.Set(CountKey, (Count + 1).ToString());
}
public static void Delete(int index)
{
Settings.Unset(string.Format(EffectKey, index), true);
Settings.Unset(string.Format(FileKey, index), true);
Settings.Unset(string.Format(LineKey, index), true);
Settings.Unset(string.Format(TriggetTextKey, index), true);
Settings.Unset(String.Format(EffectKey, index), true);
Settings.Unset(String.Format(FileKey, index), true);
Settings.Unset(String.Format(LineKey, index), true);
Settings.Unset(String.Format(TriggerTextKey, index), true);
int count = Count;
for (int current = index + 1; index < count; ++index)
var count = Count;
for (; index < count; ++index)
{
Settings.Rename(string.Format(EffectKey, index), string.Format(EffectKey, index - 1), true);
Settings.Rename(string.Format(FileKey, index), string.Format(FileKey, index - 1), true);
Settings.Rename(string.Format(LineKey, index), string.Format(LineKey, index - 1), true);
Settings.Rename(string.Format(TriggetTextKey, index), string.Format(TriggetTextKey, index - 1), true);
Settings.Rename(String.Format(EffectKey, index), String.Format(EffectKey, index - 1), true);
Settings.Rename(String.Format(FileKey, index), String.Format(FileKey, index - 1), true);
Settings.Rename(String.Format(LineKey, index), String.Format(LineKey, index - 1), true);
Settings.Rename(String.Format(TriggerTextKey, index), String.Format(TriggerTextKey, index - 1), true);
}
Settings.Set(CountKey, (count - 1).ToString());
}
public override string ToString()
{
return string.Format("{0} \"{1}\" in \"{2}\": {3}", Effect, Line, File, TriggerText);
return String.Format("{0} \"{1}\" in \"{2}\": {3}", Effect, Line, File, TriggerText);
}
}
public static int Count
{
get { return Mathf.Max(0, int.Parse(Settings.Get(CountKey, "0"))); }
}
public GitIgnoreRuleEffect Effect { get; private set; }
public string FileString { get; private set; }
public string LineString { get; private set; }
public Regex File { get; private set; }
public Regex Line { get; private set; }
public string TriggerText { get; private set; }
}
class EvaluateProjectConfigurationTask : ITask
{
enum SerializationSetting
{
Mixed = 0,
ForceBinary = 1,
ForceText = 2
}
struct GitIgnoreFile
{
public string Path { get; private set; }
public string[] Contents { get; private set; }
public GitIgnoreFile(string path)
{
Path = path.Substring(Utility.GitRoot.Length + 1);
Contents = File.ReadAllLines(path).Select(l => l.Trim()).Where(l => !string.IsNullOrEmpty(l)).ToArray();
}
public override string ToString()
{
return string.Format("{0}:\n{1}", Path, string.Join("\n", Contents));
}
}
private const string GitIgnoreFilePattern = ".gitignore";
private const string VCSPropertyName = "m_ExternalVersionControlSupport";
private const string SerializationPropertyName = "m_SerializationMode";
private const string VisibleMetaFilesValue = "Visible Meta Files";
private const string HiddenMetaFilesValue = "Hidden Meta Files";
private const int ThreadSyncDelay = 100;
public static string EditorSettingsPath = "ProjectSettings/EditorSettings.asset";
private static Action<IEnumerable<ProjectConfigurationIssue>> onEvaluationResult;
const string
GitIgnoreFilePattern = ".gitignore",
VCSPropertyName = "m_ExternalVersionControlSupport",
SerializationPropertyName = "m_SerializationMode",
VisibleMetaFilesValue = "Visible Meta Files",
HiddenMetaFilesValue = "Hidden Meta Files";
const int ThreadSyncDelay = 100;
static Action<IEnumerable<ProjectConfigurationIssue>> onEvaluationResult;
private readonly List<ProjectConfigurationIssue> issues = new List<ProjectConfigurationIssue>();
public static void RegisterCallback(Action<IEnumerable<ProjectConfigurationIssue>> callback)
{
onEvaluationResult += callback;
}
public static void UnregisterCallback(Action<IEnumerable<ProjectConfigurationIssue>> callback)
{
onEvaluationResult -= callback;
}
public static void Schedule()
{
Tasks.Add(new EvaluateProjectConfigurationTask());
}
public static Object LoadEditorSettings()
{
return UnityEditorInternal.InternalEditorUtility.LoadSerializedFileAndForget(EditorSettingsPath).FirstOrDefault();
return InternalEditorUtility.LoadSerializedFileAndForget(EditorSettingsPath).FirstOrDefault();
}
List<ProjectConfigurationIssue> issues = new List<ProjectConfigurationIssue>();
public bool Blocking { get { return false; } }
public float Progress { get; protected set; }
public bool Done { get; protected set; }
public TaskQueueSetting Queued { get { return TaskQueueSetting.QueueSingle; } }
public bool Critical { get { return false; } }
public bool Cached { get { return false; } }
public Action<ITask> OnBegin { set; protected get; }
public Action<ITask> OnEnd { set; protected get; }
public string Label { get { return "Project Evaluation"; } }
public void Run()
{
Done = false;
@ -280,7 +210,7 @@ namespace GitHub.Unity
issues.Clear();
if(OnBegin != null)
if (OnBegin != null)
{
OnBegin(this);
}
@ -292,12 +222,15 @@ namespace GitHub.Unity
EvaluateGitIgnore();
// Wait for main thread work to complete
while(!Done) { Thread.Sleep(ThreadSyncDelay); }
while (!Done)
{
Thread.Sleep(ThreadSyncDelay);
}
Progress = 1f;
Done = true;
if(OnEnd != null)
if (OnEnd != null)
{
OnEnd(this);
}
@ -308,26 +241,40 @@ namespace GitHub.Unity
}
}
void EvaluateLocalConfiguration()
public void Abort()
{
ProjectSettingsEvaluation result = ProjectSettingsEvaluation.None;
Done = true;
}
Object settingsAsset = LoadEditorSettings();
public void Disconnect()
{}
public void Reconnect()
{}
public void WriteCache(TextWriter cache)
{}
private void EvaluateLocalConfiguration()
{
var result = ProjectSettingsEvaluation.None;
var settingsAsset = LoadEditorSettings();
if (settingsAsset == null)
{
result |= ProjectSettingsEvaluation.EditorSettingsMissing;
return;
}
SerializedObject settingsObject = new SerializedObject(settingsAsset);
string vcsSetting = settingsObject.FindProperty(VCSPropertyName).stringValue;
var settingsObject = new SerializedObject(settingsAsset);
var vcsSetting = settingsObject.FindProperty(VCSPropertyName).stringValue;
if (!vcsSetting.Equals(VisibleMetaFilesValue) && !vcsSetting.Equals(HiddenMetaFilesValue))
{
result |= ProjectSettingsEvaluation.BadVCSSettings;
}
SerializationSetting serializationSetting = (SerializationSetting)settingsObject.FindProperty(SerializationPropertyName).intValue;
var serializationSetting = (SerializationSetting)settingsObject.FindProperty(SerializationPropertyName).intValue;
if (serializationSetting == SerializationSetting.ForceBinary)
{
result |= ProjectSettingsEvaluation.BinarySerialization;
@ -345,12 +292,11 @@ namespace GitHub.Unity
Done = true;
}
void EvaluateGitIgnore()
private void EvaluateGitIgnore()
{
// Read rules
List<GitIgnoreRule> rules = new List<GitIgnoreRule>(GitIgnoreRule.Count);
for (int index = 0; index < rules.Capacity; ++index)
var rules = new List<GitIgnoreRule>(GitIgnoreRule.Count);
for (var index = 0; index < rules.Capacity; ++index)
{
GitIgnoreRule rule;
if (GitIgnoreRule.TryLoad(index, out rule))
@ -368,7 +314,10 @@ namespace GitHub.Unity
GitIgnoreFile[] files;
try
{
files = Directory.GetFiles(Utility.GitRoot, GitIgnoreFilePattern, SearchOption.AllDirectories).Select(p => new GitIgnoreFile(p)).ToArray();
files =
Directory.GetFiles(Utility.GitRoot, GitIgnoreFilePattern, SearchOption.AllDirectories)
.Select(p => new GitIgnoreFile(p))
.ToArray();
if (files.Length < 1)
{
@ -383,12 +332,12 @@ namespace GitHub.Unity
}
// Evaluate each rule
for (int ruleIndex = 0; ruleIndex < rules.Count; ++ruleIndex)
for (var ruleIndex = 0; ruleIndex < rules.Count; ++ruleIndex)
{
GitIgnoreRule rule = rules[ruleIndex];
for (int fileIndex = 0; fileIndex < files.Length; ++fileIndex)
var rule = rules[ruleIndex];
for (var fileIndex = 0; fileIndex < files.Length; ++fileIndex)
{
GitIgnoreFile file = files[fileIndex];
var file = files[fileIndex];
// Check against all files with matching path
if (rule.File == null || !rule.File.IsMatch(file.Path))
{
@ -396,27 +345,26 @@ namespace GitHub.Unity
}
// Validate all lines in that file
for (int lineIndex = 0; lineIndex < file.Contents.Length; ++lineIndex)
for (var lineIndex = 0; lineIndex < file.Contents.Length; ++lineIndex)
{
string line = file.Contents[lineIndex];
bool match = rule.Line != null && rule.Line.IsMatch(line);
bool broken = false;
var line = file.Contents[lineIndex];
var match = rule.Line != null && rule.Line.IsMatch(line);
if (rule.Effect == GitIgnoreRuleEffect.Disallow && match)
// This line is not allowed
// This line is not allowed
{
issues.Add(new GitIgnoreIssue(file.Path, line, rule.TriggerText));
}
else if (rule.Effect == GitIgnoreRuleEffect.Require)
// If the line is required, see if we're there
// If the line is required, see if we're there
{
if (match)
// We found it! No sense in searching further in this file.
// We found it! No sense in searching further in this file.
{
break;
}
else if (lineIndex == file.Contents.Length - 1)
// We reached the last line without finding it
// We reached the last line without finding it
{
issues.Add(new GitIgnoreIssue(file.Path, string.Empty, rule.TriggerText));
}
@ -426,15 +374,59 @@ namespace GitHub.Unity
}
}
public void Abort()
public bool Blocking
{
Done = true;
get { return false; }
}
public float Progress { get; protected set; }
public bool Done { get; protected set; }
public void Disconnect() {}
public void Reconnect() {}
public void WriteCache(TextWriter cache) {}
public TaskQueueSetting Queued
{
get { return TaskQueueSetting.QueueSingle; }
}
public bool Critical
{
get { return false; }
}
public bool Cached
{
get { return false; }
}
public Action<ITask> OnBegin { set; protected get; }
public Action<ITask> OnEnd { set; protected get; }
public string Label
{
get { return "Project Evaluation"; }
}
private enum SerializationSetting
{
Mixed = 0,
ForceBinary = 1,
ForceText = 2
}
private struct GitIgnoreFile
{
public string Path { get; private set; }
public string[] Contents { get; private set; }
public GitIgnoreFile(string path)
{
Path = path.Substring(Utility.GitRoot.Length + 1);
Contents = File.ReadAllLines(path).Select(l => l.Trim()).Where(l => !string.IsNullOrEmpty(l)).ToArray();
}
public override string ToString()
{
return String.Format("{0}:{1}{2}", Path, Environment.NewLine, string.Join(Environment.NewLine, Contents));
}
}
}
}

Просмотреть файл

@ -1,25 +1,18 @@
using UnityEngine;
using UnityEditor;
using System;
using System.IO;
using System.Text;
namespace GitHub.Unity
{
class FindGitTask : ProcessTask
{
public static string DefaultGitPath
{
get
{
return Utility.IsWindows ?
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Local\\GitHub\\PortableGit_\\cmd\\git.exe")
:
"/usr/bin/git";
}
}
private Action onFailure;
private Action<string> onSuccess;
private FindGitTask(Action<string> onSuccess, Action onFailure = null)
{
this.onSuccess = onSuccess;
this.onFailure = onFailure;
}
public static bool ValidateGitInstall(string path)
{
@ -31,39 +24,11 @@ namespace GitHub.Unity
return true;
}
public static void Schedule(Action<string> onSuccess, Action onFailure = null)
{
Tasks.Add(new FindGitTask(onSuccess, onFailure));
}
Action<string> onSuccess;
Action onFailure;
StringWriter
output = new StringWriter(),
error = new StringWriter();
FindGitTask(Action<string> onSuccess, Action onFailure = null)
{
this.onSuccess = onSuccess;
this.onFailure = onFailure;
}
public override bool Blocking { get { return false; } }
public override bool Critical { get { return false; } }
public override bool Cached { get { return false; } }
public override string Label { get { return "find git"; } }
protected override string ProcessName { get { return Utility.IsWindows ? "where" : "which"; } }
protected override string ProcessArguments { get { return "git"; } }
protected override TextWriter OutputBuffer { get { return output; } }
protected override TextWriter ErrorBuffer { get { return error; } }
protected override void OnProcessOutputUpdate()
{
if (!Done)
@ -71,7 +36,7 @@ namespace GitHub.Unity
return;
}
StringBuilder buffer = error.GetStringBuilder();
var buffer = ErrorBuffer.GetStringBuilder();
if (buffer.Length > 0)
{
Tasks.ReportFailure(FailureSeverity.Critical, this, buffer.ToString());
@ -83,8 +48,49 @@ namespace GitHub.Unity
}
else if (onSuccess != null)
{
Tasks.ScheduleMainThread(() => onSuccess(output.ToString().Trim()));
Tasks.ScheduleMainThread(() => onSuccess(OutputBuffer.ToString().Trim()));
}
}
public static string DefaultGitPath
{
get
{
return Utility.IsWindows
? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"Local\\GitHub\\PortableGit_\\cmd\\git.exe")
: "/usr/bin/git";
}
}
public override bool Blocking
{
get { return false; }
}
public override bool Critical
{
get { return false; }
}
public override bool Cached
{
get { return false; }
}
public override string Label
{
get { return "find git"; }
}
protected override string ProcessName
{
get { return Utility.IsWindows ? "where" : "which"; }
}
protected override string ProcessArguments
{
get { return "git"; }
}
}
}

Просмотреть файл

@ -1,44 +1,21 @@
using UnityEngine;
using UnityEditor;
using System;
using System.IO;
using System.Text;
using System.Collections.Generic;
using System.IO;
namespace GitHub.Unity
{
class GitAddTask : GitTask
{
public static void Schedule(IEnumerable<string> files, Action onSuccess = null, Action onFailure = null)
{
Tasks.Add(new GitAddTask(files, onSuccess, onFailure));
}
private string arguments = "";
private Action onFailure;
private Action onSuccess;
StringWriter error = new StringWriter();
string arguments = "";
Action
onSuccess,
onFailure;
public override bool Blocking { get { return false; } }
public override bool Critical { get { return true; } }
public override bool Cached { get { return true; } }
public override string Label { get { return "git add"; } }
protected override string ProcessArguments { get { return arguments; } }
protected override TextWriter ErrorBuffer { get { return error; } }
GitAddTask(IEnumerable<string> files, Action onSuccess = null, Action onFailure = null)
private GitAddTask(IEnumerable<string> files, Action onSuccess = null, Action onFailure = null)
{
arguments = "add ";
arguments += " -- ";
foreach (string file in files)
foreach (var file in files)
{
arguments += " " + file;
}
@ -47,6 +24,10 @@ namespace GitHub.Unity
this.onFailure = onFailure;
}
public static void Schedule(IEnumerable<string> files, Action onSuccess = null, Action onFailure = null)
{
Tasks.Add(new GitAddTask(files, onSuccess, onFailure));
}
protected override void OnProcessOutputUpdate()
{
@ -56,7 +37,7 @@ namespace GitHub.Unity
}
// Handle failure / success
StringBuilder buffer = error.GetStringBuilder();
var buffer = ErrorBuffer.GetStringBuilder();
if (buffer.Length > 0)
{
Tasks.ReportFailure(FailureSeverity.Critical, this, buffer.ToString());
@ -74,5 +55,30 @@ namespace GitHub.Unity
// Always update
GitStatusTask.Schedule();
}
public override bool Blocking
{
get { return false; }
}
public override bool Critical
{
get { return true; }
}
public override bool Cached
{
get { return true; }
}
public override string Label
{
get { return "git add"; }
}
protected override string ProcessArguments
{
get { return arguments; }
}
}
}

Просмотреть файл

@ -1,43 +1,16 @@
using UnityEngine;
using UnityEditor;
using System;
using System.IO;
using System.Text;
namespace GitHub.Unity
{
class GitBranchCreateTask : GitTask
{
public static void Schedule(string newBranch, string baseBranch, Action onSuccess, Action onFailure = null)
{
Tasks.Add(new GitBranchCreateTask(newBranch, baseBranch, onSuccess, onFailure));
}
private string baseBranch;
private string newBranch;
private Action onFailure;
private Action onSuccess;
string
newBranch,
baseBranch;
Action
onSuccess,
onFailure;
StringWriter
output = new StringWriter(),
error = new StringWriter();
public override bool Blocking { get { return false; } }
public override TaskQueueSetting Queued { get { return TaskQueueSetting.Queue; } }
public override bool Critical { get { return false; } }
public override bool Cached { get { return true; } }
public override string Label { get { return "git branch"; } }
protected override string ProcessArguments { get { return string.Format("branch {0} {1}", newBranch, baseBranch); } }
protected override TextWriter OutputBuffer { get { return output; } }
protected override TextWriter ErrorBuffer { get { return error; } }
GitBranchCreateTask(string newBranch, string baseBranch, Action onSuccess, Action onFailure)
private GitBranchCreateTask(string newBranch, string baseBranch, Action onSuccess, Action onFailure)
{
this.newBranch = newBranch;
this.baseBranch = baseBranch;
@ -45,13 +18,17 @@ namespace GitHub.Unity
this.onFailure = onFailure;
}
public static void Schedule(string newBranch, string baseBranch, Action onSuccess, Action onFailure = null)
{
Tasks.Add(new GitBranchCreateTask(newBranch, baseBranch, onSuccess, onFailure));
}
protected override void OnProcessOutputUpdate()
{
if (Done)
{
// Handle failure / success
StringBuilder buffer = error.GetStringBuilder();
var buffer = ErrorBuffer.GetStringBuilder();
if (buffer.Length > 0)
{
Tasks.ReportFailure(FailureSeverity.Critical, this, buffer.ToString());
@ -69,5 +46,35 @@ namespace GitHub.Unity
}
}
}
public override bool Blocking
{
get { return false; }
}
public override TaskQueueSetting Queued
{
get { return TaskQueueSetting.Queue; }
}
public override bool Critical
{
get { return false; }
}
public override bool Cached
{
get { return true; }
}
public override string Label
{
get { return "git branch"; }
}
protected override string ProcessArguments
{
get { return String.Format("branch {0} {1}", newBranch, baseBranch); }
}
}
}

Просмотреть файл

@ -1,53 +1,33 @@
using UnityEngine;
using UnityEditor;
using System;
using System.IO;
using System.Text;
using System;
using System.Collections.Generic;
using System.IO;
namespace GitHub.Unity
{
class GitCommitTask : GitTask
{
public static void Schedule(IEnumerable<string> files, string message, string body, Action onSuccess = null, Action onFailure = null)
{
GitAddTask.Schedule(files, () => Schedule(message, body, onSuccess, onFailure), onFailure);
}
private string arguments = "";
private Action onFailure;
private Action onSuccess;
public static void Schedule(string message, string body, Action onSuccess = null, Action onFailure = null)
{
Tasks.Add(new GitCommitTask(message, body, onSuccess, onFailure));
}
StringWriter error = new StringWriter();
string arguments = "";
Action
onSuccess,
onFailure;
GitCommitTask(string message, string body, Action onSuccess = null, Action onFailure = null)
private GitCommitTask(string message, string body, Action onSuccess = null, Action onFailure = null)
{
arguments = "commit ";
arguments += string.Format(@" -m ""{0}{1}{2}""", message, Environment.NewLine, body);
arguments += String.Format(@" -m ""{0}{1}{2}""", message, Environment.NewLine, body);
this.onSuccess = onSuccess;
this.onFailure = onFailure;
}
public static void Schedule(IEnumerable<string> files, string message, string body, Action onSuccess = null, Action onFailure = null)
{
GitAddTask.Schedule(files, () => Schedule(message, body, onSuccess, onFailure), onFailure);
}
public override bool Blocking { get { return false; } }
public override bool Critical { get { return true; } }
public override bool Cached { get { return true; } }
public override string Label { get { return "git commit"; } }
protected override string ProcessArguments { get { return arguments; } }
protected override TextWriter ErrorBuffer { get { return error; } }
public static void Schedule(string message, string body, Action onSuccess = null, Action onFailure = null)
{
Tasks.Add(new GitCommitTask(message, body, onSuccess, onFailure));
}
protected override void OnProcessOutputUpdate()
{
@ -57,7 +37,7 @@ namespace GitHub.Unity
}
// Handle failure / success
StringBuilder buffer = error.GetStringBuilder();
var buffer = ErrorBuffer.GetStringBuilder();
if (buffer.Length > 0)
{
Tasks.ReportFailure(FailureSeverity.Critical, this, buffer.ToString());
@ -75,5 +55,30 @@ namespace GitHub.Unity
// Always update
GitStatusTask.Schedule();
}
public override bool Blocking
{
get { return false; }
}
public override bool Critical
{
get { return true; }
}
public override bool Cached
{
get { return true; }
}
public override string Label
{
get { return "git commit"; }
}
protected override string ProcessArguments
{
get { return arguments; }
}
}
}

Просмотреть файл

@ -1,21 +1,14 @@
using UnityEngine;
using UnityEditor;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using System.Linq;
namespace GitHub.Unity
{
struct GitBranch
{
public string Name{ get; private set; }
public string Tracking{ get; private set; }
public bool Active{ get; private set; }
public string Name { get; private set; }
public string Tracking { get; private set; }
public bool Active { get; private set; }
public GitBranch(string name, string tracking, bool active)
{
@ -25,78 +18,46 @@ namespace GitHub.Unity
}
}
class GitListBranchesTask : GitTask
{
enum Mode
{
Local,
Remote
}
private const string LocalArguments = "branch -vv";
private const string RemoteArguments = "branch -r";
private const string UnmatchedLineError = "Unable to match the line '{0}'";
private List<GitBranch> branches = new List<GitBranch>();
private Mode mode;
private Action onFailure;
private Action<IEnumerable<GitBranch>> onSuccess;
const string
LocalArguments = "branch -vv",
RemoteArguments = "branch -r",
UnmatchedLineError = "Unable to match the line '{0}'";
public static void ScheduleLocal(Action<IEnumerable<GitBranch>> onSuccess, Action onFailure = null)
{
Schedule(Mode.Local, onSuccess, onFailure);
}
public static void ScheduleRemote(Action<IEnumerable<GitBranch>> onSuccess, Action onFailure = null)
{
Schedule(Mode.Remote, onSuccess, onFailure);
}
static void Schedule(Mode mode, Action<IEnumerable<GitBranch>> onSuccess, Action onFailure = null)
{
Tasks.Add(new GitListBranchesTask(mode, onSuccess, onFailure));
}
StringWriter
output = new StringWriter(),
error = new StringWriter();
Mode mode;
List<GitBranch> branches = new List<GitBranch>();
int activeIndex;
Action<IEnumerable<GitBranch>> onSuccess;
Action onFailure;
public override bool Blocking { get { return false; } }
public override TaskQueueSetting Queued { get { return TaskQueueSetting.Queue; } }
public override bool Critical { get { return false; } }
public override bool Cached { get { return false; } }
public override string Label { get { return "git branch"; } }
protected override string ProcessArguments { get { return mode == Mode.Local ? LocalArguments : RemoteArguments; } }
protected override TextWriter OutputBuffer { get { return output; } }
protected override TextWriter ErrorBuffer { get { return error; } }
GitListBranchesTask(Mode mode, Action<IEnumerable<GitBranch>> onSuccess, Action onFailure = null)
private GitListBranchesTask(Mode mode, Action<IEnumerable<GitBranch>> onSuccess, Action onFailure = null)
{
this.mode = mode;
this.onSuccess = onSuccess;
this.onFailure = onFailure;
}
public static void ScheduleLocal(Action<IEnumerable<GitBranch>> onSuccess, Action onFailure = null)
{
Schedule(Mode.Local, onSuccess, onFailure);
}
public static void ScheduleRemote(Action<IEnumerable<GitBranch>> onSuccess, Action onFailure = null)
{
Schedule(Mode.Remote, onSuccess, onFailure);
}
private static void Schedule(Mode mode, Action<IEnumerable<GitBranch>> onSuccess, Action onFailure = null)
{
Tasks.Add(new GitListBranchesTask(mode, onSuccess, onFailure));
}
protected override void OnProcessOutputUpdate()
{
Utility.ParseLines(output.GetStringBuilder(), ParseOutputLine, Done);
Utility.ParseLines(OutputBuffer.GetStringBuilder(), ParseOutputLine, Done);
if (Done)
{
// Handle failure / success
StringBuilder buffer = error.GetStringBuilder();
var buffer = ErrorBuffer.GetStringBuilder();
if (buffer.Length > 0)
{
Tasks.ReportFailure(FailureSeverity.Moderate, this, buffer.ToString());
@ -112,8 +73,7 @@ namespace GitHub.Unity
}
}
void DeliverResult()
private void DeliverResult()
{
if (onSuccess == null)
{
@ -123,18 +83,54 @@ namespace GitHub.Unity
onSuccess(branches);
}
void ParseOutputLine(string line)
private void ParseOutputLine(string line)
{
Match match = Utility.ListBranchesRegex.Match(line);
var match = Utility.ListBranchesRegex.Match(line);
if (!match.Success)
{
Tasks.ReportFailure(FailureSeverity.Moderate, this, string.Format(UnmatchedLineError, line));
Tasks.ReportFailure(FailureSeverity.Moderate, this, String.Format(UnmatchedLineError, line));
return;
}
branches.Add(new GitBranch(match.Groups["name"].Value, match.Groups["tracking"].Value, !string.IsNullOrEmpty(match.Groups["active"].Value)));
branches.Add(new GitBranch(match.Groups["name"].Value, match.Groups["tracking"].Value,
!string.IsNullOrEmpty(match.Groups["active"].Value)));
}
public override bool Blocking
{
get { return false; }
}
public override TaskQueueSetting Queued
{
get { return TaskQueueSetting.Queue; }
}
public override bool Critical
{
get { return false; }
}
public override bool Cached
{
get { return false; }
}
public override string Label
{
get { return "git branch"; }
}
protected override string ProcessArguments
{
get { return mode == Mode.Local ? LocalArguments : RemoteArguments; }
}
private enum Mode
{
Local,
Remote
}
}
}

Просмотреть файл

@ -1,11 +1,8 @@
using UnityEngine;
using UnityEditor;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using System.Collections.Generic;
using UnityEngine;
namespace GitHub.Unity
{
@ -17,12 +14,19 @@ namespace GitHub.Unity
Both
}
struct GitRemote
{
public string Name;
public string URL;
public string Login;
public string User;
public string Token;
public string Host;
public GitRemoteFunction Function;
public static bool TryParse(string line, out GitRemote result)
{
Match match = Utility.ListRemotesRegex.Match(line);
var match = Utility.ListRemotesRegex.Match(line);
if (!match.Success)
{
@ -30,8 +34,7 @@ namespace GitHub.Unity
return false;
}
result = new GitRemote()
{
result = new GitRemote() {
Name = match.Groups["name"].Value,
URL = match.Groups["url"].Value,
Login = match.Groups["login"].Value,
@ -44,97 +47,56 @@ namespace GitHub.Unity
{
result.Function = (GitRemoteFunction)Enum.Parse(typeof(GitRemoteFunction), match.Groups["function"].Value, true);
}
catch(Exception)
catch (Exception)
{}
return true;
}
public string
Name,
URL,
Login,
User,
Token,
Host;
public GitRemoteFunction Function;
public override string ToString()
{
return string.Format(
@"Name: {0}
URL: {1}
Login: {2}
User: {3}
Token: {4}
Host: {5}
Function: {6}",
Name,
URL,
Login,
User,
Token,
Host,
Function
);
var sb = new StringBuilder();
sb.AppendLine(String.Format("Name: {0}", Name));
sb.AppendLine(String.Format("URL: {0}", URL));
sb.AppendLine(String.Format("Login: {0}", Login));
sb.AppendLine(String.Format("User: {0}", User));
sb.AppendLine(String.Format("Token: {0}", Token));
sb.AppendLine(String.Format("Host: {0}", Host));
sb.AppendLine(String.Format("Function: {0}", Function));
return sb.ToString();
}
}
class GitListRemotesTask : GitTask
{
const string ParseFailedError = "Remote parse error in line: '{0}'";
static Action<IList<GitRemote>> onRemotesListed;
private const string ParseFailedError = "Remote parse error in line: '{0}'";
private static Action<IList<GitRemote>> onRemotesListed;
private List<GitRemote> entries = new List<GitRemote>();
public static void RegisterCallback(Action<IList<GitRemote>> callback)
{
onRemotesListed += callback;
}
public static void UnregisterCallback(Action<IList<GitRemote>> callback)
{
onRemotesListed -= callback;
}
public static void Schedule()
{
Tasks.Add(new GitListRemotesTask());
}
StringWriter
output = new StringWriter(),
error = new StringWriter();
List<GitRemote> entries = new List<GitRemote>();
public override bool Blocking { get { return false; } }
public virtual TaskQueueSetting Queued { get { return TaskQueueSetting.QueueSingle; } }
public override bool Critical { get { return false; } }
public override bool Cached { get { return false; } }
public override string Label { get { return "git remote"; } }
protected override string ProcessArguments { get { return "remote -v"; } }
protected override TextWriter OutputBuffer { get { return output; } }
protected override TextWriter ErrorBuffer { get { return error; } }
protected override void OnProcessOutputUpdate()
{
Utility.ParseLines(output.GetStringBuilder(), ParseOutputLine, Done);
Utility.ParseLines(OutputBuffer.GetStringBuilder(), ParseOutputLine, Done);
if (Done)
{
// Handle failure / success
StringBuilder buffer = error.GetStringBuilder();
var buffer = ErrorBuffer.GetStringBuilder();
if (buffer.Length > 0)
{
Tasks.ReportFailure(FailureSeverity.Moderate, this, buffer.ToString());
@ -146,8 +108,7 @@ Function: {6}",
}
}
void DeliverResult()
private void DeliverResult()
{
if (onRemotesListed != null)
{
@ -157,20 +118,16 @@ Function: {6}",
entries.Clear();
}
void ParseOutputLine(string line)
private void ParseOutputLine(string line)
{
// Parse line as a remote
GitRemote remote;
if (GitRemote.TryParse(line, out remote))
{
// Join Fetch/Push entries into single Both entries
if (
remote.Function != GitRemoteFunction.Unknown &&
entries.RemoveAll(e =>
e.Function != GitRemoteFunction.Unknown && e.Function != remote.Function && e.Name.Equals(remote.Name)
) > 0
)
if (remote.Function != GitRemoteFunction.Unknown &&
entries.RemoveAll(
e => e.Function != GitRemoteFunction.Unknown && e.Function != remote.Function && e.Name.Equals(remote.Name)) > 0)
{
remote.Function = GitRemoteFunction.Both;
}
@ -183,5 +140,35 @@ Function: {6}",
Debug.LogWarningFormat(ParseFailedError, line);
}
}
public override bool Blocking
{
get { return false; }
}
public virtual TaskQueueSetting Queued
{
get { return TaskQueueSetting.QueueSingle; }
}
public override bool Critical
{
get { return false; }
}
public override bool Cached
{
get { return false; }
}
public override string Label
{
get { return "git remote"; }
}
protected override string ProcessArguments
{
get { return "remote -v"; }
}
}
}

Просмотреть файл

@ -1,67 +1,44 @@
using UnityEngine;
using UnityEditor;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Collections.Generic;
namespace GitHub.Unity
{
class GitListUntrackedFilesTask : GitTask
{
public static void Schedule(Action<GitListUntrackedFilesTask> success, Action failure = null)
{
Tasks.Add(new GitListUntrackedFilesTask(success, failure));
}
private List<GitStatusEntry> entries = new List<GitStatusEntry>();
private Action onFailure;
private Action<GitListUntrackedFilesTask> onSuccess;
public override bool Blocking { get { return false; } }
public override bool Critical { get { return false; } }
public override bool Cached { get { return false; } }
public override string Label { get { return "git ls-files"; } }
public IList<GitStatusEntry> Entries { get { return entries; } }
protected override string ProcessArguments { get { return "ls-files -o --exclude-standard"; } }
protected override TextWriter OutputBuffer { get { return output; } }
protected override TextWriter ErrorBuffer { get { return error; } }
Action<GitListUntrackedFilesTask> onSuccess;
Action onFailure;
StringWriter
output = new StringWriter(),
error = new StringWriter();
List<GitStatusEntry> entries = new List<GitStatusEntry>();
GitListUntrackedFilesTask(Action<GitListUntrackedFilesTask> success, Action failure = null)
private GitListUntrackedFilesTask(Action<GitListUntrackedFilesTask> success, Action failure = null)
{
onSuccess = success;
onFailure = failure;
}
public static void Schedule(Action<GitListUntrackedFilesTask> success, Action failure = null)
{
Tasks.Add(new GitListUntrackedFilesTask(success, failure));
}
protected override void OnProcessOutputUpdate()
{
StringBuilder buffer = output.GetStringBuilder();
int end = buffer.Length - 1;
var buffer = OutputBuffer.GetStringBuilder();
var end = buffer.Length - 1;
if(!Done)
// Only try to avoid partial lines if the process did not already end
if (!Done)
{
for(; end > 0 && buffer[end] != '\n'; --end);
for (; end > 0 && buffer[end] != '\n'; --end) ;
}
if(end > 0)
// Parse output lines into the entries list if we have any buffer to parse
if (end > 0)
{
for(int index = 0, last = -1; index <= end; ++index)
for (int index = 0, last = -1; index <= end; ++index)
{
if(buffer[index] == '\n')
if (buffer[index] == '\n')
{
ParseOutputLine(last + 1, index);
last = index;
@ -71,12 +48,13 @@ namespace GitHub.Unity
buffer.Remove(0, end + 1);
}
if(Done)
// Process results when we are done
if (Done)
{
buffer = error.GetStringBuilder();
if (buffer.Length > 0)
buffer = ErrorBuffer.GetStringBuilder();
// We failed. Make a little noise.
if (buffer.Length > 0)
{
Tasks.ReportFailure(FailureSeverity.Moderate, this, buffer.ToString());
if (onFailure != null)
@ -84,22 +62,51 @@ namespace GitHub.Unity
Tasks.ScheduleMainThread(() => onFailure());
}
}
else if (onSuccess != null)
// We succeeded. Hand over the results!
else if (onSuccess != null)
{
Tasks.ScheduleMainThread(() => onSuccess(this));
}
}
}
void ParseOutputLine(int start, int end)
private void ParseOutputLine(int start, int end)
{
string path = output.GetStringBuilder().ToString(start, end - start);
var path = OutputBuffer.GetStringBuilder().ToString(start, end - start);
if (!entries.Any(e => e.Path.Equals(path)))
{
entries.Add(new GitStatusEntry(path, GitFileStatus.Untracked));
}
}
public override bool Blocking
{
get { return false; }
}
public override bool Critical
{
get { return false; }
}
public override bool Cached
{
get { return false; }
}
public override string Label
{
get { return "git ls-files"; }
}
public IList<GitStatusEntry> Entries
{
get { return entries; }
}
protected override string ProcessArguments
{
get { return "ls-files -o --exclude-standard"; }
}
}
}

Просмотреть файл

@ -1,61 +1,40 @@
using UnityEngine;
using UnityEditor;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using System.Collections.Generic;
using System.Globalization;
using UnityEngine;
namespace GitHub.Unity
{
struct GitLogEntry
{
const string
Today = "Today",
Yesterday = "Yesterday";
private const string Today = "Today";
private const string Yesterday = "Yesterday";
public string
CommitID,
MergeA,
MergeB,
AuthorName,
AuthorEmail,
Summary,
Description;
public string CommitID, MergeA, MergeB, AuthorName, AuthorEmail, Summary, Description;
public DateTimeOffset Time;
public List<GitStatusEntry> Changes;
public string ShortID
{
get
{
return CommitID.Length < 7 ? CommitID : CommitID.Substring(0, 7);
}
get { return CommitID.Length < 7 ? CommitID : CommitID.Substring(0, 7); }
}
public string PrettyTimeString
{
get
{
DateTimeOffset
now = DateTimeOffset.Now,
relative = Time.ToLocalTime();
DateTimeOffset now = DateTimeOffset.Now, relative = Time.ToLocalTime();
return string.Format(
"{0}, {1:HH}:{1:mm}",
relative.DayOfYear == now.DayOfYear ? Today :
relative.DayOfYear == now.DayOfYear - 1 ? Yesterday :
relative.ToString("d MMM yyyy"),
relative
);
return String.Format("{0}, {1:HH}:{1:mm}",
relative.DayOfYear == now.DayOfYear
? Today
: relative.DayOfYear == now.DayOfYear - 1 ? Yesterday : relative.ToString("d MMM yyyy"), relative);
}
}
public void Clear()
{
CommitID = MergeA = MergeB = AuthorName = AuthorEmail = Summary = Description = "";
@ -63,106 +42,63 @@ namespace GitHub.Unity
Changes = new List<GitStatusEntry>();
}
public override string ToString()
{
return string.Format(
@"CommitID: {0}
MergeA: {1}
MergeB: {2}
AuthorName: {3}
AuthorEmail: {4}
Time: {5}
Summary: {6}
Description: {7}",
CommitID,
MergeA,
MergeB,
AuthorName,
AuthorEmail,
Time.ToString(),
Summary,
Description
);
var sb = new StringBuilder();
sb.AppendLine(String.Format("CommitID: {0}", CommitID));
sb.AppendLine(String.Format("MergeA: {0}", MergeA));
sb.AppendLine(String.Format("MergeB: {0}", MergeB));
sb.AppendLine(String.Format("AuthorName: {0}", AuthorName));
sb.AppendLine(String.Format("AuthorEmail: {0}", AuthorEmail));
sb.AppendLine(String.Format("Time: {0}", Time.ToString()));
sb.AppendLine(String.Format("Summary: {0}", Summary));
sb.AppendLine(String.Format("Description: {0}", Description));
return sb.ToString();
}
}
class GitLogTask : GitTask
{
enum ParsePhase
private const string UnhandledParsePhaseError = "Unhandled parse phase: '{0}'";
private const string LineParseError = "Log parse error in line: '{0}', parse phase: '{1}'";
private const string GitTimeFormat = "ddd MMM d HH:mm:ss yyyy zz";
private static Action<IList<GitLogEntry>> onLogUpdate;
private string arguments = "log --name-status";
private bool completed = false;
private List<GitLogEntry> entries = new List<GitLogEntry>();
private GitLogEntry parsedEntry = new GitLogEntry();
private ParsePhase parsePhase;
private GitLogTask(string file = null)
{
Commit,
Author,
Time,
Description,
Changes
parsedEntry.Clear();
if (!string.IsNullOrEmpty(file))
{
arguments = String.Format("{0} --follow -m {1}", arguments, file);
}
}
const string
UnhandledParsePhaseError = "Unhandled parse phase: '{0}'",
LineParseError = "Log parse error in line: '{0}', parse phase: '{1}'",
GitTimeFormat = "ddd MMM d HH:mm:ss yyyy zz";
static Action<IList<GitLogEntry>> onLogUpdate;
public static void RegisterCallback(Action<IList<GitLogEntry>> callback)
{
onLogUpdate += callback;
}
public static void UnregisterCallback(Action<IList<GitLogEntry>> callback)
{
onLogUpdate -= callback;
}
public static void Schedule(string file = null)
{
Tasks.Add(new GitLogTask(file));
}
string arguments = "log --name-status";
StringWriter
output = new StringWriter(),
error = new StringWriter();
List<GitLogEntry> entries = new List<GitLogEntry>();
GitLogEntry parsedEntry = new GitLogEntry();
ParsePhase parsePhase;
bool completed = false;
public override bool Blocking { get { return false; } }
public virtual TaskQueueSetting Queued { get { return TaskQueueSetting.QueueSingle; } }
public override bool Critical { get { return false; } }
public override bool Cached { get { return false; } }
public override string Label { get { return "git log"; } }
protected override string ProcessArguments { get { return arguments; } }
protected override TextWriter OutputBuffer { get { return output; } }
protected override TextWriter ErrorBuffer { get { return error; } }
GitLogTask(string file = null)
{
parsedEntry.Clear();
if (!string.IsNullOrEmpty(file))
{
arguments = string.Format("{0} --follow -m {1}", arguments, file);
}
}
protected override void OnProcessOutputUpdate()
{
Utility.ParseLines(output.GetStringBuilder(), ParseOutputLine, Done);
Utility.ParseLines(OutputBuffer.GetStringBuilder(), ParseOutputLine, Done);
if (Done && !completed)
{
@ -172,7 +108,7 @@ Description: {7}",
ParseOutputLine(null);
// Handle failure / success
StringBuilder buffer = error.GetStringBuilder();
var buffer = ErrorBuffer.GetStringBuilder();
if (buffer.Length > 0)
{
Tasks.ReportFailure(FailureSeverity.Moderate, this, buffer.ToString());
@ -184,8 +120,7 @@ Description: {7}",
}
}
void DeliverResult()
private void DeliverResult()
{
if (onLogUpdate != null)
{
@ -195,8 +130,7 @@ Description: {7}",
entries.Clear();
}
void ParseOutputLine(string line)
private void ParseOutputLine(string line)
{
// Empty lines are section or commit dividers
if (string.IsNullOrEmpty(line))
@ -207,10 +141,10 @@ Description: {7}",
entries.Add(parsedEntry);
parsedEntry.Clear();
parsePhase = ParsePhase.Commit;
return;
return;
default:
++parsePhase;
return;
return;
}
}
@ -226,7 +160,8 @@ Description: {7}",
++parsePhase;
return;
}
break;
break;
case ParsePhase.Author:
// If this is a marge commit, merge info comes before author info, so we parse this and stay in the author phase
match = Utility.LogMergeRegex.Match(line);
@ -245,33 +180,25 @@ Description: {7}",
++parsePhase;
return;
}
break;
break;
case ParsePhase.Time:
match = Utility.LogTimeRegex.Match(line);
if (match.Groups.Count == 2)
{
string time = match.Groups[1].ToString();
var time = match.Groups[1].ToString();
parsedEntry.Time = DateTimeOffset.ParseExact(
time,
GitTimeFormat,
System.Globalization.CultureInfo.InvariantCulture,
System.Globalization.DateTimeStyles.None
);
parsedEntry.Time = DateTimeOffset.ParseExact(time, GitTimeFormat, CultureInfo.InvariantCulture, DateTimeStyles.None);
if (DateTimeOffset.TryParseExact(
time,
GitTimeFormat,
CultureInfo.InvariantCulture,
DateTimeStyles.None,
out parsedEntry.Time
))
if (DateTimeOffset.TryParseExact(time, GitTimeFormat, CultureInfo.InvariantCulture, DateTimeStyles.None,
out parsedEntry.Time))
{
// NOTE: Time is always last in the header, so we should not progress to next phase here - the divider will do that
return;
}
}
break;
break;
case ParsePhase.Description:
match = Utility.LogDescriptionRegex.Match(line);
if (match.Groups.Count == 2)
@ -286,31 +213,72 @@ Description: {7}",
}
return;
}
break;
break;
case ParsePhase.Changes:
GitStatusEntry entry;
if (GitStatusEntry.TryParse(line, out entry))
// Try to read the line as a change entry
if (GitStatusEntry.TryParse(line, out entry))
{
parsedEntry.Changes.Add(entry);
return;
}
else if ((match = Utility.LogCommitRegex.Match(line)).Groups.Count == 2)
// This commit had no changes, so complete parsing it and pass the next commit header into a new session
else if ((match = Utility.LogCommitRegex.Match(line)).Groups.Count == 2)
{
ParseOutputLine(null);
ParseOutputLine(line);
return;
}
break;
break;
default:
throw new ApplicationException(string.Format(UnhandledParsePhaseError, parsePhase));
throw new TaskException(String.Format(UnhandledParsePhaseError, parsePhase));
}
// Garbled input. Eject!
Debug.LogErrorFormat(LineParseError, line, parsePhase);
Abort();
}
public override bool Blocking
{
get { return false; }
}
public virtual TaskQueueSetting Queued
{
get { return TaskQueueSetting.QueueSingle; }
}
public override bool Critical
{
get { return false; }
}
public override bool Cached
{
get { return false; }
}
public override string Label
{
get { return "git log"; }
}
protected override string ProcessArguments
{
get { return arguments; }
}
private enum ParsePhase
{
Commit,
Author,
Time,
Description,
Changes
}
}
}

Просмотреть файл

@ -1,12 +1,7 @@
using UnityEngine;
using UnityEditor;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Collections.Generic;
namespace GitHub.Unity
{
@ -20,18 +15,12 @@ namespace GitHub.Unity
Copied
}
struct GitStatus
{
public string
LocalBranch,
RemoteBranch;
public int
Ahead,
Behind;
public string LocalBranch, RemoteBranch;
public int Ahead, Behind;
public List<GitStatusEntry> Entries;
public void Clear()
{
LocalBranch = RemoteBranch = "";
@ -39,34 +28,22 @@ namespace GitHub.Unity
}
}
struct GitStatusEntry
{
const string UnknownStatusKeyError = "Unknown file status key: '{0}'";
private const string UnknownStatusKeyError = "Unknown file status key: '{0}'";
// NOTE: Has to stay in sync with GitFileStatus enum for FileStatusFromKey to function as intended
static readonly string[] GitFileStatusKeys = {
"??",
"M",
"A",
"D",
"R",
"C"
};
private static readonly string[] GitFileStatusKeys = { "??", "M", "A", "D", "R", "C" };
public static bool TryParse(string line, out GitStatusEntry entry)
{
Match match = Utility.StatusStartRegex.Match(line);
string
statusKey = match.Groups["status"].Value,
path = match.Groups["path"].Value;
var match = Utility.StatusStartRegex.Match(line);
string statusKey = match.Groups["status"].Value, path = match.Groups["path"].Value;
if (!string.IsNullOrEmpty(statusKey) && !string.IsNullOrEmpty(path))
{
GitFileStatus status = FileStatusFromKey(statusKey);
int renameIndex = line.IndexOf(Utility.StatusRenameDivider);
var status = FileStatusFromKey(statusKey);
var renameIndex = line.IndexOf(Utility.StatusRenameDivider);
if (renameIndex >= 0)
{
@ -86,29 +63,22 @@ namespace GitHub.Unity
return false;
}
static GitFileStatus FileStatusFromKey(string key)
private static GitFileStatus FileStatusFromKey(string key)
{
for(int index = 0; index < GitFileStatusKeys.Length; ++index)
for (var index = 0; index < GitFileStatusKeys.Length; ++index)
{
if(key.Equals(GitFileStatusKeys[index]))
if (key.Equals(GitFileStatusKeys[index]))
{
return (GitFileStatus)index;
}
}
throw new ArgumentException(string.Format(UnknownStatusKeyError, key));
throw new ArgumentException(String.Format(UnknownStatusKeyError, key));
}
public readonly string
Path,
FullPath,
ProjectPath,
OriginalPath;
public readonly string Path, FullPath, ProjectPath, OriginalPath;
public readonly GitFileStatus Status;
public GitStatusEntry(string path, GitFileStatus status, string originalPath = "")
{
Path = path;
@ -118,65 +88,25 @@ namespace GitHub.Unity
OriginalPath = originalPath;
}
public override int GetHashCode()
{
return Path.GetHashCode();
}
public override string ToString()
{
return string.Format("'{0}': {1}", Path, Status);
return String.Format("'{0}': {1}", Path, Status);
}
}
class GitStatusTask : GitTask
{
const string BranchNamesSeparator = "...";
private const string BranchNamesSeparator = "...";
private static Action<GitStatus> onStatusUpdate;
private GitStatus status;
static Action<GitStatus> onStatusUpdate;
public static void RegisterCallback(Action<GitStatus> callback)
{
onStatusUpdate += callback;
}
public static void UnregisterCallback(Action<GitStatus> callback)
{
onStatusUpdate -= callback;
}
public static void Schedule()
{
GitListUntrackedFilesTask.Schedule(task => Tasks.Add(new GitStatusTask(task.Entries)));
}
public override bool Blocking { get { return false; } }
public virtual TaskQueueSetting Queued { get { return TaskQueueSetting.QueueSingle; } }
public override bool Critical { get { return false; } }
public override bool Cached { get { return false; } }
public override string Label { get { return "git status"; } }
protected override string ProcessArguments { get { return "status -b --porcelain"; } }
protected override TextWriter OutputBuffer { get { return output; } }
protected override TextWriter ErrorBuffer { get { return error; } }
StringWriter
output = new StringWriter(),
error = new StringWriter();
GitStatus status;
GitStatusTask(IList<GitStatusEntry> existingEntries = null)
private GitStatusTask(IList<GitStatusEntry> existingEntries = null)
{
status.Entries = new List<GitStatusEntry>();
if (existingEntries != null)
@ -185,30 +115,42 @@ namespace GitHub.Unity
}
}
public static void RegisterCallback(Action<GitStatus> callback)
{
onStatusUpdate += callback;
}
public static void UnregisterCallback(Action<GitStatus> callback)
{
onStatusUpdate -= callback;
}
public static void Schedule()
{
GitListUntrackedFilesTask.Schedule(task => Tasks.Add(new GitStatusTask(task.Entries)));
}
protected override void OnProcessOutputUpdate()
{
Utility.ParseLines(output.GetStringBuilder(), ParseOutputLine, Done);
Utility.ParseLines(OutputBuffer.GetStringBuilder(), ParseOutputLine, Done);
if(Done)
// If we are done, hand over the results to any listeners on the main thread
if (Done)
{
Tasks.ScheduleMainThread(DeliverResult);
}
}
void DeliverResult()
private void DeliverResult()
{
if(onStatusUpdate != null)
if (onStatusUpdate != null)
{
onStatusUpdate(status);
}
status.Clear();
}
void ParseOutputLine(string line)
private void ParseOutputLine(string line)
{
GitStatusEntry entry;
@ -222,27 +164,26 @@ namespace GitHub.Unity
return;
}
// Grab local and remote branch
if (Utility.StatusBranchLineValidRegex.Match(line).Success)
{
int index = line.IndexOf(BranchNamesSeparator);
if (index >= 0)
var index = line.IndexOf(BranchNamesSeparator);
// Remote branch available
if (index >= 0)
{
status.LocalBranch = line.Substring(2, index - 2);
status.RemoteBranch = line.Substring(index + BranchNamesSeparator.Length);
index = status.RemoteBranch.IndexOf('[');
if (index > 0)
// Ahead and/or behind information available
if (index > 0)
{
Match match = Utility.StatusAheadBehindRegex.Match(status.RemoteBranch.Substring(index - 1));
var match = Utility.StatusAheadBehindRegex.Match(status.RemoteBranch.Substring(index - 1));
status.RemoteBranch = status.RemoteBranch.Substring(0, index).Trim();
string
aheadString = match.Groups["ahead"].Value,
behindString = match.Groups["behind"].Value;
string aheadString = match.Groups["ahead"].Value, behindString = match.Groups["behind"].Value;
status.Ahead = string.IsNullOrEmpty(aheadString) ? 0 : Int32.Parse(aheadString);
status.Behind = string.IsNullOrEmpty(behindString) ? 0 : Int32.Parse(behindString);
@ -252,12 +193,42 @@ namespace GitHub.Unity
status.RemoteBranch = status.RemoteBranch.Trim();
}
}
else
// No remote branch
else
{
status.LocalBranch = line.Substring(2).Trim();
}
}
}
public override bool Blocking
{
get { return false; }
}
public virtual TaskQueueSetting Queued
{
get { return TaskQueueSetting.QueueSingle; }
}
public override bool Critical
{
get { return false; }
}
public override bool Cached
{
get { return false; }
}
public override string Label
{
get { return "git status"; }
}
protected override string ProcessArguments
{
get { return "status -b --porcelain"; }
}
}
}

Просмотреть файл

@ -1,62 +1,39 @@
using UnityEngine;
using UnityEditor;
using System;
using System.IO;
using System.Text;
namespace GitHub.Unity
{
class GitSwitchBranchesTask : GitTask
{
const string SwitchConfirmedMessage = "Switched to branch '{0}'";
private const string SwitchConfirmedMessage = "Switched to branch '{0}'";
private string branch;
private Action onFailure;
private Action onSuccess;
public static void Schedule(string branch, Action onSuccess, Action onFailure = null)
{
Tasks.Add(new GitSwitchBranchesTask(branch, onSuccess, onFailure));
}
string branch;
Action
onSuccess,
onFailure;
StringWriter
output = new StringWriter(),
error = new StringWriter();
public override bool Blocking { get { return true; } }
public override TaskQueueSetting Queued { get { return TaskQueueSetting.QueueSingle; } }
public override bool Critical { get { return true; } }
public override bool Cached { get { return false; } }
public override string Label { get { return "git checkout"; } }
protected override string ProcessArguments { get { return string.Format("checkout {0}", branch); } }
protected override TextWriter OutputBuffer { get { return output; } }
protected override TextWriter ErrorBuffer { get { return error; } }
GitSwitchBranchesTask(string branch, Action onSuccess, Action onFailure = null)
private GitSwitchBranchesTask(string branch, Action onSuccess, Action onFailure = null)
{
this.branch = branch;
this.onSuccess = onSuccess;
this.onFailure = onFailure;
}
public static void Schedule(string branch, Action onSuccess, Action onFailure = null)
{
Tasks.Add(new GitSwitchBranchesTask(branch, onSuccess, onFailure));
}
protected override void OnProcessOutputUpdate()
{
if (Done)
{
// Handle failure / success
StringBuilder buffer = error.GetStringBuilder();
var buffer = ErrorBuffer.GetStringBuilder();
if (buffer.Length > 0)
{
string message = buffer.ToString().Trim();
var message = buffer.ToString().Trim();
if (!message.Equals(string.Format(SwitchConfirmedMessage, branch)))
if (!message.Equals(String.Format(SwitchConfirmedMessage, branch)))
{
Tasks.ReportFailure(FailureSeverity.Critical, this, message);
if (onFailure != null)
@ -74,5 +51,35 @@ namespace GitHub.Unity
}
}
}
public override bool Blocking
{
get { return true; }
}
public override TaskQueueSetting Queued
{
get { return TaskQueueSetting.QueueSingle; }
}
public override bool Critical
{
get { return true; }
}
public override bool Cached
{
get { return false; }
}
public override string Label
{
get { return "git checkout"; }
}
protected override string ProcessArguments
{
get { return String.Format("checkout {0}", branch); }
}
}
}

Просмотреть файл

@ -2,11 +2,7 @@ namespace GitHub.Unity
{
class GitTask : ProcessTask
{
const string NoGitError = "Tried to run git task while git was not found.";
protected override string ProcessName { get { return Utility.GitInstallPath; } }
private const string NoGitError = "Tried to run git task while git was not found.";
public override void Run()
{
@ -19,5 +15,10 @@ namespace GitHub.Unity
base.Run();
}
protected override string ProcessName
{
get { return Utility.GitInstallPath; }
}
}
}

Просмотреть файл

@ -1,54 +1,35 @@
using UnityEngine;
using UnityEditor;
using System;
using System.IO;
using System.Collections.Generic;
using System.Threading;
using System.Diagnostics;
using System.IO;
using System.Threading;
using UnityEditor;
using Debug = UnityEngine.Debug;
namespace GitHub.Unity
{
class ProcessTask : ITask
class ProcessTask : ITask, IDisposable
{
const int ExitMonitorSleep = 10;
[MenuItem("Assets/GitHub/Process Test")]
static void Test()
{
EditorApplication.delayCall += () => Tasks.Add(new ProcessTask());
}
public virtual bool Blocking { get { return true; } }
public virtual float Progress { get; protected set; }
public virtual bool Done { get; protected set; }
public virtual TaskQueueSetting Queued { get { return TaskQueueSetting.Queue; } }
public virtual bool Critical { get { return true; } }
public virtual bool Cached { get { return true; } }
public virtual Action<ITask> OnBegin { get; set; }
public virtual Action<ITask> OnEnd { get; set; }
public virtual string Label { get { return "Process task"; } }
protected virtual string ProcessName { get { return "sleep"; } }
protected virtual string ProcessArguments { get { return "20"; } }
protected virtual CachedTask CachedTaskType { get { return CachedTask.ProcessTask; } }
protected virtual TextWriter OutputBuffer { get { return null; } }
protected virtual TextWriter ErrorBuffer { get { return null; } }
Process process;
private const int ExitMonitorSleep = 10;
private readonly StringWriter error = new StringWriter();
private readonly StringWriter output = new StringWriter();
private Process process;
protected ProcessTask()
{}
[MenuItem("Assets/GitHub/Process Test")]
public static void Test()
{
EditorApplication.delayCall += () => Tasks.Add(new ProcessTask());
}
/// <summary>
/// Try to reattach to the process. Assume that we're done if that fails.
/// </summary>
/// <returns></returns>
public static ProcessTask Parse(IDictionary<string, object> data)
// Try to reattach to the process. Assume that we're done if that fails.
{
Process resumedProcess;
@ -57,20 +38,18 @@ namespace GitHub.Unity
resumedProcess = Process.GetProcessById((int)(Int64)data[Tasks.ProcessKey]);
resumedProcess.StartInfo.RedirectStandardOutput = resumedProcess.StartInfo.RedirectStandardError = true;
}
catch(Exception)
catch (Exception)
{
resumedProcess = null;
}
return new ProcessTask()
{
return new ProcessTask() {
process = resumedProcess,
Done = resumedProcess == null,
Progress = resumedProcess == null ? 1f : 0f
};
}
public virtual void Run()
{
Debug.LogFormat("{0} {1}", Label, process == null ? "start" : "reconnect");
@ -78,66 +57,53 @@ namespace GitHub.Unity
Done = false;
Progress = 0.0f;
if(OnBegin != null)
if (OnBegin != null)
{
OnBegin(this);
}
if(process == null)
// Only start the process if we haven't already reconnected to an existing instance
if (process == null)
{
process = Process.Start(new ProcessStartInfo(ProcessName, ProcessArguments)
{
CreateNoWindow = true,
UseShellExecute = false,
RedirectStandardError = true,
RedirectStandardOutput = true,
WorkingDirectory = Utility.GitRoot
});
process =
Process.Start(new ProcessStartInfo(ProcessName, ProcessArguments) {
CreateNoWindow = true,
UseShellExecute = false,
RedirectStandardError = true,
RedirectStandardOutput = true,
WorkingDirectory = Utility.GitRoot
});
}
TextWriter
output = OutputBuffer,
error = ErrorBuffer;
// NOTE: WaitForExit is too low level here. Won't be properly interrupted by thread abort.
do
{
// Wait a bit
Thread.Sleep(ExitMonitorSleep);
// Read all available process output //
// Read all available process output
var updated = false;
bool updated = false;
while(!process.StandardOutput.EndOfStream)
while (!process.StandardOutput.EndOfStream)
{
char read = (char)process.StandardOutput.Read();
if(output != null)
{
output.Write(read);
updated = true;
}
var read = (char)process.StandardOutput.Read();
OutputBuffer.Write(read);
updated = true;
}
while(!process.StandardError.EndOfStream)
while (!process.StandardError.EndOfStream)
{
char read = (char)process.StandardError.Read();
if(error != null)
{
error.Write(read);
updated = true;
}
var read = (char)process.StandardError.Read();
ErrorBuffer.Write(read);
updated = true;
}
// Notify if anything was read //
if(updated)
// Notify if anything was read
if (updated)
{
OnProcessOutputUpdate();
}
}
while(!process.HasExited);
} while (!process.HasExited);
Progress = 1.0f;
Done = true;
@ -146,13 +112,12 @@ namespace GitHub.Unity
Debug.LogFormat("{0} end", Label);
if(OnEnd != null)
if (OnEnd != null)
{
OnEnd(this);
}
}
public void Abort()
{
Debug.LogFormat("Aborting {0}", Label);
@ -161,18 +126,17 @@ namespace GitHub.Unity
{
process.Kill();
}
catch(Exception)
catch (Exception)
{}
Done = true;
if(OnEnd != null)
if (OnEnd != null)
{
OnEnd(this);
}
}
public void Disconnect()
{
Debug.LogFormat("Disconnect {0}", Label);
@ -180,27 +144,95 @@ namespace GitHub.Unity
process = null;
}
public void Reconnect()
{}
public void WriteCache(TextWriter cache)
{
Debug.LogFormat("Writing cache for {0}", Label);
cache.Write(
@"{{
""{0}"": ""{1}"",
""{2}"": {3}
}}",
Tasks.TypeKey, CachedTaskType,
Tasks.ProcessKey, process == null ? -1 : process.Id
);
cache.WriteLine("{");
cache.WriteLine(String.Format("\"{0}\": \"{1}\",", Tasks.TypeKey, CachedTaskType));
cache.WriteLine(String.Format("\"{0}\": \"{1}\"", Tasks.ProcessKey, process == null ? -1 : process.Id));
cache.WriteLine("}");
}
protected virtual void OnProcessOutputUpdate()
{}
bool disposed = false;
public virtual void Dispose(bool disposing)
{
if (disposing)
{
if (!disposed)
{
disposed = true;
ErrorBuffer.Dispose();
OutputBuffer.Dispose();
}
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
public virtual bool Blocking
{
get { return true; }
}
public virtual float Progress { get; protected set; }
public virtual bool Done { get; protected set; }
public virtual TaskQueueSetting Queued
{
get { return TaskQueueSetting.Queue; }
}
public virtual bool Critical
{
get { return true; }
}
public virtual bool Cached
{
get { return true; }
}
public virtual Action<ITask> OnBegin { get; set; }
public virtual Action<ITask> OnEnd { get; set; }
public virtual string Label
{
get { return "Process task"; }
}
protected virtual string ProcessName
{
get { return "sleep"; }
}
protected virtual string ProcessArguments
{
get { return "20"; }
}
protected virtual CachedTask CachedTaskType
{
get { return CachedTask.ProcessTask; }
}
protected virtual StringWriter OutputBuffer
{
get { return output; }
}
protected virtual StringWriter ErrorBuffer
{
get { return error; }
}
}
}

Просмотреть файл

@ -0,0 +1,25 @@
using System;
using System.Runtime.Serialization;
namespace GitHub.Unity
{
[Serializable]
public class TaskException : Exception
{
public TaskException()
{
}
public TaskException(string message) : base(message)
{
}
public TaskException(string message, Exception innerException) : base(message, innerException)
{
}
protected TaskException(SerializationInfo info, StreamingContext context) : base(info, context)
{
}
}
}

Просмотреть файл

@ -1,14 +1,12 @@
using UnityEngine;
using UnityEditor;
using UnityEngine.Events;
using System.Reflection;
using System.Collections.Generic;
using System.Threading;
using System.Text;
using System.IO;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
using UnityEditor;
using UnityEngine;
using UnityEngine.Events;
/*
@ -22,7 +20,6 @@ using System.Linq;
*/
namespace GitHub.Unity
{
enum TaskQueueSetting
@ -32,9 +29,13 @@ namespace GitHub.Unity
QueueSingle
}
interface ITask
{
void Run();
void Abort();
void Disconnect();
void Reconnect();
void WriteCache(TextWriter cache);
bool Blocking { get; }
float Progress { get; }
bool Done { get; }
@ -44,107 +45,76 @@ namespace GitHub.Unity
Action<ITask> OnBegin { set; }
Action<ITask> OnEnd { set; }
string Label { get; }
void Run();
void Abort();
void Disconnect();
void Reconnect();
void WriteCache(TextWriter cache);
};
enum CachedTask
{
TestTask,
ProcessTask
};
enum FailureSeverity
{
Moderate,
Critical
};
class Tasks
{
enum WaitMode
internal const string TypeKey = "type", ProcessKey = "process";
private const int NoTasksSleep = 100;
private const int BlockingTaskWaitSleep = 10;
private const int FailureDelayDefault = 1;
private const int FailureDelayLong = 5000;
private const string CacheFileName = "GitHubCache";
private const string QuitActionFieldName = "editorApplicationQuit";
private const string TaskThreadExceptionRestartError = "GitHub task thread restarting after encountering an exception: {0}";
private const string TaskCacheWriteExceptionError = "GitHub: Exception when writing task cache: {0}";
private const string TaskCacheParseError = "GitHub: Failed to parse task cache";
private const string TaskParseUnhandledTypeError = "GitHub: Trying to parse unhandled cached task: {0}";
private const string TaskFailureTitle = "GitHub";
private const string TaskFailureMessage = "{0} failed:\n{1}";
private const string TaskFailureOK = "OK";
private const string TaskProgressTitle = "GitHub";
private const string TaskBlockingTitle = "Critical GitHub task";
private const string TaskBlockingDescription = "A critical GitHub task ({0}) has yet to complete. What would you like to do?";
private const string TaskBlockingComplete = "Complete";
private const string TaskBlockingInterrupt = "Interrupt";
private const BindingFlags kQuitActionBindingFlags = BindingFlags.NonPublic | BindingFlags.Static;
private static FieldInfo quitActionField;
private static ProgressBarDisplayMethod displayBackgroundProgressBar;
private static Action clearBackgroundProgressBar;
private ITask activeTask;
private Exception lastException;
private bool running = false;
private Queue<ITask> tasks;
private object tasksLock = new object();
private Thread thread;
private Tasks()
{
Background,
Modal,
Blocking
};
delegate void ProgressBarDisplayMethod(string text, float progress);
internal const string
TypeKey = "type",
ProcessKey = "process";
const int
NoTasksSleep = 100,
BlockingTaskWaitSleep = 10,
FailureDelayDefault = 1,
FailureDelayLong = 5000;
const string
CacheFileName = "GitHubCache",
QuitActionFieldName = "editorApplicationQuit",
TaskThreadExceptionRestartError = "GitHub task thread restarting after encountering an exception: {0}",
TaskCacheWriteExceptionError = "GitHub: Exception when writing task cache: {0}",
TaskCacheParseError = "GitHub: Failed to parse task cache",
TaskParseUnhandledTypeError = "GitHub: Trying to parse unhandled cached task: {0}",
TaskFailureTitle = "GitHub",
TaskFailureMessage = "{0} failed:\n{1}",
TaskFailureOK = "OK",
TaskProgressTitle = "GitHub",
TaskBlockingTitle = "Critical GitHub task",
TaskBlockingDescription = "A critical GitHub task ({0}) has yet to complete. What would you like to do?",
TaskBlockingComplete = "Complete",
TaskBlockingInterrupt = "Interrupt";
const BindingFlags kQuitActionBindingFlags = BindingFlags.NonPublic | BindingFlags.Static;
static FieldInfo quitActionField;
static ProgressBarDisplayMethod displayBackgroundProgressBar;
static Action clearBackgroundProgressBar;
static Tasks Instance { get; set; }
static string CacheFilePath { get; set; }
static void SecureQuitActionField()
{
if(quitActionField == null)
{
quitActionField = typeof(EditorApplication).GetField(QuitActionFieldName, kQuitActionBindingFlags);
if(quitActionField == null)
editorApplicationQuit = (UnityAction)Delegate.Combine(editorApplicationQuit, new UnityAction(OnQuit));
CacheFilePath = Path.Combine(Application.dataPath, Path.Combine("..", Path.Combine("Temp", CacheFileName)));
EditorApplication.playmodeStateChanged += () => {
if (EditorApplication.isPlayingOrWillChangePlaymode && !EditorApplication.isPlaying)
{
throw new NullReferenceException("Unable to reflect EditorApplication." + QuitActionFieldName);
OnPlaymodeEnter();
}
};
tasks = new Queue<ITask>();
if (File.Exists(CacheFilePath))
{
ReadCache();
File.Delete(CacheFilePath);
OnSessionRestarted();
}
}
static UnityAction editorApplicationQuit
{
get
{
SecureQuitActionField();
return (UnityAction)quitActionField.GetValue(null);
}
set
{
SecureQuitActionField();
quitActionField.SetValue(null, value);
}
}
// "Everything is broken - let's rebuild from the ashes (read: cache)"
public static void Initialize()
{
@ -157,46 +127,12 @@ namespace GitHub.Unity
Instance.thread.Start();
}
bool running = false;
Thread thread;
ITask activeTask;
Queue<ITask> tasks;
object tasksLock = new object();
Exception lastException;
Tasks()
{
editorApplicationQuit = (UnityAction)Delegate.Combine(editorApplicationQuit, new UnityAction(OnQuit));
CacheFilePath = Path.Combine(Application.dataPath, Path.Combine("..", Path.Combine("Temp", CacheFileName)));
EditorApplication.playmodeStateChanged += () =>
{
if(EditorApplication.isPlayingOrWillChangePlaymode && !EditorApplication.isPlaying)
{
OnPlaymodeEnter();
}
};
tasks = new Queue<ITask>();
if(File.Exists(CacheFilePath))
{
ReadCache();
File.Delete(CacheFilePath);
OnSessionRestarted();
}
}
public static void Add(ITask task)
{
lock(Instance.tasksLock)
{
if(
(task.Queued == TaskQueueSetting.NoQueue && Instance.tasks.Count > 0) ||
(task.Queued == TaskQueueSetting.QueueSingle && Instance.tasks.Any(t => t.GetType() == task.GetType()))
)
if ((task.Queued == TaskQueueSetting.NoQueue && Instance.tasks.Count > 0) ||
(task.Queued == TaskQueueSetting.QueueSingle && Instance.tasks.Any(t => t.GetType() == task.GetType())))
{
return;
}
@ -207,10 +143,63 @@ namespace GitHub.Unity
Instance.WriteCache();
}
void Start()
public static void ScheduleMainThread(Action action)
{
while(true)
EditorApplication.delayCall += () => action();
}
public static void ReportFailure(FailureSeverity severity, ITask task, string error)
{
if (severity == FailureSeverity.Moderate)
{
Debug.LogErrorFormat(TaskFailureMessage, task.Label, error);
}
else
{
ScheduleMainThread(
() => EditorUtility.DisplayDialog(TaskFailureTitle, String.Format(TaskFailureMessage, task.Label, error), TaskFailureOK));
}
}
private static void SecureQuitActionField()
{
if (quitActionField == null)
{
quitActionField = typeof(EditorApplication).GetField(QuitActionFieldName, kQuitActionBindingFlags);
if (quitActionField == null)
{
throw new TaskException("Unable to reflect EditorApplication." + QuitActionFieldName);
}
}
}
private static void DisplayBackgroundProgressBar(string description, float progress)
{
if (displayBackgroundProgressBar == null)
{
var type = typeof(EditorWindow).Assembly.GetType("UnityEditor.AsyncProgressBar");
displayBackgroundProgressBar =
(ProgressBarDisplayMethod)
Delegate.CreateDelegate(typeof(ProgressBarDisplayMethod),
type.GetMethod("Display", new Type[] { typeof(string), typeof(float) }));
}
displayBackgroundProgressBar(description, progress);
}
private static void ClearBackgroundProgressBar()
{
if (clearBackgroundProgressBar == null)
{
var type = typeof(EditorWindow).Assembly.GetType("UnityEditor.AsyncProgressBar");
clearBackgroundProgressBar = (Action)Delegate.CreateDelegate(typeof(Action), type.GetMethod("Clear", new Type[] { }));
}
clearBackgroundProgressBar();
}
private void Start()
{
while (true)
{
try
{
@ -218,15 +207,15 @@ namespace GitHub.Unity
break;
}
catch(ThreadAbortException)
// Aborted by domain unload or explicitly via the editor quit handler. Button down the hatches.
// Aborted by domain unload or explicitly via the editor quit handler. Button down the hatches.
catch (ThreadAbortException)
{
running = false;
// Disconnect or abort the active task
if(activeTask != null && !activeTask.Done)
if (activeTask != null && !activeTask.Done)
{
if(activeTask.Cached)
if (activeTask.Cached)
{
try
{
@ -252,14 +241,14 @@ namespace GitHub.Unity
break;
}
catch(Exception e)
// Something broke internally - reboot
// Something broke internally - reboot
catch (Exception e)
{
running = false;
bool repeat = lastException != null && e.TargetSite.Equals(lastException.TargetSite);
var repeat = lastException != null && e.TargetSite.Equals(lastException.TargetSite);
lastException = e;
if(!repeat)
if (!repeat)
{
Debug.LogErrorFormat(TaskThreadExceptionRestartError, e);
Thread.Sleep(FailureDelayDefault);
@ -272,60 +261,56 @@ namespace GitHub.Unity
}
}
void OnPlaymodeEnter()
// About to enter playmode
private void OnPlaymodeEnter()
{
if(activeTask != null)
if (activeTask != null)
{
ClearBackgroundProgressBar();
EditorUtility.ClearProgressBar();
}
}
void OnSessionRestarted()
// A recompile or playmode enter/exit cause the script environment to reload while we had tasks at hand
private void OnSessionRestarted()
{
ClearBackgroundProgressBar();
EditorUtility.ClearProgressBar();
if(activeTask != null)
if (activeTask != null)
{
activeTask.Reconnect();
}
}
void OnQuit()
private void OnQuit()
{
// Stop the queue
running = false;
if(activeTask != null && activeTask.Critical)
if (activeTask != null && activeTask.Critical)
{
WaitForTask(activeTask, WaitMode.Blocking);
}
}
void RunInternal()
private void RunInternal()
{
running = true;
while(running)
while (running)
{
// Clear any completed task
if(activeTask != null && activeTask.Done)
if (activeTask != null && activeTask.Done)
{
activeTask = null;
}
// Grab a new task
if(activeTask == null)
if (activeTask == null)
{
lock(tasksLock)
{
if(tasks.Count > 0)
if (tasks.Count > 0)
{
activeTask = tasks.Dequeue();
activeTask.OnBegin = task => ScheduleMainThread(WriteCache);
@ -333,12 +318,11 @@ namespace GitHub.Unity
}
}
if(activeTask != null)
// Run and monitor active task
if (activeTask != null)
{
ScheduleMainThread(() =>
{
if(activeTask != null)
ScheduleMainThread(() => {
if (activeTask != null)
{
WaitForTask(activeTask, activeTask.Blocking ? WaitMode.Modal : WaitMode.Background);
}
@ -357,16 +341,15 @@ namespace GitHub.Unity
thread.Abort();
}
void WriteCache()
private void WriteCache()
{
try
{
StreamWriter cache = File.CreateText(CacheFilePath);
var cache = File.CreateText(CacheFilePath);
cache.Write("[");
// Cache the active task
if(activeTask != null && !activeTask.Done && activeTask.Cached)
if (activeTask != null && !activeTask.Done && activeTask.Cached)
{
activeTask.WriteCache(cache);
}
@ -378,9 +361,9 @@ namespace GitHub.Unity
// Cache the queue
lock(tasksLock)
{
foreach(ITask task in tasks)
foreach (var task in tasks)
{
if(!task.Cached)
if (!task.Cached)
{
continue;
}
@ -393,38 +376,37 @@ namespace GitHub.Unity
cache.Write("]");
cache.Close();
}
catch(Exception e)
catch (Exception e)
{
Debug.LogErrorFormat(TaskCacheWriteExceptionError, e);
}
}
bool ReadCache()
private bool ReadCache()
{
string text = File.ReadAllText(CacheFilePath);
var text = File.ReadAllText(CacheFilePath);
object parseResult;
IList<object> cache;
// Parse root list with at least one item (active task) or fail
if(!SimpleJson.TryDeserializeObject(text, out parseResult) || (cache = parseResult as IList<object>) == null || cache.Count < 1)
if (!SimpleJson.TryDeserializeObject(text, out parseResult) || (cache = parseResult as IList<object>) == null || cache.Count < 1)
{
Debug.LogError(TaskCacheParseError);
return false;
}
// Parse active task
IDictionary<string, object> taskData = cache[0] as IDictionary<string, object>;
ITask cachedActiveTask = (taskData != null) ? ParseTask(taskData) : null;
var taskData = cache[0] as IDictionary<string, object>;
var cachedActiveTask = taskData != null ? ParseTask(taskData) : null;
// Parse tasks list or fail
Queue<ITask> cachedTasks = new Queue<ITask>(cache.Count - 1);
for(int index = 1; index < cache.Count; ++index)
var cachedTasks = new Queue<ITask>(cache.Count - 1);
for (var index = 1; index < cache.Count; ++index)
{
taskData = cache[index] as IDictionary<string, object>;
if(taskData == null)
if (taskData == null)
{
Debug.LogError(TaskCacheParseError);
return false;
@ -440,8 +422,7 @@ namespace GitHub.Unity
return true;
}
ITask ParseTask(IDictionary<string, object> data)
private ITask ParseTask(IDictionary<string, object> data)
{
CachedTask type;
@ -449,81 +430,77 @@ namespace GitHub.Unity
{
type = (CachedTask)Enum.Parse(typeof(CachedTask), (string)data[TypeKey]);
}
catch(Exception)
catch (Exception)
{
return null;
}
try
{
switch(type)
switch (type)
{
case CachedTask.TestTask:
return TestTask.Parse(data);
return TestTask.Parse(data);
case CachedTask.ProcessTask:
return ProcessTask.Parse(data);
return ProcessTask.Parse(data);
default:
Debug.LogErrorFormat(TaskParseUnhandledTypeError, type);
return null;
return null;
}
}
catch(Exception)
catch (Exception)
{
return null;
}
}
void WaitForTask(ITask task, WaitMode mode = WaitMode.Background)
// Update progress bars to match progress of given task
/// <summary>
/// Update progress bars to match progress of given task
/// </summary>
private void WaitForTask(ITask task, WaitMode mode = WaitMode.Background)
{
if(activeTask != task)
if (activeTask != task)
{
return;
}
if(mode == WaitMode.Background)
// Unintrusive background process
if (mode == WaitMode.Background)
{
task.OnEnd = OnWaitingBackgroundTaskEnd;
DisplayBackgroundProgressBar(task.Label, task.Progress);
if(!task.Done)
if (!task.Done)
{
ScheduleMainThread(() => WaitForTask(task, mode));
}
}
else if(mode == WaitMode.Modal)
// Obstruct editor interface, while offering cancel button
else if (mode == WaitMode.Modal)
{
task.OnEnd = OnWaitingModalTaskEnd;
if(!EditorUtility.DisplayCancelableProgressBar(TaskProgressTitle, task.Label, task.Progress) && !task.Done)
if (!EditorUtility.DisplayCancelableProgressBar(TaskProgressTitle, task.Label, task.Progress) && !task.Done)
{
ScheduleMainThread(() => WaitForTask(task, mode));
}
else if(!task.Done)
else if (!task.Done)
{
task.Abort();
}
}
else
// Offer to interrupt task via dialog box, else block main thread until completion
else
{
if(EditorUtility.DisplayDialog(
TaskBlockingTitle,
string.Format(TaskBlockingDescription, task.Label),
TaskBlockingComplete,
TaskBlockingInterrupt
))
if (EditorUtility.DisplayDialog(TaskBlockingTitle, String.Format(TaskBlockingDescription, task.Label), TaskBlockingComplete,
TaskBlockingInterrupt))
{
do
{
EditorUtility.DisplayProgressBar(TaskProgressTitle, task.Label, task.Progress);
Thread.Sleep(BlockingTaskWaitSleep);
}
while(!task.Done);
} while (!task.Done);
EditorUtility.ClearProgressBar();
}
@ -534,62 +511,40 @@ namespace GitHub.Unity
}
}
static void DisplayBackgroundProgressBar(string description, float progress)
{
if(displayBackgroundProgressBar == null)
{
Type type = typeof(EditorWindow).Assembly.GetType("UnityEditor.AsyncProgressBar");
displayBackgroundProgressBar = (ProgressBarDisplayMethod)Delegate.CreateDelegate(
typeof(ProgressBarDisplayMethod),
type.GetMethod("Display", new Type[]{ typeof(string), typeof(float) })
);
}
displayBackgroundProgressBar(description, progress);
}
static void ClearBackgroundProgressBar()
{
if(clearBackgroundProgressBar == null)
{
Type type = typeof(EditorWindow).Assembly.GetType("UnityEditor.AsyncProgressBar");
clearBackgroundProgressBar = (Action)Delegate.CreateDelegate(typeof(Action), type.GetMethod("Clear", new Type[]{}));
}
clearBackgroundProgressBar();
}
void OnWaitingBackgroundTaskEnd(ITask task)
private void OnWaitingBackgroundTaskEnd(ITask task)
{
ScheduleMainThread(() => ClearBackgroundProgressBar());
}
void OnWaitingModalTaskEnd(ITask task)
private void OnWaitingModalTaskEnd(ITask task)
{
ScheduleMainThread(() => EditorUtility.ClearProgressBar());
}
private static Tasks Instance { get; set; }
private static string CacheFilePath { get; set; }
public static void ScheduleMainThread(Action action)
private static UnityAction editorApplicationQuit
{
EditorApplication.delayCall += () => action();
}
public static void ReportFailure(FailureSeverity severity, ITask task, string error)
{
if (severity == FailureSeverity.Moderate)
get
{
Debug.LogErrorFormat(TaskFailureMessage, task.Label, error);
SecureQuitActionField();
return (UnityAction)quitActionField.GetValue(null);
}
else
set
{
ScheduleMainThread(() => EditorUtility.DisplayDialog(TaskFailureTitle, string.Format(TaskFailureMessage, task.Label, error), TaskFailureOK));
SecureQuitActionField();
quitActionField.SetValue(null, value);
}
}
private enum WaitMode
{
Background,
Modal,
Blocking
};
private delegate void ProgressBarDisplayMethod(string text, float progress);
}
}

Просмотреть файл

@ -1,66 +1,39 @@
using UnityEngine;
using UnityEditor;
using System.Threading;
using System;
using System.IO;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using UnityEditor;
using UnityEngine;
namespace GitHub.Unity
{
class TestTask : ITask
{
[MenuItem("Assets/GitHub/Test Blocking Critical")]
static void TestA()
{
Test(new TestTask(true));
}
private bool reconnecting = false;
[MenuItem("Assets/GitHub/Test Non-blocking Critical")]
static void TestB()
{
Test(new TestTask(false));
}
static void Test(TestTask task)
{
EditorApplication.delayCall += () => Tasks.Add(task);
}
public static TestTask Parse(IDictionary<string, object> data)
{
return new TestTask(false)
{
reconnecting = true,
Done = false
};
}
TestTask(bool shouldBlock)
private TestTask(bool shouldBlock)
{
Blocking = shouldBlock;
Done = false;
Progress = 0.0f;
}
public static TestTask Parse(IDictionary<string, object> data)
{
return new TestTask(false) { reconnecting = true, Done = false };
}
public bool Blocking { get; protected set; }
public float Progress { get; protected set; }
public bool Done { get; protected set; }
public TaskQueueSetting Queued { get { return TaskQueueSetting.Queue; } }
public bool Critical { get { return true; } }
public bool Cached { get { return true; } }
public Action<ITask> OnBegin { get; set; }
public Action<ITask> OnEnd { get; set; }
public string Label { get { return "Test task"; } }
bool reconnecting = false;
[MenuItem("Assets/GitHub/Test Blocking Critical")]
public static void TestA()
{
Test(new TestTask(true));
}
[MenuItem("Assets/GitHub/Test Non-blocking Critical")]
public static void TestB()
{
Test(new TestTask(false));
}
public void Run()
{
@ -69,19 +42,17 @@ namespace GitHub.Unity
Done = false;
Progress = 0.0f;
if(OnBegin != null)
if (OnBegin != null)
{
OnBegin(this);
}
const int
kSteps = 20,
kStepSleep = 1000;
const int kSteps = 20, kStepSleep = 1000;
for(int step = 0; !Done && step < kSteps; ++step)
for (var step = 0; !Done && step < kSteps; ++step)
{
Progress = step / (float)kSteps;
Thread.Sleep (kStepSleep);
Thread.Sleep(kStepSleep);
}
Progress = 1.0f;
@ -89,13 +60,12 @@ namespace GitHub.Unity
Debug.LogFormat("{0} end", Label);
if(OnEnd != null)
if (OnEnd != null)
{
OnEnd(this);
}
}
public void Abort()
{
Debug.LogFormat("Aborting {0}", Label);
@ -103,27 +73,52 @@ namespace GitHub.Unity
Done = true;
}
public void Disconnect()
{
Abort();
}
public void Reconnect()
{}
public void WriteCache(TextWriter cache)
{
Debug.LogFormat("Writing cache for {0}", Label);
cache.WriteLine("{");
cache.WriteLine("\"{0}\": \"{1}\"", Tasks.TypeKey, CachedTask.TestTask);
cache.WriteLine("}");
}
cache.Write(
@"{{
""{0}"": ""{1}""
}}",
Tasks.TypeKey, CachedTask.TestTask
);
private static void Test(TestTask task)
{
EditorApplication.delayCall += () => Tasks.Add(task);
}
public bool Blocking { get; protected set; }
public float Progress { get; protected set; }
public bool Done { get; protected set; }
public TaskQueueSetting Queued
{
get { return TaskQueueSetting.Queue; }
}
public bool Critical
{
get { return true; }
}
public bool Cached
{
get { return true; }
}
public Action<ITask> OnBegin { get; set; }
public Action<ITask> OnEnd { get; set; }
public string Label
{
get { return "Test task"; }
}
}
}

Просмотреть файл

@ -1,57 +1,241 @@
using UnityEngine;
using UnityEditor;
using System;
using System.IO;
using System.Text;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using UnityEditor;
using UnityEngine;
namespace GitHub.Unity
{
class Utility : ScriptableObject
{
public static readonly Regex
ListBranchesRegex = new Regex(@"^(?<active>\*)?\s+(?<name>[\w\d\/\-\_]+)\s*(?:[a-z|0-9]{7} \[(?<tracking>[\w\d\/\-\_]+)\])?"),
public const string StatusRenameDivider = "->";
public static readonly Regex ListBranchesRegex =
new Regex(@"^(?<active>\*)?\s+(?<name>[\w\d\/\-\_]+)\s*(?:[a-z|0-9]{7} \[(?<tracking>[\w\d\/\-\_]+)\])?"),
ListRemotesRegex =
new Regex(
@"(?<name>[\w\d\-\_]+)\s+(?<url>https?:\/\/(?<login>(?<user>[\w\d]+)(?::(?<token>[\w\d]+))?)@(?<host>[\w\d\.\/\%]+))\s+\((?<function>fetch|push)\)"),
LogCommitRegex = new Regex(@"commit\s(\S+)"),
LogMergeRegex = new Regex(@"Merge:\s+(\S+)\s+(\S+)"),
LogAuthorRegex = new Regex(@"Author:\s+(.+)\s<(.+)>"),
LogTimeRegex = new Regex(@"Date:\s+(.+)"),
LogDescriptionRegex = new Regex(@"^\s+(.+)"),
StatusStartRegex = new Regex(@"(?<status>[AMRDC]|\?\?)(?:\d*)\s+(?<path>[\w\d\/\.\-_ \@]+)"),
StatusEndRegex = new Regex(@"->\s(?<path>[\w\d\/\.\-_ ]+)"),
StatusBranchLineValidRegex = new Regex(@"\#\#\s+(?:[\w\d\/\-_\.]+)"),
StatusAheadBehindRegex =
new Regex(
@"\[ahead (?<ahead>\d+), behind (?<behind>\d+)\]|\[ahead (?<ahead>\d+)\]|\[behind (?<behind>\d+>)\]"),
BranchNameRegex = new Regex(@"^(?<name>[\w\d\/\-\_]+)$");
ListRemotesRegex = new Regex(
@"(?<name>[\w\d\-\_]+)\s+(?<url>https?:\/\/(?<login>(?<user>[\w\d]+)(?::(?<token>[\w\d]+))?)@(?<host>[\w\d\.\/\%]+))\s+\((?<function>fetch|push)\)"
),
private static bool ready;
private static Action onReady;
LogCommitRegex = new Regex(@"commit\s(\S+)"),
LogMergeRegex = new Regex(@"Merge:\s+(\S+)\s+(\S+)"),
LogAuthorRegex = new Regex(@"Author:\s+(.+)\s<(.+)>"),
LogTimeRegex = new Regex(@"Date:\s+(.+)"),
LogDescriptionRegex = new Regex(@"^\s+(.+)"),
StatusStartRegex = new Regex(@"(?<status>[AMRDC]|\?\?)(?:\d*)\s+(?<path>[\w\d\/\.\-_ \@]+)"),
StatusEndRegex = new Regex(@"->\s(?<path>[\w\d\/\.\-_ ]+)"),
StatusBranchLineValidRegex = new Regex(@"\#\#\s+(?:[\w\d\/\-_\.]+)"),
StatusAheadBehindRegex = new Regex(@"\[ahead (?<ahead>\d+), behind (?<behind>\d+)\]|\[ahead (?<ahead>\d+)\]|\[behind (?<behind>\d+>)\]"),
BranchNameRegex = new Regex(@"^(?<name>[\w\d\/\-\_]+)$");
public const string
StatusRenameDivider = "->";
static bool ready = false;
static Action onReady;
public static string GitInstallPath
public static void RegisterReadyCallback(Action callback)
{
get
onReady += callback;
if (ready)
{
return Settings.Get("GitInstallPath");
}
set
{
Settings.Set("GitInstallPath", value);
callback();
}
}
public static void UnregisterReadyCallback(Action callback)
{
onReady -= callback;
}
public static void Initialize()
{
// Unity paths
UnityAssetsPath = Application.dataPath;
UnityProjectPath = UnityAssetsPath.Substring(0, UnityAssetsPath.Length - "Assets".Length - 1);
// Secure settings here so other threads don't try to reload
Settings.Reload();
// Juggling to find out where we got installed
var instance = FindObjectOfType(typeof(Utility)) as Utility;
if (instance == null)
{
instance = CreateInstance<Utility>();
}
var script = MonoScript.FromScriptableObject(instance);
if (script == null)
{
ExtensionInstallPath = string.Empty;
}
else
{
ExtensionInstallPath = AssetDatabase.GetAssetPath(script);
ExtensionInstallPath = ExtensionInstallPath.Substring(0, ExtensionInstallPath.LastIndexOf('/'));
ExtensionInstallPath = ExtensionInstallPath.Substring(0, ExtensionInstallPath.LastIndexOf('/'));
}
DestroyImmediate(instance);
// Evaluate project settings
Issues = new List<ProjectConfigurationIssue>();
EvaluateProjectConfigurationTask.UnregisterCallback(OnEvaluationResult);
EvaluateProjectConfigurationTask.RegisterCallback(OnEvaluationResult);
EvaluateProjectConfigurationTask.Schedule();
// Root paths
if (string.IsNullOrEmpty(GitInstallPath) || !File.Exists(GitInstallPath))
{
FindGitTask.Schedule(path =>
{
Debug.Log("found " + path);
if (!string.IsNullOrEmpty(path))
{
GitInstallPath = path;
DetermineGitRoot();
OnPrepareCompleted();
}
},
() => Debug.Log("NOT FOUND")
);
}
else
{
DetermineGitRoot();
OnPrepareCompleted();
}
}
public static string RepositoryPathToAbsolute(string repositoryPath)
{
return Path.Combine(GitRoot, repositoryPath);
}
public static string RepositoryPathToAsset(string repositoryPath)
{
var localDataPath = UnityAssetsPath.Substring(GitRoot.Length + 1);
return repositoryPath.IndexOf(localDataPath) == 0
? ("Assets" + repositoryPath.Substring(localDataPath.Length)).Replace(Path.DirectorySeparatorChar, '/')
: null;
}
public static string AssetPathToRepository(string assetPath)
{
var localDataPath = UnityAssetsPath.Substring(GitRoot.Length + 1);
return Path.Combine(localDataPath.Substring(0, localDataPath.Length - "Assets".Length),
assetPath.Replace('/', Path.DirectorySeparatorChar));
}
public static void ParseLines(StringBuilder buffer, Action<string> lineParser, bool parseAll)
{
var end = buffer.Length - 1;
// Try to avoid partial lines unless asked not to
if (!parseAll)
{
for (; end > 0 && buffer[end] != '\n'; --end) ;
}
// Parse lines if we have any buffer to parse
if (end > 0)
{
for (int index = 0, last = -1; index <= end; ++index)
{
if (buffer[index] == '\n')
{
var start = last + 1;
// TODO: Figure out how we get out of doing that ToString call
var line = buffer.ToString(start, index - start);
lineParser(line);
last = index;
}
}
buffer.Remove(0, end + 1);
}
}
public static Texture2D GetIcon(string filename, string filename2x = "")
{
if (EditorGUIUtility.pixelsPerPoint > 1f && !string.IsNullOrEmpty(filename2x))
{
filename = filename2x;
}
return Assembly.GetExecutingAssembly().GetManifestResourceStream(filename).ToTexture2D();
}
// Based on: https://www.rosettacode.org/wiki/Find_common_directory_path#C.23
public static string FindCommonPath(string separator, IEnumerable<string> paths)
{
var commonPath = string.Empty;
var separatedPath =
paths.First(first => first.Length == paths.Max(second => second.Length))
.Split(new[] { separator }, StringSplitOptions.RemoveEmptyEntries)
.ToList();
foreach (var pathSegment in separatedPath.AsEnumerable())
{
var pathExtension = pathSegment + separator;
if (commonPath.Length == 0 && paths.All(path => path.StartsWith(pathExtension)))
{
commonPath = pathExtension;
}
else if (paths.All(path => path.StartsWith(commonPath + pathExtension)))
{
commonPath += pathExtension;
}
else
{
break;
}
}
return commonPath;
}
private static void OnPrepareCompleted()
{
ready = true;
if (onReady != null)
{
onReady();
}
}
private static void OnEvaluationResult(IEnumerable<ProjectConfigurationIssue> result)
{
Issues = new List<ProjectConfigurationIssue>(result);
}
private static void DetermineGitRoot()
{
GitRoot = FindRoot(UnityAssetsPath);
}
// TODO: replace with libgit2sharp call
private static string FindRoot(string path)
{
if (string.IsNullOrEmpty(Path.GetDirectoryName(path)))
{
return null;
}
if (Directory.Exists(Path.Combine(path, ".git")))
{
return path;
}
return FindRoot(Directory.GetParent(path).FullName);
}
public static string GitInstallPath
{
get { return Settings.Get("GitInstallPath"); }
set { Settings.Set("GitInstallPath", value); }
}
public static string GitRoot { get; protected set; }
public static string UnityAssetsPath { get; protected set; }
@ -59,25 +243,16 @@ namespace GitHub.Unity
public static string ExtensionInstallPath { get; protected set; }
public static List<ProjectConfigurationIssue> Issues { get; protected set; }
public static bool GitFound
{
get
{
return !string.IsNullOrEmpty(GitInstallPath);
}
get { return !string.IsNullOrEmpty(GitInstallPath); }
}
public static bool ActiveRepository
{
get
{
return !string.IsNullOrEmpty(GitRoot);
}
get { return !string.IsNullOrEmpty(GitRoot); }
}
public static bool IsWindows
{
get
@ -95,218 +270,7 @@ namespace GitHub.Unity
public static bool IsDevelopmentBuild
{
get
{
return File.Exists(Path.Combine(UnityProjectPath.Replace('/', Path.DirectorySeparatorChar), ".devroot"));
}
}
public static void RegisterReadyCallback(Action callback)
{
onReady += callback;
if (ready)
{
callback();
}
}
public static void UnregisterReadyCallback(Action callback)
{
onReady -= callback;
}
public static void Initialize()
{
// Unity paths
UnityAssetsPath = Application.dataPath;
UnityProjectPath = UnityAssetsPath.Substring(0, UnityAssetsPath.Length - "Assets".Length - 1);
// Secure settings here so other threads don't try to reload
Settings.Reload();
// Juggling to find out where we got installed
Utility instance = FindObjectOfType(typeof(Utility)) as Utility;
if (instance == null)
{
instance = CreateInstance<Utility>();
}
MonoScript script = MonoScript.FromScriptableObject(instance);
if (script == null)
{
ExtensionInstallPath = string.Empty;
}
else
{
ExtensionInstallPath = AssetDatabase.GetAssetPath(script);
ExtensionInstallPath = ExtensionInstallPath.Substring(0, ExtensionInstallPath.LastIndexOf('/'));
ExtensionInstallPath = ExtensionInstallPath.Substring(0, ExtensionInstallPath.LastIndexOf('/'));
}
DestroyImmediate(instance);
// Evaluate project settings
Issues = new List<ProjectConfigurationIssue>();
EvaluateProjectConfigurationTask.UnregisterCallback(OnEvaluationResult);
EvaluateProjectConfigurationTask.RegisterCallback(OnEvaluationResult);
EvaluateProjectConfigurationTask.Schedule();
// Root paths
if (string.IsNullOrEmpty(GitInstallPath) || !File.Exists(GitInstallPath))
{
FindGitTask.Schedule(path =>
{
Debug.Log("found " + path);
if (!string.IsNullOrEmpty(path))
{
GitInstallPath = path;
DetermineGitRoot();
OnPrepareCompleted();
}
},
() =>
{
Debug.Log("NOT FOUND");
});
}
else
{
DetermineGitRoot();
OnPrepareCompleted();
}
}
static void OnPrepareCompleted()
{
ready = true;
if (onReady != null)
{
onReady();
}
}
static void OnEvaluationResult(IEnumerable<ProjectConfigurationIssue> result)
{
Issues = new List<ProjectConfigurationIssue>(result);
}
static void DetermineGitRoot()
{
GitRoot = FindRoot(UnityAssetsPath);
}
// TODO: replace with libgit2sharp call
static string FindRoot(string path)
{
if (string.IsNullOrEmpty(Path.GetDirectoryName(path)))
{
return null;
}
if (Directory.Exists(Path.Combine(path, ".git")))
{
return path;
}
return FindRoot(Directory.GetParent(path).FullName);
}
public static string RepositoryPathToAbsolute(string repositoryPath)
{
return Path.Combine(Utility.GitRoot, repositoryPath);
}
public static string RepositoryPathToAsset(string repositoryPath)
{
string localDataPath = UnityAssetsPath.Substring(GitRoot.Length + 1);
return (repositoryPath.IndexOf(localDataPath) == 0) ?
("Assets" + repositoryPath.Substring(localDataPath.Length)).Replace(Path.DirectorySeparatorChar, '/') :
null;
}
public static string AssetPathToRepository(string assetPath)
{
string localDataPath = UnityAssetsPath.Substring(GitRoot.Length + 1);
return Path.Combine(localDataPath.Substring(0, localDataPath.Length - "Assets".Length), assetPath.Replace('/', Path.DirectorySeparatorChar));
}
public static void ParseLines(StringBuilder buffer, Action<string> lineParser, bool parseAll)
{
int end = buffer.Length - 1;
if (!parseAll)
// Try to avoid partial lines unless asked not to
{
for (; end > 0 && buffer[end] != '\n'; --end) ;
}
if (end > 0)
// Parse lines if we have any buffer to parse
{
for (int index = 0, last = -1; index <= end; ++index)
{
if (buffer[index] == '\n')
{
int start = last + 1;
// TODO: Figure out how we get out of doing that ToString call
string line = buffer.ToString(start, index - start);
lineParser(line);
last = index;
}
}
buffer.Remove(0, end + 1);
}
}
public static Texture2D GetIcon(string filename, string filename2x = "")
{
if (EditorGUIUtility.pixelsPerPoint > 1f && !string.IsNullOrEmpty(filename2x))
{
filename = filename2x;
}
return Assembly.GetExecutingAssembly().GetManifestResourceStream(filename).ToTexture2D();
}
// Based on: https://www.rosettacode.org/wiki/Find_common_directory_path#C.23
public static string FindCommonPath(string separator, IEnumerable<string> paths)
{
string commonPath = string.Empty;
List<string> separatedPath = paths
.First(first => first.Length == paths.Max(second => second.Length))
.Split(new string[] { separator }, StringSplitOptions.RemoveEmptyEntries)
.ToList();
foreach (string pathSegment in separatedPath.AsEnumerable())
{
string pathExtension = pathSegment + separator;
if (commonPath.Length == 0 && paths.All(path => path.StartsWith(pathExtension)))
{
commonPath = pathExtension;
}
else if (paths.All(path => path.StartsWith(commonPath + pathExtension)))
{
commonPath += pathExtension;
}
else
{
break;
}
}
return commonPath;
get { return File.Exists(Path.Combine(UnityProjectPath.Replace('/', Path.DirectorySeparatorChar), ".devroot")); }
}
}
@ -314,14 +278,15 @@ namespace GitHub.Unity
{
public static Texture2D ToTexture2D(this Stream input)
{
byte[] buffer = new byte[16 * 1024];
using (MemoryStream ms = new MemoryStream())
var buffer = new byte[16 * 1024];
using (var ms = new MemoryStream())
{
int read;
while ((read = input.Read(buffer, 0, buffer.Length)) > 0)
{
ms.Write(buffer, 0, read);
}
var tex = new Texture2D(1, 1);
tex.LoadImage(ms.ToArray());
return tex;

Просмотреть файл

@ -1,102 +1,43 @@
using UnityEngine;
using UnityEditor;
using System;
using System.Collections.Generic;
using System.Linq;
using System;
using UnityEditor;
using UnityEngine;
namespace GitHub.Unity
{
[System.Serializable]
[Serializable]
class BranchesView : Subview
{
enum NodeType
{
Folder,
LocalBranch,
RemoteBranch
}
private const string ConfirmSwitchTitle = "Confirm branch switch";
private const string ConfirmSwitchMessage = "Switch branch to {0}?";
private const string ConfirmSwitchOK = "Switch";
private const string ConfirmSwitchCancel = "Cancel";
private const string NewBranchCancelButton = "x";
private const string NewBranchConfirmButton = "Create";
private const string FavouritesSetting = "Favourites";
private const string FavouritesTitle = "FAVOURITES";
private const string LocalTitle = "LOCAL BRANCHES";
private const string RemoteTitle = "REMOTE BRANCHES";
private const string CreateBranchButton = "+ New branch";
[NonSerialized] private List<BranchTreeNode> favourites = new List<BranchTreeNode>();
[NonSerialized] private int listID = -1;
[NonSerialized] private List<GitBranch> newLocalBranches;
[NonSerialized] private BranchTreeNode newNodeSelection;
[NonSerialized] private BranchesMode targetMode;
enum BranchesMode
{
Default,
Create
}
[Serializable]
class BranchTreeNode
{
List<BranchTreeNode> children = new List<BranchTreeNode>();
public string Label;
public BranchTreeNode Tracking;
public string Name { get; protected set; }
public NodeType Type { get; protected set; }
public bool Active { get; protected set; }
public IList<BranchTreeNode> Children { get { return children; } }
public BranchTreeNode(string name, NodeType type, bool active)
{
Label = Name = name;
Type = type;
Active = active;
}
}
struct Remote
{
// TODO: Pull in and store more data from GitListRemotesTask
public string Name;
public BranchTreeNode Root;
}
const string
ConfirmSwitchTitle = "Confirm branch switch",
ConfirmSwitchMessage = "Switch branch to {0}?",
ConfirmSwitchOK = "Switch",
ConfirmSwitchCancel = "Cancel",
NewBranchCancelButton = "x",
NewBranchConfirmButton = "Create",
FavouritesSetting = "Favourites",
FavouritesTitle = "FAVOURITES",
LocalTitle = "LOCAL BRANCHES",
RemoteTitle = "REMOTE BRANCHES",
CreateBranchButton = "+ New branch";
[SerializeField] Vector2 scroll;
[SerializeField] BranchTreeNode localRoot;
[SerializeField] List<Remote> remotes = new List<Remote>();
[SerializeField] BranchTreeNode selectedNode = null;
[SerializeField] BranchTreeNode activeBranchNode = null;
[SerializeField] BranchesMode mode = BranchesMode.Default;
[SerializeField] string newBranchName;
BranchTreeNode newNodeSelection = null;
List<GitBranch> newLocalBranches;
List<BranchTreeNode> favourites = new List<BranchTreeNode>();
int listID = -1;
BranchesMode targetMode;
protected override void OnShow()
{
targetMode = mode;
}
[SerializeField] private BranchTreeNode activeBranchNode;
[SerializeField] private BranchTreeNode localRoot;
[SerializeField] private BranchesMode mode = BranchesMode.Default;
[SerializeField] private string newBranchName;
[SerializeField] private List<Remote> remotes = new List<Remote>();
[SerializeField] private Vector2 scroll;
[SerializeField] private BranchTreeNode selectedNode;
public override void Refresh()
{
HistoryView historyView = ((Window)parent).HistoryTab;
var historyView = ((Window)parent).HistoryTab;
if (historyView.BroadMode)
{
@ -108,220 +49,15 @@ namespace GitHub.Unity
}
}
public void RefreshEmbedded()
{
GitListBranchesTask.ScheduleLocal(OnLocalBranchesUpdate);
GitListBranchesTask.ScheduleRemote(OnRemoteBranchesUpdate);
}
void OnLocalBranchesUpdate(IEnumerable<GitBranch> list)
{
newLocalBranches = new List<GitBranch>(list);
}
void OnRemoteBranchesUpdate(IEnumerable<GitBranch> list)
{
BuildTree(newLocalBranches, list);
newLocalBranches.Clear();
}
void BuildTree(IEnumerable<GitBranch> local, IEnumerable<GitBranch> remote)
{
// Sort
List<GitBranch>
localBranches = new List<GitBranch>(local),
remoteBranches = new List<GitBranch>(remote);
localBranches.Sort(CompareBranches);
remoteBranches.Sort(CompareBranches);
// Prepare for tracking
List<KeyValuePair<int, int>> tracking = new List<KeyValuePair<int, int>>();
List<BranchTreeNode> localBranchNodes = new List<BranchTreeNode>();
// Prepare for updated favourites listing
favourites.Clear();
// Just build directly on the local root, keep track of active branch
localRoot = new BranchTreeNode("", NodeType.Folder, false);
for (int index = 0; index < localBranches.Count; ++index)
{
GitBranch branch = localBranches[index];
BranchTreeNode node = new BranchTreeNode(branch.Name, NodeType.LocalBranch, branch.Active);
localBranchNodes.Add(node);
// Keep active node for quick reference
if (branch.Active)
{
activeBranchNode = node;
}
// Add to tracking
if (!string.IsNullOrEmpty(branch.Tracking))
{
int trackingIndex = !remoteBranches.Any() ? -1 :
Enumerable.Range(1, remoteBranches.Count + 1).FirstOrDefault(
i => remoteBranches[i - 1].Name.Equals(branch.Tracking)
) - 1;
if (trackingIndex > -1)
{
tracking.Add(new KeyValuePair<int, int>(index, trackingIndex));
}
}
// Add to favourites
if (Settings.GetElementIndex(FavouritesSetting, branch.Name) > -1)
{
favourites.Add(node);
}
// Build into tree
BuildTree(localRoot, node);
}
// Maintain list of remotes before building their roots, ignoring active state
remotes.Clear();
for (int index = 0; index < remoteBranches.Count; ++index)
{
GitBranch branch = remoteBranches[index];
// Remote name is always the first level
string remoteName = branch.Name.Substring(0, branch.Name.IndexOf('/'));
// Get or create this remote
int remoteIndex = Enumerable.Range(1, remotes.Count + 1).FirstOrDefault(i => remotes.Count > i - 1 && remotes[i - 1].Name.Equals(remoteName)) - 1;
if (remoteIndex < 0)
{
remotes.Add(new Remote() { Name = remoteName, Root = new BranchTreeNode("", NodeType.Folder, false) });
remoteIndex = remotes.Count - 1;
}
// Create the branch
BranchTreeNode node = new BranchTreeNode(branch.Name, NodeType.RemoteBranch, false) { Label = branch.Name.Substring(remoteName.Length + 1) };
// Establish tracking link
for (int trackingIndex = 0; trackingIndex < tracking.Count; ++trackingIndex)
{
KeyValuePair<int, int> pair = tracking[trackingIndex];
if (pair.Value == index)
{
localBranchNodes[pair.Key].Tracking = node;
}
}
// Add to favourites
if (Settings.GetElementIndex(FavouritesSetting, branch.Name) > -1)
{
favourites.Add(node);
}
// Build on the root of the remote, just like with locals
BuildTree(remotes[remoteIndex].Root, node);
}
Repaint();
}
static int CompareBranches(GitBranch a, GitBranch b)
{
if (GetFavourite(a.Name))
{
return -1;
}
if (GetFavourite(b.Name))
{
return 1;
}
if (a.Name.Equals("master"))
{
return -1;
}
if (b.Name.Equals("master"))
{
return 1;
}
return 0;
}
void BuildTree(BranchTreeNode parent, BranchTreeNode child)
{
int firstSplit = child.Label.IndexOf('/');
// No nesting needed here, this is just a straight add
if (firstSplit < 0)
{
parent.Children.Add(child);
return;
}
// Get or create the next folder level
string folderName = child.Label.Substring(0, firstSplit);
BranchTreeNode folder = parent.Children.FirstOrDefault(f => f.Label.Equals(folderName));
if (folder == null)
{
folder = new BranchTreeNode("", NodeType.Folder, false) { Label = folderName };
parent.Children.Add(folder);
}
// Pop the folder name from the front of the child label and add it to the folder
child.Label = child.Label.Substring(folderName.Length + 1);
BuildTree(folder, child);
}
static bool GetFavourite(BranchTreeNode branch)
{
return GetFavourite(branch.Name);
}
static bool GetFavourite(string branchName)
{
if (string.IsNullOrEmpty(branchName))
{
return false;
}
return Settings.GetElementIndex(FavouritesSetting, branchName) > -1;
}
void SetFavourite(BranchTreeNode branch, bool favourite)
{
if (string.IsNullOrEmpty(branch.Name))
{
return;
}
if (!favourite)
{
Settings.RemoveElement(FavouritesSetting, branch.Name);
favourites.Remove(branch);
}
else
{
Settings.RemoveElement(FavouritesSetting, branch.Name, false);
Settings.AddElement(FavouritesSetting, branch.Name);
favourites.Remove(branch);
favourites.Add(branch);
}
}
public override void OnGUI()
{
HistoryView historyView = ((Window)parent).HistoryTab;
var historyView = ((Window)parent).HistoryTab;
if (historyView.BroadMode)
{
@ -341,6 +77,7 @@ namespace GitHub.Unity
public void OnEmbeddedGUI()
{
scroll = GUILayout.BeginScrollView(scroll);
{
listID = GUIUtility.GetControlID(FocusType.Keyboard);
// Favourites list
@ -348,13 +85,19 @@ namespace GitHub.Unity
{
GUILayout.Label(FavouritesTitle);
GUILayout.BeginHorizontal();
{
GUILayout.Space(Styles.BranchListIndentation);
GUILayout.BeginVertical();
for (int index = 0; index < favourites.Count; ++index)
{
for (var index = 0; index < favourites.Count; ++index)
{
OnTreeNodeGUI(favourites[index]);
}
}
GUILayout.EndVertical();
}
GUILayout.EndHorizontal();
GUILayout.Space(Styles.BranchListSeperation);
@ -363,14 +106,18 @@ namespace GitHub.Unity
// Local branches and "create branch" button
GUILayout.Label(LocalTitle);
GUILayout.BeginHorizontal();
{
GUILayout.Space(Styles.BranchListIndentation);
GUILayout.BeginVertical();
{
OnTreeNodeChildrenGUI(localRoot);
GUILayout.Space(EditorGUIUtility.standardVerticalSpacing);
OnCreateGUI();
}
GUILayout.EndVertical();
}
GUILayout.EndHorizontal();
GUILayout.Space(Styles.BranchListSeperation);
@ -378,25 +125,35 @@ namespace GitHub.Unity
// Remotes
GUILayout.Label(RemoteTitle);
GUILayout.BeginHorizontal();
{
GUILayout.Space(Styles.BranchListIndentation);
GUILayout.BeginVertical();
for (int index = 0; index < remotes.Count; ++index)
for (var index = 0; index < remotes.Count; ++index)
{
var remote = remotes[index];
GUILayout.Label(remote.Name);
// Branches of the remote
GUILayout.BeginHorizontal();
{
Remote remote = remotes[index];
GUILayout.Label(remote.Name);
// Branches of the remote
GUILayout.BeginHorizontal();
GUILayout.Space(Styles.TreeIndentation);
GUILayout.BeginVertical();
OnTreeNodeChildrenGUI(remote.Root);
GUILayout.EndVertical();
GUILayout.EndHorizontal();
GUILayout.Space(Styles.BranchListSeperation);
GUILayout.Space(Styles.TreeIndentation);
GUILayout.BeginVertical();
{
OnTreeNodeChildrenGUI(remote.Root);
}
GUILayout.EndVertical();
}
GUILayout.EndHorizontal();
GUILayout.Space(Styles.BranchListSeperation);
}
GUILayout.EndVertical();
}
GUILayout.EndHorizontal();
}
GUILayout.EndScrollView();
if (Event.current.type == EventType.Repaint)
@ -425,28 +182,230 @@ namespace GitHub.Unity
}
}
void OnCreateGUI()
private static int CompareBranches(GitBranch a, GitBranch b)
{
if (GetFavourite(a.Name))
{
return -1;
}
if (GetFavourite(b.Name))
{
return 1;
}
if (a.Name.Equals("master"))
{
return -1;
}
if (b.Name.Equals("master"))
{
return 1;
}
return 0;
}
private static bool GetFavourite(BranchTreeNode branch)
{
return GetFavourite(branch.Name);
}
private static bool GetFavourite(string branchName)
{
if (string.IsNullOrEmpty(branchName))
{
return false;
}
return Settings.GetElementIndex(FavouritesSetting, branchName) > -1;
}
protected override void OnShow()
{
targetMode = mode;
}
private void OnLocalBranchesUpdate(IEnumerable<GitBranch> list)
{
newLocalBranches = new List<GitBranch>(list);
}
private void OnRemoteBranchesUpdate(IEnumerable<GitBranch> list)
{
BuildTree(newLocalBranches, list);
newLocalBranches.Clear();
}
private void BuildTree(IEnumerable<GitBranch> local, IEnumerable<GitBranch> remote)
{
// Sort
var localBranches = new List<GitBranch>(local);
var remoteBranches = new List<GitBranch>(remote);
localBranches.Sort(CompareBranches);
remoteBranches.Sort(CompareBranches);
// Prepare for tracking
var tracking = new List<KeyValuePair<int, int>>();
var localBranchNodes = new List<BranchTreeNode>();
// Prepare for updated favourites listing
favourites.Clear();
// Just build directly on the local root, keep track of active branch
localRoot = new BranchTreeNode("", NodeType.Folder, false);
for (var index = 0; index < localBranches.Count; ++index)
{
var branch = localBranches[index];
var node = new BranchTreeNode(branch.Name, NodeType.LocalBranch, branch.Active);
localBranchNodes.Add(node);
// Keep active node for quick reference
if (branch.Active)
{
activeBranchNode = node;
}
// Add to tracking
if (!string.IsNullOrEmpty(branch.Tracking))
{
var trackingIndex = !remoteBranches.Any()
? -1
: Enumerable.Range(0, remoteBranches.Count).FirstOrDefault(i => remoteBranches[i].Name.Equals(branch.Tracking));
if (trackingIndex > -1)
{
tracking.Add(new KeyValuePair<int, int>(index, trackingIndex));
}
}
// Add to favourites
if (Settings.GetElementIndex(FavouritesSetting, branch.Name) > -1)
{
favourites.Add(node);
}
// Build into tree
BuildTree(localRoot, node);
}
// Maintain list of remotes before building their roots, ignoring active state
remotes.Clear();
for (var index = 0; index < remoteBranches.Count; ++index)
{
var branch = remoteBranches[index];
// Remote name is always the first level
var remoteName = branch.Name.Substring(0, branch.Name.IndexOf('/'));
// Get or create this remote
var remoteIndex = Enumerable.Range(1, remotes.Count + 1)
.FirstOrDefault(i => remotes.Count > i - 1 && remotes[i - 1].Name.Equals(remoteName)) - 1;
if (remoteIndex < 0)
{
remotes.Add(new Remote { Name = remoteName, Root = new BranchTreeNode("", NodeType.Folder, false) });
remoteIndex = remotes.Count - 1;
}
// Create the branch
var node = new BranchTreeNode(branch.Name, NodeType.RemoteBranch, false) {
Label = branch.Name.Substring(remoteName.Length + 1)
};
// Establish tracking link
for (var trackingIndex = 0; trackingIndex < tracking.Count; ++trackingIndex)
{
var pair = tracking[trackingIndex];
if (pair.Value == index)
{
localBranchNodes[pair.Key].Tracking = node;
}
}
// Add to favourites
if (Settings.GetElementIndex(FavouritesSetting, branch.Name) > -1)
{
favourites.Add(node);
}
// Build on the root of the remote, just like with locals
BuildTree(remotes[remoteIndex].Root, node);
}
Repaint();
}
private void BuildTree(BranchTreeNode parent, BranchTreeNode child)
{
var firstSplit = child.Label.IndexOf('/');
// No nesting needed here, this is just a straight add
if (firstSplit < 0)
{
parent.Children.Add(child);
return;
}
// Get or create the next folder level
var folderName = child.Label.Substring(0, firstSplit);
var folder = parent.Children.FirstOrDefault(f => f.Label.Equals(folderName));
if (folder == null)
{
folder = new BranchTreeNode("", NodeType.Folder, false) { Label = folderName };
parent.Children.Add(folder);
}
// Pop the folder name from the front of the child label and add it to the folder
child.Label = child.Label.Substring(folderName.Length + 1);
BuildTree(folder, child);
}
private void SetFavourite(BranchTreeNode branch, bool favourite)
{
if (string.IsNullOrEmpty(branch.Name))
{
return;
}
if (!favourite)
{
Settings.RemoveElement(FavouritesSetting, branch.Name);
favourites.Remove(branch);
}
else
{
Settings.RemoveElement(FavouritesSetting, branch.Name, false);
Settings.AddElement(FavouritesSetting, branch.Name);
favourites.Remove(branch);
favourites.Add(branch);
}
}
private void OnCreateGUI()
{
if (mode == BranchesMode.Default)
// Create button
if (mode == BranchesMode.Default)
{
if (GUILayout.Button(CreateBranchButton, GUI.skin.label, GUILayout.ExpandWidth(false)))
{
targetMode = BranchesMode.Create;
}
}
else if (mode == BranchesMode.Create)
// Branch name + cancel + create
else if (mode == BranchesMode.Create)
{
GUILayout.BeginHorizontal();
bool
createBranch = false,
cancelCreate = false,
cannotCreate = selectedNode == null || selectedNode.Type == NodeType.Folder || !Utility.BranchNameRegex.IsMatch(newBranchName);
{
var createBranch = false;
var cancelCreate = false;
var cannotCreate = selectedNode == null ||
selectedNode.Type == NodeType.Folder ||
!Utility.BranchNameRegex.IsMatch(newBranchName);
// Create on return/enter or cancel on escape
int offsetID = GUIUtility.GetControlID(FocusType.Passive);
var offsetID = GUIUtility.GetControlID(FocusType.Passive);
if (Event.current.isKey && GUIUtility.keyboardControl == offsetID + 1)
{
if (Event.current.keyCode == KeyCode.Escape)
@ -478,10 +437,12 @@ namespace GitHub.Unity
// Create
EditorGUI.BeginDisabledGroup(cannotCreate);
{
if (GUILayout.Button(NewBranchConfirmButton, EditorStyles.miniButtonRight, GUILayout.ExpandWidth(false)))
{
createBranch = true;
}
}
EditorGUI.EndDisabledGroup();
// Effectuate create
@ -497,23 +458,22 @@ namespace GitHub.Unity
GUIUtility.keyboardControl = -1;
targetMode = BranchesMode.Default;
}
}
GUILayout.EndHorizontal();
}
}
void OnTreeNodeGUI(BranchTreeNode node)
private void OnTreeNodeGUI(BranchTreeNode node)
{
// Content, style, and rects
GUIContent content = new GUIContent(node.Label, node.Children.Count > 0 ? Styles.FolderIcon : Styles.DefaultAssetIcon);
GUIStyle style = node.Active ? Styles.BoldLabel : Styles.Label;
Rect rect = GUILayoutUtility.GetRect(content, style, GUILayout.MaxHeight(EditorGUIUtility.singleLineHeight));
Rect clickRect = new Rect(0f, rect.y, position.width, rect.height);
Rect favouriteRect = new Rect(clickRect.xMax - clickRect.height * 2f, clickRect.y, clickRect.height, clickRect.height);
var content = new GUIContent(node.Label, node.Children.Count > 0 ? Styles.FolderIcon : Styles.DefaultAssetIcon);
var style = node.Active ? Styles.BoldLabel : Styles.Label;
var rect = GUILayoutUtility.GetRect(content, style, GUILayout.MaxHeight(EditorGUIUtility.singleLineHeight));
var clickRect = new Rect(0f, rect.y, position.width, rect.height);
var favouriteRect = new Rect(clickRect.xMax - clickRect.height * 2f, clickRect.y, clickRect.height, clickRect.height);
bool
selected = selectedNode == node,
keyboardFocus = GUIUtility.keyboardControl == listID;
var selected = selectedNode == node;
var keyboardFocus = GUIUtility.keyboardControl == listID;
// Selection highlight and favourite toggle
if (selected)
@ -525,7 +485,7 @@ namespace GitHub.Unity
if (node.Type != NodeType.Folder)
{
bool favourite = GetFavourite(node);
var favourite = GetFavourite(node);
if (Event.current.type == EventType.Repaint)
{
GUI.DrawTexture(favouriteRect, favourite ? Styles.FavouriteIconOn : Styles.FavouriteIconOff);
@ -552,20 +512,20 @@ namespace GitHub.Unity
// State marks
if (Event.current.type == EventType.Repaint)
{
Rect indicatorRect = new Rect(rect.x - rect.height, rect.y, rect.height, rect.height);
var indicatorRect = new Rect(rect.x - rect.height, rect.y, rect.height, rect.height);
if (selectedNode != null && selectedNode.Tracking == node)
// Being tracked by current selection mark
if (selectedNode != null && selectedNode.Tracking == node)
{
GUI.DrawTexture(indicatorRect, Styles.TrackingBranchIcon);
}
else if (node.Active)
// Active branch mark
else if (node.Active)
{
GUI.DrawTexture(indicatorRect, Styles.ActiveBranchIcon);
}
else if (node.Tracking != null)
// Tracking mark
else if (node.Tracking != null)
{
GUI.DrawTexture(indicatorRect, Styles.TrackingBranchIcon);
}
@ -573,10 +533,14 @@ namespace GitHub.Unity
// Children
GUILayout.BeginHorizontal();
{
GUILayout.Space(Styles.TreeIndentation);
GUILayout.BeginVertical();
{
OnTreeNodeChildrenGUI(node);
}
GUILayout.EndVertical();
}
GUILayout.EndHorizontal();
// Click selection of the node as well as branch switch
@ -587,32 +551,29 @@ namespace GitHub.Unity
if (Event.current.clickCount > 1 && mode == BranchesMode.Default)
{
if (node.Type == NodeType.LocalBranch && EditorUtility.DisplayDialog(
ConfirmSwitchTitle,
string.Format(ConfirmSwitchMessage, node.Name),
ConfirmSwitchOK,
ConfirmSwitchCancel
))
if (node.Type == NodeType.LocalBranch &&
EditorUtility.DisplayDialog(ConfirmSwitchTitle, String.Format(ConfirmSwitchMessage, node.Name), ConfirmSwitchOK,
ConfirmSwitchCancel))
{
GitSwitchBranchesTask.Schedule(node.Name, Refresh);
}
else if (node.Type == NodeType.RemoteBranch)
{
GitBranchCreateTask.Schedule(selectedNode.Name.Substring(selectedNode.Name.IndexOf('/') + 1), selectedNode.Name, Refresh);
GitBranchCreateTask.Schedule(selectedNode.Name.Substring(selectedNode.Name.IndexOf('/') + 1), selectedNode.Name,
Refresh);
}
}
}
}
void OnTreeNodeChildrenGUI(BranchTreeNode node)
private void OnTreeNodeChildrenGUI(BranchTreeNode node)
{
if (node == null || node.Children == null)
{
return;
}
for (int index = 0; index < node.Children.Count; ++index)
for (var index = 0; index < node.Children.Count; ++index)
{
// The actual GUI of the child
OnTreeNodeGUI(node.Children[index]);
@ -620,22 +581,8 @@ namespace GitHub.Unity
// Keyboard navigation if this child is the current selection
if (selectedNode == node.Children[index] && GUIUtility.keyboardControl == listID && Event.current.type == EventType.KeyDown)
{
int directionY =
Event.current.keyCode == KeyCode.UpArrow ?
-1
:
Event.current.keyCode == KeyCode.DownArrow ?
1
:
0,
directionX =
Event.current.keyCode == KeyCode.LeftArrow ?
-1
:
Event.current.keyCode == KeyCode.RightArrow ?
1
:
0;
int directionY = Event.current.keyCode == KeyCode.UpArrow ? -1 : Event.current.keyCode == KeyCode.DownArrow ? 1 : 0,
directionX = Event.current.keyCode == KeyCode.LeftArrow ? -1 : Event.current.keyCode == KeyCode.RightArrow ? 1 : 0;
if (directionY < 0 && index > 0)
{
@ -647,12 +594,12 @@ namespace GitHub.Unity
newNodeSelection = node.Children[index + 1];
Event.current.Use();
}
else if(directionX < 0)
else if (directionX < 0)
{
newNodeSelection = node;
Event.current.Use();
}
else if(directionX > 0 && node.Children[index].Children.Count > 0)
else if (directionX > 0 && node.Children[index].Children.Count > 0)
{
newNodeSelection = node.Children[index].Children[0];
Event.current.Use();
@ -660,5 +607,50 @@ namespace GitHub.Unity
}
}
}
private enum NodeType
{
Folder,
LocalBranch,
RemoteBranch
}
private enum BranchesMode
{
Default,
Create
}
[Serializable]
private class BranchTreeNode
{
private readonly List<BranchTreeNode> children = new List<BranchTreeNode>();
public string Label;
public BranchTreeNode Tracking;
public BranchTreeNode(string name, NodeType type, bool active)
{
Label = Name = name;
Type = type;
Active = active;
}
public string Name { get; }
public NodeType Type { get; }
public bool Active { get; }
public IList<BranchTreeNode> Children
{
get { return children; }
}
}
private struct Remote
{
// TODO: Pull in and store more data from GitListRemotesTask
public string Name;
public BranchTreeNode Root;
}
}
}

Просмотреть файл

@ -1,37 +1,100 @@
using UnityEngine;
using UnityEditor;
using System;
using System.Linq;
using UnityEditor;
using UnityEngine;
namespace GitHub.Unity
{
[System.Serializable]
[Serializable]
class ChangesView : Subview
{
const string
SummaryLabel = "Commit summary",
DescriptionLabel = "Commit description",
CommitButton = "Commit to <b>{0}</b>",
SelectAllButton = "All",
SelectNoneButton = "None",
ChangedFilesLabel = "{0} changed files",
OneChangedFileLabel = "1 changed file",
NoChangedFilesLabel = "No changed files";
private const string SummaryLabel = "Commit summary";
private const string DescriptionLabel = "Commit description";
private const string CommitButton = "Commit to <b>{0}</b>";
private const string SelectAllButton = "All";
private const string SelectNoneButton = "None";
private const string ChangedFilesLabel = "{0} changed files";
private const string OneChangedFileLabel = "1 changed file";
private const string NoChangedFilesLabel = "No changed files";
[NonSerialized] private bool lockCommit = true;
[SerializeField] private string commitBody = "";
[SerializeField] Vector2
verticalScroll,
horizontalScroll;
[SerializeField] string
commitMessage = "",
commitBody = "",
currentBranch = "[unknown]";
[SerializeField] ChangesetTreeView tree = new ChangesetTreeView();
[SerializeField] private string commitMessage = "";
[SerializeField] private string currentBranch = "[unknown]";
[SerializeField] private Vector2 horizontalScroll;
[SerializeField] private ChangesetTreeView tree = new ChangesetTreeView();
[SerializeField] private Vector2 verticalScroll;
public override void Refresh()
{
GitStatusTask.Schedule();
}
bool lockCommit = true;
public override void OnGUI()
{
var scroll = verticalScroll;
scroll = GUILayout.BeginScrollView(verticalScroll);
{
if (tree.Height > 0)
{
verticalScroll = scroll;
}
GUILayout.BeginHorizontal();
{
EditorGUI.BeginDisabledGroup(tree.Entries.Count == 0);
{
if (GUILayout.Button(SelectAllButton, EditorStyles.miniButtonLeft))
{
SelectAll();
}
if (GUILayout.Button(SelectNoneButton, EditorStyles.miniButtonRight))
{
SelectNone();
}
}
EditorGUI.EndDisabledGroup();
GUILayout.FlexibleSpace();
GUILayout.Label(
tree.Entries.Count == 0
? NoChangedFilesLabel
: tree.Entries.Count == 1 ? OneChangedFileLabel : String.Format(ChangedFilesLabel, tree.Entries.Count),
EditorStyles.miniLabel);
}
GUILayout.EndHorizontal();
GUILayout.BeginVertical(Styles.CommitFileAreaStyle);
{
// Specify a minimum height if we can - avoiding vertical scrollbars on both the outer and inner scroll view
if (tree.Height > 0)
{
horizontalScroll = GUILayout.BeginScrollView(horizontalScroll, GUILayout.MinHeight(tree.Height),
GUILayout.MaxHeight(100000f)
// NOTE: This ugliness is necessary as unbounded MaxHeight appears impossible when MinHeight is specified
);
}
else // if we have no minimum height to work with, just stretch and hope
{
horizontalScroll = GUILayout.BeginScrollView(horizontalScroll);
}
{// scroll view block started above
tree.OnGUI();
}
GUILayout.EndScrollView();
}
GUILayout.EndVertical();
// Do the commit details area
OnCommitDetailsAreaGUI();
}
GUILayout.EndScrollView();
}
protected override void OnShow()
{
@ -39,20 +102,12 @@ namespace GitHub.Unity
GitStatusTask.RegisterCallback(OnStatusUpdate);
}
protected override void OnHide()
{
GitStatusTask.UnregisterCallback(OnStatusUpdate);
}
public override void Refresh()
{
GitStatusTask.Schedule();
}
void OnStatusUpdate(GitStatus update)
private void OnStatusUpdate(GitStatus update)
{
// Set branch state
currentBranch = update.LocalBranch;
@ -63,70 +118,14 @@ namespace GitHub.Unity
lockCommit = false;
}
public override void OnGUI()
private void OnCommitDetailsAreaGUI()
{
if (tree.Height > 0)
{
verticalScroll = GUILayout.BeginScrollView(verticalScroll);
}
else
{
GUILayout.BeginScrollView(verticalScroll);
}
GUILayout.BeginHorizontal();
EditorGUI.BeginDisabledGroup(tree.Entries.Count == 0);
if (GUILayout.Button(SelectAllButton, EditorStyles.miniButtonLeft))
{
SelectAll();
}
if (GUILayout.Button(SelectNoneButton, EditorStyles.miniButtonRight))
{
SelectNone();
}
EditorGUI.EndDisabledGroup();
GUILayout.FlexibleSpace();
GUILayout.Label(
tree.Entries.Count == 0 ? NoChangedFilesLabel :
tree.Entries.Count == 1 ? OneChangedFileLabel :
string.Format(ChangedFilesLabel, tree.Entries.Count),
EditorStyles.miniLabel
);
GUILayout.EndHorizontal();
GUILayout.BeginVertical(Styles.CommitFileAreaStyle);
if (tree.Height > 0)
// Specify a minimum height if we can - avoiding vertical scrollbars on both the outer and inner scroll view
{
horizontalScroll = GUILayout.BeginScrollView(
horizontalScroll,
GUILayout.MinHeight(tree.Height),
GUILayout.MaxHeight(100000f) // NOTE: This ugliness is necessary as unbounded MaxHeight appears impossible when MinHeight is specified
);
}
// If we have no minimum height to work with, just stretch and hope
else
{
horizontalScroll = GUILayout.BeginScrollView(horizontalScroll);
}
tree.OnGUI();
GUILayout.EndScrollView();
GUILayout.EndVertical();
// Do the commit details area
OnCommitDetailsAreaGUI();
GUILayout.EndScrollView();
}
void OnCommitDetailsAreaGUI()
{
GUILayout.BeginVertical(
GUILayout.Height(Mathf.Clamp(position.height * Styles.CommitAreaDefaultRatio, Styles.CommitAreaMinHeight, Styles.CommitAreaMaxHeight))
GUILayout.BeginVertical(GUILayout.Height(
Mathf.Clamp(position.height * Styles.CommitAreaDefaultRatio,
Styles.CommitAreaMinHeight,
Styles.CommitAreaMaxHeight))
);
{
GUILayout.Label(SummaryLabel);
commitMessage = GUILayout.TextField(commitMessage);
@ -135,37 +134,39 @@ namespace GitHub.Unity
// Disable committing when already committing or if we don't have all the data needed
EditorGUI.BeginDisabledGroup(lockCommit || string.IsNullOrEmpty(commitMessage) || !tree.CommitTargets.Any(t => t.Any));
{
GUILayout.BeginHorizontal();
{
GUILayout.FlexibleSpace();
if (GUILayout.Button(string.Format(CommitButton, currentBranch), Styles.CommitButtonStyle))
if (GUILayout.Button(String.Format(CommitButton, currentBranch), Styles.CommitButtonStyle))
{
Commit();
}
}
GUILayout.EndHorizontal();
}
EditorGUI.EndDisabledGroup();
}
GUILayout.EndVertical();
}
void SelectAll()
private void SelectAll()
{
for (int index = 0; index < tree.CommitTargets.Count; ++index)
for (var index = 0; index < tree.CommitTargets.Count; ++index)
{
tree.CommitTargets[index].All = true;
}
}
void SelectNone()
private void SelectNone()
{
for (int index = 0; index < tree.CommitTargets.Count; ++index)
for (var index = 0; index < tree.CommitTargets.Count; ++index)
{
tree.CommitTargets[index].All = false;
}
}
void Commit()
private void Commit()
{
// Do not allow new commits before we have received one successful update
lockCommit = true;
@ -175,11 +176,10 @@ namespace GitHub.Unity
Enumerable.Range(0, tree.Entries.Count).Where(i => tree.CommitTargets[i].All).Select(i => tree.Entries[i].Path),
commitMessage,
commitBody,
() =>
{
() => {
commitMessage = "";
commitBody = "";
for (int index = 0; index < tree.Entries.Count; ++index)
for (var index = 0; index < tree.Entries.Count; ++index)
{
tree.CommitTargets[index].Clear();
}

Просмотреть файл

@ -1,19 +1,24 @@
using UnityEngine;
using UnityEditor;
using System;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEngine;
namespace GitHub.Unity
{
[Serializable]
class GitCommitTarget
{
public bool All = false;
// TODO: Add line tracking here
[SerializeField] public bool All = false;
public void Clear()
{
All = false;
// TODO: Add line tracking here
}
// TODO: Add line tracking here
public bool Any
{
@ -22,39 +27,354 @@ namespace GitHub.Unity
return All; // TODO: Add line tracking here
}
}
public void Clear()
{
All = false;
// TODO: Add line tracking here
}
}
[System.Serializable]
[Serializable]
class ChangesetTreeView : Subview
{
enum CommitState
private const string BasePathLabel = "{0}";
private const string NoChangesLabel = "No changes found";
[SerializeField] private List<GitStatusEntry> entries = new List<GitStatusEntry>();
[SerializeField] private List<GitCommitTarget> entryCommitTargets = new List<GitCommitTarget>();
[SerializeField] private List<string> foldedTreeEntries = new List<string>();
[SerializeField] private FileTreeNode tree;
public void Update(IList<GitStatusEntry> newEntries)
{
// Handle the empty list scenario
if (!newEntries.Any())
{
entries.Clear();
entryCommitTargets.Clear();
tree = null;
foldedTreeEntries.Clear();
OnCommitTreeChange();
return;
}
// Remove what got nuked
for (var index = 0; index < entries.Count;)
{
if (!newEntries.Contains(entries[index]))
{
entries.RemoveAt(index);
entryCommitTargets.RemoveAt(index);
}
else
{
index++;
}
}
// Remove folding state of nuked items
for (var index = 0; index < foldedTreeEntries.Count;)
{
if (!newEntries.Any(e => e.Path.IndexOf(foldedTreeEntries[index]) == 0))
{
foldedTreeEntries.RemoveAt(index);
}
else
{
index++;
}
}
// Add new stuff
for (var index = 0; index < newEntries.Count; ++index)
{
var entry = newEntries[index];
if (!entries.Contains(entry))
{
entries.Add(entry);
entryCommitTargets.Add(new GitCommitTarget());
}
}
// TODO: Filter .meta files - consider adding them as children of the asset or folder they're supporting
// TODO: In stead of completely rebuilding the tree structure, figure out a way to migrate open/closed states from the old tree to the new
// Build tree structure
tree = new FileTreeNode(Utility.FindCommonPath("" + Path.DirectorySeparatorChar, entries.Select(e => e.Path)));
tree.RepositoryPath = tree.Path;
for (var index = 0; index < entries.Count; index++)
{
var node = new FileTreeNode(entries[index].Path.Substring(tree.Path.Length)) { Target = entryCommitTargets[index] };
if (!string.IsNullOrEmpty(entries[index].ProjectPath))
{
node.Icon = AssetDatabase.GetCachedIcon(entries[index].ProjectPath);
}
BuildTree(tree, node);
}
OnCommitTreeChange();
}
public override void OnGUI()
{
GUILayout.BeginVertical();
{
// The file tree (when available)
if (tree != null && entries.Any())
{
// Base path label
if (!string.IsNullOrEmpty(tree.Path))
{
GUILayout.Label(String.Format(BasePathLabel, tree.Path));
}
GUILayout.BeginHorizontal();
{
GUILayout.Space(Styles.TreeIndentation + Styles.TreeRootIndentation);
GUILayout.BeginVertical();
{
// Root nodes
foreach (var node in tree.Children)
{
TreeNode(node);
}
}
GUILayout.EndVertical();
}
GUILayout.EndHorizontal();
// If we have no minimum height calculated, do that now and repaint so it can be used
if (Height == 0f && Event.current.type == EventType.Repaint)
{
Height = GUILayoutUtility.GetLastRect().yMax + Styles.MinCommitTreePadding;
Repaint();
}
GUILayout.FlexibleSpace();
}
else
{
GUILayout.FlexibleSpace();
GUILayout.BeginHorizontal();
{
GUILayout.FlexibleSpace();
GUILayout.Label(NoChangesLabel);
GUILayout.FlexibleSpace();
}
GUILayout.EndHorizontal();
GUILayout.FlexibleSpace();
}
}
GUILayout.EndVertical();
}
private void OnCommitTreeChange()
{
Height = 0f;
Repaint();
Repaint();
}
private void BuildTree(FileTreeNode parent, FileTreeNode node)
{
if (string.IsNullOrEmpty(node.Label))
{
// TODO: We should probably reassign this target onto the parent? Depends on how we want to handle .meta files for folders
return;
}
node.RepositoryPath = Path.Combine(parent.RepositoryPath, node.Label);
parent.Open = !foldedTreeEntries.Contains(parent.RepositoryPath);
// Is this node inside a folder?
var index = node.Label.IndexOf(Path.DirectorySeparatorChar);
if (index > 0)
{
// Figure out what the root folder is and chop it from the path
var root = node.Label.Substring(0, index);
node.Label = node.Label.Substring(index + 1);
// Look for a branch matching our root in the existing children
var found = false;
foreach (var child in parent.Children)
{
// If we found the branch, continue building from that branch
if (child.Label.Equals(root))
{
found = true;
BuildTree(child, node);
break;
}
}
// No existing branch - we will have to add a new one to build from
if (!found)
{
BuildTree(parent.Add(new FileTreeNode(root) { RepositoryPath = Path.Combine(parent.RepositoryPath, root) }), node);
}
}
// Not inside a folder - just add this node right here
else
{
parent.Add(node);
}
}
private void TreeNode(FileTreeNode node)
{
var target = node.Target;
var isFolder = node.Children.Any();
GUILayout.BeginHorizontal();
{
if (!Readonly)
{
// Commit inclusion toggle
var state = node.State;
var toggled = state == CommitState.All;
EditorGUI.BeginChangeCheck();
{
toggled = GUILayout.Toggle(toggled, "", state == CommitState.Some ? Styles.ToggleMixedStyle : GUI.skin.toggle,
GUILayout.ExpandWidth(false));
}
if (EditorGUI.EndChangeCheck())
{
node.State = toggled ? CommitState.All : CommitState.None;
}
}
// Foldout
if (isFolder)
{
Rect foldoutRect;
if (Readonly)
{
foldoutRect = GUILayoutUtility.GetRect(1, 1);
foldoutRect.Set(foldoutRect.x - 5f, foldoutRect.y, 0f, EditorGUIUtility.singleLineHeight);
}
else
{
foldoutRect = GUILayoutUtility.GetLastRect();
}
foldoutRect.Set(foldoutRect.x - Styles.FoldoutWidth + Styles.FoldoutIndentation, foldoutRect.y, Styles.FoldoutWidth,
foldoutRect.height);
EditorGUI.BeginChangeCheck();
{
node.Open = GUI.Toggle(foldoutRect, node.Open, "", EditorStyles.foldout);
}
if (EditorGUI.EndChangeCheck())
{
if (!node.Open && !foldedTreeEntries.Contains(node.RepositoryPath))
{
foldedTreeEntries.Add(node.RepositoryPath);
}
else if (node.Open)
{
foldedTreeEntries.Remove(node.RepositoryPath);
}
OnCommitTreeChange();
}
}
// Node icon and label
GUILayout.BeginHorizontal();
{
GUILayout.Space(Styles.CommitIconHorizontalPadding);
var iconRect = GUILayoutUtility.GetRect(Styles.CommitIconSize, Styles.CommitIconSize, GUILayout.ExpandWidth(false));
if (Event.current.type == EventType.Repaint)
{
GUI.DrawTexture(iconRect, node.Icon ?? (isFolder ? Styles.FolderIcon : Styles.DefaultAssetIcon),
ScaleMode.ScaleToFit);
}
GUILayout.Space(Styles.CommitIconHorizontalPadding);
}
GUILayout.EndHorizontal();
GUILayout.Label(new GUIContent(node.Label, node.RepositoryPath), GUILayout.ExpandWidth(true));
GUILayout.FlexibleSpace();
// Current status (if any)
if (target != null)
{
var status = entries[entryCommitTargets.IndexOf(target)].Status;
var statusIcon = Styles.GetGitFileStatusIcon(status);
GUILayout.Label(statusIcon != null ? new GUIContent(statusIcon) : new GUIContent(status.ToString()),
GUILayout.ExpandWidth(false), GUILayout.MaxHeight(EditorGUIUtility.singleLineHeight));
}
}
GUILayout.EndHorizontal();
GUILayout.BeginHorizontal();
{
// Render children (if any and folded out)
if (isFolder && node.Open)
{
GUILayout.Space(Styles.TreeIndentation);
GUILayout.BeginVertical();
{
foreach (var child in node.Children)
{
TreeNode(child);
}
}
GUILayout.EndVertical();
}
}
GUILayout.EndHorizontal();
}
public float Height { get; protected set; }
public bool Readonly { get; set; }
public IList<GitStatusEntry> Entries
{
get { return entries; }
}
public IList<GitCommitTarget> CommitTargets
{
get { return entryCommitTargets; }
}
private enum CommitState
{
None,
Some,
All
}
[Serializable]
class FileTreeNode
private class FileTreeNode
{
public string Label, RepositoryPath;
public bool Open = true;
public GitCommitTarget Target;
public Texture Icon;
[SerializeField] private List<FileTreeNode> children = new List<FileTreeNode>();
[SerializeField] public Texture Icon;
[SerializeField] public string Label;
[SerializeField] public bool Open = true;
[SerializeField] private string path;
[SerializeField] public string RepositoryPath;
[SerializeField] public GitCommitTarget Target;
[SerializeField] string path;
[SerializeField] List<FileTreeNode> children = new List<FileTreeNode>();
public FileTreeNode(string path)
{
this.path = path;
Label = path;
}
public FileTreeNode Add(FileTreeNode child)
{
children.Add(child);
return child;
}
public CommitState State
{
@ -64,22 +384,20 @@ namespace GitHub.Unity
{
return Target.All ? CommitState.All : Target.Any ? CommitState.Some : CommitState.None;
}
else
var allCount = children.Count(c => c.State == CommitState.All);
if (allCount == children.Count)
{
int allCount = children.Count(c => c.State == CommitState.All);
if (allCount == children.Count)
{
return CommitState.All;
}
if (allCount > 0)
{
return CommitState.Some;
}
return children.Count(c => c.State == CommitState.Some) > 0 ? CommitState.Some : CommitState.None;
return CommitState.All;
}
if (allCount > 0)
{
return CommitState.Some;
}
return children.Count(c => c.State == CommitState.Some) > 0 ? CommitState.Some : CommitState.None;
}
set
{
@ -101,7 +419,7 @@ namespace GitHub.Unity
}
else
{
for (int index = 0; index < children.Count; ++index)
for (var index = 0; index < children.Count; ++index)
{
children[index].State = value;
}
@ -109,329 +427,15 @@ namespace GitHub.Unity
}
}
public FileTreeNode(string path)
public string Path
{
this.path = path;
Label = path;
get { return path; }
}
public string Path { get { return path; } }
public IEnumerable<FileTreeNode> Children { get { return children; } }
public FileTreeNode Add(FileTreeNode child)
public IEnumerable<FileTreeNode> Children
{
children.Add(child);
return child;
get { return children; }
}
}
const string
BasePathLabel = "{0}",
NoChangesLabel = "No changes found";
[SerializeField] List<GitStatusEntry> entries = new List<GitStatusEntry>();
[SerializeField] List<GitCommitTarget> entryCommitTargets = new List<GitCommitTarget>();
[SerializeField] FileTreeNode tree;
[SerializeField] List<string> foldedTreeEntries = new List<string>();
public float Height { get; protected set; }
public bool Readonly { get; set; }
public IList<GitStatusEntry> Entries
{
get
{
return entries;
}
}
public IList<GitCommitTarget> CommitTargets
{
get
{
return entryCommitTargets;
}
}
public void Update(IList<GitStatusEntry> newEntries)
{
// Handle the empty list scenario
if (!newEntries.Any())
{
entries.Clear();
entryCommitTargets.Clear();
tree = null;
foldedTreeEntries.Clear();
OnCommitTreeChange();
return;
}
// Remove what got nuked
for (int index = 0; index < entries.Count;)
{
if (!newEntries.Contains(entries[index]))
{
entries.RemoveAt(index);
entryCommitTargets.RemoveAt(index);
}
else
{
++index;
}
}
// Remove folding state of nuked items
for (int index = 0; index < foldedTreeEntries.Count;)
{
if (!newEntries.Any(e => e.Path.IndexOf(foldedTreeEntries[index]) == 0))
{
foldedTreeEntries.RemoveAt(index);
}
else
{
++index;
}
}
// Add new stuff
for (int index = 0; index < newEntries.Count; ++index)
{
GitStatusEntry entry = newEntries[index];
if (!entries.Contains(entry))
{
entries.Add(entry);
entryCommitTargets.Add(new GitCommitTarget());
}
}
// TODO: Filter .meta files - consider adding them as children of the asset or folder they're supporting
// TODO: In stead of completely rebuilding the tree structure, figure out a way to migrate open/closed states from the old tree to the new
// Build tree structure
tree = new FileTreeNode(Utility.FindCommonPath("" + Path.DirectorySeparatorChar, entries.Select(e => e.Path)));
tree.RepositoryPath = tree.Path;
for (int index = 0; index < entries.Count; ++index)
{
FileTreeNode node = new FileTreeNode(entries[index].Path.Substring(tree.Path.Length)){ Target = entryCommitTargets[index] };
if (!string.IsNullOrEmpty(entries[index].ProjectPath))
{
node.Icon = AssetDatabase.GetCachedIcon(entries[index].ProjectPath);
}
BuildTree(tree, node);
}
OnCommitTreeChange();
}
void OnCommitTreeChange()
{
Height = 0f;
Repaint();
Repaint();
}
void BuildTree(FileTreeNode parent, FileTreeNode node)
{
if (string.IsNullOrEmpty(node.Label))
{
// TODO: We should probably reassign this target onto the parent? Depends on how we want to handle .meta files for folders
return;
}
node.RepositoryPath = Path.Combine(parent.RepositoryPath, node.Label);
parent.Open = !foldedTreeEntries.Contains(parent.RepositoryPath);
// Is this node inside a folder?
int index = node.Label.IndexOf(Path.DirectorySeparatorChar);
if (index > 0)
{
// Figure out what the root folder is and chop it from the path
string root = node.Label.Substring(0, index);
node.Label = node.Label.Substring(index + 1);
// Look for a branch matching our root in the existing children
bool found = false;
foreach (FileTreeNode child in parent.Children)
{
if (child.Label.Equals(root))
// If we found the branch, continue building from that branch
{
found = true;
BuildTree(child, node);
break;
}
}
// No existing branch - we will have to add a new one to build from
if (!found)
{
BuildTree(parent.Add(new FileTreeNode(root){ RepositoryPath = Path.Combine(parent.RepositoryPath, root) }), node);
}
}
// Not inside a folder - just add this node right here
else
{
parent.Add(node);
}
}
public override void OnGUI()
{
GUILayout.BeginVertical();
// The file tree (when available)
if (tree != null && entries.Any())
{
// Base path label
if (!string.IsNullOrEmpty(tree.Path))
{
GUILayout.Label(string.Format(BasePathLabel, tree.Path));
}
GUILayout.BeginHorizontal();
GUILayout.Space(Styles.TreeIndentation + Styles.TreeRootIndentation);
GUILayout.BeginVertical();
// Root nodes
foreach (FileTreeNode node in tree.Children)
{
TreeNode(node);
}
GUILayout.EndVertical();
GUILayout.EndHorizontal();
if (Height == 0f && Event.current.type == EventType.Repaint)
// If we have no minimum height calculated, do that now and repaint so it can be used
{
Height = GUILayoutUtility.GetLastRect().yMax + Styles.MinCommitTreePadding;
Repaint();
}
GUILayout.FlexibleSpace();
}
else
{
GUILayout.FlexibleSpace();
GUILayout.BeginHorizontal();
GUILayout.FlexibleSpace();
GUILayout.Label(NoChangesLabel);
GUILayout.FlexibleSpace();
GUILayout.EndHorizontal();
GUILayout.FlexibleSpace();
}
GUILayout.EndVertical();
}
void TreeNode(FileTreeNode node)
{
GitCommitTarget target = node.Target;
bool isFolder = node.Children.Any();
GUILayout.BeginHorizontal();
if (!Readonly)
{
// Commit inclusion toggle
CommitState state = node.State;
bool toggled = state == CommitState.All;
EditorGUI.BeginChangeCheck();
toggled = GUILayout.Toggle(toggled, "", state == CommitState.Some ? Styles.ToggleMixedStyle : GUI.skin.toggle, GUILayout.ExpandWidth(false));
if (EditorGUI.EndChangeCheck())
{
node.State = toggled ? CommitState.All : CommitState.None;
}
}
// Foldout
if (isFolder)
{
Rect foldoutRect;
if (Readonly)
{
foldoutRect = GUILayoutUtility.GetRect(1, 1);
foldoutRect.Set(foldoutRect.x - 5f, foldoutRect.y, 0f, EditorGUIUtility.singleLineHeight);
}
else
{
foldoutRect = GUILayoutUtility.GetLastRect();
}
foldoutRect.Set(foldoutRect.x - Styles.FoldoutWidth + Styles.FoldoutIndentation, foldoutRect.y, Styles.FoldoutWidth, foldoutRect.height);
EditorGUI.BeginChangeCheck();
node.Open = GUI.Toggle(foldoutRect, node.Open, "", EditorStyles.foldout);
if (EditorGUI.EndChangeCheck())
{
if (!node.Open && !foldedTreeEntries.Contains(node.RepositoryPath))
{
foldedTreeEntries.Add(node.RepositoryPath);
}
else if (node.Open)
{
foldedTreeEntries.Remove(node.RepositoryPath);
}
OnCommitTreeChange();
}
}
// Node icon and label
GUILayout.BeginHorizontal();
GUILayout.Space(Styles.CommitIconHorizontalPadding);
Rect iconRect = GUILayoutUtility.GetRect(Styles.CommitIconSize, Styles.CommitIconSize, GUILayout.ExpandWidth(false));
if (Event.current.type == EventType.Repaint)
{
GUI.DrawTexture(iconRect, node.Icon ?? (isFolder ? Styles.FolderIcon : Styles.DefaultAssetIcon), ScaleMode.ScaleToFit);
}
GUILayout.Space(Styles.CommitIconHorizontalPadding);
GUILayout.EndHorizontal();
GUILayout.Label(new GUIContent(node.Label, node.RepositoryPath), GUILayout.ExpandWidth(true));
GUILayout.FlexibleSpace();
// Current status (if any)
if (target != null)
{
GitFileStatus status = entries[entryCommitTargets.IndexOf(target)].Status;
Texture2D statusIcon = Styles.GetGitFileStatusIcon(status);
GUILayout.Label(
statusIcon != null ? new GUIContent(statusIcon) : new GUIContent(status.ToString()),
GUILayout.ExpandWidth(false),
GUILayout.MaxHeight(EditorGUIUtility.singleLineHeight)
);
}
GUILayout.EndHorizontal();
GUILayout.BeginHorizontal();
// Render children (if any and folded out)
if (isFolder && node.Open)
{
GUILayout.Space(Styles.TreeIndentation);
GUILayout.BeginVertical();
foreach (FileTreeNode child in node.Children)
{
TreeNode(child);
}
GUILayout.EndVertical();
}
GUILayout.EndHorizontal();
}
}
}

Просмотреть файл

@ -1,107 +1,55 @@
using UnityEngine;
using UnityEditor;
using System;
using System.Collections.Generic;
using System.Linq;
using System;
using UnityEditor;
using UnityEngine;
using Object = UnityEngine.Object;
namespace GitHub.Unity
{
[System.Serializable]
[Serializable]
class HistoryView : Subview
{
enum LogEntryState
{
Normal,
Local,
Remote
}
private const string HistoryFocusAll = "(All)";
private const string HistoryFocusSingle = "Focus: <b>{0}</b>";
private const string PullButton = "Pull";
private const string PullButtonCount = "Pull (<b>{0}</b>)";
private const string PushButton = "Push";
private const string PushButtonCount = "Push (<b>{0}</b>)";
private const string PullConfirmTitle = "Pull Changes?";
private const string PullConfirmDescription = "Would you like to pull changes from remote '{0}'?";
private const string PullConfirmYes = "Pull";
private const string PullConfirmCancel = "Cancel";
private const string PushConfirmTitle = "Push Changes?";
private const string PushConfirmDescription = "Would you like to push changes to remote '{0}'?";
private const string PushConfirmYes = "Push";
private const string PushConfirmCancel = "Cancel";
private const string ClearSelectionButton = "x";
private const int HistoryExtraItemCount = 10;
private const float MaxChangelistHeightRatio = .2f;
[NonSerialized] private string currentRemote = "placeholder";
[NonSerialized] private int historyStartIndex;
[NonSerialized] private int historyStopIndex;
[NonSerialized] private float lastWidth;
[NonSerialized] private int listID;
[NonSerialized] private int newSelectionIndex;
[NonSerialized] private float scrollOffset;
[NonSerialized] private DateTimeOffset scrollTime = DateTimeOffset.Now;
[NonSerialized] private int selectionIndex;
[NonSerialized] private int statusAhead;
[NonSerialized] private int statusBehind;
[NonSerialized] private bool updated = true;
[NonSerialized] private bool useScrollTime;
const string
HistoryFocusAll = "(All)",
HistoryFocusSingle = "Focus: <b>{0}</b>",
PullButton = "Pull",
PullButtonCount = "Pull ({<b>0</b>})",
PushButton = "Push",
PushButtonCount = "Push (<b>{0}</b>)",
PullConfirmTitle = "Pull Changes?",
PullConfirmDescription = "Would you like to pull changes from remote '{0}'?",
PullConfirmYes = "Pull",
PullConfirmCancel = "Cancel",
PushConfirmTitle = "Push Changes?",
PushConfirmDescription = "Would you like to push changes to remote '{0}'?",
PushConfirmYes = "Push",
PushConfirmCancel = "Cancel",
ClearSelectionButton = "x";
const int
HistoryExtraItemCount = 10;
const float
MaxChangelistHeightRatio = .2f;
[SerializeField] List<GitLogEntry> history = new List<GitLogEntry>();
[SerializeField] bool historyLocked = true;
[SerializeField] Object historyTarget = null;
[SerializeField] Vector2 scroll;
[SerializeField] string selectionID;
[SerializeField] ChangesetTreeView changesetTree = new ChangesetTreeView();
[SerializeField] Vector2 detailsScroll;
[SerializeField] bool broadMode = false;
string currentRemote = "placeholder";
int
historyStartIndex,
historyStopIndex,
statusAhead,
statusBehind;
bool
updated = true,
useScrollTime = false;
DateTimeOffset scrollTime = DateTimeOffset.Now;
float
lastWidth,
scrollOffset;
int
listID,
selectionIndex,
newSelectionIndex;
public bool BroadMode { get { return broadMode; } }
float EntryHeight
{
get
{
return Styles.HistoryEntryHeight + Styles.HistoryEntryPadding;
}
}
protected override void OnShow()
{
lastWidth = position.width;
selectionIndex = newSelectionIndex = -1;
GitLogTask.RegisterCallback(OnLogUpdate);
GitStatusTask.RegisterCallback(OnStatusUpdate);
changesetTree.Show(this);
changesetTree.Readonly = true;
}
protected override void OnHide()
{
GitStatusTask.UnregisterCallback(OnStatusUpdate);
GitLogTask.UnregisterCallback(OnLogUpdate);
}
[SerializeField] private bool broadMode;
[SerializeField] private ChangesetTreeView changesetTree = new ChangesetTreeView();
[SerializeField] private Vector2 detailsScroll;
[SerializeField] private List<GitLogEntry> history = new List<GitLogEntry>();
[SerializeField] private bool historyLocked = true;
[SerializeField] private Object historyTarget;
[SerializeField] private Vector2 scroll;
[SerializeField] private string selectionID;
public override void Refresh()
{
@ -122,68 +70,6 @@ namespace GitHub.Unity
}
}
void OnStatusUpdate(GitStatus update)
{
// Set branch state
// TODO: Update currentRemote
statusAhead = update.Ahead;
statusBehind = update.Behind;
}
void OnLogUpdate(IList<GitLogEntry> entries)
{
updated = true;
history.Clear();
history.AddRange(entries);
if (history.Any())
{
// Make sure that scroll as much as possible focuses the same time period in the new entry list
if (useScrollTime)
{
int closestIndex = -1;
double closestDifference = Mathf.Infinity;
for (int index = 0; index < history.Count; ++index)
{
double diff = Math.Abs((history[index].Time - scrollTime).TotalSeconds);
if (diff < closestDifference)
{
closestDifference = diff;
closestIndex = index;
}
}
ScrollTo(closestIndex, scrollOffset);
}
CullHistory();
}
// Restore selection index or clear it
newSelectionIndex = -1;
if (!string.IsNullOrEmpty(selectionID))
{
selectionIndex = Enumerable.Range(1, history.Count + 1).FirstOrDefault(index => history[index - 1].CommitID.Equals(selectionID)) - 1;
if (selectionIndex < 0)
{
selectionID = string.Empty;
}
}
Repaint();
}
void ScrollTo(int index, float offset = 0f)
{
scroll.Set(scroll.x, EntryHeight * index + offset);
}
public override void OnSelectionChange()
{
if (!historyLocked && !string.IsNullOrEmpty(AssetDatabase.GetAssetPath(Selection.activeObject)))
@ -193,10 +79,9 @@ namespace GitHub.Unity
}
}
public bool EvaluateBroadMode()
{
bool past = broadMode;
var past = broadMode;
// Flip when the limits are breached
if (position.width > Styles.BroadModeLimit)
@ -209,9 +94,8 @@ namespace GitHub.Unity
}
// Show the layout notification while scaling
Window window = (Window)parent;
bool scaled = position.width != lastWidth;
var window = (Window)parent;
var scaled = position.width != lastWidth;
lastWidth = position.width;
if (scaled)
@ -223,7 +107,6 @@ namespace GitHub.Unity
return broadMode != past;
}
public override void OnGUI()
{
if (broadMode)
@ -241,62 +124,68 @@ namespace GitHub.Unity
}
}
public void OnBroadGUI()
{
GUILayout.BeginHorizontal();
{
GUILayout.BeginVertical(
GUILayout.MinWidth(Styles.BroadModeBranchesMinWidth),
GUILayout.MaxWidth(Mathf.Max(Styles.BroadModeBranchesMinWidth, position.width * Styles.BroadModeBranchesRatio))
);
{
((Window)parent).BranchesTab.OnEmbeddedGUI();
}
GUILayout.EndVertical();
GUILayout.BeginVertical();
{
OnEmbeddedGUI();
}
GUILayout.EndVertical();
}
GUILayout.EndHorizontal();
}
public void OnEmbeddedGUI()
{
// History toolbar
GUILayout.BeginHorizontal(EditorStyles.toolbar);
{
// Target indicator / clear button
EditorGUI.BeginDisabledGroup(historyTarget == null);
{
if (GUILayout.Button(
historyTarget == null ? HistoryFocusAll : string.Format(HistoryFocusSingle, historyTarget.name),
Styles.HistoryToolbarButtonStyle
))
historyTarget == null ? HistoryFocusAll : String.Format(HistoryFocusSingle, historyTarget.name),
Styles.HistoryToolbarButtonStyle)
)
{
historyTarget = null;
Refresh();
}
}
EditorGUI.EndDisabledGroup();
GUILayout.FlexibleSpace();
// Pull / Push buttons
if (
GUILayout.Button(statusBehind > 0 ? string.Format(PullButtonCount, statusBehind) : PullButton, Styles.HistoryToolbarButtonStyle) &&
EditorUtility.DisplayDialog(
PullConfirmTitle,
string.Format(PullConfirmDescription, currentRemote),
var pullButtonText = statusBehind > 0 ? String.Format(PullButtonCount, statusBehind) : PullButton;
var pullClicked = GUILayout.Button(pullButtonText, Styles.HistoryToolbarButtonStyle);
if (pullClicked &&
EditorUtility.DisplayDialog(PullConfirmTitle,
String.Format(PullConfirmDescription, currentRemote),
PullConfirmYes,
PullConfirmCancel
)
PullConfirmCancel)
)
{
Pull();
}
if (
GUILayout.Button(statusAhead > 0 ? string.Format(PushButtonCount, statusAhead) : PushButton, Styles.HistoryToolbarButtonStyle) &&
EditorUtility.DisplayDialog(
PushConfirmTitle,
string.Format(PushConfirmDescription, currentRemote),
var pushButtonText = statusAhead > 0 ? String.Format(PushButtonCount, statusAhead) : PushButton;
var pushClicked = GUILayout.Button(pushButtonText, Styles.HistoryToolbarButtonStyle);
if (pushClicked &&
EditorUtility.DisplayDialog(PushConfirmTitle,
String.Format(PushConfirmDescription, currentRemote),
PushConfirmYes,
PushConfirmCancel
)
PushConfirmCancel)
)
{
Push();
@ -304,11 +193,14 @@ namespace GitHub.Unity
// Target lock button
EditorGUI.BeginChangeCheck();
{
historyLocked = GUILayout.Toggle(historyLocked, GUIContent.none, Styles.HistoryLockStyle);
}
if (EditorGUI.EndChangeCheck())
{
OnSelectionChange();
}
}
GUILayout.EndHorizontal();
// When history scroll actually changes, store time value of topmost visible entry. This is the value we use to reposition scroll on log update - not the pixel value.
@ -317,55 +209,56 @@ namespace GitHub.Unity
listID = GUIUtility.GetControlID(FocusType.Keyboard);
// Only update time scroll
Vector2 lastScroll = scroll;
var lastScroll = scroll;
scroll = GUILayout.BeginScrollView(scroll);
if (lastScroll != scroll && !updated)
if (lastScroll != scroll && !updated)
{
scrollTime = history[historyStartIndex].Time;
scrollOffset = scroll.y - historyStartIndex * EntryHeight;
useScrollTime = true;
}
// Handle only the selected range of history items - adding spacing for the rest
var start = Mathf.Max(0, historyStartIndex - HistoryExtraItemCount);
var stop = Mathf.Min(historyStopIndex + HistoryExtraItemCount, history.Count);
GUILayout.Space(start * EntryHeight);
for (var index = start; index < stop; ++index)
{
if (HistoryEntry(history[index], GetEntryState(index), selectionIndex == index))
{
scrollTime = history[historyStartIndex].Time;
scrollOffset = scroll.y - historyStartIndex * EntryHeight;
useScrollTime = true;
newSelectionIndex = index;
GUIUtility.keyboardControl = listID;
}
// Handle only the selected range of history items - adding spacing for the rest
int
start = Mathf.Max(0, historyStartIndex - HistoryExtraItemCount),
stop = Mathf.Min(historyStopIndex + HistoryExtraItemCount, history.Count);
GUILayout.Space(start * EntryHeight);
for (int index = start; index < stop; ++index)
GUILayout.Space(Styles.HistoryEntryPadding);
}
GUILayout.Space((history.Count - stop) * EntryHeight);
// Keyboard control
if (GUIUtility.keyboardControl == listID && Event.current.type == EventType.KeyDown)
{
var change = 0;
if (Event.current.keyCode == KeyCode.DownArrow)
{
if (HistoryEntry(history[index], GetEntryState(index), selectionIndex == index))
{
newSelectionIndex = index;
GUIUtility.keyboardControl = listID;
}
GUILayout.Space(Styles.HistoryEntryPadding);
change = 1;
}
GUILayout.Space((history.Count - stop) * EntryHeight);
// Keyboard control
if (GUIUtility.keyboardControl == listID && Event.current.type == EventType.KeyDown)
else if (Event.current.keyCode == KeyCode.UpArrow)
{
int change = 0;
if (Event.current.keyCode == KeyCode.DownArrow)
{
change = 1;
}
else if (Event.current.keyCode == KeyCode.UpArrow)
{
change = -1;
}
if (change != 0)
{
newSelectionIndex = (selectionIndex + change) % history.Count;
if (newSelectionIndex < historyStartIndex || newSelectionIndex > historyStopIndex)
{
ScrollTo(newSelectionIndex, (position.height - position.height * MaxChangelistHeightRatio - 30f - EntryHeight) * -.5f);
}
Event.current.Use();
}
change = -1;
}
if (change != 0)
{
newSelectionIndex = (selectionIndex + change) % history.Count;
if (newSelectionIndex < historyStartIndex || newSelectionIndex > historyStopIndex)
{
ScrollTo(newSelectionIndex,
(position.height - position.height * MaxChangelistHeightRatio - 30f - EntryHeight) * -.5f);
}
Event.current.Use();
}
}
}
else
{
@ -377,10 +270,11 @@ namespace GitHub.Unity
// Selection info
if (selectionIndex >= 0)
{
GitLogEntry selection = history[selectionIndex];
var selection = history[selectionIndex];
// Top bar for scrolling to selection or clearing it
GUILayout.BeginHorizontal(EditorStyles.toolbar);
{
if (GUILayout.Button(selection.ShortID, Styles.HistoryToolbarButtonStyle))
{
ScrollTo(selectionIndex);
@ -389,25 +283,28 @@ namespace GitHub.Unity
{
newSelectionIndex = -2;
}
}
GUILayout.EndHorizontal();
// Log entry details - including changeset tree (if any changes are found)
if (changesetTree.Entries.Any())
{
detailsScroll = GUILayout.BeginScrollView(
detailsScroll,
GUILayout.MinHeight(Mathf.Min(changesetTree.Height, position.height * MaxChangelistHeightRatio))
);
detailsScroll = GUILayout.BeginScrollView(detailsScroll,
GUILayout.MinHeight(Mathf.Min(changesetTree.Height, position.height * MaxChangelistHeightRatio)));
{
HistoryEntry(selection, GetEntryState(selectionIndex), false);
GUILayout.Space(EditorGUIUtility.standardVerticalSpacing);
GUILayout.BeginHorizontal();
{
GUILayout.Space(Styles.HistoryChangesIndentation);
changesetTree.OnGUI();
}
GUILayout.EndHorizontal();
GUILayout.Space(EditorGUIUtility.standardVerticalSpacing);
}
GUILayout.EndScrollView();
}
else
@ -438,43 +335,119 @@ namespace GitHub.Unity
}
}
protected override void OnShow()
{
lastWidth = position.width;
selectionIndex = newSelectionIndex = -1;
LogEntryState GetEntryState(int index)
GitLogTask.RegisterCallback(OnLogUpdate);
GitStatusTask.RegisterCallback(OnStatusUpdate);
changesetTree.Show(this);
changesetTree.Readonly = true;
}
protected override void OnHide()
{
GitStatusTask.UnregisterCallback(OnStatusUpdate);
GitLogTask.UnregisterCallback(OnLogUpdate);
}
private void OnStatusUpdate(GitStatus update)
{
// Set branch state
// TODO: Update currentRemote
statusAhead = update.Ahead;
statusBehind = update.Behind;
}
private void OnLogUpdate(IList<GitLogEntry> entries)
{
updated = true;
history.Clear();
history.AddRange(entries);
if (history.Any())
{
// Make sure that scroll as much as possible focuses the same time period in the new entry list
if (useScrollTime)
{
var closestIndex = -1;
double closestDifference = Mathf.Infinity;
for (var index = 0; index < history.Count; ++index)
{
var diff = Math.Abs((history[index].Time - scrollTime).TotalSeconds);
if (diff < closestDifference)
{
closestDifference = diff;
closestIndex = index;
}
}
ScrollTo(closestIndex, scrollOffset);
}
CullHistory();
}
// Restore selection index or clear it
newSelectionIndex = -1;
if (!string.IsNullOrEmpty(selectionID))
{
selectionIndex =
Enumerable.Range(1, history.Count + 1).FirstOrDefault(index => history[index - 1].CommitID.Equals(selectionID)) - 1;
if (selectionIndex < 0)
{
selectionID = string.Empty;
}
}
Repaint();
}
private void ScrollTo(int index, float offset = 0f)
{
scroll.Set(scroll.x, EntryHeight * index + offset);
}
private LogEntryState GetEntryState(int index)
{
return historyTarget == null ? (index < statusAhead ? LogEntryState.Local : LogEntryState.Normal) : LogEntryState.Normal;
}
void CullHistory()
// Recalculate the range of history items to handle - based on what is visible, plus a bit of padding for fast scrolling
/// <summary>
/// Recalculate the range of history items to handle - based on what is visible, plus a bit of padding for fast scrolling
/// </summary>
private void CullHistory()
{
historyStartIndex = (int)Mathf.Clamp(scroll.y / EntryHeight, 0, history.Count);
historyStopIndex = (int)Mathf.Clamp(
historyStartIndex + (position.height - 2f * Mathf.Min(changesetTree.Height, position.height * MaxChangelistHeightRatio)) / EntryHeight +
1,
0,
history.Count
);
historyStopIndex =
(int)
Mathf.Clamp(
historyStartIndex +
(position.height - 2f * Mathf.Min(changesetTree.Height, position.height * MaxChangelistHeightRatio)) /
EntryHeight + 1, 0, history.Count);
}
bool HistoryEntry(GitLogEntry entry, LogEntryState state, bool selected)
private bool HistoryEntry(GitLogEntry entry, LogEntryState state, bool selected)
{
Rect entryRect = GUILayoutUtility.GetRect(Styles.HistoryEntryHeight, Styles.HistoryEntryHeight);
var entryRect = GUILayoutUtility.GetRect(Styles.HistoryEntryHeight, Styles.HistoryEntryHeight);
if (Event.current.type == EventType.Repaint)
{
bool keyboardFocus = GUIUtility.keyboardControl == listID;
var keyboardFocus = GUIUtility.keyboardControl == listID;
Rect
summaryRect = new Rect(entryRect.x, entryRect.y, entryRect.width, Styles.HistorySummaryHeight),
timestampRect = new Rect(entryRect.x, entryRect.yMax - Styles.HistoryDetailsHeight, entryRect.width * .5f, Styles.HistoryDetailsHeight);
Rect authorRect = new Rect(timestampRect.xMax, timestampRect.y, timestampRect.width, timestampRect.height);
var summaryRect = new Rect(entryRect.x, entryRect.y, entryRect.width, Styles.HistorySummaryHeight);
var timestampRect = new Rect(entryRect.x, entryRect.yMax - Styles.HistoryDetailsHeight, entryRect.width * .5f,
Styles.HistoryDetailsHeight);
var authorRect = new Rect(timestampRect.xMax, timestampRect.y, timestampRect.width, timestampRect.height);
if (!string.IsNullOrEmpty(entry.MergeA))
{
const float MergeIndicatorSize = 40f;
Rect mergeIndicatorRect = new Rect(summaryRect.x, summaryRect.y, MergeIndicatorSize, summaryRect.height);
var mergeIndicatorRect = new Rect(summaryRect.x, summaryRect.y, MergeIndicatorSize, summaryRect.height);
// TODO: Get an icon or something here
Styles.HistoryEntryDetailsStyle.Draw(mergeIndicatorRect, "Merge:", false, false, selected, keyboardFocus);
@ -485,7 +458,7 @@ namespace GitHub.Unity
if (state == LogEntryState.Local)
{
const float LocalIndicatorSize = 40f;
Rect localIndicatorRect = new Rect(summaryRect.x, summaryRect.y, LocalIndicatorSize, summaryRect.height);
var localIndicatorRect = new Rect(summaryRect.x, summaryRect.y, LocalIndicatorSize, summaryRect.height);
// TODO: Get an icon or something here
Styles.HistoryEntryDetailsStyle.Draw(localIndicatorRect, "Local:", false, false, selected, keyboardFocus);
@ -506,16 +479,31 @@ namespace GitHub.Unity
return false;
}
void Pull()
private void Pull()
{
Debug.Log("TODO: Pull");
}
void Push()
private void Push()
{
Debug.Log("TODO: Push");
}
public bool BroadMode
{
get { return broadMode; }
}
private float EntryHeight
{
get { return Styles.HistoryEntryHeight + Styles.HistoryEntryPadding; }
}
private enum LogEntryState
{
Normal,
Local,
Remote
}
}
}

Просмотреть файл

@ -1,80 +1,69 @@
using UnityEngine;
using UnityEditor;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Linq;
using UnityEditor;
using UnityEngine;
namespace GitHub.Unity
{
[System.Serializable]
[Serializable]
class SettingsView : Subview
{
const string
EditorSettingsMissingTitle = "Missing editor settings",
EditorSettingsMissingMessage = "No valid editor settings found when looking in expected path '{0}'. Please save the project.",
BadVCSSettingsTitle = "Update settings",
BadVCSSettingsMessage = "To use Git, you will need to set project Version Control Mode to either 'Visible Meta Files' or 'Hidden Meta Files'.",
SelectEditorSettingsButton = "View settings",
NoActiveRepositoryTitle = "No repository found",
NoActiveRepositoryMessage = "Your current project is not currently in an active Git repository:",
TextSerialisationMessage = "For optimal Git use, it is recommended that you configure Unity to serialize assets using text serialization. Note that this may cause editor slowdowns for projects with very large datasets.",
BinarySerialisationMessage = "This project is currently configured for binary serialization.",
MixedSerialisationMessage = "This project is currently configured for mixed serialization.",
IgnoreSerialisationIssuesSetting = "IgnoreSerializationIssues",
IgnoreSerialisationSettingsButton = "Ignore forever",
RefreshIssuesButton = "Refresh",
GitIgnoreExceptionWarning = "Exception when searching .gitignore files: {0}",
GitIgnoreIssueWarning = "{0}: {2}\n\nIn line \"{1}\"",
GitIgnoreIssueNoLineWarning = "{0}: {1}",
GitInitBrowseTitle = "Pick desired repository root",
GitInitButton = "Set up Git",
InvalidInitDirectoryTitle = "Invalid repository root",
InvalidInitDirectoryMessage = "Your selected folder '{0}' is not a valid repository root for your current project.",
InvalidInitDirectoryOK = "OK",
GitInstallTitle = "Git install",
GitInstallMissingMessage = "GitHub was unable to locate a valid Git install. Please specify install location or install git.",
GitInstallBrowseTitle = "Select git binary",
GitInstallPickInvalidTitle = "Invalid Git install",
GitInstallPickInvalidMessage = "The selected file is not a valid Git install.",
GitInstallPickInvalidOK = "OK",
GitInstallFindButton = "Find install",
GitInstallButton = "New Git install",
GitInstallURL = "http://desktop.github.com",
GitIgnoreRulesTitle = "gitignore rules",
GitIgnoreRulesEffect = "Effect",
GitIgnoreRulesFile = "File",
GitIgnoreRulesLine = "Line",
GitIgnoreRulesDescription = "Description",
NewGitIgnoreRuleButton = "New",
DeleteGitIgnoreRuleButton = "Delete",
RemotesTitle = "Remotes",
RemoteNameTitle = "Name",
RemoteUserTitle = "User",
RemoteHostTitle = "Host",
RemoteAccessTitle = "Access";
private const string EditorSettingsMissingTitle = "Missing editor settings";
private const string EditorSettingsMissingMessage =
"No valid editor settings found when looking in expected path '{0}'. Please save the project.";
private const string BadVCSSettingsTitle = "Update settings";
private const string BadVCSSettingsMessage =
"To use Git, you will need to set project Version Control Mode to either 'Visible Meta Files' or 'Hidden Meta Files'.";
private const string SelectEditorSettingsButton = "View settings";
private const string NoActiveRepositoryTitle = "No repository found";
private const string NoActiveRepositoryMessage = "Your current project is not currently in an active Git repository:";
private const string TextSerialisationMessage =
"For optimal Git use, it is recommended that you configure Unity to serialize assets using text serialization. Note that this may cause editor slowdowns for projects with very large datasets.";
private const string BinarySerialisationMessage = "This project is currently configured for binary serialization.";
private const string MixedSerialisationMessage = "This project is currently configured for mixed serialization.";
private const string IgnoreSerialisationIssuesSetting = "IgnoreSerializationIssues";
private const string IgnoreSerialisationSettingsButton = "Ignore forever";
private const string RefreshIssuesButton = "Refresh";
private const string GitIgnoreExceptionWarning = "Exception when searching .gitignore files: {0}";
private const string GitIgnoreIssueWarning = "{0}: {2}\n\nIn line \"{1}\"";
private const string GitIgnoreIssueNoLineWarning = "{0}: {1}";
private const string GitInitBrowseTitle = "Pick desired repository root";
private const string GitInitButton = "Set up Git";
private const string InvalidInitDirectoryTitle = "Invalid repository root";
private const string InvalidInitDirectoryMessage =
"Your selected folder '{0}' is not a valid repository root for your current project.";
private const string InvalidInitDirectoryOK = "OK";
private const string GitInstallTitle = "Git install";
private const string GitInstallMissingMessage =
"GitHub was unable to locate a valid Git install. Please specify install location or install git.";
private const string GitInstallBrowseTitle = "Select git binary";
private const string GitInstallPickInvalidTitle = "Invalid Git install";
private const string GitInstallPickInvalidMessage = "The selected file is not a valid Git install. {0}";
private const string GitInstallPickInvalidOK = "OK";
private const string GitInstallFindButton = "Find install";
private const string GitInstallButton = "New Git install";
private const string GitInstallURL = "http://desktop.github.com";
private const string GitIgnoreRulesTitle = "gitignore rules";
private const string GitIgnoreRulesEffect = "Effect";
private const string GitIgnoreRulesFile = "File";
private const string GitIgnoreRulesLine = "Line";
private const string GitIgnoreRulesDescription = "Description";
private const string NewGitIgnoreRuleButton = "New";
private const string DeleteGitIgnoreRuleButton = "Delete";
private const string RemotesTitle = "Remotes";
private const string RemoteNameTitle = "Name";
private const string RemoteUserTitle = "User";
private const string RemoteHostTitle = "Host";
private const string RemoteAccessTitle = "Access";
[NonSerialized] private int newGitIgnoreRulesSelection = -1;
[SerializeField] List<GitRemote> remotes = new List<GitRemote>();
[SerializeField] string initDirectory;
[SerializeField] int gitIgnoreRulesSelection = 0;
[SerializeField] Vector2 scroll;
int newGitIgnoreRulesSelection = -1;
protected override void OnShow()
{
GitListRemotesTask.RegisterCallback(OnRemotesUpdate);
}
protected override void OnHide()
{
GitListRemotesTask.UnregisterCallback(OnRemotesUpdate);
}
[SerializeField] private int gitIgnoreRulesSelection = 0;
[SerializeField] private string initDirectory;
[SerializeField] private List<GitRemote> remotes = new List<GitRemote>();
[SerializeField] private Vector2 scroll;
public override void Refresh()
{
@ -82,20 +71,11 @@ namespace GitHub.Unity
GitStatusTask.Schedule();
}
void OnRemotesUpdate(IList<GitRemote> entries)
{
remotes.Clear();
remotes.AddRange(entries);
Repaint();
}
public override void OnGUI()
{
scroll = GUILayout.BeginScrollView(scroll);
{
// Issues
if (!OnIssuesGUI())
{
return;
@ -127,6 +107,8 @@ namespace GitHub.Unity
GUILayout.Label(GitInstallTitle, EditorStyles.boldLabel);
OnInstallPathGUI();
}
GUILayout.EndScrollView();
// Effectuate new selection at end of frame
@ -138,19 +120,62 @@ namespace GitHub.Unity
}
}
bool OnIssuesGUI()
private static void TableCell(string label, float width)
{
ProjectSettingsIssue settingsIssues = Utility.Issues.Select(i => i as ProjectSettingsIssue).FirstOrDefault(i => i != null);
GUILayout.Label(label, EditorStyles.miniLabel, GUILayout.Width(width), GUILayout.MaxWidth(width));
}
private static bool ValidateInitDirectory(string path)
{
if (Utility.UnityProjectPath.IndexOf(path) != 0)
{
EditorUtility.DisplayDialog(InvalidInitDirectoryTitle, String.Format(InvalidInitDirectoryMessage, path),
InvalidInitDirectoryOK);
return false;
}
return true;
}
private static bool ValidateGitInstall(string path)
{
if (!FindGitTask.ValidateGitInstall(path))
{
EditorUtility.DisplayDialog(GitInstallPickInvalidTitle, String.Format(GitInstallPickInvalidMessage, path),
GitInstallPickInvalidOK);
return false;
}
return true;
}
protected override void OnShow()
{
GitListRemotesTask.RegisterCallback(OnRemotesUpdate);
}
protected override void OnHide()
{
GitListRemotesTask.UnregisterCallback(OnRemotesUpdate);
}
private void OnRemotesUpdate(IList<GitRemote> entries)
{
remotes.Clear();
remotes.AddRange(entries);
Repaint();
}
private bool OnIssuesGUI()
{
var settingsIssues = Utility.Issues.Select(i => i as ProjectSettingsIssue).FirstOrDefault(i => i != null);
if (settingsIssues != null)
{
if (settingsIssues.WasCaught(ProjectSettingsEvaluation.EditorSettingsMissing))
{
Styles.BeginInitialStateArea(
EditorSettingsMissingTitle,
string.Format(EditorSettingsMissingMessage, EvaluateProjectConfigurationTask.EditorSettingsPath)
);
Styles.BeginInitialStateArea(EditorSettingsMissingTitle,
String.Format(EditorSettingsMissingMessage, EvaluateProjectConfigurationTask.EditorSettingsPath));
Styles.EndInitialStateArea();
return false;
@ -158,6 +183,7 @@ namespace GitHub.Unity
else if (settingsIssues.WasCaught(ProjectSettingsEvaluation.BadVCSSettings))
{
Styles.BeginInitialStateArea(BadVCSSettingsTitle, BadVCSSettingsMessage);
{
GUILayout.Space(EditorGUIUtility.standardVerticalSpacing);
// Button to select editor settings - for remedying the bad setting
@ -165,6 +191,7 @@ namespace GitHub.Unity
{
Selection.activeObject = EvaluateProjectConfigurationTask.LoadEditorSettings();
}
}
Styles.EndInitialStateArea();
return false;
@ -174,7 +201,9 @@ namespace GitHub.Unity
if (!Utility.GitFound)
{
Styles.BeginInitialStateArea(GitInstallTitle, GitInstallMissingMessage);
{
OnInstallPathGUI();
}
Styles.EndInitialStateArea();
return false;
@ -182,8 +211,10 @@ namespace GitHub.Unity
else if (!Utility.ActiveRepository)
{
Styles.BeginInitialStateArea(NoActiveRepositoryTitle, NoActiveRepositoryMessage);
{
// Init directory path field
Styles.PathField(ref initDirectory, () => EditorUtility.OpenFolderPanel(GitInitBrowseTitle, initDirectory, ""), ValidateInitDirectory);
Styles.PathField(ref initDirectory, () => EditorUtility.OpenFolderPanel(GitInitBrowseTitle, initDirectory, ""),
ValidateInitDirectory);
GUILayout.Space(EditorGUIUtility.standardVerticalSpacing);
@ -199,6 +230,7 @@ namespace GitHub.Unity
ResetInitDirectory();
}
}
}
Styles.EndInitialStateArea();
return false;
@ -206,9 +238,8 @@ namespace GitHub.Unity
if (settingsIssues != null && !Settings.Get(IgnoreSerialisationIssuesSetting, "0").Equals("1"))
{
bool
binary = settingsIssues.WasCaught(ProjectSettingsEvaluation.BinarySerialization),
mixed = settingsIssues.WasCaught(ProjectSettingsEvaluation.MixedSerialization);
var binary = settingsIssues.WasCaught(ProjectSettingsEvaluation.BinarySerialization);
var mixed = settingsIssues.WasCaught(ProjectSettingsEvaluation.MixedSerialization);
if (binary || mixed)
{
@ -216,6 +247,7 @@ namespace GitHub.Unity
Styles.Warning(binary ? BinarySerialisationMessage : MixedSerialisationMessage);
GUILayout.BeginHorizontal();
{
if (GUILayout.Button(IgnoreSerialisationSettingsButton))
{
Settings.Set(IgnoreSerialisationIssuesSetting, "1");
@ -232,173 +264,177 @@ namespace GitHub.Unity
{
Selection.activeObject = EvaluateProjectConfigurationTask.LoadEditorSettings();
}
}
GUILayout.EndHorizontal();
}
}
GitIgnoreException gitIgnoreException = Utility.Issues.Select(i => i as GitIgnoreException).FirstOrDefault(i => i != null);
var gitIgnoreException = Utility.Issues.Select(i => i as GitIgnoreException).FirstOrDefault(i => i != null);
if (gitIgnoreException != null)
{
Styles.Warning(string.Format(GitIgnoreExceptionWarning, gitIgnoreException.Exception));
Styles.Warning(String.Format(GitIgnoreExceptionWarning, gitIgnoreException.Exception));
}
foreach (GitIgnoreIssue issue in Utility.Issues.Select(i => i as GitIgnoreIssue).Where(i => i != null))
foreach (var issue in Utility.Issues.Select(i => i as GitIgnoreIssue).Where(i => i != null))
{
if (string.IsNullOrEmpty(issue.Line))
{
Styles.Warning(string.Format(GitIgnoreIssueNoLineWarning, issue.File, issue.Description));
Styles.Warning(String.Format(GitIgnoreIssueNoLineWarning, issue.File, issue.Description));
}
else
{
Styles.Warning(string.Format(GitIgnoreIssueWarning, issue.File, issue.Line, issue.Description));
Styles.Warning(String.Format(GitIgnoreIssueWarning, issue.File, issue.Line, issue.Description));
}
}
return true;
}
void OnRemotesGUI()
private void OnRemotesGUI()
{
float remotesWith = position.width - Styles.RemotesTotalHorizontalMargin - 16f;
float
nameWidth = remotesWith * Styles.RemotesNameRatio,
userWidth = remotesWith * Styles.RemotesUserRatio,
hostWidth = remotesWith * Styles.RemotesHostRation,
accessWidth = remotesWith * Styles.RemotesAccessRatio;
var remotesWith = position.width - Styles.RemotesTotalHorizontalMargin - 16f;
var nameWidth = remotesWith * Styles.RemotesNameRatio;
var userWidth = remotesWith * Styles.RemotesUserRatio;
var hostWidth = remotesWith * Styles.RemotesHostRation;
var accessWidth = remotesWith * Styles.RemotesAccessRatio;
GUILayout.Label(RemotesTitle, EditorStyles.boldLabel);
GUILayout.BeginVertical(GUI.skin.box);
{
GUILayout.BeginHorizontal(EditorStyles.toolbar);
{
TableCell(RemoteNameTitle, nameWidth);
TableCell(RemoteUserTitle, userWidth);
TableCell(RemoteHostTitle, hostWidth);
TableCell(RemoteAccessTitle, accessWidth);
}
GUILayout.EndHorizontal();
for (int index = 0; index < remotes.Count; ++index)
for (var index = 0; index < remotes.Count; ++index)
{
GitRemote remote = remotes[index];
var remote = remotes[index];
GUILayout.BeginHorizontal();
{
TableCell(remote.Name, nameWidth);
TableCell(remote.User, userWidth);
TableCell(remote.Host, hostWidth);
TableCell(remote.Function.ToString(), accessWidth);
}
GUILayout.EndHorizontal();
}
}
GUILayout.EndVertical();
}
void OnGitIgnoreRulesGUI()
private void OnGitIgnoreRulesGUI()
{
float gitignoreRulesWith = position.width - Styles.GitIgnoreRulesTotalHorizontalMargin - Styles.GitIgnoreRulesSelectorWidth - 16f;
float
effectWidth = gitignoreRulesWith * Styles.GitIgnoreRulesEffectRatio,
fileWidth = gitignoreRulesWith * Styles.GitIgnoreRulesFileRatio,
lineWidth = gitignoreRulesWith * Styles.GitIgnoreRulesLineRatio;
var gitignoreRulesWith = position.width - Styles.GitIgnoreRulesTotalHorizontalMargin - Styles.GitIgnoreRulesSelectorWidth - 16f;
var effectWidth = gitignoreRulesWith * Styles.GitIgnoreRulesEffectRatio;
var fileWidth = gitignoreRulesWith * Styles.GitIgnoreRulesFileRatio;
var lineWidth = gitignoreRulesWith * Styles.GitIgnoreRulesLineRatio;
GUILayout.Label(GitIgnoreRulesTitle, EditorStyles.boldLabel);
GUILayout.BeginVertical(GUI.skin.box);
GUILayout.BeginHorizontal(EditorStyles.toolbar);
GUILayout.Space(Styles.GitIgnoreRulesSelectorWidth);
TableCell(GitIgnoreRulesEffect, effectWidth);
TableCell(GitIgnoreRulesFile, fileWidth);
TableCell(GitIgnoreRulesLine, lineWidth);
GUILayout.EndHorizontal();
GUILayout.BeginHorizontal(EditorStyles.toolbar);
{
GUILayout.Space(Styles.GitIgnoreRulesSelectorWidth);
TableCell(GitIgnoreRulesEffect, effectWidth);
TableCell(GitIgnoreRulesFile, fileWidth);
TableCell(GitIgnoreRulesLine, lineWidth);
}
GUILayout.EndHorizontal();
int count = GitIgnoreRule.Count;
for (int index = 0; index < count; ++index)
var count = GitIgnoreRule.Count;
for (var index = 0; index < count; ++index)
{
GitIgnoreRule rule;
if (GitIgnoreRule.TryLoad(index, out rule))
{
GitIgnoreRule rule;
if (GitIgnoreRule.TryLoad(index, out rule))
GUILayout.BeginHorizontal();
{
GUILayout.BeginHorizontal();
GUILayout.Space(Styles.GitIgnoreRulesSelectorWidth);
GUILayout.Space(Styles.GitIgnoreRulesSelectorWidth);
if (gitIgnoreRulesSelection == index && Event.current.type == EventType.Repaint)
{
Rect selectorRect = GUILayoutUtility.GetLastRect();
selectorRect.Set(selectorRect.x, selectorRect.y + 2f, selectorRect.width - 2f, EditorGUIUtility.singleLineHeight);
EditorStyles.foldout.Draw(selectorRect, false, false, false, false);
}
TableCell(rule.Effect.ToString(), effectWidth);
// TODO: Tint if the regex is null
TableCell(rule.FileString, fileWidth);
TableCell(rule.LineString, lineWidth);
GUILayout.EndHorizontal();
if (Event.current.type == EventType.MouseDown && GUILayoutUtility.GetLastRect().Contains(Event.current.mousePosition))
if (gitIgnoreRulesSelection == index && Event.current.type == EventType.Repaint)
{
newGitIgnoreRulesSelection = index;
Event.current.Use();
var selectorRect = GUILayoutUtility.GetLastRect();
selectorRect.Set(selectorRect.x, selectorRect.y + 2f, selectorRect.width - 2f, EditorGUIUtility.singleLineHeight);
EditorStyles.foldout.Draw(selectorRect, false, false, false, false);
}
TableCell(rule.Effect.ToString(), effectWidth);
// TODO: Tint if the regex is null
TableCell(rule.FileString, fileWidth);
TableCell(rule.LineString, lineWidth);
}
GUILayout.EndHorizontal();
if (Event.current.type == EventType.MouseDown && GUILayoutUtility.GetLastRect().Contains(Event.current.mousePosition))
{
newGitIgnoreRulesSelection = index;
Event.current.Use();
}
}
}
GUILayout.BeginHorizontal();
GUILayout.FlexibleSpace();
if (GUILayout.Button(NewGitIgnoreRuleButton, EditorStyles.miniButton))
{
GitIgnoreRule.New();
GUIUtility.hotControl = GUIUtility.keyboardControl = -1;
}
GUILayout.EndHorizontal();
GUILayout.Space(EditorGUIUtility.standardVerticalSpacing);
// Selected gitignore rule edit
GitIgnoreRule selectedRule;
if (GitIgnoreRule.TryLoad(gitIgnoreRulesSelection, out selectedRule))
GUILayout.BeginHorizontal();
{
GUILayout.FlexibleSpace();
if (GUILayout.Button(NewGitIgnoreRuleButton, EditorStyles.miniButton))
{
GUILayout.BeginVertical(GUI.skin.box);
GUILayout.BeginHorizontal();
GUILayout.FlexibleSpace();
if (GUILayout.Button(DeleteGitIgnoreRuleButton, EditorStyles.miniButton))
{
GitIgnoreRule.Delete(gitIgnoreRulesSelection);
newGitIgnoreRulesSelection = gitIgnoreRulesSelection - 1;
}
GUILayout.EndHorizontal();
EditorGUI.BeginChangeCheck();
GitIgnoreRuleEffect newEffect = (GitIgnoreRuleEffect)EditorGUILayout.EnumPopup(GitIgnoreRulesEffect, selectedRule.Effect);
string newFile = EditorGUILayout.TextField(GitIgnoreRulesFile, selectedRule.FileString);
string newLine = EditorGUILayout.TextField(GitIgnoreRulesLine, selectedRule.LineString);
GUILayout.Label(GitIgnoreRulesDescription);
string newDescription = EditorGUILayout.TextArea(selectedRule.TriggerText, Styles.CommitDescriptionFieldStyle);
if (EditorGUI.EndChangeCheck())
{
GitIgnoreRule.Save(gitIgnoreRulesSelection, newEffect, newFile, newLine, newDescription);
EvaluateProjectConfigurationTask.Schedule();
}
GUILayout.EndVertical();
GitIgnoreRule.New();
GUIUtility.hotControl = GUIUtility.keyboardControl = -1;
}
}
GUILayout.EndHorizontal();
GUILayout.Space(EditorGUIUtility.standardVerticalSpacing);
// Selected gitignore rule edit
GitIgnoreRule selectedRule;
if (GitIgnoreRule.TryLoad(gitIgnoreRulesSelection, out selectedRule))
{
GUILayout.BeginVertical(GUI.skin.box);
{
GUILayout.BeginHorizontal();
{
GUILayout.FlexibleSpace();
if (GUILayout.Button(DeleteGitIgnoreRuleButton, EditorStyles.miniButton))
{
GitIgnoreRule.Delete(gitIgnoreRulesSelection);
newGitIgnoreRulesSelection = gitIgnoreRulesSelection - 1;
}
}
GUILayout.EndHorizontal();
EditorGUI.BeginChangeCheck();
var newEffect = (GitIgnoreRuleEffect)EditorGUILayout.EnumPopup(GitIgnoreRulesEffect, selectedRule.Effect);
var newFile = EditorGUILayout.TextField(GitIgnoreRulesFile, selectedRule.FileString);
var newLine = EditorGUILayout.TextField(GitIgnoreRulesLine, selectedRule.LineString);
GUILayout.Label(GitIgnoreRulesDescription);
var newDescription = EditorGUILayout.TextArea(selectedRule.TriggerText, Styles.CommitDescriptionFieldStyle);
if (EditorGUI.EndChangeCheck())
{
GitIgnoreRule.Save(gitIgnoreRulesSelection, newEffect, newFile, newLine, newDescription);
EvaluateProjectConfigurationTask.Schedule();
}
}
GUILayout.EndVertical();
}
GUILayout.EndVertical();
}
static void TableCell(string label, float width)
{
GUILayout.Label(label, EditorStyles.miniLabel, GUILayout.Width(width), GUILayout.MaxWidth(width));
}
void OnInstallPathGUI()
private void OnInstallPathGUI()
{
var gitInstallPath = Utility.GitInstallPath;
// Install path field
EditorGUI.BeginChangeCheck();
string gitInstallPath = Utility.GitInstallPath;
Styles.PathField(
ref gitInstallPath,
() => EditorUtility.OpenFilePanel(
GitInstallBrowseTitle,
Path.GetDirectoryName(FindGitTask.DefaultGitPath),
Path.GetExtension(FindGitTask.DefaultGitPath)
),
ValidateGitInstall
);
{
Styles.PathField(ref gitInstallPath,
() =>
EditorUtility.OpenFilePanel(GitInstallBrowseTitle, Path.GetDirectoryName(FindGitTask.DefaultGitPath),
Path.GetExtension(FindGitTask.DefaultGitPath)), ValidateGitInstall);
}
if (EditorGUI.EndChangeCheck())
{
Utility.GitInstallPath = gitInstallPath;
@ -407,11 +443,11 @@ namespace GitHub.Unity
GUILayout.Space(EditorGUIUtility.standardVerticalSpacing);
GUILayout.BeginHorizontal();
{
// Find button - for attempting to locate a new install
if (GUILayout.Button(GitInstallFindButton, GUILayout.ExpandWidth(false)))
{
FindGitTask.Schedule(path =>
{
FindGitTask.Schedule(path => {
if (!string.IsNullOrEmpty(path))
{
Utility.GitInstallPath = path;
@ -426,52 +462,17 @@ namespace GitHub.Unity
{
Application.OpenURL(GitInstallURL);
}
}
GUILayout.EndHorizontal();
}
void ResetInitDirectory()
private void ResetInitDirectory()
{
initDirectory = Utility.UnityProjectPath;
GUIUtility.keyboardControl = GUIUtility.hotControl = 0;
}
static bool ValidateInitDirectory(string path)
{
if (Utility.UnityProjectPath.IndexOf(path) != 0)
{
EditorUtility.DisplayDialog(
InvalidInitDirectoryTitle,
string.Format(InvalidInitDirectoryMessage, path),
InvalidInitDirectoryOK
);
return false;
}
return true;
}
static bool ValidateGitInstall(string path)
{
if (!FindGitTask.ValidateGitInstall(path))
{
EditorUtility.DisplayDialog(
GitInstallPickInvalidTitle,
string.Format(GitInstallPickInvalidMessage, path),
GitInstallPickInvalidOK
);
return false;
}
return true;
}
void Init()
private void Init()
{
Debug.LogFormat("TODO: Init '{0}'", initDirectory);
}

Просмотреть файл

@ -1,45 +1,50 @@
using UnityEngine;
using UnityEditor;
using Debug = System.Diagnostics.Debug;
namespace GitHub.Unity
{
interface IView
{
Rect position { get; }
void Refresh();
void Repaint();
void OnGUI();
Rect position { get; }
}
abstract class Subview : IView
{
const string NullParentError = "Subview parent is null";
private const string NullParentError = "Subview parent is null";
protected IView parent;
public virtual void Refresh()
{}
public Rect position
public abstract void OnGUI();
public void Repaint()
{
get
{
return parent.position;
}
parent.Repaint();
}
public void Show(IView parent)
public void Show(IView parentView)
{
System.Diagnostics.Debug.Assert(parent != null, NullParentError);
Debug.Assert(parentView != null, NullParentError);
this.parent = parent;
parent = parentView;
OnShow();
}
public virtual void OnSelectionChange()
{}
void OnEnable()
protected virtual void OnShow()
{}
protected virtual void OnHide()
{}
private void OnEnable()
{
if (parent != null)
{
@ -47,25 +52,14 @@ namespace GitHub.Unity
}
}
void OnDisable()
private void OnDisable()
{
OnHide();
}
protected virtual void OnShow() {}
protected virtual void OnHide() {}
public virtual void Refresh() {}
public virtual void OnSelectionChange() {}
public abstract void OnGUI();
public void Repaint()
public Rect position
{
parent.Repaint();
get { return parent.position; }
}
}
}

Просмотреть файл

@ -1,161 +1,56 @@
using UnityEngine;
using UnityEditor;
using System;
using System.Collections.Generic;
using Object = UnityEngine.Object;
using System.Linq;
using UnityEditor;
using UnityEngine;
using Debug = System.Diagnostics.Debug;
namespace GitHub.Unity
{
[System.Serializable]
[Serializable]
class Window : EditorWindow, IView
{
private const float DefaultNotificationTimeout = 4f;
private const string Title = "GitHub";
private const string LaunchMenu = "Window/GitHub";
private const string RefreshButton = "Refresh";
private const string UnknownSubTabError = "Unsupported view mode: {0}";
private const string BadNotificationDelayError = "A delay of {0} is shorter than the default delay and thus would get pre-empted.";
private const string HistoryTitle = "History";
private const string ChangesTitle = "Changes";
private const string BranchesTitle = "Branches";
private const string SettingsTitle = "Settings";
[NonSerialized] private double notificationClearTime = -1;
[SerializeField] private SubTab activeTab = SubTab.History;
[SerializeField] private BranchesView branchesTab = new BranchesView();
[SerializeField] private ChangesView changesTab = new ChangesView();
[SerializeField] private HistoryView historyTab = new HistoryView();
[SerializeField] private SettingsView settingsTab = new SettingsView();
public static void Initialize()
{
RefreshRunner.Initialize();
}
class RefreshRunner : AssetPostprocessor
{
public static void Initialize()
{
Tasks.ScheduleMainThread(Refresh);
}
static void OnPostprocessAllAssets(string[] imported, string[] deleted, string[] moveDestination, string[] moveSource)
{
Refresh();
}
static void Refresh()
{
Utility.UnregisterReadyCallback(OnReady);
Utility.RegisterReadyCallback(OnReady);
}
static void OnReady()
{
foreach (Window window in Object.FindObjectsOfTypeAll(typeof(Window)))
{
window.Refresh();
}
}
}
enum SubTab
{
History,
Changes,
Branches,
Settings
}
const float DefaultNotificationTimeout = 4f;
const string
Title = "GitHub",
LaunchMenu = "Window/GitHub",
RefreshButton = "Refresh",
UnknownSubTabError = "Unsupported view mode: {0}",
BadNotificationDelayError = "A delay of {0} is shorter than the default delay and thus would get pre-empted.",
HistoryTitle = "History",
ChangesTitle = "Changes",
BranchesTitle = "Branches",
SettingsTitle = "Settings";
[MenuItem(LaunchMenu)]
static void Launch()
public static void Launch()
{
GetWindow<Window>().Show();
}
[SerializeField] SubTab activeTab = SubTab.History;
[SerializeField] HistoryView historyTab = new HistoryView();
[SerializeField] ChangesView changesTab = new ChangesView();
[SerializeField] BranchesView branchesTab = new BranchesView();
[SerializeField] SettingsView settingsTab = new SettingsView();
double notificationClearTime = -1;
public HistoryView HistoryTab { get { return historyTab; } }
public ChangesView ChangesTab { get { return changesTab; } }
public BranchesView BranchesTab { get { return branchesTab; } }
public SettingsView SettingsTab { get { return settingsTab; } }
Subview ActiveTab
{
get
{
switch(activeTab)
{
case SubTab.History:
return historyTab;
case SubTab.Changes:
return changesTab;
case SubTab.Branches:
return branchesTab;
case SubTab.Settings:
return settingsTab;
default:
throw new ArgumentException(string.Format(UnknownSubTabError, activeTab));
}
}
}
void OnEnable()
{
historyTab.Show(this);
changesTab.Show(this);
branchesTab.Show(this);
settingsTab.Show(this);
Utility.UnregisterReadyCallback(Refresh);
Utility.RegisterReadyCallback(Refresh);
}
new public void ShowNotification(GUIContent content)
{
ShowNotification(content, DefaultNotificationTimeout);
}
public void ShowNotification(GUIContent content, float timeout)
{
  System.Diagnostics.Debug.Assert(timeout <= DefaultNotificationTimeout, string.Format(BadNotificationDelayError, timeout));
notificationClearTime = timeout < DefaultNotificationTimeout ? EditorApplication.timeSinceStartup + timeout : -1f;
base.ShowNotification(content);
}
public void OnGUI()
{
// Set window title
titleContent = new GUIContent(Title, Styles.TitleIcon);
ProjectSettingsIssue settingsIssues = Utility.Issues.Select(i => i as ProjectSettingsIssue).FirstOrDefault(i => i != null);
var settingsIssues = Utility.Issues.Select(i => i as ProjectSettingsIssue).FirstOrDefault(i => i != null);
// Initial state
if (
!Utility.ActiveRepository ||
!Utility.GitFound ||
(settingsIssues != null && (
settingsIssues.WasCaught(ProjectSettingsEvaluation.EditorSettingsMissing) ||
settingsIssues.WasCaught(ProjectSettingsEvaluation.BadVCSSettings))
)
)
if (!Utility.ActiveRepository || !Utility.GitFound ||
(settingsIssues != null &&
(settingsIssues.WasCaught(ProjectSettingsEvaluation.EditorSettingsMissing) ||
settingsIssues.WasCaught(ProjectSettingsEvaluation.BadVCSSettings))))
{
activeTab = SubTab.Settings; // If we do complete init, make sure that we return to the settings tab for further setup
settingsTab.OnGUI();
@ -164,42 +59,27 @@ namespace GitHub.Unity
// Subtabs & toolbar
GUILayout.BeginHorizontal(EditorStyles.toolbar);
{
EditorGUI.BeginChangeCheck();
{
TabButton(ref activeTab, SubTab.History, HistoryTitle);
TabButton(ref activeTab, SubTab.Changes, ChangesTitle);
TabButton(ref activeTab, SubTab.Branches, BranchesTitle);
TabButton(ref activeTab, SubTab.Settings, SettingsTitle);
}
if (EditorGUI.EndChangeCheck())
{
Refresh();
}
GUILayout.FlexibleSpace();
}
GUILayout.EndHorizontal();
// GUI for the active tab
ActiveTab.OnGUI();
}
void Update()
{
// Notification auto-clear timer override
if (notificationClearTime > 0f && EditorApplication.timeSinceStartup > notificationClearTime)
{
notificationClearTime = -1f;
RemoveNotification();
Repaint();
}
}
static void TabButton(ref SubTab activeTab, SubTab tab, string title)
{
activeTab = GUILayout.Toggle(activeTab == tab, title, EditorStyles.toolbarButton) ? tab : activeTab;
}
public void Refresh()
{
EvaluateProjectConfigurationTask.Schedule();
@ -210,10 +90,124 @@ namespace GitHub.Unity
}
}
public new void ShowNotification(GUIContent content)
{
ShowNotification(content, DefaultNotificationTimeout);
}
void OnSelectionChange()
public void ShowNotification(GUIContent content, float timeout)
{
Debug.Assert(timeout <= DefaultNotificationTimeout, String.Format(BadNotificationDelayError, timeout));
notificationClearTime = timeout < DefaultNotificationTimeout ? EditorApplication.timeSinceStartup + timeout : -1f;
base.ShowNotification(content);
}
private static void TabButton(ref SubTab activeTab, SubTab tab, string title)
{
activeTab = GUILayout.Toggle(activeTab == tab, title, EditorStyles.toolbarButton) ? tab : activeTab;
}
private void OnEnable()
{
historyTab.Show(this);
changesTab.Show(this);
branchesTab.Show(this);
settingsTab.Show(this);
Utility.UnregisterReadyCallback(Refresh);
Utility.RegisterReadyCallback(Refresh);
}
private void Update()
{
// Notification auto-clear timer override
if (notificationClearTime > 0f && EditorApplication.timeSinceStartup > notificationClearTime)
{
notificationClearTime = -1f;
RemoveNotification();
Repaint();
}
}
private void OnSelectionChange()
{
ActiveTab.OnSelectionChange();
}
public HistoryView HistoryTab
{
get { return historyTab; }
}
public ChangesView ChangesTab
{
get { return changesTab; }
}
public BranchesView BranchesTab
{
get { return branchesTab; }
}
public SettingsView SettingsTab
{
get { return settingsTab; }
}
private Subview ActiveTab
{
get
{
switch (activeTab)
{
case SubTab.History:
return historyTab;
case SubTab.Changes:
return changesTab;
case SubTab.Branches:
return branchesTab;
case SubTab.Settings:
return settingsTab;
default:
throw new ArgumentException(String.Format(UnknownSubTabError, activeTab));
}
}
}
private class RefreshRunner : AssetPostprocessor
{
public static void Initialize()
{
Tasks.ScheduleMainThread(Refresh);
}
private static void OnPostprocessAllAssets(string[] imported, string[] deleted, string[] moveDestination, string[] moveSource)
{
Refresh();
}
private static void Refresh()
{
Utility.UnregisterReadyCallback(OnReady);
Utility.RegisterReadyCallback(OnReady);
}
private static void OnReady()
{
foreach (Window window in FindObjectsOfTypeAll(typeof(Window)))
{
window.Refresh();
}
}
}
private enum SubTab
{
History,
Changes,
Branches,
Settings
}
}
}

87
src/common/GitHub.ruleset Normal file
Просмотреть файл

@ -0,0 +1,87 @@
<?xml version="1.0" encoding="utf-8"?>
<RuleSet Name="GitHub" Description="This ruleset only includes the rules we care about. I'll be adding new ones as we fix our codebase." ToolsVersion="14.0">
<IncludeAll Action="Warning" />
<Rules AnalyzerId="Microsoft.Analyzers.ManagedCodeAnalysis" RuleNamespace="Microsoft.Rules.Managed">
<Rule Id="CA1000" Action="None" />
<Rule Id="CA1002" Action="None" />
<Rule Id="CA1006" Action="None" />
<Rule Id="CA1014" Action="None" />
<Rule Id="CA1017" Action="None" />
<Rule Id="CA1020" Action="None" />
<Rule Id="CA1021" Action="None" />
<Rule Id="CA1024" Action="None" />
<Rule Id="CA1026" Action="None" />
<Rule Id="CA1030" Action="None" />
<Rule Id="CA1031" Action="None" />
<Rule Id="CA1033" Action="None" />
<Rule Id="CA1034" Action="None" />
<Rule Id="CA1045" Action="None" />
<Rule Id="CA1051" Action="None" />
<Rule Id="CA1054" Action="None" />
<Rule Id="CA1055" Action="None" />
<Rule Id="CA1056" Action="None" />
<Rule Id="CA1062" Action="None" />
<Rule Id="CA1063" Action="None" />
<Rule Id="CA1303" Action="None" />
<Rule Id="CA1305" Action="None" />
<Rule Id="CA1307" Action="None" />
<Rule Id="CA1401" Action="None" />
<Rule Id="CA1402" Action="None" />
<Rule Id="CA1403" Action="None" />
<Rule Id="CA1405" Action="None" />
<Rule Id="CA1406" Action="None" />
<Rule Id="CA1407" Action="None" />
<Rule Id="CA1408" Action="None" />
<Rule Id="CA1409" Action="None" />
<Rule Id="CA1410" Action="None" />
<Rule Id="CA1411" Action="None" />
<Rule Id="CA1412" Action="None" />
<Rule Id="CA1413" Action="None" />
<Rule Id="CA1501" Action="None" />
<Rule Id="CA1502" Action="None" />
<Rule Id="CA1506" Action="None" />
<Rule Id="CA1701" Action="None" />
<Rule Id="CA1702" Action="None" />
<Rule Id="CA1703" Action="None" />
<Rule Id="CA1704" Action="None" />
<Rule Id="CA1707" Action="None" />
<Rule Id="CA1708" Action="None" />
<Rule Id="CA1709" Action="None" />
<Rule Id="CA1710" Action="None" />
<Rule Id="CA1711" Action="None" />
<Rule Id="CA1712" Action="None" />
<Rule Id="CA1713" Action="None" />
<Rule Id="CA1714" Action="None" />
<Rule Id="CA1715" Action="None" />
<Rule Id="CA1716" Action="None" />
<Rule Id="CA1717" Action="None" />
<Rule Id="CA1719" Action="None" />
<Rule Id="CA1720" Action="None" />
<Rule Id="CA1721" Action="None" />
<Rule Id="CA1722" Action="None" />
<Rule Id="CA1724" Action="None" />
<Rule Id="CA1725" Action="None" />
<Rule Id="CA1726" Action="None" />
<Rule Id="CA1800" Action="None" />
<Rule Id="CA1801" Action="None" />
<Rule Id="CA1810" Action="None" />
<Rule Id="CA1811" Action="None" />
<Rule Id="CA1812" Action="None" />
<Rule Id="CA1822" Action="None" />
<Rule Id="CA2000" Action="None" />
<Rule Id="CA2118" Action="None" />
<Rule Id="CA2122" Action="None" />
<Rule Id="CA2200" Action="None" />
<Rule Id="CA2201" Action="Warning" />
<Rule Id="CA2204" Action="None" />
<Rule Id="CA2210" Action="None" />
<Rule Id="CA2211" Action="None" />
<Rule Id="CA2213" Action="None" />
<Rule Id="CA2227" Action="None" />
<Rule Id="CA2235" Action="None" />
<Rule Id="IDE001" Action="None" />
</Rules>
<Rules AnalyzerId="Microsoft.CodeAnalysis.CSharp.Features" RuleNamespace="Microsoft.CodeAnalysis.CSharp.Features">
<Rule Id="IDE0001" Action="None" />
</Rules>
</RuleSet>