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
8 changes: 8 additions & 0 deletions src/Resources/Locales/en_US.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,9 @@
<x:String x:Key="Text.Blame.Tips" xml:space="preserve">Holding Ctrl (on Windows/Linux) or ⌘ (on macOS) when clicking commit SHA will rebuild blame data based on that revision.</x:String>
<x:String x:Key="Text.Blame.TypeNotSupported" xml:space="preserve">BLAME ON THIS FILE IS NOT SUPPORTED!!!</x:String>
<x:String x:Key="Text.BranchCM.Checkout" xml:space="preserve">Checkout ${0}$...</x:String>
<x:String x:Key="Text.BranchCM.CheckoutAndMerge" xml:space="preserve">Checkout ${1}$ and merge ${0}$ into it...</x:String>
<x:String x:Key="Text.BranchCM.CheckoutAsWorktree" xml:space="preserve">Checkout ${0}$ as worktree...</x:String>
<x:String x:Key="Text.BranchCM.CompareBranches" xml:space="preserve">Compare ${0}$ with ${1}$</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.CompareWithHead" xml:space="preserve">Compare with HEAD</x:String>
Expand All @@ -71,6 +73,7 @@
<x:String x:Key="Text.BranchCM.DeleteMultiBranches" xml:space="preserve">Delete selected {0} branches...</x:String>
<x:String x:Key="Text.BranchCM.EditDescription" xml:space="preserve">Edit description for ${0}$...</x:String>
<x:String x:Key="Text.BranchCM.FastForward" xml:space="preserve">Fast-Forward to ${0}$</x:String>
<x:String x:Key="Text.BranchCM.FastForwardBranch" xml:space="preserve">Fast-Forward ${0}$ to ${1}$</x:String>
<x:String x:Key="Text.BranchCM.FetchInto" xml:space="preserve">Fetch ${0}$ into ${1}$</x:String>
<x:String x:Key="Text.BranchCM.Finish" xml:space="preserve">Git Flow - Finish ${0}$...</x:String>
<x:String x:Key="Text.BranchCM.InteractiveRebase.Manually" xml:space="preserve">Interactively Rebase ${0}$ on ${1}$</x:String>
Expand Down Expand Up @@ -629,6 +632,11 @@
<x:String x:Key="Text.MergeMultiple.CommitChanges" xml:space="preserve">Commit all changes</x:String>
<x:String x:Key="Text.MergeMultiple.Strategy" xml:space="preserve">Strategy:</x:String>
<x:String x:Key="Text.MergeMultiple.Targets" xml:space="preserve">Targets:</x:String>
<x:String x:Key="Text.MoveBranches" xml:space="preserve">Move Branches</x:String>
<x:String x:Key="Text.MoveBranches.Branches" xml:space="preserve">Branches:</x:String>
<x:String x:Key="Text.MoveBranches.Count" xml:space="preserve">{0} items</x:String>
<x:String x:Key="Text.MoveBranches.Root" xml:space="preserve">(top level)</x:String>
<x:String x:Key="Text.MoveBranches.Target" xml:space="preserve">Move Into:</x:String>
<x:String x:Key="Text.MoveSubmodule" xml:space="preserve">Move Submodule</x:String>
<x:String x:Key="Text.MoveSubmodule.MoveTo" xml:space="preserve">Move To:</x:String>
<x:String x:Key="Text.MoveSubmodule.Submodule" xml:space="preserve">Submodule:</x:String>
Expand Down
7 changes: 7 additions & 0 deletions src/ViewModels/BranchTreeNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ public CornerRadius CornerRadius
set => SetProperty(ref _cornerRadius, value);
}

public bool IsDropTarget
{
get => _isDropTarget;
set => SetProperty(ref _isDropTarget, value);
}

