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,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(|_| ())
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
Reference in New Issue
Block a user