summaryrefslogblamecommitdiff
path: root/makima/src/daemon/worktree/manager.rs
blob: 310627c4085bd3d25e34e15171e183cd6c0f76e3 (plain) (tree)
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521

































































































































                                                                                                 
                                                     
                                                                                                  





















                                                                                      










                                                                                       
                                                                                        



                                  
                                                                                   







                                                                                   
                                                                                       



                                              
                                                                      








                                                                                    
                                                                                      



                                  



























                                                                                            
                                      
                                                                                                                             




























                                                                                          






























                                                                                                          








                                                                       
                                                                                                    















                                                                                      


                                                                              

                                        




























                                                                                               
             
         
 










                                                                                 









































































                                                                                                                





                                                                                       
 


                                                                                          

                                     


                                        
 





















































                                                                                   













































































                                                                                       





                                                                                       
 


                                                                                          

                                      


                                        
 























































                                                                               































































































































































































































































































































































































































































































































































































































































































































































































































































































                                                                                                    
                                                                                                                  


                                     
                                   






                                                
                                                                        





















                                                                      







                                                                                   
 














                                                                                    
 








                                                                                                                                                                      

                                                      






                                                                                     
 




                                                                  
 






                                                                     
 





                                                                                                    
 





















                                                                                                 

         
                                 








                                        
                                         



























































































                                                                                   






































































































































                                                                                       



































































                                                                              

                   





























                                                                                  
//! Worktree manager implementation.

use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::LazyLock;

use tokio::process::Command;
use tokio::sync::Mutex;
use uuid::Uuid;

/// Errors that can occur during worktree operations.
#[derive(Debug, thiserror::Error)]
pub enum WorktreeError {
    #[error("Git command failed: {0}")]
    GitCommand(String),

    #[error("Repository not found: {0}")]
    RepoNotFound(String),

    #[error("Failed to create directory: {0}")]
    CreateDir(#[from] std::io::Error),

    #[error("Invalid repository path: {0}")]
    InvalidPath(String),

    #[error("Worktree already exists: {0}")]
    AlreadyExists(String),

    #[error("Clone failed: {0}")]
    CloneFailed(String),

    #[error("Merge in progress")]
    MergeInProgress,

    #[error("No merge in progress")]
    NoMergeInProgress,

    #[error("Merge has conflicts: {0}")]
    MergeConflicts(String),
}

/// Strategy for resolving a merge conflict.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConflictResolution {
    /// Use our version (the branch being merged into).
    Ours,
    /// Use their version (the branch being merged).
    Theirs,
}

/// State of an in-progress merge.
#[derive(Debug, Clone)]
pub struct MergeState {
    /// The branch being merged.
    pub source_branch: String,
    /// Files with unresolved conflicts.
    pub conflicted_files: Vec<String>,
    /// Whether a merge is currently in progress.
    pub in_progress: bool,
}

/// Information about a task branch.
#[derive(Debug, Clone)]
pub struct TaskBranchInfo {
    /// Full branch name.
    pub name: String,
    /// Task ID extracted from branch name (if parseable).
    pub task_id: Option<Uuid>,
    /// Whether this branch has been merged into the current branch.
    pub is_merged: bool,
    /// Short SHA of the last commit.
    pub last_commit: String,
    /// Subject line of the last commit.
    pub last_commit_message: String,
}

/// Information about a created worktree.
#[derive(Debug, Clone)]
pub struct WorktreeInfo {
    /// Path to the worktree directory.
    pub path: PathBuf,
    /// Git branch name for this worktree.
    pub branch: String,
    /// Source repository path.
    pub source_repo: PathBuf,
}

/// Manages git worktrees for task isolation.
pub struct WorktreeManager {
    /// Base directory for all worktrees (~/.makima/worktrees).
    base_dir: PathBuf,
    /// Base directory for cloned repos (~/.makima/repos).
    repos_dir: PathBuf,
    /// Branch prefix for task branches.
    branch_prefix: String,
}

/// Per-worktree locks to prevent concurrent creation issues.
static WORKTREE_LOCKS: LazyLock<Mutex<HashMap<String, std::sync::Arc<tokio::sync::Mutex<()>>>>> =
    LazyLock::new(|| Mutex::new(HashMap::new()));

impl WorktreeManager {
    /// Create a new WorktreeManager with the given base directory.
    pub fn new(base_dir: PathBuf) -> Self {
        let repos_dir = base_dir.parent()
            .map(|p| p.join("repos"))
            .unwrap_or_else(|| base_dir.join("repos"));

        Self {
            base_dir,
            repos_dir,
            branch_prefix: "makima/task-".to_string(),
        }
    }

    /// Get the default worktree base directory (~/.makima/worktrees).
    pub fn default_base_dir() -> PathBuf {
        dirs::home_dir()
            .unwrap_or_else(|| PathBuf::from("."))
            .join(".makima")
            .join("worktrees")
    }

    /// Get the base directory for worktrees.
    pub fn base_dir(&self) -> &Path {
        &self.base_dir
    }

    /// Detect the default branch of a repository.
    /// Tries to find HEAD's target, falling back to common branch names.
    /// Works for both regular and bare repositories.
    pub async fn detect_default_branch(&self, repo_path: &Path) -> Result<String, WorktreeError> {
        tracing::debug!("Detecting default branch for repo: {}", repo_path.display());

        // First, try to read HEAD directly (works for bare repos)
        // In bare repos, HEAD is a symbolic ref to the default branch
        let output = Command::new("git")
            .args(["symbolic-ref", "HEAD", "--short"])
            .current_dir(repo_path)
            .output()
            .await?;

        if output.status.success() {
            let branch = String::from_utf8_lossy(&output.stdout).trim().to_string();
            if !branch.is_empty() {
                tracing::debug!("Detected default branch from HEAD: {}", branch);
                return Ok(branch);
            }
        } else {
            let stderr = String::from_utf8_lossy(&output.stderr);
            tracing::debug!("symbolic-ref HEAD failed: {}", stderr.trim());
        }

        // Try to get the branch that origin/HEAD points to (for regular clones)
        let output = Command::new("git")
            .args(["symbolic-ref", "refs/remotes/origin/HEAD", "--short"])
            .current_dir(repo_path)
            .output()
            .await?;

        if output.status.success() {
            let branch = String::from_utf8_lossy(&output.stdout).trim().to_string();
            // Remove "origin/" prefix if present
            let branch = branch.strip_prefix("origin/").unwrap_or(&branch).to_string();
            if !branch.is_empty() {
                tracing::debug!("Detected default branch from origin/HEAD: {}", branch);
                return Ok(branch);
            }
        }

        // Try common branch names in refs/heads (works for bare and regular repos)
        for branch in ["main", "master", "develop", "trunk"] {
            let output = Command::new("git")
                .args(["rev-parse", "--verify", &format!("refs/heads/{}", branch)])
                .current_dir(repo_path)
                .output()
                .await?;

            if output.status.success() {
                tracing::debug!("Detected default branch from refs/heads: {}", branch);
                return Ok(branch.to_string());
            }
        }

        // Fall back to getting the current branch (for regular repos)
        let output = Command::new("git")
            .args(["rev-parse", "--abbrev-ref", "HEAD"])
            .current_dir(repo_path)
            .output()
            .await?;

        if output.status.success() {
            let branch = String::from_utf8_lossy(&output.stdout).trim().to_string();
            if !branch.is_empty() && branch != "HEAD" {
                tracing::debug!("Detected default branch from rev-parse: {}", branch);
                return Ok(branch);
            }
        }

        // Final fallback: list all branches and pick the first one
        let output = Command::new("git")
            .args(["for-each-ref", "--format=%(refname:short)", "refs/heads/", "--count=1"])
            .current_dir(repo_path)
            .output()
            .await?;

        if output.status.success() {
            let branch = String::from_utf8_lossy(&output.stdout).trim().to_string();
            if !branch.is_empty() {
                tracing::warn!("Using first available branch as fallback: {}", branch);
                return Ok(branch);
            }
        }

        // Log what branches exist for debugging
        let output = Command::new("git")
            .args(["for-each-ref", "--format=%(refname)", "refs/"])
            .current_dir(repo_path)
            .output()
            .await?;

        let available_refs = String::from_utf8_lossy(&output.stdout);
        tracing::error!(
            "Could not detect default branch. Available refs:\n{}",
            available_refs
        );

        Err(WorktreeError::GitCommand(
            format!("Could not detect default branch. Check if the repository at {} has any branches.", repo_path.display()),
        ))
    }

