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
411
use std::path::PathBuf;
use tempfile::TempDir;

/// Tests to ensure Git hook content includes proper user feedback and messaging
///
/// These tests prevent accidental removal of user-facing messages and ensure
/// hooks provide helpful guidance to users.

#[tokio::test]
async fn test_post_commit_hook_contains_user_feedback() {
    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 hooks_manager = cascade_cli::cli::commands::hooks::HooksManager::new(&repo_path).unwrap();
    let hook_content = hooks_manager
        .generate_hook_script(&cascade_cli::cli::commands::hooks::HookType::PostCommit)
        .unwrap();

    // Verify essential user feedback messages are present (no emojis per professional output guidelines)
    assert!(
        hook_content.contains("Adding commit to active stack"),
        "Post-commit hook should show progress message"
    );
    assert!(
        hook_content.contains("Commit added to stack successfully"),
        "Post-commit hook should show success message"
    );
    assert!(
        hook_content.contains("Next: 'ca submit' to create PRs when ready"),
        "Post-commit hook should provide next steps"
    );
    assert!(
        hook_content.contains("Cascade not initialized"),
        "Post-commit hook should handle uninitialized repos gracefully"
    );
    assert!(
        hook_content.contains("Run 'ca init' to start using stacked diffs"),
        "Post-commit hook should guide users to initialize"
    );
    assert!(
        hook_content.contains("No active stack found"),
        "Post-commit hook should handle missing active stack"
    );
    assert!(
        hook_content.contains("Tip: Use 'ca stack create"),
        "Post-commit hook should guide users to create stacks"
    );
    assert!(
        hook_content.contains("Failed to add commit to stack"),
        "Post-commit hook should handle failures gracefully"
    );
    assert!(
        hook_content.contains("Tip: You can manually add it with"),
        "Post-commit hook should provide recovery instructions"
    );
}

#[tokio::test]
async fn test_pre_push_hook_contains_user_feedback() {
    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 hooks_manager = cascade_cli::cli::commands::hooks::HooksManager::new(&repo_path).unwrap();
    let hook_content = hooks_manager
        .generate_hook_script(&cascade_cli::cli::commands::hooks::HookType::PrePush)
        .unwrap();

    // Verify essential user feedback messages are present (professional output, no emojis)
    assert!(
        hook_content.contains("ERROR: Force push detected"),
        "Pre-push hook should detect and warn about force pushes"
    );
    assert!(
        hook_content.contains("Cascade CLI uses stacked diffs"),
        "Pre-push hook should explain why force pushes are problematic"
    );
    assert!(
        hook_content.contains("Instead, try these commands"),
        "Pre-push hook should provide alternatives to force push"
    );
    assert!(
        hook_content.contains("ca sync"),
        "Pre-push hook should suggest sync command"
    );
    assert!(
        hook_content.contains("ca push"),
        "Pre-push hook should suggest push command"
    );
    assert!(
        hook_content.contains("ca submit"),
        "Pre-push hook should suggest submit command"
    );
    assert!(
        hook_content.contains("ca autoland"),
        "Pre-push hook should suggest autoland command"
    );
    assert!(
        hook_content.contains("If you really need to force push"),
        "Pre-push hook should provide escape hatch"
    );
    assert!(
        hook_content.contains("Stack validation") || hook_content.contains("ca validate"),
        "Pre-push hook should show validation"
    );
    // Check for validation success/failure (no emojis)
    assert!(
        hook_content.contains("validation") || hook_content.contains("ca validate"),
        "Pre-push hook should reference validation"
    );
    // Note: We simplified the pre-push hook to just run validation
    // Removed verbose guidance messages to keep it professional and concise
}

#[tokio::test]
async fn test_commit_msg_hook_contains_user_feedback() {
    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 hooks_manager = cascade_cli::cli::commands::hooks::HooksManager::new(&repo_path).unwrap();
    let hook_content = hooks_manager
        .generate_hook_script(&cascade_cli::cli::commands::hooks::HookType::CommitMsg)
        .unwrap();

    // Verify essential validation (professional output, no emojis or conventional commit blurbs)
    // Note: We removed conventional commit format suggestions and emojis as per user request
    // The hook now focuses on essential validation only and exits 0 on success
    // Windows uses "exit /b 0", Unix uses "exit 0"
    assert!(
        hook_content.contains("exit 0") || hook_content.contains("exit /b 0"),
        "Commit-msg hook should have success path"
    );
}

#[tokio::test]
async fn test_prepare_commit_msg_hook_contains_user_feedback() {
    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 hooks_manager = cascade_cli::cli::commands::hooks::HooksManager::new(&repo_path).unwrap();
    let hook_content = hooks_manager
        .generate_hook_script(&cascade_cli::cli::commands::hooks::HookType::PrepareCommitMsg)
        .unwrap();

    // Verify essential user feedback messages are present
    assert!(
        hook_content.contains("# Stack:"),
        "Prepare-commit-msg hook should add stack context"
    );
    assert!(
        hook_content.contains("# This commit will be added to the active stack automatically"),
        "Prepare-commit-msg hook should explain automatic behavior"
    );
    assert!(
        hook_content.contains("# Use 'ca stack status' to see the current stack state"),
        "Prepare-commit-msg hook should provide helpful commands"
    );
}

