Skip to content
Merged
24 changes: 6 additions & 18 deletions backend/app/src/actors/git.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
use git2_ox::ReferenceKindFilter;
use hannibal::prelude::*;
use std::path::Path;

Expand Down Expand Up @@ -194,34 +193,23 @@ impl Handler<CreateBranch> for GitActor {
}
}

#[derive(Debug, Clone)]
pub struct RepositoryStatus {
pub head: git2_ox::CommitWithReferences,
pub current_branch: Option<String>,
}

#[message(response = Result<RepositoryStatus, git2_ox::error::Error>)]
#[message(response = Result<git2_ox::Status, git2_ox::error::Error>)]
pub struct GetRepositoryStatus;

impl Handler<GetRepositoryStatus> for GitActor {
async fn handle(
&mut self,
_ctx: &mut Context<Self>,
_msg: GetRepositoryStatus,
) -> Result<RepositoryStatus, git2_ox::error::Error> {
let head = self.repository.get_commit_for_revision("HEAD")?;
let current_branch = self.repository.current_branch_name();
Ok(RepositoryStatus {
head,
current_branch,
})
) -> Result<git2_ox::Status, git2_ox::error::Error> {
self.repository.status()
}
}

#[message(response = Result<Vec<git2_ox::ResolvedReference>, git2_ox::error::Error>)]
pub struct ListReferences {
pub filter: Option<String>,
pub filter_kinds: Option<ReferenceKindFilter>,
pub filter_kinds: Option<git2_ox::ReferenceKindFilter>,
}

