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
2 changes: 2 additions & 0 deletions GVFS/GVFS.Common/GVFSConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,8 @@ public static class DotGit
public const string HeadName = "HEAD";
public const string GitDirPrefix = "gitdir: ";
public const string CommonDirName = "commondir";
public const string WorktreeConfigName = "config.worktree";
public const string WorktreesName = "worktrees";
public const string SkipCleanCheckName = "skip-clean-check";
public const string IndexName = "index";
public const string PackedRefsName = "packed-refs";
Expand Down
67 changes: 63 additions & 4 deletions GVFS/GVFS.Common/GVFSEnlistment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
using GVFS.Common.Tracing;
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Threading;

Expand Down Expand Up @@ -90,6 +92,16 @@ private GVFSEnlistment(string enlistmentRoot, string gitBinPath, GitAuthenticati

public bool IsWorktree => this.Worktree != null;

public string PrimaryDotGVFSRoot => Path.Combine(this.PrimaryEnlistmentRoot, GVFSPlatform.Instance.Constants.DotGVFSRoot);

/// <summary>
/// Path to the config.worktree file of this enlistment. Git reads it only when
/// extensions.worktreeConfig is enabled, and then it overrides the shared config.
/// </summary>
public string WorktreeConfigPath => Path.Combine(
this.IsWorktree ? this.Worktree.WorktreeGitDir : this.DotGitRoot,
GVFSConstants.DotGit.WorktreeConfigName);

/// <summary>
/// Path to the git index file. For worktrees this is in the
/// per-worktree git dir, not in the working directory.
Expand Down Expand Up @@ -446,14 +458,61 @@ public bool TryCreateEnlistmentSubFolders()
return true;
}

public string GetMountId()
/// <summary>
/// Returns the enlistment id. The repo metadata of the primary is the source of
/// truth, and the shared git config keeps a copy of it. A linked worktree derives
/// its id from the primary id and the worktree name, because it must not write to
/// the shared config (see <see cref="RequiredGitConfig.TrySetRequiredSettings(GVFSEnlistment, out string)"/>).
/// </summary>
public string GetEnlistmentId()
{
return this.GetId(GVFSConstants.GitConfig.MountId);
if (this.IsWorktree)
{
this.TryGetPrimaryEnlistmentId(out string primaryEnlistmentId, out _);
return this.GetWorktreeEnlistmentId(primaryEnlistmentId);
}

if (RepoMetadata.TryReadEnlistmentId(this.DotGVFSRoot, out string enlistmentId, out _) &&
!string.IsNullOrWhiteSpace(enlistmentId))
{
return enlistmentId;
}

return this.GetId(GVFSConstants.GitConfig.EnlistmentId);
}

public string GetEnlistmentId()
/// <summary>
/// Reads the enlistment id from the repo metadata of the primary enlistment.
/// </summary>
public bool TryGetPrimaryEnlistmentId(out string primaryEnlistmentId, out string error)
{
return this.GetId(GVFSConstants.GitConfig.EnlistmentId);
return RepoMetadata.TryReadEnlistmentId(this.PrimaryDotGVFSRoot, out primaryEnlistmentId, out error);
}

/// <summary>
/// Returns the id that is derived from <paramref name="primaryEnlistmentId"/>, or
/// the id stored in the worktree's repo metadata when the primary id is empty.
/// </summary>
public string GetWorktreeEnlistmentId(string primaryEnlistmentId)
{
if (!string.IsNullOrWhiteSpace(primaryEnlistmentId))
{
return CreateWorktreeEnlistmentId(primaryEnlistmentId, this.Worktree.Name);
}

RepoMetadata.TryReadEnlistmentId(this.DotGVFSRoot, out string storedEnlistmentId, out _);
return storedEnlistmentId;
}

