|
1 | 1 | //! Git tagging and post-release sync |
2 | 2 |
|
| 3 | +use crate::commands::release::{plan, tags}; |
3 | 4 | use crate::core::error::RailResult; |
| 5 | +use std::env; |
| 6 | +use std::path::Path; |
| 7 | +use std::process::Command; |
4 | 8 |
|
5 | 9 | /// Run release finalize command |
6 | | -pub fn run_release_finalize( |
7 | | - _crate_name: Option<&str>, |
8 | | - _apply: bool, |
9 | | - _no_github: bool, |
10 | | - _no_sync: bool, |
11 | | -) -> RailResult<()> { |
12 | | - println!("🚧 Release finalize command (coming soon)"); |
| 10 | +pub fn run_release_finalize(crate_name: Option<&str>, apply: bool, _no_github: bool, no_sync: bool) -> RailResult<()> { |
| 11 | + // Generate release plan to determine what needs to be finalized |
| 12 | + println!("📊 Analyzing workspace for release plan..."); |
| 13 | + let mut full_plan = plan::generate_release_plan(true)?; |
| 14 | + println!(); |
| 15 | + |
| 16 | + // Filter to specific crate if requested |
| 17 | + if let Some(name) = crate_name { |
| 18 | + full_plan.crates.retain(|c| c.name == name); |
| 19 | + full_plan.publish_order.retain(|n| n == name); |
| 20 | + } |
| 21 | + |
| 22 | + // Only finalize crates with changes |
| 23 | + let plan = full_plan.only_changed(); |
| 24 | + |
| 25 | + if plan.crates.is_empty() { |
| 26 | + if crate_name.is_some() { |
| 27 | + println!("ℹ️ No changes detected for the specified crate"); |
| 28 | + } else { |
| 29 | + println!("ℹ️ No crates need to be finalized"); |
| 30 | + } |
| 31 | + return Ok(()); |
| 32 | + } |
| 33 | + |
| 34 | + // Get current directory (workspace root) |
| 35 | + let workspace_root = env::current_dir().map_err(|e| anyhow::anyhow!("Failed to get current directory: {}", e))?; |
| 36 | + |
| 37 | + // Open git repository |
| 38 | + let repo = gix::open(&workspace_root).map_err(|e| anyhow::anyhow!("Failed to open git repository: {}", e))?; |
| 39 | + |
| 40 | + println!("🏷️ Finalizing {} crate(s)", plan.crates.len()); |
| 41 | + println!(); |
| 42 | + |
| 43 | + // Get current HEAD commit |
| 44 | + let head = repo.head().map_err(|e| anyhow::anyhow!("Failed to get HEAD: {}", e))?; |
| 45 | + let commit = head |
| 46 | + .into_peeled_id() |
| 47 | + .map_err(|e| anyhow::anyhow!("Failed to resolve HEAD to commit: {}", e))?; |
| 48 | + let commit_id = commit.detach(); |
| 49 | + |
| 50 | + // Create tags for each crate |
| 51 | + for crate_plan in &plan.crates { |
| 52 | + let tag_name = tags::format_tag(&crate_plan.name, &crate_plan.next_version); |
| 53 | + |
| 54 | + println!( |
| 55 | + "📌 {} (v{} → v{})", |
| 56 | + crate_plan.name, crate_plan.current_version, crate_plan.next_version |
| 57 | + ); |
| 58 | + |
| 59 | + // Generate tag message from changelog |
| 60 | + let tag_message = format!( |
| 61 | + "Release {} v{}\n\n{}", |
| 62 | + crate_plan.name, crate_plan.next_version, crate_plan.reason |
| 63 | + ); |
| 64 | + |
| 65 | + if !apply { |
| 66 | + println!(" 💡 Would create tag: {}", tag_name); |
| 67 | + println!(" 💡 Would push to origin"); |
| 68 | + if !no_sync { |
| 69 | + println!(" 💡 Would sync to split repo"); |
| 70 | + } |
| 71 | + } else { |
| 72 | + // Create annotated tag |
| 73 | + println!(" 🏷️ Creating tag: {}", tag_name); |
| 74 | + |
| 75 | + // Check if tag already exists |
| 76 | + if repo.find_reference(&format!("refs/tags/{}", tag_name)).is_ok() { |
| 77 | + eprintln!(" ⚠️ Tag {} already exists, skipping", tag_name); |
| 78 | + continue; |
| 79 | + } |
| 80 | + |
| 81 | + // Create the tag using git command (gix doesn't have a simple tag creation API) |
| 82 | + let tag_result = Command::new("git") |
| 83 | + .arg("tag") |
| 84 | + .arg("-a") |
| 85 | + .arg(&tag_name) |
| 86 | + .arg("-m") |
| 87 | + .arg(&tag_message) |
| 88 | + .arg(commit_id.to_string()) |
| 89 | + .current_dir(&workspace_root) |
| 90 | + .output() |
| 91 | + .map_err(|e| anyhow::anyhow!("Failed to run git tag: {}", e))?; |
| 92 | + |
| 93 | + if !tag_result.status.success() { |
| 94 | + let stderr = String::from_utf8_lossy(&tag_result.stderr); |
| 95 | + return Err(anyhow::anyhow!("Failed to create tag {}:\n{}", tag_name, stderr).into()); |
| 96 | + } |
| 97 | + |
| 98 | + println!(" ✅ Created tag: {}", tag_name); |
| 99 | + |
| 100 | + // Push tag to origin |
| 101 | + println!(" 📤 Pushing tag to origin..."); |
| 102 | + let push_result = Command::new("git") |
| 103 | + .arg("push") |
| 104 | + .arg("origin") |
| 105 | + .arg(format!("refs/tags/{}", tag_name)) |
| 106 | + .current_dir(&workspace_root) |
| 107 | + .output() |
| 108 | + .map_err(|e| anyhow::anyhow!("Failed to run git push: {}", e))?; |
| 109 | + |
| 110 | + if !push_result.status.success() { |
| 111 | + let stderr = String::from_utf8_lossy(&push_result.stderr); |
| 112 | + eprintln!(" ⚠️ Warning: Failed to push tag to origin:\n{}", stderr); |
| 113 | + } else { |
| 114 | + println!(" ✅ Pushed to origin"); |
| 115 | + } |
| 116 | + |
| 117 | + // Sync to split repo (if configured and not disabled) |
| 118 | + if !no_sync { |
| 119 | + println!(" 🔄 Syncing to split repo..."); |
| 120 | + let sync_result = sync_to_split_repo(&workspace_root, &crate_plan.name)?; |
| 121 | + if sync_result { |
| 122 | + println!(" ✅ Synced to split repo"); |
| 123 | + } else { |
| 124 | + println!(" ℹ️ No split repo configured"); |
| 125 | + } |
| 126 | + } |
| 127 | + } |
| 128 | + |
| 129 | + println!(); |
| 130 | + } |
| 131 | + |
| 132 | + // Summary |
| 133 | + if !apply { |
| 134 | + println!("💡 This was a dry-run. Use --apply to create and push tags."); |
| 135 | + } else { |
| 136 | + println!("🎉 Successfully finalized {} crate(s)!", plan.crates.len()); |
| 137 | + println!(); |
| 138 | + println!("Tags created:"); |
| 139 | + for crate_plan in &plan.crates { |
| 140 | + let tag_name = tags::format_tag(&crate_plan.name, &crate_plan.next_version); |
| 141 | + println!(" - {}", tag_name); |
| 142 | + } |
| 143 | + } |
| 144 | + |
13 | 145 | Ok(()) |
14 | 146 | } |
| 147 | + |
| 148 | +/// Sync a crate to its split repository |
| 149 | +fn sync_to_split_repo(workspace_root: &Path, crate_name: &str) -> RailResult<bool> { |
| 150 | + // Check if rail.toml exists and has split repo config for this crate |
| 151 | + let rail_config = workspace_root.join(".rail").join("rail.toml"); |
| 152 | + if !rail_config.exists() { |
| 153 | + return Ok(false); |
| 154 | + } |
| 155 | + |
| 156 | + // Try to sync using cargo rail sync command |
| 157 | + let sync_result = Command::new("cargo") |
| 158 | + .arg("rail") |
| 159 | + .arg("sync") |
| 160 | + .arg(crate_name) |
| 161 | + .arg("--apply") |
| 162 | + .current_dir(workspace_root) |
| 163 | + .output() |
| 164 | + .map_err(|e| anyhow::anyhow!("Failed to run cargo rail sync: {}", e))?; |
| 165 | + |
| 166 | + if !sync_result.status.success() { |
| 167 | + let stderr = String::from_utf8_lossy(&sync_result.stderr); |
| 168 | + eprintln!(" ⚠️ Warning: Failed to sync to split repo:\n{}", stderr); |
| 169 | + return Ok(false); |
| 170 | + } |
| 171 | + |
| 172 | + Ok(true) |
| 173 | +} |
0 commit comments