#!/usr/bin/env nu # Safe Merge Upstream Script # Safely merges upstream changes while preserving local nu_* dependency versions use lib/cargo_toml_diff.nu * # Version check - mandatory for all plugin operations def version_check [] { try { nu scripts/check_version.nu --quiet | ignore } catch { print "โŒ Nushell version mismatch detected!" print "๐Ÿ”ง Run: nu scripts/check_version.nu --fix" exit 1 } } # Load plugin registry def load_registry [] { let registry_path = "etc/plugin_registry.toml" if not ($registry_path | path exists) { error make {msg: "Plugin registry not found. Please run from repository root directory."} } open $registry_path } # Load exclusions configuration def load_exclusions [] { let exclude_path = "etc/upstream_exclude.toml" if ($exclude_path | path exists) { let config = open $exclude_path { all: ($config.exclude.plugins? | default []), check: ($config.exclude.check.plugins? | default []), merge: ($config.exclude.merge.plugins? | default []), patterns: ($config.exclude.patterns.plugins? | default []) } } else { {all: [], check: [], merge: [], patterns: []} } } # Check if plugin should be excluded def is_plugin_excluded [plugin_name: string, operation: string, exclusions: record] { # Check direct exclusions if $plugin_name in $exclusions.all { return true } # Check operation-specific exclusions let op_exclusions = match $operation { "check" => $exclusions.check, "merge" => $exclusions.merge, _ => [] } if $plugin_name in $op_exclusions { return true } # Check pattern exclusions for pattern in $exclusions.patterns { if ($plugin_name | str contains ($pattern | str replace "*" "")) { return true } } false } # Save plugin registry def save_registry [registry: record] { $registry | to toml | save -f "etc/plugin_registry.toml" } # Get current date in ISO format def current_date [] { date now | format date "%Y-%m-%d %H:%M:%S" } # Create backup branch def create_backup [plugin_path: string] { cd $plugin_path let timestamp = date now | format date "%Y%m%d_%H%M%S" let backup_branch = $"backup_before_merge_($timestamp)" try { git checkout -b $backup_branch git checkout - # Return to original branch print $" ๐Ÿ“‹ Created backup branch: ($backup_branch)" $backup_branch } catch { error make {msg: "Failed to create backup branch"} } } # Create temporary merge branch def create_merge_branch [plugin_path: string] { cd $plugin_path let timestamp = date now | format date "%Y%m%d_%H%M%S" let merge_branch = $"temp_merge_($timestamp)" try { git checkout -b $merge_branch print $" ๐Ÿ”€ Created merge branch: ($merge_branch)" $merge_branch } catch { error make {msg: "Failed to create merge branch"} } } # Extract and preserve local nu dependencies def preserve_local_nu_deps [plugin_path: string] { cd $plugin_path let cargo_toml = "Cargo.toml" if not ($cargo_toml | path exists) { error make {msg: "Cargo.toml not found"} } # Parse current local dependencies let local_cargo = parse_cargo_toml $cargo_toml let local_nu_deps = extract_nu_dependencies $local_cargo print $" ๐Ÿ’พ Preserved ($local_nu_deps | columns | length) local nu_* dependencies" $local_nu_deps } # Restore local nu dependencies after merge def restore_nu_deps [plugin_path: string, local_nu_deps: record] { cd $plugin_path let cargo_toml = "Cargo.toml" let content = open $cargo_toml # Update each nu dependency with local version mut updated_content = $content for dep in ($local_nu_deps | transpose key value) { let dep_name = $dep.key let dep_value = $dep.value # Update the dependency in the TOML structure $updated_content = ($updated_content | upsert dependencies.($dep_name) $dep_value) } # Save updated Cargo.toml $updated_content | to toml | save -f $cargo_toml print $" ๐Ÿ”ง Restored ($local_nu_deps | columns | length) local nu_* dependency versions" } # Perform the merge def perform_merge [plugin_path: string, upstream_branch: string] { cd $plugin_path try { print $" ๐Ÿ”€ Merging upstream/($upstream_branch)..." git merge $"upstream/($upstream_branch)" --no-edit # Check if merge was successful let merge_status = git status --porcelain | lines if ($merge_status | length) > 0 { # Check if there are conflict markers let conflicts = $merge_status | where ($it | str starts-with "UU") if ($conflicts | length) > 0 { print $" โŒ Merge conflicts detected" return false } } print $" โœ… Merge completed successfully" true } catch { print $" โŒ Merge failed" false } } # Test if the project compiles after merge def test_compilation [plugin_path: string] { cd $plugin_path print $" ๐Ÿงช Testing compilation..." try { let result = cargo check --quiet print $" โœ… Compilation successful" true } catch { print $" โŒ Compilation failed" false } } # Run tests if they exist def run_tests [plugin_path: string] { cd $plugin_path # Check if there are tests let test_files = glob "**/*test*.rs" | length if $test_files == 0 { print $" โญ๏ธ No tests found, skipping" return true } print $" ๐Ÿงช Running tests..." try { cargo test --quiet print $" โœ… All tests passed" true } catch { print $" โŒ Tests failed" false } } # Rollback to original state def rollback_changes [plugin_path: string, original_branch: string, merge_branch: string] { cd $plugin_path print $" ๐Ÿ”„ Rolling back changes..." try { git checkout $original_branch git branch -D $merge_branch print $" โœ… Rollback completed" } catch { print $" โš ๏ธ Rollback may have failed - manual intervention needed" } } # Apply successful merge to main branch def apply_merge [plugin_path: string, original_branch: string, merge_branch: string] { cd $plugin_path print $" โœ… Applying successful merge..." try { git checkout $original_branch git merge $merge_branch --no-edit git branch -D $merge_branch print $" โœ… Merge applied to ($original_branch)" true } catch { print $" โŒ Failed to apply merge" false } } # Process a single plugin merge def process_plugin_merge [plugin_name: string, plugin_config: record, force: bool, exclusions: record] { print $"\n๐Ÿ”€ Processing merge for ($plugin_name)..." # Check if plugin is excluded if (is_plugin_excluded $plugin_name "merge" $exclusions) { print $" ๐Ÿšซ Skipping ($plugin_name) - excluded from merging" return $plugin_config } let plugin_path = $plugin_config.local_path # Validation checks if ($plugin_config.upstream_url | is-empty) { print $" โญ๏ธ Skipping ($plugin_name) - no upstream URL" return $plugin_config } if not ($plugin_path | path exists) { print $" โŒ Plugin directory not found: ($plugin_path)" return $plugin_config } if ($plugin_config.status == "ok") and not $force { print $" โœ… Plugin already marked as OK, use --force to merge anyway" return $plugin_config } if ($plugin_config.status == "local_only") { print $" ๐Ÿ  Local-only plugin, no upstream to merge" return $plugin_config } cd $plugin_path # Get current branch let original_branch = git branch --show-current | str trim # Check for uncommitted changes let uncommitted = git status --porcelain | lines if ($uncommitted | length) > 0 { print $" โš ๏ธ Uncommitted changes detected. Please commit or stash them first." return $plugin_config } # Fetch latest upstream try { git fetch upstream $plugin_config.upstream_branch } catch { print $" โŒ Failed to fetch upstream" return $plugin_config } # Check if there are actually changes to merge let changes = git diff $"HEAD..upstream/($plugin_config.upstream_branch)" --name-only | lines if ($changes | length) == 0 { print $" โœ… No changes to merge" return ($plugin_config | upsert status "ok") } print $" ๐Ÿ“‹ Found ($changes | length) changed files: ($changes | str join ', ')" # Create backup branch let backup_branch = create_backup $plugin_path # Preserve local nu dependencies let local_nu_deps = preserve_local_nu_deps $plugin_path # Create merge branch let merge_branch = create_merge_branch $plugin_path # Perform merge let merge_success = perform_merge $plugin_path $plugin_config.upstream_branch if not $merge_success { print $" โŒ Merge failed, cleaning up..." rollback_changes $plugin_path $original_branch $merge_branch return ($plugin_config | upsert status "conflict") } # Restore local nu dependencies restore_nu_deps $plugin_path $local_nu_deps # Test compilation let compile_success = test_compilation $plugin_path if not $compile_success { print $" โŒ Compilation failed after merge, rolling back..." rollback_changes $plugin_path $original_branch $merge_branch return ($plugin_config | upsert status "error") } # Run tests let test_success = run_tests $plugin_path if not $test_success { print $" โŒ Tests failed after merge, rolling back..." rollback_changes $plugin_path $original_branch $merge_branch return ($plugin_config | upsert status "error") } # Apply the successful merge let apply_success = apply_merge $plugin_path $original_branch $merge_branch if not $apply_success { print $" โŒ Failed to apply merge" return ($plugin_config | upsert status "error") } # Update plugin configuration let upstream_commit = git rev-parse $"upstream/($plugin_config.upstream_branch)" | str trim print $" โœ… Successfully merged upstream changes!" ($plugin_config | upsert status "ok" | upsert last_checked_commit $upstream_commit | upsert last_checked_date (current_date)) } # Show merge preview def show_merge_preview [plugin_name: string, plugin_config: record] { let plugin_path = $plugin_config.local_path if not ($plugin_path | path exists) { print $"Plugin directory not found: ($plugin_path)" return } cd $plugin_path print $"๐Ÿ” Merge preview for ($plugin_name):" print "=" * 50 try { let changes = git diff $"HEAD..upstream/($plugin_config.upstream_branch)" --name-only | lines print $"Files that will be changed: ($changes | length)" for file in $changes { print $" ๐Ÿ“„ ($file)" } print "\nSample diff (first 20 lines):" git diff $"HEAD..upstream/($plugin_config.upstream_branch)" | lines | first 20 | each {|line| print $" ($line)"} if ($changes | length) > 20 { print " ... (output truncated)" } } catch { print "Failed to generate preview" } } # Main function def main [ plugin?: string # Specific plugin to merge --force (-f) # Force merge even if status is OK --preview (-p) # Show merge preview only --all (-a) # Process all pending plugins --help (-h) # Show help ] { # Mandatory version check before any plugin operations version_check if $help { print "Safe Merge Upstream - Nushell Plugin Upstream Merger" print "" print "USAGE:" print " nu safe_merge_upstream.nu [OPTIONS] [PLUGIN]" print "" print "OPTIONS:" print " --force, -f Force merge even if plugin status is OK" print " --preview, -p Show merge preview without performing merge" print " --all, -a Process all plugins with pending status" print " --help, -h Show this help message" print "" print "EXAMPLES:" print " nu safe_merge_upstream.nu highlight # Merge specific plugin" print " nu safe_merge_upstream.nu --preview highlight # Preview changes" print " nu safe_merge_upstream.nu --all # Merge all pending" print " nu safe_merge_upstream.nu --force highlight # Force merge" return } # Ensure we're in the repository root directory if not ("nu_plugin_clipboard" | path exists) { error make {msg: "Please run this script from the nushell-plugins repository root directory"} } # Load registry and exclusions let registry = load_registry let exclusions = load_exclusions # Determine which plugins to process let plugins_to_process = if $all { $registry.plugins | transpose key value | where $it.value.status in ["pending", "error", "conflict"] # Filter out excluded plugins | where not (is_plugin_excluded $it.key "merge" $exclusions) } else if ($plugin | is-not-empty) { if $plugin in ($registry.plugins | columns) { [{key: $plugin, value: ($registry.plugins | get $plugin)}] } else { error make {msg: $"Plugin '($plugin)' not found in registry"} } } else { error make {msg: "Please specify a plugin name or use --all flag"} } if ($plugins_to_process | length) == 0 { print "No plugins found to process" return } # Preview mode if $preview { for plugin_entry in $plugins_to_process { show_merge_preview $plugin_entry.key $plugin_entry.value print "" } return } # Confirmation for multiple plugins if ($plugins_to_process | length) > 1 { print $"About to process ($plugins_to_process | length) plugins:" for plugin_entry in $plugins_to_process { print $" - ($plugin_entry.key) (status: ($plugin_entry.value.status))" } print "" let confirm = input "Continue? [y/N]: " if ($confirm | str downcase) != "y" { print "Aborted" return } } # Process plugins mut updated_registry = $registry for plugin_entry in $plugins_to_process { let plugin_name = $plugin_entry.key let plugin_config = $plugin_entry.value let updated_config = process_plugin_merge $plugin_name $plugin_config $force $exclusions $updated_registry = ($updated_registry | upsert plugins.($plugin_name) $updated_config) } # Save updated registry save_registry $updated_registry print $"\nโœ… Merge process completed!" print "๐Ÿ“Š Run 'nu plugin_status.nu' to see updated status" } if ($env.NUSHELL_EXECUTION_CONTEXT? | default "" | str contains "run") { main }