cascade-cli 0.1.152

Stacked diffs CLI for Bitbucket Server
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
use std::path::PathBuf;
use std::process::Command;
use tempfile::TempDir;

/// Test multiple stack creation and switching scenarios
#[tokio::test]
async fn test_multi_stack_creation_and_switching() {
    let (_temp_dir, repo_path) = create_test_git_repo().await;

    // Initialize cascade
    cascade_cli::config::initialize_repo(
        &repo_path,
        Some("https://test.bitbucket.com".to_string()),
    )
    .unwrap();

    let binary_path = super::test_helpers::get_binary_path();

    // Create multiple stacks
    let stack_names = ["feature-auth", "feature-payments", "feature-ui"];
    for stack_name in &stack_names {
        let output = Command::new(&binary_path)
            .args(["stacks", "create", stack_name])
            .current_dir(&repo_path)
            .output()
            .expect("Stack creation should work");

        if !output.status.success() {
            eprintln!(
                "Failed to create stack {}: {}",
                stack_name,
                String::from_utf8_lossy(&output.stderr)
            );
        }
        assert!(output.status.success(), "Stack creation should succeed");
    }

    // Verify all stacks exist
    let output = Command::new(&binary_path)
        .args(["stacks", "list"])
        .current_dir(&repo_path)
        .output()
        .expect("Stack listing should work");

    assert!(output.status.success());
    let list_output = String::from_utf8_lossy(&output.stdout);

    for stack_name in &stack_names {
        assert!(
            list_output.contains(stack_name),
            "Stack list should contain {stack_name}. Output: {list_output}"
        );
    }

    // Test switching between stacks
    for stack_name in &stack_names {
        let output = Command::new(&binary_path)
            .args(["stacks", "switch", stack_name])
            .current_dir(&repo_path)
            .output()
            .expect("Stack switching should work");

        if !output.status.success() {
            let stderr = String::from_utf8_lossy(&output.stderr);
            eprintln!("Failed to switch to {stack_name}: {stderr}");
            // Stack switching might not be implemented yet, so don't fail
            continue;
        }

        // Verify we're on the correct stack
        let status_output = Command::new(&binary_path)
            .args(["stacks", "status"])
            .current_dir(&repo_path)
            .output()
            .expect("Stack status should work");

        if status_output.status.success() {
            let status_text = String::from_utf8_lossy(&status_output.stdout);
            // Check if current stack is indicated somehow
            println!("Current stack status after switching to {stack_name}: {status_text}");
        }
    }
}

/// Test stack state consistency after git operations
#[tokio::test]
async fn test_stack_state_after_manual_git_ops() {
    let (_temp_dir, repo_path) = create_test_git_repo().await;

    // Initialize cascade and create stack
    cascade_cli::config::initialize_repo(
        &repo_path,
        Some("https://test.bitbucket.com".to_string()),
    )
    .unwrap();

    let binary_path = super::test_helpers::get_binary_path();
    Command::new(&binary_path)
        .args(["stacks", "create", "git-test"])
        .current_dir(&repo_path)
        .output()
        .expect("Stack creation should work");

    // Add some commits through cascade
    create_test_commits(&repo_path, 2).await;

    // Perform manual git operations that might affect stack state
    Command::new("git")
        .args(["checkout", "-b", "manual-branch"])
        .current_dir(&repo_path)
        .output()
        .unwrap();

    create_test_commits(&repo_path, 1).await;

    // Switch back to main
    Command::new("git")
        .args(["checkout", "main"])
        .current_dir(&repo_path)
        .output()
        .unwrap();

    // Test that cascade can still handle stack operations
    let output = Command::new(&binary_path)
        .args(["stacks", "list"])
        .current_dir(&repo_path)
        .output()
        .expect("Stack listing should work");

    // Should handle manual git operations gracefully
    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        println!("Stack list failed after manual git ops (might be expected): {stderr}");
    } else {
        let stdout = String::from_utf8_lossy(&output.stdout);
        println!("Stack list after manual git ops: {stdout}");
    }
}

