@@ -1121,6 +1121,179 @@ impl Filesystem for BranchFs {
11211121 self . unlink ( _req, parent, name, reply) ;
11221122 }
11231123
1124+ fn rename (
1125+ & mut self ,
1126+ _req : & Request ,
1127+ parent : u64 ,
1128+ name : & OsStr ,
1129+ newparent : u64 ,
1130+ newname : & OsStr ,
1131+ flags : u32 ,
1132+ reply : ReplyEmpty ,
1133+ ) {
1134+ if flags & libc:: RENAME_EXCHANGE != 0 {
1135+ reply. error ( libc:: EINVAL ) ;
1136+ return ;
1137+ }
1138+
1139+ let parent_path = match self . inodes . get_path ( parent) {
1140+ Some ( p) => p,
1141+ None => {
1142+ reply. error ( libc:: ENOENT ) ;
1143+ return ;
1144+ }
1145+ } ;
1146+ let newparent_path = match self . inodes . get_path ( newparent) {
1147+ Some ( p) => p,
1148+ None => {
1149+ reply. error ( libc:: ENOENT ) ;
1150+ return ;
1151+ }
1152+ } ;
1153+
1154+ let name_str = name. to_string_lossy ( ) ;
1155+ let newname_str = newname. to_string_lossy ( ) ;
1156+
1157+ // Normalize both parents to (branch, parent_rel, inode_prefix, is_root_path).
1158+ let resolve_parent = |path : & str | -> Option < ( String , String , String , bool ) > {
1159+ match classify_path ( path) {
1160+ PathContext :: BranchDir ( b) => {
1161+ Some ( ( b. clone ( ) , "/" . into ( ) , format ! ( "/@{}" , b) , false ) )
1162+ }
1163+ PathContext :: BranchPath ( b, rel) => {
1164+ Some ( ( b. clone ( ) , rel, format ! ( "/@{}" , b) , false ) )
1165+ }
1166+ PathContext :: RootPath ( rp) => Some ( ( String :: new ( ) , rp, String :: new ( ) , true ) ) ,
1167+ _ => None ,
1168+ }
1169+ } ;
1170+
1171+ let ( src_branch, src_parent_rel, src_prefix, src_is_root) =
1172+ match resolve_parent ( & parent_path) {
1173+ Some ( t) => t,
1174+ None => {
1175+ reply. error ( libc:: EPERM ) ;
1176+ return ;
1177+ }
1178+ } ;
1179+ let ( dst_branch, dst_parent_rel, dst_prefix, dst_is_root) =
1180+ match resolve_parent ( & newparent_path) {
1181+ Some ( t) => t,
1182+ None => {
1183+ reply. error ( libc:: EPERM ) ;
1184+ return ;
1185+ }
1186+ } ;
1187+
1188+ // Both must be the same kind (both root or both same branch)
1189+ if src_is_root != dst_is_root || ( !src_is_root && src_branch != dst_branch) {
1190+ reply. error ( libc:: EXDEV ) ;
1191+ return ;
1192+ }
1193+
1194+ let branch = if src_is_root {
1195+ self . get_branch_name ( )
1196+ } else {
1197+ if !self . manager . is_branch_valid ( & src_branch) {
1198+ reply. error ( libc:: ENOENT ) ;
1199+ return ;
1200+ }
1201+ src_branch
1202+ } ;
1203+
1204+ let join_rel = |parent_rel : & str , child : & str | -> String {
1205+ if parent_rel == "/" {
1206+ format ! ( "/{}" , child)
1207+ } else {
1208+ format ! ( "{}/{}" , parent_rel, child)
1209+ }
1210+ } ;
1211+ let src_rel = join_rel ( & src_parent_rel, & name_str) ;
1212+ let dst_rel = join_rel ( & dst_parent_rel, & newname_str) ;
1213+
1214+ // Check source exists
1215+ if self . resolve_for_branch ( & branch, & src_rel) . is_none ( ) {
1216+ reply. error ( libc:: ENOENT ) ;
1217+ return ;
1218+ }
1219+
1220+ // RENAME_NOREPLACE
1221+ if flags & libc:: RENAME_NOREPLACE != 0
1222+ && self . resolve_for_branch ( & branch, & dst_rel) . is_some ( )
1223+ {
1224+ reply. error ( libc:: EEXIST ) ;
1225+ return ;
1226+ }
1227+
1228+ // COW source into delta
1229+ let src_delta = match self . ensure_cow_for_branch ( & branch, & src_rel) {
1230+ Ok ( p) => p,
1231+ Err ( _) => {
1232+ reply. error ( libc:: EIO ) ;
1233+ return ;
1234+ }
1235+ } ;
1236+
1237+ let dst_delta = self . get_delta_path_for_branch ( & branch, & dst_rel) ;
1238+ if storage:: ensure_parent_dirs ( & dst_delta) . is_err ( ) {
1239+ reply. error ( libc:: EIO ) ;
1240+ return ;
1241+ }
1242+
1243+ // If destination already exists, remove its delta so rename can overwrite
1244+ let dst_existed = self . resolve_for_branch ( & branch, & dst_rel) . is_some ( ) ;
1245+ if dst_existed {
1246+ let _ = self . manager . with_branch ( & branch, |b| {
1247+ let d = b. delta_path ( & dst_rel) ;
1248+ if d. exists ( ) {
1249+ if d. is_dir ( ) {
1250+ let _ = std:: fs:: remove_dir_all ( & d) ;
1251+ } else {
1252+ let _ = std:: fs:: remove_file ( & d) ;
1253+ }
1254+ }
1255+ Ok ( ( ) )
1256+ } ) ;
1257+ }
1258+
1259+ // Move within the delta layer (same filesystem, always succeeds)
1260+ if std:: fs:: rename ( & src_delta, & dst_delta) . is_err ( ) {
1261+ reply. error ( libc:: EIO ) ;
1262+ return ;
1263+ }
1264+
1265+ // Update tombstones: mark src deleted, revive dst, tombstone old dst
1266+ let result = self . manager . with_branch ( & branch, |b| {
1267+ b. add_tombstone ( & src_rel) ?;
1268+ if dst_existed {
1269+ b. add_tombstone ( & dst_rel) ?;
1270+ }
1271+ b. remove_tombstone ( & dst_rel) ;
1272+ Ok ( ( ) )
1273+ } ) ;
1274+ if result. is_err ( ) {
1275+ reply. error ( libc:: EIO ) ;
1276+ return ;
1277+ }
1278+
1279+ if src_is_root && self . is_stale ( ) {
1280+ reply. error ( libc:: ESTALE ) ;
1281+ return ;
1282+ }
1283+
1284+ // Update inode cache
1285+ let src_inode_path = format ! ( "{}{}" , src_prefix, src_rel) ;
1286+ let dst_inode_path = format ! ( "{}{}" , dst_prefix, dst_rel) ;
1287+ let is_dir = dst_delta. is_dir ( ) ;
1288+ self . inodes . remove ( & src_inode_path) ;
1289+ self . inodes . remove ( & dst_inode_path) ;
1290+ let new_ino = self . inodes . get_or_create ( & dst_inode_path, is_dir) ;
1291+ self . open_cache . invalidate_ino ( new_ino) ;
1292+ self . write_cache . invalidate_ino ( new_ino) ;
1293+
1294+ reply. ok ( ) ;
1295+ }
1296+
11241297 fn open ( & mut self , _req : & Request , ino : u64 , flags : i32 , reply : ReplyOpen ) {
11251298 // Control file is always openable (no epoch check)
11261299 if ino == CTL_INO {
0 commit comments