From 3f6ee524d7fd99146f98964aefc21be72de374c4 Mon Sep 17 00:00:00 2001
From: Sebastian Thiel <sebastian.thiel@icloud.com>
Date: Sun, 22 Dec 2024 10:13:17 +0100
Subject: [PATCH 1/2] feat: add `io_err` module with classifiers.

This makes it easier to portably identify IO errors, especially while
the MSRV is not Rustc v1.83 which stabilizes the error kind variants.
---
 gix-fs/src/lib.rs | 12 ++++++++++++
 1 file changed, 12 insertions(+)

diff --git a/gix-fs/src/lib.rs b/gix-fs/src/lib.rs
index 187097573cb..9f77d8e8614 100644
--- a/gix-fs/src/lib.rs
+++ b/gix-fs/src/lib.rs
@@ -83,6 +83,18 @@ pub fn is_executable(metadata: &std::fs::Metadata) -> bool {
     (metadata.mode() & 0o100) != 0
 }
 
+/// Classifiers for IO-errors.
+pub mod io_err {
+    use std::io::ErrorKind;
+
+    /// Return `true` if `err` indicates that the entry doesn't exist on disk. `raw` is used as well
+    /// for additional checks while the variants are outside the MSRV.
+    pub fn is_not_found(err: ErrorKind, raw_err: Option<i32>) -> bool {
+        // TODO: use variant once MSRV is 1.83
+        err == ErrorKind::NotFound || raw_err == Some(20)
+    }
+}
+
 #[cfg(not(unix))]
 /// Returns whether a a file has the executable permission set.
 pub fn is_executable(_metadata: &std::fs::Metadata) -> bool {

From f3b76d0f52da8b4ea0f4d03490fc0c18fd7c3a96 Mon Sep 17 00:00:00 2001
From: Sebastian Thiel <sebastian.thiel@icloud.com>
Date: Sun, 22 Dec 2024 09:36:14 +0100
Subject: [PATCH 2/2] fix: Replacing a directory with non-directory will not
 cause an error anymore (#1735).

---
 gix-status/src/index_as_worktree/function.rs  |  12 ++++++---
 .../generated-archives/status_many.tar        | Bin 144384 -> 205312 bytes
 gix-status/tests/fixtures/status_many.sh      |  13 +++++++++
 gix-status/tests/status/index_as_worktree.rs  |  25 ++++++++++++++++++
 4 files changed, 46 insertions(+), 4 deletions(-)

diff --git a/gix-status/src/index_as_worktree/function.rs b/gix-status/src/index_as_worktree/function.rs
index 5dbf16715ac..8fb300abdb5 100644
--- a/gix-status/src/index_as_worktree/function.rs
+++ b/gix-status/src/index_as_worktree/function.rs
@@ -356,8 +356,10 @@ impl<'index> State<'_, 'index> {
     {
         let worktree_path = match self.path_stack.verified_path(gix_path::from_bstr(rela_path).as_ref()) {
             Ok(path) => path,
-            Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(Some(Change::Removed.into())),
-            Err(err) => return Err(Error::Io(err)),
+            Err(err) if gix_fs::io_err::is_not_found(err.kind(), err.raw_os_error()) => {
+                return Ok(Some(Change::Removed.into()))
+            }
+            Err(err) => return Err(err.into()),
         };
         self.symlink_metadata_calls.fetch_add(1, Ordering::Relaxed);
         let metadata = match gix_index::fs::Metadata::from_path_no_follow(worktree_path) {
@@ -379,7 +381,9 @@ impl<'index> State<'_, 'index> {
                 }
             }
             Ok(metadata) => metadata,
-            Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(Some(Change::Removed.into())),
+            Err(err) if gix_fs::io_err::is_not_found(err.kind(), err.raw_os_error()) => {
+                return Ok(Some(Change::Removed.into()))
+            }
             Err(err) => {
                 return Err(err.into());
             }
@@ -539,7 +543,7 @@ where
             // conversion to bstr can never fail because symlinks are only used
             // on unix (by git) so no reason to use the try version here
             let symlink_path =
-                gix_path::to_unix_separators_on_windows(gix_path::into_bstr(std::fs::read_link(self.path)?));
+                gix_path::to_unix_separators_on_windows(gix_path::into_bstr(std::fs::read_link(self.path).unwrap()));
             self.buf.extend_from_slice(&symlink_path);
             self.worktree_bytes.fetch_add(self.buf.len() as u64, Ordering::Relaxed);
             Stream {
diff --git a/gix-status/tests/fixtures/generated-archives/status_many.tar b/gix-status/tests/fixtures/generated-archives/status_many.tar
index 8e656859d8e5d1a1f98718e26d0255db17efa0cf..2f9698d313302c48bd939b74be637ac5ec7dccdf 100644
GIT binary patch
delta 5860
zcmdT|dsGzH8Q*W%U0{{h0xP(R8wxBDmYvtmu#KrPK2i|cn)bw`i81?R@dW}_O-Z7j
z#MY!yVN&+Av57fAFeeE?#yVAFPlCp9jIl&58bhfu28|lqLyQunZtpBRJAw@BY0v37
z{b%pqx!-T@_xRoK`|hl}5VP*!#I{sSNKJ$|tR>R`>XyvOm|Dop01eh+?gF)7?uI`2
zURiIFu1UM0EHr{jl?aY)CseBSO)3?9t5T^_IK0CWlxzMO7L-F{NbavA<kp4c7JvA8
zP!5eDxlcyOb#-J1-?#p=KLzF7tuKe<jv=|P!~3QkC{J0Kuy@wmXWWC%n+1d2WiQr#
zz02}6=a{s#p$GDWywd=fkbf3#K#REtw1Sz0XiO+@LLg4CtOK>4%Wk6T+ti$|$tw8w
z!q#ZPTnIWfm+c*X>_v}jp&?4J&=92-@Erg#Ty~DQDEMZ;2bva32^d-|_k-GRX@iH;
zY?O+?X@<Z_oTP9Sj*}!yq7muj=HcasHT>f3uSM0Fv<-zoaM`uPZ>on6QU%LA7}T_w
zO``8RP#A>#PPh@>Vm^q1W6lR{i#b<ZItMXWi}f;^TQLm%42k~+06mKlr+vswYdrRi
zM#vmQu@$WSFre|9vtUs=Nf4uyBUqZHhLsZ_8y<!H*z&R!6;&RWquOJ-*Hlxr%A<>|
zMDvQ}l@-;VRV&I}6>e#TpY`M#*qwsqU_wqBHUx-A7$#(9VZG2`fC8alGF**n$uwbz
zTp8ku9=?g<Q!&7JLyQJ%H11XN#`pUFe@WJU(1h`(BR8PYcu30|r$l3Z#4V=IG!gNG
z_m%94^COnA(tOGOBtE<8S~PFW)T#Mw?@*K<(JHj;_t9YG{TVMt`4P)#2~)p6n$OM|
z()bb2!jdJTB=dZ#AMq~y_WV8Qb#2m|^ZkD67-Yn4G&=?v+iUMK_!0lYFT;6FqY)`-
zd`p8YY`+oRXlzwC8V%Y;qaHmy8iV-}1#Oa+-TO3t#K!RavN#`-*ZaizfIL6D5B!L(
zVflV>y-!~666f*q`ar9OH=K(@oA%2>J-mi9#+Q#pYa2z9{~fJgf^<IpCdwX@F&K(%
zHlTlocnE-yIuYd*-k`o1wL1sXV}d>(y8;OmYiCJ{Vknyhosl&{(lPi4_-~rAU!~Eb
zGXg=gEIq6p2M}nOj>V@?_5zzJcUEQPGLPwj@)`?aqwf}y)X)pbRi4Ua4wuK`F0Znz
zF0b)fyyYB`LDiWuf{81}s-*~qQQ3wwOfo?7ZSu=J{<|_@_HeW~Uksrp*@%EH8E`6N
z5;;0^Fa#S)YoZb94!7InEl1jyuP{}wa*En|Ts0L{Yeg&0L+Th-h+1{ao9K*_2^_yI
zL)i#1jSd_4z7DcAVo8X9qWU+(W6IORrQr{AMwF0HNs4#eXbj3ZwnIp6?g+V1NlJLM
zVZSIRjv=|VBjma|(%E=qlNH%!UE!&&@widxQw87s^VY%O_()G%2s?8WHdxLQGW(AQ
z_dsJvZZ(p-wVYAj1uC0~(0!4&v(2!1`J7jsRj)6K-#G6>XU9Ega}|Zwj%*G*_2#r^
z4{Y7o{P%9_*1s`(Y73B3D#C>8ew6;iJF$-}p1x~nOZ}vk)jMB*_3WjS`>S@Z%+vpo
zK0ajr>B)P({N0Im`!GH0Oo>WiIW31J2G_86K(Dhhq8-tTj!3T_8PG+jZDd41uMFt4
zYMBvdD3@hP+QX6_hsW(?TrLOWU>UEIrb)(L$hv9V?shP4mSjaXo>F8ZDQ)pvAKZ~n
zP6@#IN^lKhBP6Dl1|}-`cAbn_f;<(12(TUkit*ShW10vx5jqe8?1O+-X`ezFlZk66
zHoTD?0+<r0Rt9<Hjd&<ihR_luSi!V7E2+oBG-cEd3B~#9aScj$Qo4R)9Fu0qpd3O?
zhx39tpjI-dM1uHzsA_6Sk_qor3V{?Q*!>cW^W}@qV<h!vkK?>H8Kh8r37bTaTRK4{
z9qOgm`F-_Bb<|acwBX?>WzFYFD9(r98bLzYM<yAfLv9W0I6*ml9Wq2r5U7caRDU{U
z1WFK8<nw-50{DF-;2NBWT;q;N5YDHYl5WFqtGp;_g!%#=gl$lNIQMSTou&?ML7Uz9
zeUoCL<7PeiXQfacjHDQ9%R;>T-zt_bD;7-jk;76TBhnDR!dj;*lO<P%=!30CsGKJ&
zL(a(%1FoSNJR-j?%Lv3TJTh6XAiz|yQRjT8<bxuhzAb=dkJ+wR3(CrCtQF2BsI#fI
zvTR+_%pe@?hYq%U0E)+Gz(!-OcUG_JlG1`(w4!EybPI<1O~mk^F9n!@Veri6_jMh3
z;+%WW=Upo(ZZBeEe?5@*sr!ZJx{FI@1$s9;m~iyVxTu7jzZ|FO`(D`h(A@j-Nn`iM
z4L?~~aP8TQ<W+O$n;!Ty*}LnlD{mZrd;O-IzQ-S%R@~;!YB&>h>4c}CbCbS(XTuMh
zn-`teE#CHncRQ)l>kBroedTE4)CHBZmY%wlYg;n)WJT%pq0{AWKGS+#%PwqwaQn1*
z>p$0Sc`m`TqyDqU7GI1z`d)Dl|61EaibWX1!i_3&EE}1TY6J)c@(QjY<y@`7lnFK@
z(*?C3DCf!HD~cFCm&;n{@p@Ue!%27xX%}V33mJ#a?R7Jx*N(g06yvnBE|Meg^kDdO
zD17v&4w|GGJd|n$_zETfH{s%ptd9<=Vp5);&!r7zC3jcPJG6B9cgJTIz1z0ti}vrf
zJRYUm6f^yd*uK4_=)apXuP>m}7Oq@@aVpn<?2WUC%#pKs%!nYN?+UW}#=d^&<RD!Y
zgA~4=O&BLgeX?326(Xy3LAL!EDj8BgCi+*I3h^Us!6V;{5@lNkQ7H>I6iMTlGANsJ
zXt<$}J(d}fa5Q2Py79$H=>}cBWO(FsLwl@*oxvSm+-|emyrjcPdtHTIJ4vuC!BS4b
z!PvbX(&LoUjecqxGrD+^6e+bYqCcY{k8$8Mj#I>f&iR&tqEg2i=K3d>ODDZ!|K;kX
zt$ADauDnq6<A2Uk%`VgHMaq5*cal~;R5-1YQMbk-vY8&rN;;i{6SWG39>z`6b}xgN
z=3s1Y+*8QVjEiMR7fFT96gfhN!}k@WK<ZP0r3*h2ro7U%`|3^GzhYN^{EO}j>D=@a
zMd|}*tOn7HO^PS4UyiXz*T{FEt1#Pd)xmHP>P5%G7ETD?jzicic^_04ZvUufBc-eP
zfBI2RsgFsKj*8!>LoF%AO62E3`NM;ZnTT#xa>sxWDPu9;pRb3)I+Gztc6G)id{p8j
aY~i}cgC-gTy&A57U^)h8Lcd&_y7=FOE^dAR

delta 5910
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
K;QhUf+x`u%v3cwO

diff --git a/gix-status/tests/fixtures/status_many.sh b/gix-status/tests/fixtures/status_many.sh
index 9a11f3f4f8d..e7a67475920 100755
--- a/gix-status/tests/fixtures/status_many.sh
+++ b/gix-status/tests/fixtures/status_many.sh
@@ -39,3 +39,16 @@ cp -R changed-and-untracked changed-and-untracked-and-renamed
   echo change >> content-with-rewrite
 
 )
+
+cp -R changed-and-untracked replace-dir-with-file
+(cd replace-dir-with-file
+  git checkout executable
+  rm untracked dir/untracked
+
+  mkdir dir/sub
+  touch dir/sub/nested
+  git add dir && git commit -m "add file in sub-directory"
+
+  rm -Rf dir/
+  touch dir
+)
diff --git a/gix-status/tests/status/index_as_worktree.rs b/gix-status/tests/status/index_as_worktree.rs
index 27a3a272c0b..dc4b357902e 100644
--- a/gix-status/tests/status/index_as_worktree.rs
+++ b/gix-status/tests/status/index_as_worktree.rs
@@ -243,6 +243,31 @@ fn removed() {
     );
 }
 
+#[test]
+fn replace_dir_with_file() {
+    let out = fixture_filtered_detailed(
+        "status_many",
+        "replace-dir-with-file",
+        &[],
+        &[
+            (BStr::new(b"dir/content"), 0, status_removed()),
+            (BStr::new(b"dir/content2"), 1, status_removed()),
+            (BStr::new(b"dir/sub/nested"), 2, status_removed()),
+        ],
+        |_| {},
+        false,
+    );
+    assert_eq!(
+        out,
+        Outcome {
+            entries_to_process: 5,
+            entries_processed: 5,
+            symlink_metadata_calls: if cfg!(windows) { 5 } else { 4 },
+            ..Default::default()
+        }
+    );
+}
+
 #[test]
 fn subomdule_nochange() {
     assert_eq!(