Skip to content

Commit a3533e1

Browse files
committed
merge: workspace-cli + PR jj-vcs#9068 (LFS ignore-filters) + PR jj-vcs#8719 fixes (gitattr filter)
3 parents 587d64e + 45471fb + 5e3a5ac commit a3533e1

61 files changed

Lines changed: 3953 additions & 3113 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/sami-build.yml

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
name: Build sami branch
2+
3+
on:
4+
push:
5+
branches:
6+
- sami
7+
workflow_dispatch:
8+
9+
permissions:
10+
contents: write
11+
12+
env:
13+
CARGO_INCREMENTAL: 0
14+
15+
jobs:
16+
version:
17+
name: Compute version
18+
runs-on: ubuntu-latest
19+
outputs:
20+
tag: ${{ steps.version.outputs.tag }}
21+
steps:
22+
- uses: actions/checkout@v4
23+
- uses: sjawhar/.github/.github/actions/sami-version@v1
24+
id: version
25+
with:
26+
source: cargo
27+
28+
build-linux:
29+
name: Build ${{ matrix.build }}
30+
needs: version
31+
strategy:
32+
fail-fast: false
33+
matrix:
34+
build: [linux-x86_64, linux-aarch64]
35+
include:
36+
- build: linux-x86_64
37+
os: ubuntu-24.04
38+
target: x86_64-unknown-linux-musl
39+
- build: linux-aarch64
40+
os: ubuntu-24.04
41+
target: aarch64-unknown-linux-musl
42+
runs-on: ${{ matrix.os }}
43+
timeout-minutes: 30
44+
steps:
45+
- uses: actions/checkout@v4
46+
47+
- name: Install packages
48+
run: |
49+
sudo apt-get update
50+
sudo apt-get install -y --no-install-recommends musl-tools
51+
if [[ "${{ matrix.target }}" == aarch64* ]]; then
52+
sudo apt-get install -y --no-install-recommends \
53+
gcc-aarch64-linux-gnu \
54+
libc6-dev-arm64-cross
55+
fi
56+
57+
- name: Install Rust
58+
uses: dtolnay/rust-toolchain@stable
59+
with:
60+
target: ${{ matrix.target }}
61+
62+
- name: Configure aarch64 cross-compilation
63+
if: contains(matrix.target, 'aarch64')
64+
run: |
65+
echo '[target.aarch64-unknown-linux-musl]' >> ~/.cargo/config.toml
66+
echo 'linker = "aarch64-linux-gnu-gcc"' >> ~/.cargo/config.toml
67+
68+
- name: Build release binary
69+
run: cargo build --target ${{ matrix.target }} --release -p jj-cli
70+
71+
- name: Package binary
72+
id: package
73+
run: |
74+
tag="${{ needs.version.outputs.tag }}"
75+
staging="jj-${tag}-${{ matrix.target }}"
76+
mkdir "$staging"
77+
cp "target/${{ matrix.target }}/release/jj" "$staging/"
78+
tar czf "$staging.tar.gz" -C "$staging" .
79+
echo "asset=${staging}.tar.gz" >> "$GITHUB_OUTPUT"
80+
81+
- name: Upload artifact
82+
uses: actions/upload-artifact@v4
83+
with:
84+
name: jj-${{ matrix.target }}
85+
path: ${{ steps.package.outputs.asset }}
86+
87+
build-macos:
88+
name: Build ${{ matrix.build }}
89+
needs: version
90+
strategy:
91+
fail-fast: false
92+
matrix:
93+
build: [macos-x86_64, macos-aarch64]
94+
include:
95+
- build: macos-x86_64
96+
target: x86_64-apple-darwin
97+
- build: macos-aarch64
98+
target: aarch64-apple-darwin
99+
runs-on: ubuntu-24.04
100+
timeout-minutes: 30
101+
steps:
102+
- uses: actions/checkout@v4
103+
104+
- name: Install Rust
105+
uses: dtolnay/rust-toolchain@stable
106+
with:
107+
target: ${{ matrix.target }}
108+
109+
- name: Install Zig
110+
uses: mlugg/setup-zig@v2
111+
with:
112+
version: "0.14.0"
113+
114+
- name: Install cargo-zigbuild
115+
run: pip install cargo-zigbuild
116+
117+
- name: Download macOS SDK
118+
run: |
119+
curl -sqL https://github.com/phracker/MacOSX-SDKs/releases/download/11.3/MacOSX11.3.sdk.tar.xz | tar -Jx
120+
echo "SDKROOT=$PWD/MacOSX11.3.sdk" >> $GITHUB_ENV
121+
122+
- name: Build release binary
123+
run: cargo zigbuild --target ${{ matrix.target }} --release -p jj-cli
124+
125+
- name: Package binary
126+
id: package
127+
run: |
128+
tag="${{ needs.version.outputs.tag }}"
129+
staging="jj-${tag}-${{ matrix.target }}"
130+
mkdir "$staging"
131+
cp "target/${{ matrix.target }}/release/jj" "$staging/"
132+
tar czf "$staging.tar.gz" -C "$staging" .
133+
echo "asset=${staging}.tar.gz" >> "$GITHUB_OUTPUT"
134+
135+
- name: Upload artifact
136+
uses: actions/upload-artifact@v4
137+
with:
138+
name: jj-${{ matrix.target }}
139+
path: ${{ steps.package.outputs.asset }}
140+
141+
build-windows:
142+
name: Build win-x86_64
143+
needs: version
144+
runs-on: windows-2022
145+
timeout-minutes: 30
146+
steps:
147+
- uses: actions/checkout@v4
148+
149+
- name: Install Rust
150+
uses: dtolnay/rust-toolchain@stable
151+
with:
152+
target: x86_64-pc-windows-msvc
153+
154+
- name: Build release binary
155+
run: cargo build --target x86_64-pc-windows-msvc --release -p jj-cli
156+
157+
- name: Package binary
158+
id: package
159+
shell: bash
160+
run: |
161+
tag="${{ needs.version.outputs.tag }}"
162+
staging="jj-${tag}-x86_64-pc-windows-msvc"
163+
mkdir "$staging"
164+
cp "target/x86_64-pc-windows-msvc/release/jj.exe" "$staging/"
165+
cd "$staging"
166+
7z a "../$staging.zip" .
167+
echo "asset=${staging}.zip" >> "$GITHUB_OUTPUT"
168+
169+
- name: Upload artifact
170+
uses: actions/upload-artifact@v4
171+
with:
172+
name: jj-x86_64-pc-windows-msvc
173+
path: ${{ steps.package.outputs.asset }}
174+
175+
release:
176+
name: Create release
177+
needs: [version, build-linux, build-macos, build-windows]
178+
runs-on: ubuntu-latest
179+
steps:
180+
- name: Download all artifacts
181+
uses: actions/download-artifact@v4
182+
with:
183+
path: artifacts
184+
185+
- uses: sjawhar/.github/.github/actions/sami-release@v1
186+
with:
187+
tag: ${{ needs.version.outputs.tag }}
188+
files: artifacts/**/*

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
3636
interpolates `$path` with the repo-relative file path. This enables Git LFS
3737
and other gitattributes filter integrations in colocated repositories.
3838
([Issue #80](https://github.com/jj-vcs/jj/issues/80))
39+
40+
* Added `git.ignore-filters` setting to specify what filtered files in
41+
`.gitattributes` are ignored by `jj`. Defaults to `["lfs"]`.
42+
3943
### Fixed bugs
4044

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

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cli/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ once_cell = { workspace = true }
9494
pest = { workspace = true }
9595
pest_derive = { workspace = true }
9696
pollster = { workspace = true }
97+
prost = { workspace = true }
9798
rand = { workspace = true }
9899
rand_chacha = { workspace = true }
99100
regex = { workspace = true }

cli/examples/custom-backend/main.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,9 @@ fn create_store_factories() -> StoreFactories {
6161
// must match `Backend::name()`.
6262
store_factories.add_backend(
6363
"jit",
64-
Box::new(|settings, store_path| Ok(Box::new(JitBackend::load(settings, store_path)?))),
64+
Box::new(|settings, store_path, _workspace_root| {
65+
Ok(Box::new(JitBackend::load(settings, store_path)?))
66+
}),
6567
);
6668
store_factories
6769
}

cli/src/cli_util.rs

Lines changed: 56 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,13 @@ use crate::diff_util::DiffRenderer;
178178
use crate::formatter::FormatRecorder;
179179
use crate::formatter::Formatter;
180180
use crate::formatter::FormatterExt as _;
181+
use crate::git_util::is_colocated_git_workspace;
182+
#[cfg(feature = "git")]
183+
use crate::git_util::load_git_import_options;
184+
#[cfg(feature = "git")]
185+
use crate::git_util::print_git_export_stats;
186+
#[cfg(feature = "git")]
187+
use crate::git_util::print_git_import_stats_summary;
181188
use crate::merge_tools::DiffEditor;
182189
use crate::merge_tools::MergeEditor;
183190
use crate::merge_tools::MergeToolConfigError;
@@ -1125,8 +1132,7 @@ impl WorkspaceCommandHelper {
11251132
let op_summary_template_text = settings.get_string("templates.op_summary")?;
11261133
let may_update_working_copy =
11271134
loaded_at_head && !env.command.global_args().ignore_working_copy;
1128-
let working_copy_shared_with_git =
1129-
crate::git_util::is_colocated_git_workspace(&workspace, &repo);
1135+
let working_copy_shared_with_git = is_colocated_git_workspace(&workspace, &repo);
11301136

11311137
let helper = Self {
11321138
workspace,
@@ -1241,7 +1247,7 @@ impl WorkspaceCommandHelper {
12411247
}
12421248

12431249
/// Snapshots the working copy if allowed, and imports Git refs if the
1244-
/// working copy is collocated with Git.
1250+
/// working copy is colocated with Git.
12451251
///
12461252
/// Returns whether a snapshot was taken.
12471253
#[instrument(skip_all)]
@@ -1269,17 +1275,45 @@ impl WorkspaceCommandHelper {
12691275
git_import_export_lock: &GitImportExportLock,
12701276
) -> Result<(), CommandError> {
12711277
assert!(self.may_update_working_copy);
1278+
let workspace_name = self.workspace_name().to_owned();
1279+
// Check if workspace had a git_head before we start the transaction
1280+
let old_workspace_git_head_present = self
1281+
.repo()
1282+
.view()
1283+
.get_workspace_git_head(&workspace_name)
1284+
.is_present();
12721285
let mut tx = self.start_transaction();
1273-
jj_lib::git::import_head(tx.repo_mut()).block_on()?;
1274-
if !tx.repo().has_changes() {
1286+
let head_changed = jj_lib::git::import_head(tx.repo_mut(), &workspace_name).block_on()?;
1287+
if !head_changed {
1288+
// No change for this workspace's git HEAD
1289+
if tx.repo().has_changes() {
1290+
// Other worktree heads may have been imported
1291+
self.user_repo =
1292+
ReadonlyUserRepo::new(tx.into_inner().commit("import git head").block_on()?);
1293+
}
12751294
return Ok(());
12761295
}
12771296

12781297
let mut tx = tx.into_inner();
1279-
let old_git_head = self.repo().view().git_head().clone();
1280-
let new_git_head = tx.repo().view().git_head().clone();
1281-
if let Some(new_git_head_id) = new_git_head.as_normal() {
1282-
let workspace_name = self.workspace_name().to_owned();
1298+
let new_workspace_git_head = tx
1299+
.repo()
1300+
.view()
1301+
.get_workspace_git_head(&workspace_name)
1302+
.clone();
1303+
if let Some(new_git_head_id) = new_workspace_git_head.as_normal() {
1304+
// Check if workspace already has a WC commit with the correct parent.
1305+
// This avoids creating spurious commits when switching between workspaces.
1306+
if let Some(current_wc_id) = tx.repo().view().get_wc_commit_id(&workspace_name) {
1307+
let current_wc = tx.repo().store().get_commit(current_wc_id)?;
1308+
if current_wc.parent_ids().contains(new_git_head_id) {
1309+
// Workspace already has a working copy with the correct parent,
1310+
// no new checkout needed
1311+
self.user_repo =
1312+
ReadonlyUserRepo::new(tx.commit("import git head").block_on()?);
1313+
return Ok(());
1314+
}
1315+
}
1316+
12831317
let new_git_head_commit = tx.repo().store().get_commit(new_git_head_id)?;
12841318
let wc_commit = tx
12851319
.repo_mut()
@@ -1295,7 +1329,7 @@ impl WorkspaceCommandHelper {
12951329
locked_ws
12961330
.finish(self.user_repo.repo.op_id().clone())
12971331
.block_on()?;
1298-
if old_git_head.is_present() {
1332+
if old_workspace_git_head_present {
12991333
writeln!(
13001334
ui.status(),
13011335
"Reset the working copy parent to the new Git HEAD."
@@ -1328,11 +1362,10 @@ impl WorkspaceCommandHelper {
13281362
use jj_lib::git;
13291363
let git_settings = git::GitSettings::from_settings(self.settings())?;
13301364
let remote_settings = self.settings().remote_settings()?;
1331-
let import_options =
1332-
crate::git_util::load_git_import_options(ui, &git_settings, &remote_settings)?;
1365+
let import_options = load_git_import_options(ui, &git_settings, &remote_settings)?;
13331366
let mut tx = self.start_transaction();
13341367
let stats = git::import_refs(tx.repo_mut(), &import_options).block_on()?;
1335-
crate::git_util::print_git_import_stats_summary(ui, &stats)?;
1368+
print_git_import_stats_summary(ui, &stats)?;
13361369
if !tx.repo().has_changes() {
13371370
return Ok(());
13381371
}
@@ -2213,7 +2246,14 @@ to the current parents may contain changes from multiple commits.
22132246
// This can still fail if HEAD was updated concurrently by another JJ process
22142247
// (overlapping transaction) or a non-JJ process (e.g., git checkout). In that
22152248
// case, the actual state will be imported on the next snapshot.
2216-
match jj_lib::git::reset_head(tx.repo_mut(), wc_commit).block_on() {
2249+
match jj_lib::git::reset_head_at_workspace(
2250+
tx.repo_mut(),
2251+
wc_commit,
2252+
self.workspace_name(),
2253+
Some(self.workspace_root()),
2254+
)
2255+
.block_on()
2256+
{
22172257
Ok(()) => {}
22182258
Err(err @ jj_lib::git::GitResetHeadError::UpdateHeadRef(_)) => {
22192259
writeln!(ui.warning_default(), "{err}")?;
@@ -2223,7 +2263,7 @@ to the current parents may contain changes from multiple commits.
22232263
}
22242264
}
22252265
let stats = jj_lib::git::export_refs(tx.repo_mut())?;
2226-
crate::git_util::print_git_export_stats(ui, &stats)?;
2266+
print_git_export_stats(ui, &stats)?;
22272267
}
22282268

22292269
self.user_repo = ReadonlyUserRepo::new(tx.commit(description).block_on()?);
@@ -2493,7 +2533,7 @@ pub fn export_working_copy_changes_to_git(
24932533
let repo = mut_repo.base_repo().as_ref();
24942534
jj_lib::git::update_intent_to_add(repo, old_tree, new_tree).block_on()?;
24952535
let stats = jj_lib::git::export_refs(mut_repo)?;
2496-
crate::git_util::print_git_export_stats(ui, &stats)?;
2536+
print_git_export_stats(ui, &stats)?;
24972537
Ok(())
24982538
}
24992539
#[cfg(not(feature = "git"))]

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::object_id::ObjectId as _;
@@ -768,6 +769,12 @@ impl From<GitIgnoreError> for CommandError {
768769
}
769770
}
770771

772+
impl From<GitAttributesError> for CommandError {
773+
fn from(err: GitAttributesError) -> Self {
774+
user_error_with_message("Failed to process .gitattributes.", err)
775+
}
776+
}
777+
771778
impl From<ParseBulkEditMessageError> for CommandError {
772779
fn from(err: ParseBulkEditMessageError) -> Self {
773780
user_error(err)

0 commit comments

Comments
 (0)