Skip to content

feat(revset): add + as shortcut for "only child" #1461

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 10 commits into
base: master
Choose a base branch
from
Draft
1 change: 1 addition & 0 deletions git-branchless-init/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ const ALL_ALIASES: &[(&str, &str)] = &[
("restack", "restack"),
("reword", "reword"),
("sl", "smartlog"),
("split", "split"),
("smartlog", "smartlog"),
("submit", "submit"),
("sw", "switch"),
Expand Down
7 changes: 6 additions & 1 deletion git-branchless-lib/src/core/check_out.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ pub struct CheckOutCommitOptions {
/// Additional arguments to pass to `git checkout`.
pub additional_args: Vec<OsString>,

/// Ignore the `autoSwitchBranches` setting?
pub force_detach: bool,

/// Use `git reset` rather than `git checkout`; that is, leave the index and
/// working copy unchanged, and just adjust the `HEAD` pointer.
pub reset: bool,
Expand All @@ -56,6 +59,7 @@ impl Default for CheckOutCommitOptions {
fn default() -> Self {
Self {
additional_args: Default::default(),
force_detach: false,
reset: false,
render_smartlog: true,
}
Expand Down Expand Up @@ -116,6 +120,7 @@ pub fn check_out_commit(
) -> EyreExitOr<()> {
let CheckOutCommitOptions {
additional_args,
force_detach,
reset,
render_smartlog,
} = options;
Expand All @@ -134,7 +139,7 @@ pub fn check_out_commit(
create_snapshot(effects, git_run_info, repo, event_log_db, event_tx_id)?;
}

let target = if get_auto_switch_branches(repo)? && !reset {
let target = if get_auto_switch_branches(repo)? && !reset && !force_detach {
maybe_get_branch_name(target, oid, repo)?
} else {
target
Expand Down
1 change: 1 addition & 0 deletions git-branchless-lib/src/core/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ pub mod node_descriptors;
pub mod repo_ext;
pub mod rewrite;
pub mod task;
pub mod untracked_file_cache;
166 changes: 166 additions & 0 deletions git-branchless-lib/src/core/untracked_file_cache.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
//! Utilities to fetch, confirm and save a list of untracked files, so we can
//! prompt the user about them.

use console::{Key, Term};
use eyre::Context;
use std::io::Write as IoWrite;
use std::time::SystemTime;
use std::{collections::HashSet, fmt::Write};
use tracing::instrument;

use crate::git::{GitRunInfo, Repo};

use super::{effects::Effects, eventlog::EventTransactionId, formatting::Pluralize};

/// TODO
#[instrument]
pub fn prompt_about_untracked_files(
effects: &Effects,
git_run_info: &GitRunInfo,
repo: &Repo,
event_tx_id: EventTransactionId,
) -> eyre::Result<Vec<String>> {
let conn = repo.get_db_conn()?;

let cached_files = get_cached_untracked_files(&conn)?;
let real_files = get_real_untracked_files(repo, event_tx_id, git_run_info)?;
let new_files: Vec<&String> = real_files.difference(&cached_files).collect();

let mut files_to_add = vec![];
if !new_files.is_empty() {
writeln!(
effects.get_output_stream(),
"Found {}:",
Pluralize {
determiner: None,
amount: new_files.len(),
unit: ("new untracked file", "new untracked files"),
},
)?;
'outer: for file in new_files {
write!(
effects.get_output_stream(),
" Add file '{file}'? [Yes/(N)o/nOne] "
)?;
std::io::stdout().flush()?;

let term = Term::stderr();
'inner: loop {
let key = term.read_key()?;
match key {
Key::Char('y') | Key::Char('Y') => {
files_to_add.push(file.clone());
writeln!(effects.get_output_stream(), "adding")?;
}
Key::Char('n') | Key::Char('N') | Key::Enter => {
writeln!(effects.get_output_stream(), "not adding")?;
}
Key::Char('o') | Key::Char('O') => {
writeln!(effects.get_output_stream(), "skipping remaining")?;
break 'outer;
}
_ => continue 'inner,
};
continue 'outer;
}
}
}

cache_untracked_files(&conn, real_files)?;

Ok(files_to_add)
}

/// TODO
#[instrument]
fn get_real_untracked_files(
repo: &Repo,
event_tx_id: EventTransactionId,
git_run_info: &GitRunInfo,
) -> eyre::Result<HashSet<String>> {
let args = vec!["ls-files", "--others", "--exclude-standard", "-z"];
let files_str = git_run_info
.run_silent(repo, Some(event_tx_id), &args, Default::default())
.wrap_err("calling `git ls-files`")?
.stdout;
let files_str = String::from_utf8(files_str).wrap_err("Decoding stdout from Git subprocess")?;
let files = files_str
.trim()
.split('\0')
.filter_map(|s| {
if s.is_empty() {
None
} else {
Some(s.to_owned())
}
})
.collect();
Ok(files)
}

/// TODO
#[instrument]
fn cache_untracked_files(conn: &rusqlite::Connection, files: HashSet<String>) -> eyre::Result<()> {
{
conn.execute("DROP TABLE IF EXISTS untracked_files", rusqlite::params![])
.wrap_err("Removing `untracked_files` table")?;
}

init_untracked_files_table(conn)?;

{
let tx = conn.unchecked_transaction()?;

let timestamp = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.wrap_err("Calculating event transaction timestamp")?
.as_secs_f64();
for file in files {
tx.execute(
"
INSERT INTO untracked_files
(timestamp, file)
VALUES
(:timestamp, :file)
",
rusqlite::named_params! {
":timestamp": timestamp,
":file": file,
},
)?;
}
tx.commit()?;
}

Ok(())
}

/// Ensure the untracked_files table exists; creating it if it does not.
#[instrument]
fn init_untracked_files_table(conn: &rusqlite::Connection) -> eyre::Result<()> {
conn.execute(
"
CREATE TABLE IF NOT EXISTS untracked_files (
timestamp REAL NOT NULL,
file TEXT NOT NULL
)
",
rusqlite::params![],
)
.wrap_err("Creating `untracked_files` table")?;

Ok(())
}

/// TODO
#[instrument]
pub fn get_cached_untracked_files(conn: &rusqlite::Connection) -> eyre::Result<HashSet<String>> {
init_untracked_files_table(conn)?;

let mut stmt = conn.prepare("SELECT file FROM untracked_files")?;
let paths = stmt
.query_map(rusqlite::named_params![], |row| row.get("file"))?
.filter_map(|p| p.ok())
.collect();
Ok(paths)
}
66 changes: 65 additions & 1 deletion git-branchless-lib/src/git/diff.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};

use eyre::Context;
use eyre::{Context, OptionExt};
use itertools::Itertools;
use scm_record::helpers::make_binary_description;
use scm_record::{ChangeType, File, FileMode, Section, SectionChangedLine};
Expand All @@ -15,6 +15,17 @@ pub struct Diff<'repo> {
pub(super) inner: git2::Diff<'repo>,
}

impl Diff<'_> {
/// Summarize this diff into a single line "short" format.
pub fn short_stats(&self) -> eyre::Result<String> {
let stats = self.inner.stats()?;
let buf = stats.to_buf(git2::DiffStatsFormat::SHORT, usize::MAX)?;
buf.as_str()
.ok_or_eyre("converting buf to str")
.map(|s| s.trim().to_string())
}
}

#[derive(Clone, Debug, PartialEq, Eq, Hash)]
struct GitHunk {
old_start: usize,
Expand All @@ -23,6 +34,59 @@ struct GitHunk {
new_lines: usize,
}

/// Summarize a diff for use as part of a temporary commit message.
pub fn summarize_diff_for_temporary_commit(diff: &Diff) -> eyre::Result<String> {
// this returns something like `1 file changed, 1 deletion(-)`
// diff.short_stats()

// this returns something like `test2.txt (-1)` or `2 files (+1/-2)`
let stats = diff.inner.stats()?;
let prefix = if stats.files_changed() == 1 {
let mut prefix = None;
// returning false terminates iteration, but that also returns Err, so
// catch and ignore it
let _ = diff.inner.foreach(
&mut |delta: git2::DiffDelta, _| {
if let Some(path) = delta.old_file().path() {
// prefix = Some(format!("{}", path.file_name().unwrap().to_string_lossy()));
prefix = Some(format!("{}", path.display()));
} else if let Some(path) = delta.new_file().path() {
prefix = Some(format!("{}", path.display()));
}

false
},
None,
None,
None,
);
prefix
} else {
Some(format!("{} files", stats.files_changed()))
};

let i = stats.insertions();
let d = stats.deletions();
Ok(format!(
"{prefix} ({i}{slash}{d})",
prefix = prefix.unwrap(),
i = if i > 0 {
format!("+{i}")
} else {
String::new()
},
slash = if i > 0 && d > 0 { "/" } else { "" },
d = if d > 0 {
format!("-{d}")
} else {
String::new()
}
))
// stats.files_changed()
// stats.insertions()
// stats.deletions()
}

/// Calculate the diff between the index and the working copy.
pub fn process_diff_for_record(repo: &Repo, diff: &Diff) -> eyre::Result<Vec<File<'static>>> {
let Diff { inner: diff } = diff;
Expand Down
8 changes: 7 additions & 1 deletion git-branchless-lib/src/git/index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use tracing::instrument;

use crate::core::eventlog::EventTransactionId;

use super::{FileMode, GitRunInfo, GitRunOpts, GitRunResult, MaybeZeroOid, NonZeroOid, Repo};
use super::{FileMode, GitRunInfo, GitRunOpts, GitRunResult, MaybeZeroOid, NonZeroOid, Repo, Tree};

/// The possible stages for items in the index.
#[derive(Copy, Clone, Debug)]
Expand Down Expand Up @@ -88,6 +88,12 @@ impl Index {
},
})
}