    /// Ensure the source repository exists locally and is up-to-date.
    /// If repo_source is a URL, clone it. If it's a path, verify it exists.
    /// For both cases, fetch latest changes from remote if available.
    pub async fn ensure_repo(&self, repo_source: &str) -> Result<PathBuf, WorktreeError> {
        // Check if it's a URL (simple heuristic)
        if repo_source.starts_with("http://")
            || repo_source.starts_with("https://")
            || repo_source.starts_with("git@")
            || repo_source.starts_with("ssh://")
        {
            self.clone_or_fetch_repo(repo_source).await
        } else {
            // Treat as local path - expand tilde if present
            let path = expand_tilde(repo_source);
            if !path.exists() {
                return Err(WorktreeError::RepoNotFound(repo_source.to_string()));
            }
            // Verify it's a git repo
            let git_dir = path.join(".git");
            if !git_dir.exists() {
                return Err(WorktreeError::InvalidPath(format!(
                    "{} is not a git repository",
                    repo_source
                )));
            }

            // Check if 'origin' remote exists
            let origin_check = Command::new("git")
                .args(["remote", "get-url", "origin"])
                .current_dir(&path)
                .output()
                .await;

            let has_origin = origin_check
                .map(|o| o.status.success())
                .unwrap_or(false);

            if has_origin {
                // Fetch from origin specifically to get the latest changes
                tracing::info!("Fetching latest from origin for local repo: {}", repo_source);
                let fetch_output = Command::new("git")
                    .args(["fetch", "origin"])
                    .current_dir(&path)
                    .output()
                    .await?;

                if !fetch_output.status.success() {
                    let stderr = String::from_utf8_lossy(&fetch_output.stderr);
                    tracing::warn!("Git fetch from origin failed (continuing anyway): {}", stderr);
                } else {
                    tracing::info!("Successfully fetched latest changes from origin for {}", repo_source);
                }
            } else {
                tracing::debug!("Local repo has no origin remote: {}", repo_source);
            }

            // Fetch from all remotes (includes any other remotes besides origin)
            let output = Command::new("git")
                .args(["fetch", "--all", "--prune"])
                .current_dir(&path)
                .output()
                .await?;

            if !output.status.success() {
                let stderr = String::from_utf8_lossy(&output.stderr);
                // Don't fail - repo might not have a remote configured
                tracing::debug!("Git fetch --all for local repo (may not have remote): {}", stderr);
            }

            Ok(path)
        }
    }

    /// Clone a repository or fetch if already cloned.
    async fn clone_or_fetch_repo(&self, url: &str) -> Result<PathBuf, WorktreeError> {
        // Extract repo name from URL
        let repo_name = extract_repo_name(url);
        let repo_path = self.repos_dir.join(&repo_name);

        // Create repos directory if needed
        tokio::fs::create_dir_all(&self.repos_dir).await?;

        if repo_path.exists() {
            // Verify this is actually a git repository before trying to fetch
            let is_git_repo = Command::new("git")
                .args(["rev-parse", "--is-bare-repository"])
                .current_dir(&repo_path)
                .output()
                .await
                .map(|o| o.status.success())
                .unwrap_or(false);

            if !is_git_repo {
                // Directory exists but is not a git repository - remove and re-clone
                tracing::warn!(
                    "Directory {} exists but is not a git repository, removing and re-cloning",
                    repo_path.display()
                );
                tokio::fs::remove_dir_all(&repo_path).await?;

                // Fall through to clone below
            } else {
                // Fetch latest changes
                tracing::info!("Fetching updates for existing repo: {}", repo_name);
                let output = Command::new("git")
                    .args(["fetch", "--all", "--prune"])
                    .current_dir(&repo_path)
                    .output()
                    .await?;

                if !output.status.success() {
                    let stderr = String::from_utf8_lossy(&output.stderr);
                    tracing::warn!("Git fetch warning: {}", stderr);
                    // Don't fail on fetch errors, repo might still be usable
                }

                return Ok(repo_path);
            }
        }

        // Clone the repository
        tracing::info!("Cloning repository: {} -> {}", url, repo_path.display());
        let output = Command::new("git")
            .args(["clone", "--bare", url])
            .arg(&repo_path)
            .output()
            .await?;

        if !output.status.success() {
            let stderr = String::from_utf8_lossy(&output.stderr);
            return Err(WorktreeError::CloneFailed(stderr.to_string()));
        }

        Ok(repo_path)
    }

