From 3c267a1861cd45946670dc50ad74a73a9c12c705 Mon Sep 17 00:00:00 2001 From: Matthew John Cheetham Date: Thu, 15 Oct 2020 11:27:46 +0100 Subject: [PATCH] clone: respect remote default branch name Attempt to determine the remote's default branch name when performing a `scalar clone` using the vanilla Git protocols (not using the GVFS helper). Use ls-remote to lookup the HEAD ref on the remote, and parse the results. If the remote's HEAD is not a branch, or we fail to parse the output for any reason, consult Git itself for the default branch name as a fallback. --- Scalar.Common/Git/GitProcess.cs | 57 +++++++++++++++++++++++++++++++++ Scalar/CommandLine/CloneVerb.cs | 23 ++++++++++--- 2 files changed, 75 insertions(+), 5 deletions(-) diff --git a/Scalar.Common/Git/GitProcess.cs b/Scalar.Common/Git/GitProcess.cs index 0c3a8b020b..3a6191bbda 100644 --- a/Scalar.Common/Git/GitProcess.cs +++ b/Scalar.Common/Git/GitProcess.cs @@ -604,6 +604,63 @@ public Result RemoteAdd(string remoteName, string url) return this.InvokeGitAgainstDotGitFolder("remote add " + remoteName + " " + url); } + public bool TryGetRemoteDefaultBranch(string remoteName, out string defaultBranch, out string error) + { + defaultBranch = null; + error = null; + + Result lsRemoteResult = this.InvokeGitAgainstDotGitFolder("ls-remote --symref origin HEAD"); + if (lsRemoteResult.ExitCodeIsFailure) + { + error = "Failed to call ls-remote to determine remote HEAD"; + return false; + } + + string output = lsRemoteResult.Output; + + const string refPrefix = "ref: "; + const string headsPrefix = "ref: refs/heads/"; + const string suffix = "\tHEAD"; + + var cmp = StringComparison.Ordinal; + foreach (string line in output.Split('\n')) + { + int suffixStart = line.IndexOf(suffix, cmp); + if (line.StartsWith(refPrefix, cmp) && suffixStart > -1) + { + // HEAD is a branch + if (line.StartsWith(headsPrefix, cmp)) + { + defaultBranch = line.Substring(headsPrefix.Length, suffixStart - headsPrefix.Length); + return true; + } + + error = "HEAD is not a branch"; + return false; + } + } + + defaultBranch = null; + error = $"HEAD not found in ls-remote output: {output}"; + return false; + } + + public bool TryGetSymbolicRef(string name, bool shortName, out string branch, out string error) + { + branch = null; + error = null; + + Result result = this.InvokeGitAgainstDotGitFolder($"symbolic-ref {(shortName ? "--short" : string.Empty)} HEAD"); + if (result.ExitCodeIsSuccess) + { + branch = result.Output.Trim(); + return true; + } + + error = $"Failed to read symbolic ref '{name}': {result.Errors}"; + return false; + } + public Result PrunePacked(string gitObjectDirectory) { return this.InvokeGitAgainstDotGitFolder( diff --git a/Scalar/CommandLine/CloneVerb.cs b/Scalar/CommandLine/CloneVerb.cs index d8e8ad09e3..c929b0dc8d 100644 --- a/Scalar/CommandLine/CloneVerb.cs +++ b/Scalar/CommandLine/CloneVerb.cs @@ -339,10 +339,6 @@ private Result GitClone() git.SetInLocalConfig("remote.origin.promisor", "true"); git.SetInLocalConfig("remote.origin.partialCloneFilter", "blob:none"); - string branch = this.Branch ?? "master"; - git.SetInLocalConfig($"branch.{branch}.remote", "origin"); - git.SetInLocalConfig($"branch.{branch}.merge", $"refs/heads/{branch}"); - if (!this.FullClone) { GitProcess.SparseCheckoutInit(this.enlistment); @@ -393,8 +389,25 @@ private Result GitClone() return new Result($"Failed to complete regular clone: {fetchResult?.Errors}"); } - GitProcess.Result checkoutResult = null; + // Configure the specified branch, or the default branch on the remote if not specified + string branch = this.Branch; + if (branch is null && !git.TryGetRemoteDefaultBranch("origin", out branch, out string defaultBranchError)) + { + // Failed to get the remote's default branch name - ask Git for the prefered local default branch + // instead, and show a warning message. + this.Output.WriteLine($"warning: failed to get default branch name from remote; using local default: {defaultBranchError}"); + if (!git.TryGetSymbolicRef("HEAD", shortName: true, out branch, out string localDefaultError)) + { + return new Result($"Failed to determine local default branch name: {localDefaultError}"); + } + } + + git.SetInLocalConfig($"branch.{branch}.remote", "origin"); + git.SetInLocalConfig($"branch.{branch}.merge", $"refs/heads/{branch}"); + + // Checkout the branch + GitProcess.Result checkoutResult = null; if (!this.ShowStatusWhileRunning(() => { using (ITracer activity = this.tracer.StartActivity("git-checkout", EventLevel.LogAlways))