impl Handler<ListReferences> for GitActor {
Expand All @@ -240,10 +228,10 @@ impl Handler<ListReferences> for GitActor {

let ref_kind_ok = match &msg.filter_kinds {
None => true,
Some(ReferenceKindFilter::Include {
Some(git2_ox::ReferenceKindFilter::Include {
include: include_kinds,
}) => include_kinds.contains(&ref_kind),
Some(ReferenceKindFilter::Exclude {
Some(git2_ox::ReferenceKindFilter::Exclude {
exclude: exclude_kinds,
}) => !exclude_kinds.contains(&ref_kind),
};
Expand Down
13 changes: 4 additions & 9 deletions backend/app/src/web/api/v1/git.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use crate::{actors, web, web::api};

use axum::extract::{Path, Query, State};
use axum::{Json, routing};
use git2_ox::{ReferenceKind, ReferenceKindFilter, ResolvedReference, commit};
use git2_ox::{ReferenceKind, ReferenceKindFilter, ResolvedReference, Status, commit};
use serde::{Deserialize, Serialize};
use utoipa::{IntoParams, ToSchema};

Expand Down Expand Up @@ -341,10 +341,8 @@ async fn create_branch(
#[derive(Serialize, ToSchema)]
#[serde(rename_all = "camelCase")]
struct RepositoryStatusResponse {
/// The current HEAD commit
head: git2_ox::CommitWithReferences,
/// The current branch name, not set if in a detached HEAD state
current_branch: Option<String>,
#[serde(flatten)]
status: Status,
}

#[utoipa::path(
Expand All @@ -363,10 +361,7 @@ async fn get_repository_status(
let actor = state.git_actor();
let msg = actors::git::GetRepositoryStatus;
let status = actor.call(msg).await??;
Ok(Json(RepositoryStatusResponse {
head: status.head,
current_branch: status.current_branch,
}))
Ok(Json(RepositoryStatusResponse { status }))
}

#[derive(Deserialize, IntoParams)]
Expand Down
2 changes: 2 additions & 0 deletions backend/git2-ox/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pub mod diff;
pub mod error;
pub mod reference;
pub mod repository;
pub mod status;
pub mod tag;
pub mod utils;

Expand All @@ -12,6 +13,7 @@ pub use commit::{Commit, CommitProperties, CommitWithReferences};
pub use diff::Diff;
pub use reference::{ReferenceKind, ReferenceMetadata, ResolvedReference};
pub use repository::{ReferenceKindFilter, Repository};
pub use status::Status;
pub use tag::TaggedCommit;

type Result<T> = std::result::Result<T, error::Error>;
34 changes: 33 additions & 1 deletion backend/git2-ox/src/repository.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
use git2::IntoCString;

use crate::commit::{CommitProperties, CommitWithReferences};
use crate::error::Error;
use crate::reference::ReferencesMap;
use crate::{Branch, Commit, Diff, ReferenceKind, ResolvedReference, Result, TaggedCommit, utils};
use crate::{
Branch, Commit, Diff, ReferenceKind, ResolvedReference, Result, Status, TaggedCommit, utils,
};
use std::path::Path;

pub struct Repository {
Expand Down Expand Up @@ -217,6 +221,34 @@ impl Repository {
.filter_map(std::result::Result::ok)
.filter_map(move |r| ResolvedReference::try_from(r).ok()))
}

/// Get the status of the repository
pub fn status(&self) -> Result<Status> {
self.try_into()
}

/// Add all paths matching the pathspecs to the index
/// See `git2::Index::add_all for reference`
/// * `pathspecs` - List of file names or shell glob patterns that will matched against files in the repository’s
/// working directory. Each file that matches will be added to the index (either updating an existing entry or
/// adding a new entry).
pub fn add_all<T, I>(&self, pathspecs: I) -> Result<()>
where
T: AsRef<str> + IntoCString,
I: IntoIterator<Item = T>,
{
let mut index = self
.repo()
.index()
.map_err(|e| Error::from_ctx_and_error("Failed getting the index", e))?;
index
.add_all(pathspecs, git2::IndexAddOption::DEFAULT, None)
.map_err(|e| Error::from_ctx_and_error("Failed adding path to index", e))?;
index
.write()
.map_err(|e| Error::from_ctx_and_error("Failed writing index path to index", e))?;
Ok(())
}
}

/// Include or exclude `Reference`s
Expand Down
175 changes: 175 additions & 0 deletions backend/git2-ox/src/status.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
use crate::{CommitWithReferences, Repository, Result, error::Error};

type Files = Vec<String>;

#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize),
serde(rename_all = "camelCase")
)]
#[derive(Default, Debug)]
pub struct TreeStatus {
/// Added files
new_files: Files,
/// Modified files
modified_files: Files,
/// Deleted files
deleted_files: Files,
/// Renamed files
renamed_files: Files,
}

impl TreeStatus {
/// Files that were added
pub fn new_files(&self) -> &Files {
&self.new_files
}

/// Files that were modified
pub fn modified_files(&self) -> &Files {
&self.modified_files
}

/// Files that were deleted
pub fn deleted_files(&self) -> &Files {
&self.deleted_files
}

/// Files that were renamed
pub fn renamed_files(&self) -> &Files {
&self.renamed_files
}
}

#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize),
serde(rename_all = "camelCase")
)]
#[derive(Debug)]
pub struct Status {
/// Name of the current branch, not set if `is_detached_head` is true
current_branch: Option<String>,
/// The commit current HEAD points to
head: CommitWithReferences,
/// Whether the head is detached
is_detached_head: bool,
/// Whether the worktree or index have changes
is_dirty: bool,
/// Status of the index
index: TreeStatus,
/// Status in the worktree
worktree: TreeStatus,
/// Paths with conflicts
conflicts: Files,
}

impl Status {
pub fn head(&self) -> &CommitWithReferences {
&self.head
}

pub fn is_detached_head(&self) -> bool {
self.is_detached_head
}

pub fn is_dirty(&self) -> bool {
self.is_dirty
}

pub fn index(&self) -> &TreeStatus {
&self.index
}

pub fn worktree(&self) -> &TreeStatus {
&self.worktree
}

pub fn conflicted(&self) -> &Files {
&self.conflicts
}
}

impl TryFrom<&Repository> for Status {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I would probably prefer a method e.g. Status::try_from_repository in this case. As TryFrom is more about type conversions.
Alternatively we could just have an impl block:

impl Repository {
pub fn status() Result<Status> {
....
}
}

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

used Status::try_from_repository I don't like to have all this logic in git2_ox::Repository

type Error = Error;

fn try_from(repo: &Repository) -> Result<Self> {
let head = repo.get_commit_for_revision("HEAD")?;

let mut opts = git2::StatusOptions::new();
opts.include_untracked(true)
.recurse_untracked_dirs(true)
.include_ignored(false)
.renames_head_to_index(true)
.renames_index_to_workdir(true);

let statuses = repo
.repo()
.statuses(Some(&mut opts))
.map_err(|e| Error::from_ctx_and_error("Failed to create statuses", e))?;

let mut index_status = TreeStatus::default();
let mut worktree_status = TreeStatus::default();
let mut conflicts = Vec::new();

for entry in statuses.iter() {
let status = entry.status();
if status.is_index_new() {
index_status
.new_files
.push(entry.path().unwrap_or("<invalid utf-8>").to_string());
} else if status.is_index_renamed() {
index_status
.renamed_files
.push(entry.path().unwrap_or("<invalid utf-8>").to_string());
} else if status.is_index_modified() {
index_status
.modified_files
.push(entry.path().unwrap_or("<invalid utf-8>").to_string());
} else if status.is_index_deleted() {
index_status
.deleted_files
.push(entry.path().unwrap_or("<invalid utf-8>").to_string());
} else if status.is_wt_new() {
worktree_status
.new_files
.push(entry.path().unwrap_or("<invalid utf-8>").to_string());
} else if status.is_wt_renamed() {
worktree_status
.renamed_files
.push(entry.path().unwrap_or("<invalid utf-8>").to_string());
} else if status.is_wt_modified() {
worktree_status
.modified_files
.push(entry.path().unwrap_or("<invalid utf-8>").to_string());
} else if status.is_wt_deleted() {
worktree_status
.deleted_files
.push(entry.path().unwrap_or("<invalid utf-8>").to_string());
} else if status.is_conflicted() {
conflicts.push(entry.path().unwrap_or("<invalid utf-8>").to_string());
}
}

Ok(Self {
current_branch: repo.current_branch_name(),
head,
is_detached_head: repo.repo().head_detached().map_err(|e| {
Error::from_ctx_and_error("Failed to determined if head is detached", e)
})?,
is_dirty: !statuses.is_empty(),
index: index_status,
worktree: worktree_status,
conflicts,
})
}
}

impl TryFrom<Repository> for Status {
type Error = Error;
fn try_from(repo: Repository) -> Result<Self> {
Status::try_from(&repo)
}
}
7 changes: 6 additions & 1 deletion backend/git2-ox/tests/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,15 @@ impl TempRepository {
self.temp_dir.path()
}

pub fn create_and_commit_random_file(&self) -> (FileName, CommitId) {
pub fn create_random_file(&self) -> FileName {
let file_name = Uuid::new_v4().to_string();
let file_path = self.path().join(&file_name);
std::fs::write(&file_path, "random content").unwrap();
file_name
}

pub fn create_and_commit_random_file(&self) -> (FileName, CommitId) {
let file_name = self.create_random_file();

let mut index = self.repo.repo().index().unwrap();
index.add_path(std::path::Path::new(&file_name)).unwrap();
Expand Down
Loading