mirror of
https://github.com/gitui-org/gitui
synced 2026-05-23 08:58:21 +00:00
Merge branch 'master'
Conflicts: Cargo.lock asyncgit/Cargo.toml
This commit is contained in:
commit
378b8655ab
22 changed files with 544 additions and 213 deletions
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -12,10 +12,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||

|
||||
|
||||
## 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
|
||||
|
||||
|
|
|
|||
34
Cargo.lock
generated
34
Cargo.lock
generated
|
|
@ -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",
|
||||
|
|
|
|||
11
README.md
11
README.md
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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)?;
|
||||
|
||||
repo.merge(&[&annotated_upstream], Some(&mut opt), None)?;
|
||||
if !repo.index()?.has_conflicts() {
|
||||
let msg = merge_msg(repo_path)?;
|
||||
|
||||
if repo.index()?.has_conflicts() {
|
||||
return Err(Error::Generic("creates conflicts".into()));
|
||||
let commit_id =
|
||||
commit_merge_with_head(&repo, &[upstream_commit], &msg)?;
|
||||
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -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()));
|
||||
|
|
|
|||
141
asyncgit/src/sync/merge.rs
Normal file
141
asyncgit/src/sync/merge.rs
Normal file
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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(())
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
@ -150,6 +150,14 @@ impl Component for BranchListComponent {
|
|||
self.local,
|
||||
));
|
||||
|
||||
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()
|
||||
|
|
|
|||
|
|
@ -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.mode = Mode::Normal;
|
||||
|
||||
self.commit_template =
|
||||
get_config_string(CWD, "commit.template")
|
||||
.ok()
|
||||
.flatten()
|
||||
.and_then(|path| read_to_string(path).ok());
|
||||
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());
|
||||
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(())
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
strings::confirm_msg_reset(&self.key_config),
|
||||
strings::confirm_title_reset(),
|
||||
strings::confirm_msg_reset(),
|
||||
),
|
||||
Action::StashDrop(_) => (
|
||||
strings::confirm_title_stashdrop(
|
||||
|
|
@ -152,12 +152,12 @@ impl ResetComponent {
|
|||
strings::confirm_msg_stashpop(&self.key_config),
|
||||
),
|
||||
Action::ResetHunk(_, _) => (
|
||||
strings::confirm_title_reset(&self.key_config),
|
||||
strings::confirm_title_reset(),
|
||||
strings::confirm_msg_resethunk(&self.key_config),
|
||||
),
|
||||
Action::ResetLines(_, lines) => (
|
||||
strings::confirm_title_reset(&self.key_config),
|
||||
strings::confirm_msg_reset_lines(&self.key_config,lines.len()),
|
||||
strings::confirm_title_reset(),
|
||||
strings::confirm_msg_reset_lines(lines.len()),
|
||||
),
|
||||
Action::DeleteBranch(branch_ref) => (
|
||||
strings::confirm_title_delete_branch(
|
||||
|
|
@ -181,6 +181,10 @@ impl ResetComponent {
|
|||
strings::confirm_title_merge(&self.key_config,*rebase),
|
||||
strings::confirm_msg_merge(&self.key_config,*incoming,*rebase),
|
||||
),
|
||||
Action::AbortMerge => (
|
||||
strings::confirm_title_abortmerge(),
|
||||
strings::confirm_msg_abortmerge(),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -207,6 +207,7 @@ impl FileTreeItems {
|
|||
for c in &ancestors {
|
||||
if c.parent().is_some() && !paths_added.contains(c) {
|
||||
paths_added.insert(c);
|
||||
//TODO: get rid of expect
|
||||
let path_string =
|
||||
String::from(c.to_str().expect("invalid path"));
|
||||
let is_collapsed = collapsed.contains(&path_string);
|
||||
|
|
|
|||
68
src/input.rs
68
src/input.rs
|
|
@ -1,7 +1,9 @@
|
|||
use crate::notify_mutex::NotifyableMutex;
|
||||
use crossbeam_channel::{unbounded, Receiver};
|
||||
use anyhow::Result;
|
||||
use crossbeam_channel::{unbounded, Receiver, Sender};
|
||||
use crossterm::event::{self, Event};
|
||||
use std::{
|
||||
process,
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
|
|
@ -44,33 +46,12 @@ impl Input {
|
|||
let arc_desired = Arc::clone(&desired_state);
|
||||
let arc_current = Arc::clone(¤t_state);
|
||||
|
||||
thread::spawn(move || loop {
|
||||
if arc_desired.get() {
|
||||
if !arc_current.load(Ordering::Relaxed) {
|
||||
log::info!("input polling resumed");
|
||||
|
||||
tx.send(InputEvent::State(InputState::Polling))
|
||||
.expect("send state failed");
|
||||
}
|
||||
arc_current.store(true, Ordering::Relaxed);
|
||||
|
||||
if let Some(e) = Self::poll(POLL_DURATION)
|
||||
.expect("failed to pull events.")
|
||||
{
|
||||
tx.send(InputEvent::Input(e))
|
||||
.expect("send input failed");
|
||||
}
|
||||
} else {
|
||||
if arc_current.load(Ordering::Relaxed) {
|
||||
log::info!("input polling suspended");
|
||||
|
||||
tx.send(InputEvent::State(InputState::Paused))
|
||||
.expect("send state failed");
|
||||
}
|
||||
|
||||
arc_current.store(false, Ordering::Relaxed);
|
||||
|
||||
arc_desired.wait(true);
|
||||
thread::spawn(move || {
|
||||
if let Err(e) =
|
||||
Self::input_loop(&arc_desired, &arc_current, &tx)
|
||||
{
|
||||
log::error!("input thread error: {}", e);
|
||||
process::abort();
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -108,4 +89,35 @@ impl Input {
|
|||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
fn input_loop(
|
||||
arc_desired: &Arc<NotifyableMutex<bool>>,
|
||||
arc_current: &Arc<AtomicBool>,
|
||||
tx: &Sender<InputEvent>,
|
||||
) -> Result<()> {
|
||||
loop {
|
||||
if arc_desired.get() {
|
||||
if !arc_current.load(Ordering::Relaxed) {
|
||||
log::info!("input polling resumed");
|
||||
|
||||
tx.send(InputEvent::State(InputState::Polling))?;
|
||||
}
|
||||
arc_current.store(true, Ordering::Relaxed);
|
||||
|
||||
if let Some(e) = Self::poll(POLL_DURATION)? {
|
||||
tx.send(InputEvent::Input(e))?;
|
||||
}
|
||||
} else {
|
||||
if arc_current.load(Ordering::Relaxed) {
|
||||
log::info!("input polling suspended");
|
||||
|
||||
tx.send(InputEvent::State(InputState::Paused))?
|
||||
}
|
||||
|
||||
arc_current.store(false, Ordering::Relaxed);
|
||||
|
||||
arc_desired.wait(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
10
src/keys.rs
10
src/keys.rs
|
|
@ -68,9 +68,11 @@ pub struct KeyConfig {
|
|||
pub rename_branch: KeyEvent,
|
||||
pub select_branch: KeyEvent,
|
||||
pub delete_branch: KeyEvent,
|
||||
pub merge_branch: KeyEvent,
|
||||
pub push: KeyEvent,
|
||||
pub force_push: KeyEvent,
|
||||
pub pull: KeyEvent,
|
||||
pub abort_merge: KeyEvent,
|
||||
}
|
||||
|
||||
#[rustfmt::skip]
|
||||
|
|
@ -121,13 +123,15 @@ impl Default for KeyConfig {
|
|||
log_tag_commit: KeyEvent { code: KeyCode::Char('t'), modifiers: KeyModifiers::empty()},
|
||||
commit_amend: KeyEvent { code: KeyCode::Char('a'), modifiers: KeyModifiers::CONTROL},
|
||||
copy: KeyEvent { code: KeyCode::Char('y'), modifiers: KeyModifiers::empty()},
|
||||
create_branch: KeyEvent { code: KeyCode::Char('c'), modifiers: KeyModifiers::NONE},
|
||||
rename_branch: KeyEvent { code: KeyCode::Char('r'), modifiers: KeyModifiers::NONE},
|
||||
select_branch: KeyEvent { code: KeyCode::Char('b'), modifiers: KeyModifiers::NONE},
|
||||
create_branch: KeyEvent { code: KeyCode::Char('c'), modifiers: KeyModifiers::empty()},
|
||||
rename_branch: KeyEvent { code: KeyCode::Char('r'), modifiers: KeyModifiers::empty()},
|
||||
select_branch: KeyEvent { code: KeyCode::Char('b'), modifiers: KeyModifiers::empty()},
|
||||
delete_branch: KeyEvent{code: KeyCode::Char('D'), modifiers: KeyModifiers::SHIFT},
|
||||
merge_branch: KeyEvent{code: KeyCode::Char('m'), modifiers: KeyModifiers::empty()},
|
||||
push: KeyEvent { code: KeyCode::Char('p'), modifiers: KeyModifiers::empty()},
|
||||
force_push: KeyEvent { code: KeyCode::Char('P'), modifiers: KeyModifiers::SHIFT},
|
||||
pull: KeyEvent { code: KeyCode::Char('f'), modifiers: KeyModifiers::empty()},
|
||||
abort_merge: KeyEvent { code: KeyCode::Char('M'), modifiers: KeyModifiers::SHIFT},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
28
src/main.rs
28
src/main.rs
|
|
@ -11,6 +11,8 @@
|
|||
#![deny(clippy::needless_update)]
|
||||
#![allow(clippy::module_name_repetitions)]
|
||||
#![allow(clippy::multiple_crate_versions)]
|
||||
//TODO:
|
||||
// #![deny(clippy::expect_used)]
|
||||
|
||||
mod app;
|
||||
mod clipboard;
|
||||
|
|
@ -100,7 +102,7 @@ fn main() -> Result<()> {
|
|||
|
||||
setup_terminal()?;
|
||||
defer! {
|
||||
shutdown_terminal().expect("shutdown failed");
|
||||
shutdown_terminal();
|
||||
}
|
||||
|
||||
set_panic_handlers()?;
|
||||
|
|
@ -181,10 +183,19 @@ fn setup_terminal() -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn shutdown_terminal() -> Result<()> {
|
||||
io::stdout().execute(LeaveAlternateScreen)?;
|
||||
disable_raw_mode()?;
|
||||
Ok(())
|
||||
fn shutdown_terminal() {
|
||||
let leave_screen =
|
||||
io::stdout().execute(LeaveAlternateScreen).map(|_f| ());
|
||||
|
||||
if let Err(e) = leave_screen {
|
||||
eprintln!("leave_screen failed:\n{}", e);
|
||||
}
|
||||
|
||||
let leave_raw_mode = disable_raw_mode();
|
||||
|
||||
if let Err(e) = leave_raw_mode {
|
||||
eprintln!("leave_raw_mode failed:\n{}", e);
|
||||
}
|
||||
}
|
||||
|
||||
fn draw<B: Backend>(
|
||||
|
|
@ -336,19 +347,20 @@ fn set_panic_handlers() -> Result<()> {
|
|||
// regular panic handler
|
||||
panic::set_hook(Box::new(|e| {
|
||||
let backtrace = Backtrace::new();
|
||||
//TODO: create macro to do both in one
|
||||
log::error!("panic: {:?}\ntrace:\n{:?}", e, backtrace);
|
||||
shutdown_terminal().expect("shutdown failed inside panic");
|
||||
eprintln!("panic: {:?}\ntrace:\n{:?}", e, backtrace);
|
||||
shutdown_terminal();
|
||||
}));
|
||||
|
||||
// global threadpool
|
||||
rayon_core::ThreadPoolBuilder::new()
|
||||
.panic_handler(|e| {
|
||||
let backtrace = Backtrace::new();
|
||||
//TODO: create macro to do both in one
|
||||
log::error!("panic: {:?}\ntrace:\n{:?}", e, backtrace);
|
||||
shutdown_terminal()
|
||||
.expect("shutdown failed inside panic");
|
||||
eprintln!("panic: {:?}\ntrace:\n{:?}", e, backtrace);
|
||||
shutdown_terminal();
|
||||
process::abort();
|
||||
})
|
||||
.num_threads(4)
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ bitflags! {
|
|||
const DIFF = 0b010;
|
||||
/// commands might need updating (app::update_commands)
|
||||
const COMMANDS = 0b100;
|
||||
/// branches have changed
|
||||
const BRANCHES = 0b1000;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -33,6 +35,7 @@ pub enum Action {
|
|||
DeleteBranch(String),
|
||||
ForcePush(String, bool),
|
||||
PullMerge { incoming: usize, rebase: bool },
|
||||
AbortMerge,
|
||||
}
|
||||
|
||||
///
|
||||
|
|
|
|||
|
|
@ -62,9 +62,12 @@ pub fn msg_opening_editor(_key_config: &SharedKeyConfig) -> String {
|
|||
pub fn msg_title_error(_key_config: &SharedKeyConfig) -> String {
|
||||
"Error".to_string()
|
||||
}
|
||||
pub fn commit_title(_key_config: &SharedKeyConfig) -> String {
|
||||
pub fn commit_title() -> String {
|
||||
"Commit".to_string()
|
||||
}
|
||||
pub fn commit_title_merge() -> String {
|
||||
"Commit (Merge)".to_string()
|
||||
}
|
||||
pub fn commit_title_amend(_key_config: &SharedKeyConfig) -> String {
|
||||
"Commit (Amend)".to_string()
|
||||
}
|
||||
|
|
@ -86,7 +89,7 @@ pub fn stash_popup_title(_key_config: &SharedKeyConfig) -> String {
|
|||
pub fn stash_popup_msg(_key_config: &SharedKeyConfig) -> String {
|
||||
"type name (optional)".to_string()
|
||||
}
|
||||
pub fn confirm_title_reset(_key_config: &SharedKeyConfig) -> String {
|
||||
pub fn confirm_title_reset() -> String {
|
||||
"Reset".to_string()
|
||||
}
|
||||
pub fn confirm_title_stashdrop(
|
||||
|
|
@ -120,13 +123,17 @@ pub fn confirm_msg_merge(
|
|||
format!("Merge of {} incoming commits?", incoming)
|
||||
}
|
||||
}
|
||||
pub fn confirm_msg_reset(_key_config: &SharedKeyConfig) -> String {
|
||||
|
||||
pub fn confirm_title_abortmerge() -> String {
|
||||
"Abort merge?".to_string()
|
||||
}
|
||||
pub fn confirm_msg_abortmerge() -> String {
|
||||
"This will revert all changes. Are you sure?".to_string()
|
||||
}
|
||||
pub fn confirm_msg_reset() -> String {
|
||||
"confirm file reset?".to_string()
|
||||
}
|
||||
pub fn confirm_msg_reset_lines(
|
||||
_key_config: &SharedKeyConfig,
|
||||
lines: usize,
|
||||
) -> String {
|
||||
pub fn confirm_msg_reset_lines(lines: usize) -> String {
|
||||
format!(
|
||||
"are you sure you want to discard {} selected lines?",
|
||||
lines
|
||||
|
|
@ -520,6 +527,16 @@ pub mod commands {
|
|||
CMD_GROUP_GENERAL,
|
||||
)
|
||||
}
|
||||
pub fn abort_merge(key_config: &SharedKeyConfig) -> CommandText {
|
||||
CommandText::new(
|
||||
format!(
|
||||
"Abort merge [{}]",
|
||||
key_config.get_hint(key_config.abort_merge),
|
||||
),
|
||||
"abort ongoing merge",
|
||||
CMD_GROUP_GENERAL,
|
||||
)
|
||||
}
|
||||
pub fn select_staging(
|
||||
key_config: &SharedKeyConfig,
|
||||
) -> CommandText {
|
||||
|
|
@ -918,6 +935,18 @@ pub mod commands {
|
|||
CMD_GROUP_GENERAL,
|
||||
)
|
||||
}
|
||||
pub fn merge_branch_popup(
|
||||
key_config: &SharedKeyConfig,
|
||||
) -> CommandText {
|
||||
CommandText::new(
|
||||
format!(
|
||||
"Merge [{}]",
|
||||
key_config.get_hint(key_config.merge_branch),
|
||||
),
|
||||
"merge a branch",
|
||||
CMD_GROUP_GENERAL,
|
||||
)
|
||||
}
|
||||
pub fn select_branch_popup(
|
||||
key_config: &SharedKeyConfig,
|
||||
) -> CommandText {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ use crate::{
|
|||
},
|
||||
keys::SharedKeyConfig,
|
||||
queue::{Action, InternalEvent, Queue, ResetItem},
|
||||
strings,
|
||||
strings, try_or_popup,
|
||||
ui::style::SharedTheme,
|
||||
};
|
||||
use anyhow::Result;
|
||||
|
|
@ -21,6 +21,7 @@ use asyncgit::{
|
|||
};
|
||||
use crossbeam_channel::Sender;
|
||||
use crossterm::event::Event;
|
||||
use itertools::Itertools;
|
||||
use std::convert::Into;
|
||||
use std::convert::TryFrom;
|
||||
use tui::{
|
||||
|
|
@ -118,7 +119,7 @@ impl DrawableComponent for Status {
|
|||
self.index.draw(f, left_chunks[1])?;
|
||||
self.diff.draw(f, chunks[1])?;
|
||||
self.draw_branch_state(f, &left_chunks);
|
||||
Self::draw_repo_state(f, left_chunks[0]);
|
||||
Self::draw_repo_state(f, left_chunks[0])?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -213,12 +214,21 @@ impl Status {
|
|||
fn draw_repo_state<B: tui::backend::Backend>(
|
||||
f: &mut tui::Frame<B>,
|
||||
r: tui::layout::Rect,
|
||||
) {
|
||||
if let Ok(state) = asyncgit::sync::repo_state(CWD) {
|
||||
) -> Result<()> {
|
||||
if let Ok(state) = sync::repo_state(CWD) {
|
||||
if state != RepoState::Clean {
|
||||
let txt = format!("{:?}", state);
|
||||
let txt_len = u16::try_from(txt.len())
|
||||
.expect("state name too long");
|
||||
let ids =
|
||||
sync::mergehead_ids(CWD).unwrap_or_default();
|
||||
let ids = format!(
|
||||
"({})",
|
||||
ids.iter()
|
||||
.map(|id| sync::CommitId::get_short_string(
|
||||
id
|
||||
))
|
||||
.join(",")
|
||||
);
|
||||
let txt = format!("{:?} {}", state, ids);
|
||||
let txt_len = u16::try_from(txt.len())?;
|
||||
let w = Paragraph::new(txt)
|
||||
.style(Style::default().fg(Color::Red))
|
||||
.alignment(Alignment::Left);
|
||||
|
|
@ -235,6 +245,8 @@ impl Status {
|
|||
f.render_widget(w, rect);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn can_focus_diff(&self) -> bool {
|
||||
|
|
@ -464,6 +476,61 @@ impl Status {
|
|||
.as_ref()
|
||||
.map_or(true, |state| state.ahead > 0)
|
||||
}
|
||||
|
||||
fn can_abort_merge() -> bool {
|
||||
sync::repo_state(CWD).unwrap_or(RepoState::Clean)
|
||||
== RepoState::Merge
|
||||
}
|
||||
|
||||
pub fn abort_merge(&self) {
|
||||
try_or_popup!(self, "abort merge", sync::abort_merge(CWD))
|
||||
}
|
||||
|
||||
fn commands_nav(
|
||||
&self,
|
||||
out: &mut Vec<CommandInfo>,
|
||||
force_all: bool,
|
||||
) {
|
||||
let focus_on_diff = self.is_focus_on_diff();
|
||||
out.push(
|
||||
CommandInfo::new(
|
||||
strings::commands::diff_focus_left(&self.key_config),
|
||||
true,
|
||||
(self.visible && focus_on_diff) || force_all,
|
||||
)
|
||||
.order(strings::order::NAV),
|
||||
);
|
||||
out.push(
|
||||
CommandInfo::new(
|
||||
strings::commands::diff_focus_right(&self.key_config),
|
||||
self.can_focus_diff(),
|
||||
(self.visible && !focus_on_diff) || force_all,
|
||||
)
|
||||
.order(strings::order::NAV),
|
||||
);
|
||||
out.push(
|
||||
CommandInfo::new(
|
||||
strings::commands::select_staging(&self.key_config),
|
||||
!focus_on_diff,
|
||||
(self.visible
|
||||
&& !focus_on_diff
|
||||
&& self.focus == Focus::WorkDir)
|
||||
|| force_all,
|
||||
)
|
||||
.order(strings::order::NAV),
|
||||
);
|
||||
out.push(
|
||||
CommandInfo::new(
|
||||
strings::commands::select_unstaged(&self.key_config),
|
||||
!focus_on_diff,
|
||||
(self.visible
|
||||
&& !focus_on_diff
|
||||
&& self.focus == Focus::Stage)
|
||||
|| force_all,
|
||||
)
|
||||
.order(strings::order::NAV),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for Status {
|
||||
|
|
@ -506,6 +573,12 @@ impl Component for Status {
|
|||
true,
|
||||
!focus_on_diff,
|
||||
));
|
||||
|
||||
out.push(CommandInfo::new(
|
||||
strings::commands::abort_merge(&self.key_config),
|
||||
true,
|
||||
Self::can_abort_merge() || force_all,
|
||||
));
|
||||
}
|
||||
|
||||
{
|
||||
|
|
@ -518,52 +591,6 @@ impl Component for Status {
|
|||
},
|
||||
self.visible || force_all,
|
||||
));
|
||||
out.push(
|
||||
CommandInfo::new(
|
||||
strings::commands::diff_focus_left(
|
||||
&self.key_config,
|
||||
),
|
||||
true,
|
||||
(self.visible && focus_on_diff) || force_all,
|
||||
)
|
||||
.order(strings::order::NAV),
|
||||
);
|
||||
out.push(
|
||||
CommandInfo::new(
|
||||
strings::commands::diff_focus_right(
|
||||
&self.key_config,
|
||||
),
|
||||
self.can_focus_diff(),
|
||||
(self.visible && !focus_on_diff) || force_all,
|
||||
)
|
||||
.order(strings::order::NAV),
|
||||
);
|
||||
out.push(
|
||||
CommandInfo::new(
|
||||
strings::commands::select_staging(
|
||||
&self.key_config,
|
||||
),
|
||||
!focus_on_diff,
|
||||
(self.visible
|
||||
&& !focus_on_diff
|
||||
&& self.focus == Focus::WorkDir)
|
||||
|| force_all,
|
||||
)
|
||||
.order(strings::order::NAV),
|
||||
);
|
||||
out.push(
|
||||
CommandInfo::new(
|
||||
strings::commands::select_unstaged(
|
||||
&self.key_config,
|
||||
),
|
||||
!focus_on_diff,
|
||||
(self.visible
|
||||
&& !focus_on_diff
|
||||
&& self.focus == Focus::Stage)
|
||||
|| force_all,
|
||||
)
|
||||
.order(strings::order::NAV),
|
||||
);
|
||||
|
||||
out.push(
|
||||
CommandInfo::new(
|
||||
|
|
@ -575,6 +602,8 @@ impl Component for Status {
|
|||
)
|
||||
.hidden(),
|
||||
);
|
||||
|
||||
self.commands_nav(out, force_all);
|
||||
}
|
||||
|
||||
visibility_blocking(self)
|
||||
|
|
@ -652,6 +681,16 @@ impl Component for Status {
|
|||
&& !self.is_focus_on_diff()
|
||||
{
|
||||
self.pull();
|
||||
Ok(EventState::Consumed)
|
||||
} else if k == self.key_config.abort_merge
|
||||
&& Self::can_abort_merge()
|
||||
{
|
||||
self.queue.borrow_mut().push_back(
|
||||
InternalEvent::ConfirmAction(
|
||||
Action::AbortMerge,
|
||||
),
|
||||
);
|
||||
|
||||
Ok(EventState::Consumed)
|
||||
} else {
|
||||
Ok(EventState::NotConsumed)
|
||||
|
|
|
|||
|
|
@ -72,6 +72,9 @@
|
|||
rename_branch: ( code: Char('r'), modifiers: ( bits: 0,),),
|
||||
select_branch: ( code: Char('b'), modifiers: ( bits: 0,),),
|
||||
delete_branch: ( code: Char('D'), modifiers: ( bits: 1,),),
|
||||
merge_branch: ( code: Char('m'), modifiers: ( bits: 0,),),
|
||||
abort_merge: ( code: Char('M'), modifiers: ( bits: 1,),),
|
||||
|
||||
push: ( code: Char('p'), modifiers: ( bits: 0,),),
|
||||
force_push: ( code: Char('P'), modifiers: ( bits: 1,),),
|
||||
pull: ( code: Char('f'), modifiers: ( bits: 0,),),
|
||||
|
|
|
|||
Loading…
Reference in a new issue