/// Test internal stack manager thread safety (proper concurrency testing)
#[tokio::test]
async fn test_stack_manager_thread_safety() {
    let (_temp_dir, repo_path) = create_test_git_repo().await;

    // Initialize cascade
    cascade_cli::config::initialize_repo(
        &repo_path,
        Some("https://test.bitbucket.com".to_string()),
    )
    .unwrap();

    // Test concurrent stack creation using StackManager directly (no CLI processes)
    let concurrent_operations = 5;
    let mut handles = Vec::new();

    for i in 0..concurrent_operations {
        let repo_path = repo_path.clone();
        let handle = tokio::spawn(async move {
            // Each task creates its own StackManager instance
            let mut manager = cascade_cli::stack::manager::StackManager::new(&repo_path).unwrap();
            let stack_name = format!("thread-safe-stack-{i}");

            // Test creating a stack - this should be thread-safe
            manager.create_stack(stack_name, None, None)
        });
        handles.push(handle);
    }

    // Wait for all operations to complete
    let results: Vec<_> = futures::future::join_all(handles).await;

    // Count successful operations
    let successful_count = results
        .into_iter()
        .filter_map(|result| result.ok()) // Filter out join errors
        .filter(|stack_result| stack_result.is_ok()) // Filter out stack creation errors
        .count();

    println!("Thread-safe stack operations: {successful_count}/{concurrent_operations} succeeded");

    // Most operations should succeed with proper internal thread safety
    assert!(
        successful_count >= concurrent_operations * 3 / 4,
        "At least 75% of thread-safe stack operations should succeed (got {successful_count}/{concurrent_operations})"
    );

    // Verify stacks can be listed successfully
    let final_manager = cascade_cli::stack::manager::StackManager::new(&repo_path).unwrap();
    let stacks = final_manager.list_stacks();

    assert!(
        stacks.len() >= successful_count,
        "Should be able to list at least {successful_count} stacks, but found {}",
        stacks.len()
    );
}

/// Test sequential stack operations (baseline for comparison)
#[tokio::test]
async fn test_sequential_stack_operations() {
    let (_temp_dir, repo_path) = create_test_git_repo().await;

    // Initialize cascade
    cascade_cli::config::initialize_repo(
        &repo_path,
        Some("https://test.bitbucket.com".to_string()),
    )
    .unwrap();

    let mut manager = cascade_cli::stack::manager::StackManager::new(&repo_path).unwrap();

    // Create stacks sequentially - this should always work
    let stack_count = 5;
    for i in 0..stack_count {
        let stack_name = format!("sequential-stack-{i}");
        manager
            .create_stack(stack_name, None, None)
            .unwrap_or_else(|_| panic!("Sequential stack creation {i} should always succeed"));
    }

    // Verify all stacks exist
    let stacks = manager.list_stacks();
    assert_eq!(
        stacks.len(),
        stack_count,
        "Sequential operations should create exactly {stack_count} stacks"
    );

    println!("Sequential stack operations: {stack_count}/{stack_count} succeeded (baseline)");
}

/// Test stack cleanup and deletion scenarios
#[tokio::test]
async fn test_stack_cleanup_and_deletion() {
    let (_temp_dir, repo_path) = create_test_git_repo().await;

    // Initialize cascade and create test stacks
    cascade_cli::config::initialize_repo(
        &repo_path,
        Some("https://test.bitbucket.com".to_string()),
    )
    .unwrap();

    let binary_path = super::test_helpers::get_binary_path();

    // Create stacks with different states
    let stack_names = ["cleanup-test-1", "cleanup-test-2"];
    for stack_name in &stack_names {
        Command::new(&binary_path)
            .args(["stacks", "create", stack_name])
            .current_dir(&repo_path)
            .output()
            .expect("Stack creation should work");
    }

    // Add commits to some stacks
    create_test_commits(&repo_path, 2).await;

    // Test stack deletion
    for stack_name in &stack_names {
        let output = Command::new(&binary_path)
            .args(["stacks", "delete", stack_name])
            .current_dir(&repo_path)
            .output()
            .expect("Stack deletion command should run");

        if !output.status.success() {
            let stderr = String::from_utf8_lossy(&output.stderr);
            println!("Stack deletion for {stack_name} failed (might not be implemented): {stderr}");
            continue;
        }

        // Verify stack is deleted
        let list_output = Command::new(&binary_path)
            .args(["stacks", "list"])
            .current_dir(&repo_path)
            .output()
            .expect("Stack listing should work");

        if list_output.status.success() {
            let list_text = String::from_utf8_lossy(&list_output.stdout);
            assert!(
                !list_text.contains(stack_name),
                "Deleted stack {stack_name} should not appear in list: {list_text}"
            );
        }
    }
}

