Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/default_config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ help_menu.quit = ["q", "h", "?", "<esc>"]
root.branch_menu = ["b"]
branch_menu.checkout = ["b"]
branch_menu.checkout_new_branch = ["c"]
branch_menu.delete = ["K"]
branch_menu.quit = ["q", "<esc>"]

root.commit_menu = ["c"]
Expand Down
8 changes: 8 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ pub enum Error {
GetCurrentBranchUpstream(git2::Error),
GetCurrentBranchUpstreamUtf8(Utf8Error),
RemoteNameUtf8(Utf8Error),
CannotDeleteCurrentBranch,
BranchNameRequired,
IsBranchMerged(git2::Error),
GetRemote(git2::Error),
ReadGitConfig(git2::Error),
ReadGitConfigUtf8(Utf8Error),
Expand Down Expand Up @@ -94,6 +97,11 @@ impl Display for Error {
f.write_str("Current branch upstream is not valid UTF-8")
}
Error::RemoteNameUtf8(_e) => f.write_str("Remote name is not valid UTF-8"),
Error::CannotDeleteCurrentBranch => f.write_str("Cannot delete current branch"),
Error::BranchNameRequired => f.write_str("Branch name required"),
Error::IsBranchMerged(e) => {
f.write_fmt(format_args!("Couldn't check if branch is merged: {}", e))
}
Error::GetRemote(e) => f.write_fmt(format_args!("Couldn't get remote: {}", e)),
Error::ReadGitConfig(e) => f.write_fmt(format_args!("Couldn't read git config: {}", e)),
Error::ReadGitConfigUtf8(_e) => f.write_str("Git config is not valid UTF-8"),
Expand Down
26 changes: 26 additions & 0 deletions src/git/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use diff::Diff;
use git2::{Branch, Repository};
use itertools::Itertools;
use remote::get_branch_upstream;

use self::{commit::Commit, merge_status::MergeStatus, rebase_status::RebaseStatus};
use crate::{
Expand Down Expand Up @@ -241,3 +242,28 @@ pub(crate) fn get_current_branch(repo: &git2::Repository) -> Res<Branch> {
Err(Error::NotOnBranch)
}
}

pub(crate) fn is_branch_merged(repo: &git2::Repository, name: &str) -> Res<bool> {
let branch = repo
.find_branch(name, git2::BranchType::Local)
.map_err(Error::IsBranchMerged)?;

let upstream = get_branch_upstream(&branch)?;

let reference = match upstream {
Some(u) => u.into_reference(),
None => repo.head().map_err(Error::GetHead)?,
};

let ref_commit = reference.peel_to_commit().map_err(Error::IsBranchMerged)?;

let commit = branch
.into_reference()
.peel_to_commit()
.map_err(Error::IsBranchMerged)?;

Ok(commit.id() == ref_commit.id()
|| repo
.graph_descendant_of(ref_commit.id(), commit.id())
.map_err(Error::IsBranchMerged)?)
}
6 changes: 5 additions & 1 deletion src/git/remote.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ use crate::{git, Res};
use super::{Error, Utf8Error};

pub(crate) fn get_upstream(repo: &Repository) -> Res<Option<Branch>> {
match git::get_current_branch(repo)?.upstream() {
get_branch_upstream(&git::get_current_branch(repo)?)
}

pub(crate) fn get_branch_upstream<'repo>(branch: &Branch<'repo>) -> Res<Option<Branch<'repo>>> {
match branch.upstream() {
Ok(v) => Ok(Some(v)),
Err(e) if e.class() == git2::ErrorClass::Config => Ok(None),
Err(e) => Err(Error::GetCurrentBranchUpstream(e)),
Expand Down
2 changes: 1 addition & 1 deletion src/menu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ impl PendingMenu {
is_hidden: false,
args: match menu {
Menu::Root => vec![],
Menu::Branch => ops::checkout::init_args(),
Menu::Branch => ops::branch::init_args(),
Comment thread
altsem marked this conversation as resolved.
Menu::Commit => ops::commit::init_args(),
Menu::Fetch => ops::fetch::init_args(),
Menu::Help => vec![],
Expand Down
60 changes: 57 additions & 3 deletions src/ops/checkout.rs → src/ops/branch.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use super::{selected_rev, Action, OpTrait};
use crate::{
error::Error,
git::{get_current_branch_name, is_branch_merged},
items::TargetData,
menu::arg::Arg,
state::{PromptParams, State},
Expand Down Expand Up @@ -37,9 +39,7 @@ impl OpTrait for Checkout {

fn checkout(state: &mut State, term: &mut Term, rev: &str) -> Res<()> {
let mut cmd = Command::new("git");
cmd.args(["checkout"]);
cmd.args(state.pending_menu.as_ref().unwrap().args());
cmd.arg(rev);
cmd.args(["checkout", rev]);

state.close_menu();
state.run_cmd(term, &[], cmd)?;
Expand Down Expand Up @@ -80,3 +80,57 @@ fn checkout_new_branch_prompt_update(
state.run_cmd(term, &[], cmd)?;
Ok(())
}

pub(crate) struct Delete;
impl OpTrait for Delete {
fn get_action(&self, target: Option<&TargetData>) -> Option<Action> {
let default = match target {
Some(TargetData::Branch(b)) => Some(b.clone()),
_ => None,
};

Some(Rc::new(move |state: &mut State, term: &mut Term| {
let default = default.clone();

let branch_name = state.prompt(
term,
&PromptParams {
prompt: "Delete",
create_default_value: Box::new(move |_| default.clone()),
..Default::default()
},
)?;

delete(state, term, &branch_name)?;
Ok(())
}))
}

fn display(&self, _state: &State) -> String {
"Delete branch".into()
}
}

fn delete(state: &mut State, term: &mut Term, branch_name: &str) -> Res<()> {
if branch_name.is_empty() {
return Err(Error::BranchNameRequired);
}

if get_current_branch_name(&state.repo).unwrap() == branch_name {
return Err(Error::CannotDeleteCurrentBranch);
}

let mut cmd = Command::new("git");
cmd.args(["branch", "-d"]);

if !is_branch_merged(&state.repo, branch_name).unwrap_or(false) {
state.confirm(term, "Branch is not fully merged. Really delete? (y or n)")?;
cmd.arg("-f");
}

cmd.arg(branch_name);

state.close_menu();
state.run_cmd(term, &[], cmd)?;
Ok(())
}
8 changes: 5 additions & 3 deletions src/ops/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize};
use crate::{items::TargetData, menu::Menu, state::State, term::Term, Res};
use std::{fmt::Display, rc::Rc};

pub(crate) mod checkout;
pub(crate) mod branch;
pub(crate) mod commit;
pub(crate) mod copy_hash;
pub(crate) mod discard;
Expand Down Expand Up @@ -42,6 +42,7 @@ pub(crate) trait OpTrait {
pub(crate) enum Op {
Checkout,
CheckoutNewBranch,
Delete,
Commit,
CommitAmend,
FetchAll,
Expand Down Expand Up @@ -120,8 +121,9 @@ impl Op {
Op::HalfPageUp => Box::new(editor::HalfPageUp),
Op::HalfPageDown => Box::new(editor::HalfPageDown),

Op::Checkout => Box::new(checkout::Checkout),
Op::CheckoutNewBranch => Box::new(checkout::CheckoutNewBranch),
Op::Checkout => Box::new(branch::Checkout),
Op::CheckoutNewBranch => Box::new(branch::CheckoutNewBranch),
Op::Delete => Box::new(branch::Delete),
Op::Commit => Box::new(commit::Commit),
Op::CommitAmend => Box::new(commit::CommitAmend),
Op::FetchAll => Box::new(fetch::FetchAll),
Expand Down
51 changes: 51 additions & 0 deletions src/tests/branch.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
use super::*;

fn setup() -> TestContext {
let ctx = TestContext::setup_clone();
run(ctx.dir.path(), &["git", "checkout", "-b", "merged"]);
run(ctx.dir.path(), &["git", "checkout", "-b", "unmerged"]);
commit(ctx.dir.path(), "first commit", "");
run(ctx.dir.path(), &["git", "checkout", "main"]);
ctx
}

#[test]
fn branch_menu() {
snapshot!(setup(), "Yjb");
}

#[test]
fn switch_branch_selected() {
snapshot!(setup(), "Yjjbb<enter>");
}

#[test]
fn switch_branch_input() {
snapshot!(setup(), "Ybbmerged<enter>");
}

#[test]
fn checkout_new_branch() {
snapshot!(setup(), "bcnew<enter>");
}

#[test]
fn delete_branch_selected() {
snapshot!(setup(), "YjjbK<enter>");
}

#[test]
fn delete_branch_input() {
snapshot!(setup(), "bKmerged<enter>");
}

#[test]
fn delete_branch_empty() {
snapshot!(setup(), "bK<enter>");
}

#[test]
fn delete_unmerged_branch() {
// TODO: Remove <esc> once #368 is fixed
snapshot!(setup(), "bKunmerged<enter>n<esc>bKunmerged<enter>y");
}
31 changes: 1 addition & 30 deletions src/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ use std::fs;
#[macro_use]
mod helpers;
mod arg;
mod branch;
mod commit;
mod discard;
mod editor;
Expand Down Expand Up @@ -257,36 +258,6 @@ mod show_refs {
}
}

mod checkout {
use super::*;

#[test]
pub(crate) fn checkout_menu() {
let ctx = TestContext::setup_clone();
run(ctx.dir.path(), &["git", "branch", "other-branch"]);
snapshot!(ctx, "Yjb");
}

#[test]
pub(crate) fn switch_branch_selected() {
let ctx = TestContext::setup_clone();
run(ctx.dir.path(), &["git", "branch", "other-branch"]);
snapshot!(ctx, "Yjjbb<enter>");
}

#[test]
pub(crate) fn switch_branch_input() {
let ctx = TestContext::setup_clone();
run(ctx.dir.path(), &["git", "branch", "hi"]);
snapshot!(ctx, "Yjjbbhi<enter>");
}

#[test]
pub(crate) fn checkout_new_branch() {
snapshot!(TestContext::setup_clone(), "bcf<esc>cx<enter>");
}
}

#[test]
fn updated_externally() {
let mut ctx = TestContext::setup_init();
Expand Down
25 changes: 25 additions & 0 deletions src/tests/snapshots/gitu__tests__branch__branch_menu.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
source: src/tests/branch.rs
expression: ctx.redact_buffer()
---
Branches |
▌* main |
merged |
unmerged |
|
Remote origin |
origin/HEAD |
origin/main |
|
|
|
|
|
|
────────────────────────────────────────────────────────────────────────────────|
Branch |
b Checkout branch/revision |
c Checkout new branch |
K Delete branch |
q/<esc> Quit/Close |
styles_hash: c4812f62d5c8483d
25 changes: 25 additions & 0 deletions src/tests/snapshots/gitu__tests__branch__checkout_new_branch.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
source: src/tests/branch.rs
expression: ctx.redact_buffer()
---
▌On branch new |
|
Recent commits |
b66a0bf main merged new origin/main add initial-file |
|
|
|
|
|
|
|
|
|
|
|
|
|
────────────────────────────────────────────────────────────────────────────────|
$ git checkout -b new |
Switched to a new branch 'new' |
styles_hash: c7925dfc1f0f00cd
25 changes: 25 additions & 0 deletions src/tests/snapshots/gitu__tests__branch__delete_branch_empty.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
source: src/tests/branch.rs
expression: ctx.redact_buffer()
---
▌On branch main |
▌Your branch is up to date with 'origin/main'. |
|
Recent commits |
b66a0bf main merged origin/main add initial-file |
|
|
|
|
|
|
|
────────────────────────────────────────────────────────────────────────────────|
Branch |
b Checkout branch/revision |
c Checkout new branch |
K Delete branch |
q/<esc> Quit/Close |
────────────────────────────────────────────────────────────────────────────────|
! Branch name required |
styles_hash: 343b1fffa75cf86c

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This snapshot seems stale

Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
source: src/tests/mod.rs
expression: ctx.redact_buffer()
---
Branches |
▌* main |
|
Remote origin |
origin/HEAD |
origin/main |
|
|
|
|
|
|
|
|
|
|
|
────────────────────────────────────────────────────────────────────────────────|
$ git branch -d --force hi |
Deleted branch hi (was b66a0bf). |
styles_hash: 5a3690c1fa3b2467
Loading