git_worktree_cli/commands/
list.rs1use 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 let git_dir = find_git_directory()?;
27
28 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 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 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 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 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 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 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 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 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 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 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 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 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 if pr.state == "OPEN" {
228 let branch_name = pr.source.branch.name.clone();
229 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 if pr.state == "OPEN" {
252 let branch_name = pr.from_ref.display_id.clone();
253 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 if !remote_prs.is_empty() && !local_only {
278 if !display_worktrees.is_empty() {
279 println!(); }
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 println!("{}", worktree.branch.cyan());
318
319 if let Some(ref pr_info) = worktree.pr_info {
321 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 if !pr_info.title.is_empty() {
333 println!(" {}", pr_info.title.dimmed());
334 }
335 }
336 println!(); }
338
339fn display_remote_pr(pr: &RemotePullRequest) {
340 println!("{}", pr.branch.cyan());
342
343 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 if !pr.pr_info.title.is_empty() {
355 println!(" {}", pr.pr_info.title.dimmed());
356 }
357 println!(); }