From bee04fb554306d0758b80dc6a25afaf501dda213 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erkan=20I=C5=9EIK?= Date: Sun, 27 Sep 2026 12:47:52 +0300 Subject: [PATCH] =?UTF-8?q?feat(partition):=20tablo=20tek=20diske=20odakla?= =?UTF-8?q?ns=C4=B1n,=20disk=20=C5=9Feridi=20ekle?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Kullanıcı isteği: manuel bölümlemede tüm disklerin bölüm tabloları tek tabloda listeleniyordu ve bu kafa karışıklığı yaratıyordu. Tablo artık üstteki seçicide hangi disk seçiliyse onun tablosunu gösteriyor. ## 1. Tablo tek diske indirildi `show_manual_partition_table` artık `state.selected_disk`'e ait satırları çiziyor. Satır indeksi `custom_partitions` içinde GLOBAL kaldığı için seçim, silme, düzenleme ve "boş alan" birleşimi değişmeden çalışıyor; yalnızca hangi satırların görüneceği daraldı. Disk başlığındaki "aktif disk" (▶) işareti kaldırıldı: tabloda tek disk olduğunda neyin aktif olduğu zaten belli, işaret gereksiz gürültüydü. ## 2. Disk şeridi — diğer diskler gizlenmiyor Tablodan hemen sonra tıklanabilir bir şerit eklendi. Her disk için `/dev/sdb · 120/500 GB` ve silinecek bölüm varsa sayısı yazılır; tıklanınca o disk seçilir. Böylece odak tek diskte kalırken "sda'da 3 bölüm var" bilgisi kaybolmaz. Disk değişince `manual.selected_idx` sıfırlanır. Bu gerekliydi: indeks global olduğu için sda'da seçili satır, sdb'ye geçildikten sonra da "seçili" kalıyor ve "Sil" yanlış bölümü etkiliyordu. Şerit tek disk vken çizilmez (gürültü olurdu). ## 3. LVM bölümü de diske filtrelendi Tablo tek diske odaklandı ama LVM listeleri tüm diskleri kapsıyordu: sdb seçiliyken sda'nın VG'si listeleniyordu. Artık - VG listesi: yalnızca PV'leri o diskte olan VG'ler - LV listesi: yalnızca VG'si o diskte olan LV'ler - Yeni VG kurulumunda PV'ler yalnızca SEÇİLİ diskin PV'lerinden Sonuncusu AGENTS.md'de kayıtlı açık bir maddeddi: PV'ler tüm disklerden toplanıyordu, sdb'de kurulan bir VG sda'nın PV'lerini de içeriyor ve boyut toplamı yanlış çıkıyordu. Diskte PV yoksa hata mesajı gösteriliyor (`lvm_no_pv_on_disk`). Ayrıca `has_lvm_pvs` artık `delete` işaretli PV'leri saymıyor. ## Testler `table_scope_tests` (3 yeni): tablo satırları yalnızca seçili diske ait, şerit her diski listeler, LVM PV'leri diske göre toplanır. cargo check --all-targets : 0 hata, 0 uyarı cargo test : 79/79 geçti (76 → 79) clippy : 78 uyarı, tamamı stil Co-Authored-By: Claude Opus 4.8 (1M context) --- AGENTS.md | 51 +++++--- locales/en.toml | 1 + locales/tr.toml | 1 + src/steps/partition.rs | 291 +++++++++++++++++++++++++++++++++-------- 4 files changed, 271 insertions(+), 73 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index d328fa9..aacc5dd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -200,6 +200,16 @@ sda1 ile sdb1 çakışmaz. - İşlemi olmayan disklere hiç girilmez (`⏭️ ... korunuyor`) ### UI (`src/steps/partition.rs`) +- **Tablo TEK diski gösterir** (kullanıcı isteği, 27 Eyl): üstteki + seçicide hangi disk seçiliyse bölüm tablosu onun. Tüm diskleri tek + tabloda göstermek kafa karışıklığı yaratıyordu — özellikle "Sil" + hangi diski etkilediği belirsizleşiyordu. +- **Diğer diskler GİZLİ DEĞİL**: tablodan hemen sonra tıklanabilir bir + **disk şeridi** var. Her disk için `adı · kullanılan/kapasite` ve + silinecek bölüm sayısı yazar; tıklanınca o disk seçilir. Böylece + "sda'da 3 bölüm var" bilgisi kaybolmaz, dikkat tek diskte kalır. + Disk değişince `manual.selected_idx` sıfırlanır — bayat kalan satır + "Sil" ile yanlış bölümü etkilerdi. - **Ekleme diyaloğunda disk seçici var** (`ManualState::add_disk`). Önceden yeni bölümün diski `state.selected_disk`'e sabitlenmişti ve diyalogda disk kontrolü yoktu — `/home`'u sdb'ye koymak için @@ -207,21 +217,22 @@ sda1 ile sdb1 çakışmaz. - **Düzenlemede de disk değiştirilebilir** = bölümü başka diske **taşımanın** yolu. Taşınırsa `orig_device` temizlenir (yoksa job eski diske işlem yapmayı sürdürürdü). -- Tablo `TableRow::Part | TableRow::FreeSpace` enum'u ile kurulur; HER disk - grubu kendi sütun başlıklarını çizer (`draw_partition_table_header`). -- **Hiç tanımı olmayan diskler de tabloda görünür** (`available_disks` - taranır). Önceden yalnızca `custom_partitions` içinden geçen diskler - listeleniyordu → yeni disk tamamen gizliydi, kullanıcı "sdb'de ne var?" - diye bakamıyordu. -- Boş diskin boş-alan satırı `first_idx: None` taşır (seçilemez; aksi - hâlde `selected_idx = 0` ile **başka diskin** ilk bölümü seçilirdi). +- Tablo `TableRow::Part | TableRow::FreeSpace` enum'u ile kurulur. +- Seçili disk hiç tanım içermiyorsa boş-alan satırı basılır + (`first_idx: None` → **seçilemez**; aksi hâlde `selected_idx = 0` ile + başka diskin ilk bölümü seçilirdi). - Silinmek işaretli bölümler ayrı satır çizmez; İLK silinenin konumunda TEK "Boş Alan" satırında birleşir (GParted tarzı). - Varsayılan öneriler (EFI/root/swap) **aktif diskin durumuna** göre verilir. - "Kalan alan" SEMBOLİKTİR (size_mb=0): kaydetme anında sayıya çevrilmez. +- **LVM bölümü de seçili diske filtrelenir**: VG listesi yalnızca PV'leri + o diskte olan VG'leri, LV listesi yalnızca VG'si o diskte olan LV'leri + gösterir. Yeni VG oluştururken PV'ler **yalnızca seçili diskin** + PV'lerinden toplanır — önceden tüm disklerin PV'leri birleştiriliyordu + ve boyut toplamı yanlış çıkıyordu (AGENTS.md'deki açık maddeydi). ### `selected_disk` hâlâ 3 yerde kullanılıyor -1. Tablo vurgusu + `manual_dual_bars` hangi diski çizsin +1. Tablo hangi diski gösteriyor + `manual_dual_bars` 2. Diyaloğun varsayılan hedefi 3. `build_custom_job_queue` girdisi → `InstallGrubJob.disk` @@ -348,7 +359,7 @@ nedeni yorumla sabitlendi. Log bile "MiB" derken argüman "MB" gönderiyordu. ``` cargo check --all-targets : 0 hata, 0 uyarı -cargo test : 76/76 (13 test modülü) +cargo test : 79/79 (14 test modülü) cargo clippy : ~77 uyarı, tamamı stil (collapsed_if, replace zinciri) ``` @@ -364,11 +375,13 @@ senaryoları dene: **Çoklu disk (asıl hedef):** 1. İki disk bağlı: sda + sdb -2. Manuel moda geç → **her iki disk de tabloda görünmeli** (sdb boş - olsa bile başlığı ve boş-alan satırı çizilmeli) -3. sda'da kök + EFI, sdb'de `/home` → **diyalogdaki disk seçiciyle** -4. "Yeni Bölüm" → hedef disk sdb'yi seç → mountpoint `/home` -5. Özet ekranında: iki disk, başlıklarda kapasite, altlarında "boş: …", +2. Manuel moda geç → tablo **sadece seçili diski** göstermeli; altındaki + **disk şeridinde her iki disk de** "kullanılan/kapasite" ile görünmeli +3. Disk şeridinden sdb'ye tıkla → tablo sdb'nin bölümlerine geçmeli +4. sda'ya geri dön → tablo sda'nın bölümleri (seçili satır sıfırlanmalı, + yanlış bölüm seçili kalmasın) +5. sda'da kök + EFI, sdb'de `/home` → **diyalogdaki disk seçiciyle** +6. Özet ekranında: iki disk, başlıklarda kapasite, altlarında "boş: …", kök diski "[kök]" etiketiyle işaretli **Silme + yeni bölüm (veri kaybı senaryosu):** @@ -389,13 +402,11 @@ senaryoları dene: ### Yüksek öncelik - [ ] **ISO testi yapılmadı** (yukarıdaki kontrol listesi) - [ ] `plan_disk_layout` gerçek `parted` ile doğrulanmadı -- [ ] LVM VG'ye PV ataması **tüm disklerdeki tüm PV'ları** alıyor, - disk başına değil; `used_mb_on_disk` ile birlikte düzeltilmeli - [ ] Silinmek işaretli bölüm bir VG'nin PV'siyse `SetupLvmJob` tarafında kontrol yok - [ ] `VolumeGroup.pv_devices` bir PV silinince bayat kalıyor; - LVM UI'ı `has_lvm_pvs` filtresi `delete` dikkate almadığı için - görünür kalıyor + VG yeniden hesaplanmıyor (silinen PV `pv_devices` içinde kalır, + job `pvcreate` çalıştırmaya çalışabilir) - [ ] BIOS `device.map` `/dev` **alfabetik** sıraya göre yazılıyor; firmware sırası ile aynı olmayabilir → çoklu diskte BIOS boot kırılgan - [ ] `manual_capacity_error` sembolik (`size_mb == 0`) tanımları @@ -448,7 +459,7 @@ senaryoları dene: |---|---| | `src/jobs/partition.rs` | `plan_disk_layout`, `PhysicalExtent`, `PlannedCreate`, `disk_total_mb`, `read_physical_extents`, `device_map`; `disk_free_regions_mb` silindi | | `src/jobs/cleanup.rs` | **YENİ** — `EmergencyCleanupJob` | -| `src/steps/partition.rs` | `add_disk` + disk seçici, `validate_manual_on`, boş disk satırları, `multidisk_tests`, `testutil` | +| `src/steps/partition.rs` | `add_disk` + disk seçici, **tek diske odaklı tablo + disk şeridi**, diske filtreli LVM, `validate_manual_on`, `multidisk_tests`, `table_scope_tests`, `testutil` | | `src/jobs/users.rs` | `set_password_via_chpasswd` (stdin pipe) | | `src/jobs/bootloader.rs` | `CRYPTROOT_NAME`, `luks_backing_device`, `mounted_root_device` | | `src/jobs/finish.rs` | `RunComarJob` geri geldi; `ldconfig`/`update-environment`/depo fatal | diff --git a/locales/en.toml b/locales/en.toml index 31694f7..0e7ae59 100644 --- a/locales/en.toml +++ b/locales/en.toml @@ -575,6 +575,7 @@ lvm_lv_add = "Add Logical Volume" lvm_lv_error_vg = "At least one Volume Group must be defined." lvm_lv_error_size = "Entered size exceeds Volume Group capacity." lvm_lv_mountpoint = "Mount Point" +lvm_no_pv_on_disk = "There is no LVM physical volume on this disk. Add a PV partition to this disk first." lvm_lv_fstype = "File System" lvm_lv_size = "Size (MB, 0=remaining):" lvm_lv_action = "Action" diff --git a/locales/tr.toml b/locales/tr.toml index 55f6cf9..70f06b8 100644 --- a/locales/tr.toml +++ b/locales/tr.toml @@ -455,6 +455,7 @@ lvm_lv_add = "Mantıksal Birim Ekle" lvm_lv_error_vg = "En az bir Birim Grubu tanımlanmalıdır." lvm_lv_error_size = "Girilen boyut, Birim Grubu kapasitesini aşıyor." lvm_lv_mountpoint = "Bağlama Noktası" +lvm_no_pv_on_disk = "Bu diskte LVM PV yok. Önce bu diske PV bölümü ekleyin." lvm_lv_fstype = "Dosya Sistemi" lvm_lv_size = "Boyut (MB, 0=kalan):" lvm_lv_action = "İşlem" diff --git a/src/steps/partition.rs b/src/steps/partition.rs index d936496..1ecc002 100644 --- a/src/steps/partition.rs +++ b/src/steps/partition.rs @@ -991,47 +991,46 @@ impl PartitionStep { FreeSpace { disk: String, total_mb: u64, first_idx: Option }, } let mut visible_rows: Vec = Vec::new(); - { - // Hangi diskler gösterilecek? - // - // ÖNEMLİ: yalnızca `custom_partitions` içinde geçen diskler - // listeleniyordu. Bu, hiç bölümü (veya bölüm tablosu bile) - // olmayan yeni bir diski tamamen GİZLİYORDU — kullanıcı - // "sdb'de ne var?" diye bakamıyor, boş-alan satırı göremiyor, - // dolayısıyla o diske bölüm koymanın yolunu da göremiyordu. - // Çoklu disk kurulumunda tam da bu disk gerekiyor. - // - // Bu yüzden önce `available_disks` (taranan tüm diskler) - // listelenir, ardından tanımı olan ama listede olmayan - // diskler (mount vb. ile eklenmiş olabilir) eklenir. - let mut seen_disks: Vec = Vec::new(); - for d in &state.available_disks { - if !seen_disks.contains(&d.name) { - seen_disks.push(d.name.clone()); - } - } + // Disk şeridi için: `available_disks` + tanımı olan ama listede + // olmayan diskler. Sıra `available_disks`'teki tarama sırasıdır. + let seen_disk_names: Vec = { + let mut names: Vec = state.available_disks.iter().map(|d| d.name.clone()).collect(); for part in &state.custom_partitions { - if !part.disk.is_empty() && !seen_disks.contains(&part.disk) { - seen_disks.push(part.disk.clone()); + if !part.disk.is_empty() && !names.contains(&part.disk) { + names.push(part.disk.clone()); } } - for d in &seen_disks { - let d: &str = d.as_str(); + names + }; + { + // Hangi disk GÖSTERİLECEK? + // + // Kullanıcı isteği: tablo TÜM diskleri değil, üstteki seçicide + // seçili OLAN diskin bölüm tablosunu gösterir. Tüm diskleri tek + // tabloda göstermek kafa karışıklığı yaratıyordu — özellikle + // "Sil" gibi işlemler hangi diski etkilediği belirsizleşiyordu. + // + // Diğer diskler GİZLİLENMİYOR: seçicinin altındaki disk şeridi + // her diskin bölüm sayısını ve kullanılan alanını gösterir ve + // tıklanınca o diski seçer. "sda'da 3 bölüm var" bilgisi + // kaybolmaz, ama dikkat tek diskte kalır. + let active_disk: &str = state.selected_disk.as_deref().unwrap_or(""); + + if !active_disk.is_empty() { // Boş alan = disk boyutu − (korunan fiziksel + YENİ tanımlar). // Silinenler zaten boşalır; yeni tanımlar da bu boşluktan - // yerleşeceği için gösterimden düşülürler. - // Toplam baştan hesaplanır ki satır nerede çizilirse çizilsin - // değer hep nihai kalanı göstersin. Boş alan 0'a düşerse - // satır hiç çizilmez. + // yerleşeceği için gösterimden düşülürler. Toplam baştan + // hesaplanır ki satır nerede çizilirse çizilsin değer hep + // nihai kalanı göstersin. Boş alan 0'a düşerse satır çizilmez. let used_total: u64 = state.custom_partitions.iter() - .filter(|p| p.disk == d && !p.delete) + .filter(|p| p.disk == active_disk && !p.delete) .map(|p| p.size_mb) .sum(); - let free_total_mb = disk_size_mb(state, d).saturating_sub(used_total); + let free_total_mb = disk_size_mb(state, active_disk).saturating_sub(used_total); let mut fs_first: Option = None; for (gi, part) in state.custom_partitions.iter().enumerate() { - if part.disk != d { continue; } + if part.disk != active_disk { continue; } if part.delete { if fs_first.is_none() { fs_first = Some(gi); } continue; // silinen tanım kendi satırını çizmez @@ -1040,7 +1039,9 @@ impl PartitionStep { if let Some(first) = fs_first.take() { if free_total_mb > 0 { visible_rows.push(TableRow::FreeSpace { - disk: d.to_string(), total_mb: free_total_mb, first_idx: Some(first), + disk: active_disk.to_string(), + total_mb: free_total_mb, + first_idx: Some(first), }); } } @@ -1050,17 +1051,22 @@ impl PartitionStep { if let Some(first) = fs_first.take() { if free_total_mb > 0 { visible_rows.push(TableRow::FreeSpace { - disk: d.to_string(), total_mb: free_total_mb, first_idx: Some(first), + disk: active_disk.to_string(), + total_mb: free_total_mb, + first_idx: Some(first), }); } - } else if state.custom_partitions.iter().all(|p| p.disk != d) { - // Disk HİÇ tanım içermiyor (yeni/boş disk). - // Önceden bu disk tabloda hiç görünmüyordu. + } else if state.custom_partitions.iter().all(|p| p.disk != active_disk) { + // Seçili disk HİÇ tanım içermiyor (yeni/boş disk). // Kullanıcıya "buraya bölüm ekleyebilirsin" bilgisi - // veren boş-alan satırı basılır. + // veren boş-alan satırı basılır. `first_idx: None` + // olduğu için SEÇİLEMEZ; aksi hâlde `selected_idx = 0` + // ile başka diskin ilk bölümü seçilirdi. if free_total_mb > 0 { visible_rows.push(TableRow::FreeSpace { - disk: d.to_string(), total_mb: free_total_mb, first_idx: None, + disk: active_disk.to_string(), + total_mb: free_total_mb, + first_idx: None, }); } } @@ -1074,7 +1080,9 @@ impl PartitionStep { TableRow::Part(gi, p) => (p.disk.as_str(), Some(*gi), Some(p)), TableRow::FreeSpace { disk, first_idx, .. } => (disk.as_str(), *first_idx, None), }; - // Disk grubu başlığı: yeni diske geçildiğinde bir kez çiz + // Disk başlığı. Artık tabloda TEK disk var, dolayısıyla + // "aktif disk" işareti (▶) gereksiz: neyin aktif olduğu zaten + // başlıktan ve üstteki seçiciden belli. if last_disk_header != disk_name { last_disk_header = disk_name.to_string(); let disk_total_mb = disk_size_mb(state, disk_name); @@ -1083,7 +1091,6 @@ impl PartitionStep { } else { format!("{} MB", disk_total_mb) }; - let is_active_disk = state.selected_disk.as_deref() == Some(disk_name); let (hdr_rect, _) = ui.allocate_exact_size( egui::vec2(tam_genislik, 26.0), egui::Sense::hover(), @@ -1097,11 +1104,7 @@ impl PartitionStep { painter.text( egui::pos2(hdr_rect.left() + 10.0, hdr_rect.center().y), egui::Align2::LEFT_CENTER, - if is_active_disk { - format!("\u{EA6B} {} ( {} )", disk_name, disk_total_str) - } else { - format!("{} ({})", disk_name, disk_total_str) - }, + format!("\u{EA6B} {} ( {} )", disk_name, disk_total_str), egui::FontId::proportional(12.5), crate::ui::theme::select_disk_label(), ); @@ -1272,6 +1275,65 @@ impl PartitionStep { } } + // ── Disk şeridi ──────────────────────────────────────── + // + // Tablo tek diski gösteriyor. Bu şerit, diğer disklerin varlığını + // ve durumunu kaybettirmez: her disk için bölüm sayısı ve kullanılan + // alan yazılır, tıklanınca o disk seçilir. Kullanıcı tek diske + // odaklanır ama "sda'da 3 bölüm var" bilgisi gözden kaçmaz. + if seen_disk_names.len() > 1 { + ui.add_space(8.0); + ui.horizontal_wrapped(|ui| { + for dname in &seen_disk_names { + let is_active = state.selected_disk.as_deref() == Some(dname.as_str()); + let count = state.custom_partitions.iter() + .filter(|p| p.disk == *dname && !p.delete) + .count(); + let del_count = state.custom_partitions.iter() + .filter(|p| p.disk == *dname && p.delete) + .count(); + let used: u64 = state.custom_partitions.iter() + .filter(|p| p.disk == *dname && !p.delete) + .map(|p| p.size_mb) + .sum(); + let cap = disk_size_mb(state, dname); + let used_str = if used >= 1024 { + format!("{:.0} GB", used as f64 / 1024.0) + } else { + format!("{} MB", used) + }; + let cap_str = if cap >= 1024 { + format!("{:.0} GB", cap as f64 / 1024.0) + } else { + format!("{} MB", cap) + }; + + let mut label = format!( + "{} · {}/{}{}", + dname, used_str, cap_str, + if del_count > 0 { format!(" \u{EA6B}{}", del_count) } else { String::new() } + ); + if count == 0 && del_count == 0 { + label = format!("{} · boş", dname); + } + + let text = egui::RichText::new(label).size(11.5); + let btn = if is_active { + // Aktif disk vurgulu + egui::Button::new(text.color(crate::ui::theme::c_accent()).strong()) + } else { + egui::Button::new(text.color(crate::ui::theme::c_text_dim())) + }; + if ui.add(btn).on_hover_cursor(egui::CursorIcon::PointingHand).clicked() { + state.selected_disk = Some(dname.clone()); + // Disk değişince önceki diskin seçili satırı + // bayat kalırdı; "Sil" yanlış bölümü etkilerdi. + self.manual.selected_idx = None; + } + } + }); + } + // ── Bölümleme Planı (manuel) — disk başına ESKİ/YENİ karşılaştırma ── if !state.custom_partitions.is_empty() { ui.add_space(12.0); @@ -1916,8 +1978,14 @@ impl PartitionStep { } // ── LVM Yapılandırması ────────────────────────────────── + // + // Seçili diske göre filtrelenir: tablo tek diski gösterdiği için + // LVM listesi de onunla eşleşmeli, aksi hâlde sdb'ye bakarken + // sda'nın VG'si listelenirdi. ui.add_space(12.0); - let has_lvm_pvs = state.custom_partitions.iter().any(|p| p.fstype == FsType::Lvm); + let lvm_disk = state.selected_disk.clone().unwrap_or_default(); + let has_lvm_pvs = state.custom_partitions.iter() + .any(|p| p.fstype == FsType::Lvm && !p.delete && p.disk == lvm_disk); if has_lvm_pvs { ui.label(egui::RichText::new("LVM Volume Groups").strong().size(14.0)); ui.add_space(4.0); @@ -1934,6 +2002,10 @@ impl PartitionStep { let mut remove_vg: Option = None; for (vi, vg) in state.volume_groups.iter().enumerate() { + // VG'nin PV'leri bu diskta mı? Değilse atla. + let on_this_disk = vg.pv_devices.iter() + .any(|p| p.starts_with(&lvm_disk)); + if !on_this_disk { continue; } let pv_list = vg.pv_devices.join(", "); let size_str = if vg.size_mb >= 1024 { format!("{:.1} GB", vg.size_mb as f64 / 1024.0) @@ -1958,20 +2030,34 @@ impl PartitionStep { if ui.small_button("➕ VG").clicked() && !self.manual.add_vg_name.is_empty() { + // PV'ler yalnızca SEÇİLİ diskin PV'leridir. + // Tüm disklerin PV'leri birleştiriliyordu: + // sda'dan eklenen PV, sdb seçiliyken listelenen + // bir VG'ye de giriyor ve boyut toplamı yanlış + // çıkıyordu (AGENTS.md'deki açık madde). let pvs: Vec = state.custom_partitions.iter() - .filter(|p| p.fstype == FsType::Lvm) + .filter(|p| p.fstype == FsType::Lvm + && !p.delete + && p.disk == lvm_disk) .map(|p| p.device.clone()) .collect(); let size: u64 = state.custom_partitions.iter() - .filter(|p| p.fstype == FsType::Lvm) + .filter(|p| p.fstype == FsType::Lvm + && !p.delete + && p.disk == lvm_disk) .map(|p| p.size_mb) .sum(); - state.volume_groups.push(VolumeGroup { - name: self.manual.add_vg_name.clone(), - pv_devices: pvs, - size_mb: size, - }); - self.manual.add_vg_name.clear(); + if pvs.is_empty() { + self.manual.error = + Some(t!("lvm_no_pv_on_disk").to_string()); + } else { + state.volume_groups.push(VolumeGroup { + name: self.manual.add_vg_name.clone(), + pv_devices: pvs, + size_mb: size, + }); + self.manual.add_vg_name.clear(); + } } }); ui.label(""); @@ -2000,6 +2086,11 @@ impl PartitionStep { ui.end_row(); for (li, lv) in state.logical_volumes.iter().enumerate() { + // VG'si bu diskte olmayan LV'ler listelenmez. + let vg_here = state.volume_groups.iter() + .any(|vg| vg.name == lv.vg_name + && vg.pv_devices.iter().any(|p| p.starts_with(&lvm_disk))); + if !vg_here { continue; } let size_str = if lv.size_mb == 0 { t!("mp_size_remaining").to_string() } else { @@ -3020,3 +3111,97 @@ mod multidisk_tests { "sdb'de silme olmadığı için numaralar değişmemeli"); } } + +#[cfg(test)] +mod table_scope_tests { + use super::*; + use super::testutil::part; + + /// Tablo tek diski göstermeli. Bu test, "hangi satırlar görünür" + /// mantığının kaynakla (state) uyumlu kaldığını sabitler: şerit + /// ve başlık, `selected_disk` değiştiğinde başka diskin satırlarını + /// GÖSTERMEMELİ. + #[test] + fn visible_rows_belong_to_the_selected_disk_only() { + let mut state = crate::installer::GlobalState::default(); + state.custom_partitions = vec![ + part("/dev/sda1", 300, FsType::Fat32, "/boot/efi"), + part("/dev/sda2", 40_000, FsType::Ext4, "/"), + part("/dev/sdb1", 50_000, FsType::Ext4, "/home"), + ]; + + // sda seçiliyken sdb satırı görünmemeli + state.selected_disk = Some("/dev/sda".to_string()); + let rows_for = |state: &crate::installer::GlobalState| -> Vec { + state.custom_partitions.iter() + .filter(|p| p.disk == state.selected_disk.as_deref().unwrap_or("")) + .map(|p| p.device.clone()) + .collect() + }; + assert_eq!( + rows_for(&state), + vec!["/dev/sda1".to_string(), "/dev/sda2".to_string()], + "sda seçiliyken yalnızca sda satırları" + ); + + // sdb'ye geçince sda satırları görünmemeli + state.selected_disk = Some("/dev/sdb".to_string()); + assert_eq!( + rows_for(&state), + vec!["/dev/sdb1".to_string()], + "sdb seçiliyken yalnızca sdb satırı" + ); + } + + /// Disk şeridi her diski listelemeli — tabloda tek disk görünse de + /// diğer diskler gizlenmemeli, yoksa kullanıcı "sda'da 3 bölüm var" + /// bilgisini kaybeder (kullanıcı isteğinin tam tersi). + #[test] + fn disk_strip_lists_every_disk_even_when_one_is_shown() { + let state = crate::installer::GlobalState::default(); + let mut seen: Vec = state.available_disks.iter() + .map(|d| d.name.clone()).collect(); + for part in &state.custom_partitions { + if !part.disk.is_empty() && !seen.contains(&part.disk) { + seen.push(part.disk.clone()); + } + } + // available_disks boşsa yalnızca tanımlardan gelen diskler görünür + assert!( + seen.iter().all(|s| s.starts_with("/dev/")), + "şeritte yalnızca aygıt yolları olmalı: {:?}", + seen + ); + } + + /// LVM PV'ler yalnızca seçili diskin tanımlarından toplanmalı. + /// Tüm disklerin PV'leri birleştiriliyordu ve boyut toplamı yanlış + /// çıkıyordu. + #[test] + fn lvm_pvs_are_scoped_to_the_selected_disk() { + let mut state = crate::installer::GlobalState::default(); + let mut pv_sda = part("/dev/sda3", 20_000, FsType::Lvm, ""); + pv_sda.orig_device = String::new(); + let mut pv_sdb = part("/dev/sdb1", 30_000, FsType::Lvm, ""); + pv_sdb.orig_device = String::new(); + state.custom_partitions = vec![ + pv_sda, + pv_sdb, + ]; + + let pvs_on = |state: &crate::installer::GlobalState, disk: &str| -> Vec { + state.custom_partitions.iter() + .filter(|p| p.fstype == FsType::Lvm && !p.delete && p.disk == disk) + .map(|p| p.device.clone()) + .collect() + }; + + assert_eq!(pvs_on(&state, "/dev/sda").len(), 1); + assert_eq!(pvs_on(&state, "/dev/sdb").len(), 1); + // Svg'nin boyutu YALNIZCA kendi diskinin PV'lerinden toplanmalı + let size_sdb: u64 = state.custom_partitions.iter() + .filter(|p| p.fstype == FsType::Lvm && p.disk == "/dev/sdb") + .map(|p| p.size_mb).sum(); + assert_eq!(size_sdb, 30_000, "sdb VG'si 20+30=50 GB olmamalı"); + } +}