git_x/commands/
repository.rs1use crate::core::traits::*;
2use crate::core::{git::*, output::*};
3use crate::{GitXError, Result};
4
5pub struct RepositoryCommands;
7
8impl RepositoryCommands {
9 pub fn info() -> Result<String> {
11 InfoCommand::new().execute()
12 }
13
14 pub fn health() -> Result<String> {
16 HealthCommand::new().execute()
17 }
18
19 pub fn sync(strategy: SyncStrategy) -> Result<String> {
21 SyncCommand::new(strategy).execute()
22 }
23
24 pub fn upstream(action: UpstreamAction) -> Result<String> {
26 UpstreamCommand::new(action).execute()
27 }
28
29 pub fn what(target: Option<String>) -> Result<String> {
31 WhatCommand::new(target).execute()
32 }
33}
34
35pub struct InfoCommand {
37 show_detailed: bool,
38}
39
40impl Default for InfoCommand {
41 fn default() -> Self {
42 Self::new()
43 }
44}
45
46impl InfoCommand {
47 pub fn new() -> Self {
48 Self {
49 show_detailed: false,
50 }
51 }
52
53 pub fn with_details(mut self) -> Self {
54 self.show_detailed = true;
55 self
56 }
57
58 fn format_branch_info(
59 current: &str,
60 upstream: Option<&str>,
61 ahead: u32,
62 behind: u32,
63 ) -> String {
64 let mut info = format!("š Current branch: {}", Format::bold(current));
65
66 if let Some(upstream_branch) = upstream {
67 info.push_str(&format!("\nš Upstream: {upstream_branch}"));
68
69 if ahead > 0 || behind > 0 {
70 let mut status_parts = Vec::new();
71 if ahead > 0 {
72 status_parts.push(format!("{ahead} ahead"));
73 }
74 if behind > 0 {
75 status_parts.push(format!("{behind} behind"));
76 }
77 info.push_str(&format!("\nš Status: {}", status_parts.join(", ")));
78 } else {
79 info.push_str("\nā
Status: Up to date");
80 }
81 } else {
82 info.push_str("\nā No upstream configured");
83 }
84
85 info
86 }
87}
88
89impl Command for InfoCommand {
90 fn execute(&self) -> Result<String> {
91 let mut output = BufferedOutput::new();
92
93 let repo_name = match GitOperations::repo_root() {
95 Ok(path) => std::path::Path::new(&path)
96 .file_name()
97 .map(|s| s.to_string_lossy().to_string())
98 .unwrap_or_else(|| "Unknown".to_string()),
99 Err(_) => return Err(GitXError::GitCommand("Not in a git repository".to_string())),
100 };
101
102 output.add_line(format!("šļø Repository: {}", Format::bold(&repo_name)));
103
104 let (current, upstream, ahead, behind) = GitOperations::branch_info_optimized()?;
106 output.add_line(Self::format_branch_info(
107 ¤t,
108 upstream.as_deref(),
109 ahead,
110 behind,
111 ));
112
113 if GitOperations::is_working_directory_clean()? {
115 output.add_line("ā
Working directory: Clean".to_string());
116 } else {
117 output.add_line("ā ļø Working directory: Has changes".to_string());
118 }
119
120 let staged = GitOperations::staged_files()?;
122 if staged.is_empty() {
123 output.add_line("š Staged files: None".to_string());
124 } else {
125 output.add_line(format!("š Staged files: {} file(s)", staged.len()));
126 if self.show_detailed {
127 for file in staged {
128 output.add_line(format!(" ⢠{file}"));
129 }
130 }
131 }
132
133 if self.show_detailed {
135 match GitOperations::recent_branches(Some(5)) {
136 Ok(recent) if !recent.is_empty() => {
137 output.add_line("\nš Recent branches:".to_string());
138 for (i, branch) in recent.iter().enumerate() {
139 let prefix = if i == 0 { "š" } else { "š" };
140 output.add_line(format!(" {prefix} {branch}"));
141 }
142 }
143 _ => {}
144 }
145 }
146
147 Ok(output.content())
148 }
149
150 fn name(&self) -> &'static str {
151 "info"
152 }
153
154 fn description(&self) -> &'static str {
155 "Show repository information and status"
156 }
157}
158
159impl GitCommand for InfoCommand {}
160
161pub struct HealthCommand;
163
164impl Default for HealthCommand {
165 fn default() -> Self {
166 Self::new()
167 }
168}
169
170impl HealthCommand {
171 pub fn new() -> Self {
172 Self
173 }
174
175 fn check_git_config() -> Vec<String> {
176 let mut issues = Vec::new();
177
178 if GitOperations::run(&["config", "user.name"]).is_err() {
180 issues.push("ā Git user.name not configured".to_string());
181 }
182 if GitOperations::run(&["config", "user.email"]).is_err() {
183 issues.push("ā Git user.email not configured".to_string());
184 }
185
186 issues
187 }
188
189 fn check_remotes() -> Vec<String> {
190 let mut issues = Vec::new();
191
192 match RemoteOperations::list() {
193 Ok(remotes) => {
194 if remotes.is_empty() {
195 issues.push("ā ļø No remotes configured".to_string());
196 }
197 }
198 Err(_) => {
199 issues.push("ā Could not check remotes".to_string());
200 }
201 }
202
203 issues
204 }
205
206 fn check_branches() -> Vec<String> {
207 let mut issues = Vec::new();
208
209 match GitOperations::local_branches() {
211 Ok(branches) => {
212 if branches.len() > 20 {
213 issues.push(format!(
214 "ā ļø Many local branches ({}) - consider cleaning up",
215 branches.len()
216 ));
217 }
218 }
219 Err(_) => {
220 issues.push("ā Could not check branches".to_string());
221 }
222 }
223
224 issues
225 }
226}
227
228impl Command for HealthCommand {
229 fn execute(&self) -> Result<String> {
230 let mut output = BufferedOutput::new();
231 output.add_line("š„ Repository Health Check".to_string());
232 output.add_line("=".repeat(30));
233
234 let mut all_issues = Vec::new();
235
236 let config_issues = Self::check_git_config();
238 if config_issues.is_empty() {
239 output.add_line("ā
Git configuration: OK".to_string());
240 } else {
241 output.add_line("ā Git configuration: Issues found".to_string());
242 all_issues.extend(config_issues);
243 }
244
245 let remote_issues = Self::check_remotes();
247 if remote_issues.is_empty() {
248 output.add_line("ā
Remotes: OK".to_string());
249 } else {
250 output.add_line("ā ļø Remotes: Issues found".to_string());
251 all_issues.extend(remote_issues);
252 }
253
254 let branch_issues = Self::check_branches();
256 if branch_issues.is_empty() {
257 output.add_line("ā
Branches: OK".to_string());
258 } else {
259 output.add_line("ā ļø Branches: Issues found".to_string());
260 all_issues.extend(branch_issues);
261 }
262
263 if all_issues.is_empty() {
265 output.add_line("\nš Repository is healthy!".to_string());
266 } else {
267 output.add_line(format!("\nš§ Found {} issue(s):", all_issues.len()));
268 for issue in all_issues {
269 output.add_line(format!(" {issue}"));
270 }
271 }
272
273 Ok(output.content())
274 }
275
276 fn name(&self) -> &'static str {
277 "health"
278 }
279
280 fn description(&self) -> &'static str {
281 "Check repository health and configuration"
282 }
283}
284
285impl GitCommand for HealthCommand {}
286
287#[derive(Debug, Clone)]
289pub enum SyncStrategy {
290 Merge,
291 Rebase,
292 Auto,
293}
294
295pub struct SyncCommand {
297 strategy: SyncStrategy,
298}
299
300impl SyncCommand {
301 pub fn new(strategy: SyncStrategy) -> Self {
302 Self { strategy }
303 }
304}
305
306impl Command for SyncCommand {
307 fn execute(&self) -> Result<String> {
308 RemoteOperations::fetch(None)?;
310
311 let (current_branch, upstream, ahead, behind) = GitOperations::branch_info_optimized()?;
312
313 let upstream_branch = upstream.ok_or_else(|| {
314 GitXError::GitCommand(format!(
315 "No upstream configured for branch '{current_branch}'"
316 ))
317 })?;
318
319 if behind == 0 {
320 return Ok("ā
Already up to date with upstream".to_string());
321 }
322
323 let strategy_name = match self.strategy {
324 SyncStrategy::Merge => "merge",
325 SyncStrategy::Rebase => "rebase",
326 SyncStrategy::Auto => {
327 if ahead == 0 { "merge" } else { "rebase" }
329 }
330 };
331
332 match strategy_name {
334 "merge" => {
335 GitOperations::run_status(&["merge", &upstream_branch])?;
336 Ok(format!("ā
Merged {behind} commits from {upstream_branch}"))
337 }
338 "rebase" => {
339 GitOperations::run_status(&["rebase", &upstream_branch])?;
340 Ok(format!("ā
Rebased {ahead} commits onto {upstream_branch}"))
341 }
342 _ => unreachable!(),
343 }
344 }
345
346 fn name(&self) -> &'static str {
347 "sync"
348 }
349
350 fn description(&self) -> &'static str {
351 "Sync current branch with upstream"
352 }
353}
354
355impl GitCommand for SyncCommand {}
356
357#[derive(Debug, Clone)]
359pub enum UpstreamAction {
360 Set { remote: String, branch: String },
361 Status,
362 SyncAll,
363}
364
365pub struct UpstreamCommand {
367 action: UpstreamAction,
368}
369
370impl UpstreamCommand {
371 pub fn new(action: UpstreamAction) -> Self {
372 Self { action }
373 }
374}
375
376impl Command for UpstreamCommand {
377 fn execute(&self) -> Result<String> {
378 match &self.action {
379 UpstreamAction::Set { remote, branch } => {
380 RemoteOperations::set_upstream(remote, branch)?;
381 Ok(format!("ā
Set upstream to {remote}/{branch}"))
382 }
383 UpstreamAction::Status => {
384 let branches = GitOperations::local_branches()?;
385 let mut output = BufferedOutput::new();
386 output.add_line("š Upstream Status:".to_string());
387
388 for branch in branches {
389 output.add_line(format!("š {branch}: (checking...)"));
392 }
393
394 Ok(output.content())
395 }
396 UpstreamAction::SyncAll => {
397 let current_branch = GitOperations::current_branch()?;
398 let branches = GitOperations::local_branches()?;
399 let mut synced = 0;
400
401 for branch in branches {
402 if branch == current_branch {
403 continue; }
405
406 if BranchOperations::switch(&branch).is_ok()
408 && SyncCommand::new(SyncStrategy::Auto).execute().is_ok()
409 {
410 synced += 1;
411 }
412 }
413
414 BranchOperations::switch(¤t_branch)?;
416
417 Ok(format!("ā
Synced {synced} branches"))
418 }
419 }
420 }
421
422 fn name(&self) -> &'static str {
423 "upstream"
424 }
425
426 fn description(&self) -> &'static str {
427 "Manage upstream branch configuration"
428 }
429}
430
431impl GitCommand for UpstreamCommand {}
432
433pub struct WhatCommand {
435 target: Option<String>,
436}
437
438impl WhatCommand {
439 pub fn new(target: Option<String>) -> Self {
440 Self { target }
441 }
442}
443
444impl Command for WhatCommand {
445 fn execute(&self) -> Result<String> {
446 let (current_branch, upstream, ahead, behind) = GitOperations::branch_info_optimized()?;
447 let mut output = BufferedOutput::new();
448
449 let target_ref = self
450 .target
451 .as_deref()
452 .unwrap_or_else(|| upstream.as_deref().unwrap_or("origin/main"));
453
454 output.add_line(format!("š Comparing {current_branch} with {target_ref}"));
455 output.add_line("=".repeat(50));
456
457 if ahead > 0 {
459 output.add_line(format!("š¤ {ahead} commit(s) to push:"));
460 match GitOperations::run(&["log", "--oneline", &format!("{target_ref}..HEAD")]) {
461 Ok(commits) => {
462 for line in commits.lines() {
463 output.add_line(format!(" ⢠{line}"));
464 }
465 }
466 Err(_) => {
467 output.add_line(" (Could not retrieve commit details)".to_string());
468 }
469 }
470 } else {
471 output.add_line("š¤ No commits to push".to_string());
472 }
473
474 if behind > 0 {
476 output.add_line(format!("\nš„ {behind} commit(s) to pull:"));
477 match GitOperations::run(&["log", "--oneline", &format!("HEAD..{target_ref}")]) {
478 Ok(commits) => {
479 for line in commits.lines() {
480 output.add_line(format!(" ⢠{line}"));
481 }
482 }
483 Err(_) => {
484 output.add_line(" (Could not retrieve commit details)".to_string());
485 }
486 }
487 } else {
488 output.add_line("\nš„ No commits to pull".to_string());
489 }
490
491 Ok(output.content())
492 }
493
494 fn name(&self) -> &'static str {
495 "what"
496 }
497
498 fn description(&self) -> &'static str {
499 "Show what would be pushed or pulled"
500 }
501}
502
503impl GitCommand for WhatCommand {}