using System; using System.Collections.Generic; using System.IO; using System.Text; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Avalonia.Collections; using Avalonia.Controls; using Avalonia.Media; using Avalonia.Media.Imaging; using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; namespace SourceGit.ViewModels { public class Repository : ObservableObject, Models.IRepository { public bool IsBare { get; } public string FullPath { get => _fullpath; set { if (value != null) { var normalized = value.Replace('\\', '/'); SetProperty(ref _fullpath, normalized); } else { SetProperty(ref _fullpath, null); } } } public string GitDir { get => _gitDir; set => SetProperty(ref _gitDir, value); } public Models.RepositorySettings Settings { get => _settings; } public Models.FilterMode HistoriesFilterMode { get => _historiesFilterMode; private set => SetProperty(ref _historiesFilterMode, value); } public bool HasAllowedSignersFile { get => _hasAllowedSignersFile; } public int SelectedViewIndex { get => _selectedViewIndex; set { if (SetProperty(ref _selectedViewIndex, value)) { switch (value) { case 1: SelectedView = _workingCopy; break; case 2: SelectedView = _stashesPage; break; default: SelectedView = _histories; break; } } } } public object SelectedView { get => _selectedView; set => SetProperty(ref _selectedView, value); } public bool EnableReflog { get => _settings.EnableReflog; set { if (value != _settings.EnableReflog) { _settings.EnableReflog = value; OnPropertyChanged(); Task.Run(RefreshCommits); } } } public bool EnableFirstParentInHistories { get => _settings.EnableFirstParentInHistories; set { if (value != _settings.EnableFirstParentInHistories) { _settings.EnableFirstParentInHistories = value; OnPropertyChanged(); Task.Run(RefreshCommits); } } } public bool OnlyHighlightCurrentBranchInHistories { get => _settings.OnlyHighlighCurrentBranchInHistories; set { if (value != _settings.OnlyHighlighCurrentBranchInHistories) { _settings.OnlyHighlighCurrentBranchInHistories = value; OnPropertyChanged(); } } } public string Filter { get => _filter; set { if (SetProperty(ref _filter, value)) { var builder = BuildBranchTree(_branches, _remotes); LocalBranchTrees = builder.Locals; RemoteBranchTrees = builder.Remotes; VisibleTags = BuildVisibleTags(); VisibleSubmodules = BuildVisibleSubmodules(); } } } public List Remotes { get => _remotes; private set => SetProperty(ref _remotes, value); } public List Branches { get => _branches; private set => SetProperty(ref _branches, value); } public Models.Branch CurrentBranch { get => _currentBranch; private set { var oldHead = _currentBranch?.Head; if (SetProperty(ref _currentBranch, value)) { if (oldHead != _currentBranch.Head && _workingCopy is { UseAmend: true }) _workingCopy.UseAmend = false; } } } public List LocalBranchTrees { get => _localBranchTrees; private set => SetProperty(ref _localBranchTrees, value); } public List RemoteBranchTrees { get => _remoteBranchTrees; private set => SetProperty(ref _remoteBranchTrees, value); } public List Worktrees { get => _worktrees; private set => SetProperty(ref _worktrees, value); } public List Tags { get => _tags; private set => SetProperty(ref _tags, value); } public List VisibleTags { get => _visibleTags; private set => SetProperty(ref _visibleTags, value); } public List Submodules { get => _submodules; private set => SetProperty(ref _submodules, value); } public List VisibleSubmodules { get => _visibleSubmodules; private set => SetProperty(ref _visibleSubmodules, value); } public int LocalChangesCount { get => _localChangesCount; private set => SetProperty(ref _localChangesCount, value); } public int StashesCount { get => _stashesCount; private set => SetProperty(ref _stashesCount, value); } public bool IncludeUntracked { get => _settings.IncludeUntrackedInLocalChanges; set { if (value != _settings.IncludeUntrackedInLocalChanges) { _settings.IncludeUntrackedInLocalChanges = value; OnPropertyChanged(); Task.Run(RefreshWorkingCopyChanges); } } } public bool IsSearching { get => _isSearching; set { if (SetProperty(ref _isSearching, value)) { if (value) { SelectedViewIndex = 0; CalcWorktreeFilesForSearching(); } else { SearchedCommits = new List(); SelectedSearchedCommit = null; SearchCommitFilter = string.Empty; MatchedFilesForSearching = null; _worktreeFiles = null; } } } } public bool IsSearchLoadingVisible { get => _isSearchLoadingVisible; private set => SetProperty(ref _isSearchLoadingVisible, value); } public bool OnlySearchCommitsInCurrentBranch { get => _onlySearchCommitsInCurrentBranch; set { if (SetProperty(ref _onlySearchCommitsInCurrentBranch, value) && !string.IsNullOrEmpty(_searchCommitFilter)) StartSearchCommits(); } } public int SearchCommitFilterType { get => _searchCommitFilterType; set { if (SetProperty(ref _searchCommitFilterType, value)) { CalcWorktreeFilesForSearching(); if (!string.IsNullOrEmpty(_searchCommitFilter)) StartSearchCommits(); } } } public string SearchCommitFilter { get => _searchCommitFilter; set { if (SetProperty(ref _searchCommitFilter, value) && IsSearchingCommitsByFilePath()) CalcMatchedFilesForSearching(); } } public List MatchedFilesForSearching { get => _matchedFilesForSearching; private set => SetProperty(ref _matchedFilesForSearching, value); } public List SearchedCommits { get => _searchedCommits; set => SetProperty(ref _searchedCommits, value); } public Models.Commit SelectedSearchedCommit { get => _selectedSearchedCommit; set { if (SetProperty(ref _selectedSearchedCommit, value) && value != null) NavigateToCommit(value.SHA); } } public bool IsLocalBranchGroupExpanded { get => _settings.IsLocalBranchesExpandedInSideBar; set { if (value != _settings.IsLocalBranchesExpandedInSideBar) { _settings.IsLocalBranchesExpandedInSideBar = value; OnPropertyChanged(); } } } public bool IsRemoteGroupExpanded { get => _settings.IsRemotesExpandedInSideBar; set { if (value != _settings.IsRemotesExpandedInSideBar) { _settings.IsRemotesExpandedInSideBar = value; OnPropertyChanged(); } } } public bool IsTagGroupExpanded { get => _settings.IsTagsExpandedInSideBar; set { if (value != _settings.IsTagsExpandedInSideBar) { _settings.IsTagsExpandedInSideBar = value; OnPropertyChanged(); } } } public bool IsSubmoduleGroupExpanded { get => _settings.IsSubmodulesExpandedInSideBar; set { if (value != _settings.IsSubmodulesExpandedInSideBar) { _settings.IsSubmodulesExpandedInSideBar = value; OnPropertyChanged(); } } } public bool IsWorktreeGroupExpanded { get => _settings.IsWorktreeExpandedInSideBar; set { if (value != _settings.IsWorktreeExpandedInSideBar) { _settings.IsWorktreeExpandedInSideBar = value; OnPropertyChanged(); } } } public InProgressContext InProgressContext { get => _workingCopy?.InProgressContext; } public Models.BisectState BisectState { get => _bisectState; private set => SetProperty(ref _bisectState, value); } public bool IsBisectCommandRunning { get => _isBisectCommandRunning; private set => SetProperty(ref _isBisectCommandRunning, value); } public bool IsAutoFetching { get => _isAutoFetching; private set => SetProperty(ref _isAutoFetching, value); } public int CommitDetailActivePageIndex { get; set; } = 0; public AvaloniaList Logs { get; private set; } = new AvaloniaList(); public Repository(bool isBare, string path, string gitDir) { IsBare = isBare; FullPath = path; GitDir = gitDir; } public void Open() { var settingsFile = Path.Combine(_gitDir, "sourcegit.settings"); if (File.Exists(settingsFile)) { try { _settings = JsonSerializer.Deserialize(File.ReadAllText(settingsFile), JsonCodeGen.Default.RepositorySettings); } catch { _settings = new Models.RepositorySettings(); } } else { _settings = new Models.RepositorySettings(); } try { // For worktrees, we need to watch the $GIT_COMMON_DIR instead of the $GIT_DIR. var gitDirForWatcher = _gitDir; if (_gitDir.Replace("\\", "/").IndexOf("/worktrees/", StringComparison.Ordinal) > 0) { var commonDir = new Commands.QueryGitCommonDir(_fullpath).Result(); if (!string.IsNullOrEmpty(commonDir)) gitDirForWatcher = commonDir; } _watcher = new Models.Watcher(this, _fullpath, gitDirForWatcher); } catch (Exception ex) { App.RaiseException(string.Empty, $"Failed to start watcher for repository: '{_fullpath}'. You may need to press 'F5' to refresh repository manually!\n\nReason: {ex.Message}"); } if (_settings.HistoriesFilters.Count > 0) _historiesFilterMode = _settings.HistoriesFilters[0].Mode; else _historiesFilterMode = Models.FilterMode.None; _histories = new Histories(this); _workingCopy = new WorkingCopy(this); _stashesPage = new StashesPage(this); _selectedView = _histories; _selectedViewIndex = 0; _workingCopy.CommitMessage = _settings.LastCommitMessage; _autoFetchTimer = new Timer(AutoFetchImpl, null, 5000, 5000); RefreshAll(); } public void Close() { SelectedView = null; // Do NOT modify. Used to remove exists widgets for GC.Collect Logs.Clear(); _settings.LastCommitMessage = _workingCopy.CommitMessage; var settingsSerialized = JsonSerializer.Serialize(_settings, JsonCodeGen.Default.RepositorySettings); try { File.WriteAllText(Path.Combine(_gitDir, "sourcegit.settings"), settingsSerialized); } catch { // Ignore } _autoFetchTimer.Dispose(); _autoFetchTimer = null; _settings = null; _historiesFilterMode = Models.FilterMode.None; _watcher?.Dispose(); _histories.Cleanup(); _workingCopy.Cleanup(); _stashesPage.Cleanup(); _watcher = null; _histories = null; _workingCopy = null; _stashesPage = null; _localChangesCount = 0; _stashesCount = 0; _remotes.Clear(); _branches.Clear(); _localBranchTrees.Clear(); _remoteBranchTrees.Clear(); _tags.Clear(); _visibleTags.Clear(); _submodules.Clear(); _visibleSubmodules.Clear(); _searchedCommits.Clear(); _selectedSearchedCommit = null; _worktreeFiles = null; _matchedFilesForSearching = null; } public bool CanCreatePopup() { var page = GetOwnerPage(); if (page == null) return false; return !_isAutoFetching && page.CanCreatePopup(); } public void ShowPopup(Popup popup) { var page = GetOwnerPage(); if (page != null) page.Popup = popup; } public void ShowAndStartPopup(Popup popup) { GetOwnerPage()?.StartPopup(popup); } public CommandLog CreateLog(string name) { var log = new CommandLog(name); Logs.Insert(0, log); return log; } public void RefreshAll() { Task.Run(() => { var allowedSignersFile = new Commands.Config(_fullpath).Get("gpg.ssh.allowedSignersFile"); _hasAllowedSignersFile = !string.IsNullOrEmpty(allowedSignersFile); }); Task.Run(RefreshBranches); Task.Run(RefreshTags); Task.Run(RefreshCommits); Task.Run(RefreshSubmodules); Task.Run(RefreshWorktrees); Task.Run(RefreshWorkingCopyChanges); Task.Run(RefreshStashes); } public void OpenInFileManager() { Native.OS.OpenInFileManager(_fullpath); } public void OpenInTerminal() { Native.OS.OpenTerminal(_fullpath); } public ContextMenu CreateContextMenuForExternalTools() { var tools = Native.OS.ExternalTools; if (tools.Count == 0) { App.RaiseException(_fullpath, "No available external editors found!"); return null; } var menu = new ContextMenu(); menu.Placement = PlacementMode.BottomEdgeAlignedLeft; RenderOptions.SetBitmapInterpolationMode(menu, BitmapInterpolationMode.HighQuality); foreach (var tool in tools) { var dupTool = tool; var item = new MenuItem(); item.Header = App.Text("Repository.OpenIn", dupTool.Name); item.Icon = new Image { Width = 16, Height = 16, Source = dupTool.IconImage }; item.Click += (_, e) => { dupTool.Open(_fullpath); e.Handled = true; }; menu.Items.Add(item); } return menu; } public void Fetch(bool autoStart) { if (!CanCreatePopup()) return; if (_remotes.Count == 0) { App.RaiseException(_fullpath, "No remotes added to this repository!!!"); return; } if (autoStart) ShowAndStartPopup(new Fetch(this)); else ShowPopup(new Fetch(this)); } public void Pull(bool autoStart) { if (!CanCreatePopup()) return; if (_remotes.Count == 0) { App.RaiseException(_fullpath, "No remotes added to this repository!!!"); return; } if (_currentBranch == null) { App.RaiseException(_fullpath, "Can NOT found current branch!!!"); return; } var pull = new Pull(this, null); if (autoStart && pull.SelectedBranch != null) ShowAndStartPopup(pull); else ShowPopup(pull); } public void Push(bool autoStart) { if (!CanCreatePopup()) return; if (_remotes.Count == 0) { App.RaiseException(_fullpath, "No remotes added to this repository!!!"); return; } if (_currentBranch == null) { App.RaiseException(_fullpath, "Can NOT found current branch!!!"); return; } if (autoStart) ShowAndStartPopup(new Push(this, null)); else ShowPopup(new Push(this, null)); } public void ApplyPatch() { if (!CanCreatePopup()) return; ShowPopup(new Apply(this)); } public void Cleanup() { if (!CanCreatePopup()) return; ShowAndStartPopup(new Cleanup(this)); } public void ClearFilter() { Filter = string.Empty; } public void ClearSearchCommitFilter() { SearchCommitFilter = string.Empty; } public void ClearMatchedFilesForSearching() { MatchedFilesForSearching = null; } public void StartSearchCommits() { if (_histories == null) return; IsSearchLoadingVisible = true; SelectedSearchedCommit = null; MatchedFilesForSearching = null; Task.Run(() => { var visible = null as List; var method = (Models.CommitSearchMethod)_searchCommitFilterType; if (method == Models.CommitSearchMethod.BySHA) { var commit = new Commands.QuerySingleCommit(_fullpath, _searchCommitFilter).Result(); visible = commit == null ? [] : [commit]; } else { visible = new Commands.QueryCommits(_fullpath, _searchCommitFilter, method, _onlySearchCommitsInCurrentBranch).Result(); } Dispatcher.UIThread.Invoke(() => { SearchedCommits = visible; IsSearchLoadingVisible = false; }); }); } public void SetWatcherEnabled(bool enabled) { _watcher?.SetEnabled(enabled); } public void MarkBranchesDirtyManually() { if (_watcher == null) { Task.Run(RefreshBranches); Task.Run(RefreshCommits); Task.Run(RefreshWorkingCopyChanges); Task.Run(RefreshWorktrees); } else { _watcher.MarkBranchDirtyManually(); } } public void MarkTagsDirtyManually() { if (_watcher == null) { Task.Run(RefreshTags); Task.Run(RefreshCommits); } else { _watcher.MarkTagDirtyManually(); } } public void MarkWorkingCopyDirtyManually() { if (_watcher == null) Task.Run(RefreshWorkingCopyChanges); else _watcher.MarkWorkingCopyDirtyManually(); } public void MarkFetched() { _lastFetchTime = DateTime.Now; } public void NavigateToCommit(string sha) { if (_histories != null) { SelectedViewIndex = 0; _histories.NavigateTo(sha); } } public void NavigateToCurrentHead() { if (_currentBranch != null) NavigateToCommit(_currentBranch.Head); } public void NavigateToBranchDelayed(string branch) { _navigateToBranchDelayed = branch; } public void ClearHistoriesFilter() { _settings.HistoriesFilters.Clear(); HistoriesFilterMode = Models.FilterMode.None; ResetBranchTreeFilterMode(LocalBranchTrees); ResetBranchTreeFilterMode(RemoteBranchTrees); ResetTagFilterMode(); Task.Run(RefreshCommits); } public void RemoveHistoriesFilter(Models.Filter filter) { if (_settings.HistoriesFilters.Remove(filter)) { HistoriesFilterMode = _settings.HistoriesFilters.Count > 0 ? _settings.HistoriesFilters[0].Mode : Models.FilterMode.None; RefreshHistoriesFilters(true); } } public void UpdateBranchNodeIsExpanded(BranchTreeNode node) { if (_settings == null || !string.IsNullOrWhiteSpace(_filter)) return; if (node.IsExpanded) { if (!_settings.ExpandedBranchNodesInSideBar.Contains(node.Path)) _settings.ExpandedBranchNodesInSideBar.Add(node.Path); } else { _settings.ExpandedBranchNodesInSideBar.Remove(node.Path); } } public void SetTagFilterMode(Models.Tag tag, Models.FilterMode mode) { var changed = _settings.UpdateHistoriesFilter(tag.Name, Models.FilterType.Tag, mode); if (changed) RefreshHistoriesFilters(true); } public void SetBranchFilterMode(Models.Branch branch, Models.FilterMode mode, bool clearExists, bool refresh) { var node = FindBranchNode(branch.IsLocal ? _localBranchTrees : _remoteBranchTrees, branch.FullName); if (node != null) SetBranchFilterMode(node, mode, clearExists, refresh); } public void SetBranchFilterMode(BranchTreeNode node, Models.FilterMode mode, bool clearExists, bool refresh) { var isLocal = node.Path.StartsWith("refs/heads/", StringComparison.Ordinal); var tree = isLocal ? _localBranchTrees : _remoteBranchTrees; if (clearExists) { _settings.HistoriesFilters.Clear(); HistoriesFilterMode = Models.FilterMode.None; } if (node.Backend is Models.Branch branch) { var type = isLocal ? Models.FilterType.LocalBranch : Models.FilterType.RemoteBranch; var changed = _settings.UpdateHistoriesFilter(node.Path, type, mode); if (!changed) return; if (isLocal && !string.IsNullOrEmpty(branch.Upstream)) _settings.UpdateHistoriesFilter(branch.Upstream, Models.FilterType.RemoteBranch, mode); } else { var type = isLocal ? Models.FilterType.LocalBranchFolder : Models.FilterType.RemoteBranchFolder; var changed = _settings.UpdateHistoriesFilter(node.Path, type, mode); if (!changed) return; _settings.RemoveChildrenBranchFilters(node.Path); } var parentType = isLocal ? Models.FilterType.LocalBranchFolder : Models.FilterType.RemoteBranchFolder; var cur = node; do { var lastSepIdx = cur.Path.LastIndexOf('/'); if (lastSepIdx <= 0) break; var parentPath = cur.Path.Substring(0, lastSepIdx); var parent = FindBranchNode(tree, parentPath); if (parent == null) break; _settings.UpdateHistoriesFilter(parent.Path, parentType, Models.FilterMode.None); cur = parent; } while (true); RefreshHistoriesFilters(refresh); } public void StashAll(bool autoStart) { _workingCopy?.StashAll(autoStart); } public void SkipMerge() { _workingCopy?.SkipMerge(); } public void AbortMerge() { _workingCopy?.AbortMerge(); } public List GetCustomActions(Models.CustomActionScope scope) { var actions = new List(); foreach (var act in Preferences.Instance.CustomActions) { if (act.Scope == scope) actions.Add(act); } foreach (var act in _settings.CustomActions) { if (act.Scope == scope) actions.Add(act); } return actions; } public void Bisect(string subcmd) { IsBisectCommandRunning = true; SetWatcherEnabled(false); var log = CreateLog($"Bisect({subcmd})"); Task.Run(() => { var succ = new Commands.Bisect(_fullpath, subcmd).Use(log).Exec(); log.Complete(); Dispatcher.UIThread.Invoke(() => { if (!succ) App.RaiseException(_fullpath, log.Content.Substring(log.Content.IndexOf('\n')).Trim()); else if (log.Content.Contains("is the first bad commit")) App.SendNotification(_fullpath, log.Content.Substring(log.Content.IndexOf('\n')).Trim()); MarkBranchesDirtyManually(); SetWatcherEnabled(true); IsBisectCommandRunning = false; }); }); } public void RefreshBranches() { var branches = new Commands.QueryBranches(_fullpath).Result(); var remotes = new Commands.QueryRemotes(_fullpath).Result(); var builder = BuildBranchTree(branches, remotes); Dispatcher.UIThread.Invoke(() => { lock (_lockRemotes) Remotes = remotes; Branches = branches; CurrentBranch = branches.Find(x => x.IsCurrent); LocalBranchTrees = builder.Locals; RemoteBranchTrees = builder.Remotes; if (_workingCopy != null) _workingCopy.HasRemotes = remotes.Count > 0; }); } public void RefreshWorktrees() { var worktrees = new Commands.Worktree(_fullpath).List(); var cleaned = new List(); foreach (var worktree in worktrees) { if (worktree.IsBare || worktree.FullPath.Equals(_fullpath)) continue; cleaned.Add(worktree); } Dispatcher.UIThread.Invoke(() => { Worktrees = cleaned; }); } public void RefreshTags() { var tags = new Commands.QueryTags(_fullpath).Result(); Dispatcher.UIThread.Invoke(() => { Tags = tags; VisibleTags = BuildVisibleTags(); }); } public void RefreshCommits() { Dispatcher.UIThread.Invoke(() => _histories.IsLoading = true); var builder = new StringBuilder(); builder.Append($"-{Preferences.Instance.MaxHistoryCommits} "); if (_settings.EnableTopoOrderInHistories) builder.Append("--topo-order "); else builder.Append("--date-order "); if (_settings.EnableReflog) builder.Append("--reflog "); if (_settings.EnableFirstParentInHistories) builder.Append("--first-parent "); var filters = _settings.BuildHistoriesFilter(); if (string.IsNullOrEmpty(filters)) builder.Append("--branches --remotes --tags HEAD"); else builder.Append(filters); var commits = new Commands.QueryCommits(_fullpath, builder.ToString()).Result(); var graph = Models.CommitGraph.Parse(commits, _settings.EnableFirstParentInHistories); Dispatcher.UIThread.Invoke(() => { if (_histories != null) { _histories.IsLoading = false; _histories.Commits = commits; _histories.Graph = graph; BisectState = _histories.UpdateBisectInfo(); if (!string.IsNullOrEmpty(_navigateToBranchDelayed)) { var branch = _branches.Find(x => x.FullName == _navigateToBranchDelayed); if (branch != null) NavigateToCommit(branch.Head); } } _navigateToBranchDelayed = string.Empty; }); } public void RefreshSubmodules() { var submodules = new Commands.QuerySubmodules(_fullpath).Result(); _watcher?.SetSubmodules(submodules); Dispatcher.UIThread.Invoke(() => { Submodules = submodules; VisibleSubmodules = BuildVisibleSubmodules(); }); } public void RefreshWorkingCopyChanges() { if (IsBare) return; var changes = new Commands.QueryLocalChanges(_fullpath, _settings.IncludeUntrackedInLocalChanges).Result(); if (_workingCopy == null) return; _workingCopy.SetData(changes); Dispatcher.UIThread.Invoke(() => { LocalChangesCount = changes.Count; OnPropertyChanged(nameof(InProgressContext)); }); } public void RefreshStashes() { if (IsBare) return; var stashes = new Commands.QueryStashes(_fullpath).Result(); Dispatcher.UIThread.Invoke(() => { if (_stashesPage != null) _stashesPage.Stashes = stashes; StashesCount = stashes.Count; }); } public void CreateNewBranch() { if (_currentBranch == null) { App.RaiseException(_fullpath, "Git do not hold any branch until you do first commit."); return; } if (CanCreatePopup()) ShowPopup(new CreateBranch(this, _currentBranch)); } public void CheckoutBranch(Models.Branch branch) { if (branch.IsLocal) { var worktree = _worktrees.Find(x => x.Branch == branch.FullName); if (worktree != null) { OpenWorktree(worktree); return; } } if (IsBare) return; if (!CanCreatePopup()) return; if (branch.IsLocal) { if (_localChangesCount > 0) ShowPopup(new Checkout(this, branch.Name)); else ShowAndStartPopup(new Checkout(this, branch.Name)); } else { foreach (var b in _branches) { if (b.IsLocal && b.Upstream == branch.FullName) { if (!b.IsCurrent) CheckoutBranch(b); return; } } ShowPopup(new CreateBranch(this, branch)); } } public void DeleteMultipleBranches(List branches, bool isLocal) { if (CanCreatePopup()) ShowPopup(new DeleteMultipleBranches(this, branches, isLocal)); } public void MergeMultipleBranches(List branches) { if (CanCreatePopup()) ShowPopup(new MergeMultiple(this, branches)); } public void CreateNewTag() { if (_currentBranch == null) { App.RaiseException(_fullpath, "Git do not hold any branch until you do first commit."); return; } if (CanCreatePopup()) ShowPopup(new CreateTag(this, _currentBranch)); } public void AddRemote() { if (CanCreatePopup()) ShowPopup(new AddRemote(this)); } public void AddSubmodule() { if (CanCreatePopup()) ShowPopup(new AddSubmodule(this)); } public void UpdateSubmodules() { if (CanCreatePopup()) ShowPopup(new UpdateSubmodules(this)); } public void OpenSubmodule(string submodule) { var selfPage = GetOwnerPage(); if (selfPage == null) return; var root = Path.GetFullPath(Path.Combine(_fullpath, submodule)); var normalizedPath = root.Replace("\\", "/"); var node = Preferences.Instance.FindNode(normalizedPath); if (node == null) { node = new RepositoryNode() { Id = normalizedPath, Name = Path.GetFileName(normalizedPath), Bookmark = selfPage.Node.Bookmark, IsRepository = true, }; } App.GetLauncer().OpenRepositoryInTab(node, null); } public void AddWorktree() { if (CanCreatePopup()) ShowPopup(new AddWorktree(this)); } public void PruneWorktrees() { if (CanCreatePopup()) ShowAndStartPopup(new PruneWorktrees(this)); } public void OpenWorktree(Models.Worktree worktree) { var node = Preferences.Instance.FindNode(worktree.FullPath); if (node == null) { node = new RepositoryNode() { Id = worktree.FullPath, Name = Path.GetFileName(worktree.FullPath), Bookmark = 0, IsRepository = true, }; } App.GetLauncer()?.OpenRepositoryInTab(node, null); } public List GetPreferedOpenAIServices() { var services = Preferences.Instance.OpenAIServices; if (services == null || services.Count == 0) return []; if (services.Count == 1) return [services[0]]; var prefered = _settings.PreferedOpenAIService; var all = new List(); foreach (var service in services) { if (service.Name.Equals(prefered, StringComparison.Ordinal)) return [service]; all.Add(service); } return all; } public ContextMenu CreateContextMenuForGitFlow() { var menu = new ContextMenu(); menu.Placement = PlacementMode.BottomEdgeAlignedLeft; var isGitFlowEnabled = Commands.GitFlow.IsEnabled(_fullpath, _branches); if (isGitFlowEnabled) { var startFeature = new MenuItem(); startFeature.Header = App.Text("GitFlow.StartFeature"); startFeature.Icon = App.CreateMenuIcon("Icons.GitFlow.Feature"); startFeature.Click += (_, e) => { if (CanCreatePopup()) ShowPopup(new GitFlowStart(this, "feature")); e.Handled = true; }; var startRelease = new MenuItem(); startRelease.Header = App.Text("GitFlow.StartRelease"); startRelease.Icon = App.CreateMenuIcon("Icons.GitFlow.Release"); startRelease.Click += (_, e) => { if (CanCreatePopup()) ShowPopup(new GitFlowStart(this, "release")); e.Handled = true; }; var startHotfix = new MenuItem(); startHotfix.Header = App.Text("GitFlow.StartHotfix"); startHotfix.Icon = App.CreateMenuIcon("Icons.GitFlow.Hotfix"); startHotfix.Click += (_, e) => { if (CanCreatePopup()) ShowPopup(new GitFlowStart(this, "hotfix")); e.Handled = true; }; menu.Items.Add(startFeature); menu.Items.Add(startRelease); menu.Items.Add(startHotfix); } else { var init = new MenuItem(); init.Header = App.Text("GitFlow.Init"); init.Icon = App.CreateMenuIcon("Icons.Init"); init.Click += (_, e) => { if (CanCreatePopup()) ShowPopup(new InitGitFlow(this)); e.Handled = true; }; menu.Items.Add(init); } return menu; } public ContextMenu CreateContextMenuForGitLFS() { var menu = new ContextMenu(); menu.Placement = PlacementMode.BottomEdgeAlignedLeft; var lfs = new Commands.LFS(_fullpath); if (lfs.IsEnabled()) { var addPattern = new MenuItem(); addPattern.Header = App.Text("GitLFS.AddTrackPattern"); addPattern.Icon = App.CreateMenuIcon("Icons.File.Add"); addPattern.Click += (_, e) => { if (CanCreatePopup()) ShowPopup(new LFSTrackCustomPattern(this)); e.Handled = true; }; menu.Items.Add(addPattern); menu.Items.Add(new MenuItem() { Header = "-" }); var fetch = new MenuItem(); fetch.Header = App.Text("GitLFS.Fetch"); fetch.Icon = App.CreateMenuIcon("Icons.Fetch"); fetch.IsEnabled = _remotes.Count > 0; fetch.Click += (_, e) => { if (CanCreatePopup()) { if (_remotes.Count == 1) ShowAndStartPopup(new LFSFetch(this)); else ShowPopup(new LFSFetch(this)); } e.Handled = true; }; menu.Items.Add(fetch); var pull = new MenuItem(); pull.Header = App.Text("GitLFS.Pull"); pull.Icon = App.CreateMenuIcon("Icons.Pull"); pull.IsEnabled = _remotes.Count > 0; pull.Click += (_, e) => { if (CanCreatePopup()) { if (_remotes.Count == 1) ShowAndStartPopup(new LFSPull(this)); else ShowPopup(new LFSPull(this)); } e.Handled = true; }; menu.Items.Add(pull); var push = new MenuItem(); push.Header = App.Text("GitLFS.Push"); push.Icon = App.CreateMenuIcon("Icons.Push"); push.IsEnabled = _remotes.Count > 0; push.Click += (_, e) => { if (CanCreatePopup()) { if (_remotes.Count == 1) ShowAndStartPopup(new LFSPush(this)); else ShowPopup(new LFSPush(this)); } e.Handled = true; }; menu.Items.Add(push); var prune = new MenuItem(); prune.Header = App.Text("GitLFS.Prune"); prune.Icon = App.CreateMenuIcon("Icons.Clean"); prune.Click += (_, e) => { if (CanCreatePopup()) ShowAndStartPopup(new LFSPrune(this)); e.Handled = true; }; menu.Items.Add(new MenuItem() { Header = "-" }); menu.Items.Add(prune); var locks = new MenuItem(); locks.Header = App.Text("GitLFS.Locks"); locks.Icon = App.CreateMenuIcon("Icons.Lock"); locks.IsEnabled = _remotes.Count > 0; if (_remotes.Count == 1) { locks.Click += (_, e) => { App.ShowWindow(new LFSLocks(this, _remotes[0].Name), true); e.Handled = true; }; } else { foreach (var remote in _remotes) { var remoteName = remote.Name; var lockRemote = new MenuItem(); lockRemote.Header = remoteName; lockRemote.Click += (_, e) => { App.ShowWindow(new LFSLocks(this, remoteName), true); e.Handled = true; }; locks.Items.Add(lockRemote); } } menu.Items.Add(new MenuItem() { Header = "-" }); menu.Items.Add(locks); } else { var install = new MenuItem(); install.Header = App.Text("GitLFS.Install"); install.Icon = App.CreateMenuIcon("Icons.Init"); install.Click += (_, e) => { var log = CreateLog("Install LFS"); var succ = new Commands.LFS(_fullpath).Install(log); if (succ) App.SendNotification(_fullpath, $"LFS enabled successfully!"); log.Complete(); e.Handled = true; }; menu.Items.Add(install); } return menu; } public ContextMenu CreateContextMenuForCustomAction() { var menu = new ContextMenu(); menu.Placement = PlacementMode.BottomEdgeAlignedLeft; var actions = GetCustomActions(Models.CustomActionScope.Repository); if (actions.Count > 0) { foreach (var action in actions) { var dup = action; var item = new MenuItem(); item.Icon = App.CreateMenuIcon("Icons.Action"); item.Header = dup.Name; item.Click += (_, e) => { if (CanCreatePopup()) ShowAndStartPopup(new ExecuteCustomAction(this, dup)); e.Handled = true; }; menu.Items.Add(item); } } else { menu.Items.Add(new MenuItem() { Header = App.Text("Repository.CustomActions.Empty") }); } return menu; } public ContextMenu CreateContextMenuForHistoriesPage() { var layout = new MenuItem(); layout.Header = App.Text("Repository.HistoriesLayout"); layout.IsEnabled = false; var isHorizontal = Preferences.Instance.UseTwoColumnsLayoutInHistories; var horizontal = new MenuItem(); horizontal.Header = App.Text("Repository.HistoriesLayout.Horizontal"); if (isHorizontal) horizontal.Icon = App.CreateMenuIcon("Icons.Check"); horizontal.Click += (_, ev) => { Preferences.Instance.UseTwoColumnsLayoutInHistories = true; ev.Handled = true; }; var vertical = new MenuItem(); vertical.Header = App.Text("Repository.HistoriesLayout.Vertical"); if (!isHorizontal) vertical.Icon = App.CreateMenuIcon("Icons.Check"); vertical.Click += (_, ev) => { Preferences.Instance.UseTwoColumnsLayoutInHistories = false; ev.Handled = true; }; var order = new MenuItem(); order.Header = App.Text("Repository.HistoriesOrder"); order.IsEnabled = false; var dateOrder = new MenuItem(); dateOrder.Header = App.Text("Repository.HistoriesOrder.ByDate"); dateOrder.SetValue(Views.MenuItemExtension.CommandProperty, "--date-order"); if (!_settings.EnableTopoOrderInHistories) dateOrder.Icon = App.CreateMenuIcon("Icons.Check"); dateOrder.Click += (_, ev) => { if (_settings.EnableTopoOrderInHistories) { _settings.EnableTopoOrderInHistories = false; Task.Run(RefreshCommits); } ev.Handled = true; }; var topoOrder = new MenuItem(); topoOrder.Header = App.Text("Repository.HistoriesOrder.Topo"); topoOrder.SetValue(Views.MenuItemExtension.CommandProperty, "--top-order"); if (_settings.EnableTopoOrderInHistories) topoOrder.Icon = App.CreateMenuIcon("Icons.Check"); topoOrder.Click += (_, ev) => { if (!_settings.EnableTopoOrderInHistories) { _settings.EnableTopoOrderInHistories = true; Task.Run(RefreshCommits); } ev.Handled = true; }; var menu = new ContextMenu(); menu.Items.Add(layout); menu.Items.Add(horizontal); menu.Items.Add(vertical); menu.Items.Add(new MenuItem() { Header = "-" }); menu.Items.Add(order); menu.Items.Add(dateOrder); menu.Items.Add(topoOrder); return menu; } public ContextMenu CreateContextMenuForLocalBranch(Models.Branch branch) { var menu = new ContextMenu(); var push = new MenuItem(); push.Header = App.Text("BranchCM.Push", branch.Name); push.Icon = App.CreateMenuIcon("Icons.Push"); push.IsEnabled = _remotes.Count > 0; push.Click += (_, e) => { if (CanCreatePopup()) ShowPopup(new Push(this, branch)); e.Handled = true; }; if (branch.IsCurrent) { if (!IsBare) { var discard = new MenuItem(); discard.Header = App.Text("BranchCM.DiscardAll"); discard.Icon = App.CreateMenuIcon("Icons.Undo"); discard.Click += (_, e) => { if (CanCreatePopup()) ShowPopup(new Discard(this)); e.Handled = true; }; menu.Items.Add(discard); menu.Items.Add(new MenuItem() { Header = "-" }); if (!string.IsNullOrEmpty(branch.Upstream)) { var upstream = branch.Upstream.Substring(13); var fastForward = new MenuItem(); fastForward.Header = App.Text("BranchCM.FastForward", upstream); fastForward.Icon = App.CreateMenuIcon("Icons.FastForward"); fastForward.IsEnabled = branch.TrackStatus.Ahead.Count == 0; fastForward.Click += (_, e) => { var b = _branches.Find(x => x.FriendlyName == upstream); if (b == null) return; if (CanCreatePopup()) ShowAndStartPopup(new Merge(this, b, branch.Name, true)); e.Handled = true; }; var pull = new MenuItem(); pull.Header = App.Text("BranchCM.Pull", upstream); pull.Icon = App.CreateMenuIcon("Icons.Pull"); pull.Click += (_, e) => { if (CanCreatePopup()) ShowPopup(new Pull(this, null)); e.Handled = true; }; menu.Items.Add(fastForward); menu.Items.Add(pull); } } menu.Items.Add(push); } else { if (!IsBare) { var checkout = new MenuItem(); checkout.Header = App.Text("BranchCM.Checkout", branch.Name); checkout.Icon = App.CreateMenuIcon("Icons.Check"); checkout.Click += (_, e) => { CheckoutBranch(branch); e.Handled = true; }; menu.Items.Add(checkout); menu.Items.Add(new MenuItem() { Header = "-" }); } var worktree = _worktrees.Find(x => x.Branch == branch.FullName); var upstream = _branches.Find(x => x.FullName == branch.Upstream); if (upstream != null && worktree == null) { var fastForward = new MenuItem(); fastForward.Header = App.Text("BranchCM.FastForward", upstream.FriendlyName); fastForward.Icon = App.CreateMenuIcon("Icons.FastForward"); fastForward.IsEnabled = branch.TrackStatus.Ahead.Count == 0; fastForward.Click += (_, e) => { if (CanCreatePopup()) ShowAndStartPopup(new FastForwardWithoutCheckout(this, branch, upstream)); e.Handled = true; }; var fetchInto = new MenuItem(); fetchInto.Header = App.Text("BranchCM.FetchInto", upstream.FriendlyName, branch.Name); fetchInto.Icon = App.CreateMenuIcon("Icons.Fetch"); fetchInto.IsEnabled = branch.TrackStatus.Ahead.Count == 0; fetchInto.Click += (_, e) => { if (CanCreatePopup()) ShowAndStartPopup(new FetchInto(this, branch, upstream)); e.Handled = true; }; menu.Items.Add(fastForward); menu.Items.Add(new MenuItem() { Header = "-" }); menu.Items.Add(fetchInto); } menu.Items.Add(push); if (!IsBare) { var merge = new MenuItem(); merge.Header = App.Text("BranchCM.Merge", branch.Name, _currentBranch.Name); merge.Icon = App.CreateMenuIcon("Icons.Merge"); merge.Click += (_, e) => { if (CanCreatePopup()) ShowPopup(new Merge(this, branch, _currentBranch.Name, false)); e.Handled = true; }; var rebase = new MenuItem(); rebase.Header = App.Text("BranchCM.Rebase", _currentBranch.Name, branch.Name); rebase.Icon = App.CreateMenuIcon("Icons.Rebase"); rebase.Click += (_, e) => { if (CanCreatePopup()) ShowPopup(new Rebase(this, _currentBranch, branch)); e.Handled = true; }; menu.Items.Add(merge); menu.Items.Add(rebase); } var compareWithHead = new MenuItem(); compareWithHead.Header = App.Text("BranchCM.CompareWithHead"); compareWithHead.Icon = App.CreateMenuIcon("Icons.Compare"); compareWithHead.Click += (_, _) => { App.ShowWindow(new BranchCompare(_fullpath, branch, _currentBranch), false); }; menu.Items.Add(new MenuItem() { Header = "-" }); menu.Items.Add(compareWithHead); if (_localChangesCount > 0) { var compareWithWorktree = new MenuItem(); compareWithWorktree.Header = App.Text("BranchCM.CompareWithWorktree"); compareWithWorktree.Icon = App.CreateMenuIcon("Icons.Compare"); compareWithWorktree.Click += (_, _) => { SelectedSearchedCommit = null; if (_histories != null) { var target = new Commands.QuerySingleCommit(_fullpath, branch.Head).Result(); _histories.AutoSelectedCommit = null; _histories.DetailContext = new RevisionCompare(_fullpath, target, null); } }; menu.Items.Add(compareWithWorktree); } } if (!IsBare) { var detect = Commands.GitFlow.DetectType(_fullpath, _branches, branch.Name); if (detect.IsGitFlowBranch) { var finish = new MenuItem(); finish.Header = App.Text("BranchCM.Finish", branch.Name); finish.Icon = App.CreateMenuIcon("Icons.GitFlow"); finish.Click += (_, e) => { if (CanCreatePopup()) ShowPopup(new GitFlowFinish(this, branch, detect.Type, detect.Prefix)); e.Handled = true; }; menu.Items.Add(new MenuItem() { Header = "-" }); menu.Items.Add(finish); } } var rename = new MenuItem(); rename.Header = App.Text("BranchCM.Rename", branch.Name); rename.Icon = App.CreateMenuIcon("Icons.Rename"); rename.Click += (_, e) => { if (CanCreatePopup()) ShowPopup(new RenameBranch(this, branch)); e.Handled = true; }; var delete = new MenuItem(); delete.Header = App.Text("BranchCM.Delete", branch.Name); delete.Icon = App.CreateMenuIcon("Icons.Clear"); delete.IsEnabled = !branch.IsCurrent; delete.Click += (_, e) => { if (CanCreatePopup()) ShowPopup(new DeleteBranch(this, branch)); e.Handled = true; }; var createBranch = new MenuItem(); createBranch.Icon = App.CreateMenuIcon("Icons.Branch.Add"); createBranch.Header = App.Text("CreateBranch"); createBranch.Click += (_, e) => { if (CanCreatePopup()) ShowPopup(new CreateBranch(this, branch)); e.Handled = true; }; var createTag = new MenuItem(); createTag.Icon = App.CreateMenuIcon("Icons.Tag.Add"); createTag.Header = App.Text("CreateTag"); createTag.Click += (_, e) => { if (CanCreatePopup()) ShowPopup(new CreateTag(this, branch)); e.Handled = true; }; menu.Items.Add(new MenuItem() { Header = "-" }); menu.Items.Add(rename); menu.Items.Add(delete); menu.Items.Add(new MenuItem() { Header = "-" }); menu.Items.Add(createBranch); menu.Items.Add(createTag); menu.Items.Add(new MenuItem() { Header = "-" }); TryToAddCustomActionsToBranchContextMenu(menu, branch); if (!IsBare) { var remoteBranches = new List(); foreach (var b in _branches) { if (!b.IsLocal) remoteBranches.Add(b); } if (remoteBranches.Count > 0) { var tracking = new MenuItem(); tracking.Header = App.Text("BranchCM.Tracking"); tracking.Icon = App.CreateMenuIcon("Icons.Track"); tracking.Click += (_, e) => { if (CanCreatePopup()) ShowPopup(new SetUpstream(this, branch, remoteBranches)); e.Handled = true; }; menu.Items.Add(tracking); } } var archive = new MenuItem(); archive.Icon = App.CreateMenuIcon("Icons.Archive"); archive.Header = App.Text("Archive"); archive.Click += (_, e) => { if (CanCreatePopup()) ShowPopup(new Archive(this, branch)); e.Handled = true; }; menu.Items.Add(archive); menu.Items.Add(new MenuItem() { Header = "-" }); var copy = new MenuItem(); copy.Header = App.Text("BranchCM.CopyName"); copy.Icon = App.CreateMenuIcon("Icons.Copy"); copy.Click += (_, e) => { App.CopyText(branch.Name); e.Handled = true; }; menu.Items.Add(copy); return menu; } public ContextMenu CreateContextMenuForRemote(Models.Remote remote) { var menu = new ContextMenu(); if (remote.TryGetVisitURL(out string visitURL)) { var visit = new MenuItem(); visit.Header = App.Text("RemoteCM.OpenInBrowser"); visit.Icon = App.CreateMenuIcon("Icons.OpenWith"); visit.Click += (_, e) => { Native.OS.OpenBrowser(visitURL); e.Handled = true; }; menu.Items.Add(visit); menu.Items.Add(new MenuItem() { Header = "-" }); } var fetch = new MenuItem(); fetch.Header = App.Text("RemoteCM.Fetch"); fetch.Icon = App.CreateMenuIcon("Icons.Fetch"); fetch.Click += (_, e) => { if (CanCreatePopup()) ShowAndStartPopup(new Fetch(this, remote)); e.Handled = true; }; var prune = new MenuItem(); prune.Header = App.Text("RemoteCM.Prune"); prune.Icon = App.CreateMenuIcon("Icons.Clean"); prune.Click += (_, e) => { if (CanCreatePopup()) ShowAndStartPopup(new PruneRemote(this, remote)); e.Handled = true; }; var edit = new MenuItem(); edit.Header = App.Text("RemoteCM.Edit"); edit.Icon = App.CreateMenuIcon("Icons.Edit"); edit.Click += (_, e) => { if (CanCreatePopup()) ShowPopup(new EditRemote(this, remote)); e.Handled = true; }; var delete = new MenuItem(); delete.Header = App.Text("RemoteCM.Delete"); delete.Icon = App.CreateMenuIcon("Icons.Clear"); delete.Click += (_, e) => { if (CanCreatePopup()) ShowPopup(new DeleteRemote(this, remote)); e.Handled = true; }; var copy = new MenuItem(); copy.Header = App.Text("RemoteCM.CopyURL"); copy.Icon = App.CreateMenuIcon("Icons.Copy"); copy.Click += (_, e) => { App.CopyText(remote.URL); e.Handled = true; }; menu.Items.Add(fetch); menu.Items.Add(prune); menu.Items.Add(new MenuItem() { Header = "-" }); menu.Items.Add(edit); menu.Items.Add(delete); menu.Items.Add(new MenuItem() { Header = "-" }); menu.Items.Add(copy); return menu; } public ContextMenu CreateContextMenuForRemoteBranch(Models.Branch branch) { var menu = new ContextMenu(); var name = branch.FriendlyName; var checkout = new MenuItem(); checkout.Header = App.Text("BranchCM.Checkout", name); checkout.Icon = App.CreateMenuIcon("Icons.Check"); checkout.Click += (_, e) => { CheckoutBranch(branch); e.Handled = true; }; menu.Items.Add(checkout); menu.Items.Add(new MenuItem() { Header = "-" }); if (_currentBranch != null) { var pull = new MenuItem(); pull.Header = App.Text("BranchCM.PullInto", name, _currentBranch.Name); pull.Icon = App.CreateMenuIcon("Icons.Pull"); pull.Click += (_, e) => { if (CanCreatePopup()) ShowPopup(new Pull(this, branch)); e.Handled = true; }; var merge = new MenuItem(); merge.Header = App.Text("BranchCM.Merge", name, _currentBranch.Name); merge.Icon = App.CreateMenuIcon("Icons.Merge"); merge.Click += (_, e) => { if (CanCreatePopup()) ShowPopup(new Merge(this, branch, _currentBranch.Name, false)); e.Handled = true; }; var rebase = new MenuItem(); rebase.Header = App.Text("BranchCM.Rebase", _currentBranch.Name, name); rebase.Icon = App.CreateMenuIcon("Icons.Rebase"); rebase.Click += (_, e) => { if (CanCreatePopup()) ShowPopup(new Rebase(this, _currentBranch, branch)); e.Handled = true; }; menu.Items.Add(pull); menu.Items.Add(merge); menu.Items.Add(rebase); menu.Items.Add(new MenuItem() { Header = "-" }); } var compareWithHead = new MenuItem(); compareWithHead.Header = App.Text("BranchCM.CompareWithHead"); compareWithHead.Icon = App.CreateMenuIcon("Icons.Compare"); compareWithHead.Click += (_, _) => { App.ShowWindow(new BranchCompare(_fullpath, branch, _currentBranch), false); }; menu.Items.Add(compareWithHead); if (_localChangesCount > 0) { var compareWithWorktree = new MenuItem(); compareWithWorktree.Header = App.Text("BranchCM.CompareWithWorktree"); compareWithWorktree.Icon = App.CreateMenuIcon("Icons.Compare"); compareWithWorktree.Click += (_, _) => { SelectedSearchedCommit = null; if (_histories != null) { var target = new Commands.QuerySingleCommit(_fullpath, branch.Head).Result(); _histories.AutoSelectedCommit = null; _histories.DetailContext = new RevisionCompare(_fullpath, target, null); } }; menu.Items.Add(compareWithWorktree); } menu.Items.Add(new MenuItem() { Header = "-" }); var delete = new MenuItem(); delete.Header = App.Text("BranchCM.Delete", name); delete.Icon = App.CreateMenuIcon("Icons.Clear"); delete.Click += (_, e) => { if (CanCreatePopup()) ShowPopup(new DeleteBranch(this, branch)); e.Handled = true; }; var createBranch = new MenuItem(); createBranch.Icon = App.CreateMenuIcon("Icons.Branch.Add"); createBranch.Header = App.Text("CreateBranch"); createBranch.Click += (_, e) => { if (CanCreatePopup()) ShowPopup(new CreateBranch(this, branch)); e.Handled = true; }; var createTag = new MenuItem(); createTag.Icon = App.CreateMenuIcon("Icons.Tag.Add"); createTag.Header = App.Text("CreateTag"); createTag.Click += (_, e) => { if (CanCreatePopup()) ShowPopup(new CreateTag(this, branch)); e.Handled = true; }; var archive = new MenuItem(); archive.Icon = App.CreateMenuIcon("Icons.Archive"); archive.Header = App.Text("Archive"); archive.Click += (_, e) => { if (CanCreatePopup()) ShowPopup(new Archive(this, branch)); e.Handled = true; }; var copy = new MenuItem(); copy.Header = App.Text("BranchCM.CopyName"); copy.Icon = App.CreateMenuIcon("Icons.Copy"); copy.Click += (_, e) => { App.CopyText(name); e.Handled = true; }; menu.Items.Add(delete); menu.Items.Add(new MenuItem() { Header = "-" }); menu.Items.Add(createBranch); menu.Items.Add(createTag); menu.Items.Add(new MenuItem() { Header = "-" }); menu.Items.Add(archive); menu.Items.Add(new MenuItem() { Header = "-" }); TryToAddCustomActionsToBranchContextMenu(menu, branch); menu.Items.Add(copy); return menu; } public ContextMenu CreateContextMenuForTag(Models.Tag tag) { var createBranch = new MenuItem(); createBranch.Icon = App.CreateMenuIcon("Icons.Branch.Add"); createBranch.Header = App.Text("CreateBranch"); createBranch.Click += (_, ev) => { if (CanCreatePopup()) ShowPopup(new CreateBranch(this, tag)); ev.Handled = true; }; var pushTag = new MenuItem(); pushTag.Header = App.Text("TagCM.Push", tag.Name); pushTag.Icon = App.CreateMenuIcon("Icons.Push"); pushTag.IsEnabled = _remotes.Count > 0; pushTag.Click += (_, ev) => { if (CanCreatePopup()) ShowPopup(new PushTag(this, tag)); ev.Handled = true; }; var deleteTag = new MenuItem(); deleteTag.Header = App.Text("TagCM.Delete", tag.Name); deleteTag.Icon = App.CreateMenuIcon("Icons.Clear"); deleteTag.Click += (_, ev) => { if (CanCreatePopup()) ShowPopup(new DeleteTag(this, tag)); ev.Handled = true; }; var archive = new MenuItem(); archive.Icon = App.CreateMenuIcon("Icons.Archive"); archive.Header = App.Text("Archive"); archive.Click += (_, ev) => { if (CanCreatePopup()) ShowPopup(new Archive(this, tag)); ev.Handled = true; }; var copy = new MenuItem(); copy.Header = App.Text("TagCM.Copy"); copy.Icon = App.CreateMenuIcon("Icons.Copy"); copy.Click += (_, ev) => { App.CopyText(tag.Name); ev.Handled = true; }; var copyMessage = new MenuItem(); copyMessage.Header = App.Text("TagCM.CopyMessage"); copyMessage.Icon = App.CreateMenuIcon("Icons.Copy"); copyMessage.IsEnabled = !string.IsNullOrEmpty(tag.Message); copyMessage.Click += (_, ev) => { App.CopyText(tag.Message); ev.Handled = true; }; var menu = new ContextMenu(); menu.Items.Add(createBranch); menu.Items.Add(new MenuItem() { Header = "-" }); menu.Items.Add(pushTag); menu.Items.Add(deleteTag); menu.Items.Add(new MenuItem() { Header = "-" }); menu.Items.Add(archive); menu.Items.Add(new MenuItem() { Header = "-" }); menu.Items.Add(copy); menu.Items.Add(copyMessage); return menu; } public ContextMenu CreateContextMenuForTagSortMode() { var mode = _settings.TagSortMode; var changeMode = new Action((m) => { if (_settings.TagSortMode != m) { _settings.TagSortMode = m; VisibleTags = BuildVisibleTags(); } }); var byCreatorDate = new MenuItem(); byCreatorDate.Header = App.Text("Repository.Tags.OrderByCreatorDate"); if (mode == Models.TagSortMode.CreatorDate) byCreatorDate.Icon = App.CreateMenuIcon("Icons.Check"); byCreatorDate.Click += (_, ev) => { changeMode(Models.TagSortMode.CreatorDate); ev.Handled = true; }; var byNameAsc = new MenuItem(); byNameAsc.Header = App.Text("Repository.Tags.OrderByNameAsc"); if (mode == Models.TagSortMode.NameInAscending) byNameAsc.Icon = App.CreateMenuIcon("Icons.Check"); byNameAsc.Click += (_, ev) => { changeMode(Models.TagSortMode.NameInAscending); ev.Handled = true; }; var byNameDes = new MenuItem(); byNameDes.Header = App.Text("Repository.Tags.OrderByNameDes"); if (mode == Models.TagSortMode.NameInDescending) byNameDes.Icon = App.CreateMenuIcon("Icons.Check"); byNameDes.Click += (_, ev) => { changeMode(Models.TagSortMode.NameInDescending); ev.Handled = true; }; var menu = new ContextMenu(); menu.Items.Add(byCreatorDate); menu.Items.Add(byNameAsc); menu.Items.Add(byNameDes); return menu; } public ContextMenu CreateContextMenuForSubmodule(string submodule) { var open = new MenuItem(); open.Header = App.Text("Submodule.Open"); open.Icon = App.CreateMenuIcon("Icons.Folder.Open"); open.Click += (_, ev) => { OpenSubmodule(submodule); ev.Handled = true; }; var copy = new MenuItem(); copy.Header = App.Text("Submodule.CopyPath"); copy.Icon = App.CreateMenuIcon("Icons.Copy"); copy.Click += (_, ev) => { App.CopyText(submodule); ev.Handled = true; }; var rm = new MenuItem(); rm.Header = App.Text("Submodule.Remove"); rm.Icon = App.CreateMenuIcon("Icons.Clear"); rm.Click += (_, ev) => { if (CanCreatePopup()) ShowPopup(new DeleteSubmodule(this, submodule)); ev.Handled = true; }; var menu = new ContextMenu(); menu.Items.Add(open); menu.Items.Add(copy); menu.Items.Add(rm); return menu; } public ContextMenu CreateContextMenuForWorktree(Models.Worktree worktree) { var menu = new ContextMenu(); if (worktree.IsLocked) { var unlock = new MenuItem(); unlock.Header = App.Text("Worktree.Unlock"); unlock.Icon = App.CreateMenuIcon("Icons.Unlock"); unlock.Click += (_, ev) => { SetWatcherEnabled(false); var log = CreateLog("Unlock Worktree"); var succ = new Commands.Worktree(_fullpath).Use(log).Unlock(worktree.FullPath); if (succ) worktree.IsLocked = false; log.Complete(); SetWatcherEnabled(true); ev.Handled = true; }; menu.Items.Add(unlock); } else { var loc = new MenuItem(); loc.Header = App.Text("Worktree.Lock"); loc.Icon = App.CreateMenuIcon("Icons.Lock"); loc.Click += (_, ev) => { SetWatcherEnabled(false); var log = CreateLog("Lock Worktree"); var succ = new Commands.Worktree(_fullpath).Use(log).Lock(worktree.FullPath); if (succ) worktree.IsLocked = true; log.Complete(); SetWatcherEnabled(true); ev.Handled = true; }; menu.Items.Add(loc); } var remove = new MenuItem(); remove.Header = App.Text("Worktree.Remove"); remove.Icon = App.CreateMenuIcon("Icons.Clear"); remove.Click += (_, ev) => { if (CanCreatePopup()) ShowPopup(new RemoveWorktree(this, worktree)); ev.Handled = true; }; menu.Items.Add(remove); var copy = new MenuItem(); copy.Header = App.Text("Worktree.CopyPath"); copy.Icon = App.CreateMenuIcon("Icons.Copy"); copy.Click += (_, e) => { App.CopyText(worktree.FullPath); e.Handled = true; }; menu.Items.Add(new MenuItem() { Header = "-" }); menu.Items.Add(copy); return menu; } private LauncherPage GetOwnerPage() { var launcher = App.GetLauncer(); if (launcher == null) return null; foreach (var page in launcher.Pages) { if (page.Node.Id.Equals(_fullpath)) return page; } return null; } private BranchTreeNode.Builder BuildBranchTree(List branches, List remotes) { var builder = new BranchTreeNode.Builder(); if (string.IsNullOrEmpty(_filter)) { builder.SetExpandedNodes(_settings.ExpandedBranchNodesInSideBar); builder.Run(branches, remotes, false); foreach (var invalid in builder.InvalidExpandedNodes) _settings.ExpandedBranchNodesInSideBar.Remove(invalid); } else { var visibles = new List(); foreach (var b in branches) { if (b.FullName.Contains(_filter, StringComparison.OrdinalIgnoreCase)) visibles.Add(b); } builder.Run(visibles, remotes, true); } var historiesFilters = _settings.CollectHistoriesFilters(); UpdateBranchTreeFilterMode(builder.Locals, historiesFilters); UpdateBranchTreeFilterMode(builder.Remotes, historiesFilters); return builder; } private List BuildVisibleTags() { switch (_settings.TagSortMode) { case Models.TagSortMode.CreatorDate: _tags.Sort((l, r) => r.CreatorDate.CompareTo(l.CreatorDate)); break; case Models.TagSortMode.NameInAscending: _tags.Sort((l, r) => Models.NumericSort.Compare(l.Name, r.Name)); break; default: _tags.Sort((l, r) => Models.NumericSort.Compare(r.Name, l.Name)); break; } var visible = new List(); if (string.IsNullOrEmpty(_filter)) { visible.AddRange(_tags); } else { foreach (var t in _tags) { if (t.Name.Contains(_filter, StringComparison.OrdinalIgnoreCase)) visible.Add(t); } } var historiesFilters = _settings.CollectHistoriesFilters(); UpdateTagFilterMode(historiesFilters); return visible; } private List BuildVisibleSubmodules() { var visible = new List(); if (string.IsNullOrEmpty(_filter)) { visible.AddRange(_submodules); } else { foreach (var s in _submodules) { if (s.Path.Contains(_filter, StringComparison.OrdinalIgnoreCase)) visible.Add(s); } } return visible; } private void RefreshHistoriesFilters(bool refresh) { if (_settings.HistoriesFilters.Count > 0) HistoriesFilterMode = _settings.HistoriesFilters[0].Mode; else HistoriesFilterMode = Models.FilterMode.None; if (!refresh) return; var filters = _settings.CollectHistoriesFilters(); UpdateBranchTreeFilterMode(LocalBranchTrees, filters); UpdateBranchTreeFilterMode(RemoteBranchTrees, filters); UpdateTagFilterMode(filters); Task.Run(RefreshCommits); } private void UpdateBranchTreeFilterMode(List nodes, Dictionary filters) { foreach (var node in nodes) { if (filters.TryGetValue(node.Path, out var value)) node.FilterMode = value; else node.FilterMode = Models.FilterMode.None; if (!node.IsBranch) UpdateBranchTreeFilterMode(node.Children, filters); } } private void UpdateTagFilterMode(Dictionary filters) { foreach (var tag in _tags) { if (filters.TryGetValue(tag.Name, out var value)) tag.FilterMode = value; else tag.FilterMode = Models.FilterMode.None; } } private void ResetBranchTreeFilterMode(List nodes) { foreach (var node in nodes) { node.FilterMode = Models.FilterMode.None; if (!node.IsBranch) ResetBranchTreeFilterMode(node.Children); } } private void ResetTagFilterMode() { foreach (var tag in _tags) tag.FilterMode = Models.FilterMode.None; } private BranchTreeNode FindBranchNode(List nodes, string path) { foreach (var node in nodes) { if (node.Path.Equals(path, StringComparison.Ordinal)) return node; if (path!.StartsWith(node.Path, StringComparison.Ordinal)) { var founded = FindBranchNode(node.Children, path); if (founded != null) return founded; } } return null; } private void TryToAddCustomActionsToBranchContextMenu(ContextMenu menu, Models.Branch branch) { var actions = GetCustomActions(Models.CustomActionScope.Branch); if (actions.Count == 0) return; var custom = new MenuItem(); custom.Header = App.Text("BranchCM.CustomAction"); custom.Icon = App.CreateMenuIcon("Icons.Action"); foreach (var action in actions) { var dup = action; var item = new MenuItem(); item.Icon = App.CreateMenuIcon("Icons.Action"); item.Header = dup.Name; item.Click += (_, e) => { if (CanCreatePopup()) ShowAndStartPopup(new ExecuteCustomAction(this, dup, branch)); e.Handled = true; }; custom.Items.Add(item); } menu.Items.Add(custom); menu.Items.Add(new MenuItem() { Header = "-" }); } private bool IsSearchingCommitsByFilePath() { return _isSearching && _searchCommitFilterType == (int)Models.CommitSearchMethod.ByFile; } private void CalcWorktreeFilesForSearching() { if (!IsSearchingCommitsByFilePath()) { _worktreeFiles = null; MatchedFilesForSearching = null; GC.Collect(); return; } Task.Run(() => { _worktreeFiles = new Commands.QueryRevisionFileNames(_fullpath, "HEAD").Result(); Dispatcher.UIThread.Invoke(() => { if (IsSearchingCommitsByFilePath()) CalcMatchedFilesForSearching(); }); }); } private void CalcMatchedFilesForSearching() { if (_worktreeFiles == null || _worktreeFiles.Count == 0 || _searchCommitFilter.Length < 3) { MatchedFilesForSearching = null; return; } var matched = new List(); foreach (var file in _worktreeFiles) { if (file.Contains(_searchCommitFilter, StringComparison.OrdinalIgnoreCase) && file.Length != _searchCommitFilter.Length) { matched.Add(file); if (matched.Count > 100) break; } } MatchedFilesForSearching = matched; } private void AutoFetchImpl(object sender) { try { if (!_settings.EnableAutoFetch || _isAutoFetching) return; var lockFile = Path.Combine(_gitDir, "index.lock"); if (File.Exists(lockFile)) return; var now = DateTime.Now; var desire = _lastFetchTime.AddMinutes(_settings.AutoFetchInterval); if (desire > now) return; var remotes = new List(); lock (_lockRemotes) { foreach (var remote in _remotes) remotes.Add(remote.Name); } Dispatcher.UIThread.Invoke(() => IsAutoFetching = true); foreach (var remote in remotes) new Commands.Fetch(_fullpath, remote, false, false) { RaiseError = false }.Exec(); _lastFetchTime = DateTime.Now; Dispatcher.UIThread.Invoke(() => IsAutoFetching = false); } catch { // DO nothing, but prevent `System.AggregateException` } } private string _fullpath = string.Empty; private string _gitDir = string.Empty; private Models.RepositorySettings _settings = null; private Models.FilterMode _historiesFilterMode = Models.FilterMode.None; private bool _hasAllowedSignersFile = false; private Models.Watcher _watcher = null; private Histories _histories = null; private WorkingCopy _workingCopy = null; private StashesPage _stashesPage = null; private int _selectedViewIndex = 0; private object _selectedView = null; private int _localChangesCount = 0; private int _stashesCount = 0; private bool _isSearching = false; private bool _isSearchLoadingVisible = false; private int _searchCommitFilterType = (int)Models.CommitSearchMethod.ByMessage; private bool _onlySearchCommitsInCurrentBranch = false; private string _searchCommitFilter = string.Empty; private List _searchedCommits = new List(); private Models.Commit _selectedSearchedCommit = null; private List _worktreeFiles = null; private List _matchedFilesForSearching = null; private string _filter = string.Empty; private object _lockRemotes = new object(); private List _remotes = new List(); private List _branches = new List(); private Models.Branch _currentBranch = null; private List _localBranchTrees = new List(); private List _remoteBranchTrees = new List(); private List _worktrees = new List(); private List _tags = new List(); private List _visibleTags = new List(); private List _submodules = new List(); private List _visibleSubmodules = new List(); private bool _isAutoFetching = false; private Timer _autoFetchTimer = null; private DateTime _lastFetchTime = DateTime.MinValue; private Models.BisectState _bisectState = Models.BisectState.None; private bool _isBisectCommandRunning = false; private string _navigateToBranchDelayed = string.Empty; } }