Skip to content
Open
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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased

### Added
* new `--only-this-dir` (`-x`) flag to restrict the status, log and files tabs to the directory gitui is started in, while still opening the repository it belongs to
* support x509 commit signing [[@kaden-l-nelson](https://github.com/kaden-l-nelson)] ([#2514](https://github.com/gitui-org/gitui/issues/2514))

### Changed
Expand Down
12 changes: 11 additions & 1 deletion asyncgit/src/status.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ pub struct AsyncStatus {
sender: Sender<AsyncGitNotification>,
pending: Arc<AtomicUsize>,
repo: RepoPath,
/// limits the status to this repo relative path (see `PathScope`)
pathspec: Option<String>,
/// Counter that increments after each completed fetch.
generation: Arc<AtomicU64>,
}
Expand All @@ -58,9 +60,11 @@ impl AsyncStatus {
pub fn new(
repo: RepoPath,
sender: Sender<AsyncGitNotification>,
pathspec: Option<String>,
) -> Self {
Self {
repo,
pathspec,
current: Arc::new(Mutex::new(Request(0, None))),
last: Arc::new(Mutex::new(Status::default())),
sender,
Expand Down Expand Up @@ -119,6 +123,7 @@ impl AsyncStatus {
let status_type = params.status_type;
let config = params.config;
let repo = self.repo.clone();
let pathspec = self.pathspec.clone();

self.pending.fetch_add(1, Ordering::Relaxed);

Expand All @@ -127,6 +132,7 @@ impl AsyncStatus {
&repo,
status_type,
config,
pathspec.as_deref(),
hash_request,
&arc_current,
&arc_last,
Expand All @@ -151,11 +157,13 @@ impl AsyncStatus {
repo: &RepoPath,
status_type: StatusType,
config: Option<ShowUntrackedFilesConfig>,
pathspec: Option<&str>,
hash_request: u64,
arc_current: &Arc<Mutex<Request<u64, Status>>>,
arc_last: &Arc<Mutex<Status>>,
) -> Result<()> {
let res = Self::get_status(repo, status_type, config)?;
let res =
Self::get_status(repo, status_type, config, pathspec)?;
log::trace!(
"status fetched: {hash_request} (type: {status_type:?})",
);
Expand All @@ -179,12 +187,14 @@ impl AsyncStatus {
repo: &RepoPath,
status_type: StatusType,
config: Option<ShowUntrackedFilesConfig>,
pathspec: Option<&str>,
) -> Result<Status> {
Ok(Status {
items: sync::status::get_status(
repo,
status_type,
config,
pathspec,
)?,
})
}
Expand Down
5 changes: 3 additions & 2 deletions asyncgit/src/sync/diff.rs
Original file line number Diff line number Diff line change
Expand Up @@ -531,8 +531,9 @@ mod tests {
.unwrap();
}

let res = get_status(repo_path, StatusType::WorkingDir, None)
.unwrap();
let res =
get_status(repo_path, StatusType::WorkingDir, None, None)
.unwrap();
assert_eq!(res.len(), 1);
assert_eq!(res[0].path, "bar.txt");

Expand Down
4 changes: 2 additions & 2 deletions asyncgit/src/sync/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -241,10 +241,10 @@ pub mod tests {
/// helper returning amount of files with changes in the (wd,stage)
pub fn get_statuses(repo_path: &RepoPath) -> (usize, usize) {
(
get_status(repo_path, StatusType::WorkingDir, None)
get_status(repo_path, StatusType::WorkingDir, None, None)
.unwrap()
.len(),
get_status(repo_path, StatusType::Stage, None)
get_status(repo_path, StatusType::Stage, None, None)
.unwrap()
.len(),
)
Expand Down
5 changes: 3 additions & 2 deletions asyncgit/src/sync/reset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,9 @@ mod tests {
let repo_path: &RepoPath =
&root.as_os_str().to_str().unwrap().into();

let res = get_status(repo_path, StatusType::WorkingDir, None)
.unwrap();
let res =
get_status(repo_path, StatusType::WorkingDir, None, None)
.unwrap();
assert_eq!(res.len(), 0);

let file_path = root.join("bar.txt");
Expand Down
122 changes: 112 additions & 10 deletions asyncgit/src/sync/status.rs
Original file line number Diff line number Diff line change
Expand Up @@ -168,10 +168,14 @@ impl From<ShowUntrackedFilesConfig> for gix::status::UntrackedFiles {
}

/// guarantees sorting
///
/// `pathspec` limits the result to a path relative to the root of the
/// repository, `None` covers the whole repository
pub fn get_status(
repo_path: &RepoPath,
status_type: StatusType,
show_untracked: Option<ShowUntrackedFilesConfig>,
pathspec: Option<&str>,
) -> Result<Vec<StatusItem>> {
scope_time!("get_status");

Expand All @@ -195,11 +199,18 @@ pub fn get_status(
.status(gix::progress::Discard)?
.untracked_files(show_untracked.into());

// `gix` resolves patterns relative to the current directory,
// `:(top)` makes them relative to the root of the repository,
// which is what `pathspec` is
let patterns: Vec<gix::bstr::BString> = pathspec
.map(|pathspec| vec![format!(":(top){pathspec}").into()])
.unwrap_or_default();

let mut res = Vec::new();

match status_type {
StatusType::WorkingDir => {
let iter = status.into_index_worktree_iter(Vec::new())?;
let iter = status.into_index_worktree_iter(patterns)?;

for item in iter {
let Ok(item) = item else {
Expand Down Expand Up @@ -227,7 +238,7 @@ pub fn get_status(

let mut pathspec = repo.pathspec(
false, /* empty patterns match prefix */
None::<&str>,
patterns.iter(),
true, /* inherit ignore case */
&gix::index::State::new(repo.object_hash()),
gix::worktree::stack::state::attributes::Source::WorktreeThenIdMapping
Expand Down Expand Up @@ -255,7 +266,7 @@ pub fn get_status(
)?;
}
StatusType::Both => {
let iter = status.into_iter(Vec::new())?;
let iter = status.into_iter(patterns)?;

for item in iter {
let item = item?;
Expand Down Expand Up @@ -300,16 +311,103 @@ mod tests {
use super::*;
use crate::{
sync::{
commit, stage_add_file,
commit, stage_add_all, stage_add_file,
status::{get_status, StatusType},
tests::{repo_init, repo_init_bare},
RepoPath,
},
StatusItem, StatusItemType,
};
use std::{fs::File, io::Write, path::Path};
use std::{
fs::{self, File},
io::Write,
path::Path,
};
use tempfile::TempDir;

fn paths(items: &[StatusItem]) -> Vec<&str> {
items.iter().map(|i| i.path.as_str()).collect()
}

#[test]
fn test_status_pathspec() {
let (_td, repo) = repo_init().unwrap();
let root = repo.path().parent().unwrap();
let repo_path: &RepoPath =
&root.as_os_str().to_str().unwrap().into();

fs::create_dir(root.join("sub")).unwrap();
File::create(root.join("sub/a.txt"))
.unwrap()
.write_all(b"a")
.unwrap();
File::create(root.join("b.txt"))
.unwrap()
.write_all(b"b")
.unwrap();

let all = get_status(
repo_path,
StatusType::WorkingDir,
Some(ShowUntrackedFilesConfig::All),
None,
)
.unwrap();
assert_eq!(paths(&all), vec!["b.txt", "sub/a.txt"]);

let scoped = get_status(
repo_path,
StatusType::WorkingDir,
Some(ShowUntrackedFilesConfig::All),
Some("sub"),
)
.unwrap();
assert_eq!(paths(&scoped), vec!["sub/a.txt"]);

stage_add_all(repo_path, "*", None).unwrap();

let scoped = get_status(
repo_path,
StatusType::Stage,
Some(ShowUntrackedFilesConfig::All),
Some("sub"),
)
.unwrap();
assert_eq!(paths(&scoped), vec!["sub/a.txt"]);

let scoped = get_status(
repo_path,
StatusType::Both,
Some(ShowUntrackedFilesConfig::All),
Some("sub"),
)
.unwrap();
assert_eq!(paths(&scoped), vec!["sub/a.txt"]);
}

/// a pathspec that matches nothing is not the same as no pathspec
#[test]
fn test_status_pathspec_without_match() {
let (_td, repo) = repo_init().unwrap();
let root = repo.path().parent().unwrap();
let repo_path: &RepoPath =
&root.as_os_str().to_str().unwrap().into();

File::create(root.join("b.txt"))
.unwrap()
.write_all(b"b")
.unwrap();

let scoped = get_status(
repo_path,
StatusType::WorkingDir,
Some(ShowUntrackedFilesConfig::All),
Some("sub"),
)
.unwrap();
assert!(scoped.is_empty());
}

#[test]
fn test_discard_status() {
let file_path = Path::new("README.md");
Expand All @@ -327,14 +425,14 @@ mod tests {
writeln!(file, "Test for discard_status").unwrap();

let statuses =
get_status(repo_path, StatusType::WorkingDir, None)
get_status(repo_path, StatusType::WorkingDir, None, None)
.unwrap();
assert_eq!(statuses.len(), 1);

discard_status(repo_path).unwrap();

let statuses =
get_status(repo_path, StatusType::WorkingDir, None)
get_status(repo_path, StatusType::WorkingDir, None, None)
.unwrap();
assert_eq!(statuses.len(), 0);
}
Expand All @@ -356,9 +454,13 @@ mod tests {
workdir: separate_workdir.path().into(),
};

let status =
get_status(&repo_path, StatusType::WorkingDir, None)
.unwrap();
let status = get_status(
&repo_path,
StatusType::WorkingDir,
None,
None,
)
.unwrap();

assert_eq!(
status,
Expand Down
6 changes: 3 additions & 3 deletions asyncgit/src/sync/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,7 @@ mod tests {
&root.as_os_str().to_str().unwrap().into();

let status_count = |s: StatusType| -> usize {
get_status(repo_path, s, None).unwrap().len()
get_status(repo_path, s, None, None).unwrap().len()
};

fs::create_dir_all(root.join("a/d"))?;
Expand Down Expand Up @@ -422,7 +422,7 @@ mod tests {
&root.as_os_str().to_str().unwrap().into();

let status_count = |s: StatusType| -> usize {
get_status(repo_path, s, None).unwrap().len()
get_status(repo_path, s, None, None).unwrap().len()
};

let full_path = &root.join(file_path);
Expand Down Expand Up @@ -457,7 +457,7 @@ mod tests {
&root.as_os_str().to_str().unwrap().into();

let status_count = |s: StatusType| -> usize {
get_status(repo_path, s, None).unwrap().len()
get_status(repo_path, s, None, None).unwrap().len()
};

let sub = &root.join("sub");
Expand Down
17 changes: 15 additions & 2 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ use crate::{
Action, AppTabs, InternalEvent, NeedsUpdate, Queue,
StackablePopupOpen,
},
scope::PathScope,
setup_popups,
strings::{self, ellipsis_trim_start, order},
tabs::{FilesTab, Revlog, StashList, Stashing, Status},
Expand Down Expand Up @@ -126,6 +127,8 @@ pub struct Environment {
pub theme: SharedTheme,
pub key_config: SharedKeyConfig,
pub repo: RepoPathRef,
/// the directory of the repository the ui is restricted to
pub scope: PathScope,
pub options: SharedOptions,
pub sender_git: Sender<AsyncGitNotification>,
pub sender_app: Sender<AsyncAppNotification>,
Expand All @@ -141,6 +144,7 @@ impl Environment {
theme: Default::default(),
key_config: Default::default(),
repo: RefCell::new(RepoPath::Path(Default::default())),
scope: PathScope::everything(),
options: Rc::new(RefCell::new(Options::test_env())),
sender_git: unbounded().0,
sender_app: unbounded().0,
Expand All @@ -154,6 +158,7 @@ impl App {
#[allow(clippy::too_many_lines)]
pub fn new(
cliargs: CliArgs,
scope: PathScope,
sender_git: Sender<AsyncGitNotification>,
sender_app: Sender<AsyncAppNotification>,
input: Input,
Expand All @@ -163,15 +168,23 @@ impl App {
let repo = RefCell::new(cliargs.repo_path.clone());
log::trace!("open repo at: {repo:?}");

let repo_path_text =
repo_work_dir(&repo.borrow()).unwrap_or_default();
let repo_path_text = scope.path().map_or_else(
|| repo_work_dir(&repo.borrow()).unwrap_or_default(),
|scope| {
format!(
"{} [{scope}]",
repo_work_dir(&repo.borrow()).unwrap_or_default()
)
},
);

let env = Environment {
queue: Queue::new(),
theme: Rc::new(theme),
key_config: Rc::new(key_config),
options: Options::new(repo.clone()),
repo,
scope,
sender_git,
sender_app,
};
Expand Down
Loading