git_worktree_cli/commands/
list.rs

1use colored::Colorize;
2
3use super::list_helpers::{
4    extract_bitbucket_cloud_url, extract_bitbucket_data_center_url, fetch_pr_for_branch, PullRequestInfo,
5};
6use crate::{
7    bitbucket_api, bitbucket_auth, bitbucket_data_center_api, bitbucket_data_center_auth, config,
8    core::project::{clean_branch_name, find_git_directory},
9    error::Result,
10    git, github,
11};
12
13struct WorktreeDisplay {
14    branch: String,
15    pr_info: Option<PullRequestInfo>,
16}
17
18struct RemotePullRequest {
19    branch: String,
20    pr_info: PullRequestInfo,
21}
22
23#[tokio::main]
24pub async fn run(local_only: bool) -> Result<()> {
25    // Find a git directory to work with
26    let git_dir = find_git_directory()?;
27
28    // Get the list of worktrees
29    let worktrees = git::list_worktrees(Some(&git_dir))?;
30
31    if worktrees.is_empty() {
32        println!("{}", "No worktrees found.".yellow());
33        return Ok(());
34    }
35
36    // Try to get GitHub/Bitbucket info automatically
37    let (github_client, bitbucket_client, bitbucket_data_center_client, repo_info) = {
38        let github_client = github::GitHubClient::new();
39        let mut bitbucket_client: Option<bitbucket_api::BitbucketClient> = None;
40        let mut bitbucket_data_center_client: Option<bitbucket_data_center_api::BitbucketDataCenterClient> = None;
41
42        if let Some((_, config)) = config::GitWorktreeConfig::find_config()? {
43            let repo_url = &config.repository_url;
44
45            // Use the configured sourceControl instead of URL pattern matching
46            match config.source_control.as_str() {
47                "bitbucket-cloud" => {
48                    if let Some((workspace, repo)) = bitbucket_api::extract_bitbucket_info_from_url(repo_url) {
49                        // Try to get Bitbucket Cloud auth
50                        if let Ok(auth) = bitbucket_auth::BitbucketAuth::new(
51                            workspace.clone(),
52                            repo.clone(),
53                            config.bitbucket_email.clone(),
54                        ) {
55                            if auth.has_stored_token() {
56                                bitbucket_client = Some(bitbucket_api::BitbucketClient::new(auth));
57                            }
58                        }
59                        (
60                            Some(github_client),
61                            bitbucket_client,
62                            None,
63                            Some(("bitbucket-cloud".to_string(), workspace, repo)),
64                        )
65                    } else {
66                        (Some(github_client), None, None, None)
67                    }
68                }
69                "bitbucket-data-center" => {
70                    // Always use get_auth_from_config for bitbucket-data-center since it can derive the API URL
71                    if let Ok((base_url, project_key, repo_slug)) = bitbucket_data_center_auth::get_auth_from_config() {
72                        if let Ok(auth) = bitbucket_data_center_auth::BitbucketDataCenterAuth::new(
73                            project_key.clone(),
74                            repo_slug.clone(),
75                            base_url.clone(),
76                        ) {
77                            if auth.get_token().is_ok() {
78                                bitbucket_data_center_client = Some(
79                                    bitbucket_data_center_api::BitbucketDataCenterClient::new(auth, base_url),
80                                );
81                            }
82                        }
83                        (
84                            Some(github_client),
85                            None,
86                            bitbucket_data_center_client,
87                            Some(("bitbucket-data-center".to_string(), project_key, repo_slug)),
88                        )
89                    } else {
90                        // Could not get auth config - extract repo info for display but no client
91                        let (owner, repo) = github::GitHubClient::parse_github_url(repo_url)
92                            .unwrap_or_else(|| ("".to_string(), "".to_string()));
93                        if !owner.is_empty() && !repo.is_empty() {
94                            (
95                                Some(github_client),
96                                None,
97                                None,
98                                Some(("bitbucket-data-center".to_string(), owner, repo)),
99                            )
100                        } else {
101                            (Some(github_client), None, None, None)
102                        }
103                    }
104                }
105                _ => {
106                    // Try GitHub
107                    let (owner, repo) = github::GitHubClient::parse_github_url(repo_url)
108                        .unwrap_or_else(|| ("".to_string(), "".to_string()));
109
110                    if !owner.is_empty() && !repo.is_empty() {
111                        (
112                            Some(github_client),
113                            None,
114                            None,
115                            Some(("github".to_string(), owner, repo)),
116                        )
117                    } else {
118                        (Some(github_client), None, None, None)
119                    }
120                }
121            }
122        } else {
123            (Some(github_client), None, None, None)
124        }
125    };
126
127    let has_pr_info = repo_info.is_some()
128        && match &repo_info {
129            Some((platform, _, _)) => match platform.as_str() {
130                "github" => github_client.as_ref().map(|c| c.has_auth()).unwrap_or(false),
131                "bitbucket-cloud" => bitbucket_client.is_some(),
132                "bitbucket-data-center" => bitbucket_data_center_client.is_some(),
133                _ => false,
134            },
135            None => false,
136        };
137
138    // Get local branch names for filtering
139    let local_branches: Vec<String> = worktrees
140        .iter()
141        .filter_map(|wt| wt.branch.as_ref().map(|b| clean_branch_name(b).to_string()))
142        .collect();
143
144    // Convert to display format
145    let mut display_worktrees: Vec<WorktreeDisplay> = Vec::new();
146
147    for wt in &worktrees {
148        let branch = wt
149            .branch
150            .as_ref()
151            .map(|b| clean_branch_name(b).to_string())
152            .unwrap_or_else(|| {
153                if wt.bare {
154                    "(bare)".to_string()
155                } else {
156                    wt.head.chars().take(8).collect()
157                }
158            });
159
160        // Fetch PR info if available
161        let pr_info = if has_pr_info && !wt.bare && branch != "(bare)" {
162            match &repo_info {
163                Some((platform, owner_or_workspace, repo)) => {
164                    let pr_result = fetch_pr_for_branch(
165                        platform,
166                        owner_or_workspace,
167                        repo,
168                        &branch,
169                        &github_client,
170                        &bitbucket_client,
171                        &bitbucket_data_center_client,
172                    )
173                    .await;
174
175                    pr_result.unwrap_or_default()
176                }
177                None => None,
178            }
179        } else {
180            None
181        };
182
183        display_worktrees.push(WorktreeDisplay { branch, pr_info });
184    }
185
186    // Display local worktrees
187    if !display_worktrees.is_empty() {
188        println!("{}", "Local Worktrees:".bold());
189        println!();
190
191        for worktree in &display_worktrees {
192            display_worktree(worktree);
193        }
194    }
195
196    // Fetch all open pull requests and add ones that don't have local worktrees
197    let mut remote_prs: Vec<RemotePullRequest> = Vec::new();
198
199    if has_pr_info && !local_only {
200        if let Some((platform, owner_or_workspace, repo)) = &repo_info {
201            match platform.as_str() {
202                "github" => {
203                    if let Some(ref client) = github_client {
204                        if let Ok(all_prs) = client.get_all_pull_requests(owner_or_workspace, repo) {
205                            for (pr, branch_name) in all_prs {
206                                // Skip if we already have a local worktree for this branch
207                                if !local_branches.contains(&branch_name) {
208                                    let status = if pr.draft { "DRAFT" } else { "OPEN" };
209                                    remote_prs.push(RemotePullRequest {
210                                        branch: branch_name,
211                                        pr_info: PullRequestInfo {
212                                            url: pr.html_url,
213                                            status: status.to_string(),
214                                            title: pr.title.clone(),
215                                        },
216                                    });
217                                }
218                            }
219                        }
220                    }
221                }
222                "bitbucket-cloud" => {
223                    if let Some(ref client) = bitbucket_client {
224                        if let Ok(all_prs) = client.get_pull_requests(owner_or_workspace, repo).await {
225                            for pr in all_prs {
226                                // Only include open PRs
227                                if pr.state == "OPEN" {
228                                    let branch_name = pr.source.branch.name.clone();
229                                    // Skip if we already have a local worktree for this branch
230                                    if !local_branches.contains(&branch_name) {
231                                        let url = extract_bitbucket_cloud_url(&pr);
232                                        remote_prs.push(RemotePullRequest {
233                                            branch: branch_name,
234                                            pr_info: PullRequestInfo {
235                                                url,
236                                                status: "OPEN".to_string(),
237                                                title: pr.title.clone(),
238                                            },
239                                        });
240                                    }
241                                }
242                            }
243                        }
244                    }
245                }
246                "bitbucket-data-center" => {
247                    if let Some(ref client) = bitbucket_data_center_client {
248                        if let Ok(all_prs) = client.get_pull_requests(owner_or_workspace, repo).await {
249                            for pr in all_prs {
250                                // Only include open PRs
251                                if pr.state == "OPEN" {
252                                    let branch_name = pr.from_ref.display_id.clone();
253                                    // Skip if we already have a local worktree for this branch
254                                    if !local_branches.contains(&branch_name) {
255                                        let status = if pr.draft.unwrap_or(false) { "DRAFT" } else { "OPEN" };
256                                        let url = extract_bitbucket_data_center_url(&pr);
257                                        remote_prs.push(RemotePullRequest {
258                                            branch: branch_name,
259                                            pr_info: PullRequestInfo {
260                                                url,
261                                                status: status.to_string(),
262                                                title: pr.title.clone(),
263                                            },
264                                        });
265                                    }
266                                }
267                            }
268                        }
269                    }
270                }
271                _ => {}
272            }
273        }
274    }
275
276    // Display remote PRs if any exist
277    if !remote_prs.is_empty() && !local_only {
278        if !display_worktrees.is_empty() {
279            println!(); // Add spacing between sections
280        }
281        println!("{}", "Open Pull Requests (no local worktree):".bold());
282        println!();
283
284        for pr in &remote_prs {
285            display_remote_pr(pr);
286        }
287    }
288
289    if !has_pr_info && !local_only {
290        if let Some((_, config)) = config::GitWorktreeConfig::find_config()? {
291            match config.source_control.as_str() {
292                "bitbucket-cloud" => {
293                    println!(
294                        "\n{}",
295                        "Tip: Run 'gwt auth bitbucket-cloud setup' to enable Bitbucket Cloud pull request information"
296                            .dimmed()
297                    );
298                }
299                "bitbucket-data-center" => {
300                    println!("\n{}", "Tip: Run 'gwt auth bitbucket-data-center setup' to enable Bitbucket Data Center pull request information".dimmed());
301                }
302                _ => {
303                    println!(
304                        "\n{}",
305                        "Tip: Run 'gh auth login' to enable GitHub pull request information".dimmed()
306                    );
307                }
308            }
309        }
310    }
311
312    Ok(())
313}
314
315fn display_worktree(worktree: &WorktreeDisplay) {
316    // Display branch name in cyan
317    println!("{}", worktree.branch.cyan());
318
319    // Display PR info if available
320    if let Some(ref pr_info) = worktree.pr_info {
321        // Display URL with status
322        let status_colored = match pr_info.status.as_str() {
323            "OPEN" => "open".green(),
324            "CLOSED" => "closed".red(),
325            "MERGED" => "merged".green(),
326            "DRAFT" => "draft".yellow(),
327            _ => pr_info.status.normal(),
328        };
329        println!("  {} ({})", pr_info.url.blue().underline(), status_colored);
330
331        // Display title if not empty
332        if !pr_info.title.is_empty() {
333            println!("  {}", pr_info.title.dimmed());
334        }
335    }
336    println!(); // Empty line between worktrees
337}
338
339fn display_remote_pr(pr: &RemotePullRequest) {
340    // Display branch name in cyan
341    println!("{}", pr.branch.cyan());
342
343    // Display URL with status
344    let status_colored = match pr.pr_info.status.as_str() {
345        "OPEN" => "open".green(),
346        "CLOSED" => "closed".red(),
347        "MERGED" => "merged".green(),
348        "DRAFT" => "draft".yellow(),
349        _ => pr.pr_info.status.normal(),
350    };
351    println!("  {} ({})", pr.pr_info.url.blue().underline(), status_colored);
352
353    // Display title
354    if !pr.pr_info.title.is_empty() {
355        println!("  {}", pr.pr_info.title.dimmed());
356    }
357    println!(); // Empty line between PRs
358}