From fce4bba35630597042aee958d9125deaa2d4272c Mon Sep 17 00:00:00 2001 From: Vladislav Ivanov Date: Thu, 24 Nov 2022 22:33:27 +0700 Subject: [PATCH 1/4] ssh test server: wait for stdout copy commit-id:56d1b514 --- josh-ssh-dev-server/main.go | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/josh-ssh-dev-server/main.go b/josh-ssh-dev-server/main.go index af4fbadc5..6c45f4f32 100644 --- a/josh-ssh-dev-server/main.go +++ b/josh-ssh-dev-server/main.go @@ -113,12 +113,10 @@ func runServer(port uint, shell string) { } }() - go func() { - _, err = io.Copy(session, stdout) - if err != nil { - return - } - }() + _, err = io.Copy(session, stdout) + if err != nil { + return + } _ = cmd.Wait() log.Printf("subprocess with task_id %d exited\n", taskId) From 80f848b82442e475fdce24ee1e9824076a2cc20b Mon Sep 17 00:00:00 2001 From: Christian Schilling Date: Fri, 25 Nov 2022 11:59:44 +0100 Subject: [PATCH 2/4] Remove use of deprecated function Change: from-timestamp-opt --- src/graphql.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/graphql.rs b/src/graphql.rs index c84177a27..f312c69cb 100644 --- a/src/graphql.rs +++ b/src/graphql.rs @@ -147,7 +147,8 @@ impl Revision { let ts = filter_commit.time().seconds(); - let ndt = chrono::NaiveDateTime::from_timestamp(ts, 0); + let ndt = chrono::NaiveDateTime::from_timestamp_opt(ts, 0) + .ok_or(josh_error("from_timestamp_opt"))?; Ok(ndt.format(&format).to_string()) } From 455275ca878bfd760c385f0acb363fe6a7c11bee Mon Sep 17 00:00:00 2001 From: Louis-Marie Givel Date: Wed, 19 Oct 2022 15:42:56 +0200 Subject: [PATCH 3/4] Add :unsign filter Change: drop --- docs/src/reference/filters.md | 7 +++++ josh-proxy/src/lib.rs | 1 + src/filter/mod.rs | 26 ++++++++++++++++++ src/filter/opt.rs | 1 + src/filter/parse.rs | 1 + src/history.rs | 51 ++++++++++++++++++++--------------- tests/filter/gpgsig.t | 22 +++++++++++++++ 7 files changed, 88 insertions(+), 21 deletions(-) diff --git a/docs/src/reference/filters.md b/docs/src/reference/filters.md index 398e43439..0d07c53cf 100644 --- a/docs/src/reference/filters.md +++ b/docs/src/reference/filters.md @@ -65,6 +65,13 @@ workspace root as well as additional files specified in the ``workspace.josh`` f ### Text replacement **`:replace="regex","replacement"`** Applies the supplied regular expression to every file in the input tree. +### Signature removal **`:unsign` +The default behaviour of Josh is to copy, if it exsists, the signature of the original commit in +the filtered commit. This makes the signature invalid, but allows a perfect round-trip: josh will be +able to recreate the original commit from the filtered one. + +This behaviour might not be desirable, and this filter drops the signatures from the history. + ## Pattern filters The following filters accept a glob like pattern ``X`` that can contain ``*`` to diff --git a/josh-proxy/src/lib.rs b/josh-proxy/src/lib.rs index 78ebf498a..89b84b67c 100644 --- a/josh-proxy/src/lib.rs +++ b/josh-proxy/src/lib.rs @@ -403,6 +403,7 @@ fn split_changes( &vec![&parent], &new_tree, None, + false, )?; changes[i].1 = new_commit; new_bases.push(new_commit); diff --git a/src/filter/mod.rs b/src/filter/mod.rs index ed854ef8f..da618f9a1 100644 --- a/src/filter/mod.rs +++ b/src/filter/mod.rs @@ -100,6 +100,7 @@ enum Op { Squash(Option>), Rev(std::collections::HashMap), Linear, + Unsign, RegexReplace(regex::Regex, String), @@ -277,6 +278,7 @@ fn spec2(op: &Op) -> String { format!(":SQUASH={}", s) } Op::Linear => ":linear".to_string(), + Op::Unsign => ":unsign".to_string(), Op::Subdir(path) => format!(":/{}", parse::quote(&path.to_string_lossy())), Op::File(path) => format!("::{}", parse::quote(&path.to_string_lossy())), Op::Prefix(path) => format!(":prefix={}", parse::quote(&path.to_string_lossy())), @@ -387,6 +389,7 @@ fn apply_to_commit2( &[], &commit.tree()?, None, + true, )) .transpose() } @@ -467,6 +470,28 @@ fn apply_to_commit2( )) .transpose(); } + Op::Unsign => { + let parents: Vec<_> = commit.parent_ids().collect(); + + let filtered_parents: Vec<_> = parents + .iter() + .map(|p| transaction.get(filter, *p)) + .collect(); + if filtered_parents.iter().any(|p| p.is_none()) { + return Ok(None); + } + let filtered_parents = filtered_parents.iter().map(|p| p.unwrap()).collect(); + + return Some(history::remove_commit_signature( + commit, + filtered_parents, + commit.tree()?, + transaction, + filter, + None, + )) + .transpose(); + } Op::Compose(filters) => { let filtered = filters .iter() @@ -661,6 +686,7 @@ fn apply2<'a>( Op::Squash(None) => Ok(tree), Op::Squash(Some(_)) => Err(josh_error("not applicable to tree")), Op::Linear => Ok(tree), + Op::Unsign => Ok(tree), Op::Rev(_) => Err(josh_error("not applicable to tree")), Op::RegexReplace(regex, replacement) => { tree::regex_replace(tree.id(), ®ex, &replacement, transaction) diff --git a/src/filter/opt.rs b/src/filter/opt.rs index 868ae0808..6beb8df65 100644 --- a/src/filter/opt.rs +++ b/src/filter/opt.rs @@ -422,6 +422,7 @@ pub fn invert(filter: Filter) -> JoshResult { let result = match to_op(filter) { Op::Nop => Some(Op::Nop), Op::Linear => Some(Op::Nop), + Op::Unsign => Some(Op::Unsign), Op::Empty => Some(Op::Empty), Op::Subdir(path) => Some(Op::Prefix(path)), Op::File(path) => Some(Op::File(path)), diff --git a/src/filter/parse.rs b/src/filter/parse.rs index dbf084568..4127cfcbb 100644 --- a/src/filter/parse.rs +++ b/src/filter/parse.rs @@ -37,6 +37,7 @@ fn make_op(args: &[&str]) -> JoshResult { ["SQUASH"] => Ok(Op::Squash(None)), ["SQUASH", _ids @ ..] => Err(josh_error("SQUASH with ids can't be parsed")), ["linear"] => Ok(Op::Linear), + ["unsign"] => Ok(Op::Unsign), ["PATHS"] => Ok(Op::Paths), #[cfg(feature = "search")] ["INDEX"] => Ok(Op::Index), diff --git a/src/history.rs b/src/history.rs index 21a70c6c7..25eea1953 100644 --- a/src/history.rs +++ b/src/history.rs @@ -182,13 +182,8 @@ pub fn rewrite_commit( parents: &[&git2::Commit], tree: &git2::Tree, message: Option<(String, String, String)>, + unsign: bool, ) -> JoshResult { - if message == None && base.tree()?.id() == tree.id() && all_equal(base.parents(), parents) { - // Looks like an optimization, but in fact serves to not change the commit in case - // it was signed. - return Ok(base.id()); - } - let b = if let Some((message, author, email)) = message { let a = base.author(); let new_a = git2::Signature::new(&author, &email, &a.when())?; @@ -205,7 +200,7 @@ pub fn rewrite_commit( )? }; - if let Ok((sig, _)) = repo.extract_signature(&base.id(), None) { + if let (false, Ok((sig, _))) = (unsign, repo.extract_signature(&base.id(), None)) { // Re-create the object with the original signature (which of course does not match any // more, but this is needed to guarantee perfect round-trips). let b = b @@ -220,20 +215,6 @@ pub fn rewrite_commit( return Ok(repo.odb()?.write(git2::ObjectType::Commit, &b)?); } -fn all_equal(a: git2::Parents, b: &[&git2::Commit]) -> bool { - let a: Vec<_> = a.collect(); - if a.len() != b.len() { - return false; - } - - for (x, y) in b.iter().zip(a.iter()) { - if x.id() != y.id() { - return false; - } - } - true -} - fn find_oldest_similar_commit( transaction: &cache::Transaction, filter: filter::Filter, @@ -527,6 +508,7 @@ pub fn unapply_filter( &original_parents_refs, &new_tree, None, + false, )?; if let Some(ref mut change_ids) = change_ids { @@ -560,6 +542,30 @@ fn select_parent_commits<'a>( } } +pub fn remove_commit_signature<'a>( + original_commit: &'a git2::Commit, + filtered_parent_ids: Vec, + filtered_tree: git2::Tree<'a>, + transaction: &cache::Transaction, + filter: filter::Filter, + message: Option<(String, String, String)>, +) -> JoshResult { + let (r, is_new) = create_filtered_commit2( + transaction.repo(), + original_commit, + filtered_parent_ids, + filtered_tree, + message, + true, + )?; + + let store = is_new || original_commit.parent_ids().len() != 1; + + transaction.insert(filter, original_commit.id(), r, store); + + Ok(r) +} + pub fn drop_commit<'a>( original_commit: &'a git2::Commit, filtered_parent_ids: Vec, @@ -591,6 +597,7 @@ pub fn create_filtered_commit<'a>( filtered_parent_ids, filtered_tree, message, + false, )?; let store = is_new || original_commit.parent_ids().len() != 1; @@ -606,6 +613,7 @@ fn create_filtered_commit2<'a>( filtered_parent_ids: Vec, filtered_tree: git2::Tree<'a>, message: Option<(String, String, String)>, + unsign: bool, ) -> JoshResult<(git2::Oid, bool)> { let filtered_parent_commits: Result, _> = filtered_parent_ids .iter() @@ -651,6 +659,7 @@ fn create_filtered_commit2<'a>( &selected_filtered_parent_commits, &filtered_tree, message, + unsign, )?, true, )) diff --git a/tests/filter/gpgsig.t b/tests/filter/gpgsig.t index c29e3caba..cfdd817e8 100644 --- a/tests/filter/gpgsig.t +++ b/tests/filter/gpgsig.t @@ -35,3 +35,25 @@ If 0b4cf6c9efbbda1eada39fa9c1d21d2525b027bb shows up then the signature was lost $ git rev-parse master double-filtered cb22ebb8e47b109f7add68b1043e561e0db09802 cb22ebb8e47b109f7add68b1043e561e0db09802 + +Remove the signature, the shas are different. + $ josh-filter :unsign refs/heads/master --update refs/heads/filtered -s + [1] :unsign + $ git rev-parse master filtered + cb22ebb8e47b109f7add68b1043e561e0db09802 + 0b4cf6c9efbbda1eada39fa9c1d21d2525b027bb + $ josh-filter --reverse :unsign refs/heads/double-filtered --update refs/heads/filtered -s + [1] :unsign + $ git rev-parse master double-filtered + cb22ebb8e47b109f7add68b1043e561e0db09802 + cb22ebb8e47b109f7add68b1043e561e0db09802 + +Round trip does not work but reversed works since the commit exists + $ josh-filter :prefix=extra:unsign refs/heads/master --update refs/heads/filtered + $ josh-filter :/extra refs/heads/filtered --update refs/heads/double-filtered + $ git branch reversed + $ josh-filter --reverse :prefix=extra:unsign refs/heads/reversed --update refs/heads/filtered + $ git rev-parse master double-filtered reversed + cb22ebb8e47b109f7add68b1043e561e0db09802 + 0b4cf6c9efbbda1eada39fa9c1d21d2525b027bb + cb22ebb8e47b109f7add68b1043e561e0db09802 From 7a64351583e0f0640b4265e584cca4ca99ae49d5 Mon Sep 17 00:00:00 2001 From: Vladislav Ivanov Date: Thu, 24 Nov 2022 11:42:18 +0700 Subject: [PATCH 4/4] Finalize SSH fetch support * Merge s6 into the main release container * Switch docker image to alpine * Rewrite startup scripts commit-id:f2953c48 --- .github/workflows/release.yml | 3 +- Dockerfile | 174 ++++++++++++------ .../etc/ssh/sshd_config.template | 4 +- .../docker/s6-rc.d => docker}/finish | 0 .../josh-auth-key.sh => docker/josh-auth-key | 0 docker/josh-ensure-dir | 17 ++ docker/josh-ensure-mode | 21 +++ docker/josh-ensure-owner | 21 +++ docker/s6-rc.d/josh-create-local/type | 1 + docker/s6-rc.d/josh-create-local/up | 7 + docker/s6-rc.d/josh-generate-keys/type | 1 + docker/s6-rc.d/josh-generate-keys/up | 28 +++ docker/s6-rc.d/josh/dependencies | 2 + docker/s6-rc.d/josh/run | 21 +++ docker/s6-rc.d/josh/type | 1 + docker/s6-rc.d/sshd-generate-config/type | 1 + docker/s6-rc.d/sshd-generate-config/up | 11 ++ docker/s6-rc.d/sshd/dependencies | 2 + docker/s6-rc.d/sshd/run | 3 + docker/s6-rc.d/sshd/type | 1 + docker/s6-rc.d/user/contents.d/josh | 0 docker/s6-rc.d/user/contents.d/sshd | 0 josh-proxy/src/lib.rs | 1 - josh-ssh-shell/docker/Dockerfile | 99 ---------- josh-ssh-shell/docker/josh-generate-keys.sh | 59 ------ tester.sh | 2 +- tests/filter/cmdline.t | 4 +- tests/filter/workspace_modify_chain.t | 2 +- tests/filter/workspace_shadow_file.t | 7 +- tests/proxy/clone_subtree_no_master.t | 4 +- tests/proxy/workspace_in_workspace_prefix.t | 2 +- .../workspace_modify_chain_prefix_subtree.t | 2 +- 32 files changed, 271 insertions(+), 230 deletions(-) rename josh-ssh-shell/docker/etc/ssh/sshd_config => docker/etc/ssh/sshd_config.template (90%) rename {josh-ssh-shell/docker/s6-rc.d => docker}/finish (100%) rename josh-ssh-shell/docker/josh-auth-key.sh => docker/josh-auth-key (100%) mode change 100644 => 100755 create mode 100755 docker/josh-ensure-dir create mode 100755 docker/josh-ensure-mode create mode 100755 docker/josh-ensure-owner create mode 100644 docker/s6-rc.d/josh-create-local/type create mode 100755 docker/s6-rc.d/josh-create-local/up create mode 100644 docker/s6-rc.d/josh-generate-keys/type create mode 100755 docker/s6-rc.d/josh-generate-keys/up create mode 100644 docker/s6-rc.d/josh/dependencies create mode 100755 docker/s6-rc.d/josh/run create mode 100644 docker/s6-rc.d/josh/type create mode 100644 docker/s6-rc.d/sshd-generate-config/type create mode 100755 docker/s6-rc.d/sshd-generate-config/up create mode 100644 docker/s6-rc.d/sshd/dependencies create mode 100755 docker/s6-rc.d/sshd/run create mode 100644 docker/s6-rc.d/sshd/type create mode 100644 docker/s6-rc.d/user/contents.d/josh create mode 100644 docker/s6-rc.d/user/contents.d/sshd delete mode 100644 josh-ssh-shell/docker/Dockerfile delete mode 100644 josh-ssh-shell/docker/josh-generate-keys.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 325fc59e0..b2ac6aedb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,7 +11,7 @@ jobs: if: ${{ github.event_name == 'release' || (github.event_name == 'pull_request' && github.event.label.name == 'build-release-container') }} steps: - name: Setup BuildX - uses: docker/setup-buildx-action@v1 + uses: docker/setup-buildx-action@v2 - name: Checkout uses: actions/checkout@v3 with: @@ -40,6 +40,7 @@ jobs: cache-to: type=gha,mode=max build-contexts: | git=.git + docker=docker target: run push: ${{ github.event_name == 'release' && 'true' || 'false' }} tags: ${{ steps.meta.outputs.tags }} diff --git a/Dockerfile b/Dockerfile index bb4586ce2..5bbaa585d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,31 @@ # syntax=docker/dockerfile:1.4-labs +ARG ALPINE_VERSION=3.17 + +FROM alpine:${ALPINE_VERSION} as rust-base + +RUN apk add --no-cache ca-certificates gcc musl-dev + +ENV RUSTUP_HOME=/usr/local/rustup +ENV CARGO_HOME=/usr/local/cargo +ENV PATH=/usr/local/cargo/bin:${PATH} + +ARG RUSTUP_VERSION=1.25.1 ARG RUST_VERSION=1.61.0 +ARG RUST_ARCH=x86_64-unknown-linux-musl + +# https://github.com/sfackler/rust-openssl/issues/1462 +ENV RUSTFLAGS="-Ctarget-feature=-crt-static" -FROM rust:${RUST_VERSION} as dev-planner +ADD --chmod=755 https://static.rust-lang.org/rustup/archive/${RUSTUP_VERSION}/${RUST_ARCH}/rustup-init /tmp +RUN /tmp/rustup-init \ + -y \ + --no-modify-path \ + --profile minimal \ + --default-toolchain ${RUST_VERSION} \ + --default-host ${RUST_ARCH} + +FROM rust-base as dev-planner RUN cargo install --version 0.1.35 cargo-chef @@ -12,37 +35,32 @@ COPY . . ENV CARGO_TARGET_DIR=/opt/cargo-target RUN cargo chef prepare --recipe-path recipe.json -FROM rust:${RUST_VERSION} as dev +FROM rust-base as dev -RUN < josh::JoshResult<()> { fn make_ssh_command() -> String { let ssh_options = [ - "IdentitiesOnly=yes", "LogLevel=ERROR", "UserKnownHostsFile=/dev/null", "StrictHostKeyChecking=no", diff --git a/josh-ssh-shell/docker/Dockerfile b/josh-ssh-shell/docker/Dockerfile deleted file mode 100644 index 48918e128..000000000 --- a/josh-ssh-shell/docker/Dockerfile +++ /dev/null @@ -1,99 +0,0 @@ -# syntax = docker/dockerfile:1.4-labs - -FROM alpine:3.16 - -RUN apk add --no-cache openssh bash git xz tree shadow - -ARG S6_OVERLAY_VERSION=3.1.2.1 -ADD https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-noarch.tar.xz /tmp -RUN tar -C / -Jxpf /tmp/s6-overlay-noarch.tar.xz -ADD https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-x86_64.tar.xz /tmp -RUN tar -C / -Jxpf /tmp/s6-overlay-x86_64.tar.xz - -ARG GIT_GID_UID=2001 - -RUN addgroup -g ${GIT_GID_UID} git -RUN adduser \ - -h /home/git \ - -s josh-ssh-shell \ - -G git \ - -D \ - -u ${GIT_GID_UID} \ - git - -# sshd will call josh-ssh-shell -c "git command" - -# https://unix.stackexchange.com/a/193131/336647 -RUN usermod -p '*' git - -COPY josh-auth-key.sh /opt/scripts/ -COPY etc/ssh/sshd_config /etc/ssh/ - -ARG RC6_D=/etc/s6-overlay/s6-rc.d - -# s6: josh-generate-keys - -COPY josh-generate-keys.sh /opt/scripts/ - -WORKDIR ${RC6_D}/josh-generate-keys - -COPY <&1 echo "Persistent volume not mounted" - exit 1 - fi - - _ensure_dir ${KEY_DIR} - _ensure_owner ${KEY_DIR} git:git - _ensure_mode ${KEY_DIR} 700 - - if { - [[ ! -f ${KEY_DIR}/id_${KEY_TYPE} ]] || [[ ! -f ${KEY_DIR}/id_${KEY_TYPE}.pub ]] - }; then - 2>&1 echo "Generating SSH server key" - ssh-keygen -t ${KEY_TYPE} -N "" -f ${KEY_DIR}/id_${KEY_TYPE} -C git - fi - - _ensure_owner ${KEY_DIR}/id_${KEY_TYPE} git:git - _ensure_mode ${KEY_DIR}/id_${KEY_TYPE} 600 - - _ensure_owner ${KEY_DIR}/id_${KEY_TYPE}.pub git:git - _ensure_mode ${KEY_DIR}/id_${KEY_TYPE}.pub 644 -} - -_create_keys diff --git a/tester.sh b/tester.sh index b66508f38..608206b44 100755 --- a/tester.sh +++ b/tester.sh @@ -9,7 +9,7 @@ set -e shopt -s extglob shopt -s inherit_errexit -if (( $# > 1 )) && [[ "${1}" == "--no-build-container" ]]; then +if (( $# >= 1 )) && [[ "${1}" == "--no-build-container" ]]; then NO_BUILD_CONTAINER=1 shift else diff --git a/tests/filter/cmdline.t b/tests/filter/cmdline.t index 300f076a8..669d0fdcb 100644 --- a/tests/filter/cmdline.t +++ b/tests/filter/cmdline.t @@ -107,11 +107,11 @@ * initial $ cat c/.joshinfo - cat: c/.joshinfo: No such file or directory + *: No such file or directory (glob) [1] $ cat a/b/.joshinfo - cat: a/b/.joshinfo: No such file or directory + *: No such file or directory (glob) [1] $ git show libs/master | grep $(cat c/.joshinfo | grep commit | sed 's/commit: //') diff --git a/tests/filter/workspace_modify_chain.t b/tests/filter/workspace_modify_chain.t index 89d235bc3..71eb7dc99 100644 --- a/tests/filter/workspace_modify_chain.t +++ b/tests/filter/workspace_modify_chain.t @@ -72,7 +72,7 @@ 4 directories, 6 files $ cat ws/file1 - cat: ws/file1: No such file or directory + *: No such file or directory (glob) [1] $ cat sub1/file1 contents1 diff --git a/tests/filter/workspace_shadow_file.t b/tests/filter/workspace_shadow_file.t index e0e97930e..4a26691d2 100644 --- a/tests/filter/workspace_shadow_file.t +++ b/tests/filter/workspace_shadow_file.t @@ -1,5 +1,10 @@ $ export TESTTMP=${PWD} - $ export "PATH=${TESTDIR}/../../target/debug/:${PATH}" + $ if [ -n "${CARGO_TARGET_DIR+x}" ]; then + > export TARGET_DIR=${CARGO_TARGET_DIR} + > else + > export TARGET_DIR=${TESTDIR}/../../target + > fi + $ export "PATH=${TARGET_DIR}/debug:${PATH}" $ cd ${TESTTMP} $ git init -q real_repo 1> /dev/null diff --git a/tests/proxy/clone_subtree_no_master.t b/tests/proxy/clone_subtree_no_master.t index bc5c447c5..dbad79eba 100644 --- a/tests/proxy/clone_subtree_no_master.t +++ b/tests/proxy/clone_subtree_no_master.t @@ -58,7 +58,7 @@ [1] $ cat .git/refs/remotes/origin/HEAD - cat: .git/refs/remotes/origin/HEAD: No such file or directory + *: No such file or directory (glob) [1] $ tree @@ -71,7 +71,7 @@ [128] $ cat file1 - cat: file1: No such file or directory + *: No such file or directory (glob) [1] $ bash ${TESTDIR}/destroy_test_env.sh diff --git a/tests/proxy/workspace_in_workspace_prefix.t b/tests/proxy/workspace_in_workspace_prefix.t index 422665684..94432d5e6 100644 --- a/tests/proxy/workspace_in_workspace_prefix.t +++ b/tests/proxy/workspace_in_workspace_prefix.t @@ -231,5 +231,5 @@ 100644 blob 63c4399dfb47e109da4e7d6c01751b5171b9aa38\tworkspace.josh (esc) $ cat foo/workspace.josh - cat: foo/workspace.josh: No such file or directory + *: No such file or directory (glob) [1] diff --git a/tests/proxy/workspace_modify_chain_prefix_subtree.t b/tests/proxy/workspace_modify_chain_prefix_subtree.t index 9fd11d252..5c56987ca 100644 --- a/tests/proxy/workspace_modify_chain_prefix_subtree.t +++ b/tests/proxy/workspace_modify_chain_prefix_subtree.t @@ -249,7 +249,7 @@ Note that ws/d/ is now present in the ws * initial $ cat sub1/subsub/file1 - cat: sub1/subsub/file1: No such file or directory + *: No such file or directory (glob) [1] $ git checkout -q HEAD~1 1> /dev/null