506 lines
15 KiB
Plaintext
506 lines
15 KiB
Plaintext
![]() |
#!/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
|
||
|
}
|