/// Update the index from the given tree and write it to disk.
pub fn update_from_tree(&mut self, tree: &Tree) -> eyre::Result<()> {
self.inner.read_tree(&tree.inner)?;
self.inner.write().wrap_err("writing index")
}
}

/// The command to update the index, as defined by `git update-index`.
Expand Down
6 changes: 4 additions & 2 deletions git-branchless-lib/src/git/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ mod test;
mod tree;

pub use config::{Config, ConfigRead, ConfigValue, ConfigWrite};
pub use diff::{process_diff_for_record, Diff};
pub use diff::{process_diff_for_record, summarize_diff_for_temporary_commit, Diff};
pub use index::{update_index, Index, IndexEntry, Stage, UpdateIndexCommand};
pub use object::Commit;
pub use oid::{MaybeZeroOid, NonZeroOid};
Expand All @@ -34,4 +34,6 @@ pub use test::{
make_test_command_slug, SerializedNonZeroOid, SerializedTestResult, TestCommand,
TEST_ABORT_EXIT_CODE, TEST_INDETERMINATE_EXIT_CODE, TEST_SUCCESS_EXIT_CODE,
};
pub use tree::{dehydrate_tree, get_changed_paths_between_trees, hydrate_tree, Tree};
pub use tree::{
dehydrate_tree, get_changed_paths_between_trees, hydrate_tree, make_empty_tree, Tree,
};
25 changes: 25 additions & 0 deletions git-branchless-lib/src/git/status.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,20 @@ impl From<git2::FileMode> for FileMode {
}
}