    /// Create a worktree for a task.
    ///
    /// This creates a unique directory with a git worktree checked out to a new branch.
    pub async fn create_worktree(
        &self,
        source_repo: &Path,
        task_id: Uuid,
        task_name: &str,
        base_branch: &str,
    ) -> Result<WorktreeInfo, WorktreeError> {
        // Generate unique directory name and branch
        let dir_name = format!("{}-{}", short_uuid(task_id), sanitize_name(task_name));
        let worktree_path = self.base_dir.join(&dir_name);
        // Branch name: makima/{task-name-with-dashes}-{short-id}
        let branch_name = format!("{}{}-{}", self.branch_prefix, sanitize_name(task_name), short_uuid(task_id));

        // Acquire lock for this worktree path
        let lock = {
            let mut locks = WORKTREE_LOCKS.lock().await;
            locks
                .entry(worktree_path.to_string_lossy().to_string())
                .or_insert_with(|| std::sync::Arc::new(tokio::sync::Mutex::new(())))
                .clone()
        };
        let _guard = lock.lock().await;

        // Check if worktree already exists - reuse it if so
        if worktree_path.exists() {
            tracing::info!(
                task_id = %task_id,
                worktree_path = %worktree_path.display(),
                "Worktree already exists, reusing"
            );

            // Verify it's a valid git directory
            let git_dir = worktree_path.join(".git");
            if git_dir.exists() {
                // Get the current branch name
                let output = Command::new("git")
                    .args(["rev-parse", "--abbrev-ref", "HEAD"])
                    .current_dir(&worktree_path)
                    .output()
                    .await?;

                let current_branch = if output.status.success() {
                    String::from_utf8_lossy(&output.stdout).trim().to_string()
                } else {
                    branch_name.clone()
                };

                return Ok(WorktreeInfo {
                    path: worktree_path,
                    branch: current_branch,
                    source_repo: source_repo.to_path_buf(),
                });
            } else {
                // Directory exists but isn't a git worktree - remove and recreate
                tracing::warn!(
                    task_id = %task_id,
                    worktree_path = %worktree_path.display(),
                    "Directory exists but is not a git worktree, removing"
                );
                tokio::fs::remove_dir_all(&worktree_path).await?;
            }
        }

        // Create base directory
        tokio::fs::create_dir_all(&self.base_dir).await?;

        // Prune stale worktree entries first (handles "missing but registered" errors)
        let _ = Command::new("git")
            .args(["worktree", "prune"])
            .current_dir(source_repo)
            .output()
            .await;

        // Check if the branch already exists (e.g., from a previous run of the same task)
        let branch_exists = Command::new("git")
            .args(["rev-parse", "--verify", &format!("refs/heads/{}", branch_name)])
            .current_dir(source_repo)
            .output()
            .await
            .map(|o| o.status.success())
            .unwrap_or(false);

        if branch_exists {
            tracing::info!(
                task_id = %task_id,
                worktree_path = %worktree_path.display(),
                branch = %branch_name,
                "Branch already exists, creating worktree from existing branch"
            );

            // Use existing branch - try without force first, then with force
            let output = Command::new("git")
                .args(["worktree", "add", "-f"])
                .arg(&worktree_path)
                .arg(&branch_name)
                .current_dir(source_repo)
                .output()
                .await?;

            if !output.status.success() {
                let stderr = String::from_utf8_lossy(&output.stderr);
                return Err(WorktreeError::GitCommand(format!(
                    "Failed to create worktree from existing branch: {}",
                    stderr
                )));
            }
        } else {
            tracing::info!(
                task_id = %task_id,
                worktree_path = %worktree_path.display(),
                branch = %branch_name,
                base_branch = %base_branch,
                "Creating worktree with new branch"
            );

            // Create the worktree with a new branch based on the local base_branch
            let output = Command::new("git")
                .args([
                    "worktree",
                    "add",
                    "-b",
                    &branch_name,
                ])
                .arg(&worktree_path)
                .arg(base_branch)
                .current_dir(source_repo)
                .output()
                .await?;

            if !output.status.success() {
                let stderr = String::from_utf8_lossy(&output.stderr);
                return Err(WorktreeError::GitCommand(format!(
                    "Failed to create worktree: {}",
                    stderr
                )));
            }
        }

        tracing::info!(
            task_id = %task_id,
            worktree_path = %worktree_path.display(),
            "Worktree created successfully"
        );

        Ok(WorktreeInfo {
            path: worktree_path,
            branch: branch_name,
            source_repo: source_repo.to_path_buf(),
        })
    }

    /// Create a worktree for a task by copying from another task's worktree.
    ///
    /// This allows sequential subtasks where one continues from another's work,
    /// including uncommitted changes.
    pub async fn create_worktree_from_task(
        &self,
        source_worktree: &Path,
        task_id: Uuid,
        task_name: &str,
    ) -> Result<WorktreeInfo, WorktreeError> {
        // Verify source worktree exists
        if !source_worktree.exists() {
            return Err(WorktreeError::RepoNotFound(format!(
                "Source worktree not found: {}",
                source_worktree.display()
            )));
        }

        // Get the source repo from the source worktree
        let source_repo = self.get_worktree_source(source_worktree).await?;

        // Get the base branch from source worktree's current HEAD
        let output = Command::new("git")
            .args(["rev-parse", "HEAD"])
            .current_dir(source_worktree)
            .output()
            .await?;

        if !output.status.success() {
            return Err(WorktreeError::GitCommand(
                "Failed to get source worktree HEAD".to_string(),
            ));
        }
        let source_commit = String::from_utf8_lossy(&output.stdout).trim().to_string();

        // Generate unique directory name and branch for new worktree
        let dir_name = format!("{}-{}", short_uuid(task_id), sanitize_name(task_name));
        let worktree_path = self.base_dir.join(&dir_name);
        let branch_name = format!("{}{}", self.branch_prefix, task_id);

        // Acquire lock for this worktree path
        let lock = {
            let mut locks = WORKTREE_LOCKS.lock().await;
            locks
                .entry(worktree_path.to_string_lossy().to_string())
                .or_insert_with(|| std::sync::Arc::new(tokio::sync::Mutex::new(())))
                .clone()
        };
        let _guard = lock.lock().await;

        // Remove existing worktree if present
        if worktree_path.exists() {
            tracing::info!(
                task_id = %task_id,
                worktree_path = %worktree_path.display(),
                "Removing existing worktree before creating from source"
            );
            tokio::fs::remove_dir_all(&worktree_path).await?;
        }

        // Create base directory
        tokio::fs::create_dir_all(&self.base_dir).await?;

        // Prune stale worktree entries first (handles "missing but registered" errors)
        let _ = Command::new("git")
            .args(["worktree", "prune"])
            .current_dir(&source_repo)
            .output()
            .await;

        // Check if the branch already exists (e.g., from a previous run of the same task)
        let branch_exists = Command::new("git")
            .args(["rev-parse", "--verify", &format!("refs/heads/{}", branch_name)])
            .current_dir(&source_repo)
            .output()
            .await
            .map(|o| o.status.success())
            .unwrap_or(false);

        if branch_exists {
            tracing::info!(
                task_id = %task_id,
                source_worktree = %source_worktree.display(),
                worktree_path = %worktree_path.display(),
                branch = %branch_name,
                "Branch already exists, creating worktree from existing branch"
            );

            // Use existing branch with force flag
            let output = Command::new("git")
                .args(["worktree", "add", "-f"])
                .arg(&worktree_path)
                .arg(&branch_name)
                .current_dir(&source_repo)
                .output()
                .await?;

            if !output.status.success() {
                let stderr = String::from_utf8_lossy(&output.stderr);
                return Err(WorktreeError::GitCommand(format!(
                    "Failed to create worktree from existing branch: {}",
                    stderr
                )));
            }
        } else {
            tracing::info!(
                task_id = %task_id,
                source_worktree = %source_worktree.display(),
                worktree_path = %worktree_path.display(),
                branch = %branch_name,
                source_commit = %source_commit,
                "Creating worktree from source task with new branch"
            );

            // Create a new worktree based on the source commit
            let output = Command::new("git")
                .args([
                    "worktree",
                    "add",
                    "-b",
                    &branch_name,
                ])
                .arg(&worktree_path)
                .arg(&source_commit)
                .current_dir(&source_repo)
                .output()
                .await?;

            if !output.status.success() {
                let stderr = String::from_utf8_lossy(&output.stderr);
                return Err(WorktreeError::GitCommand(format!(
                    "Failed to create worktree: {}",
                    stderr
                )));
            }
        }

        // Now copy uncommitted changes from source worktree
        // Use rsync to copy all files except .git
        let output = Command::new("rsync")
            .args([
                "-a",
                "--exclude", ".git",
                "--exclude", ".makima",
                &format!("{}/", source_worktree.display()),
                &format!("{}/", worktree_path.display()),
            ])
            .output()
            .await?;

        if !output.status.success() {
            let stderr = String::from_utf8_lossy(&output.stderr);
            tracing::warn!(
                task_id = %task_id,
                "rsync warning (continuing anyway): {}",
                stderr
            );
        }

        tracing::info!(
            task_id = %task_id,
            worktree_path = %worktree_path.display(),
            "Worktree created from source task successfully"
        );

        Ok(WorktreeInfo {
            path: worktree_path,
            branch: branch_name,
            source_repo: source_repo.to_path_buf(),
        })
    }

