/
vasiukoff
/
sg
Обзор
Документация
Войти
/
vasiukoff
/
sg
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
src/ViewModels/Repository.cs
1 918 строк
63 KB
leo
code_review: PR #1962
08 дек 2025, 05:36
08 дек 2025, 05:36
f763060
Код
Авторство
О чём код?
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.Threading; using CommunityToolkit.Mvvm.ComponentModel; namespace SourceGit.ViewModels { public class Repository : ObservableObject, Models.IRepository { public bool IsBare { get; } public string FullPath { get; } public string GitDir { get; } public Models.RepositorySettings Settings { get => _settings; } public Models.GitFlow GitFlow { get; set; } = new(); public Models.HistoryFilterCollection HistoryFilterCollection { get => _historyFilterCollection; } public Models.FilterMode HistoryFilterMode { get => _historyFilterMode; private set => SetProperty(ref _historyFilterMode, value); } public bool HasAllowedSignersFile { get => _hasAllowedSignersFile; } public int SelectedViewIndex { get => _selectedViewIndex; set { if (SetProperty(ref _selectedViewIndex, value)) { SelectedView = value switch { 1 => _workingCopy, 2 => _stashesPage, _ => _histories, }; } } } public object SelectedView { get => _selectedView; set => SetProperty(ref _selectedView, value); } public bool EnableTopoOrderInHistories { get => _settings.EnableTopoOrderInHistories; set { if (value != _settings.EnableTopoOrderInHistories) { _settings.EnableTopoOrderInHistories = value; RefreshCommits(); } } } public Models.HistoryShowFlags HistoryShowFlags { get => _settings.HistoryShowFlags; private set { if (value != _settings.HistoryShowFlags) { _settings.HistoryShowFlags = value; RefreshCommits(); } } } public bool OnlyHighlightCurrentBranchInHistories { get => _settings.OnlyHighlightCurrentBranchInHistories; set { if (value != _settings.OnlyHighlightCurrentBranchInHistories) { _settings.OnlyHighlightCurrentBranchInHistories = 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<Models.Remote> Remotes { get => _remotes; private set => SetProperty(ref _remotes, value); } public List<Models.Branch> 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) && value != null) { if (oldHead != _currentBranch.Head && _workingCopy is { UseAmend: true }) _workingCopy.UseAmend = false; } } } public List<BranchTreeNode> LocalBranchTrees { get => _localBranchTrees; private set => SetProperty(ref _localBranchTrees, value); } public List<BranchTreeNode> RemoteBranchTrees { get => _remoteBranchTrees; private set => SetProperty(ref _remoteBranchTrees, value); } public List<Models.Worktree> Worktrees { get => _worktrees; private set => SetProperty(ref _worktrees, value); } public List<Models.Tag> Tags { get => _tags; private set => SetProperty(ref _tags, value); } public bool ShowTagsAsTree { get => Preferences.Instance.ShowTagsAsTree; set { if (value != Preferences.Instance.ShowTagsAsTree) { Preferences.Instance.ShowTagsAsTree = value; VisibleTags = BuildVisibleTags(); OnPropertyChanged(); } } } public object VisibleTags { get => _visibleTags; private set => SetProperty(ref _visibleTags, value); } public List<Models.Submodule> Submodules { get => _submodules; private set => SetProperty(ref _submodules, value); } public bool ShowSubmodulesAsTree { get => Preferences.Instance.ShowSubmodulesAsTree; set { if (value != Preferences.Instance.ShowSubmodulesAsTree) { Preferences.Instance.ShowSubmodulesAsTree = value; VisibleSubmodules = BuildVisibleSubmodules(); OnPropertyChanged(); } } } public object 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 int LocalBranchesCount { get => _localBranchesCount; private set => SetProperty(ref _localBranchesCount, value); } public bool IncludeUntracked { get => _settings.IncludeUntrackedInLocalChanges; set { if (value != _settings.IncludeUntrackedInLocalChanges) { _settings.IncludeUntrackedInLocalChanges = value; OnPropertyChanged(); RefreshWorkingCopyChanges(); } } } public bool IsSearchingCommits { get => _isSearchingCommits; set { if (SetProperty(ref _isSearchingCommits, value)) { if (value) SelectedViewIndex = 0; else _searchCommitContext.EndSearch(); } } } public SearchCommitContext SearchCommitContext { get => _searchCommitContext; } 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 bool IsSortingLocalBranchByName { get => _settings.LocalBranchSortMode == Models.BranchSortMode.Name; set { _settings.LocalBranchSortMode = value ? Models.BranchSortMode.Name : Models.BranchSortMode.CommitterDate; OnPropertyChanged(); var builder = BuildBranchTree(_branches, _remotes); LocalBranchTrees = builder.Locals; RemoteBranchTrees = builder.Remotes; } } public bool IsSortingRemoteBranchByName { get => _settings.RemoteBranchSortMode == Models.BranchSortMode.Name; set { _settings.RemoteBranchSortMode = value ? Models.BranchSortMode.Name : Models.BranchSortMode.CommitterDate; OnPropertyChanged(); var builder = BuildBranchTree(_branches, _remotes); LocalBranchTrees = builder.Locals; RemoteBranchTrees = builder.Remotes; } } public bool IsSortingTagsByName { get => _settings.TagSortMode == Models.TagSortMode.Name; set { _settings.TagSortMode = value ? Models.TagSortMode.Name : Models.TagSortMode.CreatorDate; OnPropertyChanged(); VisibleTags = BuildVisibleTags(); } } 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 AvaloniaList<Models.IssueTracker> IssueTrackers { get; } = []; public AvaloniaList<CommandLog> Logs { get; } = []; public Repository(bool isBare, string path, string gitDir) { IsBare = isBare; FullPath = path.Replace('\\', '/').TrimEnd('/'); GitDir = gitDir.Replace('\\', '/').TrimEnd('/'); var commonDirFile = Path.Combine(GitDir, "commondir"); _isWorktree = GitDir.IndexOf("/worktrees/", StringComparison.Ordinal) > 0 && File.Exists(commonDirFile); if (_isWorktree) { var commonDir = File.ReadAllText(commonDirFile).Trim(); if (!Path.IsPathRooted(commonDir)) commonDir = new DirectoryInfo(Path.Combine(GitDir, commonDir)).FullName; _gitCommonDir = commonDir; } else { _gitCommonDir = GitDir; } } public void Open() { var settingsFile = Path.Combine(_gitCommonDir, "sourcegit.settings"); if (File.Exists(settingsFile)) { try { using var stream = File.OpenRead(settingsFile); _settings = JsonSerializer.Deserialize(stream, JsonCodeGen.Default.RepositorySettings); } catch { _settings = new Models.RepositorySettings(); } } else { _settings = new Models.RepositorySettings(); } var historyFilterFile = Path.Combine(GitDir, "sourcegit.filters"); if (File.Exists(historyFilterFile)) { try { using var stream = File.OpenRead(historyFilterFile); _historyFilterCollection = JsonSerializer.Deserialize(stream, JsonCodeGen.Default.HistoryFilterCollection); } catch { _historyFilterCollection = new Models.HistoryFilterCollection(); } } else { _historyFilterCollection = new Models.HistoryFilterCollection(); } try { _watcher = new Models.Watcher(this, FullPath, _gitCommonDir); } 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}"); } _historyFilterMode = _historyFilterCollection.Mode; _histories = new Histories(this); _workingCopy = new WorkingCopy(this) { CommitMessage = _settings.LastCommitMessage }; _stashesPage = new StashesPage(this); _searchCommitContext = new SearchCommitContext(this); if (Preferences.Instance.ShowLocalChangesByDefault) { _selectedView = _workingCopy; _selectedViewIndex = 1; } else { _selectedView = _histories; _selectedViewIndex = 0; } _lastFetchTime = DateTime.Now; _autoFetchTimer = new Timer(AutoFetchByTimer, null, 5000, 5000); RefreshAll(); } public void Close() { SelectedView = null; // Do NOT modify. Used to remove exists widgets for GC.Collect Logs.Clear(); if (!_isWorktree && Directory.Exists(_gitCommonDir)) { try { if (_workingCopy.InProgressContext != null && !string.IsNullOrEmpty(_workingCopy.CommitMessage)) File.WriteAllText(Path.Combine(GitDir, "MERGE_MSG"), _workingCopy.CommitMessage); else _settings.LastCommitMessage = _workingCopy.CommitMessage; using var stream = File.Create(Path.Combine(_gitCommonDir, "sourcegit.settings")); JsonSerializer.Serialize(stream, _settings, JsonCodeGen.Default.RepositorySettings); } catch (Exception) { // Ignore } } if (Directory.Exists(GitDir)) { try { using var stream = File.Create(Path.Combine(GitDir, "sourcegit.filters")); JsonSerializer.Serialize(stream, _historyFilterCollection, JsonCodeGen.Default.HistoryFilterCollection); } catch { // Ignore } } if (_cancellationRefreshBranches is { IsCancellationRequested: false }) _cancellationRefreshBranches.Cancel(); if (_cancellationRefreshTags is { IsCancellationRequested: false }) _cancellationRefreshTags.Cancel(); if (_cancellationRefreshWorkingCopyChanges is { IsCancellationRequested: false }) _cancellationRefreshWorkingCopyChanges.Cancel(); if (_cancellationRefreshCommits is { IsCancellationRequested: false }) _cancellationRefreshCommits.Cancel(); if (_cancellationRefreshStashes is { IsCancellationRequested: false }) _cancellationRefreshStashes.Cancel(); _autoFetchTimer.Dispose(); _autoFetchTimer = null; _settings = null; _historyFilterCollection = null; _historyFilterMode = Models.FilterMode.None; _watcher?.Dispose(); _histories.Dispose(); _workingCopy.Dispose(); _stashesPage.Dispose(); _searchCommitContext.Dispose(); _watcher = null; _histories = null; _workingCopy = null; _stashesPage = null; _localChangesCount = 0; _stashesCount = 0; _remotes.Clear(); _branches.Clear(); _localBranchTrees.Clear(); _remoteBranchTrees.Clear(); _tags.Clear(); _visibleTags = null; _submodules.Clear(); _visibleSubmodules = 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 async Task ShowAndStartPopupAsync(Popup popup) { var page = GetOwnerPage(); page.Popup = popup; if (popup.CanStartDirectly()) await page.ProcessPopupAsync(); } public bool IsGitFlowEnabled() { return GitFlow is { IsValid: true } && _branches.Find(x => x.IsLocal && x.Name.Equals(GitFlow.Master, StringComparison.Ordinal)) != null && _branches.Find(x => x.IsLocal && x.Name.Equals(GitFlow.Develop, StringComparison.Ordinal)) != null; } public Models.GitFlowBranchType GetGitFlowType(Models.Branch b) { if (!IsGitFlowEnabled()) return Models.GitFlowBranchType.None; var name = b.Name; if (name.StartsWith(GitFlow.FeaturePrefix, StringComparison.Ordinal)) return Models.GitFlowBranchType.Feature; if (name.StartsWith(GitFlow.ReleasePrefix, StringComparison.Ordinal)) return Models.GitFlowBranchType.Release; if (name.StartsWith(GitFlow.HotfixPrefix, StringComparison.Ordinal)) return Models.GitFlowBranchType.Hotfix; return Models.GitFlowBranchType.None; } public bool IsLFSEnabled() { var path = Path.Combine(FullPath, ".git", "hooks", "pre-push"); if (!File.Exists(path)) return false; var content = File.ReadAllText(path); return content.Contains("git lfs pre-push"); } public async Task InstallLFSAsync() { var log = CreateLog("Install LFS"); var succ = await new Commands.LFS(FullPath).Use(log).InstallAsync(); if (succ) App.SendNotification(FullPath, "LFS enabled successfully!"); log.Complete(); } public async Task<bool> TrackLFSFileAsync(string pattern, bool isFilenameMode) { var log = CreateLog("Track LFS"); var succ = await new Commands.LFS(FullPath) .Use(log) .TrackAsync(pattern, isFilenameMode); if (succ) App.SendNotification(FullPath, $"Tracking successfully! Pattern: {pattern}"); log.Complete(); return succ; } public async Task<bool> LockLFSFileAsync(string remote, string path) { var log = CreateLog("Lock LFS File"); var succ = await new Commands.LFS(FullPath) .Use(log) .LockAsync(remote, path); if (succ) App.SendNotification(FullPath, $"Lock file successfully! File: {path}"); log.Complete(); return succ; } public async Task<bool> UnlockLFSFileAsync(string remote, string path, bool force, bool notify) { var log = CreateLog("Unlock LFS File"); var succ = await new Commands.LFS(FullPath) .Use(log) .UnlockAsync(remote, path, force); if (succ && notify) App.SendNotification(FullPath, $"Unlock file successfully! File: {path}"); log.Complete(); return succ; } public CommandLog CreateLog(string name) { var log = new CommandLog(name); Logs.Insert(0, log); return log; } public void RefreshAll() { RefreshCommits(); RefreshBranches(); RefreshTags(); RefreshSubmodules(); RefreshWorktrees(); RefreshWorkingCopyChanges(); RefreshStashes(); Task.Run(async () => { var issuetrackers = new List<Models.IssueTracker>(); await new Commands.IssueTracker(FullPath, true).ReadAllAsync(issuetrackers, true).ConfigureAwait(false); await new Commands.IssueTracker(FullPath, false).ReadAllAsync(issuetrackers, false).ConfigureAwait(false); Dispatcher.UIThread.Post(() => { IssueTrackers.Clear(); IssueTrackers.AddRange(issuetrackers); }); var config = await new Commands.Config(FullPath).ReadAllAsync().ConfigureAwait(false); _hasAllowedSignersFile = config.TryGetValue("gpg.ssh.allowedSignersFile", out var allowedSignersFile) && !string.IsNullOrEmpty(allowedSignersFile); if (config.TryGetValue("gitflow.branch.master", out var masterName)) GitFlow.Master = masterName; if (config.TryGetValue("gitflow.branch.develop", out var developName)) GitFlow.Develop = developName; if (config.TryGetValue("gitflow.prefix.feature", out var featurePrefix)) GitFlow.FeaturePrefix = featurePrefix; if (config.TryGetValue("gitflow.prefix.release", out var releasePrefix)) GitFlow.ReleasePrefix = releasePrefix; if (config.TryGetValue("gitflow.prefix.hotfix", out var hotfixPrefix)) GitFlow.HotfixPrefix = hotfixPrefix; }); } public async Task FetchAsync(bool autoStart) { if (!CanCreatePopup()) return; if (_remotes.Count == 0) { App.RaiseException(FullPath, "No remotes added to this repository!!!"); return; } if (autoStart) await ShowAndStartPopupAsync(new Fetch(this)); else ShowPopup(new Fetch(this)); } public async Task PullAsync(bool autoStart) { if (IsBare || !CanCreatePopup()) return; if (_remotes.Count == 0) { App.RaiseException(FullPath, "No remotes added to this repository!!!"); return; } if (_currentBranch == null) { App.RaiseException(FullPath, "Can NOT find current branch!!!"); return; } var pull = new Pull(this, null); if (autoStart && pull.SelectedBranch != null) await ShowAndStartPopupAsync(pull); else ShowPopup(pull); } public async Task PushAsync(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 find current branch!!!"); return; } if (autoStart) await ShowAndStartPopupAsync(new Push(this, null)); else ShowPopup(new Push(this, null)); } public void ApplyPatch() { if (CanCreatePopup()) ShowPopup(new Apply(this)); } public async Task ExecCustomActionAsync(Models.CustomAction action, object scopeTarget) { if (!CanCreatePopup()) return; var popup = new ExecuteCustomAction(this, action, scopeTarget); if (action.Controls.Count == 0) await ShowAndStartPopupAsync(popup); else ShowPopup(popup); } public async Task CleanupAsync() { if (CanCreatePopup()) await ShowAndStartPopupAsync(new Cleanup(this)); } public void ClearFilter() { Filter = string.Empty; } public IDisposable LockWatcher() { return _watcher?.Lock(); } public void MarkBranchesDirtyManually() { _watcher?.MarkBranchUpdated(); RefreshBranches(); RefreshCommits(); RefreshWorkingCopyChanges(); RefreshWorktrees(); } public void MarkTagsDirtyManually() { _watcher?.MarkTagUpdated(); RefreshTags(); RefreshCommits(); } public void MarkWorkingCopyDirtyManually() { _watcher?.MarkWorkingCopyUpdated(); RefreshWorkingCopyChanges(); } public void MarkStashesDirtyManually() { _watcher?.MarkStashUpdated(); RefreshStashes(); } public void MarkFetched() { _lastFetchTime = DateTime.Now; } public void NavigateToCommit(string sha, bool isDelayMode = false) { if (isDelayMode) { _navigateToCommitDelayed = sha; } else { SelectedViewIndex = 0; _histories?.NavigateTo(sha); } } public void ClearCommitMessage() { if (_workingCopy is not null) _workingCopy.CommitMessage = string.Empty; } public Models.Commit GetSelectedCommitInHistory() { return (_histories?.DetailContext as CommitDetail)?.Commit; } public void ClearHistoryFilters() { _historyFilterCollection.Filters.Clear(); HistoryFilterMode = Models.FilterMode.None; ResetBranchTreeFilterMode(LocalBranchTrees); ResetBranchTreeFilterMode(RemoteBranchTrees); ResetTagFilterMode(); RefreshCommits(); } public void RemoveHistoryFilter(Models.HistoryFilter filter) { if (_historyFilterCollection.Filters.Remove(filter)) { HistoryFilterMode = _historyFilterCollection.Mode; RefreshHistoryFilters(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 = _historyFilterCollection.Update(tag.Name, Models.FilterType.Tag, mode); if (changed) RefreshHistoryFilters(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) { _historyFilterCollection.Filters.Clear(); HistoryFilterMode = Models.FilterMode.None; } if (node.Backend is Models.Branch branch) { var type = isLocal ? Models.FilterType.LocalBranch : Models.FilterType.RemoteBranch; var changed = _historyFilterCollection.Update(node.Path, type, mode); if (!changed) return; if (isLocal && !string.IsNullOrEmpty(branch.Upstream) && !branch.IsUpstreamGone) _historyFilterCollection.Update(branch.Upstream, Models.FilterType.RemoteBranch, mode); } else { var type = isLocal ? Models.FilterType.LocalBranchFolder : Models.FilterType.RemoteBranchFolder; var changed = _historyFilterCollection.Update(node.Path, type, mode); if (!changed) return; _historyFilterCollection.RemoveBranchFiltersByPrefix(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; _historyFilterCollection.Update(parent.Path, parentType, Models.FilterMode.None); cur = parent; } while (true); RefreshHistoryFilters(refresh); } public async Task StashAllAsync(bool autoStart) { if (!CanCreatePopup()) return; var popup = new StashChanges(this, null); if (autoStart) await ShowAndStartPopupAsync(popup); else ShowPopup(popup); } public async Task SkipMergeAsync() { if (_workingCopy != null) await _workingCopy.SkipMergeAsync(); } public async Task AbortMergeAsync() { if (_workingCopy != null) await _workingCopy.AbortMergeAsync(); } public List<(Models.CustomAction, CustomActionContextMenuLabel)> GetCustomActions(Models.CustomActionScope scope) { var actions = new List<(Models.CustomAction, CustomActionContextMenuLabel)>(); foreach (var act in Preferences.Instance.CustomActions) { if (act.Scope == scope) actions.Add((act, new CustomActionContextMenuLabel(act.Name, true))); } foreach (var act in _settings.CustomActions) { if (act.Scope == scope) actions.Add((act, new CustomActionContextMenuLabel(act.Name, false))); } return actions; } public async Task ExecBisectCommandAsync(string subcmd) { using var lockWatcher = _watcher?.Lock(); IsBisectCommandRunning = true; var log = CreateLog($"Bisect({subcmd})"); var succ = await new Commands.Bisect(FullPath, subcmd).Use(log).ExecAsync(); log.Complete(); var head = await new Commands.QueryRevisionByRefName(FullPath, "HEAD").GetResultAsync(); 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(); NavigateToCommit(head, true); IsBisectCommandRunning = false; } public bool MayHaveSubmodules() { var modulesFile = Path.Combine(FullPath, ".gitmodules"); var info = new FileInfo(modulesFile); return info.Exists && info.Length > 20; } public void RefreshBranches() { if (_cancellationRefreshBranches is { IsCancellationRequested: false }) _cancellationRefreshBranches.Cancel(); _cancellationRefreshBranches = new CancellationTokenSource(); var token = _cancellationRefreshBranches.Token; Task.Run(async () => { var branches = await new Commands.QueryBranches(FullPath).GetResultAsync().ConfigureAwait(false); var remotes = await new Commands.QueryRemotes(FullPath).GetResultAsync().ConfigureAwait(false); var builder = BuildBranchTree(branches, remotes); Dispatcher.UIThread.Invoke(() => { if (token.IsCancellationRequested) return; Remotes = remotes; Branches = branches; CurrentBranch = branches.Find(x => x.IsCurrent); LocalBranchTrees = builder.Locals; RemoteBranchTrees = builder.Remotes; var localBranchesCount = 0; foreach (var b in branches) { if (b.IsLocal && !b.IsDetachedHead) localBranchesCount++; } LocalBranchesCount = localBranchesCount; if (_workingCopy != null) _workingCopy.HasRemotes = remotes.Count > 0; var hasPendingPullOrPush = CurrentBranch?.IsTrackStatusVisible ?? false; GetOwnerPage()?.ChangeDirtyState(Models.DirtyState.HasPendingPullOrPush, !hasPendingPullOrPush); }); }, token); } public void RefreshWorktrees() { Task.Run(async () => { var worktrees = await new Commands.Worktree(FullPath).ReadAllAsync().ConfigureAwait(false); if (worktrees.Count == 0) { Dispatcher.UIThread.Invoke(() => Worktrees = worktrees); return; } var cleaned = new List<Models.Worktree>(); foreach (var worktree in worktrees) { if (worktree.FullPath.Equals(FullPath, StringComparison.Ordinal) || worktree.FullPath.Equals(GitDir, StringComparison.Ordinal)) continue; cleaned.Add(worktree); } Dispatcher.UIThread.Invoke(() => Worktrees = cleaned); }); } public void RefreshTags() { if (_cancellationRefreshTags is { IsCancellationRequested: false }) _cancellationRefreshTags.Cancel(); _cancellationRefreshTags = new CancellationTokenSource(); var token = _cancellationRefreshTags.Token; Task.Run(async () => { var tags = await new Commands.QueryTags(FullPath).GetResultAsync().ConfigureAwait(false); Dispatcher.UIThread.Invoke(() => { if (token.IsCancellationRequested) return; Tags = tags; VisibleTags = BuildVisibleTags(); }); }, token); } public void RefreshCommits() { if (_cancellationRefreshCommits is { IsCancellationRequested: false }) _cancellationRefreshCommits.Cancel(); _cancellationRefreshCommits = new CancellationTokenSource(); var token = _cancellationRefreshCommits.Token; Task.Run(async () => { await Dispatcher.UIThread.InvokeAsync(() => _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.HistoryShowFlags.HasFlag(Models.HistoryShowFlags.Reflog)) builder.Append("--reflog "); if (_settings.HistoryShowFlags.HasFlag(Models.HistoryShowFlags.FirstParentOnly)) builder.Append("--first-parent "); if (_settings.HistoryShowFlags.HasFlag(Models.HistoryShowFlags.SimplifyByDecoration)) builder.Append("--simplify-by-decoration "); var filters = _historyFilterCollection.Build(); if (string.IsNullOrEmpty(filters)) builder.Append("--branches --remotes --tags HEAD"); else builder.Append(filters); var commits = await new Commands.QueryCommits(FullPath, builder.ToString()).GetResultAsync().ConfigureAwait(false); var graph = Models.CommitGraph.Parse(commits, _settings.HistoryShowFlags.HasFlag(Models.HistoryShowFlags.FirstParentOnly)); Dispatcher.UIThread.Invoke(() => { if (token.IsCancellationRequested) return; if (_histories != null) { _histories.IsLoading = false; _histories.Commits = commits; _histories.Graph = graph; BisectState = _histories.UpdateBisectInfo(); if (!string.IsNullOrEmpty(_navigateToCommitDelayed)) NavigateToCommit(_navigateToCommitDelayed); } _navigateToCommitDelayed = string.Empty; }); }, token); } public void RefreshSubmodules() { if (!MayHaveSubmodules()) { if (_submodules.Count > 0) { Dispatcher.UIThread.Invoke(() => { Submodules = []; VisibleSubmodules = BuildVisibleSubmodules(); }); } return; } Task.Run(async () => { var submodules = await new Commands.QuerySubmodules(FullPath).GetResultAsync().ConfigureAwait(false); Dispatcher.UIThread.Invoke(() => { bool hasChanged = _submodules.Count != submodules.Count; if (!hasChanged) { var old = new Dictionary<string, Models.Submodule>(); foreach (var module in _submodules) old.Add(module.Path, module); foreach (var module in submodules) { if (!old.TryGetValue(module.Path, out var exist)) { hasChanged = true; break; } hasChanged = !exist.SHA.Equals(module.SHA, StringComparison.Ordinal) || !exist.Branch.Equals(module.Branch, StringComparison.Ordinal) || !exist.URL.Equals(module.URL, StringComparison.Ordinal) || exist.Status != module.Status; if (hasChanged) break; } } if (hasChanged) { Submodules = submodules; VisibleSubmodules = BuildVisibleSubmodules(); } }); }); } public void RefreshWorkingCopyChanges() { if (IsBare) return; if (_cancellationRefreshWorkingCopyChanges is { IsCancellationRequested: false }) _cancellationRefreshWorkingCopyChanges.Cancel(); _cancellationRefreshWorkingCopyChanges = new CancellationTokenSource(); var token = _cancellationRefreshWorkingCopyChanges.Token; Task.Run(async () => { var changes = await new Commands.QueryLocalChanges(FullPath, _settings.IncludeUntrackedInLocalChanges) .GetResultAsync() .ConfigureAwait(false); if (_workingCopy == null || token.IsCancellationRequested) return; changes.Sort((l, r) => Models.NumericSort.Compare(l.Path, r.Path)); _workingCopy.SetData(changes, token); Dispatcher.UIThread.Invoke(() => { if (token.IsCancellationRequested) return; LocalChangesCount = changes.Count; OnPropertyChanged(nameof(InProgressContext)); GetOwnerPage()?.ChangeDirtyState(Models.DirtyState.HasLocalChanges, changes.Count == 0); }); }, token); } public void RefreshStashes() { if (IsBare) return; if (_cancellationRefreshStashes is { IsCancellationRequested: false }) _cancellationRefreshStashes.Cancel(); _cancellationRefreshStashes = new CancellationTokenSource(); var token = _cancellationRefreshStashes.Token; Task.Run(async () => { var stashes = await new Commands.QueryStashes(FullPath).GetResultAsync().ConfigureAwait(false); Dispatcher.UIThread.Invoke(() => { if (token.IsCancellationRequested) return; if (_stashesPage != null) _stashesPage.Stashes = stashes; StashesCount = stashes.Count; }); }, token); } public void ToggleHistoryShowFlag(Models.HistoryShowFlags flag) { if (_settings.HistoryShowFlags.HasFlag(flag)) HistoryShowFlags -= flag; else HistoryShowFlags |= flag; } public void CreateNewBranch() { if (_currentBranch == null) { App.RaiseException(FullPath, "Git cannot create a branch before your first commit."); return; } if (CanCreatePopup()) ShowPopup(new CreateBranch(this, _currentBranch)); } public async Task CheckoutBranchAsync(Models.Branch branch) { if (branch.IsLocal) { var worktree = _worktrees.Find(x => x.Branch.Equals(branch.FullName, StringComparison.Ordinal)); if (worktree != null) { OpenWorktree(worktree); return; } } if (IsBare) return; if (!CanCreatePopup()) return; if (branch.IsLocal) { if (_localChangesCount > 0 || _submodules.Count > 0) ShowPopup(new Checkout(this, branch.Name)); else await ShowAndStartPopupAsync(new Checkout(this, branch.Name)); } else { foreach (var b in _branches) { if (b.IsLocal && b.Upstream.Equals(branch.FullName, StringComparison.Ordinal) && b.Ahead.Count == 0) { if (b.Behind.Count > 0) ShowPopup(new CheckoutAndFastForward(this, b, branch)); else if (!b.IsCurrent) await CheckoutBranchAsync(b); return; } } ShowPopup(new CreateBranch(this, branch)); } } public async Task CheckoutTagAsync(Models.Tag tag) { var c = await new Commands.QuerySingleCommit(FullPath, tag.SHA).GetResultAsync(); if (c != null && _histories != null) await _histories.CheckoutBranchByCommitAsync(c); } public async Task CompareBranchWithWorktreeAsync(Models.Branch branch) { if (_histories != null) { _searchCommitContext.Selected = null; var target = await new Commands.QuerySingleCommit(FullPath, branch.Head).GetResultAsync(); _histories.AutoSelectedCommit = null; _histories.DetailContext = new RevisionCompare(FullPath, target, null); } } public void DeleteBranch(Models.Branch branch) { if (CanCreatePopup()) ShowPopup(new DeleteBranch(this, branch)); } public void DeleteMultipleBranches(List<Models.Branch> branches, bool isLocal) { if (CanCreatePopup()) ShowPopup(new DeleteMultipleBranches(this, branches, isLocal)); } public void MergeMultipleBranches(List<Models.Branch> branches) { if (CanCreatePopup()) ShowPopup(new MergeMultiple(this, branches)); } public void CreateNewTag() { if (_currentBranch == null) { App.RaiseException(FullPath, "Git cannot create a branch before your first commit."); return; } if (CanCreatePopup()) ShowPopup(new CreateTag(this, _currentBranch)); } public void DeleteTag(Models.Tag tag) { if (CanCreatePopup()) ShowPopup(new DeleteTag(this, tag)); } public void AddRemote() { if (CanCreatePopup()) ShowPopup(new AddRemote(this)); } public void DeleteRemote(Models.Remote remote) { if (CanCreatePopup()) ShowPopup(new DeleteRemote(this, remote)); } public void AddSubmodule() { if (CanCreatePopup()) ShowPopup(new AddSubmodule(this)); } public void UpdateSubmodules() { if (CanCreatePopup()) ShowPopup(new UpdateSubmodules(this, null)); } public void OpenSubmodule(string submodule) { var selfPage = GetOwnerPage(); if (selfPage == null) return; var root = Path.GetFullPath(Path.Combine(FullPath, submodule)); var normalizedPath = root.Replace('\\', '/').TrimEnd('/'); var node = Preferences.Instance.FindNode(normalizedPath) ?? new RepositoryNode { Id = normalizedPath, Name = Path.GetFileName(normalizedPath), Bookmark = selfPage.Node.Bookmark, IsRepository = true, }; App.GetLauncher().OpenRepositoryInTab(node, null); } public void AddWorktree() { if (CanCreatePopup()) ShowPopup(new AddWorktree(this)); } public async Task PruneWorktreesAsync() { if (CanCreatePopup()) await ShowAndStartPopupAsync(new PruneWorktrees(this)); } public void OpenWorktree(Models.Worktree worktree) { var node = Preferences.Instance.FindNode(worktree.FullPath) ?? new RepositoryNode { Id = worktree.FullPath, Name = Path.GetFileName(worktree.FullPath), Bookmark = 0, IsRepository = true, }; App.GetLauncher().OpenRepositoryInTab(node, null); } public async Task LockWorktreeAsync(Models.Worktree worktree) { using var lockWatcher = _watcher?.Lock(); var log = CreateLog("Lock Worktree"); var succ = await new Commands.Worktree(FullPath).Use(log).LockAsync(worktree.FullPath); if (succ) worktree.IsLocked = true; log.Complete(); } public async Task UnlockWorktreeAsync(Models.Worktree worktree) { using var lockWatcher = _watcher?.Lock(); var log = CreateLog("Unlock Worktree"); var succ = await new Commands.Worktree(FullPath).Use(log).UnlockAsync(worktree.FullPath); if (succ) worktree.IsLocked = false; log.Complete(); } public List<Models.OpenAIService> GetPreferredOpenAIServices() { var services = Preferences.Instance.OpenAIServices; if (services == null || services.Count == 0) return []; if (services.Count == 1) return [services[0]]; var preferred = _settings.PreferredOpenAIService; var all = new List<Models.OpenAIService>(); foreach (var service in services) { if (service.Name.Equals(preferred, StringComparison.Ordinal)) return [service]; all.Add(service); } return all; } public void DiscardAllChanges() { if (CanCreatePopup()) ShowPopup(new Discard(this)); } public void ClearStashes() { if (CanCreatePopup()) ShowPopup(new ClearStashes(this)); } public async Task<bool> SaveCommitAsPatchAsync(Models.Commit commit, string folder, int index = 0) { var ignoredChars = new HashSet<char> { '/', '\\', ':', ',', '*', '?', '\"', '<', '>', '|', '`', '$', '^', '%', '[', ']', '+', '-' }; var builder = new StringBuilder(); builder.Append(index.ToString("D4")); builder.Append('-'); var chars = commit.Subject.ToCharArray(); var len = 0; foreach (var c in chars) { if (!ignoredChars.Contains(c)) { if (c == ' ' || c == '\t') builder.Append('-'); else builder.Append(c); len++; if (len >= 48) break; } } builder.Append(".patch"); var saveTo = Path.Combine(folder, builder.ToString()); var log = CreateLog("Save Commit as Patch"); var succ = await new Commands.FormatPatch(FullPath, commit.SHA, saveTo).Use(log).ExecAsync(); log.Complete(); return succ; } private LauncherPage GetOwnerPage() { var launcher = App.GetLauncher(); 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<Models.Branch> branches, List<Models.Remote> remotes) { var builder = new BranchTreeNode.Builder(_settings.LocalBranchSortMode, _settings.RemoteBranchSortMode); 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<Models.Branch>(); foreach (var b in branches) { if (b.FullName.Contains(_filter, StringComparison.OrdinalIgnoreCase)) visibles.Add(b); } builder.Run(visibles, remotes, true); } var filterMap = _historyFilterCollection.ToMap(); UpdateBranchTreeFilterMode(builder.Locals, filterMap); UpdateBranchTreeFilterMode(builder.Remotes, filterMap); return builder; } private object BuildVisibleTags() { switch (_settings.TagSortMode) { case Models.TagSortMode.CreatorDate: _tags.Sort((l, r) => r.CreatorDate.CompareTo(l.CreatorDate)); break; default: _tags.Sort((l, r) => Models.NumericSort.Compare(l.Name, r.Name)); break; } var visible = new List<Models.Tag>(); if (string.IsNullOrEmpty(_filter)) { visible.AddRange(_tags); } else { foreach (var t in _tags) { if (t.Name.Contains(_filter, StringComparison.OrdinalIgnoreCase)) visible.Add(t); } } var filterMap = _historyFilterCollection.ToMap(); UpdateTagFilterMode(filterMap); if (Preferences.Instance.ShowTagsAsTree) { var tree = TagCollectionAsTree.Build(visible, _visibleTags as TagCollectionAsTree); foreach (var node in tree.Tree) node.UpdateFilterMode(filterMap); return tree; } else { var list = new TagCollectionAsList(visible); foreach (var item in list.TagItems) item.FilterMode = filterMap.GetValueOrDefault(item.Tag.Name, Models.FilterMode.None); return list; } } private object BuildVisibleSubmodules() { var visible = new List<Models.Submodule>(); if (string.IsNullOrEmpty(_filter)) { visible.AddRange(_submodules); } else { foreach (var s in _submodules) { if (s.Path.Contains(_filter, StringComparison.OrdinalIgnoreCase)) visible.Add(s); } } if (Preferences.Instance.ShowSubmodulesAsTree) return SubmoduleCollectionAsTree.Build(visible, _visibleSubmodules as SubmoduleCollectionAsTree); else return new SubmoduleCollectionAsList() { Submodules = visible }; } private void RefreshHistoryFilters(bool refresh) { HistoryFilterMode = _historyFilterCollection.Mode; if (!refresh) return; var map = _historyFilterCollection.ToMap(); UpdateBranchTreeFilterMode(LocalBranchTrees, map); UpdateBranchTreeFilterMode(RemoteBranchTrees, map); UpdateTagFilterMode(map); RefreshCommits(); } private void UpdateBranchTreeFilterMode(List<BranchTreeNode> nodes, Dictionary<string, Models.FilterMode> map) { foreach (var node in nodes) { node.FilterMode = map.GetValueOrDefault(node.Path, Models.FilterMode.None); if (!node.IsBranch) UpdateBranchTreeFilterMode(node.Children, map); } } private void UpdateTagFilterMode(Dictionary<string, Models.FilterMode> map) { if (VisibleTags is TagCollectionAsTree tree) { foreach (var node in tree.Tree) node.UpdateFilterMode(map); } else if (VisibleTags is TagCollectionAsList list) { foreach (var item in list.TagItems) item.FilterMode = map.GetValueOrDefault(item.Tag.Name, Models.FilterMode.None); } } private void ResetBranchTreeFilterMode(List<BranchTreeNode> nodes) { foreach (var node in nodes) { node.FilterMode = Models.FilterMode.None; if (!node.IsBranch) ResetBranchTreeFilterMode(node.Children); } } private void ResetTagFilterMode() { if (VisibleTags is TagCollectionAsTree tree) { var filters = new Dictionary<string, Models.FilterMode>(); foreach (var node in tree.Tree) node.UpdateFilterMode(filters); } else if (VisibleTags is TagCollectionAsList list) { foreach (var item in list.TagItems) item.FilterMode = Models.FilterMode.None; } } private BranchTreeNode FindBranchNode(List<BranchTreeNode> nodes, string path) { if (string.IsNullOrEmpty(path)) return null; 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 AutoFetchByTimer(object sender) { Dispatcher.UIThread.Invoke(AutoFetchOnUIThread); } private async Task AutoFetchOnUIThread() { try { if (_settings is not { EnableAutoFetch: true }) return; if (!CanCreatePopup()) { _lastFetchTime = DateTime.Now; 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<string>(); foreach (var r in _remotes) remotes.Add(r.Name); IsAutoFetching = true; if (_settings.FetchAllRemotes) { foreach (var remote in remotes) await new Commands.Fetch(FullPath, remote, false, false) { RaiseError = false }.RunAsync(); } else if (remotes.Count > 0) { var remote = string.IsNullOrEmpty(_settings.DefaultRemote) ? remotes.Find(x => x.Equals(_settings.DefaultRemote, StringComparison.Ordinal)) : remotes[0]; await new Commands.Fetch(FullPath, remote, false, false) { RaiseError = false }.RunAsync(); } _lastFetchTime = DateTime.Now; IsAutoFetching = false; } catch { // Ignore all exceptions. } } private readonly bool _isWorktree = false; private readonly string _gitCommonDir = null; private Models.RepositorySettings _settings = null; private Models.HistoryFilterCollection _historyFilterCollection = null; private Models.FilterMode _historyFilterMode = 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 _localBranchesCount = 0; private int _localChangesCount = 0; private int _stashesCount = 0; private bool _isSearchingCommits = false; private SearchCommitContext _searchCommitContext = null; private string _filter = string.Empty; private List<Models.Remote> _remotes = []; private List<Models.Branch> _branches = []; private Models.Branch _currentBranch = null; private List<BranchTreeNode> _localBranchTrees = []; private List<BranchTreeNode> _remoteBranchTrees = []; private List<Models.Worktree> _worktrees = []; private List<Models.Tag> _tags = []; private object _visibleTags = null; private List<Models.Submodule> _submodules = []; private object _visibleSubmodules = null; private string _navigateToCommitDelayed = string.Empty; 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 CancellationTokenSource _cancellationRefreshBranches = null; private CancellationTokenSource _cancellationRefreshTags = null; private CancellationTokenSource _cancellationRefreshWorkingCopyChanges = null; private CancellationTokenSource _cancellationRefreshCommits = null; private CancellationTokenSource _cancellationRefreshStashes = null; } }