Skip to content

Commit 1d5d597

Browse files
gusinaciokejadlen
authored andcommitted
git-lfs: access git attributes to ignore filtered files
1 parent 225bc3e commit 1d5d597

7 files changed

Lines changed: 78 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
2626
* The `jj arrange` TUI now includes immediate parents and children. They are not
2727
selectable and are dimmed by default.
2828

29+
* Added `git.ignore-filters` setting to specify what filtered files in
30+
`.gitattributes` are ignored by `jj`. Defaults to `["lfs"]`.
31+
2932
### Fixed bugs
3033

3134
## [0.39.0] - 2026-03-04

cli/src/command_error.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ use jj_lib::fileset::FilePatternParseError;
3434
use jj_lib::fileset::FilesetParseError;
3535
use jj_lib::fileset::FilesetParseErrorKind;
3636
use jj_lib::fix::FixError;
37+
use jj_lib::gitattributes::GitAttributesError;
3738
use jj_lib::gitignore::GitIgnoreError;
3839
use jj_lib::index::IndexError;
3940
use jj_lib::op_heads_store::OpHeadResolutionError;
@@ -725,6 +726,12 @@ impl From<GitIgnoreError> for CommandError {
725726
}
726727
}
727728

729+
impl From<GitAttributesError> for CommandError {
730+
fn from(err: GitAttributesError) -> Self {
731+
user_error_with_message("Failed to process .gitattributes.", err)
732+
}
733+
}
734+
728735
impl From<ParseBulkEditMessageError> for CommandError {
729736
fn from(err: ParseBulkEditMessageError) -> Self {
730737
user_error(err)

cli/src/merge_tools/diff_working_copies.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use std::collections::HashMap;
2+
use std::collections::HashSet;
23
use std::fs::File;
34
use std::io;
45
use std::io::Write as _;
@@ -156,6 +157,7 @@ pub(crate) async fn check_out_trees(
156157
eol_conversion_mode: EolConversionMode::None,
157158
exec_change_setting: ExecChangeSetting::Auto,
158159
fsmonitor_settings: FsmonitorSettings::None,
160+
ignore_filters: HashSet::new(),
159161
};
160162
let mut state = TreeState::init(store.clone(), wc_path, state_dir, &tree_state_settings)?;
161163
state.set_sparse_patterns(changed_files.clone())?;

lib/src/git.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ use crate::backend::CommitId;
3939
use crate::backend::TreeValue;
4040
use crate::commit::Commit;
4141
use crate::config::ConfigGetError;
42+
use crate::config::ConfigGetResultExt as _;
4243
use crate::file_util::IoResultExt as _;
4344
use crate::file_util::PathError;
4445
use crate::git_backend::GitBackend;

lib/src/gitattributes.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
// limitations under the License.
1414

1515
#![expect(missing_docs)]
16-
#![allow(unused)]
1716

1817
use std::collections::HashMap;
1918
use std::collections::HashSet;

lib/src/local_working_copy.rs

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,13 @@ use crate::fsmonitor::FsmonitorSettings;
9090
use crate::fsmonitor::WatchmanConfig;
9191
#[cfg(feature = "watchman")]
9292
use crate::fsmonitor::watchman;
93+
#[cfg(feature = "git")]
94+
use crate::git::GitSettings;
95+
use crate::gitattributes::DiskFileLoader;
96+
use crate::gitattributes::GitAttributes;
97+
use crate::gitattributes::GitAttributesFilter as _;
98+
use crate::gitattributes::SearchPriority;
99+
use crate::gitattributes::TreeFileLoader;
93100
use crate::gitignore::GitIgnoreFile;
94101
use crate::lock::FileLock;
95102
use crate::matchers::DifferenceMatcher;
@@ -952,6 +959,10 @@ pub struct TreeStateSettings {
952959
pub exec_change_setting: ExecChangeSetting,
953960
/// The fsmonitor (e.g. Watchman) to use, if any.
954961
pub fsmonitor_settings: FsmonitorSettings,
962+
963+
/// Names of .gitattributes filters whose matching files should be ignored
964+
/// in the working copy.
965+
pub ignore_filters: HashSet<String>,
955966
}
956967

957968
impl TreeStateSettings {
@@ -962,6 +973,19 @@ impl TreeStateSettings {
962973
eol_conversion_mode: EolConversionMode::try_from_settings(user_settings)?,
963974
exec_change_setting: user_settings.get("working-copy.exec-bit-change")?,
964975
fsmonitor_settings: FsmonitorSettings::from_settings(user_settings)?,
976+
ignore_filters: {
977+
#[cfg(feature = "git")]
978+
{
979+
GitSettings::from_settings(user_settings)?
980+
.ignore_filters
981+
.into_iter()
982+
.collect()
983+
}
984+
#[cfg(not(feature = "git"))]
985+
{
986+
HashSet::new()
987+
}
988+
},
965989
})
966990
}
967991
}
@@ -986,6 +1010,10 @@ pub struct TreeState {
9861010
exec_policy: ExecChangePolicy,
9871011
fsmonitor_settings: FsmonitorSettings,
9881012
target_eol_strategy: TargetEolStrategy,
1013+
1014+
// attributes
1015+
git_attributes: Arc<GitAttributes>,
1016+
ignore_filters: HashSet<String>,
9891017
}
9901018