    /// Remove a worktree and optionally its branch.
    pub async fn remove_worktree(
        &self,
        worktree_path: &Path,
        delete_branch: bool,
    ) -> Result<(), WorktreeError> {
        if !worktree_path.exists() {
            return Ok(()); // Already gone
        }

        // Get the branch name before removing
        let branch_name = if delete_branch {
            self.get_worktree_branch(worktree_path).await.ok()
        } else {
            None
        };

        // Find the source repo from worktree
        let source_repo = self.get_worktree_source(worktree_path).await?;

        tracing::info!(
            worktree_path = %worktree_path.display(),
            delete_branch = delete_branch,
            "Removing worktree"
        );

        // Remove the worktree
        let output = Command::new("git")
            .args(["worktree", "remove", "--force"])
            .arg(worktree_path)
            .current_dir(&source_repo)
            .output()
            .await?;

        if !output.status.success() {
            let stderr = String::from_utf8_lossy(&output.stderr);
            // Try force removal of directory if git worktree remove fails
            if worktree_path.exists() {
                tokio::fs::remove_dir_all(worktree_path).await?;
            }
            tracing::warn!("Git worktree remove warning: {}", stderr);
        }

        // Prune worktree references
        let _ = Command::new("git")
            .args(["worktree", "prune"])
            .current_dir(&source_repo)
            .output()
            .await;

        // Delete the branch if requested
        if let Some(branch) = branch_name {
            let output = Command::new("git")
                .args(["branch", "-D", &branch])
                .current_dir(&source_repo)
                .output()
                .await?;

            if !output.status.success() {
                let stderr = String::from_utf8_lossy(&output.stderr);
                tracing::warn!("Failed to delete branch {}: {}", branch, stderr);
            }
        }

        Ok(())
    }

    /// Get the branch name of a worktree.
    async fn get_worktree_branch(&self, worktree_path: &Path) -> Result<String, WorktreeError> {
        let output = Command::new("git")
            .args(["rev-parse", "--abbrev-ref", "HEAD"])
            .current_dir(worktree_path)
            .output()
            .await?;

        if !output.status.success() {
            let stderr = String::from_utf8_lossy(&output.stderr);
            return Err(WorktreeError::GitCommand(format!(
                "Failed to get branch: {}",
                stderr
            )));
        }

        Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
    }

    /// Get the source repository path for a worktree.
    async fn get_worktree_source(&self, worktree_path: &Path) -> Result<PathBuf, WorktreeError> {
        // Read the .git file in the worktree which contains the path to the main repo
        let git_file = worktree_path.join(".git");

        if git_file.is_file() {
            let content = tokio::fs::read_to_string(&git_file).await?;
            // Format: "gitdir: /path/to/repo/.git/worktrees/name"
            if let Some(gitdir) = content.strip_prefix("gitdir: ") {
                let gitdir = gitdir.trim();
                // Navigate from worktrees/name back to the main repo
                let path = PathBuf::from(gitdir);
                if let Some(worktrees_dir) = path.parent() {
                    if let Some(git_dir) = worktrees_dir.parent() {
                        if let Some(repo_dir) = git_dir.parent() {
                            return Ok(repo_dir.to_path_buf());
                        }
                    }
                }
            }
        }

        // Fallback: try to find it in our repos directory
        Err(WorktreeError::InvalidPath(format!(
            "Could not determine source repo for worktree: {}",
            worktree_path.display()
        )))
    }

    /// List all worktrees in the base directory.
    pub async fn list_worktrees(&self) -> Result<Vec<PathBuf>, WorktreeError> {
        let mut worktrees = Vec::new();

        if !self.base_dir.exists() {
            return Ok(worktrees);
        }

        let mut entries = tokio::fs::read_dir(&self.base_dir).await?;
        while let Some(entry) = entries.next_entry().await? {
            let path = entry.path();
            if path.is_dir() && path.join(".git").exists() {
                worktrees.push(path);
            }
        }

        Ok(worktrees)
    }

    /// Initialize a new git repository for a task.
    ///
    /// This creates a fresh git repo (not a worktree) for tasks that don't need
    /// an existing codebase. Use this when `repository_url` is `new://` or `new://project-name`.
    pub async fn init_new_repo(
        &self,
        task_id: Uuid,
        repo_source: &str,
    ) -> Result<WorktreeInfo, WorktreeError> {
        let project_name = extract_new_repo_name(repo_source);
        let dir_name = match project_name {
            Some(name) => format!("{}-{}", short_uuid(task_id), sanitize_name(name)),
            None => format!("{}-new", short_uuid(task_id)),
        };
        let repo_path = self.repos_dir.join(&dir_name);

        tracing::info!(
            task_id = %task_id,
            path = %repo_path.display(),
            project_name = ?project_name,
            "Initializing new git repository"
        );

        // Create directory
        tokio::fs::create_dir_all(&repo_path).await?;

        // git init
        let output = Command::new("git")
            .args(["init"])
            .current_dir(&repo_path)
            .output()
            .await?;

        if !output.status.success() {
            let stderr = String::from_utf8_lossy(&output.stderr);
            return Err(WorktreeError::GitCommand(format!(
                "Failed to init repository: {}",
                stderr
            )));
        }

        // Configure git user (needed for commits)
        let _ = Command::new("git")
            .args(["config", "user.email", "makima@localhost"])
            .current_dir(&repo_path)
            .output()
            .await;
        let _ = Command::new("git")
            .args(["config", "user.name", "Makima"])
            .current_dir(&repo_path)
            .output()
            .await;

        // Initial commit (required for worktrees to work later if needed)
        let output = Command::new("git")
            .args(["commit", "--allow-empty", "-m", "Initial commit"])
            .current_dir(&repo_path)
            .output()
            .await?;

        if !output.status.success() {
            let stderr = String::from_utf8_lossy(&output.stderr);
            return Err(WorktreeError::GitCommand(format!(
                "Failed to create initial commit: {}",
                stderr
            )));
        }

        tracing::info!(
            task_id = %task_id,
            path = %repo_path.display(),
            "New git repository initialized"
        );

        Ok(WorktreeInfo {
            path: repo_path.clone(),
            branch: "main".to_string(),
            source_repo: repo_path,
        })
    }

    // ========== Merge Operations ==========

