simperby_repository/raw/
mod.rs

1mod implementation;
2pub mod reserved_state;
3mod templates;
4#[cfg(test)]
5mod tests;
6
7use super::*;
8use eyre::Result;
9use git2::{
10    ApplyLocation, BranchType, DiffFormat, Email, EmailCreateOptions, IndexAddOption, ObjectType,
11    Oid, Repository, RepositoryInitOptions, ResetType, Sort, StashApplyOptions, StashFlags, Status,
12    StatusOptions, StatusShow,
13};
14use implementation::RawRepositoryInner;
15use simperby_core::reserved::ReservedState;
16use std::convert::TryFrom;
17use std::str;
18use templates::*;
19use thiserror::Error;
20
21#[derive(Error, Debug)]
22pub enum Error {
23    #[error("git2 error: {0}")]
24    Git2Error(git2::Error),
25    /// The given git object doesn't exist.
26    #[error("not found: {0}")]
27    NotFound(String),
28    /// The assumption of the method
29    /// (e.g., there is no merge commit, there must be a merge base, ..) is violated.
30    #[error("the repository is invalid: {0}")]
31    InvalidRepository(String),
32    #[error("unknown error: {0}")]
33    Unknown(String),
34}
35
36impl From<git2::Error> for Error {
37    fn from(e: git2::Error) -> Self {
38        Error::Git2Error(e)
39    }
40}
41
42/// A commit with abstracted diff.
43/// - `author` is a member name, not a git commit author.
44/// - `timestamp` is generated by `get_timestamp()` which represents up to milliseconds.
45#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
46pub struct SemanticCommit {
47    pub title: String,
48    pub body: String,
49    pub diff: Diff,
50    pub author: MemberName,
51    pub timestamp: Timestamp,
52}
53
54/// A commit related to the actual git commit.
55/// - `diff` is a string of git patch which is used to make a git commit.
56/// - `author` is same as the author of git commit.
57/// The committer is always same as the author.
58/// - `timestamp` is a git timestamp which represents up to seconds.
59/// So, the timestamp of `SemanticCommit` should be divided by 1000 to convert to that of `RawCommit`.
60#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
61pub struct RawCommit {
62    pub message: String,
63    pub diff: Option<String>,
64    pub author: String,
65    pub email: String,
66    pub timestamp: Timestamp,
67}
68
69#[derive(Debug)]
70pub struct RawRepository {
71    inner: tokio::sync::Mutex<Option<RawRepositoryInner>>,
72}
73
74impl RawRepository {
75    /// Initialize the genesis repository from the genesis working tree.
76    ///
77    /// Fails if there is already a repository.
78    pub async fn init(
79        directory: &str,
80        init_commit_message: &str,
81        init_commit_branch: &Branch,
82    ) -> Result<Self, Error>
83    where
84        Self: Sized,
85    {
86        let repo = RawRepositoryInner::init(directory, init_commit_message, init_commit_branch)?;
87        let inner = tokio::sync::Mutex::new(Some(repo));
88
89        Ok(Self { inner })
90    }
91
92    /// Loads an exisitng repository.
93    pub async fn open(directory: &str) -> Result<Self, Error>
94    where
95        Self: Sized,
96    {
97        let repo = RawRepositoryInner::open(directory)?;
98        let inner = tokio::sync::Mutex::new(Some(repo));
99
100        Ok(Self { inner })
101    }
102
103    /// Clones an exisitng repository.
104    ///
105    /// Fails if there is no repository with url.
106    pub async fn clone(directory: &str, url: &str) -> Result<Self, Error>
107    where
108        Self: Sized,
109    {
110        let repo = RawRepositoryInner::clone(directory, url)?;
111        let inner = tokio::sync::Mutex::new(Some(repo));
112
113        Ok(Self { inner })
114    }
115
116    /// Returns the full commit hash from the revision selection string.
117    ///
118    /// See the [reference](https://git-scm.com/book/en/v2/Git-Tools-Revision-Selection).
119    pub async fn retrieve_commit_hash(
120        &self,
121        revision_selection: String,
122    ) -> Result<CommitHash, Error> {
123        helper_1(
124            self,
125            RawRepositoryInner::retrieve_commit_hash,
126            revision_selection,
127        )
128        .await
129    }
130
131    // ----------------------
132    // Branch-related methods
133    // ----------------------
134
135    /// Returns the list of branches.
136    pub async fn list_branches(&self) -> Result<Vec<Branch>, Error> {
137        helper_0(self, RawRepositoryInner::list_branches).await
138    }
139
140    /// Creates a branch on the commit.
141    pub async fn create_branch(
142        &self,
143        branch_name: Branch,
144        commit_hash: CommitHash,
145    ) -> Result<(), Error> {
146        helper_2(
147            self,
148            RawRepositoryInner::create_branch,
149            branch_name,
150            commit_hash,
151        )
152        .await
153    }
154
155    /// Gets the commit that the branch points to.
156    pub async fn locate_branch(&self, branch: Branch) -> Result<CommitHash, Error> {
157        helper_1(self, RawRepositoryInner::locate_branch, branch).await
158    }
159
160    /// Gets the list of branches from the commit.
161    pub async fn get_branches(&self, commit_hash: CommitHash) -> Result<Vec<Branch>, Error> {
162        helper_1(self, RawRepositoryInner::get_branches, commit_hash).await
163    }
164
165    /// Moves the branch.
166    pub async fn move_branch(
167        &mut self,
168        branch: Branch,
169        commit_hash: CommitHash,
170    ) -> Result<(), Error> {
171        helper_2_mut(self, RawRepositoryInner::move_branch, branch, commit_hash).await
172    }
173
174    /// Deletes the branch.
175    pub async fn delete_branch(&mut self, branch: Branch) -> Result<(), Error> {
176        helper_1_mut(self, RawRepositoryInner::delete_branch, branch).await
177    }
178
179    // -------------------
180    // Tag-related methods
181    // -------------------
182
183    /// Returns the list of tags.
184    pub async fn list_tags(&self) -> Result<Vec<Tag>, Error> {
185        helper_0(self, RawRepositoryInner::list_tags).await
186    }
187
188    /// Creates a tag on the given commit.
189    pub async fn create_tag(&mut self, tag: Tag, commit_hash: CommitHash) -> Result<(), Error> {
190        helper_2_mut(self, RawRepositoryInner::create_tag, tag, commit_hash).await
191    }
192
193    /// Gets the commit that the tag points to.
194    pub async fn locate_tag(&self, tag: Tag) -> Result<CommitHash, Error> {
195        helper_1(self, RawRepositoryInner::locate_tag, tag).await
196    }
197
198    /// Gets the tags on the given commit.
199    pub async fn get_tag(&self, commit_hash: CommitHash) -> Result<Vec<Tag>, Error> {
200        helper_1(self, RawRepositoryInner::get_tag, commit_hash).await
201    }
202
203    /// Removes the tag.
204    pub async fn remove_tag(&mut self, tag: Tag) -> Result<(), Error> {
205        helper_1_mut(self, RawRepositoryInner::remove_tag, tag).await
206    }
207
208    // ----------------------
209    // Commit-related methods
210    // ----------------------
211
212    /// Creates a commit from the currently checked out branch by applying the patch.
213    ///
214    /// The commit will be empty commit if `diff` in `RawCommit` is None.
215    /// Committer will be the same as the author.
216    pub async fn create_commit(&mut self, commit: RawCommit) -> Result<CommitHash, Error> {
217        helper_1_mut(self, RawRepositoryInner::create_commit, commit).await
218    }
219
220    /// Creates a commit from the currently checked out branch without applying the patch.
221    ///
222    /// `diff` in `RawCommit` is not used in this function.
223    /// This is same as `git add . && git commit -m "commit_message"`.
224    pub async fn create_commit_all(&mut self, commit: RawCommit) -> Result<CommitHash, Error> {
225        helper_1_mut(self, RawRepositoryInner::create_commit_all, commit).await
226    }
227
228    /// Reads the raw commit at given commit hash.
229    pub async fn read_commit(&self, commit_hash: CommitHash) -> Result<RawCommit, Error> {
230        helper_1(self, RawRepositoryInner::read_commit, commit_hash).await
231    }
232
233    /// Creates a semantic commit from the currently checked out branch.
234    ///
235    /// It fails if the `diff` is not `Diff::Reserved` or `Diff::None`.
236    /// If `authored_by_simperby` is true, the author of commit will be Simperby,
237    /// otherwise, that will be user.
238    pub async fn create_semantic_commit(
239        &mut self,
240        commit: SemanticCommit,
241        authored_by_simperby: bool,
242    ) -> Result<CommitHash, Error> {
243        helper_2_mut(
244            self,
245            RawRepositoryInner::create_semantic_commit,
246            commit,
247            authored_by_simperby,
248        )
249        .await
250    }
251
252    /// Reads the semantic commmit at given commit hash.
253    pub async fn read_semantic_commit(
254        &self,
255        commit_hash: CommitHash,
256    ) -> Result<SemanticCommit, Error> {
257        helper_1(self, RawRepositoryInner::read_semantic_commit, commit_hash).await
258    }
259
260    /// Removes orphaned commits. Same as `git gc --prune=now --aggressive`
261    pub async fn run_garbage_collection(&mut self) -> Result<(), Error> {
262        helper_0_mut(self, RawRepositoryInner::run_garbage_collection).await
263    }
264
265    // ----------------------------
266    // Working-tree-related methods
267    // ----------------------------
268
269    /// Checkouts and cleans the current working tree.
270    /// This is same as `git checkout . && git clean -fd`.
271    pub async fn checkout_clean(&mut self) -> Result<(), Error> {
272        helper_0_mut(self, RawRepositoryInner::checkout_clean).await
273    }
274
275    /// Checkouts to the branch.
276    pub async fn checkout(&mut self, branch: Branch) -> Result<(), Error> {
277        helper_1_mut(self, RawRepositoryInner::checkout, branch).await
278    }
279
280    /// Checkouts to the commit and make `HEAD` in a detached mode.
281    pub async fn checkout_detach(&mut self, commit_hash: CommitHash) -> Result<(), Error> {
282        helper_1_mut(self, RawRepositoryInner::checkout_detach, commit_hash).await
283    }
284
285    /// Saves the local modifications to a new stash.
286    pub async fn stash(&mut self) -> Result<(), Error> {
287        helper_0_mut(self, RawRepositoryInner::stash).await
288    }
289
290    /// Pops the most recent stash.
291    /// If index is true, this is same as `git stash pop --index`.
292    pub async fn stash_pop(&mut self, index: bool) -> Result<(), Error> {
293        helper_1_mut(self, RawRepositoryInner::stash_pop, index).await
294    }
295
296    /// Applies the most recent stash.
297    /// If index is true, this is same as `git stash apply --index`.
298    pub async fn stash_apply(&mut self, index: bool) -> Result<(), Error> {
299        helper_1_mut(self, RawRepositoryInner::stash_apply, index).await
300    }
301
302    /// Removes the most recent stash.
303    pub async fn stash_drop(&mut self) -> Result<(), Error> {
304        helper_0_mut(self, RawRepositoryInner::stash_drop).await
305    }
306
307    /// Checks if there are no unstaged, staged and untracked files.
308    pub async fn check_clean(&self) -> Result<(), Error> {
309        helper_0(self, RawRepositoryInner::check_clean).await
310    }
311
312    // ---------------
313    // Various queries
314    // ---------------
315
316    /// Returns the path of working directory.
317    pub async fn get_working_directory_path(&self) -> Result<String, Error> {
318        helper_0(self, RawRepositoryInner::get_working_directory_path).await
319    }
320
321    /// Returns the commit hash of the current HEAD.
322    pub async fn get_head(&self) -> Result<CommitHash, Error> {
323        helper_0(self, RawRepositoryInner::get_head).await
324    }
325
326    /// Returns the currently checked-out branch, if any.
327    /// If the repository is in a detached HEAD state, it returns None.
328    pub async fn get_currently_checkout_branch(&self) -> Result<Option<Branch>, Error> {
329        helper_0(self, RawRepositoryInner::get_currently_checkout_branch).await
330    }
331
332    /// Returns the commit hash of the initial commit.
333    ///
334    /// Fails if the repository is empty.
335    pub async fn get_initial_commit(&self) -> Result<CommitHash, Error> {
336        helper_0(self, RawRepositoryInner::get_initial_commit).await
337    }
338
339    /// Returns the patch of the given commit.
340    pub async fn get_patch(&self, commit_hash: CommitHash) -> Result<String, Error> {
341        helper_1(self, RawRepositoryInner::get_patch, commit_hash).await
342    }
343
344    /// Returns the diff of the given commit.
345    pub async fn show_commit(&self, commit_hash: CommitHash) -> Result<String, Error> {
346        helper_1(self, RawRepositoryInner::show_commit, commit_hash).await
347    }
348
349    /// Lists the ancestor commits of the given commit (The first element is the direct parent).
350    ///
351    /// It fails if there is a merge commit.
352    /// * `max`: the maximum number of entries to be returned.
353    pub async fn list_ancestors(
354        &self,
355        commit_hash: CommitHash,
356        max: Option<usize>,
357    ) -> Result<Vec<CommitHash>, Error> {
358        helper_2(self, RawRepositoryInner::list_ancestors, commit_hash, max).await
359    }
360
361    /// Queries the commits from the very next commit of `ancestor` to `descendant`.
362    /// `ancestor` not included, `descendant` included.
363    ///
364    /// It fails if the two commits are the same.
365    /// It fails if the `ancestor` is not the merge base of the two commits.
366    pub async fn query_commit_path(
367        &self,
368        ancestor: CommitHash,
369        descendant: CommitHash,
370    ) -> Result<Vec<CommitHash>, Error> {
371        helper_2(
372            self,
373            RawRepositoryInner::query_commit_path,
374            ancestor,
375            descendant,
376        )
377        .await
378    }
379
380    /// Returns the children commits of the given commit.
381    pub async fn list_children(&self, commit_hash: CommitHash) -> Result<Vec<CommitHash>, Error> {
382        helper_1(self, RawRepositoryInner::list_children, commit_hash).await
383    }
384
385    /// Returns the merge base of the two commits.
386    pub async fn find_merge_base(
387        &self,
388        commit_hash1: CommitHash,
389        commit_hash2: CommitHash,
390    ) -> Result<CommitHash, Error> {
391        helper_2(
392            self,
393            RawRepositoryInner::find_merge_base,
394            commit_hash1,
395            commit_hash2,
396        )
397        .await
398    }
399
400    /// Reads the reserved state from the currently checked out branch.
401    pub async fn read_reserved_state(&self) -> Result<ReservedState, Error> {
402        helper_0(self, RawRepositoryInner::read_reserved_state).await
403    }
404
405    /// Reads the reserved state at given commit hash.
406    pub async fn read_reserved_state_at_commit(
407        &self,
408        commit_hash: CommitHash,
409    ) -> Result<ReservedState, Error> {
410        helper_1(
411            self,
412            RawRepositoryInner::read_reserved_state_at_commit,
413            commit_hash,
414        )
415        .await
416    }
417
418    // ----------------------
419    // Remote-related methods
420    // ----------------------
421
422    /// Adds a remote repository.
423    pub async fn add_remote(
424        &mut self,
425        remote_name: String,
426        remote_url: String,
427    ) -> Result<(), Error> {
428        helper_2_mut(
429            self,
430            RawRepositoryInner::add_remote,
431            remote_name,
432            remote_url,
433        )
434        .await
435    }
436
437    /// Removes a remote repository.
438    pub async fn remove_remote(&mut self, remote_name: String) -> Result<(), Error> {
439        helper_1_mut(self, RawRepositoryInner::remove_remote, remote_name).await
440    }
441
442    /// Fetches the remote repository. Same as `git fetch --all -j <LARGE NUMBER>`.
443    pub async fn fetch_all(&mut self, prune: bool) -> Result<(), Error> {
444        helper_1_mut(self, RawRepositoryInner::fetch_all, prune).await
445    }
446
447    /// Pushes to the remote repository with the push option.
448    /// This is same as `git push <remote_name> <branch_name> --push-option=<string>`.
449    pub async fn push_option(
450        &self,
451        remote_name: String,
452        branch: Branch,
453        option: Option<String>,
454    ) -> Result<(), Error> {
455        helper_3(
456            self,
457            RawRepositoryInner::push_option,
458            remote_name,
459            branch,
460            option,
461        )
462        .await
463    }
464
465    /// Checks if the server of remote repository is open.
466    pub async fn ping_remote(&self, remote_name: String) -> Result<bool, Error> {
467        helper_1(self, RawRepositoryInner::ping_remote, remote_name).await
468    }
469
470    /// Lists all the remote repositories.
471    ///
472    /// Returns `(remote_name, remote_url)`.
473    pub async fn list_remotes(&self) -> Result<Vec<(String, String)>, Error> {
474        helper_0(self, RawRepositoryInner::list_remotes).await
475    }
476
477    /// Lists all the remote tracking branches.
478    ///
479    /// Returns `(remote_name, branch_name, commit_hash)`
480    pub async fn list_remote_tracking_branches(
481        &self,
482    ) -> Result<Vec<(String, String, CommitHash)>, Error> {
483        helper_0(self, RawRepositoryInner::list_remote_tracking_branches).await
484    }
485
486    /// Returns the commit of given remote branch.
487    pub async fn locate_remote_tracking_branch(
488        &self,
489        remote_name: String,
490        branch_name: String,
491    ) -> Result<CommitHash, Error> {
492        helper_2(
493            self,
494            RawRepositoryInner::locate_remote_tracking_branch,
495            remote_name,
496            branch_name,
497        )
498        .await
499    }
500}
501
502#[cfg(target_os = "windows")]
503pub fn run_command(command: impl AsRef<str>) -> Result<(), Error> {
504    println!("> RUN: {}", command.as_ref());
505    let mut child = std::process::Command::new("C:/Program Files/Git/bin/sh.exe")
506        .arg("--login")
507        .arg("-c")
508        .arg(command.as_ref())
509        .spawn()
510        .map_err(|_| Error::Unknown("failed to execute process".to_string()))?;
511    let ecode = child
512        .wait()
513        .map_err(|_| Error::Unknown("failed to wait on child".to_string()))?;
514
515    if ecode.success() {
516        Ok(())
517    } else {
518        Err(Error::Unknown("failed to run process".to_string()))
519    }
520}
521
522#[cfg(not(target_os = "windows"))]
523pub fn run_command(command: impl AsRef<str>) -> Result<(), Error> {
524    println!("> RUN: {}", command.as_ref());
525    let mut child = std::process::Command::new("sh")
526        .arg("-c")
527        .arg(command.as_ref())
528        .spawn()
529        .map_err(|_| Error::Unknown("failed to execute process".to_string()))?;
530
531    let ecode = child
532        .wait()
533        .map_err(|_| Error::Unknown("failed to wait on child".to_string()))?;
534
535    if ecode.success() {
536        Ok(())
537    } else {
538        Err(Error::Unknown("failed to run process".to_string()))
539    }
540}