impl From<FileMode> for git2::FileMode {
fn from(file_mode: FileMode) -> Self {
match file_mode {
FileMode::Blob => git2::FileMode::Blob,
FileMode::BlobExecutable => git2::FileMode::BlobExecutable,
FileMode::BlobGroupWritable => git2::FileMode::BlobGroupWritable,
FileMode::Commit => git2::FileMode::Commit,
FileMode::Link => git2::FileMode::Link,
FileMode::Tree => git2::FileMode::Tree,
FileMode::Unreadable => git2::FileMode::Unreadable,
}
}
}

impl From<i32> for FileMode {
fn from(file_mode: i32) -> Self {
if file_mode == i32::from(git2::FileMode::Blob) {
Expand Down Expand Up @@ -177,6 +191,17 @@ pub struct StatusEntry {
}

impl StatusEntry {
/// Create a status entry for a currently-untracked, to-be-added file.
pub fn new_untracked(filename: String) -> Self {
StatusEntry {
index_status: FileStatus::Untracked,
working_copy_status: FileStatus::Untracked,
working_copy_file_mode: FileMode::Blob,
path: PathBuf::from(filename),
orig_path: None,
}
}

/// Returns the paths associated with the status entry.
pub fn paths(&self) -> Vec<PathBuf> {
let mut result = vec![self.path.clone()];
Expand Down
Loading
Loading