1
0
Fork 0
mirror of https://github.com/beefytech/Beef.git synced 2025-07-04 23:36:00 +02:00

Initial package management support

This commit is contained in:
Brian Fiete 2024-10-21 09:18:07 -04:00
parent 78138f5c5a
commit 4870c6fdd8
19 changed files with 2520 additions and 205 deletions

326
IDE/src/util/GitManager.bf Normal file
View file

@ -0,0 +1,326 @@
#pragma warning disable 168
using System.Diagnostics;
using System;
using System.Threading;
using System.IO;
using System.Collections;
namespace IDE.util;
class GitManager
{
public enum Error
{
Unknown
}
public class GitInstance : RefCounted
{
public class TagInfo
{
public String mHash ~ delete _;
public String mTag ~ delete _;
}
public GitManager mGitManager;
public bool mFailed;
public bool mDone;
public bool mStarted;
public bool mRemoved;
public String mArgs ~ delete _;
public String mPath ~ delete _;
public float mProgress;
public float mProgressRecv;
public float mProgressDeltas;
public float mProgressFiles;
public Stopwatch mStopwatch = new .()..Start() ~ delete _;
public SpawnedProcess mProcess ~ delete _;
public Monitor mMonitor = new .() ~ delete _;
public List<String> mDeferredOutput = new .() ~ DeleteContainerAndItems!(_);
public List<TagInfo> mTagInfos = new .() ~ DeleteContainerAndItems!(_);
public Thread mOutputThread ~ delete _;
public Thread mErrorThread ~ delete _;
public this(GitManager gitManager)
{
mGitManager = gitManager;
}
public ~this()
{
IDEUtils.SafeKill(mProcess);
mOutputThread?.Join();
mErrorThread?.Join();
if (!mRemoved)
mGitManager.mGitInstances.Remove(this);
}
public void Init(StringView args, StringView path)
{
mArgs = new .(args);
if (path != default)
mPath = new .(path);
}
public void Start()
{
if (mStarted)
return;
mStarted = true;
ProcessStartInfo psi = scope ProcessStartInfo();
String gitPath = scope .();
#if BF_PLATFORM_WINDOWS
Path.GetAbsolutePath(gApp.mInstallDir, "git/cmd/git.exe", gitPath);
if (!File.Exists(gitPath))
gitPath.Clear();
if (gitPath.IsEmpty)
{
Path.GetAbsolutePath(gApp.mInstallDir, "../../bin/git/cmd/git.exe", gitPath);
if (!File.Exists(gitPath))
gitPath.Clear();
}
if (gitPath.IsEmpty)
{
Path.GetAbsolutePath(gApp.mInstallDir, "../../../bin/git/cmd/git.exe", gitPath);
if (!File.Exists(gitPath))
gitPath.Clear();
}
#endif
if (gitPath.IsEmpty)
gitPath.Set("git");
psi.SetFileName(gitPath);
psi.SetArguments(mArgs);
if (mPath != null)
psi.SetWorkingDirectory(mPath);
psi.UseShellExecute = false;
psi.RedirectStandardError = true;
psi.RedirectStandardOutput = true;
psi.CreateNoWindow = true;
mProcess = new SpawnedProcess();
if (mProcess.Start(psi) case .Err)
{
gApp.OutputErrorLine("Failed to execute Git");
mFailed = true;
return;
}
mOutputThread = new Thread(new => ReadOutputThread);
mOutputThread.Start(false);
mErrorThread = new Thread(new => ReadErrorThread);
mErrorThread.Start(false);
}
public void ReadOutputThread()
{
FileStream fileStream = scope FileStream();
if (mProcess.AttachStandardOutput(fileStream) case .Err)
return;
StreamReader streamReader = scope StreamReader(fileStream, null, false, 4096);
int count = 0;
while (true)
{
count++;
var buffer = scope String();
if (streamReader.ReadLine(buffer) case .Err)
break;
using (mMonitor.Enter())
{
mDeferredOutput.Add(new .(buffer));
}
}
}
public void ReadErrorThread()
{
FileStream fileStream = scope FileStream();
if (mProcess.AttachStandardError(fileStream) case .Err)
return;
StreamReader streamReader = scope StreamReader(fileStream, null, false, 4096);
while (true)
{
var buffer = scope String();
if (streamReader.ReadLine(buffer) case .Err)
break;
using (mMonitor.Enter())
{
//mDeferredOutput.Add(new $"{mStopwatch.ElapsedMilliseconds / 1000.0:0.0}: {buffer}");
mDeferredOutput.Add(new .(buffer));
}
}
}
public void Update()
{
using (mMonitor.Enter())
{
while (!mDeferredOutput.IsEmpty)
{
var line = mDeferredOutput.PopFront();
defer delete line;
//Debug.WriteLine($"GIT: {line}");
if (line.StartsWith("Cloning into "))
{
// May be starting a submodule
mProgressRecv = 0;
mProgressDeltas = 0;
mProgressFiles = 0;
}
if (line.StartsWith("remote: Counting objects"))
{
mProgressRecv = 0.001f;
}
if (line.StartsWith("Receiving objects: "))
{
var pctStr = line.Substring("Receiving objects: ".Length, 3)..Trim();
mProgressRecv = float.Parse(pctStr).GetValueOrDefault() / 100.0f;
}
if (line.StartsWith("Resolving deltas: "))
{
var pctStr = line.Substring("Resolving deltas: ".Length, 3)..Trim();
mProgressDeltas = float.Parse(pctStr).GetValueOrDefault() / 100.0f;
mProgressRecv = 1.0f;
}
if (line.StartsWith("Updating files: "))
{
var pctStr = line.Substring("Updating files: ".Length, 3)..Trim();
mProgressFiles = float.Parse(pctStr).GetValueOrDefault() / 100.0f;
mProgressRecv = 1.0f;
mProgressDeltas = 1.0f;
}
StringView version = default;
int refTagIdx = line.IndexOf("\trefs/tags/");
if (refTagIdx == 40)
version = line.Substring(40 + "\trefs/tags/".Length);
if ((line.Length == 45) && (line.EndsWith("HEAD")))
version = "HEAD";
if (!version.IsEmpty)
{
TagInfo tagInfo = new .();
tagInfo.mHash = new .(line, 0, 40);
tagInfo.mTag = new .(version);
mTagInfos.Add(tagInfo);
}
}
}
float pct = 0;
if (mProgressRecv > 0)
pct = 0.1f + (mProgressRecv * 0.3f) + (mProgressDeltas * 0.4f) + (mProgressFiles * 0.2f);
if (pct > mProgress)
{
mProgress = pct;
//Debug.WriteLine($"Completed Pct: {pct}");
}
if (mProcess.WaitFor(0))
{
if (mProcess.ExitCode != 0)
mFailed = true;
mDone = true;
}
}
public void Cancel()
{
if (!mProcess.WaitFor(0))
{
//Debug.WriteLine($"GitManager Cancel {mProcess.ProcessId}");
IDEUtils.SafeKill(mProcess);
}
}
}
public const int sMaxActiveGitInstances = 4;
public List<GitInstance> mGitInstances = new .() ~
{
for (var gitInstance in _)
gitInstance.ReleaseRef();
delete _;
};
public void Init()
{
//StartGit("-v");
//Repository repository = Clone("https://github.com/llvm/llvm-project", "c:/temp/__LLVM");
//Repository repository = Clone("https://github.com/Starpelly/raylib-beef", "c:/temp/__RAYLIB");
/*while (true)
{
Thread.Sleep(500);
Debug.WriteLine($"Repository {repository.mStatus} {repository.GetCompletedPct()}");
}*/
}
public GitInstance StartGit(StringView cmd, StringView path = default)
{
//Debug.WriteLine($"GIT STARTING: {cmd} in {path}");
GitInstance gitInst = new .(this);
gitInst.Init(cmd, path);
mGitInstances.Add(gitInst);
return gitInst;
}
public GitInstance Clone(StringView url, StringView path)
{
return StartGit(scope $"clone -v --progress --recurse-submodules {url} \"{path}\"");
}
public GitInstance Checkout(StringView path, StringView hash)
{
return StartGit(scope $"checkout -b BeefManaged {hash}", path);
}
public GitInstance GetTags(StringView url)
{
return StartGit(scope $"ls-remote {url}");
}
public void Update()
{
for (var gitInstance in mGitInstances)
{
if (@gitInstance.Index >= sMaxActiveGitInstances)
break;
if (!gitInstance.mStarted)
gitInstance.Start();
gitInstance.Update();
if (gitInstance.mDone)
{
@gitInstance.Remove();
gitInstance.mRemoved = true;
gitInstance.ReleaseRef();
}
}
}
}