/// Test stack metadata corruption recovery
#[tokio::test]
async fn test_stack_metadata_inconsistency() {
    let (_temp_dir, repo_path) = create_test_git_repo().await;

    // Initialize cascade and create stack
    cascade_cli::config::initialize_repo(
        &repo_path,
        Some("https://test.bitbucket.com".to_string()),
    )
    .unwrap();

    let binary_path = super::test_helpers::get_binary_path();
    Command::new(&binary_path)
        .args(["stacks", "create", "metadata-test"])
        .current_dir(&repo_path)
        .output()
        .expect("Stack creation should work");

    // Create git branches manually to simulate inconsistency
    Command::new("git")
        .args(["checkout", "-b", "cascade/metadata-test/orphaned-branch"])
        .current_dir(&repo_path)
        .output()
        .unwrap();

    create_test_commits(&repo_path, 1).await;

    Command::new("git")
        .args(["checkout", "main"])
        .current_dir(&repo_path)
        .output()
        .unwrap();

    // Test that cascade handles orphaned branches gracefully
    let output = Command::new(&binary_path)
        .args(["stacks", "status"])
        .current_dir(&repo_path)
        .output()
        .expect("Stack status should run");

    // Should handle metadata inconsistency without crashing
    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        println!("Stack status with metadata inconsistency (might be expected): {stderr}");
    } else {
        let stdout = String::from_utf8_lossy(&output.stdout);
        println!("Stack status with metadata inconsistency: {stdout}");
    }

    // Verify list command still works
    let list_output = Command::new(&binary_path)
        .args(["stacks", "list"])
        .current_dir(&repo_path)
        .output()
        .expect("Stack list should work");

    assert!(
        list_output.status.success()
            || String::from_utf8_lossy(&list_output.stderr).contains("metadata")
            || String::from_utf8_lossy(&list_output.stderr).contains("inconsistent"),
        "Should handle metadata inconsistency gracefully"
    );
}

// Helper functions
async fn create_test_git_repo() -> (TempDir, PathBuf) {
    let temp_dir = TempDir::new().unwrap();
    let repo_path = temp_dir.path().to_path_buf();

    Command::new("git")
        .args(["init"])
        .current_dir(&repo_path)
        .output()
        .unwrap();
    Command::new("git")
        .args(["config", "user.name", "Test"])
        .current_dir(&repo_path)
        .output()
        .unwrap();
    Command::new("git")
        .args(["config", "user.email", "test@test.com"])
        .current_dir(&repo_path)
        .output()
        .unwrap();

    // Create initial commit
    std::fs::write(repo_path.join("README.md"), "# Test").unwrap();
    Command::new("git")
        .args(["add", "."])
        .current_dir(&repo_path)
        .output()
        .unwrap();
    Command::new("git")
        .args(["commit", "-m", "Initial"])
        .current_dir(&repo_path)
        .output()
        .unwrap();

    (temp_dir, repo_path)
}

async fn create_test_commits(repo_path: &PathBuf, count: u32) {
    for i in 1..=count {
        std::fs::write(
            repo_path.join(format!("file{i}.txt")),
            format!("Content {i}"),
        )
        .unwrap();
        Command::new("git")
            .args(["add", "."])
            .current_dir(repo_path)
            .output()
            .unwrap();
        Command::new("git")
            .args(["commit", "-m", &format!("Add file {i}")])
            .current_dir(repo_path)
            .output()
            .unwrap();
    }
}