/// <summary>
/// Derives a stable enlistment id for a linked worktree, in the same format as
/// other enlistment ids. Git keeps a worktree name when the worktree moves, and
/// reuses it when a worktree with the same name is added again.
/// </summary>
public static string CreateWorktreeEnlistmentId(string primaryEnlistmentId, string worktreeName)
{
byte[] hash = SHA256.HashData(Encoding.UTF8.GetBytes(primaryEnlistmentId + ":" + worktreeName.ToLowerInvariant()));
return Convert.ToHexString(hash, 0, 16).ToLowerInvariant();
}

private void SetOnce<T>(T value, ref T valueToSet)
Expand Down
67 changes: 66 additions & 1 deletion GVFS/GVFS.Common/Git/GitProcess.cs
Original file line number Diff line number Diff line change
Expand Up @@ -523,13 +523,18 @@ public Result AddInLocalConfig(string settingName, string value)
public Result SetInFileConfig(string configFile, string settingName, string value, bool replaceAll = false)
{
return this.InvokeGitOutsideEnlistment(string.Format(
"config --file {0} {1} \"{2}\" \"{3}\"",
"config --file \"{0}\" {1}\"{2}\" \"{3}\"",
configFile,
replaceAll ? "--replace-all " : string.Empty,
settingName,
value));
}

public Result DeleteFromFileConfig(string configFile, string settingName)
{
return this.InvokeGitOutsideEnlistment($"config --file \"{configFile}\" --unset-all {settingName}");
}

public bool TryGetConfigUrlMatch(string section, string repositoryUrl, out Dictionary<string, GitConfigSetting> configSettings)
{
// See GetFromConfig for why pre-command hook is disabled.
Expand Down Expand Up @@ -566,6 +571,29 @@ public bool TryGetAllConfig(bool localOnly, out Dictionary<string, GitConfigSett
return false;
}

/// <summary>
/// Lists the local config with every value of every key, in file order. A key
/// without a value, which git reads as boolean true, has a null value.
/// </summary>
public bool TryGetLocalConfigEntries(out Dictionary<string, List<string>> configEntries)
{
// See GetFromConfig for why pre-command hook is disabled.
return TryParseConfigEntries(
this.InvokeConfigRead("--list --local", () => this.InvokeGitAgainstDotGitFolder("config --list --local -z", usePreCommandHook: false)),
out configEntries);
}

/// <summary>
/// Lists a config file like <see cref="TryGetLocalConfigEntries"/>. Git fails when
/// the file does not exist, so check for the file before you call this.
/// </summary>
public bool TryGetFileConfigEntries(string configFile, out Dictionary<string, List<string>> configEntries)
{
return TryParseConfigEntries(
this.InvokeConfigRead(configFile, () => this.InvokeGitOutsideEnlistment($"config --file \"{configFile}\" --list -z")),
out configEntries);
}

/// <summary>
/// Get the config value give a setting name
/// </summary>
Expand Down Expand Up @@ -1449,6 +1477,43 @@ private static string ParseValue(string contents, string prefix)
return value;
}

/// <summary>
/// Parses `git config --list -z` output. Each entry ends with NUL and is either
/// "key", for a key without a value, or "key\nvalue". GitProcess reads output by
/// line, so the separator arrives as "\r\n" and a final line break follows.
/// </summary>
private static bool TryParseConfigEntries(Result result, out Dictionary<string, List<string>> configEntries)
{
configEntries = null;
if (result.ExitCodeIsFailure)
{
return false;
}

configEntries = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
foreach (string entry in (result.Output ?? string.Empty).Split('\0'))
{
int separator = entry.IndexOf('\n');
string key = (separator < 0 ? entry : entry.Substring(0, separator)).TrimEnd('\r');
if (key.Length == 0)
{
continue;
}

string value = separator < 0 ? null : entry.Substring(separator + 1);

if (!configEntries.TryGetValue(key, out List<string> values))
{
values = new List<string>();
configEntries.Add(key, values);
}

values.Add(value);
}

return true;
}

/// <summary>
/// Invokes git.exe without a working directory set.
/// </summary>
Expand Down
Loading
Loading