Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/App.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,7 @@ private bool TryLaunchAsFileHistoryViewer(IClassicDesktopStyleApplicationLifetim
var test = new Commands.QueryRepositoryRootPath(dir).GetResult();
if (!test.IsSuccess || string.IsNullOrEmpty(test.StdOut))
{
Console.Out.WriteLine($"'{args[1]}' is not in a valid git repository");
Console.Out.WriteLine(string.IsNullOrWhiteSpace(test.StdErr) ? $"'{args[1]}' is not in a valid git repository" : test.StdErr.Trim());
desktop.Shutdown(-1);
return true;
}
Expand Down Expand Up @@ -395,7 +395,7 @@ private bool TryLaunchAsBlameViewer(IClassicDesktopStyleApplicationLifetime desk
var test = new Commands.QueryRepositoryRootPath(dir).GetResult();
if (!test.IsSuccess || string.IsNullOrEmpty(test.StdOut))
{
Console.Out.WriteLine($"'{args[1]}' is not in a valid git repository");
Console.Out.WriteLine(string.IsNullOrWhiteSpace(test.StdErr) ? $"'{args[1]}' is not in a valid git repository" : test.StdErr.Trim());
desktop.Shutdown(-1);
return true;
}
Expand Down
11 changes: 11 additions & 0 deletions src/Commands/AddSafeDirectory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace SourceGit.Commands
{
public class AddSafeDirectory : Command
{
public AddSafeDirectory(string ctx, string value)
{
Context = ctx;
Args = $"config --global --add safe.directory {value.Quoted()}";
}
}
}
4 changes: 2 additions & 2 deletions src/Commands/Command.cs
Original file line number Diff line number Diff line change
Expand Up @@ -210,8 +210,8 @@ protected ProcessStartInfo CreateGitStartInfo(bool redirect)
if (!start.Environment.ContainsKey("GIT_SSH_COMMAND") && !string.IsNullOrEmpty(SSHKey))
start.Environment.Add("GIT_SSH_COMMAND", $"ssh -i '{SSHKey}' -o AddKeysToAgent=yes");

// Force using en_US.UTF-8 locale
if (OperatingSystem.IsLinux())
// Force the C locale on Unix, so that git's output (including fatal errors) can always be parsed.
if (!OperatingSystem.IsWindows())
{
start.Environment.Add("LANG", "C");
start.Environment.Add("LC_ALL", "C");
Expand Down
103 changes: 103 additions & 0 deletions src/Models/SafeDirectories.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
using System;
using System.Text;
using System.Text.RegularExpressions;

namespace SourceGit.Models
{
/// <summary>
/// Helpers for the `safe.directory` protection introduced by git 2.35.2 (CVE-2022-24765).
/// Git refuses to use a repository whose top-level directory is owned by another user (which is
/// always the case for network shares / UNC paths) unless the path is listed in the `safe.directory` config.
/// </summary>
public static partial class SafeDirectories
{
public static bool IsUntrustedRepository(string output)
{
if (string.IsNullOrWhiteSpace(output))
return false;

return output.Contains("detected dubious ownership", StringComparison.OrdinalIgnoreCase) ||
output.Contains("unsafe repository", StringComparison.OrdinalIgnoreCase);
}

/// <summary>
/// Tries to get the value that should be written into the `safe.directory` config.
/// Prefers the value suggested by git itself (which knows the correct form for the current platform),
/// and falls back to building a value from the given path.
/// </summary>
public static bool TryGetSafeDirectoryValue(string path, string output, out string value)
{
value = ParseSuggestedValue(output);
if (!string.IsNullOrEmpty(value))
return true;

var normalized = Normalize(path);
if (string.IsNullOrEmpty(normalized))
return false;

// Git for Windows uses the `%(prefix)/` prefix for UNC paths.
value = OperatingSystem.IsWindows() && normalized.StartsWith("//", StringComparison.Ordinal) ? $"%(prefix)/{normalized}" : normalized;
return true;
}

private static string Normalize(string path)
{
return path?.Replace('\\', '/').TrimEnd('/') ?? string.Empty;
}

private static string ParseSuggestedValue(string output)
{
if (string.IsNullOrEmpty(output))
return string.Empty;

var match = REG_SAFE_DIRECTORY_HINT().Match(output);
if (!match.Success)
return string.Empty;

return Dequote(match.Groups["value"].Value.Trim());
}

/// <summary>
/// Git quotes the suggested value with `sq_quote_buf()`, which escapes `'` and `!` as `'\''` and
/// `'\!'`. Decodes it back to the plain path (see `quote.c` of git).
/// </summary>
private static string Dequote(string value)
{
if (value.Length > 1 && value[0] == '\'')
{
var builder = new StringBuilder(value.Length);
for (var i = 1; i < value.Length; i++)
{
var c = value[i];
if (c != '\'')
{
builder.Append(c);
continue;
}

if (i == value.Length - 1)
return builder.ToString();

if (i + 3 < value.Length && value[i + 1] == '\\' && (value[i + 2] == '\'' || value[i + 2] == '!') && value[i + 3] == '\'')
{
builder.Append(value[i + 2]);
i += 3;
continue;
}

return string.Empty;
}

return string.Empty;
}

if (value.Length > 1 && value[0] == '"' && value[^1] == '"')
return value[1..^1];

return value;
}

[GeneratedRegex(@"--add\s+safe\.directory\s+(?<value>[^\r\n]+)", RegexOptions.Multiline)]
private static partial Regex REG_SAFE_DIRECTORY_HINT();
}
}
6 changes: 6 additions & 0 deletions src/Resources/Locales/en_US.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -997,6 +997,12 @@
<x:String x:Key="Text.TagCM.Merge" xml:space="preserve">Merge ${0}$ into ${1}$...</x:String>
<x:String x:Key="Text.TagCM.Push" xml:space="preserve">Push ${0}$...</x:String>
<x:String x:Key="Text.Terminate" xml:space="preserve">TERMINATE</x:String>
<x:String x:Key="Text.TrustRepository" xml:space="preserve">Untrusted Repository</x:String>
<x:String x:Key="Text.TrustRepository.CommandTip" xml:space="preserve">Will run: </x:String>
<x:String x:Key="Text.TrustRepository.Description" xml:space="preserve">Git refused to access this repository: </x:String>
<x:String x:Key="Text.TrustRepository.Path" xml:space="preserve">Path:</x:String>
<x:String x:Key="Text.TrustRepository.Permanent" xml:space="preserve">Trust this repository</x:String>
<x:String x:Key="Text.TrustRepository.ScanSkipped" xml:space="preserve">{0} repositories were skipped because git does not trust them. Please open them manually and confirm whether to trust them.</x:String>
<x:String x:Key="Text.UpdateSubmodules" xml:space="preserve">Update Submodules</x:String>
<x:String x:Key="Text.UpdateSubmodules.All" xml:space="preserve">All submodules</x:String>
<x:String x:Key="Text.UpdateSubmodules.Init" xml:space="preserve">Initialize as needed</x:String>
Expand Down
6 changes: 6 additions & 0 deletions src/Resources/Locales/zh_CN.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -1001,6 +1001,12 @@
<x:String x:Key="Text.TagCM.Merge" xml:space="preserve">合并 ${0}$ 到 ${1}$...</x:String>
<x:String x:Key="Text.TagCM.Push" xml:space="preserve">推送 ${0}$...</x:String>
<x:String x:Key="Text.Terminate" xml:space="preserve">终止运行</x:String>
<x:String x:Key="Text.TrustRepository" xml:space="preserve">不受信任的仓库</x:String>
<x:String x:Key="Text.TrustRepository.CommandTip" xml:space="preserve">将执行:</x:String>
<x:String x:Key="Text.TrustRepository.Description" xml:space="preserve">Git 拒绝访问该仓库: </x:String>
<x:String x:Key="Text.TrustRepository.Path" xml:space="preserve">路径 :</x:String>
<x:String x:Key="Text.TrustRepository.Permanent" xml:space="preserve">信任此仓库</x:String>
<x:String x:Key="Text.TrustRepository.ScanSkipped" xml:space="preserve">有 {0} 个仓库因 git 的信任检查被跳过,请手动打开它们并确认是否信任。</x:String>
<x:String x:Key="Text.UpdateSubmodules" xml:space="preserve">更新子模块</x:String>
<x:String x:Key="Text.UpdateSubmodules.All" xml:space="preserve">更新所有子模块</x:String>
<x:String x:Key="Text.UpdateSubmodules.Init" xml:space="preserve">如未初始化子模块,先初始化</x:String>
Expand Down
6 changes: 6 additions & 0 deletions src/Resources/Locales/zh_TW.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -991,6 +991,12 @@
<x:String x:Key="Text.TagCM.Merge" xml:space="preserve">合併 ${0}$ 到 ${1}$...</x:String>
<x:String x:Key="Text.TagCM.Push" xml:space="preserve">推送 ${0}$...</x:String>
<x:String x:Key="Text.Terminate" xml:space="preserve">終止執行</x:String>
<x:String x:Key="Text.TrustRepository" xml:space="preserve">不受信任的存放庫</x:String>
<x:String x:Key="Text.TrustRepository.CommandTip" xml:space="preserve">將執行:</x:String>
<x:String x:Key="Text.TrustRepository.Description" xml:space="preserve">Git 拒絕存取該存放庫: </x:String>
<x:String x:Key="Text.TrustRepository.Path" xml:space="preserve">路徑 :</x:String>
<x:String x:Key="Text.TrustRepository.Permanent" xml:space="preserve">信任此存放庫</x:String>
<x:String x:Key="Text.TrustRepository.ScanSkipped" xml:space="preserve">有 {0} 個存放庫因 git 的信任檢查而被略過,請手動開啟並確認是否信任。</x:String>
<x:String x:Key="Text.UpdateSubmodules" xml:space="preserve">更新子模組</x:String>
<x:String x:Key="Text.UpdateSubmodules.All" xml:space="preserve">更新所有子模組</x:String>
<x:String x:Key="Text.UpdateSubmodules.Init" xml:space="preserve">如果子模組尚未初始化,則將其初始化</x:String>
Expand Down
28 changes: 27 additions & 1 deletion src/ViewModels/Launcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,12 @@ public bool TryOpenRepositoryFromPath(string repo)
if (ActivePage is not { Data: Welcome { }, Popup: null })
AddNewTab();

ActivePage.Popup = new Init(ActivePage.Node.Id, repo, null, 0, test.StdErr ?? "Unknown error occurred while opening the repository.");
if (Models.SafeDirectories.IsUntrustedRepository(test.StdErr) &&
Models.SafeDirectories.TryGetSafeDirectoryValue(repo, test.StdErr, out var safeDirectory))
ActivePage.Popup = new TrustRepository(ActivePage.Node.Id, repo, test.StdErr, safeDirectory, null, false, true, 0);
else
ActivePage.Popup = new Init(ActivePage.Node.Id, repo, null, 0, test.StdErr ?? "Unknown error occurred while opening the repository.");

return true;
}
}
Expand Down Expand Up @@ -325,6 +330,9 @@ public void OpenRepositoryInTab(RepositoryNode node, LauncherPage page)
var gitDir = isBare ? node.Id : GetRepositoryGitDir(node.Id);
if (string.IsNullOrEmpty(gitDir))
{
if (TryShowTrustRepositoryPopup(node.Id, ActivePage))
return;

ActivePage.Notifications.Add(new Models.Notification
{
Group = node.Id,
Expand Down Expand Up @@ -404,6 +412,9 @@ public void OpenSubRepository(LauncherPage ownerPage, string fullpath)
var gitDir = GetRepositoryGitDir(normalizedPath);
if (string.IsNullOrEmpty(gitDir))
{
if (TryShowTrustRepositoryPopup(normalizedPath, ownerPage))
return;

ownerPage.Notifications.Add(new Models.Notification
{
Group = ownerPage.Node.Id,
Expand Down Expand Up @@ -439,6 +450,21 @@ public void OpenSubRepository(LauncherPage ownerPage, string fullpath)
ActivePage = page;
}

private bool TryShowTrustRepositoryPopup(string path, LauncherPage page)
{
if (page == null || !page.CanCreatePopup())
return false;

var test = new Commands.QueryRepositoryRootPath(path).GetResult();
if (test.IsSuccess ||
!Models.SafeDirectories.IsUntrustedRepository(test.StdErr) ||
!Models.SafeDirectories.TryGetSafeDirectoryValue(path, test.StdErr, out var safeDirectory))
return false;

page.Popup = new TrustRepository(page.Node.Id, path, test.StdErr, safeDirectory, null, false, true, 0);
return true;
}

private void DispatchNotification(Models.Notification notification)
{
if (!Dispatcher.UIThread.CheckAccess())
Expand Down
2 changes: 1 addition & 1 deletion src/ViewModels/LauncherPage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ public bool CanCreatePopup()

public async Task ProcessPopupAsync()
{
if (_popup is { InProgress: false } dump)
if (_popup is { InProgress: false } dump && dump.CanSure)
{
if (!dump.Check())
return;
Expand Down
23 changes: 19 additions & 4 deletions src/ViewModels/OpenLocalRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,12 +89,27 @@ public override async Task<bool> Sure()
else
{
var launcher = App.GetLauncher();
foreach (var page in launcher.Pages)
if (Models.SafeDirectories.IsUntrustedRepository(test.StdErr) &&
Models.SafeDirectories.TryGetSafeDirectoryValue(_repoPath, test.StdErr, out var safeDirectory))
{
if (page.Node.Id.Equals(_pageId, StringComparison.Ordinal))
foreach (var page in launcher.Pages)
{
page.Popup = new Init(page.Node.Id, _repoPath, parent, _bookmark, test.StdErr);
break;
if (page.Node.Id.Equals(_pageId, StringComparison.Ordinal))
{
page.Popup = new TrustRepository(page.Node.Id, _repoPath, test.StdErr, safeDirectory, parent, true, true, _bookmark);
break;
}
}
}
else
{
foreach (var page in launcher.Pages)
{
if (page.Node.Id.Equals(_pageId, StringComparison.Ordinal))
{
page.Popup = new Init(page.Node.Id, _repoPath, parent, _bookmark, test.StdErr);
break;
}
}
}

Expand Down
6 changes: 6 additions & 0 deletions src/ViewModels/Popup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,12 @@ public virtual bool CanStartDirectly()
return true;
}

/// <summary>
/// Whether the `Sure` action is currently allowed. Views should bind the confirm button's
/// `IsEnabled` to this property.
/// </summary>
public virtual bool CanSure => true;

public virtual Task<bool> Sure()
{
return null;
Expand Down
9 changes: 9 additions & 0 deletions src/ViewModels/ScanRepositories.cs
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,10 @@ public override async Task<bool> Sure()
Preferences.Instance.AutoRemoveInvalidNode();
Preferences.Instance.Save();
Welcome.Instance.Refresh();

if (_untrusted > 0)
Models.Notification.Send(null, App.Text("TrustRepository.ScanSkipped", _untrusted));

return true;
}

Expand Down Expand Up @@ -154,6 +158,10 @@ private async Task GetUnmanagedRepositoriesAsync(DirectoryInfo dir, List<string>
if (!IsManaged(normalized))
outs.Add(normalized);
}
else if (Models.SafeDirectories.IsUntrustedRepository(test.StdErr))
{
_untrusted++;
}

continue;
}
Expand Down Expand Up @@ -182,5 +190,6 @@ private bool IsManaged(string path)
private bool _useCustomDir = false;
private string _customDir = string.Empty;
private Models.ScanDir _selected = null;
private int _untrusted = 0;
}
}
Loading