    /// List all task branches in a repository.
    ///
    /// Returns branches matching the pattern `makima/task-*`.
    pub async fn list_task_branches(
        &self,
        repo_path: &Path,
    ) -> Result<Vec<TaskBranchInfo>, WorktreeError> {
        // Get all branches matching our prefix
        let output = Command::new("git")
            .args([
                "branch",
                "--list",
                &format!("{}*", self.branch_prefix),
                "--format=%(refname:short)|%(objectname:short)|%(subject)",
            ])
            .current_dir(repo_path)
            .output()
            .await?;

        if !output.status.success() {
            let stderr = String::from_utf8_lossy(&output.stderr);
            return Err(WorktreeError::GitCommand(format!(
                "Failed to list branches: {}",
                stderr
            )));
        }

        // Get list of merged branches
        let merged_output = Command::new("git")
            .args(["branch", "--merged", "HEAD", "--format=%(refname:short)"])
            .current_dir(repo_path)
            .output()
            .await?;

        let merged_branches: std::collections::HashSet<String> = if merged_output.status.success() {
            String::from_utf8_lossy(&merged_output.stdout)
                .lines()
                .map(|s| s.trim().to_string())
                .collect()
        } else {
            std::collections::HashSet::new()
        };

        let stdout = String::from_utf8_lossy(&output.stdout);
        let mut branches = Vec::new();

        for line in stdout.lines() {
            let parts: Vec<&str> = line.split('|').collect();
            if parts.len() >= 3 {
                let name = parts[0].trim().to_string();
                let last_commit = parts[1].trim().to_string();
                let last_commit_message = parts[2].trim().to_string();

                // Try to extract task ID from branch name
                let task_id = name
                    .strip_prefix(&self.branch_prefix)
                    .and_then(|s| Uuid::parse_str(s).ok());

                let is_merged = merged_branches.contains(&name);

                branches.push(TaskBranchInfo {
                    name,
                    task_id,
                    is_merged,
                    last_commit,
                    last_commit_message,
                });
            }
        }

        Ok(branches)
    }

    /// Start a merge of a branch into the current worktree.
    ///
    /// Uses `--no-commit` to allow conflict resolution before committing.
    /// Returns Ok(None) if merge succeeds without conflicts, or Ok(Some(files))
    /// with the list of conflicted files.
    pub async fn merge_branch(
        &self,
        worktree_path: &Path,
        source_branch: &str,
    ) -> Result<Option<Vec<String>>, WorktreeError> {
        // Check if there's already a merge in progress
        if self.is_merge_in_progress(worktree_path).await? {
            return Err(WorktreeError::MergeInProgress);
        }

        tracing::info!(
            worktree = %worktree_path.display(),
            source_branch = %source_branch,
            "Starting merge"
        );

        // Attempt the merge with --no-commit --no-ff
        let output = Command::new("git")
            .args(["merge", "--no-commit", "--no-ff", source_branch])
            .current_dir(worktree_path)
            .output()
            .await?;

        if output.status.success() {
            tracing::info!("Merge completed without conflicts");
            return Ok(None);
        }

        // Check if there are conflicts
        let conflicts = self.get_conflicted_files(worktree_path).await?;
        if !conflicts.is_empty() {
            tracing::info!(
                conflicts = ?conflicts,
                "Merge has conflicts"
            );
            return Ok(Some(conflicts));
        }

        // Other error
        let stderr = String::from_utf8_lossy(&output.stderr);
        Err(WorktreeError::GitCommand(format!(
            "Merge failed: {}",
            stderr
        )))
    }

    /// Check if a merge is currently in progress.
    pub async fn is_merge_in_progress(&self, worktree_path: &Path) -> Result<bool, WorktreeError> {
        // Check for MERGE_HEAD file
        let merge_head = worktree_path.join(".git").join("MERGE_HEAD");
        if merge_head.exists() {
            return Ok(true);
        }

        // Also check in .git file (for worktrees)
        let git_file = worktree_path.join(".git");
        if git_file.is_file() {
            if let Ok(content) = tokio::fs::read_to_string(&git_file).await {
                if let Some(gitdir) = content.strip_prefix("gitdir: ") {
                    let gitdir = PathBuf::from(gitdir.trim());
                    let merge_head = gitdir.join("MERGE_HEAD");
                    if merge_head.exists() {
                        return Ok(true);
                    }
                }
            }
        }

        Ok(false)
    }

    /// Get the list of files with unresolved conflicts.
    pub async fn get_conflicted_files(
        &self,
        worktree_path: &Path,
    ) -> Result<Vec<String>, WorktreeError> {
        let output = Command::new("git")
            .args(["diff", "--name-only", "--diff-filter=U"])
            .current_dir(worktree_path)
            .output()
            .await?;

        if !output.status.success() {
            // No conflicts or not in merge state
            return Ok(Vec::new());
        }

        let stdout = String::from_utf8_lossy(&output.stdout);
        let files: Vec<String> = stdout
            .lines()
            .map(|s| s.trim().to_string())
            .filter(|s| !s.is_empty())
            .collect();

        Ok(files)
    }

    /// Get the current merge state.
    pub async fn get_merge_state(
        &self,
        worktree_path: &Path,
    ) -> Result<MergeState, WorktreeError> {
        let in_progress = self.is_merge_in_progress(worktree_path).await?;

        if !in_progress {
            return Ok(MergeState {
                source_branch: String::new(),
                conflicted_files: Vec::new(),
                in_progress: false,
            });
        }

        // Get the branch being merged from MERGE_HEAD
        let source_branch = self.get_merge_source_branch(worktree_path).await?;
        let conflicted_files = self.get_conflicted_files(worktree_path).await?;

        Ok(MergeState {
            source_branch,
            conflicted_files,
            in_progress: true,
        })
    }

    /// Get the branch name being merged (from MERGE_HEAD).
    async fn get_merge_source_branch(&self, worktree_path: &Path) -> Result<String, WorktreeError> {
        // Get MERGE_HEAD commit
        let output = Command::new("git")
            .args(["rev-parse", "MERGE_HEAD"])
            .current_dir(worktree_path)
            .output()
            .await?;

        if !output.status.success() {
            return Ok("unknown".to_string());
        }

        let commit = String::from_utf8_lossy(&output.stdout).trim().to_string();

        // Try to find branch name for this commit
        let output = Command::new("git")
            .args(["name-rev", "--name-only", &commit])
            .current_dir(worktree_path)
            .output()
            .await?;

        if output.status.success() {
            let name = String::from_utf8_lossy(&output.stdout).trim().to_string();
            // Clean up the name (remove ~N suffixes, etc.)
            let name = name.split('~').next().unwrap_or(&name);
            let name = name.split('^').next().unwrap_or(name);
            return Ok(name.to_string());
        }

        Ok(commit[..8.min(commit.len())].to_string())
    }

    /// Resolve a conflict in a specific file.
    pub async fn resolve_conflict(
        &self,
        worktree_path: &Path,
        file_path: &str,
        resolution: ConflictResolution,
    ) -> Result<(), WorktreeError> {
        if !self.is_merge_in_progress(worktree_path).await? {
            return Err(WorktreeError::NoMergeInProgress);
        }

        let strategy = match resolution {
            ConflictResolution::Ours => "--ours",
            ConflictResolution::Theirs => "--theirs",
        };

        tracing::info!(
            worktree = %worktree_path.display(),
            file = %file_path,
            strategy = %strategy,
            "Resolving conflict"
        );

        // Checkout the chosen version
        let output = Command::new("git")
            .args(["checkout", strategy, "--", file_path])
            .current_dir(worktree_path)
            .output()
            .await?;

        if !output.status.success() {
            let stderr = String::from_utf8_lossy(&output.stderr);
            return Err(WorktreeError::GitCommand(format!(
                "Failed to resolve conflict: {}",
                stderr
            )));
        }

        // Stage the resolved file
        let output = Command::new("git")
            .args(["add", file_path])
            .current_dir(worktree_path)
            .output()
            .await?;

        if !output.status.success() {
            let stderr = String::from_utf8_lossy(&output.stderr);
            return Err(WorktreeError::GitCommand(format!(
                "Failed to stage resolved file: {}",
                stderr
            )));
        }

        Ok(())
    }

