Skip to main content

nargo_changes/
vcs.rs

1#![warn(missing_docs)]
2
3use nargo_types::{Error, Result, Span};
4use std::{path::{Path, PathBuf}, process::Command};
5
6use crate::types::VcsType;
7
8/// Version control system integration.
9pub struct VcsIntegration {
10    /// The base directory of the repository.
11    pub repo_dir: PathBuf,
12    /// The detected version control system type.
13    pub vcs_type: VcsType,
14}
15
16impl VcsIntegration {
17    /// Creates a new VCS integration.
18    pub fn new(repo_dir: &Path) -> Self {
19        let vcs_type = Self::detect_vcs(repo_dir);
20        Self { repo_dir: repo_dir.to_path_buf(), vcs_type }
21    }
22
23    /// Detects the version control system type in the given directory.
24    pub fn detect_vcs(repo_dir: &Path) -> VcsType {
25        if repo_dir.join(".git").exists() {
26            VcsType::Git
27        }
28        else if repo_dir.join(".svn").exists() {
29            VcsType::Svn
30        }
31        else if repo_dir.join(".hg").exists() {
32            VcsType::Mercurial
33        }
34        else {
35            VcsType::None
36        }
37    }
38
39    /// Gets the detected version control system type.
40    pub fn get_vcs_type(&self) -> VcsType {
41        self.vcs_type.clone()
42    }
43
44    /// Checks if the directory is a git repository.
45    pub fn is_git_repo(&self) -> bool {
46        self.repo_dir.join(".git").exists()
47    }
48
49    /// Checks if the directory is an SVN repository.
50    pub fn is_svn_repo(&self) -> bool {
51        self.repo_dir.join(".svn").exists()
52    }
53
54    /// Checks if the directory is a Mercurial repository.
55    pub fn is_hg_repo(&self) -> bool {
56        self.repo_dir.join(".hg").exists()
57    }
58
59    /// Gets the current branch for the detected VCS.
60    pub fn get_current_branch(&self) -> Result<String> {
61        match self.vcs_type {
62            VcsType::Git => self.get_git_branch(),
63            VcsType::Svn => self.get_svn_branch(),
64            VcsType::Mercurial => self.get_hg_branch(),
65            VcsType::None => Err(Error::external_error("vcs".to_string(), "No version control system detected".to_string(), Span::unknown())),
66        }
67    }
68
69    /// Gets the current git branch.
70    pub fn get_git_branch(&self) -> Result<String> {
71        let output = Command::new("git").arg("branch").arg("--show-current").current_dir(&self.repo_dir).output()?;
72
73        if !output.status.success() {
74            return Err(Error::external_error("vcs".to_string(), format!("Failed to get current branch: {}", String::from_utf8_lossy(&output.stderr)), Span::unknown()));
75        }
76
77        Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
78    }
79
80    /// Gets the current SVN branch.
81    pub fn get_svn_branch(&self) -> Result<String> {
82        let output = Command::new("svn").arg("info").current_dir(&self.repo_dir).output()?;
83
84        if !output.status.success() {
85            return Err(Error::external_error("vcs".to_string(), format!("Failed to get SVN info: {}", String::from_utf8_lossy(&output.stderr)), Span::unknown()));
86        }
87
88        let output_str = String::from_utf8_lossy(&output.stdout).to_string();
89        for line in output_str.lines() {
90            if line.starts_with("URL:") {
91                let url = line.split(": ").nth(1).unwrap_or("");
92                // Extract branch name from URL (assuming standard SVN layout)
93                if let Some(branch_part) = url.split("/branches/").nth(1) {
94                    return Ok(branch_part.split("/").next().unwrap_or("").to_string());
95                }
96                else if url.contains("/trunk/") {
97                    return Ok("trunk".to_string());
98                }
99                else if url.contains("/tags/") {
100                    return Ok("tags".to_string());
101                }
102            }
103        }
104
105        Ok("unknown".to_string())
106    }
107
108    /// Gets the current Mercurial branch.
109    pub fn get_hg_branch(&self) -> Result<String> {
110        let output = Command::new("hg").arg("branch").current_dir(&self.repo_dir).output()?;
111
112        if !output.status.success() {
113            return Err(Error::external_error("vcs".to_string(), format!("Failed to get Mercurial branch: {}", String::from_utf8_lossy(&output.stderr)), Span::unknown()));
114        }
115
116        Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
117    }
118
119    /// Gets the latest commit hash for the detected VCS.
120    pub fn get_latest_commit(&self) -> Result<String> {
121        match self.vcs_type {
122            VcsType::Git => self.get_git_commit(),
123            VcsType::Svn => self.get_svn_commit(),
124            VcsType::Mercurial => self.get_hg_commit(),
125            VcsType::None => Err(Error::external_error("vcs".to_string(), "No version control system detected".to_string(), Span::unknown())),
126        }
127    }
128
129    /// Gets the latest git commit hash.
130    pub fn get_git_commit(&self) -> Result<String> {
131        let output = Command::new("git").arg("rev-parse").arg("HEAD").current_dir(&self.repo_dir).output()?;
132
133        if !output.status.success() {
134            return Err(Error::external_error("vcs".to_string(), format!("Failed to get latest commit: {}", String::from_utf8_lossy(&output.stderr)), Span::unknown()));
135        }
136
137        Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
138    }
139
140    /// Gets the latest SVN revision.
141    pub fn get_svn_commit(&self) -> Result<String> {
142        let output = Command::new("svn").arg("info").current_dir(&self.repo_dir).output()?;
143
144        if !output.status.success() {
145            return Err(Error::external_error("vcs".to_string(), format!("Failed to get SVN info: {}", String::from_utf8_lossy(&output.stderr)), Span::unknown()));
146        }
147
148        let output_str = String::from_utf8_lossy(&output.stdout).to_string();
149        for line in output_str.lines() {
150            if line.starts_with("Revision:") {
151                return Ok(line.split(": ").nth(1).unwrap_or("").to_string());
152            }
153        }
154
155        Err(Error::external_error("vcs".to_string(), "Failed to extract SVN revision".to_string(), Span::unknown()))
156    }
157
158    /// Gets the latest Mercurial commit hash.
159    pub fn get_hg_commit(&self) -> Result<String> {
160        let output = Command::new("hg").arg("identify").arg("--id").current_dir(&self.repo_dir).output()?;
161
162        if !output.status.success() {
163            return Err(Error::external_error("vcs".to_string(), format!("Failed to get Mercurial commit: {}", String::from_utf8_lossy(&output.stderr)), Span::unknown()));
164        }
165
166        Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
167    }
168
169    /// Gets the status for the detected VCS.
170    pub fn get_status(&self) -> Result<String> {
171        match self.vcs_type {
172            VcsType::Git => self.get_git_status(),
173            VcsType::Svn => self.get_svn_status(),
174            VcsType::Mercurial => self.get_hg_status(),
175            VcsType::None => Err(Error::external_error("vcs".to_string(), "No version control system detected".to_string(), Span::unknown())),
176        }
177    }
178
179    /// Gets the git status.
180    pub fn get_git_status(&self) -> Result<String> {
181        let output = Command::new("git").arg("status").arg("--porcelain").current_dir(&self.repo_dir).output()?;
182
183        if !output.status.success() {
184            return Err(Error::external_error("vcs".to_string(), format!("Failed to get git status: {}", String::from_utf8_lossy(&output.stderr)), Span::unknown()));
185        }
186
187        Ok(String::from_utf8_lossy(&output.stdout).to_string())
188    }
189
190    /// Gets the SVN status.
191    pub fn get_svn_status(&self) -> Result<String> {
192        let output = Command::new("svn").arg("status").current_dir(&self.repo_dir).output()?;
193
194        if !output.status.success() {
195            return Err(Error::external_error("vcs".to_string(), format!("Failed to get SVN status: {}", String::from_utf8_lossy(&output.stderr)), Span::unknown()));
196        }
197
198        Ok(String::from_utf8_lossy(&output.stdout).to_string())
199    }
200
201    /// Gets the Mercurial status.
202    pub fn get_hg_status(&self) -> Result<String> {
203        let output = Command::new("hg").arg("status").current_dir(&self.repo_dir).output()?;
204
205        if !output.status.success() {
206            return Err(Error::external_error("vcs".to_string(), format!("Failed to get Mercurial status: {}", String::from_utf8_lossy(&output.stderr)), Span::unknown()));
207        }
208
209        Ok(String::from_utf8_lossy(&output.stdout).to_string())
210    }
211
212    /// Generates a change summary for the detected VCS.
213    pub fn generate_change_summary(&self) -> Result<String> {
214        match self.vcs_type {
215            VcsType::Git => self.generate_git_change_summary(),
216            VcsType::Svn => self.generate_svn_change_summary(),
217            VcsType::Mercurial => self.generate_hg_change_summary(),
218            VcsType::None => Err(Error::external_error("vcs".to_string(), "No version control system detected".to_string(), Span::unknown())),
219        }
220    }
221
222    /// Generates a change summary from git.
223    pub fn generate_git_change_summary(&self) -> Result<String> {
224        let status = self.get_git_status()?;
225        let mut summary = String::new();
226        let mut added = 0;
227        let mut modified = 0;
228        let mut deleted = 0;
229        let mut renamed = 0;
230
231        for line in status.lines() {
232            if line.len() < 3 {
233                continue;
234            }
235
236            let status_code = &line[0..2];
237            match status_code {
238                "A " => added += 1,
239                "M " => modified += 1,
240                "D " => deleted += 1,
241                "R " => renamed += 1,
242                _ => {}
243            }
244        }
245
246        summary.push_str(&format!("Git change summary:\n"));
247        summary.push_str(&format!("- Added: {}\n", added));
248        summary.push_str(&format!("- Modified: {}\n", modified));
249        summary.push_str(&format!("- Deleted: {}\n", deleted));
250        summary.push_str(&format!("- Renamed: {}\n", renamed));
251
252        if !status.is_empty() {
253            summary.push_str("\nDetailed changes:\n");
254            summary.push_str(&status);
255        }
256
257        Ok(summary)
258    }
259
260    /// Generates a change summary from SVN.
261    pub fn generate_svn_change_summary(&self) -> Result<String> {
262        let status = self.get_svn_status()?;
263        let mut summary = String::new();
264        let mut added = 0;
265        let mut modified = 0;
266        let mut deleted = 0;
267        let mut other = 0;
268
269        for line in status.lines() {
270            if line.is_empty() {
271                continue;
272            }
273
274            let status_char = line.chars().next().unwrap_or(' ');
275            match status_char {
276                'A' => added += 1,
277                'M' => modified += 1,
278                'D' => deleted += 1,
279                _ => other += 1,
280            }
281        }
282
283        summary.push_str(&format!("SVN change summary:\n"));
284        summary.push_str(&format!("- Added: {}\n", added));
285        summary.push_str(&format!("- Modified: {}\n", modified));
286        summary.push_str(&format!("- Deleted: {}\n", deleted));
287        if other > 0 {
288            summary.push_str(&format!("- Other: {}\n", other));
289        }
290
291        if !status.is_empty() {
292            summary.push_str("\nDetailed changes:\n");
293            summary.push_str(&status);
294        }
295
296        Ok(summary)
297    }
298
299    /// Generates a change summary from Mercurial.
300    pub fn generate_hg_change_summary(&self) -> Result<String> {
301        let status = self.get_hg_status()?;
302        let mut summary = String::new();
303        let mut added = 0;
304        let mut modified = 0;
305        let mut deleted = 0;
306        let mut renamed = 0;
307        let mut other = 0;
308
309        for line in status.lines() {
310            if line.is_empty() {
311                continue;
312            }
313
314            let status_char = line.chars().next().unwrap_or(' ');
315            match status_char {
316                'A' => added += 1,
317                'M' => modified += 1,
318                'D' => deleted += 1,
319                'R' => renamed += 1,
320                _ => other += 1,
321            }
322        }
323
324        summary.push_str(&format!("Mercurial change summary:\n"));
325        summary.push_str(&format!("- Added: {}\n", added));
326        summary.push_str(&format!("- Modified: {}\n", modified));
327        summary.push_str(&format!("- Deleted: {}\n", deleted));
328        if renamed > 0 {
329            summary.push_str(&format!("- Renamed: {}\n", renamed));
330        }
331        if other > 0 {
332            summary.push_str(&format!("- Other: {}\n", other));
333        }
334
335        if !status.is_empty() {
336            summary.push_str("\nDetailed changes:\n");
337            summary.push_str(&status);
338        }
339
340        Ok(summary)
341    }
342}