9911019
#[derive(Debug, Error)]
@@ -1046,14 +1074,21 @@ impl TreeState {
10461074
eol_conversion_mode,
10471075
exec_change_setting,
10481076
fsmonitor_settings,
1077+
ignore_filters,
10491078
}: &TreeStateSettings,
10501079
) -> Self {
10511080
let exec_policy = ExecChangePolicy::new(*exec_change_setting, &state_path);
1081+
let tree = store.empty_merged_tree();
1082+
1083+
let store_file_loader = TreeFileLoader::new(tree.clone());
1084+
let disk_file_loader = DiskFileLoader::new(working_copy_path.clone());
1085+
1086+
let git_attributes = Arc::new(GitAttributes::new(store_file_loader, disk_file_loader));
10521087
Self {
10531088
store: store.clone(),
10541089
working_copy_path,
10551090
state_path,
1056-
tree: store.empty_merged_tree(),
1091+
tree,
10571092
file_states: FileStatesMap::new(),
10581093
sparse_patterns: vec![RepoPathBuf::root()],
10591094
own_mtime: MillisSinceEpoch(0),
@@ -1063,6 +1098,9 @@ impl TreeState {
10631098
exec_policy,
10641099
fsmonitor_settings: fsmonitor_settings.clone(),
10651100
target_eol_strategy: TargetEolStrategy::new(*eol_conversion_mode),
1101+
// TODO We should update git_attributes every time TreeState::tree_id is updated
1102+
git_attributes,
1103+
ignore_filters: ignore_filters.clone(),
10661104
}
10671105
}
10681106

@@ -1311,6 +1349,8 @@ impl TreeState {
13111349
error: OnceLock::new(),
13121350
progress: *progress,
13131351
max_new_file_size: *max_new_file_size,
1352+
git_attributes: self.git_attributes.clone(),
1353+
ignore_filters: self.ignore_filters.clone(),
13141354
};
13151355
let directory_to_visit = DirectoryToVisit {
13161356
dir: RepoPathBuf::root(),
@@ -1482,6 +1522,9 @@ struct FileSnapshotter<'a> {
14821522
error: OnceLock<SnapshotError>,
14831523
progress: Option<&'a SnapshotProgress<'a>>,
14841524
max_new_file_size: u64,
1525+
1526+
git_attributes: Arc<GitAttributes>,
1527+
ignore_filters: HashSet<String>,
14851528
}
14861529

14871530
impl FileSnapshotter<'_> {
@@ -1623,7 +1666,16 @@ impl FileSnapshotter<'_> {
16231666
if let Some(progress) = self.progress {
16241667
progress(&path);
16251668
}
1626-
if maybe_current_file_state.is_none()
1669+
if self
1670+
.git_attributes
1671+
.filter_matches(&path, &self.ignore_filters, SearchPriority::Disk)
1672+
.block_on()
1673+
{
1674+
// Skip gitattributes files that we want to ignore - this
1675+
// would result in them showing up as deleted, but we also
1676+
// omit them in `emit_deleted_files` to avoid that.
1677+
Ok(None)
1678+
} else if maybe_current_file_state.is_none()
16271679
&& (git_ignore.matches(path.as_internal_file_string())
16281680
&& !self.force_tracking_matcher.matches(&path))
16291681
{
@@ -1767,6 +1819,13 @@ impl FileSnapshotter<'_> {
17671819
.flat_map(|(_, chunk)| chunk)
17681820
// Whether or not the entry exists, submodule should be ignored
17691821
.filter(|(_, state)| state.file_type != FileType::GitSubmodule)
1822+
// Whether or not the entry exists, ignored gitattributes files should be omitted
1823+
.filter(|(path, _)| {
1824+
!self
1825+
.git_attributes
1826+
.filter_matches(path, &self.ignore_filters, SearchPriority::Disk)
1827+
.block_on()
1828+
})
17701829
.filter(|(path, _)| self.matcher.matches(path))
17711830
.try_for_each(|(path, _)| self.deleted_files_tx.send(path.to_owned()))
17721831
.ok();

lib/src/working_copy.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ use tracing::instrument;
2929
use crate::backend::BackendError;
3030
use crate::commit::Commit;
3131
use crate::dag_walk;
32+
use crate::gitattributes::GitAttributesError;
3233
use crate::gitignore::GitIgnoreError;
3334
use crate::gitignore::GitIgnoreFile;
3435
use crate::matchers::Matcher;
@@ -191,6 +192,9 @@ pub enum SnapshotError {
191192
/// Checking path with ignore patterns failed.
192193
#[error(transparent)]
193194
GitIgnoreError(#[from] GitIgnoreError),
195+
/// Checking path with gitattributes patterns failed.
196+
#[error(transparent)]
197+
GitAttributesError(#[from] GitAttributesError),
194198
/// Failed to load the working copy state.
195199
#[error(transparent)]
196200
WorkingCopyStateError(#[from] WorkingCopyStateError),

0 commit comments

Comments
 (0)