Skip to content
Open
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
27 changes: 27 additions & 0 deletions src/Commands/QueryDefaultBranch.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using System.Threading.Tasks;

namespace SourceGit.Commands
{
public class QueryDefaultBranch : Command
{
public QueryDefaultBranch(string repo, string remote)
{
WorkingDirectory = repo;
Context = repo;
Args = $"symbolic-ref --short refs/remotes/{remote}/HEAD";
RaiseError = false;
}

public string GetResult()
{
var rs = ReadToEnd();
return rs.IsSuccess ? rs.StdOut.Trim() : string.Empty;
}

public async Task<string> GetResultAsync()
{
var rs = await ReadToEndAsync().ConfigureAwait(false);
return rs.IsSuccess ? rs.StdOut.Trim() : string.Empty;
}
}
}
1 change: 1 addition & 0 deletions src/Resources/Locales/en_US.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
<x:String x:Key="Text.BranchCM.Checkout" xml:space="preserve">Checkout ${0}$...</x:String>
<x:String x:Key="Text.BranchCM.CompareTwo" xml:space="preserve">Compare selected 2 branches</x:String>
<x:String x:Key="Text.BranchCM.CompareWith" xml:space="preserve">Compare with...</x:String>
<x:String x:Key="Text.BranchCM.CompareWithDefault" xml:space="preserve">Compare with ${0}$</x:String>
<x:String x:Key="Text.BranchCM.CompareWithHead" xml:space="preserve">Compare with HEAD</x:String>
<x:String x:Key="Text.BranchCM.CopyName" xml:space="preserve">Copy Branch Name</x:String>
<x:String x:Key="Text.BranchCM.CreatePR" xml:space="preserve">Create PR...</x:String>
Expand Down
67 changes: 66 additions & 1 deletion src/Views/BranchTree.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -733,14 +733,28 @@ private ContextMenu CreateContextMenuForLocalBranch(ViewModels.Repository repo,

menu.Items.Add(push);

menu.Items.Add(new MenuItem() { Header = "-" });

var defaultBase = ResolveDefaultBaseBranch(repo);
if (defaultBase != null && !defaultBase.FullName.Equals(branch.FullName, StringComparison.Ordinal))
{
var compareWithDefault = new MenuItem();
compareWithDefault.Header = App.Text("BranchCM.CompareWithDefault", defaultBase.FriendlyName);
compareWithDefault.Icon = this.CreateMenuIcon("Icons.Compare");
compareWithDefault.Click += (_, _) =>
{
this.ShowWindow(new ViewModels.Compare(repo, defaultBase, branch));
};
menu.Items.Add(compareWithDefault);
}

var compareWith = new MenuItem();
compareWith.Header = App.Text("BranchCM.CompareWith");
compareWith.Icon = this.CreateMenuIcon("Icons.Compare");
compareWith.Click += (_, _) =>
{
new ViewModels.CompareCommandPalette(repo, branch).Open();
};
menu.Items.Add(new MenuItem() { Header = "-" });
menu.Items.Add(compareWith);
}
else
Expand Down Expand Up @@ -1352,6 +1366,57 @@ private void TryToAddCustomActionsToRemoteContextMenu(ViewModels.Repository repo
menu.Items.Add(new MenuItem() { Header = "-" });
}

// Resolves the repository's "default" branch (the equivalent of GitHub's default branch).
// Remote priority: Settings.DefaultRemote -> current branch's upstream remote -> first remote.
// Prefers the remote-tracking ref (e.g. origin/master) over a same-named local branch so
// the comparison is against the actual remote default. Returns null if the remote HEAD is
// unset or no remote exists.
private Models.Branch ResolveDefaultBaseBranch(ViewModels.Repository repo)
{
if (repo.Remotes.Count == 0)
return null;

// Prefer an explicitly-configured default remote ("upstream" in fork workflows),
// then fall back to the current branch's upstream remote, then the first remote.
string remoteName = null;
if (!string.IsNullOrEmpty(repo.Settings?.DefaultRemote))
remoteName = repo.Settings.DefaultRemote;

if (string.IsNullOrEmpty(remoteName))
{
var current = repo.CurrentBranch;
const int prefixLen = 13; // "refs/remotes/"
if (current != null && !string.IsNullOrEmpty(current.Upstream) && current.Upstream.Length > prefixLen)
{
var sepIdx = current.Upstream.IndexOf('/', prefixLen);
if (sepIdx > prefixLen)
remoteName = current.Upstream.Substring(prefixLen, sepIdx - prefixLen);
}
}

if (string.IsNullOrEmpty(remoteName))
remoteName = repo.Remotes[0].Name;

var fullRef = new Commands.QueryDefaultBranch(repo.FullPath, remoteName).GetResult();
if (string.IsNullOrEmpty(fullRef))
return null;

// QueryDefaultBranch returns e.g. "origin/master". Prefer the remote-tracking ref
// (authoritative for "what is the default base on the remote?"), fall back to a
// local branch with the same short name only if the remote-tracking ref is absent.
var remoteTrackingRef = $"refs/remotes/{fullRef}";
var remoteMatch = repo.Branches.Find(b => !b.IsLocal && b.FullName.Equals(remoteTrackingRef, System.StringComparison.Ordinal));
if (remoteMatch != null)
return remoteMatch;

var slash = fullRef.IndexOf('/');
if (slash <= 0 || slash >= fullRef.Length - 1)
return null;

var shortName = fullRef.Substring(slash + 1);
return repo.Branches.Find(b => b.IsLocal && b.Name.Equals(shortName, System.StringComparison.Ordinal));
}

private bool _disableSelectionChangingEvent = false;
}
}