View file

@ -1,35 +1,397 @@
#pragma warning disable 168
using System;
using IDE.Util;
#if BF_PLATFORM_WINDOWS
using static Git.GitApi;
#define SUPPORT_GIT
#endif
using System.Collections;
using System.Security.Cryptography;
using System.IO;
using Beefy.utils;
using System.Threading;
namespace IDE.util
{
class PackMan
{
class GitHelper
public class WorkItem
{
static bool sInitialized;
public this()
public enum Kind
{
if (!sInitialized)
{
#if SUPPORT_GIT
#unwarn
var result = git_libgit2_init();
sInitialized = true;
#endif
}
None,
FindVersion,
Clone,
Checkout
}
public Kind mKind;
public String mProjectName ~ delete _;
public String mURL ~ delete _;
public List<String> mConstraints ~ DeleteContainerAndItems!(_);
public String mTag ~ delete _;
public String mHash ~ delete _;
public String mPath ~ delete _;
public GitManager.GitInstance mGitInstance ~ _?.ReleaseRef();
public ~this()
{
mGitInstance?.Cancel();
}
}
public List<WorkItem> mWorkItems = new .() ~ DeleteContainerAndItems!(_);
public bool mInitialized;
public String mManagedPath ~ delete _;
public bool mFailed;
public void Fail(StringView error)
{
gApp.OutputErrorLine(error);
if (!mFailed)
{
mFailed = true;
gApp.[Friend]FlushDeferredLoadProjects();
}
}
public bool CheckInit()
{
if (mInitialized)
return true;
if (gApp.mBeefConfig.mManagedLibPath.IsEmpty)
return false;
mManagedPath = new .(gApp.mBeefConfig.mManagedLibPath);
mInitialized = true;
return true;
}
public void GetPath(StringView url, StringView hash, String outPath)
{
//var urlHash = SHA256.Hash(url.ToRawData()).ToString(.. scope .());
//outPath.AppendF($"{mManagedPath}/{urlHash}/{hash}");
outPath.AppendF($"{mManagedPath}/{hash}");
}
public bool CheckLock(StringView projectName, String outPath)
{
if (!CheckInit())
return false;
if (gApp.mWantUpdateVersionLocks != null)
{
if ((gApp.mWantUpdateVersionLocks.IsEmpty) || (gApp.mWantUpdateVersionLocks.ContainsAlt(projectName)))
return false;
}
if (!gApp.mWorkspace.mProjectLockMap.TryGetAlt(projectName, ?, var lock))
return false;
switch (lock)
{
case .Git(let url, let tag, let hash):
var path = GetPath(url, hash, .. scope .());
var managedFilePath = scope $"{path}/BeefManaged.toml";
if (File.Exists(managedFilePath))
{
outPath.Append(path);
outPath.Append("/BeefProj.toml");
return true;
}
default:
}
return false;
}
public void CloneCompleted(StringView projectName, StringView url, StringView tag, StringView hash, StringView path)
{
gApp.mWorkspace.SetLock(projectName, .Git(new .(url), new .(tag), new .(hash)));
StructuredData sd = scope .();
sd.CreateNew();
sd.Add("FileVersion", 1);
sd.Add("Version", tag);
sd.Add("GitURL", url);
sd.Add("GitTag", tag);
sd.Add("GitHash", hash);
var tomlText = sd.ToTOML(.. scope .());
var managedFilePath = scope $"{path}/BeefManaged.toml";
File.WriteAllText(managedFilePath, tomlText).IgnoreError();
}
public void GetWithHash(StringView projectName, StringView url, StringView tag, StringView hash)
{
if (!CheckInit())
return;
String destPath = GetPath(url, hash, .. scope .());
var urlPath = Path.GetDirectoryPath(destPath, .. scope .());
Directory.CreateDirectory(urlPath).IgnoreError();
if (Directory.Exists(destPath))
{
var managedFilePath = scope $"{destPath}/BeefManaged.toml";
if (File.Exists(managedFilePath))
{
if (gApp.mVerbosity >= .Normal)
{
if (tag.IsEmpty)
gApp.OutputLine($"Git selecting library '{projectName}' at {hash.Substring(0, 7)}");
else
gApp.OutputLine($"Git selecting library '{projectName}' tag '{tag}' at {hash.Substring(0, 7)}");
}
CloneCompleted(projectName, url, tag, hash, destPath);
ProjectReady(projectName, destPath);
return;
}
String tempDir = new $"{destPath}__{(int32)Internal.GetTickCountMicro():X}";
//if (Directory.DelTree(destPath) case .Err)
if (Directory.Move(destPath, tempDir) case .Err)
{
delete tempDir;
Fail(scope $"Failed to remove directory '{destPath}'");
return;
}
ThreadPool.QueueUserWorkItem(new () =>
{
Directory.DelTree(tempDir);
}
~
{
delete tempDir;
});
}
if (gApp.mVerbosity >= .Normal)
{
if (tag.IsEmpty)
gApp.OutputLine($"Git cloning library '{projectName}' at {hash.Substring(0, 7)}...");
else
gApp.OutputLine($"Git cloning library '{projectName}' tag '{tag}' at {hash.Substring(0, 7)}");
}
WorkItem workItem = new .();
workItem.mKind = .Clone;
workItem.mProjectName = new .(projectName);
workItem.mURL = new .(url);
workItem.mTag = new .(tag);
workItem.mHash = new .(hash);
workItem.mPath = new .(destPath);
mWorkItems.Add(workItem);
}
public void GetWithVersion(StringView projectName, StringView url, SemVer semVer)
{
if (!CheckInit())
return;
bool ignoreLock = false;
if (gApp.mWantUpdateVersionLocks != null)
{
if ((gApp.mWantUpdateVersionLocks.IsEmpty) || (gApp.mWantUpdateVersionLocks.ContainsAlt(projectName)))
ignoreLock = true;
}
if ((!ignoreLock) && (gApp.mWorkspace.mProjectLockMap.TryGetAlt(projectName, ?, var lock)))
{
switch (lock)
{
case .Git(let checkURL, let tag, let hash):
if (checkURL == url)
GetWithHash(projectName, url, tag, hash);
return;
default:
}
}
if (gApp.mVerbosity >= .Normal)
gApp.OutputLine($"Git retrieving version list for '{projectName}'");
WorkItem workItem = new .();
workItem.mKind = .FindVersion;
workItem.mProjectName = new .(projectName);
workItem.mURL = new .(url);
if (semVer != null)
workItem.mConstraints = new .() { new String(semVer.mVersion) };
mWorkItems.Add(workItem);
}
public void UpdateGitConstraint(StringView url, SemVer semVer)
{
for (var workItem in mWorkItems)
{
if ((workItem.mKind == .FindVersion) && (workItem.mURL == url))
{
if (workItem.mConstraints == null)
workItem.mConstraints = new .();
workItem.mConstraints.Add(new String(semVer.mVersion));
}
}
}
public void Checkout(StringView projectName, StringView url, StringView path, StringView tag, StringView hash)
{
if (!CheckInit())
return;
WorkItem workItem = new .();
workItem.mKind = .Checkout;
workItem.mProjectName = new .(projectName);
workItem.mURL = new .(url);
workItem.mTag = new .(tag);
workItem.mHash = new .(hash);
workItem.mPath = new .(path);
mWorkItems.Add(workItem);
}
public void ProjectReady(StringView projectName, StringView path)
{
if (var project = gApp.mWorkspace.FindProject(projectName))
{
String projectPath = scope $"{path}/BeefProj.toml";
project.mProjectPath.Set(projectPath);
gApp.RetryProjectLoad(project, false);
}
}
public void Update()
{
bool executingGit = false;
// First handle active git items
for (var workItem in mWorkItems)
{
if (workItem.mGitInstance == null)
continue;
if (!workItem.mGitInstance.mDone)
{
executingGit = true;
continue;
}
if (!workItem.mGitInstance.mFailed)
{
switch (workItem.mKind)
{
case .FindVersion:
gApp.CompilerLog("");
StringView bestTag = default;
StringView bestHash = default;
for (var tag in workItem.mGitInstance.mTagInfos)
{
if ((tag.mTag == "HEAD") && (workItem.mConstraints == null))
bestHash = tag.mHash;
else if (workItem.mConstraints != null)
{
bool hasMatch = false;
for (var constraint in workItem.mConstraints)
{
if (SemVer.IsVersionMatch(tag.mTag, constraint))
{
hasMatch = true;
break;
}
}
if (hasMatch)
{
if ((bestTag.IsEmpty) || (SemVer.Compare(tag.mTag, bestTag) > 0))
{
bestTag = tag.mTag;
bestHash = tag.mHash;
}
}
}
}
if (bestHash != default)
{
GetWithHash(workItem.mProjectName, workItem.mURL, bestTag, bestHash);
}
else
{
String constraints = scope .();
for (var constraint in workItem.mConstraints)
{
if (!constraints.IsEmpty)
constraints.Append(", ");
constraints.Append('\'');
constraints.Append(constraint);
constraints.Append('\'');
}
Fail(scope $"Failed to locate version for '{workItem.mProjectName}' with constraints '{constraints}'");
}
case .Clone:
Checkout(workItem.mProjectName, workItem.mURL, workItem.mPath, workItem.mTag, workItem.mHash);
case .Checkout:
CloneCompleted(workItem.mProjectName, workItem.mURL, workItem.mTag, workItem.mHash, workItem.mPath);
ProjectReady(workItem.mProjectName, workItem.mPath);
if (gApp.mVerbosity >= .Normal)
gApp.OutputLine($"Git cloning library '{workItem.mProjectName}' done.");
default:
}
}
@workItem.Remove();
delete workItem;
}
if (!executingGit)
{
// First handle active git items
for (var workItem in mWorkItems)
{
if (workItem.mGitInstance != null)
continue;
switch (workItem.mKind)
{
case .FindVersion:
workItem.mGitInstance = gApp.mGitManager.GetTags(workItem.mURL)..AddRef();
case .Checkout:
workItem.mGitInstance = gApp.mGitManager.Checkout(workItem.mPath, workItem.mHash)..AddRef();
case .Clone:
workItem.mGitInstance = gApp.mGitManager.Clone(workItem.mURL, workItem.mPath)..AddRef();
default:
}
}
}
}
public void GetHashFromFilePath(StringView filePath, String path)
{
if (mManagedPath == null)
return;
if (!filePath.StartsWith(mManagedPath))
return;
StringView hashPart = filePath.Substring(mManagedPath.Length);
if (hashPart.Length < 42)
return;
hashPart.RemoveFromStart(1);
hashPart.Length = 40;
path.Append(hashPart);
}
public void CancelAll()
{
if (mWorkItems.IsEmpty)
return;
Fail("Aborted project transfer");
mWorkItems.ClearAndDeleteItems();
}
}
}

