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
50 changes: 32 additions & 18 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ on:
description: "Existing tag to (re)build, e.g. v0.9.0"
required: true

# Every job uploads to the release, so every job needs to write. The token
# reaches only the steps that name it: no checkout keeps it, and every
# action is pinned to a commit, since a tag can be moved under a release
# that signs with the secrets below.
permissions:
contents: write

Expand All @@ -25,9 +29,10 @@ jobs:
tag: ${{ steps.version.outputs.tag }}
version: ${{ steps.version.outputs.version }}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
with:
ref: ${{ inputs.tag || github.ref }}
persist-credentials: false

- name: Version
id: version
Expand Down Expand Up @@ -69,9 +74,10 @@ jobs:
- { runner: ubuntu-22.04-arm, arch: aarch64 }
runs-on: ${{ matrix.runner }}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
with:
ref: ${{ inputs.tag || github.ref }}
persist-credentials: false

- name: System libraries
run: |
Expand All @@ -81,11 +87,11 @@ jobs:
libx11-xcb-dev libxcb1-dev libfontconfig-dev libfreetype-dev \
libvulkan-dev libasound2-dev libssl-dev libzstd-dev

- uses: dtolnay/rust-toolchain@master
- uses: dtolnay/rust-toolchain@e2a55d2ffb04f378e9626c28d38b36d230d1e12f # master
with:
toolchain: "1.97"

- uses: Swatinem/rust-cache@v2
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2

- name: Test
run: cargo xtask test
Expand Down Expand Up @@ -137,29 +143,31 @@ jobs:
# MACOS_SIGN_IDENTITY "Developer ID Application: Name (TEAMID)"
# NOTARY_KEY_P8 base64 of an App Store Connect API key
# NOTARY_KEY_ID, NOTARY_ISSUER that key's ID and issuer
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
MACOS_SIGN_IDENTITY: ${{ secrets.MACOS_SIGN_IDENTITY }}
NOTARY_KEY_P8: ${{ secrets.NOTARY_KEY_P8 }}
NOTARY_KEY_ID: ${{ secrets.NOTARY_KEY_ID }}
NOTARY_ISSUER: ${{ secrets.NOTARY_ISSUER }}
# Only whether there is a certificate is known to the whole job. The
# secrets themselves go to the two steps that use them, not to the
# tests and every dependency's build script before those.
HAS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE != '' }}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
with:
ref: ${{ inputs.tag || github.ref }}
persist-credentials: false

- uses: dtolnay/rust-toolchain@master
- uses: dtolnay/rust-toolchain@e2a55d2ffb04f378e9626c28d38b36d230d1e12f # master
with:
toolchain: "1.97"

- uses: Swatinem/rust-cache@v2
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2

- name: Test
run: cargo xtask test

# A keychain of its own for the certificate, deleted with the runner.
- name: Signing certificate
if: env.MACOS_CERTIFICATE != ''
if: env.HAS_CERTIFICATE == 'true'
env:
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
run: |
keychain="$RUNNER_TEMP/signing.keychain-db"
password=$(uuidgen)
Expand All @@ -176,9 +184,14 @@ jobs:
rm "$RUNNER_TEMP/cert.p12"

- name: Bundle
env:
MACOS_SIGN_IDENTITY: ${{ secrets.MACOS_SIGN_IDENTITY }}
NOTARY_KEY_P8: ${{ secrets.NOTARY_KEY_P8 }}
NOTARY_KEY_ID: ${{ secrets.NOTARY_KEY_ID }}
NOTARY_ISSUER: ${{ secrets.NOTARY_ISSUER }}
run: |
args=()
if [ -n "$MACOS_CERTIFICATE" ]; then
if [ "$HAS_CERTIFICATE" = true ]; then
args+=(--sign "$MACOS_SIGN_IDENTITY")
if [ -n "$NOTARY_KEY_P8" ]; then
echo "$NOTARY_KEY_P8" | base64 --decode \
Expand Down Expand Up @@ -218,15 +231,16 @@ jobs:
steps:
- run: git config --global core.autocrlf false

- uses: actions/checkout@v5
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
with:
ref: ${{ inputs.tag || github.ref }}
persist-credentials: false

- uses: dtolnay/rust-toolchain@master
- uses: dtolnay/rust-toolchain@e2a55d2ffb04f378e9626c28d38b36d230d1e12f # master
with:
toolchain: "1.97"

