From 634f66dadbf7cccbc8ccb206ea59a965ad481362 Mon Sep 17 00:00:00 2001 From: Denis Kudelin <15978569+denis-kudelin@users.noreply.github.com> Date: Tue, 19 Aug 2025 11:01:02 +0300 Subject: [PATCH] Add force squash across merges option --- src/Commands/Command.cs | 4 + src/Commands/DiffAll.cs | 20 +++ src/Commands/DiffStat.cs | 22 ++++ src/Commands/IsAncestor.cs | 20 +++ src/Resources/Locales/de_DE.axaml | 14 ++ src/Resources/Locales/en_US.axaml | 15 +++ src/Resources/Locales/es_ES.axaml | 14 ++ src/Resources/Locales/fr_FR.axaml | 14 ++ src/Resources/Locales/it_IT.axaml | 14 ++ src/Resources/Locales/ja_JP.axaml | 14 ++ src/Resources/Locales/pt_BR.axaml | 14 ++ src/Resources/Locales/ru_RU.axaml | 17 ++- src/Resources/Locales/ta_IN.axaml | 14 ++ src/Resources/Locales/uk_UA.axaml | 14 ++ src/Resources/Locales/zh_CN.axaml | 14 ++ src/Resources/Locales/zh_TW.axaml | 14 ++ src/ViewModels/ForceSquashAcrossMerges.cs | 144 +++++++++++++++++++++ src/ViewModels/Histories.cs | 7 + src/ViewModels/Preferences.cs | 7 + src/Views/ForceSquashAcrossMerges.axaml | 25 ++++ src/Views/ForceSquashAcrossMerges.axaml.cs | 13 ++ src/Views/Histories.axaml.cs | 17 +++ src/Views/Preferences.axaml | 7 +- 23 files changed, 456 insertions(+), 2 deletions(-) create mode 100644 src/Commands/DiffAll.cs create mode 100644 src/Commands/DiffStat.cs create mode 100644 src/Commands/IsAncestor.cs create mode 100644 src/ViewModels/ForceSquashAcrossMerges.cs create mode 100644 src/Views/ForceSquashAcrossMerges.axaml create mode 100644 src/Views/ForceSquashAcrossMerges.axaml.cs diff --git a/src/Commands/Command.cs b/src/Commands/Command.cs index 44b651b20..30a9b1394 100644 --- a/src/Commands/Command.cs +++ b/src/Commands/Command.cs @@ -36,6 +36,7 @@ public enum EditorType public CancellationToken CancellationToken { get; set; } = CancellationToken.None; public bool RaiseError { get; set; } = true; public Models.ICommandLog Log { get; set; } = null; + public Dictionary Envs { get; } = new(); public async Task ExecAsync() { @@ -192,6 +193,9 @@ protected ProcessStartInfo CreateGitStartInfo(bool redirect) start.Environment.Add("LC_ALL", "C"); } + foreach (var kv in Envs) + start.Environment[kv.Key] = kv.Value; + var builder = new StringBuilder(); builder .Append("--no-pager -c core.quotepath=off -c credential.helper=") diff --git a/src/Commands/DiffAll.cs b/src/Commands/DiffAll.cs new file mode 100644 index 000000000..c1bb4eec2 --- /dev/null +++ b/src/Commands/DiffAll.cs @@ -0,0 +1,20 @@ +using System.Threading.Tasks; + +namespace SourceGit.Commands +{ + public class DiffAll : Command + { + public DiffAll(string repo, string range) + { + WorkingDirectory = repo; + Context = repo; + Args = $"diff {range}"; + } + + public async Task GetResultAsync() + { + var rs = await ReadToEndAsync(); + return rs.IsSuccess ? rs.StdOut : string.Empty; + } + } +} diff --git a/src/Commands/DiffStat.cs b/src/Commands/DiffStat.cs new file mode 100644 index 000000000..5e7d7b73f --- /dev/null +++ b/src/Commands/DiffStat.cs @@ -0,0 +1,22 @@ +using System.Threading.Tasks; + +namespace SourceGit.Commands +{ + public class DiffStat : Command + { + public DiffStat(string repo, string range) + { + WorkingDirectory = repo; + Context = repo; + Args = $"diff --stat {range}"; + } + + public async Task GetResultAsync() + { + var rs = await ReadToEndAsync().ConfigureAwait(false); + if (rs.IsSuccess && !string.IsNullOrEmpty(rs.StdOut)) + return rs.StdOut.Trim(); + return string.Empty; + } + } +} diff --git a/src/Commands/IsAncestor.cs b/src/Commands/IsAncestor.cs new file mode 100644 index 000000000..9b3e72098 --- /dev/null +++ b/src/Commands/IsAncestor.cs @@ -0,0 +1,20 @@ +using System.Threading.Tasks; + +namespace SourceGit.Commands +{ + public class IsAncestor : Command + { + public IsAncestor(string repo, string ancestor, string descendant) + { + WorkingDirectory = repo; + Context = repo; + Args = $"merge-base --is-ancestor {ancestor} {descendant}"; + RaiseError = false; + } + + public bool Test() + { + return ReadToEnd().IsSuccess; + } + } +} diff --git a/src/Resources/Locales/de_DE.axaml b/src/Resources/Locales/de_DE.axaml index 689567a12..1d31ac899 100644 --- a/src/Resources/Locales/de_DE.axaml +++ b/src/Resources/Locales/de_DE.axaml @@ -148,6 +148,7 @@ Umformulieren Als Patch speichern... Squash in den Vorgänger + Squash hierher erzwingen... ÄNDERUNGEN geänderte Datei(en) Änderungen durchsuchen... @@ -751,6 +752,19 @@ Zum Commit wechseln Squash Commits In: + Squash über Merges (Historie glätten) + Operation überschreibt die Historie vom gewählten Commit bis HEAD zu einem Commit +Alle Merge-Commits im Bereich werden entfernt +Signierte Commits oder Merges verlieren Signaturen +Anschließend ist ein Force-Push erforderlich +Schließe oder aktualisiere alle zugehörigen MR/PR + Vor dem Überschreiben Sicherungszweig erstellen + Lokale Änderungen automatisch stashen + Originalautor und -datum des Ziel-Commits beibehalten + Nachrichten der gesquashten Commits an den Body anhängen + Automatischen Stash anwenden fehlgeschlagen. + Historie abgeflacht. Sicherungszweig: {0} + Historie abgeflacht. SSH privater Schlüssel: Pfad zum privaten SSH Schlüssel START diff --git a/src/Resources/Locales/en_US.axaml b/src/Resources/Locales/en_US.axaml index 794ecab64..a685f07c5 100644 --- a/src/Resources/Locales/en_US.axaml +++ b/src/Resources/Locales/en_US.axaml @@ -144,6 +144,7 @@ Reword Save as Patch... Squash into Parent + Force squash to here... CHANGES changed file(s) Search Changes... @@ -552,6 +553,7 @@ Tool GENERAL Check for updates on startup + Enable dangerous history rewrites Date Format Language History Commits @@ -746,6 +748,19 @@ Go to Squash Commits Into: + Squash across merges (flatten history) + Operation rewrites history from selected commit to HEAD into one commit +All merge commits in range will be removed +Signed commits or merges lose signatures +Force push is required afterwards +Close or update any related MR/PR + Create safety backup branch before rewrite + Auto-stash local changes + Keep original author/date of target commit + Append messages of squashed commits to body + Failed to apply auto stash. + History flattened. Backup branch: {0} + History flattened. SSH Private Key: Private SSH key store path START diff --git a/src/Resources/Locales/es_ES.axaml b/src/Resources/Locales/es_ES.axaml index 71cd212d1..eec64efc4 100644 --- a/src/Resources/Locales/es_ES.axaml +++ b/src/Resources/Locales/es_ES.axaml @@ -148,6 +148,7 @@ Reescribir Guardar como Parche... Squash en Parent + Forzar squash aquí... CAMBIOS archivo(s) modificado(s) Buscar Cambios... @@ -750,6 +751,19 @@ Ir a Squash Commits En: + Squash a través de merges (aplanar historial) + La operación reescribe el historial desde el commit seleccionado hasta HEAD en un solo commit +Todos los commits de merge en el rango serán eliminados +Los commits o merges firmados pierden sus firmas +Se requiere un force push después +Cierra o actualiza cualquier MR/PR relacionado + Crear rama de respaldo antes de sobrescribir + Guardar automáticamente los cambios locales + Conservar autor/fecha originales del commit destino + Añadir mensajes de los commits aplastados al cuerpo + No se pudo aplicar el auto-stash. + Historial aplanado. Rama de respaldo: {0} + Historial aplanado. Clave Privada SSH: Ruta de almacenamiento de la clave privada SSH INICIAR diff --git a/src/Resources/Locales/fr_FR.axaml b/src/Resources/Locales/fr_FR.axaml index 7359db96d..8421669b4 100644 --- a/src/Resources/Locales/fr_FR.axaml +++ b/src/Resources/Locales/fr_FR.axaml @@ -108,6 +108,7 @@ Reformuler Enregistrer en tant que patch... Squash dans le parent + Forcer le squash ici... CHANGEMENTS Rechercher les changements... FICHIERS @@ -611,6 +612,19 @@ Aller à Squash les commits Dans : + Squasher à travers les fusions (aplatir l'historique) + L'opération réécrit l'historique du commit sélectionné jusqu'à HEAD en un seul commit +Tous les commits de fusion dans l'intervalle seront supprimés +Les commits ou fusions signés perdent leurs signatures +Un push forcé est nécessaire ensuite +Fermez ou mettez à jour toute MR/PR liée + Créer une branche de sauvegarde avant la réécriture + Stasher automatiquement les modifications locales + Conserver l'auteur/la date originaux du commit cible + Ajouter les messages des commits squashés au corps + Échec de l'application de l'auto-stash. + Historique aplati. Branche de sauvegarde : {0} + Historique aplati. Clé privée SSH : Chemin du magasin de clés privées SSH START diff --git a/src/Resources/Locales/it_IT.axaml b/src/Resources/Locales/it_IT.axaml index 731fb267f..3e6d661bb 100644 --- a/src/Resources/Locales/it_IT.axaml +++ b/src/Resources/Locales/it_IT.axaml @@ -119,6 +119,7 @@ Modifica Salva come Patch... Compatta nel Genitore + Forza squash qui... MODIFICHE Cerca Modifiche... FILE @@ -639,6 +640,19 @@ Vai a Compatta Commit In: + Squash attraverso i merge (appiattisci la cronologia) + L'operazione riscrive la cronologia dal commit selezionato a HEAD in un solo commit +Tutti i commit di merge nell'intervallo saranno rimossi +I commit o i merge firmati perdono le firme +È richiesto un force push dopo +Chiudi o aggiorna qualsiasi MR/PR correlato + Crea un ramo di backup prima della riscrittura + Esegui auto-stash delle modifiche locali + Mantieni autore/data originali del commit di destinazione + Aggiungi i messaggi dei commit squasciati al corpo + Applicazione dell'auto-stash non riuscita. + Cronologia appiattita. Ramo di backup: {0} + Cronologia appiattita. Chiave Privata SSH: Percorso per la chiave SSH privata AVVIA diff --git a/src/Resources/Locales/ja_JP.axaml b/src/Resources/Locales/ja_JP.axaml index 1d0f26bc5..d7c7645c0 100644 --- a/src/Resources/Locales/ja_JP.axaml +++ b/src/Resources/Locales/ja_JP.axaml @@ -107,6 +107,7 @@ 書き直す パッチとして保存... 親にスカッシュ + ここに強制スクワッシュ... 変更 変更を検索... ファイル @@ -609,6 +610,19 @@ Go to スカッシュコミット 宛先: + マージをまたいでスクワッシュ(履歴を平坦化) + 選択したコミットからHEADまでの履歴を1つのコミットに書き換えます +範囲内のすべてのマージコミットは削除されます +署名付きのコミットやマージは署名を失います +その後は強制プッシュが必要です +関連するMR/PRを閉じるか更新してください + 書き換え前に安全なバックアップブランチを作成 + ローカル変更を自動でスタッシュ + 対象コミットの元の作者/日付を保持 + スクワッシュされたコミットのメッセージを本文に追加 + 自動スタッシュの適用に失敗しました。 + 履歴を平坦化しました。バックアップブランチ: {0} + 履歴を平坦化しました。 SSH プライベートキー: プライベートSSHキーストアのパス スタート diff --git a/src/Resources/Locales/pt_BR.axaml b/src/Resources/Locales/pt_BR.axaml index bc1f92ebe..ac8abd3b0 100644 --- a/src/Resources/Locales/pt_BR.axaml +++ b/src/Resources/Locales/pt_BR.axaml @@ -97,6 +97,7 @@ Modificar Mensagem Salvar como Patch... Mesclar ao Commit Pai + Forçar squash aqui... ALTERAÇÕES Buscar Alterações... ARQUIVOS @@ -553,6 +554,19 @@ Copiar SHA Squash Commits Squash commits em: + Squash através dos merges (achatar histórico) + A operação reescreve o histórico do commit selecionado até o HEAD em um único commit +Todos os commits de merge no intervalo serão removidos +Commits ou merges assinados perdem as assinaturas +Um force push é necessário depois +Feche ou atualize qualquer MR/PR relacionado + Criar branch de backup antes de reescrever + Auto-stash das alterações locais + Manter autor/data originais do commit de destino + Anexar mensagens dos commits squashados ao corpo + Falha ao aplicar auto-stash. + Histórico achatado. Branch de backup: {0} + Histórico achatado. Chave SSH Privada: Caminho para a chave SSH privada INICIAR diff --git a/src/Resources/Locales/ru_RU.axaml b/src/Resources/Locales/ru_RU.axaml index 8a4f48a9d..5c83bc30a 100644 --- a/src/Resources/Locales/ru_RU.axaml +++ b/src/Resources/Locales/ru_RU.axaml @@ -146,9 +146,10 @@ Изменить комментарий Сохранить как заплатки... Объединить с предыдущей ревизией + Принудительно объединить сюда... ИЗМЕНЕНИЯ изменённый(х) файл(ов) - Найти изменения.... + Найти изменения... ФАЙЛЫ Файл LFS Поиск файлов... @@ -545,6 +546,7 @@ Инструмент ОСНОВНЫЕ Проверить обновления при старте + Включить опасные переписывания истории Формат даты Язык Максимальная длина истории @@ -739,6 +741,19 @@ Перейти Втиснуть ревизии В: + Втиснуть через слияния (выпрямить историю) + Операция перепишет историю от выбранного коммита до HEAD в один коммит +Все merge-коммиты в диапазоне будут удалены +Подписи коммитов и merge будут потеряны +Далее потребуется принудительная отправка +Закройте или обновите связанные MR/PR + Создать резервную ветку перед переписыванием + Авто-сохранение локальных изменений + Сохранить автора и дату целевого коммита + Добавить сообщения объединяемых коммитов в тело + Не удалось применить авто-стэш. + История сплюснута. Резервная ветка: {0} + История сплюснута. Приватный ключ SSH: Путь хранения приватного ключа SSH ЗАПУСК diff --git a/src/Resources/Locales/ta_IN.axaml b/src/Resources/Locales/ta_IN.axaml index 29150e14d..ff339f9f4 100644 --- a/src/Resources/Locales/ta_IN.axaml +++ b/src/Resources/Locales/ta_IN.axaml @@ -107,6 +107,7 @@ வேறுமொழி ஒட்டாக சேமி... பெற்றோர்களில் நொறுக்கு + இங்கே நொறுக்கு கட்டாயப்படுத்து... மாற்றங்கள் மாற்றங்களைத் தேடு... கோப்புகள் @@ -610,6 +611,19 @@ இதற்கு செல் நொறுக்கு உறுதிமொழிகள் இதில்: + இணைப்புகளை தாண்டி நொறுக்கு (வரலாற்றை சமப்படுத்து) + தேர்ந்தெடுக்கப்பட்ட commit-இலிருந்து HEAD வரை வரலாறு ஒரே commit-ஆக மாற்றப்படும் +வரம்பில் உள்ள merge commit-கள் அனைத்தும் அகற்றப்படும் +கையொப்பமிட்ட commit-கள் அல்லது merge-கள் கையொப்பங்களை இழக்கும் +பிறகு force push தேவை +தொடர்புடைய MR/PR-ஐ மூட அல்லது புதுப்பிக்கவும் + மீண்டும் எழுதுவதற்கு முன் பாதுகாப்பு காப்பு branch உருவாக்கவும் + உள்ளூர் மாற்றங்களை தானாக stash செய் + இலக்கு commit-ன் அசல் author/தேதியை வைத்திரு + நொறுக்கப்பட்ட commit-களின் செய்திகளை body-க்கு சேர்க்கவும் + Auto stash-ஐ பயன்படுத்த முடியவில்லை. + வரலாறு சமப்படுத்தப்பட்டது. காப்பு branch: {0} + வரலாறு சமப்படுத்தப்பட்டது. பாஓடு தனியார் திறவுகோல்: தனியார் பாஓடு திறவுகோல் கடை பாதை தொடங்கு diff --git a/src/Resources/Locales/uk_UA.axaml b/src/Resources/Locales/uk_UA.axaml index 4880e02c3..eee522e9d 100644 --- a/src/Resources/Locales/uk_UA.axaml +++ b/src/Resources/Locales/uk_UA.axaml @@ -108,6 +108,7 @@ Змінити повідомлення Зберегти як патч... Склеїти з батьківським комітом + Примусово сплюснути сюди... ЗМІНИ Пошук змін... ФАЙЛИ @@ -615,6 +616,19 @@ Перейти до Squash (Склеїти коміти) В: + Сплющити через мерджі (вирівняти історію) + Операція перепише історію від вибраного коміту до HEAD в один коміт +Усі коміти злиття в діапазоні буде вилучено +Підписані коміти або злиття втратять підписи +Після цього потрібен примусовий push +Закрийте або оновіть пов’язані MR/PR + Створити резервну гілку перед переписуванням + Автоматично застешити локальні зміни + Зберегти автора/дату цільового коміту + Додати повідомлення сплюснутих комітів у тіло + Не вдалося застосувати автостеш. + Історію сплюснуто. Резервна гілка: {0} + Історію сплюснуто. Приватний ключ SSH: Шлях до сховища приватного ключа SSH ПОЧАТИ diff --git a/src/Resources/Locales/zh_CN.axaml b/src/Resources/Locales/zh_CN.axaml index 0f2fe026d..8493a9fda 100644 --- a/src/Resources/Locales/zh_CN.axaml +++ b/src/Resources/Locales/zh_CN.axaml @@ -148,6 +148,7 @@ 编辑提交信息 另存为补丁 ... 合并此提交到上一个提交 + 强制压缩到此... 变更对比 个文件发生变更 查找变更... @@ -750,6 +751,19 @@ 跳转到提交 压缩为单个提交 合并入: + 跨合并压缩(扁平化历史) + 操作会把从选定提交到 HEAD 的历史重写为一个提交 +范围内的所有合并提交都会被移除 +签名的提交或合并将丢失签名 +之后需要强制推送 +请关闭或更新相关的 MR/PR + 重写前创建安全备份分支 + 自动保存本地更改 + 保留目标提交的原作者/日期 + 将被压缩提交的消息追加到正文 + 应用自动保存失败。 + 历史已扁平化。备份分支:{0} + 历史已扁平化。 SSH密钥 : SSH密钥文件 开 始 diff --git a/src/Resources/Locales/zh_TW.axaml b/src/Resources/Locales/zh_TW.axaml index 068a2fdbe..c4dd0c9a3 100644 --- a/src/Resources/Locales/zh_TW.axaml +++ b/src/Resources/Locales/zh_TW.axaml @@ -148,6 +148,7 @@ 編輯提交訊息 另存為修補檔 (patch)... 合併此提交到上一個提交 + 強制壓縮到此... 變更對比 個檔案已變更 搜尋變更... @@ -750,6 +751,19 @@ 前往此提交 壓縮為單個提交 合併入: + 跨合併壓縮(扁平化歷史) + 操作會將從選定提交到 HEAD 的歷史重寫為單一提交 +範圍內的所有合併提交都會被移除 +簽名的提交或合併會失去簽章 +之後需要強制推送 +請關閉或更新相關的 MR/PR + 重寫前建立安全備份分支 + 自動暫存本地變更 + 保留目標提交的原作者/日期 + 將被壓縮的提交訊息附加到正文 + 套用自動暫存失敗。 + 歷史已扁平化。備份分支:{0} + 歷史已扁平化。 SSH 金鑰: SSH 金鑰檔案 開 始 diff --git a/src/ViewModels/ForceSquashAcrossMerges.cs b/src/ViewModels/ForceSquashAcrossMerges.cs new file mode 100644 index 000000000..d63367f4c --- /dev/null +++ b/src/ViewModels/ForceSquashAcrossMerges.cs @@ -0,0 +1,144 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace SourceGit.ViewModels +{ + public class ForceSquashAcrossMerges : Popup + { + public Models.Commit Target { get; } + public bool CreateBackup { get; set; } = true; + public bool AutoStash { get; set; } = true; + public bool KeepAuthorDate { get; set; } + public bool AppendMessages { get; set; } + public string Message { get => _message; set => SetProperty(ref _message, value, true); } + + public ForceSquashAcrossMerges(Repository repo, Models.Commit target) + { + _repo = repo; + Target = target; + _message = target.Subject; + } + + public override async Task Sure() + { + _repo.SetWatcherEnabled(false); + ProgressDescription = "Squashing ..."; + var log = _repo.CreateLog("ForceSquash"); + Use(log); + + var baseSHA = Target.Parents[0]; + var signOff = _repo.Settings.EnableSignOffForCommit; + var stashName = string.Empty; + var succ = true; + var head = await new Commands.QueryRevisionByRefName(_repo.FullPath, "HEAD").GetResultAsync(); + var headShort = head[..7]; + + if (AutoStash) + { + var changes = await new Commands.QueryLocalChanges(_repo.FullPath, false).GetResultAsync(); + foreach (var c in changes) + { + if (c.Index != Models.ChangeState.None || c.WorkTree != Models.ChangeState.None) + { + stashName = $"sourcegit/force-squash/{headShort}-{Guid.NewGuid().ToString("N")[..6]}"; + succ = await new Commands.Stash(_repo.FullPath).Use(log).PushAsync(stashName); + break; + } + } + if (!succ) + { + log.Complete(); + _repo.SetWatcherEnabled(true); + return false; + } + } + + var backupName = string.Empty; + if (CreateBackup && _repo.CurrentBranch != null) + { + backupName = $"sourcegit/backup/flatten-{Models.Branch.FixName(_repo.CurrentBranch.Name)}-{_repo.CurrentBranch.Head[..7]}"; + succ = await new Commands.Branch(_repo.FullPath, backupName).Use(log).CreateAsync("HEAD", false); + if (!succ) + { + log.Complete(); + _repo.SetWatcherEnabled(true); + return false; + } + } + + List append = null; + if (AppendMessages) + { + append = await new Commands.QueryCommits(_repo.FullPath, $"{Target.SHA}..{head}", false).GetResultAsync(); + append.Sort((l, r) => l.CommitterTime.CompareTo(r.CommitterTime)); + } + + succ = await new Commands.Reset(_repo.FullPath, baseSHA, "--soft").Use(log).ExecAsync(); + if (!succ) + { + log.Complete(); + _repo.SetWatcherEnabled(true); + return false; + } + + var commitMsg = Message; + if (AppendMessages && append.Count > 0) + { + var lines = new List(); + foreach (var c in append) + { + var msg = c.Subject.Trim(); + if (msg.Length == 0) + continue; + if (!lines.Contains(msg)) + lines.Add(msg); + } + if (lines.Count > 0) + commitMsg += "\n\n" + string.Join("\n", lines); + } + + var commit = new Commands.Commit(_repo.FullPath, commitMsg, signOff, false, false); + if (KeepAuthorDate) + { + var author = Target.Author; + var date = DateTimeOffset.FromUnixTimeSeconds((long)Target.AuthorTime).ToString("o"); + commit.Args += $" --author={("" + author.Name + " <" + author.Email + ">").Quoted()} --date={date.Quoted()}"; + commit.Envs["GIT_COMMITTER_DATE"] = date; + } + succ = await commit.Use(log).RunAsync(); + if (!succ) + { + log.Complete(); + _repo.SetWatcherEnabled(true); + return false; + } + + if (!string.IsNullOrEmpty(stashName)) + { + succ = await new Commands.Stash(_repo.FullPath).Use(log).PopAsync(stashName); + if (!succ) + { + App.SendNotification(_repo.FullPath, App.Text("ForceSquash.StashPopFailed")); + log.Complete(); + _repo.SetWatcherEnabled(true); + return false; + } + } + + log.Complete(); + _repo.SetWatcherEnabled(true); + _repo.RefreshCommits(); + if (!string.IsNullOrEmpty(backupName)) + App.SendNotification(_repo.FullPath, App.Text("ForceSquash.Success", backupName)); + else + App.SendNotification(_repo.FullPath, App.Text("ForceSquash.SuccessNoBackup")); + _repo.ShowPopup(new Push(_repo, _repo.CurrentBranch) { ForcePush = true }); + return true; + } + + private readonly Repository _repo; + private string _message; + + } +} diff --git a/src/ViewModels/Histories.cs b/src/ViewModels/Histories.cs index da627e003..2adcd4847 100644 --- a/src/ViewModels/Histories.cs +++ b/src/ViewModels/Histories.cs @@ -331,6 +331,13 @@ public async Task SquashHeadAsync(Models.Commit head) } } + public Task ForceSquashAsync(Models.Commit commit) + { + if (_repo.CanCreatePopup()) + _repo.ShowPopup(new ForceSquashAcrossMerges(_repo, commit)); + return Task.CompletedTask; + } + public async Task InteractiveRebaseAsync(Models.Commit commit, Models.InteractiveRebaseAction act) { var prefill = new InteractiveRebasePrefill(commit.SHA, act); diff --git a/src/ViewModels/Preferences.cs b/src/ViewModels/Preferences.cs index c2335910f..9b2aea861 100644 --- a/src/ViewModels/Preferences.cs +++ b/src/ViewModels/Preferences.cs @@ -158,6 +158,12 @@ public bool Check4UpdatesOnStartup set => SetProperty(ref _check4UpdatesOnStartup, value); } + public bool EnableDangerousHistoryRewrites + { + get => _enableDangerousHistoryRewrites; + set => SetProperty(ref _enableDangerousHistoryRewrites, value); + } + public bool ShowAuthorTimeInGraph { get => _showAuthorTimeInGraph; @@ -710,6 +716,7 @@ private bool RemoveInvalidRepositoriesRecursive(List collection) private bool _showChildren = false; private bool _check4UpdatesOnStartup = true; + private bool _enableDangerousHistoryRewrites = false; private double _lastCheckUpdateTime = 0; private string _ignoreUpdateTag = string.Empty; diff --git a/src/Views/ForceSquashAcrossMerges.axaml b/src/Views/ForceSquashAcrossMerges.axaml new file mode 100644 index 000000000..685ef306c --- /dev/null +++ b/src/Views/ForceSquashAcrossMerges.axaml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + diff --git a/src/Views/ForceSquashAcrossMerges.axaml.cs b/src/Views/ForceSquashAcrossMerges.axaml.cs new file mode 100644 index 000000000..7a82196ac --- /dev/null +++ b/src/Views/ForceSquashAcrossMerges.axaml.cs @@ -0,0 +1,13 @@ +using Avalonia.Controls; + +namespace SourceGit.Views +{ + public partial class ForceSquashAcrossMerges : UserControl + { + public ForceSquashAcrossMerges() + { + InitializeComponent(); + } + + } +} diff --git a/src/Views/Histories.axaml.cs b/src/Views/Histories.axaml.cs index 6d2f48c0f..3df245200 100644 --- a/src/Views/Histories.axaml.cs +++ b/src/Views/Histories.axaml.cs @@ -10,6 +10,7 @@ using Avalonia.Interactivity; using Avalonia.Platform.Storage; using Avalonia.VisualTree; +using SourceGit.ViewModels; namespace SourceGit.Views { @@ -708,6 +709,22 @@ private ContextMenu CreateContextMenuForSingleCommit(ViewModels.Repository repo, } } + if (ViewModels.Preferences.Instance.EnableDangerousHistoryRewrites && commit.Parents.Count > 0) + { + if (new Commands.IsAncestor(repo.FullPath, commit.SHA, "HEAD").Test()) + { + var forceSquash = new MenuItem(); + forceSquash.Header = App.Text("CommitCM.ForceSquash"); + forceSquash.Icon = App.CreateMenuIcon("Icons.SquashIntoParent"); + forceSquash.Click += async (_, e) => + { + await vm.ForceSquashAsync(commit); + e.Handled = true; + }; + menu.Items.Add(forceSquash); + } + } + menu.Items.Add(new MenuItem() { Header = "-" }); } diff --git a/src/Views/Preferences.axaml b/src/Views/Preferences.axaml index cdb0940ae..afa8e8252 100644 --- a/src/Views/Preferences.axaml +++ b/src/Views/Preferences.axaml @@ -46,7 +46,7 @@ - + + +