chore(git): minor fixes and improvements across git library modules
Apply small fixes across multiple git ops files: handle errors, improve type safety, and refine HTTP handler and SSH git operations.
This commit is contained in:
parent
a26551343c
commit
64dc27161b
@ -266,7 +266,10 @@ impl GitDomain {
|
|||||||
let oid = entry.id();
|
let oid = entry.id();
|
||||||
let obj = match self.repo().find_object(oid, None) {
|
let obj = match self.repo().find_object(oid, None) {
|
||||||
Ok(o) => o,
|
Ok(o) => o,
|
||||||
Err(_) => continue,
|
Err(e) => {
|
||||||
|
tracing::warn!("archive_skip_missing_object oid={} path={} error={}", oid, full_path, e);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let mode = entry.filemode() as u32;
|
let mode = entry.filemode() as u32;
|
||||||
@ -381,7 +384,10 @@ impl GitDomain {
|
|||||||
let oid = entry.id();
|
let oid = entry.id();
|
||||||
let obj = match self.repo().find_object(oid, None) {
|
let obj = match self.repo().find_object(oid, None) {
|
||||||
Ok(o) => o,
|
Ok(o) => o,
|
||||||
Err(_) => continue,
|
Err(e) => {
|
||||||
|
tracing::warn!("archive_skip_missing_object oid={} path={} error={}", oid, full_path, e);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let mode = entry.filemode() as u32;
|
let mode = entry.filemode() as u32;
|
||||||
@ -408,7 +414,7 @@ impl GitDomain {
|
|||||||
.set_path(&full_path)
|
.set_path(&full_path)
|
||||||
.map_err(|e| GitError::Internal(e.to_string()))?;
|
.map_err(|e| GitError::Internal(e.to_string()))?;
|
||||||
header.set_size(content.len() as u64);
|
header.set_size(content.len() as u64);
|
||||||
header.set_mode(mode & 0o755);
|
header.set_mode(mode & 0o777);
|
||||||
header.set_cksum();
|
header.set_cksum();
|
||||||
|
|
||||||
builder
|
builder
|
||||||
@ -457,7 +463,10 @@ impl GitDomain {
|
|||||||
let oid = entry.id();
|
let oid = entry.id();
|
||||||
let obj = match self.repo().find_object(oid, None) {
|
let obj = match self.repo().find_object(oid, None) {
|
||||||
Ok(o) => o,
|
Ok(o) => o,
|
||||||
Err(_) => continue,
|
Err(e) => {
|
||||||
|
tracing::warn!("archive_skip_missing_object oid={} path={} error={}", oid, full_path, e);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let mode = entry.filemode() as u32;
|
let mode = entry.filemode() as u32;
|
||||||
@ -480,7 +489,7 @@ impl GitDomain {
|
|||||||
let content = blob.content();
|
let content = blob.content();
|
||||||
let options = zip::write::SimpleFileOptions::default()
|
let options = zip::write::SimpleFileOptions::default()
|
||||||
.compression_method(zip::CompressionMethod::Deflated)
|
.compression_method(zip::CompressionMethod::Deflated)
|
||||||
.unix_permissions(mode & 0o755);
|
.unix_permissions(mode & 0o777);
|
||||||
|
|
||||||
zip.start_file(&full_path, options)
|
zip.start_file(&full_path, options)
|
||||||
.map_err(|e| GitError::Internal(e.to_string()))?;
|
.map_err(|e| GitError::Internal(e.to_string()))?;
|
||||||
@ -511,14 +520,17 @@ impl GitDomain {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.max_depth.map_or(false, |d| depth >= d) {
|
if opts.max_depth.map_or(false, |d| depth > d) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let oid = entry.id();
|
let oid = entry.id();
|
||||||
let obj = match self.repo().find_object(oid, None) {
|
let obj = match self.repo().find_object(oid, None) {
|
||||||
Ok(o) => o,
|
Ok(o) => o,
|
||||||
Err(_) => continue,
|
Err(e) => {
|
||||||
|
tracing::warn!("archive_list_skip_missing_object oid={} path={} error={}", oid, full_path, e);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let mode = entry.filemode() as u32;
|
let mode = entry.filemode() as u32;
|
||||||
|
|||||||
@ -63,6 +63,9 @@ impl BlameOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl GitDomain {
|
impl GitDomain {
|
||||||
|
/// Blame a file. Note: git2's `blame_file` always operates on HEAD,
|
||||||
|
/// not an arbitrary commit. The `commit_oid` parameter is only
|
||||||
|
/// validated for existence; blame results reflect the current HEAD.
|
||||||
pub fn blame_file(
|
pub fn blame_file(
|
||||||
&self,
|
&self,
|
||||||
commit_oid: &CommitOid,
|
commit_oid: &CommitOid,
|
||||||
@ -238,8 +241,8 @@ impl GitDomain {
|
|||||||
.blame_file(std::path::Path::new(path), Some(&mut blame_opts))
|
.blame_file(std::path::Path::new(path), Some(&mut blame_opts))
|
||||||
.map_err(|e| GitError::Internal(e.to_string()))?;
|
.map_err(|e| GitError::Internal(e.to_string()))?;
|
||||||
|
|
||||||
// Use get_line to find the hunk at the given line
|
// get_line expects 1-based line numbers; caller provides 0-based.
|
||||||
let hunk_opt = blame.get_line(line_no);
|
let hunk_opt = blame.get_line(line_no + 1);
|
||||||
|
|
||||||
match hunk_opt {
|
match hunk_opt {
|
||||||
Some(hunk) => Ok(CommitBlameHunk {
|
Some(hunk) => Ok(CommitBlameHunk {
|
||||||
|
|||||||
@ -58,11 +58,8 @@ impl GitDomain {
|
|||||||
.repo()
|
.repo()
|
||||||
.find_commit(oid.to_oid()?)
|
.find_commit(oid.to_oid()?)
|
||||||
.map_err(|_e| GitError::ObjectNotFound(oid.to_string()))?;
|
.map_err(|_e| GitError::ObjectNotFound(oid.to_string()))?;
|
||||||
let len = oid.0.len();
|
let take = 7.min(oid.0.len());
|
||||||
if len < 7 {
|
Ok(oid.0[..take].to_string())
|
||||||
return Err(GitError::InvalidOid(oid.to_string()));
|
|
||||||
}
|
|
||||||
Ok(oid.0[..7].to_string())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn commit_author(&self, oid: &CommitOid) -> GitResult<CommitSignature> {
|
pub fn commit_author(&self, oid: &CommitOid) -> GitResult<CommitSignature> {
|
||||||
|
|||||||
@ -129,8 +129,8 @@ impl GitDomain {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn rebase_abort(&self) -> GitResult<()> {
|
pub fn rebase_abort(&self) -> GitResult<()> {
|
||||||
// git2 rebase sessions are not persistent across process exits.
|
self.repo()
|
||||||
// The caller resets HEAD to the original position.
|
.cleanup_state()
|
||||||
Ok(())
|
.map_err(|e| GitError::Internal(e.to_string()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,9 +21,17 @@ impl GitDomain {
|
|||||||
|
|
||||||
pub fn config_get(&self, key: &str) -> GitResult<Option<String>> {
|
pub fn config_get(&self, key: &str) -> GitResult<Option<String>> {
|
||||||
let cfg = self.config()?;
|
let cfg = self.config()?;
|
||||||
cfg.get_str(key)
|
match cfg.get_str(key) {
|
||||||
.map(Some)
|
Ok(v) => Ok(Some(v)),
|
||||||
.map_err(|e| GitError::ConfigError(e.to_string()))
|
Err(e) => {
|
||||||
|
// git2 returns an error for not-found keys.
|
||||||
|
if e.code() == git2::ErrorCode::NotFound {
|
||||||
|
Ok(None)
|
||||||
|
} else {
|
||||||
|
Err(GitError::ConfigError(e.to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn config_set(&self, key: &str, value: &str) -> GitResult<()> {
|
pub fn config_set(&self, key: &str, value: &str) -> GitResult<()> {
|
||||||
|
|||||||
@ -281,7 +281,7 @@ impl DiffOptions {
|
|||||||
|
|
||||||
pub fn to_git2(&self) -> git2::DiffOptions {
|
pub fn to_git2(&self) -> git2::DiffOptions {
|
||||||
let mut opts = git2::DiffOptions::new();
|
let mut opts = git2::DiffOptions::new();
|
||||||
if self.context_lines != 3 {
|
if self.context_lines > 0 {
|
||||||
opts.context_lines(self.context_lines);
|
opts.context_lines(self.context_lines);
|
||||||
}
|
}
|
||||||
for p in &self.pathspec {
|
for p in &self.pathspec {
|
||||||
|
|||||||
@ -16,9 +16,14 @@ const POOL_GET_TIMEOUT: Duration = Duration::from_secs(5);
|
|||||||
impl RedisConsumer {
|
impl RedisConsumer {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
pool: deadpool_redis::cluster::Pool,
|
pool: deadpool_redis::cluster::Pool,
|
||||||
prefix: String,
|
mut prefix: String,
|
||||||
block_timeout_secs: u64,
|
block_timeout_secs: u64,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
|
// Redis Cluster requires hash tags ({...}) for multi-key commands
|
||||||
|
// like BLMOVE and Lua scripts to ensure keys hash to the same slot.
|
||||||
|
if !prefix.contains('{') {
|
||||||
|
prefix = format!("{{{}}}", prefix);
|
||||||
|
}
|
||||||
Self {
|
Self {
|
||||||
pool,
|
pool,
|
||||||
prefix,
|
prefix,
|
||||||
|
|||||||
@ -20,6 +20,13 @@ pub fn is_valid_oid(oid: &str) -> bool {
|
|||||||
oid.len() == 40 && oid.chars().all(|c| c.is_ascii_hexdigit())
|
oid.len() == 40 && oid.chars().all(|c| c.is_ascii_hexdigit())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Validate a Git LFS OID (base64-encoded SHA-256 hash, ~44 chars).
|
||||||
|
pub fn is_valid_lfs_oid(oid: &str) -> bool {
|
||||||
|
// base64 alphabet: A-Z, a-z, 0-9, +, /, = (padding)
|
||||||
|
(43..=44).contains(&oid.len())
|
||||||
|
&& oid.chars().all(|c| c.is_ascii_alphanumeric() || c == '+' || c == '/' || c == '=')
|
||||||
|
}
|
||||||
|
|
||||||
pub struct GitHttpHandler {
|
pub struct GitHttpHandler {
|
||||||
storage_path: PathBuf,
|
storage_path: PathBuf,
|
||||||
repo: repo::Model,
|
repo: repo::Model,
|
||||||
@ -248,53 +255,59 @@ fn check_branch_protection(
|
|||||||
let refs = parse_ref_updates(pre_pack)?;
|
let refs = parse_ref_updates(pre_pack)?;
|
||||||
for r#ref in &refs {
|
for r#ref in &refs {
|
||||||
for protection in branch_protects {
|
for protection in branch_protects {
|
||||||
if r#ref.name.starts_with(&protection.branch) {
|
// Match exactly or as directory prefix (e.g. "refs/heads/main"
|
||||||
// Check deletion (new_oid is all zeros / 40 zeros)
|
// matches "refs/heads/main" and "refs/heads/main/*" but NOT
|
||||||
if r#ref.new_oid.as_deref() == Some("0000000000000000000000000000000000000000") {
|
// "refs/heads/main-v2").
|
||||||
if protection.forbid_deletion {
|
let matches = r#ref.name == protection.branch
|
||||||
return Err(format!(
|
|| r#ref.name.starts_with(&format!("{}/", protection.branch));
|
||||||
"Deletion of protected branch '{}' is forbidden",
|
if !matches {
|
||||||
r#ref.name
|
continue;
|
||||||
));
|
}
|
||||||
}
|
// Check deletion (new_oid is all zeros / 40 zeros)
|
||||||
continue;
|
if r#ref.new_oid.as_deref() == Some("0000000000000000000000000000000000000000") {
|
||||||
}
|
if protection.forbid_deletion {
|
||||||
|
|
||||||
// Check tag push
|
|
||||||
if r#ref.name.starts_with("refs/tags/") {
|
|
||||||
if protection.forbid_tag_push {
|
|
||||||
return Err(format!(
|
|
||||||
"Tag push to protected branch '{}' is forbidden",
|
|
||||||
r#ref.name
|
|
||||||
));
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check force push: old != new AND old is non-zero (non-fast-forward update)
|
|
||||||
if let (Some(old_oid), Some(new_oid)) =
|
|
||||||
(r#ref.old_oid.as_deref(), r#ref.new_oid.as_deref())
|
|
||||||
{
|
|
||||||
let is_new_branch = old_oid == "0000000000000000000000000000000000000000";
|
|
||||||
if !is_new_branch
|
|
||||||
&& old_oid != new_oid
|
|
||||||
&& r#ref.name.starts_with("refs/heads/")
|
|
||||||
&& protection.forbid_force_push
|
|
||||||
{
|
|
||||||
return Err(format!(
|
|
||||||
"Force push to protected branch '{}' is forbidden",
|
|
||||||
r#ref.name
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check push
|
|
||||||
if protection.forbid_push {
|
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"Push to protected branch '{}' is forbidden",
|
"Deletion of protected branch '{}' is forbidden",
|
||||||
r#ref.name
|
r#ref.name
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check tag push
|
||||||
|
if r#ref.name.starts_with("refs/tags/") {
|
||||||
|
if protection.forbid_tag_push {
|
||||||
|
return Err(format!(
|
||||||
|
"Tag push to protected branch '{}' is forbidden",
|
||||||
|
r#ref.name
|
||||||
|
));
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check force push: old != new AND old is non-zero (non-fast-forward update)
|
||||||
|
if let (Some(old_oid), Some(new_oid)) =
|
||||||
|
(r#ref.old_oid.as_deref(), r#ref.new_oid.as_deref())
|
||||||
|
{
|
||||||
|
let is_new_branch = old_oid == "0000000000000000000000000000000000000000";
|
||||||
|
if !is_new_branch
|
||||||
|
&& old_oid != new_oid
|
||||||
|
&& r#ref.name.starts_with("refs/heads/")
|
||||||
|
&& protection.forbid_force_push
|
||||||
|
{
|
||||||
|
return Err(format!(
|
||||||
|
"Force push to protected branch '{}' is forbidden",
|
||||||
|
r#ref.name
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check push
|
||||||
|
if protection.forbid_push {
|
||||||
|
return Err(format!(
|
||||||
|
"Push to protected branch '{}' is forbidden",
|
||||||
|
r#ref.name
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
use crate::error::GitError;
|
use crate::error::GitError;
|
||||||
use crate::http::handler::is_valid_oid;
|
use crate::http::handler::is_valid_lfs_oid;
|
||||||
use actix_web::{HttpResponse, web};
|
use actix_web::{HttpResponse, web};
|
||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
use base64::engine::general_purpose::STANDARD;
|
use base64::engine::general_purpose::STANDARD;
|
||||||
@ -244,7 +244,7 @@ impl LfsHandler {
|
|||||||
payload: web::Payload,
|
payload: web::Payload,
|
||||||
_auth_token: &str,
|
_auth_token: &str,
|
||||||
) -> Result<HttpResponse, GitError> {
|
) -> Result<HttpResponse, GitError> {
|
||||||
if !is_valid_oid(oid) {
|
if !is_valid_lfs_oid(oid) {
|
||||||
return Err(GitError::InvalidOid(format!("Invalid OID format: {}", oid)));
|
return Err(GitError::InvalidOid(format!("Invalid OID format: {}", oid)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -332,7 +332,7 @@ impl LfsHandler {
|
|||||||
oid: &str,
|
oid: &str,
|
||||||
_auth_token: &str,
|
_auth_token: &str,
|
||||||
) -> Result<HttpResponse, GitError> {
|
) -> Result<HttpResponse, GitError> {
|
||||||
if !is_valid_oid(oid) {
|
if !is_valid_lfs_oid(oid) {
|
||||||
return Err(GitError::InvalidOid(format!("Invalid OID format: {}", oid)));
|
return Err(GitError::InvalidOid(format!("Invalid OID format: {}", oid)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -382,7 +382,7 @@ impl LfsHandler {
|
|||||||
) -> Result<LockResponse, GitError> {
|
) -> Result<LockResponse, GitError> {
|
||||||
use sea_orm::ActiveModelTrait;
|
use sea_orm::ActiveModelTrait;
|
||||||
|
|
||||||
if !is_valid_oid(oid) {
|
if !is_valid_lfs_oid(oid) {
|
||||||
return Err(GitError::InvalidOid(format!("Invalid OID format: {}", oid)));
|
return Err(GitError::InvalidOid(format!("Invalid OID format: {}", oid)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
use crate::error::GitError;
|
use crate::error::GitError;
|
||||||
use crate::http::HttpAppState;
|
use crate::http::HttpAppState;
|
||||||
use crate::http::handler::is_valid_oid;
|
use crate::http::handler::is_valid_lfs_oid;
|
||||||
use crate::http::lfs::{BatchRequest, CreateLockRequest, LfsHandler};
|
use crate::http::lfs::{BatchRequest, CreateLockRequest, LfsHandler};
|
||||||
use crate::http::utils::get_repo_model;
|
use crate::http::utils::get_repo_model;
|
||||||
use actix_web::{Error, HttpRequest, HttpResponse, web};
|
use actix_web::{Error, HttpRequest, HttpResponse, web};
|
||||||
@ -82,7 +82,7 @@ pub async fn lfs_upload(
|
|||||||
) -> Result<HttpResponse, Error> {
|
) -> Result<HttpResponse, Error> {
|
||||||
let (namespace, repo_name, oid) = path.into_inner();
|
let (namespace, repo_name, oid) = path.into_inner();
|
||||||
|
|
||||||
if !is_valid_oid(&oid) {
|
if !is_valid_lfs_oid(&oid) {
|
||||||
return Err(actix_web::error::ErrorBadRequest("Invalid OID format"));
|
return Err(actix_web::error::ErrorBadRequest("Invalid OID format"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -112,7 +112,7 @@ pub async fn lfs_download(
|
|||||||
) -> Result<HttpResponse, Error> {
|
) -> Result<HttpResponse, Error> {
|
||||||
let (namespace, repo_name, oid) = path.into_inner();
|
let (namespace, repo_name, oid) = path.into_inner();
|
||||||
|
|
||||||
if !is_valid_oid(&oid) {
|
if !is_valid_lfs_oid(&oid) {
|
||||||
return Err(actix_web::error::ErrorBadRequest("Invalid OID format"));
|
return Err(actix_web::error::ErrorBadRequest("Invalid OID format"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -324,11 +324,12 @@ impl GitDomain {
|
|||||||
msg.push_str(&format!("- {}", commit.summary().unwrap_or("(no message)")));
|
msg.push_str(&format!("- {}", commit.summary().unwrap_or("(no message)")));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the squash commit on top of base
|
// Create the squash commit on top of base.
|
||||||
|
// Do NOT update any ref — the caller decides how to use the returned OID.
|
||||||
let squash_oid = self
|
let squash_oid = self
|
||||||
.repo()
|
.repo()
|
||||||
.commit(
|
.commit(
|
||||||
Some("HEAD"),
|
None,
|
||||||
&sig,
|
&sig,
|
||||||
&sig,
|
&sig,
|
||||||
&msg,
|
&msg,
|
||||||
|
|||||||
@ -25,6 +25,7 @@ pub fn validate_ref_name(name: &str) -> Result<(), GitError> {
|
|||||||
|| name.contains('*')
|
|| name.contains('*')
|
||||||
|| name.contains('[')
|
|| name.contains('[')
|
||||||
|| name.contains('\\')
|
|| name.contains('\\')
|
||||||
|
|| name.contains('@')
|
||||||
{
|
{
|
||||||
return Err(GitError::InvalidRefName(format!(
|
return Err(GitError::InvalidRefName(format!(
|
||||||
"invalid ref name: {}",
|
"invalid ref name: {}",
|
||||||
|
|||||||
@ -19,6 +19,8 @@ use std::path::PathBuf;
|
|||||||
use std::process::Stdio;
|
use std::process::Stdio;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
const PRE_PACK_LIMIT: usize = 1_048_576;
|
||||||
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWriteExt};
|
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWriteExt};
|
||||||
use tokio::process::ChildStdin;
|
use tokio::process::ChildStdin;
|
||||||
use tokio::sync::mpsc::Sender;
|
use tokio::sync::mpsc::Sender;
|
||||||
@ -357,6 +359,23 @@ impl russh::server::Handler for SSHandle {
|
|||||||
if matches!(self.service, Some(GitService::ReceivePack)) {
|
if matches!(self.service, Some(GitService::ReceivePack)) {
|
||||||
if !self.branch.contains_key(&channel) {
|
if !self.branch.contains_key(&channel) {
|
||||||
let bf = self.buffer.entry(channel).or_default();
|
let bf = self.buffer.entry(channel).or_default();
|
||||||
|
|
||||||
|
// Reject oversized pre-PACK data to prevent memory exhaustion
|
||||||
|
if bf.len() + data.len() > PRE_PACK_LIMIT {
|
||||||
|
tracing::warn!("ssh_pre_pack_too_large channel={:?}", channel);
|
||||||
|
let msg = "remote: Ref negotiation exceeds size limit\r\n";
|
||||||
|
let _ = session.extended_data(
|
||||||
|
channel,
|
||||||
|
1,
|
||||||
|
CryptoVec::from_slice(msg.as_bytes()),
|
||||||
|
);
|
||||||
|
let _ = session.exit_status_request(channel, 1);
|
||||||
|
let _ = session.eof(channel);
|
||||||
|
let _ = session.close(channel);
|
||||||
|
self.cleanup_channel(channel);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
bf.extend_from_slice(data);
|
bf.extend_from_slice(data);
|
||||||
|
|
||||||
if !bf.windows(4).any(|w| w == b"0000") {
|
if !bf.windows(4).any(|w| w == b"0000") {
|
||||||
@ -377,16 +396,15 @@ impl russh::server::Handler for SSHandle {
|
|||||||
})?;
|
})?;
|
||||||
|
|
||||||
for r#ref in &refs {
|
for r#ref in &refs {
|
||||||
if branch_protect_roles
|
if let Some(msg) =
|
||||||
.iter()
|
check_branch_protection(&branch_protect_roles, r#ref)
|
||||||
.any(|x| r#ref.name.starts_with(&x.branch))
|
|
||||||
{
|
{
|
||||||
let msg =
|
let full_msg =
|
||||||
format!("remote: Branch '{}' is protected\r\n", r#ref.name);
|
format!("remote: {}\r\n", msg);
|
||||||
let _ = session.extended_data(
|
let _ = session.extended_data(
|
||||||
channel,
|
channel,
|
||||||
1,
|
1,
|
||||||
CryptoVec::from_slice(msg.as_bytes()),
|
CryptoVec::from_slice(full_msg.as_bytes()),
|
||||||
);
|
);
|
||||||
let _ = session.exit_status_request(channel, 1);
|
let _ = session.exit_status_request(channel, 1);
|
||||||
let _ = session.eof(channel);
|
let _ = session.eof(channel);
|
||||||
@ -779,6 +797,71 @@ impl FromStr for GitService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Ref name matches a protection rule exactly, or as a directory prefix
|
||||||
|
/// (e.g. "refs/heads/main" matches "refs/heads/main" and "refs/heads/main/*"
|
||||||
|
/// but NOT "refs/heads/main-v2").
|
||||||
|
fn ref_matches_protection(ref_name: &str, protection_branch: &str) -> bool {
|
||||||
|
ref_name == protection_branch
|
||||||
|
|| ref_name.starts_with(&format!("{}/", protection_branch))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Granular branch protection check (same logic as HTTP handler).
|
||||||
|
/// Returns `Some(error_message)` if the push should be rejected.
|
||||||
|
fn check_branch_protection(
|
||||||
|
branch_protects: &[repo_branch_protect::Model],
|
||||||
|
r#ref: &RefUpdate,
|
||||||
|
) -> Option<String> {
|
||||||
|
for protection in branch_protects {
|
||||||
|
if !ref_matches_protection(&r#ref.name, &protection.branch) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check deletion (new_oid is all zeros)
|
||||||
|
if r#ref.new_oid == "0000000000000000000000000000000000000000" {
|
||||||
|
if protection.forbid_deletion {
|
||||||
|
return Some(format!(
|
||||||
|
"Deletion of protected branch '{}' is forbidden",
|
||||||
|
r#ref.name
|
||||||
|
));
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check tag push
|
||||||
|
if r#ref.name.starts_with("refs/tags/") {
|
||||||
|
if protection.forbid_tag_push {
|
||||||
|
return Some(format!(
|
||||||
|
"Tag push to protected branch '{}' is forbidden",
|
||||||
|
r#ref.name
|
||||||
|
));
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check force push: old != new AND old is non-zero (non-fast-forward)
|
||||||
|
let is_new_branch = r#ref.old_oid == "0000000000000000000000000000000000000000";
|
||||||
|
if !is_new_branch
|
||||||
|
&& r#ref.old_oid != r#ref.new_oid
|
||||||
|
&& r#ref.name.starts_with("refs/heads/")
|
||||||
|
&& protection.forbid_force_push
|
||||||
|
{
|
||||||
|
return Some(format!(
|
||||||
|
"Force push to protected branch '{}' is forbidden",
|
||||||
|
r#ref.name
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check push
|
||||||
|
if protection.forbid_push {
|
||||||
|
return Some(format!(
|
||||||
|
"Push to protected branch '{}' is forbidden",
|
||||||
|
r#ref.name
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
async fn forward<'a, R, Fut, Fwd>(
|
async fn forward<'a, R, Fut, Fwd>(
|
||||||
session_handle: &'a Handle,
|
session_handle: &'a Handle,
|
||||||
chan_id: ChannelId,
|
chan_id: ChannelId,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user