View file

@ -2,10 +2,51 @@ using System;
namespace IDE.Util
{
[Reflect]
class SemVer
{
public struct Parts
{
public enum Kind
{
case Empty;
case Num(int32 val);
case Wild;
public int32 NumOrDefault
{
get
{
if (this case .Num(let val))
return val;
return 0;
}
}
}
public Kind[3] mPart;
public StringView mPreRelease;
public Kind Major => mPart[0];
public Kind Minor => mPart[1];
public Kind Patch => mPart[2];
}
enum CompareKind
{
Caret, // Default
Tilde,
Equal,
Gt,
Gte,
Lt,
Lte
}
public String mVersion ~ delete _;
public bool IsEmpty => String.IsNullOrEmpty(mVersion);
public this()
{
@ -27,5 +68,240 @@ namespace IDE.Util
mVersion = new String(ver);
return .Ok;
}
public static Result<Parts> GetParts(StringView version)
{
int startIdx = 0;
int partIdx = 0;
if (version.IsEmpty)
return .Err;
if (version.StartsWith("V", .OrdinalIgnoreCase))
startIdx++;
Parts parts = .();
Result<void> SetPart(Parts.Kind kind)
{
if (partIdx >= 3)
return .Err;
parts.mPart[partIdx] = kind;
partIdx++;
return .Ok;
}
Result<void> FlushPart(int i)
{
StringView partStr = version.Substring(startIdx, i - startIdx);
if (!partStr.IsEmpty)
{
int32 partNum = Try!(int32.Parse(partStr));
Try!(SetPart(.Num(partNum)));
}
return .Ok;
}
for (int i in startIdx ..< version.Length)
{
char8 c = version[i];
if (c.IsWhiteSpace)
return .Err;
if (c == '.')
{
Try!(FlushPart(i));
startIdx = i + 1;
continue;
}
else if (c.IsNumber)
{
continue;
}
else if (c == '-')
{
if (partIdx == 0)
return .Err;
parts.mPreRelease = version.Substring(i);
return .Ok(parts);
}
else if (c == '*')
{
Try!(SetPart(.Wild));
continue;
}
return .Err;
}
Try!(FlushPart(version.Length));
return parts;
}
public Result<Parts> GetParts()
{
return GetParts(mVersion);
}
public static bool IsVersionMatch(StringView fullVersion, StringView wildcard)
{
int commaPos = wildcard.IndexOf(',');
if (commaPos != -1)
return IsVersionMatch(fullVersion, wildcard.Substring(0, commaPos)..Trim()) && IsVersionMatch(fullVersion, wildcard.Substring(commaPos + 1)..Trim());
var wildcard;
wildcard.Trim();
CompareKind compareKind = .Caret;
if (wildcard.StartsWith('^'))
{
compareKind = .Caret;
wildcard.RemoveFromStart(1);
}
else if (wildcard.StartsWith('~'))
{
compareKind = .Tilde;
wildcard.RemoveFromStart(1);
}
else if (wildcard.StartsWith('='))
{
compareKind = .Equal;
wildcard.RemoveFromStart(1);
}
else if (wildcard.StartsWith('>'))
{
compareKind = .Gt;
wildcard.RemoveFromStart(1);
if (wildcard.StartsWith('='))
{
compareKind = .Gte;
wildcard.RemoveFromStart(1);
}
}
else if (wildcard.StartsWith('<'))
{
compareKind = .Lt;
wildcard.RemoveFromStart(1);
if (wildcard.StartsWith('='))
{
compareKind = .Lte;
wildcard.RemoveFromStart(1);
}
}
wildcard.Trim();
// Does we include equality?
if ((compareKind != .Gt) && (compareKind != .Lt))
{
if (fullVersion == wildcard)
return true;
}
Parts full;
if (!(GetParts(fullVersion) case .Ok(out full)))
return false;
Parts wild;
if (!(GetParts(wildcard) case .Ok(out wild)))
return false;
// Don't allow a general wildcard to match a pre-prelease
if ((!full.mPreRelease.IsEmpty) && (full.mPreRelease != wild.mPreRelease))
return false;
for (int partIdx < 3)
{
if (wild.mPart[partIdx] case .Wild)
return true;
int comp = full.mPart[partIdx].NumOrDefault <=> wild.mPart[partIdx].NumOrDefault;
switch (compareKind)
{
case .Caret:
if ((full.mPart[partIdx].NumOrDefault > 0) || (wild.mPart[partIdx].NumOrDefault > 0))
{
if (comp != 0)
return false;
// First number matches, now make sure we are at least a high enough version on the other numbers
compareKind = .Gte;
}
case .Tilde:
if (wild.mPart[partIdx] case .Empty)
return true;
if (partIdx == 2)
{
if (comp < 0)
return false;
}
else if (comp != 0)
return false;
case .Equal:
if (wild.mPart[partIdx] case .Empty)
return true;
if (comp != 0)
return false;
case .Gt:
if (comp > 0)
return true;
if (partIdx == 2)
return false;
if (comp < 0)
return false;
case .Gte:
if (comp < 0)
return false;
case .Lt:
if (comp < 0)
return true;
if (partIdx == 2)
return false;
if (comp > 0)
return false;
case .Lte:
if (comp > 0)
return false;
default:
}
}
return true;
}
public static bool IsVersionMatch(SemVer fullVersion, SemVer wildcard) => IsVersionMatch(fullVersion.mVersion, wildcard.mVersion);
public static Result<int> Compare(StringView lhs, StringView rhs)
{
Parts lhsParts;
if (!(GetParts(lhs) case .Ok(out lhsParts)))
return .Err;
Parts rhsParts;
if (!(GetParts(rhs) case .Ok(out rhsParts)))
return .Err;
int comp = 0;
for (int partIdx < 3)
{
comp = lhsParts.mPart[partIdx].NumOrDefault <=> rhsParts.mPart[partIdx].NumOrDefault;
if (comp != 0)
return comp;
}
// Don't allow a general wildcard to match a pre-prelease
if ((!lhsParts.mPreRelease.IsEmpty) || (!rhsParts.mPreRelease.IsEmpty))
{
if (lhsParts.mPreRelease.IsEmpty)
return 1;
if (rhsParts.mPreRelease.IsEmpty)
return -1;
return lhsParts.mPreRelease <=> rhsParts.mPreRelease;
}
return comp;
}
public override void ToString(String strBuffer)
{
strBuffer.Append(mVersion);
}
public static int operator<=>(Self lhs, Self rhs) => (lhs?.mVersion ?? "") <=> (rhs?.mVersion ?? "");
}
}

View file

@ -39,6 +39,8 @@ namespace IDE.Util
case .Path(let path):
return .Path(new String(path));
case .Git(let url, let ver):
if (ver == null)
return .Git(new String(url), null);
return .Git(new String(url), new SemVer(ver));
}
}
@ -113,7 +115,7 @@ namespace IDE.Util
using (data.CreateObject(name))
{
data.Add("Git", path);
if (ver != null)
if ((ver != null) && (!ver.mVersion.IsEmpty))
data.Add("Version", ver.mVersion);
}
case .SemVer(var ver):