diff options
author | Stephan Dilly <dilly.stephan@gmail.com> | 2021-05-12 15:34:36 +0200 |
---|---|---|
committer | Stephan Dilly <dilly.stephan@gmail.com> | 2021-05-12 15:34:36 +0200 |
commit | 378b8655ab02d970feaa36989d20e0c6cd15f5f8 (patch) | |
tree | cf29969c6372c339dd3adc94b04098779eb9df49 | |
parent | b6ea2b77d103b73ed585c727bdd5ffb6d7910707 (diff) | |
parent | 017701ffac5f35447ce9d25faae2ad5456d04760 (diff) |
Merge branch 'master'fix-176-win-maxpath
Conflicts:
Cargo.lock
asyncgit/Cargo.toml
-rw-r--r-- | .github/workflows/ci.yml | 2 | ||||
-rw-r--r-- | CHANGELOG.md | 4 | ||||
-rw-r--r-- | Cargo.lock | 34 | ||||
-rw-r--r-- | README.md | 11 | ||||
-rw-r--r-- | asyncgit/Cargo.toml | 5 | ||||
-rw-r--r-- | asyncgit/src/sync/branch/merge_commit.rs | 110 | ||||
-rw-r--r-- | asyncgit/src/sync/branch/merge_ff.rs | 6 | ||||
-rw-r--r-- | asyncgit/src/sync/merge.rs | 141 | ||||
-rw-r--r-- | asyncgit/src/sync/mod.rs | 4 | ||||
-rw-r--r-- | src/app.rs | 7 | ||||
-rw-r--r-- | src/components/branchlist.rs | 34 | ||||
-rw-r--r-- | src/components/commit.rs | 88 | ||||
-rw-r--r-- | src/components/create_branch.rs | 2 | ||||
-rw-r--r-- | src/components/reset.rs | 14 | ||||
-rw-r--r-- | src/components/utils/filetree.rs | 1 | ||||
-rw-r--r-- | src/input.rs | 68 | ||||
-rw-r--r-- | src/keys.rs | 10 | ||||
-rw-r--r-- | src/main.rs | 28 | ||||
-rw-r--r-- | src/queue.rs | 3 | ||||
-rw-r--r-- | src/strings.rs | 43 | ||||
-rw-r--r-- | src/tabs/status.rs | 145 | ||||
-rw-r--r-- | vim_style_key_config.ron | 3 |
22 files changed, 547 insertions, 216 deletions
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a4744f50..df739140 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,7 +38,7 @@ jobs: - name: MacOS Workaround if: matrix.os == 'macos-latest' - run: cargo clean --locked -p serde_derive -p thiserror + run: cargo clean -p serde_derive -p thiserror - name: Install Rust uses: actions-rs/toolchain@v1 diff --git a/CHANGELOG.md b/CHANGELOG.md index c648c3f4..40ac8e31 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,10 +12,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ![warning](assets/commit-msg-length-limit.gif) ## Added +- merging arbitrary branch, commit merges ([#485](https://github.com/extrawurst/gitui/issues/485)) - warning if commit subject line gets too long ([#478](https://github.com/extrawurst/gitui/issues/478)) ## Changed -- smarter log timestamps ([#682](https://github.com/extrawurst/gitui/issues/682)) +- smarter log timestamps ([#682](https://github.com/extrawurst/gitui/issues/682)) +- create-branch popup aligned with rename-branch [[@bruceCoelho](https://github.com/bruceCoelho)] ([#679](https://github.com/extrawurst/gitui/issues/679)) ## [0.15.0] - 2020-04-27 @@ -2,9 +2,9 @@ # It is not intended for manual editing. [[package]] name = "addr2line" -version = "0.14.1" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a55f82cfe485775d02112886f4169bde0c5894d75e79ead7eafe7e40a25e45f7" +checksum = "03345e98af8f3d786b6d9f656ccfa6ac316d954e92bc4841f0bba20789d5fb5a" dependencies = [ "gimli", ] @@ -78,9 +78,9 @@ checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" [[package]] name = "backtrace" -version = "0.3.58" +version = "0.3.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88fb5a785d6b44fd9d6700935608639af1b8356de1e55d5f7c2740f4faa15d82" +checksum = "4717cfcbfaa661a0fd48f8453951837ae7e8f81e481fbb136e3202d72805a744" dependencies = [ "addr2line", "cc", @@ -306,14 +306,15 @@ dependencies = [ [[package]] name = "gimli" -version = "0.23.0" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6503fe142514ca4799d4c26297c4248239fe8838d827db6bd6065c6ed29a6ce" +checksum = "0e4075386626662786ddb0ec9081e7c7eeb1ba31951f447ca780ef9f5d568189" [[package]] name = "git2" -version = "0.13.18" -source = "git+https://github.com/rust-lang/git2-rs.git?rev=1b1499a#1b1499a78dc04c042aa7789f81f0a3308b66261b" +version = "0.13.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17929de7239dea9f68aa14f94b2ab4974e7b24c1314275ffcc12a7758172fa18" dependencies = [ "bitflags", "libc", @@ -466,8 +467,9 @@ checksum = "18794a8ad5b29321f790b55d93dfba91e125cb1a9edbd4f8e3150acc771c1a5e" [[package]] name = "libgit2-sys" -version = "0.12.19+1.1.0" -source = "git+https://github.com/rust-lang/git2-rs.git?rev=1b1499a#1b1499a78dc04c042aa7789f81f0a3308b66261b" +version = "0.12.20+1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e2f09917e00b9ad194ae72072bb5ada2cca16d8171a43e91ddba2afbb02664b" dependencies = [ "cc", "libc", @@ -652,9 +654,9 @@ dependencies = [ [[package]] name = "object" -version = "0.23.0" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9a7ab5d64814df0fe4a4b5ead45ed6c5f181ee3ff04ba344313a6c80446c5d4" +checksum = "1a5b3dd1c072ee7963717671d1ca129f1048fda25edea6b752bfc71ac8854170" [[package]] name = "once_cell" @@ -664,9 +666,9 @@ checksum = "af8b08b04175473088b46763e51ee54da5f9a164bc162f615b91bc179dbf15a3" [[package]] name = "openssl-probe" -version = "0.1.2" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77af24da69f9d9341038eba93a073b1fdaaa1b788221b00a69bce9e762cb32de" +checksum = "28988d872ab76095a6e6ac88d99b54fd267702734fd7ffe610ca27f533ddb95a" [[package]] name = "openssl-src" @@ -1168,9 +1170,9 @@ checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" [[package]] name = "url" -version = "2.2.1" +version = "2.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ccd964113622c8e9322cfac19eb1004a07e636c545f325da085d5cdde6f1f8b" +checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" dependencies = [ "form_urlencoded", "idna", @@ -55,11 +55,11 @@ For a [RustBerlin meetup presentation](https://youtu.be/rpilJV-eIVw?t=5334) ([slides](https://github.com/extrawurst/gitui-presentation)) I compared `lazygit`,`tig` and `gitui` by parsing the entire Linux git repository (which contains over 900k commits): -| | Time | Memory (GB) | Binary (MB) | Freezes | Crashes | -| --------- | ----------- | ----------- | ----------- | --------- | --------- | -| `gitui` | **24 s** ✅ | **0.17** ✅ | 1.4 | **No** ✅ | **No** ✅ | -| `lazygit` | 57 s | 2.6 | 16 | Yes | Sometimes | -| `tig` | 4 m 20 s | 1.3 | **0.6** ✅ | Sometimes | **No** ✅ | +| | Time | Memory (GB) | Binary (MB) | Freezes | Crashes | +| --------- | ---------- | ----------- | ----------- | --------- | --------- | +| `gitui` | **24 s** ✅ | **0.17** ✅ | 1.4 | **No** ✅ | **No** ✅ | +| `lazygit` | 57 s | 2.6 | 16 | Yes | Sometimes | +| `tig` | 4 m 20 s | 1.3 | **0.6** ✅ | Sometimes | **No** ✅ | ## 3. <a name="motivation"></a> Motivation <small><sup>[Top ▲](#table-of-contents)</sup></small> @@ -71,7 +71,6 @@ Over the last 2 years my go-to GUI tool for this was [fork](https://git-fork.com These are the high level goals before calling out `1.0`: -* merging with conflicts ([#485](https://github.com/extrawurst/gitui/issues/485)) * log search (commit, author, sha) ([#449](https://github.com/extrawurst/gitui/issues/449),[#429](https://github.com/extrawurst/gitui/issues/429)) * file history log ([#381](https://github.com/extrawurst/gitui/issues/381)) * more tag support ([#483](https://github.com/extrawurst/gitui/issues/483)) diff --git a/asyncgit/Cargo.toml b/asyncgit/Cargo.toml index 894c8481..b1872a65 100644 --- a/asyncgit/Cargo.toml +++ b/asyncgit/Cargo.toml @@ -13,8 +13,9 @@ keywords = ["git"] [dependencies] scopetime = { path = "../scopetime", version = "0.1" } -# git2 = { version = "0.13", features = ["vendored-openssl"] } -git2 = { git="https://github.com/rust-lang/git2-rs.git", rev="1b1499a", features = ["vendored-openssl"] } +git2 = { version = "0.13", features = ["vendored-openssl"] } +# git2 = { path = "../../github/git2-rs", features = ["vendored-openssl"]} +# git2 = { git="https://github.com/extrawurst/git2-rs.git", rev="513a8c9", features = ["vendored-openssl"]} rayon-core = "1.9" crossbeam-channel = "0.5" log = "0.4" diff --git a/asyncgit/src/sync/branch/merge_commit.rs b/asyncgit/src/sync/branch/merge_commit.rs index 40e436c6..2084bd12 100644 --- a/asyncgit/src/sync/branch/merge_commit.rs +++ b/asyncgit/src/sync/branch/merge_commit.rs @@ -3,16 +3,18 @@ use super::BranchType; use crate::{ error::{Error, Result}, - sync::{utils, CommitId}, + sync::{merge_msg, utils, CommitId}, }; -use git2::MergeOptions; +use git2::Commit; use scopetime::scope_time; -/// merge upstream using a merge commit without conflicts. fails if not possible without conflicts +/// merge upstream using a merge commit if we did not create conflicts. +/// if we did not create conflicts we create a merge commit and return the commit id. +/// Otherwise we return `None` pub fn merge_upstream_commit( repo_path: &str, branch_name: &str, -) -> Result<CommitId> { +) -> Result<Option<CommitId>> { scope_time!("merge_upstream_commit"); let repo = utils::repo(repo_path)?; @@ -22,10 +24,10 @@ pub fn merge_upstream_commit( let upstream_commit = upstream.get().peel_to_commit()?; - let annotated_upstream = - repo.find_annotated_commit(upstream_commit.id())?; + let annotated_upstream = repo + .reference_to_annotated_commit(&upstream.into_reference())?; - let (analysis, _) = + let (analysis, pref) = repo.merge_analysis(&[&annotated_upstream])?; if !analysis.is_normal() { @@ -34,61 +36,59 @@ pub fn merge_upstream_commit( )); } - //TODO: support merge on unborn + if analysis.is_fast_forward() && pref.is_fastforward_only() { + return Err(Error::Generic( + "ff merge would be possible".into(), + )); + } + + //TODO: support merge on unborn? if analysis.is_unborn() { return Err(Error::Generic("head is unborn".into())); } - let mut opt = MergeOptions::default(); - opt.fail_on_conflict(true); + repo.merge(&[&annotated_upstream], None, None)?; + + if !repo.index()?.has_conflicts() { + let msg = merge_msg(repo_path)?; - repo.merge(&[&annotated_upstream], Some(&mut opt), None)?; + let commit_id = + commit_merge_with_head(&repo, &[upstream_commit], &msg)?; - if repo.index()?.has_conflicts() { - return Err(Error::Generic("creates conflicts".into())); + return Ok(Some(commit_id)); } + Ok(None) +} + +pub(crate) fn commit_merge_with_head( + repo: &git2::Repository, + commits: &[Commit], + msg: &str, +) -> Result<CommitId> { let signature = - crate::sync::commit::signature_allow_undefined_name(&repo)?; + crate::sync::commit::signature_allow_undefined_name(repo)?; let mut index = repo.index()?; let tree_id = index.write_tree()?; let tree = repo.find_tree(tree_id)?; - let head_commit = repo.find_commit( - crate::sync::utils::get_head_repo(&repo)?.into(), + crate::sync::utils::get_head_repo(repo)?.into(), )?; - let parents = vec![&head_commit, &upstream_commit]; - - //find remote url for this branch - let remote_url = { - let branch_refname = - branch.get().name().ok_or_else(|| { - Error::Generic(String::from( - "branch refname not found", - )) - })?; - let buf = repo.branch_upstream_remote(branch_refname)?; - let remote = - repo.find_remote(buf.as_str().ok_or_else(|| { - Error::Generic(String::from("remote name not found")) - })?)?; - remote.url().unwrap_or_default().to_string() - }; + + let mut parents = vec![&head_commit]; + parents.extend(commits); let commit_id = repo .commit( Some("HEAD"), &signature, &signature, - format!("Merge '{}' of {}", branch_name, remote_url) - .as_str(), + msg, &tree, parents.as_slice(), )? .into(); - repo.cleanup_state()?; - Ok(commit_id) } @@ -160,7 +160,9 @@ mod test { ); let merge_commit = - merge_upstream_commit(clone2_dir, "master").unwrap(); + merge_upstream_commit(clone2_dir, "master") + .unwrap() + .unwrap(); let state = crate::sync::repo_state(clone2_dir).unwrap(); assert_eq!(state, RepoState::Clean); @@ -179,15 +181,12 @@ mod test { .unwrap(); assert_eq!( details.message.unwrap().combine(), - format!( - "Merge 'master' of {}", - r1_dir.path().to_str().unwrap() - ) + String::from("Merge remote-tracking branch 'refs/remotes/origin/master'") ); } #[test] - fn test_merge_normal_conflict() { + fn test_merge_normal_non_ff() { let (r1_dir, _repo) = repo_init_bare().unwrap(); let (clone1_dir, clone1) = @@ -198,7 +197,12 @@ mod test { // clone1 - write_commit_file(&clone1, "test.bin", "test", "commit1"); + write_commit_file( + &clone1, + "test.bin", + "test\nfooo", + "commit1", + ); debug_cmd_print( clone2_dir.path().to_str().unwrap(), @@ -217,7 +221,12 @@ mod test { // clone2 - write_commit_file(&clone2, "test.bin", "foobar", "commit2"); + write_commit_file( + &clone2, + "test.bin", + "foobar\ntest", + "commit2", + ); let bytes = fetch( clone2_dir.path().to_str().unwrap(), @@ -231,18 +240,19 @@ mod test { let res = merge_upstream_commit( clone2_dir.path().to_str().unwrap(), "master", - ); + ) + .unwrap(); - //this should have failed cause it would create a conflict - assert!(res.is_err()); + //this should not have commited cause we left conflicts behind + assert_eq!(res, None); let state = crate::sync::repo_state( clone2_dir.path().to_str().unwrap(), ) .unwrap(); - //make sure we left the repo not in some merging state - assert_eq!(state, RepoState::Clean); + //validate the repo is in a merge state now + assert_eq!(state, RepoState::Merge); //check that we still only have the first commit let commits = get_commit_ids(&clone1, 10); diff --git a/asyncgit/src/sync/branch/merge_ff.rs b/asyncgit/src/sync/branch/merge_ff.rs index f5265a70..fb45fe67 100644 --- a/asyncgit/src/sync/branch/merge_ff.rs +++ b/asyncgit/src/sync/branch/merge_ff.rs @@ -25,7 +25,7 @@ pub fn branch_merge_upstream_fastforward( let annotated = repo.find_annotated_commit(upstream_commit.id())?; - let (analysis, _) = repo.merge_analysis(&[&annotated])?; + let (analysis, pref) = repo.merge_analysis(&[&annotated])?; if !analysis.is_fast_forward() { return Err(Error::Generic( @@ -33,6 +33,10 @@ pub fn branch_merge_upstream_fastforward( )); } + if pref.is_no_fast_forward() { + return Err(Error::Generic("fast forward not wanted".into())); + } + //TODO: support merge on unborn if analysis.is_unborn() { return Err(Error::Generic("head is unborn".into())); diff --git a/asyncgit/src/sync/merge.rs b/asyncgit/src/sync/merge.rs new file mode 100644 index 00000000..bb6e5d2f --- /dev/null +++ b/asyncgit/src/sync/merge.rs @@ -0,0 +1,141 @@ +use std::fs::read_to_string; + +use crate::{ + error::{Error, Result}, + sync::{ + branch::merge_commit::commit_merge_with_head, reset_stage, + reset_workdir, utils, CommitId, + }, +}; +use git2::{BranchType, Commit, MergeOptions, Repository}; +use scopetime::scope_time; + +/// +pub fn mergehead_ids(repo_path: &str) -> Result<Vec<CommitId>> { + scope_time!("mergehead_ids"); + + let mut repo = utils::repo(repo_path)?; + + let mut ids: Vec<CommitId> = Vec::new(); + repo.mergehead_foreach(|id| { + ids.push(CommitId::from(*id)); + true + })?; + + Ok(ids) +} + +/// does these steps: +/// * reset all staged changes, +/// * revert all changes in workdir +/// * cleanup repo merge state +pub fn abort_merge(repo_path: &str) -> Result<()> { + scope_time!("cleanup_state"); + + let repo = utils::repo(repo_path)?; + + reset_stage(repo_path, "*")?; + reset_workdir(repo_path, "*")?; + + repo.cleanup_state()?; + + Ok(()) +} + +/// +pub fn merge_branch(repo_path: &str, branch: &str) -> Result<()> { + scope_time!("merge_branch"); + + let repo = utils::repo(repo_path)?; + + merge_branch_repo(&repo, branch)?; + + Ok(()) +} + +/// +pub fn merge_branch_repo( + repo: &Repository, + branch: &str, +) -> Result<()> { + let branch = repo.find_branch(branch, BranchType::Local)?; + + let annotated = + repo.reference_to_annotated_commit(&branch.into_reference())?; + + let (analysis, _) = repo.merge_analysis(&[&annotated])?; + + //TODO: support merge on unborn + if analysis.is_unborn() { + return Err(Error::Generic("head is unborn".into())); + } + + let mut opt = MergeOptions::default(); + + repo.merge(&[&annotated], Some(&mut opt), None)?; + + Ok(()) +} + +/// +pub fn merge_msg(repo_path: &str) -> Result<String> { + scope_time!("merge_msg"); + + let repo = utils::repo(repo_path)?; + + let msg_file = repo.path().join("MERGE_MSG"); + + let content = read_to_string(msg_file).unwrap_or_default(); + + Ok(content) +} + +/// +pub fn merge_commit( + repo_path: &str, + msg: &str, + ids: &[CommitId], +) -> Result<CommitId> { + scope_time!("merge_commit"); + + let repo = utils::repo(repo_path)?; + + let mut commits: Vec<Commit> = Vec::new(); + + for id in ids { + commits.push(repo.find_commit((*id).into())?); + } + + let id = commit_merge_with_head(&repo, &commits, msg)?; + + Ok(id) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::sync::{ + create_branch, + tests::{repo_init, write_commit_file}, + }; + + #[test] + fn test_smoke() { + let (_td, repo) = repo_init().unwrap(); + let root = repo.path().parent().unwrap(); + let repo_path = root.as_os_str().to_str().unwrap(); + + let c1 = + write_commit_file(&repo, "test.txt", "test", "commit1"); + + create_branch(repo_path, "foo").unwrap(); + + write_commit_file(&repo, "test.txt", "test2", "commit2"); + + merge_branch(repo_path, "master").unwrap(); + + let mergeheads = mergehead_ids(repo_path).unwrap(); + + assert_eq!(mergeheads[0], c1); + } +} diff --git a/asyncgit/src/sync/mod.rs b/asyncgit/src/sync/mod.rs index 1bcba4b8..838724d6 100644 --- a/asyncgit/src/sync/mod.rs +++ b/asyncgit/src/sync/mod.rs @@ -15,6 +15,7 @@ mod hooks; mod hunks; mod ignore; mod logwalker; +mod merge; mod patches; pub mod remotes; mod reset; @@ -49,6 +50,9 @@ pub use hooks::{ pub use hunks::{reset_hunk, stage_hunk, unstage_hunk}; pub use ignore::add_to_ignore; pub use logwalker::LogWalker; +pub use merge::{ + abort_merge, merge_branch, merge_commit, merge_msg, mergehead_ids, +}; pub use remotes::{ get_default_remote, get_remotes, push::AsyncProgress, tags::PushTagsProgress, @@ -475,6 +475,9 @@ impl App { if flags.contains(NeedsUpdate::COMMANDS) { self.update_commands(); } + if flags.contains(NeedsUpdate::BRANCHES) { + self.select_branch_popup.update_branches()?; + } Ok(()) } @@ -607,6 +610,10 @@ impl App { self.pull_popup.try_conflict_free_merge(rebase); flags.insert(NeedsUpdate::ALL); } + Action::AbortMerge => { + self.status_tab.abort_merge(); + flags.insert(NeedsUpdate::ALL); + } }; Ok(()) diff --git a/src/components/branchlist.rs b/src/components/branchlist.rs index de41295c..7db47b4f 100644 --- a/src/components/branchlist.rs +++ b/src/components/branchlist.rs @@ -12,7 +12,7 @@ use crate::{ use anyhow::Result; use asyncgit::{ sync::{ - branch::checkout_remote_branch, checkout_branch, + self, branch::checkout_remote_branch, checkout_branch, get_branches_info, BranchInfo, }, CWD, @@ -151,6 +151,14 @@ impl Component for BranchListComponent { )); out.push(CommandInfo::new( + strings::commands::merge_branch_popup( + &self.key_config, + ), + !self.selection_is_cur_branch(), + self.local, + )); + + out.push(CommandInfo::new( strings::commands::rename_branch_popup( &self.key_config, ), @@ -194,7 +202,6 @@ impl Component for BranchListComponent { self.queue .borrow_mut() .push_back(InternalEvent::CreateBranch); - self.hide(); } else if e == self.key_config.rename_branch && self.valid_selection() { @@ -222,6 +229,19 @@ impl Component for BranchListComponent { ), ), ); + } else if e == self.key_config.merge_branch + && !self.selection_is_cur_branch() + && self.valid_selection() + { + try_or_popup!( + self, + "merge branch error:", + self.merge_branch() + ); + self.hide(); + self.queue.borrow_mut().push_back( + InternalEvent::Update(NeedsUpdate::ALL), + ); } else if e == self.key_config.tab_toggle { self.local = !self.local; self.update_branches()?; @@ -294,6 +314,16 @@ impl BranchListComponent { !self.branches.is_empty() } + fn merge_branch(&self) -> Result<()> { + if let Some(branch) = + self.branches.get(usize::from(self.selection)) + { + sync::merge_branch(CWD, &branch.name)?; + } + + Ok(()) + } + fn selection_is_cur_branch(&self) -> bool { self.branches .iter() diff --git a/src/components/commit.rs b/src/components/commit.rs index c7b7efea..49ff4df7 100644 --- a/src/components/commit.rs +++ b/src/components/commit.rs @@ -13,7 +13,10 @@ use crate::{ use anyhow::Result; use asyncgit::{ cached, - sync::{self, utils::get_config_string, CommitId, HookResult}, + sync::{ + self, utils::get_config_string, CommitId, HookResult, + RepoState, + }, CWD, }; use crossterm::event::Event; @@ -30,9 +33,15 @@ use tui::{ Frame, }; +enum Mode { + Normal, + Amend(CommitId), + Merge(Vec<CommitId>), +} + pub struct CommitComponent { input: TextInputComponent, - amend: Option<CommitId>, + mode: Mode, queue: Queue, key_config: SharedKeyConfig, git_branch_name: cached::BranchName, @@ -128,25 +137,34 @@ impl Component for CommitComponent { } fn show(&mut self) -> Result<()> { - if self.amend.is_some() { + //only clear text if it was not a normal commit dlg before, so to preserve old commit msg that was edited + if !matches!(self.mode, Mode::Normal) { self.input.clear(); } - self.amend = None; - - self.input - .set_title(strings::commit_title(&self.key_config)); - self.commit_template = - get_config_string(CWD, "commit.template") - .ok() - .flatten() - .and_then(|path| read_to_string(path).ok()); - - if self.is_empty() { - if let Some(s) = &self.commit_template { - self.input.set_text(s.clone()); + self.mode = Mode::Normal; + + self.mode = if sync::repo_state(CWD)? == RepoState::Merge { + let ids = sync::mergehead_ids(CWD)?; + self.input.set_title(strings::commit_title_merge()); + self.input.set_text(sync::merge_msg(CWD)?); + Mode::Merge(ids) + } else { + self.commit_template = + get_config_string(CWD, "commit.template") + .ok() + .flatten() + .and_then(|path| read_to_string(path).ok()); + + if self.is_empty() { + if let Some(s) = &self.commit_template { + self.input.set_text(s.clone()); + } } - } + + self.input.set_title(strings::commit_title()); + Mode::Normal + }; self.input.show()?; @@ -163,7 +181,8 @@ impl CommitComponent { ) -> Self { Self { queue, - amend: None, + mode: Mode::Normal, + input: TextInputComponent::new( theme.clone(), key_config.clone(), @@ -281,10 +300,10 @@ impl CommitComponent { fn commit(&mut self) -> Result<()> { let msg = self.input.get_text().clone(); self.input.clear(); - self.commit_msg(msg) + self.commit_with_msg(msg) } - fn commit_msg(&mut self, msg: String) -> Result<()> { + fn commit_with_msg(&mut self, msg: String) -> Result<()> { if let HookResult::NotOk(e) = sync::hooks_pre_commit(CWD)? { log::error!("pre-commit hook error: {}", e); self.queue.borrow_mut().push_back( @@ -309,10 +328,12 @@ impl CommitComponent { return Ok(()); } - let res = self.amend.map_or_else( - || sync::commit(CWD, &msg), - |amend| sync::amend(CWD, amend, &msg), - ); + let res = match &self.mode { + Mode::Normal => sync::commit(CWD, &msg), + Mode::Amend(amend) => sync::amend(CWD, *amend, &msg), + Mode::Merge(ids) => sync::merge_commit(CWD, &msg, ids), + }; + if let Err(e) = res { log::error!("commit error: {}", &e); self.queue.borrow_mut().push_back( @@ -348,7 +369,7 @@ impl CommitComponent { } fn can_amend(&self) -> bool { - self.amend.is_none() + matches!(self.mode, Mode::Normal) && sync::get_head(CWD).is_ok() && (self.is_empty() || !self.is_changed()) } @@ -363,16 +384,19 @@ impl CommitComponent { } fn amend(&mut self) -> Result<()> { - let id = sync::get_head(CWD)?; - self.amend = Some(id); + if self.can_amend() { + let id = sync::get_head(CWD)?; + self.mode = Mode::Amend(id); - let details = sync::get_commit_details(CWD, id)?; + let details = sync::get_commit_details(CWD, id)?; - self.input - .set_title(strings::commit_title_amend(&self.key_config)); + self.input.set_title(strings::commit_title_amend( + &self.key_config, + )); - if let Some(msg) = details.message { - self.input.set_text(msg.combine()); + if let Some(msg) = details.message { + self.input.set_text(msg.combine()); + } } Ok(()) diff --git a/src/components/create_branch.rs b/src/components/create_branch.rs index d74b1ec7..f3c77ebb 100644 --- a/src/components/create_branch.rs +++ b/src/components/create_branch.rs @@ -123,7 +123,7 @@ impl CreateBranchComponent { match res { Ok(_) => { self.queue.borrow_mut().push_back( - InternalEvent::Update(NeedsUpdate::ALL), + InternalEvent::Update(NeedsUpdate::BRANCHES), ); } Err(e) => { diff --git a/src/components/reset.rs b/src/components/reset.rs index eeef90d1..2004545f 100644 --- a/src/components/reset.rs +++ b/src/components/reset.rs @@ -138,8 +138,8 @@ impl ResetComponent { if let Some(ref a) = self.target { return match a { Action::Reset(_) => ( - strings::confirm_title_reset(&self.key_config), |