    /// Abort the current merge.
    pub async fn abort_merge(&self, worktree_path: &Path) -> Result<(), WorktreeError> {
        if !self.is_merge_in_progress(worktree_path).await? {
            return Err(WorktreeError::NoMergeInProgress);
        }

        tracing::info!(
            worktree = %worktree_path.display(),
            "Aborting merge"
        );

        let output = Command::new("git")
            .args(["merge", "--abort"])
            .current_dir(worktree_path)
            .output()
            .await?;

        if !output.status.success() {
            let stderr = String::from_utf8_lossy(&output.stderr);
            return Err(WorktreeError::GitCommand(format!(
                "Failed to abort merge: {}",
                stderr
            )));
        }

        Ok(())
    }

    /// Commit the current merge.
    pub async fn commit_merge(
        &self,
        worktree_path: &Path,
        message: &str,
    ) -> Result<String, WorktreeError> {
        // Check for remaining conflicts
        let conflicts = self.get_conflicted_files(worktree_path).await?;
        if !conflicts.is_empty() {
            return Err(WorktreeError::MergeConflicts(conflicts.join(", ")));
        }

        tracing::info!(
            worktree = %worktree_path.display(),
            message = %message,
            "Committing merge"
        );

        let output = Command::new("git")
            .args(["commit", "-m", message])
            .current_dir(worktree_path)
            .output()
            .await?;

        if !output.status.success() {
            let stderr = String::from_utf8_lossy(&output.stderr);
            return Err(WorktreeError::GitCommand(format!(
                "Failed to commit merge: {}",
                stderr
            )));
        }

        // Get the new commit SHA
        let output = Command::new("git")
            .args(["rev-parse", "HEAD"])
            .current_dir(worktree_path)
            .output()
            .await?;

        if output.status.success() {
            let sha = String::from_utf8_lossy(&output.stdout).trim().to_string();
            return Ok(sha);
        }

        Ok("unknown".to_string())
    }

    // ========== Completion Action Operations ==========

    /// Push task branch from worktree to an external target repository.
    ///
    /// This stages and commits any uncommitted changes, then pushes to the target repo.
    pub async fn push_to_target_repo(
        &self,
        worktree_path: &Path,
        target_repo: &Path,
        branch_name: &str,
        task_name: &str,
    ) -> Result<(), WorktreeError> {
        tracing::info!(
            worktree = %worktree_path.display(),
            target_repo = %target_repo.display(),
            branch = %branch_name,
            "Pushing branch to target repository"
        );

        // First, stage all changes (including new files)
        let output = Command::new("git")
            .args(["add", "-A"])
            .current_dir(worktree_path)
            .output()
            .await?;

        if !output.status.success() {
            let stderr = String::from_utf8_lossy(&output.stderr);
            return Err(WorktreeError::GitCommand(format!(
                "Failed to stage changes: {}",
                stderr
            )));
        }

        // Check if there are staged changes to commit
        let output = Command::new("git")
            .args(["diff", "--cached", "--quiet"])
            .current_dir(worktree_path)
            .output()
            .await?;

        // Exit code 1 means there are staged changes
        if !output.status.success() {
            tracing::info!("Committing staged changes before push");

            let commit_message = format!("feat: {}", task_name);
            let output = Command::new("git")
                .args(["commit", "-m", &commit_message])
                .current_dir(worktree_path)
                .output()
                .await?;

            if !output.status.success() {
                let stderr = String::from_utf8_lossy(&output.stderr);
                return Err(WorktreeError::GitCommand(format!(
                    "Failed to commit changes: {}",
                    stderr
                )));
            }
        }

        // Ensure there are commits to push
        let output = Command::new("git")
            .args(["log", "--oneline", "-1"])
            .current_dir(worktree_path)
            .output()
            .await?;

        if !output.status.success() {
            return Err(WorktreeError::GitCommand(
                "No commits in worktree".to_string(),
            ));
        }

        // Add target repo as a remote in the worktree (if not already)
        let remote_name = "target";
        let target_path_str = target_repo.to_string_lossy();

        // Remove existing remote if any (ignore errors)
        let _ = Command::new("git")
            .args(["remote", "remove", remote_name])
            .current_dir(worktree_path)
            .output()
            .await;

        // Add the target as a remote
        let output = Command::new("git")
            .args(["remote", "add", remote_name, &target_path_str])
            .current_dir(worktree_path)
            .output()
            .await?;

        if !output.status.success() {
            let stderr = String::from_utf8_lossy(&output.stderr);
            return Err(WorktreeError::GitCommand(format!(
                "Failed to add remote: {}",
                stderr
            )));
        }

        // Push the branch to the target
        let output = Command::new("git")
            .args(["push", "-u", remote_name, &format!("HEAD:{}", branch_name)])
            .current_dir(worktree_path)
            .output()
            .await?;

        if !output.status.success() {
            let stderr = String::from_utf8_lossy(&output.stderr);
            return Err(WorktreeError::GitCommand(format!(
                "Failed to push to target: {}",
                stderr
            )));
        }

        tracing::info!(
            branch = %branch_name,
            target_repo = %target_repo.display(),
            "Branch pushed successfully"
        );

        // Detach HEAD in the worktree to release the branch
        // This allows the branch to be checked out in the target repo
        let output = Command::new("git")
            .args(["checkout", "--detach", "HEAD"])
            .current_dir(worktree_path)
            .output()
            .await?;

        if !output.status.success() {
            // Non-fatal: log but don't fail the push
            let stderr = String::from_utf8_lossy(&output.stderr);
            tracing::warn!(
                "Failed to detach HEAD in worktree (branch may not be checkable in target): {}",
                stderr
            );
        } else {
            tracing::info!("Detached HEAD in worktree to release branch");
        }

        Ok(())
    }

