Merge branch 'master'

Conflicts:
	Cargo.lock
	asyncgit/Cargo.toml
This commit is contained in:
Stephan Dilly 2021-05-12 15:34:36 +02:00
commit 378b8655ab
22 changed files with 544 additions and 213 deletions

View file

@ -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

View file

@ -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

34
Cargo.lock generated
View file

@ -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",

View file

@ -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))

View file

@ -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"

View file

@ -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);

View file

@ -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
View 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);
}
}

View file

@ -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,

View file

@ -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(())

View file

@ -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()

View file

@ -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(())

View file

@ -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) => {

View file

@ -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(),
),
};
}

View file

@ -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);

View file

@ -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(&current_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);
}
}
}
}

View file

@ -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},
}
}
}

View file

@ -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)

View file

@ -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,
}
///

View file

@ -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 {

View file

@ -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)

View file

@ -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,),),