sourcegit/src/ViewModels/WorkingCopy.cs
2025-02-25 10:53:42 +08:00

1730 lines
66 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Platform.Storage;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
namespace SourceGit.ViewModels
{
public class WorkingCopy : ObservableObject
{
public bool IncludeUntracked
{
get => _repo.IncludeUntracked;
set
{
if (_repo.IncludeUntracked != value)
{
_repo.IncludeUntracked = value;
OnPropertyChanged();
}
}
}
public bool HasRemotes
{
get => _hasRemotes;
set => SetProperty(ref _hasRemotes, value);
}
public bool HasUnsolvedConflicts
{
get => _hasUnsolvedConflicts;
set => SetProperty(ref _hasUnsolvedConflicts, value);
}
public InProgressContext InProgressContext
{
get => _inProgressContext;
private set => SetProperty(ref _inProgressContext, value);
}
public bool IsStaging
{
get => _isStaging;
private set => SetProperty(ref _isStaging, value);
}
public bool IsUnstaging
{
get => _isUnstaging;
private set => SetProperty(ref _isUnstaging, value);
}
public bool IsCommitting
{
get => _isCommitting;
private set => SetProperty(ref _isCommitting, value);
}
public bool EnableSignOff
{
get => _repo.Settings.EnableSignOffForCommit;
set => _repo.Settings.EnableSignOffForCommit = value;
}
public bool UseAmend
{
get => _useAmend;
set
{
if (SetProperty(ref _useAmend, value))
{
if (value)
{
var currentBranch = _repo.CurrentBranch;
if (currentBranch == null)
{
App.RaiseException(_repo.FullPath, "No commits to amend!!!");
_useAmend = false;
OnPropertyChanged();
return;
}
CommitMessage = new Commands.QueryCommitFullMessage(_repo.FullPath, currentBranch.Head).Result();
}
else
{
CommitMessage = string.Empty;
}
Staged = GetStagedChanges();
SelectedStaged = [];
}
}
}
public string UnstagedFilter
{
get => _unstagedFilter;
set
{
if (SetProperty(ref _unstagedFilter, value))
{
if (_isLoadingData)
return;
VisibleUnstaged = GetVisibleUnstagedChanges(_unstaged);
SelectedUnstaged = [];
}
}
}
public List<Models.Change> Unstaged
{
get => _unstaged;
private set => SetProperty(ref _unstaged, value);
}
public List<Models.Change> VisibleUnstaged
{
get => _visibleUnstaged;
private set => SetProperty(ref _visibleUnstaged, value);
}
public List<Models.Change> Staged
{
get => _staged;
private set => SetProperty(ref _staged, value);
}
public List<Models.Change> SelectedUnstaged
{
get => _selectedUnstaged;
set
{
if (SetProperty(ref _selectedUnstaged, value))
{
if (value == null || value.Count == 0)
{
if (_selectedStaged == null || _selectedStaged.Count == 0)
SetDetail(null, true);
}
else
{
if (_selectedStaged != null && _selectedStaged.Count > 0)
SelectedStaged = [];
if (value.Count == 1)
SetDetail(value[0], true);
else
SetDetail(null, true);
}
}
}
}
public List<Models.Change> SelectedStaged
{
get => _selectedStaged;
set
{
if (SetProperty(ref _selectedStaged, value))
{
if (value == null || value.Count == 0)
{
if (_selectedUnstaged == null || _selectedUnstaged.Count == 0)
SetDetail(null, false);
}
else
{
if (_selectedUnstaged != null && _selectedUnstaged.Count > 0)
SelectedUnstaged = [];
if (value.Count == 1)
SetDetail(value[0], false);
else
SetDetail(null, false);
}
}
}
}
public object DetailContext
{
get => _detailContext;
private set => SetProperty(ref _detailContext, value);
}
public string CommitMessage
{
get => _commitMessage;
set => SetProperty(ref _commitMessage, value);
}
public WorkingCopy(Repository repo)
{
_repo = repo;
}
public void Cleanup()
{
_repo = null;
_inProgressContext = null;
_selectedUnstaged.Clear();
OnPropertyChanged(nameof(SelectedUnstaged));
_selectedStaged.Clear();
OnPropertyChanged(nameof(SelectedStaged));
_visibleUnstaged.Clear();
OnPropertyChanged(nameof(VisibleUnstaged));
_unstaged.Clear();
OnPropertyChanged(nameof(Unstaged));
_staged.Clear();
OnPropertyChanged(nameof(Staged));
_detailContext = null;
_commitMessage = string.Empty;
}
public void SetData(List<Models.Change> changes)
{
if (!IsChanged(_cached, changes))
{
// Just force refresh selected changes.
Dispatcher.UIThread.Invoke(() =>
{
HasUnsolvedConflicts = _cached.Find(x => x.IsConflit) != null;
UpdateDetail();
UpdateInProgressState();
});
return;
}
_cached = changes;
_count = _cached.Count;
var lastSelectedUnstaged = new HashSet<string>();
var lastSelectedStaged = new HashSet<string>();
if (_selectedUnstaged != null && _selectedUnstaged.Count > 0)
{
foreach (var c in _selectedUnstaged)
lastSelectedUnstaged.Add(c.Path);
}
else if (_selectedStaged != null && _selectedStaged.Count > 0)
{
foreach (var c in _selectedStaged)
lastSelectedStaged.Add(c.Path);
}
var unstaged = new List<Models.Change>();
var hasConflict = false;
foreach (var c in changes)
{
if (c.WorkTree != Models.ChangeState.None)
{
unstaged.Add(c);
hasConflict |= c.IsConflit;
}
}
var visibleUnstaged = GetVisibleUnstagedChanges(unstaged);
var selectedUnstaged = new List<Models.Change>();
foreach (var c in visibleUnstaged)
{
if (lastSelectedUnstaged.Contains(c.Path))
selectedUnstaged.Add(c);
}
var staged = GetStagedChanges();
var selectedStaged = new List<Models.Change>();
foreach (var c in staged)
{
if (lastSelectedStaged.Contains(c.Path))
selectedStaged.Add(c);
}
Dispatcher.UIThread.Invoke(() =>
{
_isLoadingData = true;
HasUnsolvedConflicts = hasConflict;
VisibleUnstaged = visibleUnstaged;
Unstaged = unstaged;
Staged = staged;
SelectedUnstaged = selectedUnstaged;
SelectedStaged = selectedStaged;
_isLoadingData = false;
UpdateDetail();
UpdateInProgressState();
});
}
public void OpenAssumeUnchanged()
{
App.OpenDialog(new Views.AssumeUnchangedManager()
{
DataContext = new AssumeUnchangedManager(_repo.FullPath)
});
}
public void StashAll(bool autoStart)
{
if (!_repo.CanCreatePopup())
return;
if (autoStart)
_repo.ShowAndStartPopup(new StashChanges(_repo, _cached, false));
else
_repo.ShowPopup(new StashChanges(_repo, _cached, false));
}
public void StageSelected(Models.Change next)
{
StageChanges(_selectedUnstaged, next);
}
public void StageAll()
{
StageChanges(_visibleUnstaged, null);
}
public void UnstageSelected(Models.Change next)
{
UnstageChanges(_selectedStaged, next);
}
public void UnstageAll()
{
UnstageChanges(_staged, null);
}
public void Discard(List<Models.Change> changes)
{
if (_repo.CanCreatePopup())
_repo.ShowPopup(new Discard(_repo, changes));
}
public void ClearUnstagedFilter()
{
UnstagedFilter = string.Empty;
}
public async void UseTheirs(List<Models.Change> changes)
{
_repo.SetWatcherEnabled(false);
var files = new List<string>();
var needStage = new List<string>();
foreach (var change in changes)
{
if (!change.IsConflit)
continue;
if (change.WorkTree == Models.ChangeState.Deleted)
{
var fullpath = Path.Combine(_repo.FullPath, change.Path);
if (File.Exists(fullpath))
File.Delete(fullpath);
needStage.Add(change.Path);
}
else
{
files.Add(change.Path);
}
}
if (files.Count > 0)
{
var succ = await Task.Run(() => new Commands.Checkout(_repo.FullPath).UseTheirs(files));
if (succ)
needStage.AddRange(files);
}
if (needStage.Count > 0)
await Task.Run(() => new Commands.Add(_repo.FullPath, needStage).Exec());
_repo.MarkWorkingCopyDirtyManually();
_repo.SetWatcherEnabled(true);
}
public async void UseMine(List<Models.Change> changes)
{
_repo.SetWatcherEnabled(false);
var files = new List<string>();
var needStage = new List<string>();
foreach (var change in changes)
{
if (!change.IsConflit)
continue;
if (change.Index == Models.ChangeState.Deleted)
{
var fullpath = Path.Combine(_repo.FullPath, change.Path);
if (File.Exists(fullpath))
File.Delete(fullpath);
needStage.Add(change.Path);
}
else
{
files.Add(change.Path);
}
}
if (files.Count > 0)
{
var succ = await Task.Run(() => new Commands.Checkout(_repo.FullPath).UseMine(files));
if (succ)
needStage.AddRange(files);
}
if (needStage.Count > 0)
await Task.Run(() => new Commands.Add(_repo.FullPath, needStage).Exec());
_repo.MarkWorkingCopyDirtyManually();
_repo.SetWatcherEnabled(true);
}
public async void UseExternalMergeTool(Models.Change change)
{
var toolType = Preferences.Instance.ExternalMergeToolType;
var toolPath = Preferences.Instance.ExternalMergeToolPath;
await Task.Run(() => Commands.MergeTool.OpenForMerge(_repo.FullPath, toolType, toolPath, change.Path));
}
public void ContinueMerge()
{
if (_inProgressContext != null)
{
_repo.SetWatcherEnabled(false);
Task.Run(() =>
{
var succ = _inProgressContext.Continue();
Dispatcher.UIThread.Invoke(() =>
{
if (succ)
CommitMessage = string.Empty;
_repo.SetWatcherEnabled(true);
});
});
}
else
{
_repo.MarkWorkingCopyDirtyManually();
}
}
public void SkipMerge()
{
if (_inProgressContext != null)
{
_repo.SetWatcherEnabled(false);
Task.Run(() =>
{
var succ = _inProgressContext.Skip();
Dispatcher.UIThread.Invoke(() =>
{
if (succ)
CommitMessage = string.Empty;
_repo.SetWatcherEnabled(true);
});
});
}
else
{
_repo.MarkWorkingCopyDirtyManually();
}
}
public void AbortMerge()
{
if (_inProgressContext != null)
{
_repo.SetWatcherEnabled(false);
Task.Run(() =>
{
var succ = _inProgressContext.Abort();
Dispatcher.UIThread.Invoke(() =>
{
if (succ)
CommitMessage = string.Empty;
_repo.SetWatcherEnabled(true);
});
});
}
else
{
_repo.MarkWorkingCopyDirtyManually();
}
}
public void Commit()
{
DoCommit(false, false, false);
}
public void CommitWithAutoStage()
{
DoCommit(true, false, false);
}
public void CommitWithPush()
{
DoCommit(false, true, false);
}
public void CommitWithoutFiles(bool autoPush)
{
DoCommit(false, autoPush, true);
}
public ContextMenu CreateContextMenuForUnstagedChanges()
{
if (_selectedUnstaged == null || _selectedUnstaged.Count == 0)
return null;
var menu = new ContextMenu();
if (_selectedUnstaged.Count == 1)
{
var change = _selectedUnstaged[0];
var path = Path.GetFullPath(Path.Combine(_repo.FullPath, change.Path));
var explore = new MenuItem();
explore.Header = App.Text("RevealFile");
explore.Icon = App.CreateMenuIcon("Icons.Explore");
explore.IsEnabled = File.Exists(path) || Directory.Exists(path);
explore.Click += (_, e) =>
{
Native.OS.OpenInFileManager(path, true);
e.Handled = true;
};
menu.Items.Add(explore);
var openWith = new MenuItem();
openWith.Header = App.Text("OpenWith");
openWith.Icon = App.CreateMenuIcon("Icons.OpenWith");
openWith.IsEnabled = File.Exists(path);
openWith.Click += (_, e) =>
{
Native.OS.OpenWithDefaultEditor(path);
e.Handled = true;
};
menu.Items.Add(openWith);
menu.Items.Add(new MenuItem() { Header = "-" });
if (change.IsConflit)
{
var useTheirs = new MenuItem();
useTheirs.Icon = App.CreateMenuIcon("Icons.Incoming");
useTheirs.Header = App.Text("FileCM.UseTheirs");
useTheirs.Click += (_, e) =>
{
UseTheirs(_selectedUnstaged);
e.Handled = true;
};
var useMine = new MenuItem();
useMine.Icon = App.CreateMenuIcon("Icons.Local");
useMine.Header = App.Text("FileCM.UseMine");
useMine.Click += (_, e) =>
{
UseMine(_selectedUnstaged);
e.Handled = true;
};
var openMerger = new MenuItem();
openMerger.Icon = App.CreateMenuIcon("Icons.OpenWith");
openMerger.Header = App.Text("FileCM.OpenWithExternalMerger");
openMerger.Click += (_, e) =>
{
UseExternalMergeTool(change);
e.Handled = true;
};
if (_inProgressContext is CherryPickInProgress cherryPick)
{
useTheirs.Header = new Views.NameHighlightedTextBlock("FileCM.ResolveUsing", cherryPick.HeadName);
useMine.Header = new Views.NameHighlightedTextBlock("FileCM.ResolveUsing", _repo.CurrentBranch.Name);
}
else if (_inProgressContext is RebaseInProgress rebase)
{
useTheirs.Header = new Views.NameHighlightedTextBlock("FileCM.ResolveUsing", rebase.HeadName);
useMine.Header = new Views.NameHighlightedTextBlock("FileCM.ResolveUsing", rebase.BaseName);
}
else if (_inProgressContext is RevertInProgress revert)
{
useTheirs.Header = new Views.NameHighlightedTextBlock("FileCM.ResolveUsing", revert.Head.SHA.Substring(0, 10) + " (revert)");
useMine.Header = new Views.NameHighlightedTextBlock("FileCM.ResolveUsing", _repo.CurrentBranch.Name);
}
else if (_inProgressContext is MergeInProgress merge)
{
useTheirs.Header = new Views.NameHighlightedTextBlock("FileCM.ResolveUsing", merge.SourceName);
useMine.Header = new Views.NameHighlightedTextBlock("FileCM.ResolveUsing", _repo.CurrentBranch.Name);
}
menu.Items.Add(useTheirs);
menu.Items.Add(useMine);
menu.Items.Add(new MenuItem() { Header = "-" });
menu.Items.Add(openMerger);
menu.Items.Add(new MenuItem() { Header = "-" });
}
else
{
var stage = new MenuItem();
stage.Header = App.Text("FileCM.Stage");
stage.Icon = App.CreateMenuIcon("Icons.File.Add");
stage.Click += (_, e) =>
{
StageChanges(_selectedUnstaged, null);
e.Handled = true;
};
var discard = new MenuItem();
discard.Header = App.Text("FileCM.Discard");
discard.Icon = App.CreateMenuIcon("Icons.Undo");
discard.Click += (_, e) =>
{
Discard(_selectedUnstaged);
e.Handled = true;
};
var stash = new MenuItem();
stash.Header = App.Text("FileCM.Stash");
stash.Icon = App.CreateMenuIcon("Icons.Stashes.Add");
stash.Click += (_, e) =>
{
if (_repo.CanCreatePopup())
_repo.ShowPopup(new StashChanges(_repo, _selectedUnstaged, true));
e.Handled = true;
};
var patch = new MenuItem();
patch.Header = App.Text("FileCM.SaveAsPatch");
patch.Icon = App.CreateMenuIcon("Icons.Diff");
patch.Click += async (_, e) =>
{
var storageProvider = App.GetStorageProvider();
if (storageProvider == null)
return;
var options = new FilePickerSaveOptions();
options.Title = App.Text("FileCM.SaveAsPatch");
options.DefaultExtension = ".patch";
options.FileTypeChoices = [new FilePickerFileType("Patch File") { Patterns = ["*.patch"] }];
var storageFile = await storageProvider.SaveFilePickerAsync(options);
if (storageFile != null)
{
var succ = await Task.Run(() => Commands.SaveChangesAsPatch.ProcessLocalChanges(_repo.FullPath, _selectedUnstaged, true, storageFile.Path.LocalPath));
if (succ)
App.SendNotification(_repo.FullPath, App.Text("SaveAsPatchSuccess"));
}
e.Handled = true;
};
var assumeUnchanged = new MenuItem();
assumeUnchanged.Header = App.Text("FileCM.AssumeUnchanged");
assumeUnchanged.Icon = App.CreateMenuIcon("Icons.File.Ignore");
assumeUnchanged.IsVisible = change.WorkTree != Models.ChangeState.Untracked;
assumeUnchanged.Click += (_, e) =>
{
new Commands.AssumeUnchanged(_repo.FullPath).Add(change.Path);
e.Handled = true;
};
var history = new MenuItem();
history.Header = App.Text("FileHistory");
history.Icon = App.CreateMenuIcon("Icons.Histories");
history.Click += (_, e) =>
{
var window = new Views.FileHistories() { DataContext = new FileHistories(_repo, change.Path) };
window.Show();
e.Handled = true;
};
menu.Items.Add(stage);
menu.Items.Add(discard);
menu.Items.Add(stash);
menu.Items.Add(patch);
menu.Items.Add(assumeUnchanged);
menu.Items.Add(new MenuItem() { Header = "-" });
menu.Items.Add(history);
menu.Items.Add(new MenuItem() { Header = "-" });
var extension = Path.GetExtension(change.Path);
var hasExtra = false;
if (change.WorkTree == Models.ChangeState.Untracked)
{
var isRooted = change.Path.IndexOf('/', StringComparison.Ordinal) <= 0;
var addToIgnore = new MenuItem();
addToIgnore.Header = App.Text("WorkingCopy.AddToGitIgnore");
addToIgnore.Icon = App.CreateMenuIcon("Icons.GitIgnore");
var singleFile = new MenuItem();
singleFile.Header = App.Text("WorkingCopy.AddToGitIgnore.SingleFile");
singleFile.Click += (_, e) =>
{
Commands.GitIgnore.Add(_repo.FullPath, change.Path);
e.Handled = true;
};
addToIgnore.Items.Add(singleFile);
var byParentFolder = new MenuItem();
byParentFolder.Header = App.Text("WorkingCopy.AddToGitIgnore.InSameFolder");
byParentFolder.IsVisible = !isRooted;
byParentFolder.Click += (_, e) =>
{
var path = Path.GetDirectoryName(change.Path).Replace("\\", "/");
Commands.GitIgnore.Add(_repo.FullPath, path + "/");
e.Handled = true;
};
addToIgnore.Items.Add(byParentFolder);
if (!string.IsNullOrEmpty(extension))
{
var byExtension = new MenuItem();
byExtension.Header = App.Text("WorkingCopy.AddToGitIgnore.Extension", extension);
byExtension.Click += (_, e) =>
{
Commands.GitIgnore.Add(_repo.FullPath, "*" + extension);
e.Handled = true;
};
addToIgnore.Items.Add(byExtension);
var byExtensionInSameFolder = new MenuItem();
byExtensionInSameFolder.Header = App.Text("WorkingCopy.AddToGitIgnore.ExtensionInSameFolder", extension);
byExtensionInSameFolder.IsVisible = !isRooted;
byExtensionInSameFolder.Click += (_, e) =>
{
var path = Path.GetDirectoryName(change.Path).Replace("\\", "/");
Commands.GitIgnore.Add(_repo.FullPath, path + "/*" + extension);
e.Handled = true;
};
addToIgnore.Items.Add(byExtensionInSameFolder);
}
menu.Items.Add(addToIgnore);
hasExtra = true;
}
var lfsEnabled = new Commands.LFS(_repo.FullPath).IsEnabled();
if (lfsEnabled)
{
var lfs = new MenuItem();
lfs.Header = App.Text("GitLFS");
lfs.Icon = App.CreateMenuIcon("Icons.LFS");
var isLFSFiltered = new Commands.IsLFSFiltered(_repo.FullPath, change.Path).Result();
if (!isLFSFiltered)
{
var filename = Path.GetFileName(change.Path);
var lfsTrackThisFile = new MenuItem();
lfsTrackThisFile.Header = App.Text("GitLFS.Track", filename);
lfsTrackThisFile.Click += async (_, e) =>
{
var succ = await Task.Run(() => new Commands.LFS(_repo.FullPath).Track(filename, true));
if (succ)
App.SendNotification(_repo.FullPath, $"Tracking file named {filename} successfully!");
e.Handled = true;
};
lfs.Items.Add(lfsTrackThisFile);
if (!string.IsNullOrEmpty(extension))
{
var lfsTrackByExtension = new MenuItem();
lfsTrackByExtension.Header = App.Text("GitLFS.TrackByExtension", extension);
lfsTrackByExtension.Click += async (_, e) =>
{
var succ = await Task.Run(() => new Commands.LFS(_repo.FullPath).Track("*" + extension));
if (succ)
App.SendNotification(_repo.FullPath, $"Tracking all *{extension} files successfully!");
e.Handled = true;
};
lfs.Items.Add(lfsTrackByExtension);
}
lfs.Items.Add(new MenuItem() { Header = "-" });
}
var lfsLock = new MenuItem();
lfsLock.Header = App.Text("GitLFS.Locks.Lock");
lfsLock.Icon = App.CreateMenuIcon("Icons.Lock");
lfsLock.IsEnabled = _repo.Remotes.Count > 0;
if (_repo.Remotes.Count == 1)
{
lfsLock.Click += async (_, e) =>
{
var succ = await Task.Run(() => new Commands.LFS(_repo.FullPath).Lock(_repo.Remotes[0].Name, change.Path));
if (succ)
App.SendNotification(_repo.FullPath, $"Lock file \"{change.Path}\" successfully!");
e.Handled = true;
};
}
else
{
foreach (var remote in _repo.Remotes)
{
var remoteName = remote.Name;
var lockRemote = new MenuItem();
lockRemote.Header = remoteName;
lockRemote.Click += async (_, e) =>
{
var succ = await Task.Run(() => new Commands.LFS(_repo.FullPath).Lock(remoteName, change.Path));
if (succ)
App.SendNotification(_repo.FullPath, $"Lock file \"{change.Path}\" successfully!");
e.Handled = true;
};
lfsLock.Items.Add(lockRemote);
}
}
lfs.Items.Add(lfsLock);
var lfsUnlock = new MenuItem();
lfsUnlock.Header = App.Text("GitLFS.Locks.Unlock");
lfsUnlock.Icon = App.CreateMenuIcon("Icons.Unlock");
lfsUnlock.IsEnabled = _repo.Remotes.Count > 0;
if (_repo.Remotes.Count == 1)
{
lfsUnlock.Click += async (_, e) =>
{
var succ = await Task.Run(() => new Commands.LFS(_repo.FullPath).Unlock(_repo.Remotes[0].Name, change.Path, false));
if (succ)
App.SendNotification(_repo.FullPath, $"Unlock file \"{change.Path}\" successfully!");
e.Handled = true;
};
}
else
{
foreach (var remote in _repo.Remotes)
{
var remoteName = remote.Name;
var unlockRemote = new MenuItem();
unlockRemote.Header = remoteName;
unlockRemote.Click += async (_, e) =>
{
var succ = await Task.Run(() => new Commands.LFS(_repo.FullPath).Unlock(remoteName, change.Path, false));
if (succ)
App.SendNotification(_repo.FullPath, $"Unlock file \"{change.Path}\" successfully!");
e.Handled = true;
};
lfsUnlock.Items.Add(unlockRemote);
}
}
lfs.Items.Add(lfsUnlock);
menu.Items.Add(lfs);
hasExtra = true;
}
if (hasExtra)
menu.Items.Add(new MenuItem() { Header = "-" });
}
var copy = new MenuItem();
copy.Header = App.Text("CopyPath");
copy.Icon = App.CreateMenuIcon("Icons.Copy");
copy.Click += (_, e) =>
{
App.CopyText(change.Path);
e.Handled = true;
};
menu.Items.Add(copy);
var copyFileName = new MenuItem();
copyFileName.Header = App.Text("CopyFileName");
copyFileName.Icon = App.CreateMenuIcon("Icons.Copy");
copyFileName.Click += (_, e) =>
{
App.CopyText(Path.GetFileName(change.Path));
e.Handled = true;
};
menu.Items.Add(copyFileName);
}
else
{
var hasConflicts = false;
var hasNoneConflicts = false;
foreach (var change in _selectedUnstaged)
{
if (change.IsConflit)
hasConflicts = true;
else
hasNoneConflicts = true;
}
if (hasConflicts)
{
if (hasNoneConflicts)
{
App.RaiseException(_repo.FullPath, "You have selected both non-conflict changes with conflicts!");
return null;
}
var useTheirs = new MenuItem();
useTheirs.Icon = App.CreateMenuIcon("Icons.Incoming");
useTheirs.Header = App.Text("FileCM.UseTheirs");
useTheirs.Click += (_, e) =>
{
UseTheirs(_selectedUnstaged);
e.Handled = true;
};
var useMine = new MenuItem();
useMine.Icon = App.CreateMenuIcon("Icons.Local");
useMine.Header = App.Text("FileCM.UseMine");
useMine.Click += (_, e) =>
{
UseMine(_selectedUnstaged);
e.Handled = true;
};
if (_inProgressContext is CherryPickInProgress cherryPick)
{
useTheirs.Header = new Views.NameHighlightedTextBlock("FileCM.ResolveUsing", cherryPick.HeadName);
useMine.Header = new Views.NameHighlightedTextBlock("FileCM.ResolveUsing", _repo.CurrentBranch.Name);
}
else if (_inProgressContext is RebaseInProgress rebase)
{
useTheirs.Header = new Views.NameHighlightedTextBlock("FileCM.ResolveUsing", rebase.HeadName);
useMine.Header = new Views.NameHighlightedTextBlock("FileCM.ResolveUsing", rebase.BaseName);
}
else if (_inProgressContext is RevertInProgress revert)
{
useTheirs.Header = new Views.NameHighlightedTextBlock("FileCM.ResolveUsing", revert.Head.SHA.Substring(0, 10) + " (revert)");
useMine.Header = new Views.NameHighlightedTextBlock("FileCM.ResolveUsing", _repo.CurrentBranch.Name);
}
else if (_inProgressContext is MergeInProgress merge)
{
useTheirs.Header = new Views.NameHighlightedTextBlock("FileCM.ResolveUsing", merge.SourceName);
useMine.Header = new Views.NameHighlightedTextBlock("FileCM.ResolveUsing", _repo.CurrentBranch.Name);
}
menu.Items.Add(useTheirs);
menu.Items.Add(useMine);
return menu;
}
var stage = new MenuItem();
stage.Header = App.Text("FileCM.StageMulti", _selectedUnstaged.Count);
stage.Icon = App.CreateMenuIcon("Icons.File.Add");
stage.Click += (_, e) =>
{
StageChanges(_selectedUnstaged, null);
e.Handled = true;
};
var discard = new MenuItem();
discard.Header = App.Text("FileCM.DiscardMulti", _selectedUnstaged.Count);
discard.Icon = App.CreateMenuIcon("Icons.Undo");
discard.Click += (_, e) =>
{
Discard(_selectedUnstaged);
e.Handled = true;
};
var stash = new MenuItem();
stash.Header = App.Text("FileCM.StashMulti", _selectedUnstaged.Count);
stash.Icon = App.CreateMenuIcon("Icons.Stashes.Add");
stash.Click += (_, e) =>
{
if (_repo.CanCreatePopup())
_repo.ShowPopup(new StashChanges(_repo, _selectedUnstaged, true));
e.Handled = true;
};
var patch = new MenuItem();
patch.Header = App.Text("FileCM.SaveAsPatch");
patch.Icon = App.CreateMenuIcon("Icons.Diff");
patch.Click += async (_, e) =>
{
var storageProvider = App.GetStorageProvider();
if (storageProvider == null)
return;
var options = new FilePickerSaveOptions();
options.Title = App.Text("FileCM.SaveAsPatch");
options.DefaultExtension = ".patch";
options.FileTypeChoices = [new FilePickerFileType("Patch File") { Patterns = ["*.patch"] }];
var storageFile = await storageProvider.SaveFilePickerAsync(options);
if (storageFile != null)
{
var succ = await Task.Run(() => Commands.SaveChangesAsPatch.ProcessLocalChanges(_repo.FullPath, _selectedUnstaged, true, storageFile.Path.LocalPath));
if (succ)
App.SendNotification(_repo.FullPath, App.Text("SaveAsPatchSuccess"));
}
e.Handled = true;
};
menu.Items.Add(stage);
menu.Items.Add(discard);
menu.Items.Add(stash);
menu.Items.Add(patch);
}
return menu;
}
public ContextMenu CreateContextMenuForStagedChanges()
{
if (_selectedStaged == null || _selectedStaged.Count == 0)
return null;
var menu = new ContextMenu();
var ai = null as MenuItem;
var services = _repo.GetPreferedOpenAIServices();
if (services.Count > 0)
{
ai = new MenuItem();
ai.Icon = App.CreateMenuIcon("Icons.AIAssist");
ai.Header = App.Text("ChangeCM.GenerateCommitMessage");
if (services.Count == 1)
{
ai.Click += (_, e) =>
{
var dialog = new Views.AIAssistant(services[0], _repo.FullPath, this, _selectedStaged);
App.OpenDialog(dialog);
e.Handled = true;
};
}
else
{
foreach (var service in services)
{
var dup = service;
var item = new MenuItem();
item.Header = service.Name;
item.Click += (_, e) =>
{
var dialog = new Views.AIAssistant(dup, _repo.FullPath, this, _selectedStaged);
App.OpenDialog(dialog);
e.Handled = true;
};
ai.Items.Add(item);
}
}
}
if (_selectedStaged.Count == 1)
{
var change = _selectedStaged[0];
var path = Path.GetFullPath(Path.Combine(_repo.FullPath, change.Path));
var explore = new MenuItem();
explore.IsEnabled = File.Exists(path) || Directory.Exists(path);
explore.Header = App.Text("RevealFile");
explore.Icon = App.CreateMenuIcon("Icons.Explore");
explore.Click += (_, e) =>
{
Native.OS.OpenInFileManager(path, true);
e.Handled = true;
};
var openWith = new MenuItem();
openWith.Header = App.Text("OpenWith");
openWith.Icon = App.CreateMenuIcon("Icons.OpenWith");
openWith.IsEnabled = File.Exists(path);
openWith.Click += (_, e) =>
{
Native.OS.OpenWithDefaultEditor(path);
e.Handled = true;
};
var unstage = new MenuItem();
unstage.Header = App.Text("FileCM.Unstage");
unstage.Icon = App.CreateMenuIcon("Icons.File.Remove");
unstage.Click += (_, e) =>
{
UnstageChanges(_selectedStaged, null);
e.Handled = true;
};
var stash = new MenuItem();
stash.Header = App.Text("FileCM.Stash");
stash.Icon = App.CreateMenuIcon("Icons.Stashes.Add");
stash.Click += (_, e) =>
{
if (_repo.CanCreatePopup())
_repo.ShowPopup(new StashChanges(_repo, _selectedStaged, true));
e.Handled = true;
};
var patch = new MenuItem();
patch.Header = App.Text("FileCM.SaveAsPatch");
patch.Icon = App.CreateMenuIcon("Icons.Diff");
patch.Click += async (_, e) =>
{
var storageProvider = App.GetStorageProvider();
if (storageProvider == null)
return;
var options = new FilePickerSaveOptions();
options.Title = App.Text("FileCM.SaveAsPatch");
options.DefaultExtension = ".patch";
options.FileTypeChoices = [new FilePickerFileType("Patch File") { Patterns = ["*.patch"] }];
var storageFile = await storageProvider.SaveFilePickerAsync(options);
if (storageFile != null)
{
var succ = await Task.Run(() => Commands.SaveChangesAsPatch.ProcessLocalChanges(_repo.FullPath, _selectedStaged, false, storageFile.Path.LocalPath));
if (succ)
App.SendNotification(_repo.FullPath, App.Text("SaveAsPatchSuccess"));
}
e.Handled = true;
};
var history = new MenuItem();
history.Header = App.Text("FileHistory");
history.Icon = App.CreateMenuIcon("Icons.Histories");
history.Click += (_, e) =>
{
var window = new Views.FileHistories() { DataContext = new FileHistories(_repo, change.Path) };
window.Show();
e.Handled = true;
};
menu.Items.Add(explore);
menu.Items.Add(openWith);
menu.Items.Add(new MenuItem() { Header = "-" });
menu.Items.Add(unstage);
menu.Items.Add(stash);
menu.Items.Add(patch);
menu.Items.Add(new MenuItem() { Header = "-" });
menu.Items.Add(history);
menu.Items.Add(new MenuItem() { Header = "-" });
var lfsEnabled = new Commands.LFS(_repo.FullPath).IsEnabled();
if (lfsEnabled)
{
var lfs = new MenuItem();
lfs.Header = App.Text("GitLFS");
lfs.Icon = App.CreateMenuIcon("Icons.LFS");
var lfsLock = new MenuItem();
lfsLock.Header = App.Text("GitLFS.Locks.Lock");
lfsLock.Icon = App.CreateMenuIcon("Icons.Lock");
lfsLock.IsEnabled = _repo.Remotes.Count > 0;
if (_repo.Remotes.Count == 1)
{
lfsLock.Click += async (_, e) =>
{
var succ = await Task.Run(() => new Commands.LFS(_repo.FullPath).Lock(_repo.Remotes[0].Name, change.Path));
if (succ)
App.SendNotification(_repo.FullPath, $"Lock file \"{change.Path}\" successfully!");
e.Handled = true;
};
}
else
{
foreach (var remote in _repo.Remotes)
{
var remoteName = remote.Name;
var lockRemote = new MenuItem();
lockRemote.Header = remoteName;
lockRemote.Click += async (_, e) =>
{
var succ = await Task.Run(() => new Commands.LFS(_repo.FullPath).Lock(remoteName, change.Path));
if (succ)
App.SendNotification(_repo.FullPath, $"Lock file \"{change.Path}\" successfully!");
e.Handled = true;
};
lfsLock.Items.Add(lockRemote);
}
}
lfs.Items.Add(lfsLock);
var lfsUnlock = new MenuItem();
lfsUnlock.Header = App.Text("GitLFS.Locks.Unlock");
lfsUnlock.Icon = App.CreateMenuIcon("Icons.Unlock");
lfsUnlock.IsEnabled = _repo.Remotes.Count > 0;
if (_repo.Remotes.Count == 1)
{
lfsUnlock.Click += async (_, e) =>
{
var succ = await Task.Run(() => new Commands.LFS(_repo.FullPath).Unlock(_repo.Remotes[0].Name, change.Path, false));
if (succ)
App.SendNotification(_repo.FullPath, $"Unlock file \"{change.Path}\" successfully!");
e.Handled = true;
};
}
else
{
foreach (var remote in _repo.Remotes)
{
var remoteName = remote.Name;
var unlockRemote = new MenuItem();
unlockRemote.Header = remoteName;
unlockRemote.Click += async (_, e) =>
{
var succ = await Task.Run(() => new Commands.LFS(_repo.FullPath).Unlock(remoteName, change.Path, false));
if (succ)
App.SendNotification(_repo.FullPath, $"Unlock file \"{change.Path}\" successfully!");
e.Handled = true;
};
lfsUnlock.Items.Add(unlockRemote);
}
}
lfs.Items.Add(lfsUnlock);
menu.Items.Add(lfs);
menu.Items.Add(new MenuItem() { Header = "-" });
}
if (ai != null)
{
menu.Items.Add(ai);
menu.Items.Add(new MenuItem() { Header = "-" });
}
var copyPath = new MenuItem();
copyPath.Header = App.Text("CopyPath");
copyPath.Icon = App.CreateMenuIcon("Icons.Copy");
copyPath.Click += (_, e) =>
{
App.CopyText(change.Path);
e.Handled = true;
};
var copyFileName = new MenuItem();
copyFileName.Header = App.Text("CopyFileName");
copyFileName.Icon = App.CreateMenuIcon("Icons.Copy");
copyFileName.Click += (_, e) =>
{
App.CopyText(Path.GetFileName(change.Path));
e.Handled = true;
};
menu.Items.Add(copyPath);
menu.Items.Add(copyFileName);
}
else
{
var unstage = new MenuItem();
unstage.Header = App.Text("FileCM.UnstageMulti", _selectedStaged.Count);
unstage.Icon = App.CreateMenuIcon("Icons.File.Remove");
unstage.Click += (_, e) =>
{
UnstageChanges(_selectedStaged, null);
e.Handled = true;
};
var stash = new MenuItem();
stash.Header = App.Text("FileCM.StashMulti", _selectedStaged.Count);
stash.Icon = App.CreateMenuIcon("Icons.Stashes.Add");
stash.Click += (_, e) =>
{
if (_repo.CanCreatePopup())
_repo.ShowPopup(new StashChanges(_repo, _selectedStaged, true));
e.Handled = true;
};
var patch = new MenuItem();
patch.Header = App.Text("FileCM.SaveAsPatch");
patch.Icon = App.CreateMenuIcon("Icons.Diff");
patch.Click += async (_, e) =>
{
var storageProvider = App.GetStorageProvider();
if (storageProvider == null)
return;
var options = new FilePickerSaveOptions();
options.Title = App.Text("FileCM.SaveAsPatch");
options.DefaultExtension = ".patch";
options.FileTypeChoices = [new FilePickerFileType("Patch File") { Patterns = ["*.patch"] }];
var storageFile = await storageProvider.SaveFilePickerAsync(options);
if (storageFile != null)
{
var succ = await Task.Run(() => Commands.SaveChangesAsPatch.ProcessLocalChanges(_repo.FullPath, _selectedStaged, false, storageFile.Path.LocalPath));
if (succ)
App.SendNotification(_repo.FullPath, App.Text("SaveAsPatchSuccess"));
}
e.Handled = true;
};
menu.Items.Add(unstage);
menu.Items.Add(stash);
menu.Items.Add(patch);
if (ai != null)
{
menu.Items.Add(new MenuItem() { Header = "-" });
menu.Items.Add(ai);
}
}
return menu;
}
public ContextMenu CreateContextMenuForCommitMessages()
{
var menu = new ContextMenu();
var gitTemplate = new Commands.Config(_repo.FullPath).Get("commit.template");
var templateCount = _repo.Settings.CommitTemplates.Count;
if (templateCount == 0 && string.IsNullOrEmpty(gitTemplate))
{
menu.Items.Add(new MenuItem()
{
Header = App.Text("WorkingCopy.NoCommitTemplates"),
Icon = App.CreateMenuIcon("Icons.Code"),
IsEnabled = false
});
}
else
{
for (int i = 0; i < templateCount; i++)
{
var template = _repo.Settings.CommitTemplates[i];
var item = new MenuItem();
item.Header = new Views.NameHighlightedTextBlock("WorkingCopy.UseCommitTemplate", template.Name);
item.Icon = App.CreateMenuIcon("Icons.Code");
item.Click += (_, e) =>
{
CommitMessage = template.Apply(_repo.CurrentBranch, _staged);
e.Handled = true;
};
menu.Items.Add(item);
}
if (!string.IsNullOrEmpty(gitTemplate))
{
var friendlyName = gitTemplate;
if (!OperatingSystem.IsWindows())
{
var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
var prefixLen = home.EndsWith('/') ? home.Length - 1 : home.Length;
if (gitTemplate.StartsWith(home, StringComparison.Ordinal))
friendlyName = "~" + gitTemplate.Substring(prefixLen);
}
var gitTemplateItem = new MenuItem();
gitTemplateItem.Header = new Views.NameHighlightedTextBlock("WorkingCopy.UseCommitTemplate", friendlyName);
gitTemplateItem.Icon = App.CreateMenuIcon("Icons.Code");
gitTemplateItem.Click += (_, e) =>
{
if (File.Exists(gitTemplate))
CommitMessage = File.ReadAllText(gitTemplate);
e.Handled = true;
};
menu.Items.Add(gitTemplateItem);
}
}
menu.Items.Add(new MenuItem() { Header = "-" });
var historiesCount = _repo.Settings.CommitMessages.Count;
if (historiesCount == 0)
{
menu.Items.Add(new MenuItem()
{
Header = App.Text("WorkingCopy.NoCommitHistories"),
Icon = App.CreateMenuIcon("Icons.Histories"),
IsEnabled = false
});
}
else
{
for (int i = 0; i < historiesCount; i++)
{
var message = _repo.Settings.CommitMessages[i];
var item = new MenuItem();
item.Header = message;
item.Icon = App.CreateMenuIcon("Icons.Histories");
item.Click += (_, e) =>
{
CommitMessage = message;
e.Handled = true;
};
menu.Items.Add(item);
}
}
return menu;
}
public ContextMenu CreateContextForOpenAI()
{
if (_staged == null || _staged.Count == 0)
{
App.RaiseException(_repo.FullPath, "No files added to commit!");
return null;
}
var services = _repo.GetPreferedOpenAIServices();
if (services.Count == 0)
{
App.RaiseException(_repo.FullPath, "Bad configuration for OpenAI");
return null;
}
if (services.Count == 1)
{
var dialog = new Views.AIAssistant(services[0], _repo.FullPath, this, _staged);
App.OpenDialog(dialog);
return null;
}
else
{
var menu = new ContextMenu() { Placement = PlacementMode.TopEdgeAlignedLeft };
foreach (var service in services)
{
var dup = service;
var item = new MenuItem();
item.Header = service.Name;
item.Click += (_, e) =>
{
var dialog = new Views.AIAssistant(dup, _repo.FullPath, this, _staged);
App.OpenDialog(dialog);
e.Handled = true;
};
menu.Items.Add(item);
}
return menu;
}
}
private List<Models.Change> GetVisibleUnstagedChanges(List<Models.Change> unstaged)
{
if (string.IsNullOrEmpty(_unstagedFilter))
return unstaged;
var visible = new List<Models.Change>();
foreach (var c in unstaged)
{
if (c.Path.Contains(_unstagedFilter, StringComparison.OrdinalIgnoreCase))
visible.Add(c);
}
return visible;
}
private List<Models.Change> GetStagedChanges()
{
if (_useAmend)
return new Commands.QueryStagedChangesWithAmend(_repo.FullPath).Result();
var rs = new List<Models.Change>();
foreach (var c in _cached)
{
if (c.Index != Models.ChangeState.None &&
c.Index != Models.ChangeState.Untracked)
rs.Add(c);
}
return rs;
}
private void UpdateDetail()
{
if (_selectedUnstaged.Count == 1)
SetDetail(_selectedUnstaged[0], true);
else if (_selectedStaged.Count == 1)
SetDetail(_selectedStaged[0], false);
else
SetDetail(null, false);
}
private void UpdateInProgressState()
{
if (string.IsNullOrEmpty(_commitMessage))
{
var mergeMsgFile = Path.Combine(_repo.GitDir, "MERGE_MSG");
if (File.Exists(mergeMsgFile))
CommitMessage = File.ReadAllText(mergeMsgFile);
}
if (File.Exists(Path.Combine(_repo.GitDir, "CHERRY_PICK_HEAD")))
{
InProgressContext = new CherryPickInProgress(_repo);
}
else if (Directory.Exists(Path.Combine(_repo.GitDir, "rebase-merge")) || Directory.Exists(Path.Combine(_repo.GitDir, "rebase-apply")))
{
var rebasing = new RebaseInProgress(_repo);
InProgressContext = rebasing;
if (string.IsNullOrEmpty(_commitMessage))
{
var rebaseMsgFile = Path.Combine(_repo.GitDir, "rebase-merge", "message");
if (File.Exists(rebaseMsgFile))
CommitMessage = File.ReadAllText(rebaseMsgFile);
else if (rebasing.StoppedAt != null)
CommitMessage = new Commands.QueryCommitFullMessage(_repo.FullPath, rebasing.StoppedAt.SHA).Result();
}
}
else if (File.Exists(Path.Combine(_repo.GitDir, "REVERT_HEAD")))
{
InProgressContext = new RevertInProgress(_repo);
}
else if (File.Exists(Path.Combine(_repo.GitDir, "MERGE_HEAD")))
{
InProgressContext = new MergeInProgress(_repo);
}
else
{
InProgressContext = null;
}
}
private async void StageChanges(List<Models.Change> changes, Models.Change next)
{
var count = changes.Count;
if (count == 0)
return;
// Use `_selectedUnstaged` instead of `SelectedUnstaged` to avoid UI refresh.
_selectedUnstaged = next != null ? [next] : [];
IsStaging = true;
_repo.SetWatcherEnabled(false);
if (count == _unstaged.Count)
{
await Task.Run(() => new Commands.Add(_repo.FullPath, _repo.IncludeUntracked).Exec());
}
else if (Native.OS.GitVersion >= Models.GitVersions.ADD_WITH_PATHSPECFILE)
{
var paths = new List<string>();
foreach (var c in changes)
paths.Add(c.Path);
var tmpFile = Path.GetTempFileName();
File.WriteAllLines(tmpFile, paths);
await Task.Run(() => new Commands.Add(_repo.FullPath, tmpFile).Exec());
File.Delete(tmpFile);
}
else
{
var paths = new List<string>();
foreach (var c in changes)
paths.Add(c.Path);
for (int i = 0; i < count; i += 10)
{
var step = paths.GetRange(i, Math.Min(10, count - i));
await Task.Run(() => new Commands.Add(_repo.FullPath, step).Exec());
}
}
_repo.MarkWorkingCopyDirtyManually();
_repo.SetWatcherEnabled(true);
IsStaging = false;
}
private async void UnstageChanges(List<Models.Change> changes, Models.Change next)
{
if (changes.Count == 0)
return;
// Use `_selectedStaged` instead of `SelectedStaged` to avoid UI refresh.
_selectedStaged = next != null ? [next] : [];
IsUnstaging = true;
_repo.SetWatcherEnabled(false);
if (_useAmend)
{
await Task.Run(() => new Commands.UnstageChangesForAmend(_repo.FullPath, changes).Exec());
}
else if (changes.Count == _staged.Count)
{
await Task.Run(() => new Commands.Reset(_repo.FullPath).Exec());
}
else
{
for (int i = 0; i < changes.Count; i += 10)
{
var count = Math.Min(10, changes.Count - i);
var step = changes.GetRange(i, count);
await Task.Run(() => new Commands.Reset(_repo.FullPath, step).Exec());
}
}
_repo.MarkWorkingCopyDirtyManually();
_repo.SetWatcherEnabled(true);
IsUnstaging = false;
}
private void SetDetail(Models.Change change, bool isUnstaged)
{
if (_isLoadingData)
return;
if (change == null)
DetailContext = null;
else if (change.IsConflit && isUnstaged)
DetailContext = new Conflict(_repo, this, change);
else
DetailContext = new DiffContext(_repo.FullPath, new Models.DiffOption(change, isUnstaged), _detailContext as DiffContext);
}
private void DoCommit(bool autoStage, bool autoPush, bool allowEmpty)
{
if (!_repo.CanCreatePopup())
{
App.RaiseException(_repo.FullPath, "Repository has unfinished job! Please wait!");
return;
}
if (string.IsNullOrWhiteSpace(_commitMessage))
{
App.RaiseException(_repo.FullPath, "Commit without message is NOT allowed!");
return;
}
if (!_useAmend && !allowEmpty)
{
if ((autoStage && _count == 0) || (!autoStage && _staged.Count == 0))
{
App.OpenDialog(new Views.ConfirmCommitWithoutFiles()
{
DataContext = new ConfirmCommitWithoutFiles(this, autoPush)
});
return;
}
}
IsCommitting = true;
_repo.Settings.PushCommitMessage(_commitMessage);
_repo.SetWatcherEnabled(false);
Task.Run(() =>
{
var succ = true;
if (autoStage && _unstaged.Count > 0)
succ = new Commands.Add(_repo.FullPath, _repo.IncludeUntracked).Exec();
if (succ)
succ = new Commands.Commit(_repo.FullPath, _commitMessage, _useAmend, _repo.Settings.EnableSignOffForCommit).Run();
Dispatcher.UIThread.Post(() =>
{
if (succ)
{
CommitMessage = string.Empty;
UseAmend = false;
if (autoPush)
_repo.ShowAndStartPopup(new Push(_repo, null));
}
_repo.MarkBranchesDirtyManually();
_repo.SetWatcherEnabled(true);
IsCommitting = false;
});
});
}
private bool IsChanged(List<Models.Change> old, List<Models.Change> cur)
{
if (old.Count != cur.Count)
return true;
var oldSet = new HashSet<string>();
foreach (var c in old)
oldSet.Add($"{c.Path}\n{c.WorkTree}\n{c.Index}");
foreach (var c in cur)
{
if (!oldSet.Contains($"{c.Path}\n{c.WorkTree}\n{c.Index}"))
return true;
}
return false;
}
private Repository _repo = null;
private bool _isLoadingData = false;
private bool _isStaging = false;
private bool _isUnstaging = false;
private bool _isCommitting = false;
private bool _useAmend = false;
private bool _hasRemotes = false;
private List<Models.Change> _cached = [];
private List<Models.Change> _unstaged = [];
private List<Models.Change> _visibleUnstaged = [];
private List<Models.Change> _staged = [];
private List<Models.Change> _selectedUnstaged = [];
private List<Models.Change> _selectedStaged = [];
private int _count = 0;
private object _detailContext = null;
private string _unstagedFilter = string.Empty;
private string _commitMessage = string.Empty;
private bool _hasUnsolvedConflicts = false;
private InProgressContext _inProgressContext = null;
}
}