feat(core): implement Git repository operations with gRPC services
- Add advertise_refs functionality for Git protocol communication - Implement archive service with TAR/ZIP format support and streaming - Create blame service for Git file annotation with line tracking - Add branch management including create, delete, rename and compare operations - Implement merge checking with conflict detection and fast-forward handling - Add cherry-pick functionality for applying commits between branches - Integrate gix library for Git repository operations and object handling - Add comprehensive test suite covering all Git operations - Implement proper error handling and repository validation - Add pagination support for large result sets - Create protobuf definitions for all Git operations and data structures - Add build system for gRPC code generation and dependency management
This commit is contained in:
@@ -0,0 +1,375 @@
|
||||
use crate::bare::GitBare;
|
||||
use crate::error::{GitError, GitResult};
|
||||
use crate::oid::ZERO_OID;
|
||||
use crate::pb::{
|
||||
CreateCommitRequest, CreateCommitResponse, GetCommitRequest, ObjectName, ObjectSelector,
|
||||
create_commit_action, object_selector,
|
||||
};
|
||||
|
||||
impl GitBare {
|
||||
pub fn create_commit(&self, request: CreateCommitRequest) -> GitResult<CreateCommitResponse> {
|
||||
let repo = self.gix_repo()?;
|
||||
let start_rev = match request.start_revision.clone().and_then(|s| s.selector) {
|
||||
Some(object_selector::Selector::Oid(oid)) => oid.hex,
|
||||
Some(object_selector::Selector::Revision(name)) => name.revision,
|
||||
None => request.branch.clone(),
|
||||
};
|
||||
let parent_id = repo
|
||||
.rev_parse_single(start_rev.as_str())
|
||||
.ok()
|
||||
.map(|id| id.to_string());
|
||||
let current_branch_tip = repo
|
||||
.find_reference(format!("refs/heads/{}", request.branch).as_str())
|
||||
.ok()
|
||||
.and_then(|mut r| r.peel_to_id().ok())
|
||||
.map(|id| id.to_string());
|
||||
|
||||
let tree_id = if request.actions.is_empty() {
|
||||
let Some(parent) = parent_id.as_ref() else {
|
||||
return Err(GitError::InvalidArgument(
|
||||
"cannot create an empty root commit without file actions".into(),
|
||||
));
|
||||
};
|
||||
self.rev_parse_tree(parent)?
|
||||
} else {
|
||||
self.tree_from_actions(parent_id.as_deref(), &request.actions)?
|
||||
};
|
||||
|
||||
let message = commit_message_with_trailers(&request);
|
||||
let parents: Vec<String> = parent_id.iter().cloned().collect();
|
||||
let commit_id = self.commit_tree(
|
||||
&tree_id,
|
||||
&parents,
|
||||
&message,
|
||||
request.author.as_ref(),
|
||||
request.committer.as_ref(),
|
||||
)?;
|
||||
self.update_branch_after_commit(&request, &commit_id, current_branch_tip.as_deref())?;
|
||||
|
||||
let revision = Some(ObjectSelector {
|
||||
selector: Some(object_selector::Selector::Revision(ObjectName {
|
||||
revision: commit_id,
|
||||
})),
|
||||
});
|
||||
Ok(CreateCommitResponse {
|
||||
commit: Some(self.get_commit(GetCommitRequest {
|
||||
repository: request.repository,
|
||||
revision,
|
||||
include_stats: false,
|
||||
include_raw: false,
|
||||
})?),
|
||||
branch: request.branch,
|
||||
})
|
||||
}
|
||||
|
||||
fn rev_parse_tree(&self, revision: &str) -> GitResult<String> {
|
||||
let result = duct::cmd(
|
||||
"git",
|
||||
[
|
||||
"--git-dir",
|
||||
self.bare_dir.to_string_lossy().as_ref(),
|
||||
"rev-parse",
|
||||
&format!("{revision}^{{tree}}"),
|
||||
],
|
||||
)
|
||||
.stdout_capture()
|
||||
.stderr_capture()
|
||||
.unchecked()
|
||||
.run()?;
|
||||
command_ok(result).map(|stdout| stdout.trim().to_string())
|
||||
}
|
||||
|
||||
fn tree_from_actions(
|
||||
&self,
|
||||
parent_id: Option<&str>,
|
||||
actions: &[crate::pb::CreateCommitAction],
|
||||
) -> GitResult<String> {
|
||||
let tmp_index = tempfile::Builder::new()
|
||||
.prefix("gitks-index-")
|
||||
.tempfile_in(&self.bare_dir)
|
||||
.map_err(GitError::Io)?;
|
||||
let tmp_index_path = tmp_index.path().to_string_lossy().into_owned();
|
||||
|
||||
if let Some(parent) = parent_id {
|
||||
let read_tree = duct::cmd(
|
||||
"git",
|
||||
[
|
||||
"--git-dir",
|
||||
self.bare_dir.to_string_lossy().as_ref(),
|
||||
"read-tree",
|
||||
parent,
|
||||
],
|
||||
)
|
||||
.env("GIT_INDEX_FILE", &tmp_index_path)
|
||||
.stdout_capture()
|
||||
.stderr_capture()
|
||||
.unchecked()
|
||||
.run()?;
|
||||
command_ok(read_tree)?;
|
||||
}
|
||||
|
||||
for action in actions {
|
||||
self.apply_commit_action(&tmp_index_path, action)?;
|
||||
}
|
||||
|
||||
let write_tree = duct::cmd(
|
||||
"git",
|
||||
[
|
||||
"--git-dir",
|
||||
self.bare_dir.to_string_lossy().as_ref(),
|
||||
"write-tree",
|
||||
],
|
||||
)
|
||||
.env("GIT_INDEX_FILE", &tmp_index_path)
|
||||
.stdout_capture()
|
||||
.stderr_capture()
|
||||
.unchecked()
|
||||
.run()?;
|
||||
command_ok(write_tree).map(|stdout| stdout.trim().to_string())
|
||||
}
|
||||
|
||||
fn apply_commit_action(
|
||||
&self,
|
||||
index_path: &str,
|
||||
action: &crate::pb::CreateCommitAction,
|
||||
) -> GitResult<()> {
|
||||
let action_type = create_commit_action::Action::try_from(action.action)
|
||||
.unwrap_or(create_commit_action::Action::CreateCommitActionUnspecified);
|
||||
match action_type {
|
||||
create_commit_action::Action::CreateCommitActionCreate
|
||||
| create_commit_action::Action::CreateCommitActionUpdate => {
|
||||
let hash = duct::cmd(
|
||||
"git",
|
||||
[
|
||||
"--git-dir",
|
||||
self.bare_dir.to_string_lossy().as_ref(),
|
||||
"hash-object",
|
||||
"-w",
|
||||
"--stdin",
|
||||
],
|
||||
)
|
||||
.stdin_bytes(action.content.clone())
|
||||
.stdout_capture()
|
||||
.stderr_capture()
|
||||
.unchecked()
|
||||
.run()?;
|
||||
let blob = command_ok(hash)?.trim().to_string();
|
||||
let mode = if action.executable {
|
||||
"100755"
|
||||
} else {
|
||||
"100644"
|
||||
};
|
||||
self.update_index_cacheinfo(index_path, mode, &blob, &action.file_path)
|
||||
}
|
||||
create_commit_action::Action::CreateCommitActionDelete => {
|
||||
let result = duct::cmd(
|
||||
"git",
|
||||
[
|
||||
"--git-dir",
|
||||
self.bare_dir.to_string_lossy().as_ref(),
|
||||
"update-index",
|
||||
"--force-remove",
|
||||
&action.file_path,
|
||||
],
|
||||
)
|
||||
.env("GIT_INDEX_FILE", index_path)
|
||||
.env("GIT_WORK_TREE", self.bare_dir.to_string_lossy().as_ref())
|
||||
.stdout_capture()
|
||||
.stderr_capture()
|
||||
.unchecked()
|
||||
.run()?;
|
||||
command_ok(result).map(|_| ())
|
||||
}
|
||||
create_commit_action::Action::CreateCommitActionMove => {
|
||||
if action.previous_path.is_empty() {
|
||||
return Err(GitError::InvalidArgument(
|
||||
"MOVE action requires previous_path".into(),
|
||||
));
|
||||
}
|
||||
let (mode, oid) = self.index_entry(index_path, &action.previous_path)?;
|
||||
self.update_index_cacheinfo(index_path, &mode, &oid, &action.file_path)?;
|
||||
let remove = duct::cmd(
|
||||
"git",
|
||||
[
|
||||
"--git-dir",
|
||||
self.bare_dir.to_string_lossy().as_ref(),
|
||||
"update-index",
|
||||
"--force-remove",
|
||||
&action.previous_path,
|
||||
],
|
||||
)
|
||||
.env("GIT_INDEX_FILE", index_path)
|
||||
.env("GIT_WORK_TREE", self.bare_dir.to_string_lossy().as_ref())
|
||||
.stdout_capture()
|
||||
.stderr_capture()
|
||||
.unchecked()
|
||||
.run()?;
|
||||
command_ok(remove).map(|_| ())
|
||||
}
|
||||
create_commit_action::Action::CreateCommitActionChmod => {
|
||||
let (_old_mode, oid) = self.index_entry(index_path, &action.file_path)?;
|
||||
let mode = if action.executable {
|
||||
"100755"
|
||||
} else {
|
||||
"100644"
|
||||
};
|
||||
self.update_index_cacheinfo(index_path, mode, &oid, &action.file_path)
|
||||
}
|
||||
create_commit_action::Action::CreateCommitActionUnspecified => Err(
|
||||
GitError::InvalidArgument("unspecified commit action".into()),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn index_entry(&self, index_path: &str, path: &str) -> GitResult<(String, String)> {
|
||||
let result = duct::cmd(
|
||||
"git",
|
||||
[
|
||||
"--git-dir",
|
||||
self.bare_dir.to_string_lossy().as_ref(),
|
||||
"ls-files",
|
||||
"-s",
|
||||
"--",
|
||||
path,
|
||||
],
|
||||
)
|
||||
.env("GIT_INDEX_FILE", index_path)
|
||||
.env("GIT_WORK_TREE", self.bare_dir.to_string_lossy().as_ref())
|
||||
.stdout_capture()
|
||||
.stderr_capture()
|
||||
.unchecked()
|
||||
.run()?;
|
||||
let stdout = command_ok(result)?;
|
||||
let line = stdout
|
||||
.lines()
|
||||
.next()
|
||||
.ok_or_else(|| GitError::NotFound(path.to_string()))?;
|
||||
let parts = line.split_whitespace().collect::<Vec<_>>();
|
||||
if parts.len() < 2 {
|
||||
return Err(GitError::ParseError(format!(
|
||||
"invalid index entry for {path}: {line}"
|
||||
)));
|
||||
}
|
||||
Ok((parts[0].to_string(), parts[1].to_string()))
|
||||
}
|
||||
|
||||
fn update_index_cacheinfo(
|
||||
&self,
|
||||
index_path: &str,
|
||||
mode: &str,
|
||||
oid: &str,
|
||||
path: &str,
|
||||
) -> GitResult<()> {
|
||||
let result = duct::cmd(
|
||||
"git",
|
||||
[
|
||||
"--git-dir",
|
||||
self.bare_dir.to_string_lossy().as_ref(),
|
||||
"update-index",
|
||||
"--add",
|
||||
"--cacheinfo",
|
||||
mode,
|
||||
oid,
|
||||
path,
|
||||
],
|
||||
)
|
||||
.env("GIT_INDEX_FILE", index_path)
|
||||
.env("GIT_WORK_TREE", self.bare_dir.to_string_lossy().as_ref())
|
||||
.stdout_capture()
|
||||
.stderr_capture()
|
||||
.unchecked()
|
||||
.run()?;
|
||||
command_ok(result).map(|_| ())
|
||||
}
|
||||
|
||||
pub(crate) fn commit_tree(
|
||||
&self,
|
||||
tree_id: &str,
|
||||
parent_ids: &[String],
|
||||
message: &str,
|
||||
author: Option<&crate::pb::Signature>,
|
||||
committer: Option<&crate::pb::Signature>,
|
||||
) -> GitResult<String> {
|
||||
let mut args = vec![
|
||||
"--git-dir".to_string(),
|
||||
self.bare_dir.to_string_lossy().into_owned(),
|
||||
"commit-tree".into(),
|
||||
tree_id.to_string(),
|
||||
];
|
||||
for parent in parent_ids {
|
||||
args.push("-p".into());
|
||||
args.push(parent.clone());
|
||||
}
|
||||
args.push("-m".into());
|
||||
args.push(message.to_string());
|
||||
|
||||
let mut cmd = duct::cmd("git", &args).stdout_capture().stderr_capture();
|
||||
if let Some(author) = author.and_then(|s| s.identity.as_ref()) {
|
||||
cmd = cmd
|
||||
.env("GIT_AUTHOR_NAME", &author.name)
|
||||
.env("GIT_AUTHOR_EMAIL", &author.email);
|
||||
}
|
||||
if let Some(committer) = committer.and_then(|s| s.identity.as_ref()) {
|
||||
cmd = cmd
|
||||
.env("GIT_COMMITTER_NAME", &committer.name)
|
||||
.env("GIT_COMMITTER_EMAIL", &committer.email);
|
||||
}
|
||||
|
||||
let commit = cmd.unchecked().run()?;
|
||||
command_ok(commit).map(|stdout| stdout.trim().to_string())
|
||||
}
|
||||
|
||||
fn update_branch_after_commit(
|
||||
&self,
|
||||
request: &CreateCommitRequest,
|
||||
commit_id: &str,
|
||||
current_branch_tip: Option<&str>,
|
||||
) -> GitResult<()> {
|
||||
let refname = format!("refs/heads/{}", request.branch);
|
||||
let mut args = vec![
|
||||
"--git-dir".to_string(),
|
||||
self.bare_dir.to_string_lossy().into_owned(),
|
||||
"update-ref".into(),
|
||||
refname,
|
||||
commit_id.to_string(),
|
||||
];
|
||||
if !request.force {
|
||||
args.push(current_branch_tip.unwrap_or(ZERO_OID).to_string());
|
||||
}
|
||||
|
||||
let update = duct::cmd("git", &args)
|
||||
.stdout_capture()
|
||||
.stderr_capture()
|
||||
.unchecked()
|
||||
.run()?;
|
||||
command_ok(update).map(|_| ())
|
||||
}
|
||||
}
|
||||
|
||||
fn commit_message_with_trailers(request: &CreateCommitRequest) -> String {
|
||||
if request.trailers.is_empty() {
|
||||
return request.message.clone();
|
||||
}
|
||||
|
||||
let mut message = request.message.trim_end().to_string();
|
||||
message.push_str("\n\n");
|
||||
for trailer in &request.trailers {
|
||||
let separator = if trailer.separator_present { ": " } else { " " };
|
||||
message.push_str(&trailer.key);
|
||||
message.push_str(separator);
|
||||
message.push_str(&trailer.value);
|
||||
message.push('\n');
|
||||
}
|
||||
message
|
||||
}
|
||||
|
||||
pub(crate) fn command_ok(output: std::process::Output) -> GitResult<String> {
|
||||
if output.status.success() {
|
||||
Ok(String::from_utf8_lossy(&output.stdout).into_owned())
|
||||
} else {
|
||||
Err(GitError::CommandFailed {
|
||||
status_code: output.status.code(),
|
||||
stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user