- uses: Swatinem/rust-cache@v2
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2

- name: Test
run: cargo xtask test
Expand Down
57 changes: 57 additions & 0 deletions crates/disktree-app/src/git.rs
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,12 @@ fn git(path: &Path, args: &[&str]) -> Option<std::process::Output> {
.env("GIT_TERMINAL_PROMPT", "0")
.env("GIT_OPTIONAL_LOCKS", "0")
.env("GIT_NO_LAZY_FETCH", "1")
// `protocol.allow=never` above is only the default: a checkout's
// own `protocol.ext.allow=always` is more specific and wins, and a
// git older than 2.44 does not know `GIT_NO_LAZY_FETCH`. With this
// set git asks no configuration at all, only this list, and no
// transport is called `none`.
.env("GIT_ALLOW_PROTOCOL", "none")
.stdin(Stdio::null())
.stderr(Stdio::null())
.output()
Expand Down Expand Up @@ -293,6 +299,57 @@ mod tests {
assert!(!marker.exists(), "the checkout's fsmonitor ran");
}

/// A partial clone whose promisor remote is a command, allowed by the
/// checkout's own configuration, and a branch whose commits are
/// missing: fetching them would run it. Whatever git makes of such a
/// checkout, the command must not run.
#[cfg(unix)]
#[test]
fn a_checkout_cannot_make_git_fetch_through_its_own_transport() {
let dir = tempfile::tempdir().expect("tempdir");
let marker = dir.path().join("ran");
let git = |args: &[&str]| {
Command::new("git")
.arg("-C")
.arg(dir.path())
.args(args)
.output()
.is_ok_and(|output| output.status.success())
};
if !git_installed() || !git(&["init", "-q"]) {
return; // no git on this machine
}
let url = format!("ext::sh -c 'touch \"{}\"'", marker.display());
for (key, value) in [
("core.repositoryformatversion", "1"),
("extensions.partialClone", "origin"),
("remote.origin.url", url.as_str()),
("remote.origin.promisor", "true"),
("remote.origin.fetch", "+refs/heads/*:refs/remotes/origin/*"),
("protocol.ext.allow", "always"),
("branch.main.remote", "origin"),
("branch.main.merge", "refs/heads/main"),
] {
assert!(git(&["config", key, value]), "{key}");
}
// Two commits nobody has: the branch, and what it tracks.
let store = dir.path().join(".git");
for (name, id) in [
("refs/heads/main", "1".repeat(40)),
("refs/remotes/origin/main", "2".repeat(40)),
] {
let path = store.join(name);
std::fs::create_dir_all(path.parent().expect("a parent"))
.expect("mkdir");
std::fs::write(path, format!("{id}\n")).expect("write ref");
}
std::fs::write(store.join("HEAD"), "ref: refs/heads/main\n")
.expect("write HEAD");

let _ = state(dir.path());
assert!(!marker.exists(), "the checkout's transport ran");
}

/// A signed stash and `log.showSignature`: listing stashes the way
/// `git stash list` does would run the checkout's `gpg.program`.
#[cfg(unix)]
Expand Down
12 changes: 12 additions & 0 deletions crates/disktree-app/src/marks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ use rustc_hash::FxHashSet;
pub struct Marks {
items: Vec<Target>,
index: FxHashSet<PathBuf>,
/// Bumped whenever the marks, or the tree they were last read from,
/// change: what a cache derived from them is keyed by.
generation: u64,
}

impl Marks {
Expand All @@ -34,10 +37,16 @@ impl Marks {
self.items.is_empty()
}

/// See the field: differs whenever the marks may have.
pub const fn generation(&self) -> u64 {
self.generation
}

/// Mark `target`, or unmark it when it is already marked.
///
/// Returns `true` when the path ended up marked.
pub fn toggle(&mut self, target: Target) -> bool {
self.generation += 1;
if self.index.remove(&target.path) {
self.items.retain(|item| item.path != target.path);
false
Expand All @@ -49,12 +58,14 @@ impl Marks {
}

pub fn remove(&mut self, path: &Path) {
self.generation += 1;
if self.index.remove(path) {
self.items.retain(|item| item.path != path);
}
}

pub fn clear(&mut self) {
self.generation += 1;
self.items.clear();
self.index.clear();
}
Expand All @@ -78,6 +89,7 @@ impl Marks {
}
}
self.items = resolved;
self.generation += 1;
}
}

Expand Down
Loading