    /// Merge a branch into the target branch in the target repository.
    ///
    /// This pushes the branch first (if needed), then performs a merge in the target repo.
    pub async fn merge_to_target(
        &self,
        worktree_path: &Path,
        target_repo: &Path,
        source_branch: &str,
        target_branch: &str,
        task_name: &str,
    ) -> Result<String, WorktreeError> {
        tracing::info!(
            worktree = %worktree_path.display(),
            target_repo = %target_repo.display(),
            source_branch = %source_branch,
            target_branch = %target_branch,
            "Merging branch to target"
        );

        // First, push the branch to target repo
        self.push_to_target_repo(worktree_path, target_repo, source_branch, task_name)
            .await?;

        // In target repo, checkout the target branch
        let output = Command::new("git")
            .args(["checkout", target_branch])
            .current_dir(target_repo)
            .output()
            .await?;

        if !output.status.success() {
            let stderr = String::from_utf8_lossy(&output.stderr);
            return Err(WorktreeError::GitCommand(format!(
                "Failed to checkout target branch: {}",
                stderr
            )));
        }

        // Pull latest changes first
        let _ = Command::new("git")
            .args(["pull", "--ff-only"])
            .current_dir(target_repo)
            .output()
            .await;

        // Merge the source branch
        let merge_message = format!("feat: {}", task_name);
        let output = Command::new("git")
            .args(["merge", "--no-ff", source_branch, "-m", &merge_message])
            .current_dir(target_repo)
            .output()
            .await?;

        if !output.status.success() {
            let stderr = String::from_utf8_lossy(&output.stderr);

            // Check if it's a conflict
            let conflicts = self.get_conflicted_files(target_repo).await?;
            if !conflicts.is_empty() {
                // Abort the merge
                let _ = Command::new("git")
                    .args(["merge", "--abort"])
                    .current_dir(target_repo)
                    .output()
                    .await;

                return Err(WorktreeError::MergeConflicts(format!(
                    "Merge conflicts in: {}. Consider creating a PR instead.",
                    conflicts.join(", ")
                )));
            }

            return Err(WorktreeError::GitCommand(format!(
                "Failed to merge: {}",
                stderr
            )));
        }

        // Get the merge commit SHA
        let output = Command::new("git")
            .args(["rev-parse", "HEAD"])
            .current_dir(target_repo)
            .output()
            .await?;

        let commit_sha = if output.status.success() {
            String::from_utf8_lossy(&output.stdout).trim().to_string()
        } else {
            "unknown".to_string()
        };

        tracing::info!(
            commit_sha = %commit_sha,
            "Merge completed successfully"
        );

        Ok(commit_sha)
    }

    /// Create a GitHub pull request using the gh CLI.
    ///
    /// This pushes the branch first, then creates a PR.
    /// If target_repo is None, uses the worktree's origin remote directly (for repos already cloned from remote).
    pub async fn create_pull_request(
        &self,
        worktree_path: &Path,
        target_repo: Option<&Path>,
        source_branch: &str,
        target_branch: &str,
        title: &str,
        body: &str,
    ) -> Result<String, WorktreeError> {
        tracing::info!(
            worktree = %worktree_path.display(),
            target_repo = ?target_repo.map(|p| p.display().to_string()),
            source_branch = %source_branch,
            target_branch = %target_branch,
            title = %title,
            "Creating pull request"
        );

        // First, push the branch to the target repo's remote
        // For PRs, we need to push to origin (the GitHub remote)

        // Get the worktree's current branch
        let output = Command::new("git")
            .args(["rev-parse", "--abbrev-ref", "HEAD"])
            .current_dir(worktree_path)
            .output()
            .await?;

        let current_branch = if output.status.success() {
            String::from_utf8_lossy(&output.stdout).trim().to_string()
        } else {
            source_branch.to_string()
        };

        // Get the origin URL - either from target_repo or from worktree directly
        let (origin_url, gh_working_dir) = if let Some(target_repo) = target_repo {
            // Use target_repo's origin
            let output = Command::new("git")
                .args(["remote", "get-url", "origin"])
                .current_dir(target_repo)
                .output()
                .await?;

            if !output.status.success() {
                return Err(WorktreeError::GitCommand(
                    "Target repository has no origin remote configured".to_string(),
                ));
            }

            let url = String::from_utf8_lossy(&output.stdout).trim().to_string();
            (url, target_repo.to_path_buf())
        } else {
            // Check if worktree has an origin remote directly
            let output = Command::new("git")
                .args(["remote", "get-url", "origin"])
                .current_dir(worktree_path)
                .output()
                .await?;

            if !output.status.success() {
                return Err(WorktreeError::GitCommand(
                    "Repository has no origin remote configured. Either set target_repo_path or ensure the worktree was cloned from a remote repository.".to_string(),
                ));
            }

            let url = String::from_utf8_lossy(&output.stdout).trim().to_string();
            (url, worktree_path.to_path_buf())
        };

        // Push the branch from worktree to the remote
        // First add the remote to worktree (if not using worktree's origin directly)
        if target_repo.is_some() {
            let _ = Command::new("git")
                .args(["remote", "remove", "pr-origin"])
                .current_dir(worktree_path)
                .output()
                .await;

            let output = Command::new("git")
                .args(["remote", "add", "pr-origin", &origin_url])
                .current_dir(worktree_path)
                .output()
                .await?;

            if !output.status.success() {
                let stderr = String::from_utf8_lossy(&output.stderr);
                return Err(WorktreeError::GitCommand(format!(
                    "Failed to add remote: {}",
                    stderr
                )));
            }

            // Push to the remote
            let output = Command::new("git")
                .args(["push", "-u", "pr-origin", &format!("{}:{}", current_branch, source_branch)])
                .current_dir(worktree_path)
                .output()
                .await?;

            if !output.status.success() {
                let stderr = String::from_utf8_lossy(&output.stderr);
                return Err(WorktreeError::GitCommand(format!(
                    "Failed to push branch: {}",
                    stderr
                )));
            }
        } else {
            // Push directly to origin
            let output = Command::new("git")
                .args(["push", "-u", "origin", &format!("{}:{}", current_branch, source_branch)])
                .current_dir(worktree_path)
                .output()
                .await?;

            if !output.status.success() {
                let stderr = String::from_utf8_lossy(&output.stderr);
                return Err(WorktreeError::GitCommand(format!(
                    "Failed to push branch: {}",
                    stderr
                )));
            }
        }

        // Create PR using gh CLI
        let output = Command::new("gh")
            .args([
                "pr",
                "create",
                "--title", title,
                "--body", body,
                "--head", source_branch,
                "--base", target_branch,
            ])
            .current_dir(&gh_working_dir)
            .output()
            .await?;

        if !output.status.success() {
            let stderr = String::from_utf8_lossy(&output.stderr);
            return Err(WorktreeError::GitCommand(format!(
                "Failed to create PR: {}",
                stderr
            )));
        }

        // The gh CLI outputs the PR URL
        let pr_url = String::from_utf8_lossy(&output.stdout).trim().to_string();

        tracing::info!(
            pr_url = %pr_url,
            "Pull request created successfully"
        );

        Ok(pr_url)
    }

    /// Clone/copy the worktree contents to a target directory.
    ///
    /// This creates a new git repository at the target path with the same contents
    /// as the worktree. Returns (success, message).
    pub async fn clone_worktree_to_directory(
        &self,
        worktree_path: &Path,
        target_dir: &Path,
    ) -> Result<String, WorktreeError> {
        tracing::info!(
            worktree = %worktree_path.display(),
            target = %target_dir.display(),
            "Cloning worktree to target directory"
        );

        // Check if target directory already exists
        if target_dir.exists() {
            return Err(WorktreeError::AlreadyExists(format!(
                "Target directory already exists: {}",
                target_dir.display()
            )));
        }

        // Get parent directory to ensure it exists
        if let Some(parent) = target_dir.parent() {
            if !parent.exists() {
                tokio::fs::create_dir_all(parent).await?;
            }
        }

        // Use git clone --local to efficiently copy the repository
        // This is more efficient than cp -r for git repos
        let output = Command::new("git")
            .args([
                "clone",
                "--local",
                "--no-hardlinks",
                &worktree_path.to_string_lossy(),
                &target_dir.to_string_lossy(),
            ])
            .output()
            .await?;

        if !output.status.success() {
            let stderr = String::from_utf8_lossy(&output.stderr);
            return Err(WorktreeError::CloneFailed(format!(
                "Failed to clone worktree: {}",
                stderr
            )));
        }

        // Remove the 'origin' remote that points back to the worktree
        let _ = Command::new("git")
            .args(["remote", "remove", "origin"])
            .current_dir(target_dir)
            .output()
            .await;

        tracing::info!(
            target = %target_dir.display(),
            "Worktree cloned successfully"
        );

        Ok(format!("Cloned to {}", target_dir.display()))
    }

