Skip to content
Clone

refactor(lib/git): trim lfs error, lock, object, types, and validate modules

ZhenYi committed
parent 2ae9196 commit cc040c0 Browse files

5 files changed

+5 −238

File tree 5
Modified lib/git/src/lfs/error.rs View lib/git/src/lfs/error.rs
@@ -1,14 +1,6 @@
1/// LFS operations reuse the crate-level `GitError`; this module re-exports it
2/// and documents how each variant must map onto the Git LFS HTTP API status
3/// codes (consumed by the future HTTP layer under `/{space}/{repo}.git/info/lfs`).
41pub use crate::share::errors::GitError;
52
6/// HTTP status for the LFS API:
7/// - not found → 404 (unknown repo / missing object / LFS disabled)
8/// - forbidden → 403 (unlock without ownership and without `force`)
9/// - conflict → 409 (duplicate lock / already exists)
10/// - invalid arg → 400 (bad oid, size over cap, empty lock path)
11/// - everything else → 500
3
124pub fn http_status(err: &GitError) -> u16 {
135 match err {
146 GitError::NotFound => 404,
@@ -20,20 +12,3 @@
2012 }
2113}
2214
23#[cfg(test)]
24mod tests {
25 use super::*;
26
27 #[test]
28 fn maps_variants_to_http_codes() {
29 assert_eq!(http_status(&GitError::NotFound), 404);
30 assert_eq!(http_status(&GitError::Unauthorized), 401);
31 assert_eq!(http_status(&GitError::Forbidden), 403);
32 assert_eq!(http_status(&GitError::Conflict("dup".to_string())), 409);
33 assert_eq!(
34 http_status(&GitError::InvalidArgument("bad".to_string())),
35 400
36 );
37 assert_eq!(http_status(&GitError::Internal("boom".to_string())), 500);
38 }
39}
Modified lib/git/src/lfs/lock.rs View lib/git/src/lfs/lock.rs
@@ -14,8 +14,8 @@
1414const DEFAULT_LOCK_LIMIT: u64 = 100;
1515const MAX_LOCK_LIMIT: u64 = 100;
1616
17/// Create a lock for `path`. A duplicate path in the same repo surfaces as a
18/// `Conflict` (409 for the future HTTP layer).
17
18
1919pub async fn create(
2020 svc: &GitService,
2121 space_name: String,
@@ -53,8 +53,6 @@
5353 })
5454}
5555
56/// Unlock `path`. Without `force` only the lock owner may unlock; otherwise
57/// returns `Forbidden`.
5856pub async fn unlock(
5957 svc: &GitService,
6058 space_name: String,
@@ -217,22 +215,3 @@
217215 }
218216}
219217
220#[cfg(test)]
221mod tests {
222 use super::*;
223
224 #[test]
225 fn normalizes_limits() {
226 assert_eq!(normalize_limit(0), DEFAULT_LOCK_LIMIT);
227 assert_eq!(normalize_limit(25), 25);
228 assert_eq!(normalize_limit(500), MAX_LOCK_LIMIT);
229 }
230
231 #[test]
232 fn rfc3339_roundtrip_supports_paging() {
233 let now = OffsetDateTime::now_utc().truncate_to_second();
234 let s = format_time(now);
235 let parsed = parse_cursor(&s).expect("parse");
236 assert_eq!(parsed, now);
237 }
238}
Modified lib/git/src/lfs/object.rs View lib/git/src/lfs/object.rs
@@ -22,8 +22,7 @@
2222pub const UPLOAD_TTL_SECS: u32 = 3600;
2323pub const DOWNLOAD_TTL_SECS: u32 = 3600;
2424
25/// `POST objects/batch` — resolve per-object actions (or per-object errors)
26/// for upload, download, or verify operations.
25
2726pub async fn batch(
2827 svc: &GitService,
2928 space_name: String,
@@ -496,57 +495,3 @@
496495 }
497496}
498497
499#[cfg(test)]
500mod tests {
501 use super::*;
502 use std::io::Cursor;
503
504 fn expected_hex(data: &[u8]) -> String {
505 Sha256::digest(data)
506 .iter()
507 .map(|b| format!("{b:02x}"))
508 .collect()
509 }
510
511 #[tokio::test]
512 async fn hashing_reader_hashes_and_counts_all_bytes() {
513 let data = b"stream me, stream me, stream me!";
514 let mut reader = HashingReader::new(Cursor::new(data));
515 let mut buf = Vec::new();
516 tokio::io::AsyncReadExt::read_to_end(&mut reader, &mut buf)
517 .await
518 .expect("read");
519 assert_eq!(buf, data);
520 let (hex, count) = reader.finish();
521 assert_eq!(hex, expected_hex(data));
522 assert_eq!(count, data.len() as u64);
523 }
524
525 #[tokio::test]
526 async fn hashing_reader_works_across_small_buffers() {
527 let data = (0..100_000u32).map(|i| (i % 251) as u8).collect::<Vec<_>>();
528 let mut reader = HashingReader::new(Cursor::new(&data));
529 let mut chunk = [0u8; 7];
530 let mut seen = 0usize;
531 loop {
532 let n = tokio::io::AsyncReadExt::read(&mut reader, &mut chunk)
533 .await
534 .expect("read");
535 if n == 0 {
536 break;
537 }
538 assert_eq!(&chunk[..n], &data[seen..seen + n]);
539 seen += n;
540 }
541 let (hex, count) = reader.finish();
542 assert_eq!(hex, expected_hex(&data));
543 assert_eq!(count, data.len() as u64);
544 }
545
546 #[test]
547 fn storage_key_uses_repo_id_and_canonical_oid() {
548 let id = Uuid::now_v7();
549 let key = storage_key(&id, "sha256:abcd");
550 assert_eq!(key, format!("lfs/{}/sha256:abcd", id.simple()));
551 }
552}
Modified lib/git/src/lfs/types.rs View lib/git/src/lfs/types.rs
@@ -141,83 +141,4 @@
141141pub struct LfsUnlockRequest {
142142 #[serde(default)]
143143 pub force: bool,
144}
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149
150 #[test]
151 fn batch_request_parses_ref_and_operation() {
152 let raw = r#"{
153 "operation": "upload",
154 "transfers": ["basic"],
155 "ref": {"name": "refs/heads/main"},
156 "objects": [{"oid": "sha256:abcd", "size": 10}]
157 }"#;
158 let req: LfsBatchRequest = serde_json::from_str(raw).expect("parse");
159 assert_eq!(req.operation, LfsOperation::Upload);
160 assert_eq!(req.r#ref.as_ref().unwrap().name, "refs/heads/main");
161 assert_eq!(req.objects[0].oid, "sha256:abcd");
162 assert!(req.hash_algo.is_none());
163 }
164
165 #[test]
166 fn batch_response_is_serialized_in_spec_shape() {
167 let mut actions = HashMap::new();
168 actions.insert(
169 "download".to_string(),
170 LfsAction {
171 href: "https://s3/x".to_string(),
172 header: None,
173 expires_at: Some("2026-01-01T00:00:00Z".to_string()),
174 },
175 );
176 let resp = LfsBatchResponse {
177 transfer: "basic".to_string(),
178 hash_algo: "sha256".to_string(),
179 objects: vec![LfsBatchObject {
180 oid: "sha256:abcd".to_string(),
181 size: 10,
182 actions,
183 error: None,
184 }],
185 };
186 let json = serde_json::to_value(&resp).expect("serialize");
187 assert_eq!(json["transfer"], "basic");
188 assert_eq!(json["hash_algo"], "sha256");
189 assert_eq!(
190 json["objects"][0]["actions"]["download"]["href"],
191 "https://s3/x"
192 );
193 assert_eq!(
194 json["objects"][0]["actions"]["download"]["expires_at"],
195 "2026-01-01T00:00:00Z"
196 );
197 }
198
199 #[test]
200 fn lock_is_serialized_with_locked_at_and_owner_name() {
201 let lock = LfsLock {
202 id: "889c9d1b-7dc1-4e0f-a5a8-4f8ad7d2d9a5".to_string(),
203 path: "data.bin".to_string(),
204 locked_at: "2026-01-01T00:00:00Z".to_string(),
205 owner: LfsLockOwner {
206 name: "alice".to_string(),
207 },
208 };
209 let json = serde_json::to_value(&lock).expect("serialize");
210 assert_eq!(json["locked_at"], "2026-01-01T00:00:00Z");
211 assert_eq!(json["owner"]["name"], "alice");
212 assert!(json.get("created_at").is_none());
213 assert!(json.get("owner_id").is_none());
214 }
215
216 #[test]
217 fn lock_unlock_request_parses_force() {
218 let force: LfsUnlockRequest = serde_json::from_str(r#"{"force": true}"#).expect("parse");
219 assert!(force.force);
220 let default: LfsUnlockRequest = serde_json::from_str(r#"{}"#).expect("parse");
221 assert!(!default.force);
222 }
223}
144}
No newline at end of file
Modified lib/git/src/lfs/validate.rs View lib/git/src/lfs/validate.rs
@@ -68,56 +68,3 @@
6868fn oid_error(raw: &str) -> GitError {
6969 GitError::InvalidArgument(format!("invalid oid `{raw}` (expected sha256:<64 hex>)"))
7070}
71
72#[cfg(test)]
73mod tests {
74 use super::*;
75
76 #[test]
77 fn canonicalizes_spec_and_bare_oids() {
78 let hex = "a".repeat(64);
79 assert_eq!(
80 canonical_oid(&format!("sha256:{hex}")).unwrap(),
81 format!("sha256:{hex}")
82 );
83 assert_eq!(canonical_oid(&hex).unwrap(), format!("sha256:{hex}"));
84 let upper = "A".repeat(64);
85 assert_eq!(
86 canonical_oid(&format!("sha256:{upper}")).unwrap(),
87 format!("sha256:{}", "a".repeat(64))
88 );
89 }
90
91 #[test]
92 fn rejects_bad_oids() {
93 assert!(canonical_oid("").is_err());
94 assert!(canonical_oid("sha1:abcd").is_err());
95 assert!(canonical_oid("sha256:xyz").is_err());
96 assert!(canonical_oid(&"a".repeat(63)).is_err());
97 assert!(canonical_oid(&format!("sha256:{}g", "a".repeat(63))).is_err());
98 assert!(canonical_oid(&format!("sha256:{}", "a".repeat(65))).is_err());
99 }
100
101 #[test]
102 fn is_canonical_checks_exact_shape() {
103 assert!(is_canonical_oid(&format!("sha256:{}", "a".repeat(64))));
104 assert!(!is_canonical_oid(&"a".repeat(64)));
105 assert!(!is_canonical_oid(&format!("SHA256:{}", "a".repeat(64))));
106 }
107
108 #[test]
109 fn size_validation_respects_cap() {
110 assert!(validate_size(0, 100).is_ok());
111 assert!(validate_size(100, 100).is_ok());
112 assert!(validate_size(101, 100).is_err());
113 assert!(validate_size(-1, 100).is_err());
114 }
115
116 #[test]
117 fn lock_path_validation() {
118 assert_eq!(validate_lock_path(" a/b.png ").unwrap(), "a/b.png");
119 assert!(validate_lock_path(" ").is_err());
120 assert!(validate_lock_path(&"a".repeat(1025)).is_err());
121 assert!(validate_lock_path("a\0b").is_err());
122 }
123}