public bool IsBranch
{
get => Backend is Models.Branch;
Expand All @@ -56,6 +62,7 @@ public string BranchesCount

private Models.FilterMode _filterMode = Models.FilterMode.None;
private bool _isExpanded = false;
private bool _isDropTarget = false;
private CornerRadius _cornerRadius = new CornerRadius(4);

public class Builder
Expand Down
173 changes: 173 additions & 0 deletions src/ViewModels/MoveBranches.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace SourceGit.ViewModels
{
public class MoveBranches : Popup
{
public const string LocalPrefix = "refs/heads/";

public string Target
{
get;
}

public List<string> Preview
{
get;
} = [];

public string ErrorMessage
{
get;
} = string.Empty;

public bool HasError
{
get => !string.IsNullOrEmpty(ErrorMessage);
}

public MoveBranches(Repository repo, IReadOnlyList<string> paths, string targetPrefix)
{
_repo = repo;
Target = string.IsNullOrEmpty(targetPrefix) ? App.Text("MoveBranches.Root") : $"{targetPrefix}/";

foreach (var path in paths)
{
if (!path.StartsWith(LocalPrefix, StringComparison.Ordinal))
continue;

var parent = GetParent(path.Substring(LocalPrefix.Length));
if (parent.Equals(targetPrefix, StringComparison.Ordinal))
continue;

var folderPrefix = $"{path}/";
foreach (var branch in repo.Branches)
{
if (!branch.IsLocal)
continue;

if (branch.FullName.Equals(path, StringComparison.Ordinal) ||
branch.FullName.StartsWith(folderPrefix, StringComparison.Ordinal))
{
var leaf = parent.Length == 0 ? branch.Name : branch.Name.Substring(parent.Length + 1);
var newName = targetPrefix.Length == 0 ? leaf : $"{targetPrefix}/{leaf}";
_moves.Add(new KeyValuePair<Models.Branch, string>(branch, newName));
Preview.Add($"{branch.Name} → {newName}");
}
}
}

ErrorMessage = Validate();
}

// Whether dropping the given tree node paths on the folder `targetPrefix` ("" for the top level) would
// move anything. Folders cannot be dropped into themselves, and items already there are no-ops.
public static bool CanDropOnto(IReadOnlyList<string> paths, string targetPrefix)
{
if (paths.Count == 0)
return false;

var anyMoves = false;
foreach (var path in paths)
{
if (!path.StartsWith(LocalPrefix, StringComparison.Ordinal))
return false;

var relative = path.Substring(LocalPrefix.Length);
if (targetPrefix.Equals(relative, StringComparison.Ordinal) ||
targetPrefix.StartsWith($"{relative}/", StringComparison.Ordinal))
return false;

if (!GetParent(relative).Equals(targetPrefix, StringComparison.Ordinal))
anyMoves = true;
}

return anyMoves;
}

public override async Task<bool> Sure()
{
if (HasError)
{
_repo.SendNotification(ErrorMessage, true);
return false;
}

using var lockWatcher = _repo.LockWatcher();

var log = _repo.CreateLog("Move Branches");
Use(log);

var succ = true;
foreach (var (branch, newName) in _moves)
{
ProgressDescription = $"Rename '{branch.Name}'";

succ = await new Commands.Branch(_repo.FullPath, branch.Name)
.Use(log)
.RenameAsync(newName);

if (!succ)
break;

_repo.RefreshAfterRenameBranch(branch, newName);
}

log.Complete();
return succ;
}

private string Validate()
{
if (_moves.Count == 0)
return "Nothing to move!";

var moving = new HashSet<Models.Branch>();
foreach (var (branch, _) in _moves)
moving.Add(branch);

for (var i = 0; i < _moves.Count; i++)
{
var name = _moves[i].Value;
if (!Models.RefName.IsValidBranchName(name))
return $"Bad branch name format: {name}";

foreach (var other in _repo.Branches)
{
if (!other.IsLocal || moving.Contains(other))
continue;

if (Conflicts(name, other.Name))
return $"A branch named '{other.Name}' already exists!!!";
}

for (var j = i + 1; j < _moves.Count; j++)
{
if (Conflicts(name, _moves[j].Value))
return $"Branches would end up with the same name: {name}";
}
}

return string.Empty;
}

// Two branch names cannot coexist when they are equal or one is a "directory" of the other.
private static bool Conflicts(string a, string b)
{
return a.Equals(b, StringComparison.Ordinal) ||
a.StartsWith($"{b}/", StringComparison.Ordinal) ||
b.StartsWith($"{a}/", StringComparison.Ordinal);
}

private static string GetParent(string relativePath)
{
var idx = relativePath.LastIndexOf('/');
return idx < 0 ? string.Empty : relativePath.Substring(0, idx);
}

private readonly Repository _repo;
private readonly List<KeyValuePair<Models.Branch, string>> _moves = [];
}
}
18 changes: 17 additions & 1 deletion src/Views/BranchTree.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,26 @@

<ListBox.ItemTemplate>
<DataTemplate DataType="vm:BranchTreeNode">
<Border Background="Transparent"
<Border Classes="branch_node"
Classes.drop_target="{Binding IsDropTarget}"
PointerPressed="OnNodePointerPressed"
PointerMoved="OnNodePointerMoved"
PointerReleased="OnNodePointerReleased"
DragDrop.AllowDrop="True"
DragDrop.DragOver="OnNodeDragOver"
DragDrop.DragLeave="OnNodeDragLeave"
DragDrop.Drop="OnNodeDrop"
ToolTip.Tip="{Binding Backend}"
ToolTip.Placement="Right">
<Border.Styles>
<Style Selector="Border.branch_node">
<Setter Property="Background" Value="Transparent"/>
</Style>
<Style Selector="Border.branch_node.drop_target">
<Setter Property="Background" Value="{DynamicResource Brush.AccentHovered}"/>
</Style>
</Border.Styles>

<Border.DataTemplates>
<DataTemplate DataType="m:Branch">
<StackPanel Orientation="Vertical">
Expand Down
Loading