#[tokio::test]
async fn test_pre_commit_hook_contains_edit_mode_guidance() {
    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 hooks_manager = cascade_cli::cli::commands::hooks::HooksManager::new(&repo_path).unwrap();
    let hook_content = hooks_manager
        .generate_hook_script(&cascade_cli::cli::commands::hooks::HookType::PreCommit)
        .unwrap();

    // Verify edit mode guidance is present
    assert!(
        hook_content.contains("entry status --quiet"),
        "Pre-commit hook should check edit mode status"
    );
    assert!(
        hook_content.contains("You're in EDIT MODE for a stack entry"),
        "Pre-commit hook should provide edit mode message"
    );
    assert!(
        hook_content.contains("[a] amend: Modify the current entry"),
        "Pre-commit hook should explain amend option"
    );
    assert!(
        hook_content.contains("[n] new:   Create new entry on top"),
        "Pre-commit hook should explain new commit option"
    );
    assert!(
        hook_content.contains("[c] cancel: Stop and think about it"),
        "Pre-commit hook should provide cancel option"
    );
    assert!(
        hook_content.contains("entry amend --restack"),
        "Pre-commit hook should call ca entry amend with restack for amend option"
    );
    assert!(
        !hook_content.contains("entry amend --all"),
        "Pre-commit hook should avoid staging unstaged changes when amending"
    );
    assert!(
        hook_content.contains("Creating new stack entry..."),
        "Pre-commit hook should show what happens when creating new entry"
    );
}

#[tokio::test]
async fn test_prepare_commit_msg_hook_contains_edit_mode_guidance() {
    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 hooks_manager = cascade_cli::cli::commands::hooks::HooksManager::new(&repo_path).unwrap();
    let hook_content = hooks_manager
        .generate_hook_script(&cascade_cli::cli::commands::hooks::HookType::PrepareCommitMsg)
        .unwrap();

    // Verify stack context is added (but no edit mode guidance - that's in pre-commit now)
    assert!(
        hook_content.contains("Stack:"),
        "Prepare-commit-msg hook should add stack context"
    );
    assert!(
        hook_content.contains("stack list --active"),
        "Prepare-commit-msg hook should check for active stack"
    );
    // Edit mode guidance was removed - it's now in pre-commit hook only
    assert!(
        !hook_content.contains("[EDIT MODE]"),
        "Prepare-commit-msg hook should NOT contain edit mode guidance anymore"
    );
}

#[tokio::test]
async fn test_hooks_are_platform_specific() {
    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 hooks_manager = cascade_cli::cli::commands::hooks::HooksManager::new(&repo_path).unwrap();
    let hook_content = hooks_manager
        .generate_hook_script(&cascade_cli::cli::commands::hooks::HookType::PostCommit)
        .unwrap();

    // Verify platform-specific content
    #[cfg(windows)]
    {
        assert!(
            hook_content.starts_with("@echo off"),
            "Windows hooks should start with @echo off"
        );
        assert!(
            hook_content.contains("rem Cascade CLI Hook"),
            "Windows hooks should use rem for comments"
        );
        assert!(
            hook_content.contains("%ERRORLEVEL%"),
            "Windows hooks should use ERRORLEVEL for error checking"
        );
    }

    #[cfg(not(windows))]
    {
        assert!(
            hook_content.starts_with("#!/bin/sh"),
            "Unix hooks should start with shebang"
        );
        assert!(
            hook_content.contains("# Cascade CLI Hook"),
            "Unix hooks should use # for comments"
        );
        assert!(
            hook_content.contains("set -e"),
            "Unix hooks should use set -e for error handling"
        );
    }
}

#[tokio::test]
async fn test_hook_content_includes_binary_path() {
    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 hooks_manager = cascade_cli::cli::commands::hooks::HooksManager::new(&repo_path).unwrap();

    let hook_types = [
        cascade_cli::cli::commands::hooks::HookType::PostCommit,
        cascade_cli::cli::commands::hooks::HookType::PrePush,
        cascade_cli::cli::commands::hooks::HookType::PrepareCommitMsg,
    ];

    for hook_type in &hook_types {
        let hook_content = hooks_manager.generate_hook_script(hook_type).unwrap();

        // Since the hook uses the current executable path, we can't test for a specific custom path
        // but we can verify that some executable path is present
        assert!(
            !hook_content.is_empty(),
            "Hook {hook_type:?} should contain content"
        );
    }
}

#[tokio::test]
async fn test_hooks_handle_edge_cases_gracefully() {
    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 hooks_manager = cascade_cli::cli::commands::hooks::HooksManager::new(&repo_path).unwrap();
    let hook_content = hooks_manager
        .generate_hook_script(&cascade_cli::cli::commands::hooks::HookType::PostCommit)
        .unwrap();

    // Verify graceful handling of edge cases
    assert!(
        hook_content.contains("if [ ! -d \"$REPO_ROOT/.cascade\" ]")
            || hook_content.contains("if not exist \"%REPO_ROOT%\\.cascade\""),
        "Hooks should check for Cascade initialization"
    );
    assert!(
        hook_content.contains("exit 0") || hook_content.contains("exit /b 0"),
        "Hooks should exit gracefully when Cascade is not initialized"
    );
}

// Helper function to create test git repository
async fn create_test_git_repo() -> (TempDir, PathBuf) {
    let temp_dir = TempDir::new().unwrap();
    let repo_path = temp_dir.path().to_path_buf();

    // Initialize git repository
    std::process::Command::new("git")
        .args(["init"])
        .current_dir(&repo_path)
        .output()
        .unwrap();
    std::process::Command::new("git")
        .args(["config", "user.name", "Test"])
        .current_dir(&repo_path)
        .output()
        .unwrap();
    std::process::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();
    std::process::Command::new("git")
        .args(["add", "."])
        .current_dir(&repo_path)
        .output()
        .unwrap();
    std::process::Command::new("git")
        .args(["commit", "-m", "Initial"])
        .current_dir(&repo_path)
        .output()
        .unwrap();

    (temp_dir, repo_path)
}