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:
zhenyi
2026-06-04 13:05:38 +08:00
commit dcb0fb74c5
98 changed files with 20569 additions and 0 deletions
+161
View File
@@ -0,0 +1,161 @@
use crate::bare::GitBare;
use crate::commit::create_commit::command_ok;
use crate::error::{GitError, GitResult};
use crate::pb::{CherryPickCommitRequest, CreateCommitResponse, GetCommitRequest};
impl GitBare {
pub fn cherry_pick_commit(
&self,
request: CherryPickCommitRequest,
) -> GitResult<CreateCommitResponse> {
let target_branch = request.branch.clone();
let cp_revision = match request.commit.and_then(|s| s.selector) {
Some(crate::pb::object_selector::Selector::Oid(oid)) => oid.hex,
Some(crate::pb::object_selector::Selector::Revision(name)) => name.revision,
None => return Err(GitError::InvalidArgument("commit is required".into())),
};
let repo = self.gix_repo()?;
let branch_ref = format!("refs/heads/{}", target_branch);
let branch_tip = repo
.find_reference(branch_ref.as_str())
.ok()
.and_then(|mut r| r.peel_to_id().ok())
.map(|id| id.to_string())
.ok_or_else(|| GitError::RefNotFound(target_branch.clone()))?;
let cp_id = repo.rev_parse_single(cp_revision.as_str())?;
let cp_obj = cp_id
.object()?
.try_into_commit()
.map_err(|e| GitError::Gix(e.to_string()))?;
let parent_id = cp_obj.parent_ids().next().map(|p| p.to_string());
let tmp_index = tempfile::Builder::new()
.prefix("gitks-cp-")
.tempfile_in(&self.bare_dir)?;
let idx_path = tmp_index.path().to_string_lossy().into_owned();
let bare = self.bare_dir.to_string_lossy().into_owned();
let read_tree = duct::cmd(
"git",
["--git-dir", bare.as_str(), "read-tree", branch_tip.as_str()],
)
.env("GIT_INDEX_FILE", &idx_path)
.stdout_capture()
.stderr_capture()
.unchecked()
.run()?;
command_ok(read_tree)?;
let mut format_patch_args = vec![
"--git-dir".to_string(),
bare.clone(),
"format-patch".to_string(),
"--stdout".to_string(),
"--full-index".to_string(),
"--binary".to_string(),
"-1".to_string(),
];
if parent_id.is_none() {
format_patch_args.push("--root".to_string());
}
format_patch_args.push(cp_revision.clone());
let diff = duct::cmd("git", &format_patch_args)
.env("GIT_INDEX_FILE", &idx_path)
.stdout_capture()
.stderr_capture()
.unchecked()
.run()?;
let patch_data = command_ok(diff)?;
let apply = duct::cmd(
"git",
[
"--git-dir",
bare.as_str(),
"apply",
"--cached",
"--allow-empty",
"-",
],
)
.env("GIT_INDEX_FILE", &idx_path)
.stdin_bytes(patch_data.as_bytes())
.stdout_capture()
.stderr_capture()
.unchecked()
.run()?;
if !apply.status.success() {
return Err(GitError::Internal(format!(
"cherry-pick apply failed: {}",
String::from_utf8_lossy(&apply.stderr)
)));
}
let write_tree = duct::cmd("git", ["--git-dir", bare.as_str(), "write-tree"])
.env("GIT_INDEX_FILE", &idx_path)
.stdout_capture()
.stderr_capture()
.unchecked()
.run()?;
let tree_id = command_ok(write_tree)?.trim().to_string();
let message = cp_obj.message_raw()?.to_string();
let parents = vec![branch_tip.clone()];
let commit_id = self.commit_tree(
&tree_id,
&parents,
&message,
request.committer.as_ref(),
request.committer.as_ref(),
)?;
self.update_branch_ref(&target_branch, &commit_id, Some(&branch_tip), false)?;
Ok(CreateCommitResponse {
commit: Some(self.get_commit(GetCommitRequest {
repository: request.repository,
revision: Some(crate::pb::ObjectSelector {
selector: Some(crate::pb::object_selector::Selector::Revision(
crate::pb::ObjectName {
revision: commit_id,
},
)),
}),
include_stats: false,
include_raw: false,
})?),
branch: target_branch,
})
}
pub(crate) fn update_branch_ref(
&self,
branch: &str,
commit_id: &str,
old_value: Option<&str>,
force: bool,
) -> GitResult<()> {
let refname = format!("refs/heads/{}", 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 !force {
args.push(old_value.unwrap_or(crate::oid::ZERO_OID).to_string());
}
let update = duct::cmd("git", &args)
.stdout_capture()
.stderr_capture()
.unchecked()
.run()?;
command_ok(update).map(|_| ())
}
}
+94
View File
@@ -0,0 +1,94 @@
use crate::bare::GitBare;
use crate::diff::get_diff_stats::diff_stats_for_range;
use crate::error::{GitError, GitResult};
use crate::paginate;
use crate::pb::{
CommitStats, CompareCommitsRequest, CompareCommitsResponse, GetCommitRequest, object_selector,
};
impl GitBare {
pub fn compare_commits(
&self,
request: CompareCommitsRequest,
) -> GitResult<CompareCommitsResponse> {
let repo = self.gix_repo()?;
let base = match request.base.clone().and_then(|s| s.selector) {
Some(object_selector::Selector::Oid(oid)) => oid.hex,
Some(object_selector::Selector::Revision(name)) => name.revision,
None => "HEAD".into(),
};
let head = match request.head.clone().and_then(|s| s.selector) {
Some(object_selector::Selector::Oid(oid)) => oid.hex,
Some(object_selector::Selector::Revision(name)) => name.revision,
None => "HEAD".into(),
};
let base_id = repo.rev_parse_single(base.as_str())?;
let head_id = repo.rev_parse_single(head.as_str())?;
let merge_base = repo
.merge_base(base_id.detach(), head_id.detach())
.ok()
.map(|id| self.oid_to_pb(id.to_string()));
let range = if request.straight {
format!("{base}..{head}")
} else {
format!("{base}...{head}")
};
let mut args = vec![
"--git-dir".to_string(),
self.bare_dir.to_string_lossy().into_owned(),
"rev-list".into(),
];
if request.first_parent {
args.push("--first-parent".into());
}
args.push(range);
let rev_list = duct::cmd("git", &args)
.stdout_capture()
.stderr_capture()
.unchecked()
.run()?;
if !rev_list.status.success() {
return Err(GitError::CommandFailed {
status_code: rev_list.status.code(),
stderr: String::from_utf8_lossy(&rev_list.stderr).into_owned(),
});
}
let ids = String::from_utf8_lossy(&rev_list.stdout)
.lines()
.map(str::trim)
.filter(|line| !line.is_empty())
.map(ToOwned::to_owned)
.collect::<Vec<_>>();
let (page_ids, page_info) = paginate::paginate(&ids, request.pagination.as_ref());
let mut commits = Vec::with_capacity(page_ids.len());
for id in page_ids {
commits.push(self.get_commit(GetCommitRequest {
repository: request.repository.clone(),
revision: Some(crate::pb::ObjectSelector {
selector: Some(object_selector::Selector::Revision(crate::pb::ObjectName {
revision: id,
})),
}),
include_stats: false,
include_raw: false,
})?);
}
let diff_stats = diff_stats_for_range(self, &base, &head, None)?;
Ok(CompareCommitsResponse {
commits,
stats: Some(CommitStats {
additions: diff_stats.additions,
deletions: diff_stats.deletions,
changed_files: diff_stats.changed_files,
}),
page_info: Some(page_info),
merge_base,
})
}
}
+375
View File
@@ -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(),
})
}
}
+76
View File
@@ -0,0 +1,76 @@
use crate::bare::GitBare;
use crate::error::{GitError, GitResult};
use crate::pb::{Commit, GetCommitRequest, object_selector};
impl GitBare {
pub fn get_commit(&self, request: GetCommitRequest) -> GitResult<Commit> {
let repo = self.gix_repo()?;
let revision = match request.revision.and_then(|s| s.selector) {
Some(object_selector::Selector::Oid(oid)) => oid.hex,
Some(object_selector::Selector::Revision(name)) => name.revision,
None => "HEAD".into(),
};
let id = repo.rev_parse_single(revision.as_str())?;
let commit = id
.object()?
.try_into_commit()
.map_err(|e| GitError::Gix(e.to_string()))?;
let hex = commit.id.to_string();
let tree_hex = commit.tree_id()?.to_string();
let message = commit.message_raw()?.to_string();
let (subject, body) = message
.split_once('\n')
.map(|(s, b)| (s.to_string(), b.trim_start_matches('\n').to_string()))
.unwrap_or_else(|| (message.clone(), String::new()));
let author_sig = commit.author().ok();
let committer_sig = commit.committer().ok();
Ok(Commit {
oid: Some(self.oid_to_pb(hex.clone())),
abbreviated_oid: commit
.short_id()
.map(|s| s.to_string())
.unwrap_or_else(|_| hex.chars().take(7).collect()),
parent_oids: commit
.parent_ids()
.map(|p| self.oid_to_pb(p.to_string()))
.collect(),
tree_oid: Some(self.oid_to_pb(tree_hex)),
author: author_sig.as_ref().map(gix_sig_to_pb),
committer: committer_sig.as_ref().map(gix_sig_to_pb),
subject,
body,
message,
trailers: Vec::new(),
signature: None,
stats: None,
authored_at: author_sig.as_ref().map(|s| prost_types::Timestamp {
seconds: s.seconds(),
nanos: 0,
}),
committed_at: committer_sig.as_ref().map(|s| prost_types::Timestamp {
seconds: s.seconds(),
nanos: 0,
}),
raw: if request.include_raw {
commit.data.clone()
} else {
Vec::new()
},
})
}
}
pub(crate) fn gix_sig_to_pb(sig: &gix::actor::SignatureRef<'_>) -> crate::pb::Signature {
let time = sig.time().ok();
crate::pb::Signature {
identity: Some(crate::pb::Identity {
name: sig.name.to_string(),
email: sig.email.to_string(),
}),
when: Some(prost_types::Timestamp {
seconds: sig.seconds(),
nanos: 0,
}),
timezone_offset: time.map(|t| t.offset / 60).unwrap_or(0),
}
}
+28
View File
@@ -0,0 +1,28 @@
use crate::bare::GitBare;
use crate::error::GitResult;
use crate::pb::{GetCommitAncestorsRequest, GetCommitAncestorsResponse, ListCommitsRequest};
impl GitBare {
pub fn get_commit_ancestors(
&self,
request: GetCommitAncestorsRequest,
) -> GitResult<GetCommitAncestorsResponse> {
let response = self.list_commits(ListCommitsRequest {
repository: request.repository,
revision: request.revision,
path: String::new(),
since: None,
until: None,
first_parent: request.first_parent,
all: false,
reverse: false,
max_parents: 0,
min_parents: 0,
pagination: request.pagination,
})?;
Ok(GetCommitAncestorsResponse {
commits: response.commits,
page_info: response.page_info,
})
}
}
+86
View File
@@ -0,0 +1,86 @@
use crate::bare::GitBare;
use crate::error::{GitError, GitResult};
use crate::paginate;
use crate::pb::{GetCommitRequest, ListCommitsRequest, ListCommitsResponse, object_selector};
impl GitBare {
pub fn list_commits(&self, request: ListCommitsRequest) -> GitResult<ListCommitsResponse> {
let revision = match request.revision.clone().and_then(|s| s.selector) {
Some(object_selector::Selector::Oid(oid)) => oid.hex,
Some(object_selector::Selector::Revision(name)) => name.revision,
None => "HEAD".into(),
};
let mut args = vec![
"--git-dir".to_string(),
self.bare_dir.to_string_lossy().into_owned(),
"rev-list".into(),
];
if request.first_parent {
args.push("--first-parent".into());
}
if request.reverse {
args.push("--reverse".into());
}
if request.max_parents > 0 {
args.push(format!("--max-parents={}", request.max_parents));
}
if request.min_parents > 0 {
args.push(format!("--min-parents={}", request.min_parents));
}
if let Some(since) = request.since.as_ref() {
args.push(format!("--since=@{}", since.seconds));
}
if let Some(until) = request.until.as_ref() {
args.push(format!("--until=@{}", until.seconds));
}
if request.all {
args.push("--all".into());
} else {
args.push(revision);
}
if !request.path.is_empty() {
args.push("--".into());
args.push(request.path.clone());
}
let result = duct::cmd("git", &args)
.stdout_capture()
.stderr_capture()
.unchecked()
.run()?;
if !result.status.success() {
return Err(GitError::CommandFailed {
status_code: result.status.code(),
stderr: String::from_utf8_lossy(&result.stderr).into_owned(),
});
}
let ids = String::from_utf8_lossy(&result.stdout)
.lines()
.map(str::trim)
.filter(|line| !line.is_empty())
.map(ToOwned::to_owned)
.collect::<Vec<_>>();
let (page_ids, page_info) = paginate::paginate(&ids, request.pagination.as_ref());
let mut commits = Vec::with_capacity(page_ids.len());
for id in page_ids {
commits.push(self.get_commit(GetCommitRequest {
repository: request.repository.clone(),
revision: Some(crate::pb::ObjectSelector {
selector: Some(object_selector::Selector::Revision(crate::pb::ObjectName {
revision: id,
})),
}),
include_stats: false,
include_raw: false,
})?);
}
Ok(ListCommitsResponse {
commits,
page_info: Some(page_info),
})
}
}
+7
View File
@@ -0,0 +1,7 @@
pub mod cherry_pick_commit;
pub mod compare_commits;
pub mod create_commit;
pub mod get_commit;
pub mod get_commit_ancestors;
pub mod list_commits;
pub mod revert_commit;
+146
View File
@@ -0,0 +1,146 @@
use crate::bare::GitBare;
use crate::commit::create_commit::command_ok;
use crate::error::{GitError, GitResult};
use crate::pb::{CreateCommitResponse, GetCommitRequest, RevertCommitRequest};
impl GitBare {
pub fn revert_commit(&self, request: RevertCommitRequest) -> GitResult<CreateCommitResponse> {
let target_branch = request.branch.clone();
let revert_revision = match request.commit.and_then(|s| s.selector) {
Some(crate::pb::object_selector::Selector::Oid(oid)) => oid.hex,
Some(crate::pb::object_selector::Selector::Revision(name)) => name.revision,
None => return Err(GitError::InvalidArgument("commit is required".into())),
};
let repo = self.gix_repo()?;
let branch_ref = format!("refs/heads/{}", target_branch);
let branch_tip = repo
.find_reference(branch_ref.as_str())
.ok()
.and_then(|mut r| r.peel_to_id().ok())
.map(|id| id.to_string())
.ok_or_else(|| GitError::RefNotFound(target_branch.clone()))?;
let revert_id = repo.rev_parse_single(revert_revision.as_str())?;
let revert_obj = revert_id
.object()?
.try_into_commit()
.map_err(|e| GitError::Gix(e.to_string()))?;
let parent_ids: Vec<String> = revert_obj.parent_ids().map(|p| p.to_string()).collect();
if parent_ids.len() > 1 {
return Err(GitError::InvalidArgument(
"reverting merge commits is not supported without mainline".into(),
));
}
let parent_hex = parent_ids
.first()
.ok_or_else(|| GitError::InvalidArgument("cannot revert root commit".into()))?;
let tmp_index = tempfile::Builder::new()
.prefix("gitks-revert-")
.tempfile_in(&self.bare_dir)?;
let idx_path = tmp_index.path().to_string_lossy().into_owned();
let bare = self.bare_dir.to_string_lossy().into_owned();
let read_tree = duct::cmd(
"git",
["--git-dir", bare.as_str(), "read-tree", branch_tip.as_str()],
)
.env("GIT_INDEX_FILE", &idx_path)
.stdout_capture()
.stderr_capture()
.unchecked()
.run()?;
command_ok(read_tree)?;
let diff = duct::cmd(
"git",
[
"--git-dir",
bare.as_str(),
"diff",
"--binary",
"--full-index",
revert_revision.as_str(),
parent_hex.as_str(),
],
)
.stdout_capture()
.stderr_capture()
.unchecked()
.run()?;
let patch_data = command_ok(diff)?;
let apply = duct::cmd(
"git",
[
"--git-dir",
bare.as_str(),
"apply",
"--cached",
"--allow-empty",
"-",
],
)
.env("GIT_INDEX_FILE", &idx_path)
.stdin_bytes(patch_data.as_bytes())
.stdout_capture()
.stderr_capture()
.unchecked()
.run()?;
if !apply.status.success() {
return Err(GitError::Internal(format!(
"revert apply failed: {}",
String::from_utf8_lossy(&apply.stderr)
)));
}
let write_tree = duct::cmd("git", ["--git-dir", bare.as_str(), "write-tree"])
.env("GIT_INDEX_FILE", &idx_path)
.stdout_capture()
.stderr_capture()
.unchecked()
.run()?;
let tree_id = command_ok(write_tree)?.trim().to_string();
let subject = revert_obj
.message_raw()?
.to_string()
.lines()
.next()
.unwrap_or_default()
.to_string();
let message = format!(
"Revert \"{}\"\n\nThis reverts commit {}.",
subject, revert_revision
);
let commit_id = self.commit_tree(
&tree_id,
std::slice::from_ref(&branch_tip),
&message,
request.committer.as_ref(),
request.committer.as_ref(),
)?;
self.update_branch_ref(&target_branch, &commit_id, Some(&branch_tip), false)?;
Ok(CreateCommitResponse {
commit: Some(self.get_commit(GetCommitRequest {
repository: request.repository,
revision: Some(crate::pb::ObjectSelector {
selector: Some(crate::pb::object_selector::Selector::Revision(
crate::pb::ObjectName {
revision: commit_id,
},
)),
}),
include_stats: false,
include_raw: false,
})?),
branch: target_branch,
})
}
}
+1
View File
@@ -0,0 +1 @@