    /// Check if a target directory exists.
    pub async fn target_directory_exists(&self, target_dir: &Path) -> bool {
        target_dir.exists()
    }

    /// Restore a worktree from a stored patch.
    ///
    /// This is used for task recovery when the local worktree has been lost.
    /// 1. Clone/fetch the source repo to get the base commit
    /// 2. Create a new worktree at the base commit
    /// 3. Apply the patch to restore the task's state
    pub async fn restore_from_patch(
        &self,
        source_repo: &str,
        task_id: Uuid,
        task_name: &str,
        base_commit_sha: &str,
        patch_data: &[u8],
    ) -> Result<WorktreeInfo, WorktreeError> {
        use crate::daemon::storage;

        // Generate directory and branch names
        let dir_name = format!("{}-{}", short_uuid(task_id), sanitize_name(task_name));
        let worktree_path = self.base_dir.join(&dir_name);
        let branch_name = format!(
            "{}{}-{}",
            self.branch_prefix,
            sanitize_name(task_name),
            short_uuid(task_id)
        );

        // Ensure base directory exists
        tokio::fs::create_dir_all(&self.base_dir).await?;

        // Remove existing worktree if present (we're restoring from scratch)
        if worktree_path.exists() {
            tracing::info!(
                task_id = %task_id,
                worktree_path = %worktree_path.display(),
                "Removing existing worktree before restore"
            );
            tokio::fs::remove_dir_all(&worktree_path).await?;
        }

        // Clone the source repo if needed
        let repo_path = self.ensure_repo(source_repo).await?;

        // Create worktree at the base commit
        // First, we need to make sure the base commit is available
        let fetch_output = Command::new("git")
            .args(["fetch", "--all"])
            .current_dir(&repo_path)
            .output()
            .await?;

        if !fetch_output.status.success() {
            tracing::warn!(
                task_id = %task_id,
                stderr = %String::from_utf8_lossy(&fetch_output.stderr),
                "git fetch failed, continuing anyway"
            );
        }

        // Create the worktree from the base commit
        let output = Command::new("git")
            .args([
                "worktree",
                "add",
                "-b",
                &branch_name,
                worktree_path.to_str().ok_or_else(|| {
                    WorktreeError::InvalidPath("Invalid worktree path".to_string())
                })?,
                base_commit_sha,
            ])
            .current_dir(&repo_path)
            .output()
            .await?;

        if !output.status.success() {
            let stderr = String::from_utf8_lossy(&output.stderr);
            // If branch already exists, try without -b flag
            if stderr.contains("already exists") {
                // Remove the branch and try again
                let _ = Command::new("git")
                    .args(["branch", "-D", &branch_name])
                    .current_dir(&repo_path)
                    .output()
                    .await;

                let retry_output = Command::new("git")
                    .args([
                        "worktree",
                        "add",
                        "-b",
                        &branch_name,
                        worktree_path.to_str().unwrap(),
                        base_commit_sha,
                    ])
                    .current_dir(&repo_path)
                    .output()
                    .await?;

                if !retry_output.status.success() {
                    return Err(WorktreeError::GitCommand(format!(
                        "Failed to create worktree after retry: {}",
                        String::from_utf8_lossy(&retry_output.stderr)
                    )));
                }
            } else {
                return Err(WorktreeError::GitCommand(format!(
                    "Failed to create worktree: {}",
                    stderr
                )));
            }
        }

        // Apply the patch to restore the task's state
        if let Err(e) = storage::apply_patch(&worktree_path, patch_data).await {
            tracing::error!(
                task_id = %task_id,
                error = %e,
                "Failed to apply patch, worktree is at base commit"
            );
            // Don't fail - the worktree is usable, just at the base commit
        } else {
            tracing::info!(
                task_id = %task_id,
                worktree_path = %worktree_path.display(),
                "Successfully restored worktree from patch"
            );
        }

        Ok(WorktreeInfo {
            path: worktree_path,
            branch: branch_name,
            source_repo: repo_path,
        })
    }
}

/// Check if repo_source is a "new repo" request.
///
/// Accepts `new://` or `new://project-name` to create a fresh git repository.
pub fn is_new_repo_request(source: &str) -> bool {
    source == "new" || source == "new://" || source.starts_with("new://")
}

/// Extract optional project name from new:// URL.
fn extract_new_repo_name(source: &str) -> Option<&str> {
    source.strip_prefix("new://").filter(|s| !s.is_empty())
}

/// Extract repository name from URL.
fn extract_repo_name(url: &str) -> String {
    // Handle various URL formats:
    // https://github.com/user/repo.git -> repo
    // git@github.com:user/repo.git -> repo
    // https://github.com/user/repo -> repo

    let url = url.trim_end_matches('/');
    let url = url.trim_end_matches(".git");

    url.rsplit('/')
        .next()
        .or_else(|| url.rsplit(':').next())
        .unwrap_or("repo")
        .to_string()
}

/// Create a short UUID string for directory naming.
pub fn short_uuid(id: Uuid) -> String {
    id.to_string()[..8].to_string()
}

/// Expand tilde (~) in path to home directory.
pub fn expand_tilde(path: &str) -> PathBuf {
    if let Some(rest) = path.strip_prefix("~/") {
        if let Some(home) = dirs::home_dir() {
            return home.join(rest);
        }
    } else if path == "~" {
        if let Some(home) = dirs::home_dir() {
            return home;
        }
    }
    PathBuf::from(path)
}

/// Sanitize a name for use in directory/branch names.
pub fn sanitize_name(name: &str) -> String {
    name.chars()
        .map(|c| {
            if c.is_alphanumeric() || c == '-' || c == '_' {
                c.to_ascii_lowercase()
            } else {
                '-'
            }
        })
        .collect::<String>()
        .chars()
        .take(50) // Limit length
        .collect()
}

#[cfg(test)]
mod tests {
    use super::*;
    use uuid::Uuid;

    #[test]
    fn test_extract_repo_name() {
        assert_eq!(
            extract_repo_name("https://github.com/user/repo.git"),
            "repo"
        );
        assert_eq!(
            extract_repo_name("https://github.com/user/repo"),
            "repo"
        );
        assert_eq!(
            extract_repo_name("git@github.com:user/repo.git"),
            "repo"
        );
    }

    #[test]
    fn test_sanitize_name() {
        assert_eq!(sanitize_name("Hello World!"), "hello-world-");
        assert_eq!(sanitize_name("test_name-123"), "test_name-123");
        assert_eq!(sanitize_name("A".repeat(100).as_str()).len(), 50);
    }

    #[test]
    fn test_short_uuid() {
        let id = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
        assert_eq!(short_uuid(id), "550e8400");
    }
}