| 1 |
|
- |
//! Git abstraction layer, a thin wrapper with no HTTP/Axum concerns. Reads go
|
| 2 |
|
- |
//! through gitoxide; repository creation is still libgit2.
|
|
1 |
+ |
//! Git abstraction layer over gitoxide, with no HTTP/Axum concerns.
|
| 3 |
2 |
|
//!
|
| 4 |
3 |
|
//! All functions take a filesystem path and return `Result<T>`.
|
| 5 |
4 |
|
//! Repository is opened per-request (cheap, just file descriptors).
|
| 14 |
13 |
|
|
| 15 |
14 |
|
use std::path::{Path, PathBuf};
|
| 16 |
15 |
|
|
| 17 |
|
- |
use git2::Repository;
|
| 18 |
16 |
|
use syntect::highlighting::ThemeSet;
|
| 19 |
17 |
|
use syntect::html::{ClassStyle, ClassedHTMLGenerator};
|
| 20 |
18 |
|
use syntect::parsing::SyntaxSet;
|
| 236 |
234 |
|
|
| 237 |
235 |
|
/// Open a bare repository with path traversal validation.
|
| 238 |
236 |
|
///
|
| 239 |
|
- |
/// `pub(crate)` by design: `git2::Repository` is `!Send` and blocks, so every
|
| 240 |
|
- |
/// caller must hold it inside a `spawn_blocking` closure (see
|
| 241 |
|
- |
/// `routes::git::GitState::with_repo`). Keeping the opener crate-private marks
|
| 242 |
|
- |
/// it as an internal primitive, not a general-purpose entry point.
|
|
237 |
+ |
/// `pub(crate)` by design: opening blocks on disk I/O, so every caller must
|
|
238 |
+ |
/// hold it inside a `spawn_blocking` closure (see
|
|
239 |
+ |
/// `routes::git::ResolvedRepo::with_repo`). Keeping the opener crate-private
|
|
240 |
+ |
/// marks it as an internal primitive, not a general-purpose entry point.
|
| 243 |
241 |
|
pub(crate) fn open_repo(
|
| 244 |
242 |
|
repos_root: &Path,
|
| 245 |
243 |
|
owner: &str,
|
| 246 |
244 |
|
repo: &str,
|
| 247 |
|
- |
) -> Result<Repository, GitError> {
|
| 248 |
|
- |
validate_segment(owner)?;
|
| 249 |
|
- |
validate_segment(repo)?;
|
| 250 |
|
- |
|
| 251 |
|
- |
let repo_path = repos_root.join(owner).join(format!("{repo}.git"));
|
| 252 |
|
- |
|
| 253 |
|
- |
// Canonicalize both paths to prevent symlink-based traversal
|
| 254 |
|
- |
let canonical_root = repos_root
|
| 255 |
|
- |
.canonicalize()
|
| 256 |
|
- |
.map_err(|_| GitError::RepoNotFound)?;
|
| 257 |
|
- |
let canonical_repo = repo_path
|
| 258 |
|
- |
.canonicalize()
|
| 259 |
|
- |
.map_err(|_| GitError::RepoNotFound)?;
|
| 260 |
|
- |
|
| 261 |
|
- |
if !canonical_repo.starts_with(&canonical_root) {
|
| 262 |
|
- |
return Err(GitError::RepoNotFound);
|
| 263 |
|
- |
}
|
| 264 |
|
- |
|
| 265 |
|
- |
Repository::open_bare(&canonical_repo).map_err(|_| GitError::RepoNotFound)
|
| 266 |
|
- |
}
|
| 267 |
|
- |
|
| 268 |
|
- |
/// Open a bare repository with gitoxide, with the same path traversal
|
| 269 |
|
- |
/// validation [`open_repo`] applies.
|
| 270 |
|
- |
pub(crate) fn open_gix_repo(
|
| 271 |
|
- |
repos_root: &Path,
|
| 272 |
|
- |
owner: &str,
|
| 273 |
|
- |
repo: &str,
|
| 274 |
245 |
|
) -> Result<gix::Repository, GitError> {
|
| 275 |
246 |
|
open_gix_repo_at(&repo_disk_path(repos_root, owner, repo)?)
|
| 276 |
247 |
|
}
|
| 372 |
343 |
|
/// `receive.maxInputSize`, which makes git-receive-pack abort a push whose pack
|
| 373 |
344 |
|
/// exceeds the cap. Shared by repo creation and the one-time backfill command so
|
| 374 |
345 |
|
/// the limit can never drift between the two.
|
| 375 |
|
- |
pub fn apply_bare_repo_limits(repo: &Repository) -> Result<(), git2::Error> {
|
| 376 |
|
- |
let mut cfg = repo.config()?;
|
| 377 |
|
- |
cfg.set_i64(
|
| 378 |
|
- |
"receive.maxInputSize",
|
| 379 |
|
- |
crate::constants::GIT_SSH_MAX_PACK_BYTES,
|
| 380 |
|
- |
)?;
|
|
346 |
+ |
/// Written straight into the repository's own config file. gitoxide's in-memory
|
|
347 |
+ |
/// config edits are not persisted by `commit()`, and `receive.maxInputSize` is
|
|
348 |
+ |
/// not a key its typed config tree knows, so the file is parsed, amended and
|
|
349 |
+ |
/// rewritten instead.
|
|
350 |
+ |
pub fn apply_bare_repo_limits(repo: &gix::Repository) -> Result<(), GitError> {
|
|
351 |
+ |
let config_path = repo.path().join("config");
|
|
352 |
+ |
let mut config =
|
|
353 |
+ |
gix::config::File::from_path_no_includes(config_path.clone(), gix::config::Source::Local)
|
|
354 |
+ |
.map_err(|e| GitError::Git(e.to_string()))?;
|
|
355 |
+ |
config
|
|
356 |
+ |
.set_raw_value(
|
|
357 |
+ |
"receive.maxInputSize",
|
|
358 |
+ |
crate::constants::GIT_SSH_MAX_PACK_BYTES.to_string(),
|
|
359 |
+ |
)
|
|
360 |
+ |
.map_err(|e| GitError::Git(e.to_string()))?;
|
|
361 |
+ |
std::fs::write(&config_path, config.to_bstring()).map_err(|e| GitError::Git(e.to_string()))?;
|
| 381 |
362 |
|
Ok(())
|
| 382 |
363 |
|
}
|
| 383 |
364 |
|
|
| 384 |
365 |
|
/// Initialize a bare repository on disk with the standard resource limits
|
| 385 |
366 |
|
/// applied. This is the single production path for creating a bare repo, so a
|
| 386 |
367 |
|
/// new repo can never be created without its pack-size cap.
|
| 387 |
|
- |
pub fn init_bare_repo(repo_dir: &Path) -> Result<Repository, GitError> {
|
| 388 |
|
- |
let repo = Repository::init_bare(repo_dir)?;
|
|
368 |
+ |
pub fn init_bare_repo(repo_dir: &Path) -> Result<gix::Repository, GitError> {
|
|
369 |
+ |
let repo = gix::init_bare(repo_dir).map_err(|e| GitError::Git(e.to_string()))?;
|
| 389 |
370 |
|
apply_bare_repo_limits(&repo)?;
|
| 390 |
371 |
|
Ok(repo)
|
| 391 |
372 |
|
}
|
| 405 |
386 |
|
#[error("Path not found")]
|
| 406 |
387 |
|
PathNotFound,
|
| 407 |
388 |
|
#[error("Git error: {0}")]
|
| 408 |
|
- |
Git2(#[from] git2::Error),
|
|
389 |
+ |
Git(String),
|
| 409 |
390 |
|
}
|
| 410 |
391 |
|
|
| 411 |
392 |
|
impl From<GitError> for crate::error::AppError {
|
| 415 |
396 |
|
GitError::RefNotFound | GitError::TreeNotFound | GitError::PathNotFound => {
|
| 416 |
397 |
|
crate::error::AppError::NotFound
|
| 417 |
398 |
|
}
|
| 418 |
|
- |
GitError::Git2(ref git_err)
|
| 419 |
|
- |
if git_err.code() == git2::ErrorCode::NotFound
|
| 420 |
|
- |
&& matches!(
|
| 421 |
|
- |
git_err.class(),
|
| 422 |
|
- |
git2::ErrorClass::Reference | git2::ErrorClass::Object
|
| 423 |
|
- |
) =>
|
| 424 |
|
- |
{
|
| 425 |
|
- |
crate::error::AppError::NotFound
|
| 426 |
|
- |
}
|
| 427 |
|
- |
GitError::Git2(git_err) => crate::error::AppError::Internal(anyhow::anyhow!(git_err)),
|
|
399 |
+ |
GitError::Git(message) => crate::error::AppError::Internal(anyhow::anyhow!(message)),
|
| 428 |
400 |
|
}
|
| 429 |
401 |
|
}
|
| 430 |
402 |
|
}
|
| 467 |
439 |
|
assert!(validate_segment(&long).is_err());
|
| 468 |
440 |
|
}
|
| 469 |
441 |
|
|
|
442 |
+ |
/// Open a test repo through gitoxide.
|
|
443 |
+ |
fn open_gix(bare_path: &std::path::Path) -> gix::Repository {
|
|
444 |
+ |
open_gix_repo_at(bare_path).unwrap()
|
|
445 |
+ |
}
|
|
446 |
+ |
|
|
447 |
+ |
/// Create an empty bare repo under `<tmp>/owner/<name>.git`.
|
|
448 |
+ |
fn init_test_repo(name: &str) -> (tempfile::TempDir, std::path::PathBuf, gix::Repository) {
|
|
449 |
+ |
let tmp = tempfile::TempDir::new().unwrap();
|
|
450 |
+ |
let bare_path = tmp.path().join("owner").join(format!("{name}.git"));
|
|
451 |
+ |
std::fs::create_dir_all(&bare_path).unwrap();
|
|
452 |
+ |
let repo = gix::init_bare(&bare_path).unwrap();
|
|
453 |
+ |
(tmp, bare_path, repo)
|
|
454 |
+ |
}
|
|
455 |
+ |
|
|
456 |
+ |
/// Write a tree from `(name, id, kind)` entries. Git requires tree entries
|
|
457 |
+ |
/// in sorted order, which `gix_object::Tree` handles on write.
|
|
458 |
+ |
fn write_tree(
|
|
459 |
+ |
repo: &gix::Repository,
|
|
460 |
+ |
entries: &[(&str, gix::ObjectId, gix::objs::tree::EntryKind)],
|
|
461 |
+ |
) -> gix::ObjectId {
|
|
462 |
+ |
let mut tree = gix::objs::Tree::empty();
|
|
463 |
+ |
for (name, oid, kind) in entries {
|
|
464 |
+ |
tree.entries.push(gix::objs::tree::Entry {
|
|
465 |
+ |
mode: (*kind).into(),
|
|
466 |
+ |
filename: (*name).into(),
|
|
467 |
+ |
oid: *oid,
|
|
468 |
+ |
});
|
|
469 |
+ |
}
|
|
470 |
+ |
tree.entries.sort();
|
|
471 |
+ |
repo.write_object(&tree).unwrap().detach()
|
|
472 |
+ |
}
|
|
473 |
+ |
|
|
474 |
+ |
/// Commit `tree` onto `refs/heads/main` with a fixed test identity.
|
|
475 |
+ |
fn commit_main(
|
|
476 |
+ |
repo: &gix::Repository,
|
|
477 |
+ |
message: &str,
|
|
478 |
+ |
tree: gix::ObjectId,
|
|
479 |
+ |
parents: Vec<gix::ObjectId>,
|
|
480 |
+ |
) -> gix::ObjectId {
|
|
481 |
+ |
let signature = gix::actor::SignatureRef {
|
|
482 |
+ |
name: "Test".into(),
|
|
483 |
+ |
email: "test@example.com".into(),
|
|
484 |
+ |
time: "1700000000 +0000",
|
|
485 |
+ |
};
|
|
486 |
+ |
repo.commit_as(
|
|
487 |
+ |
signature,
|
|
488 |
+ |
signature,
|
|
489 |
+ |
"refs/heads/main",
|
|
490 |
+ |
message,
|
|
491 |
+ |
tree,
|
|
492 |
+ |
parents,
|
|
493 |
+ |
)
|
|
494 |
+ |
.unwrap()
|
|
495 |
+ |
.detach()
|
|
496 |
+ |
}
|
|
497 |
+ |
|
| 470 |
498 |
|
/// Create a temporary bare repo with some commits for testing.
|
| 471 |
499 |
|
/// Returns (tmpdir, bare_path). The branch name is "main".
|
| 472 |
500 |
|
fn make_test_repo() -> (tempfile::TempDir, std::path::PathBuf) {
|
| 473 |
|
- |
let tmp = tempfile::TempDir::new().unwrap();
|
| 474 |
|
- |
let bare_path = tmp.path().join("owner").join("testrepo.git");
|
| 475 |
|
- |
std::fs::create_dir_all(&bare_path).unwrap();
|
| 476 |
|
- |
let bare_repo = Repository::init_bare(&bare_path).unwrap();
|
|
501 |
+ |
use gix::objs::tree::EntryKind;
|
| 477 |
502 |
|
|
| 478 |
|
- |
// Build tree directly in the bare repo (no clone/push needed)
|
| 479 |
|
- |
let sig = git2::Signature::now("Test", "test@example.com").unwrap();
|
|
503 |
+ |
let (tmp, bare_path, repo) = init_test_repo("testrepo");
|
| 480 |
504 |
|
|
| 481 |
|
- |
// Create blobs
|
| 482 |
|
- |
let readme_oid = bare_repo.blob(b"# Test Repo\n\nHello world.").unwrap();
|
| 483 |
|
- |
let main_rs_oid = bare_repo
|
| 484 |
|
- |
.blob(b"fn main() {\n println!(\"hello\");\n}\n")
|
|
505 |
+ |
let readme = repo.write_blob(b"# Test Repo\n\nHello world.").unwrap();
|
|
506 |
+ |
let main_rs = repo
|
|
507 |
+ |
.write_blob(b"fn main() {\n println!(\"hello\");\n}\n")
|
| 485 |
508 |
|
.unwrap();
|
| 486 |
509 |
|
|
| 487 |
|
- |
// Build src/ subtree
|
| 488 |
|
- |
let mut src_tb = bare_repo.treebuilder(None).unwrap();
|
| 489 |
|
- |
src_tb.insert("main.rs", main_rs_oid, 0o100_644).unwrap();
|
| 490 |
|
- |
let src_tree_oid = src_tb.write().unwrap();
|
|
510 |
+ |
let src_tree = write_tree(&repo, &[("main.rs", main_rs.detach(), EntryKind::Blob)]);
|
|
511 |
+ |
let root_tree = write_tree(
|
|
512 |
+ |
&repo,
|
|
513 |
+ |
&[
|
|
514 |
+ |
("README.md", readme.detach(), EntryKind::Blob),
|
|
515 |
+ |
("src", src_tree, EntryKind::Tree),
|
|
516 |
+ |
],
|
|
517 |
+ |
);
|
| 491 |
518 |
|
|
| 492 |
|
- |
// Build root tree
|
| 493 |
|
- |
let mut root_tb = bare_repo.treebuilder(None).unwrap();
|
| 494 |
|
- |
root_tb.insert("README.md", readme_oid, 0o100_644).unwrap();
|
| 495 |
|
- |
root_tb.insert("src", src_tree_oid, 0o040_000).unwrap();
|
| 496 |
|
- |
let root_tree_oid = root_tb.write().unwrap();
|
| 497 |
|
- |
let root_tree = bare_repo.find_tree(root_tree_oid).unwrap();
|
| 498 |
|
- |
|
| 499 |
|
- |
// Create commit on refs/heads/main
|
| 500 |
|
- |
bare_repo
|
| 501 |
|
- |
.commit(
|
| 502 |
|
- |
Some("refs/heads/main"),
|
| 503 |
|
- |
&sig,
|
| 504 |
|
- |
&sig,
|
| 505 |
|
- |
"Initial commit",
|
| 506 |
|
- |
&root_tree,
|
| 507 |
|
- |
&[],
|
| 508 |
|
- |
)
|
| 509 |
|
- |
.unwrap();
|
| 510 |
|
- |
|
| 511 |
|
- |
// Set HEAD to point to main
|
| 512 |
|
- |
bare_repo.set_head("refs/heads/main").unwrap();
|
|
519 |
+ |
commit_main(&repo, "Initial commit", root_tree, Vec::new());
|
| 513 |
520 |
|
|
| 514 |
521 |
|
(tmp, bare_path)
|
| 515 |
522 |
|
}
|
| 516 |
523 |
|
|
| 517 |
|
- |
/// Open the same test repo through gitoxide. The fixtures are still built
|
| 518 |
|
- |
/// with git2 (write paths are not part of this migration), while the ported
|
| 519 |
|
- |
/// read paths take a `gix::Repository`.
|
| 520 |
|
- |
fn open_gix(bare_path: &std::path::Path) -> gix::Repository {
|
| 521 |
|
- |
open_gix_repo_at(bare_path).unwrap()
|
| 522 |
|
- |
}
|
| 523 |
|
- |
|
| 524 |
524 |
|
#[test]
|
| 525 |
525 |
|
fn open_repo_valid() {
|
| 526 |
526 |
|
let (tmp, _) = make_test_repo();
|
| 545 |
545 |
|
fn blame_gates_oversize_file_before_walk() {
|
| 546 |
546 |
|
// R6-Sec-L3: an over-cap file is rejected by the size gate before the
|
| 547 |
547 |
|
// expensive blame walk; a small file still blames normally.
|
| 548 |
|
- |
let tmp = tempfile::TempDir::new().unwrap();
|
| 549 |
|
- |
let bare_path = tmp.path().join("owner").join("blame.git");
|
| 550 |
|
- |
std::fs::create_dir_all(&bare_path).unwrap();
|
| 551 |
|
- |
let repo = Repository::init_bare(&bare_path).unwrap();
|
| 552 |
|
- |
let sig = git2::Signature::now("Test", "test@example.com").unwrap();
|
|
548 |
+ |
use gix::objs::tree::EntryKind;
|
|
549 |
+ |
|
|
550 |
+ |
let (_tmp, _bare_path, repo) = init_test_repo("blame");
|
| 553 |
551 |
|
|
| 554 |
552 |
|
let big = vec![b'a'; crate::constants::GIT_MAX_FILE_SIZE_BYTES + 1024];
|
| 555 |
|
- |
let big_oid = repo.blob(&big).unwrap();
|
| 556 |
|
- |
let small_oid = repo.blob(b"line one\nline two\n").unwrap();
|
| 557 |
|
- |
let mut tb = repo.treebuilder(None).unwrap();
|
| 558 |
|
- |
tb.insert("big.txt", big_oid, 0o100_644).unwrap();
|
| 559 |
|
- |
tb.insert("small.txt", small_oid, 0o100_644).unwrap();
|
| 560 |
|
- |
let tree_oid = tb.write().unwrap();
|
| 561 |
|
- |
let tree = repo.find_tree(tree_oid).unwrap();
|
| 562 |
|
- |
let commit_oid = repo
|
| 563 |
|
- |
.commit(Some("refs/heads/main"), &sig, &sig, "c", &tree, &[])
|
| 564 |
|
- |
.unwrap();
|
| 565 |
|
- |
|
| 566 |
|
- |
let repo = open_gix(&bare_path);
|
| 567 |
|
- |
let commit_oid = gix::ObjectId::from_bytes_or_panic(commit_oid.as_bytes());
|
|
553 |
+ |
let big_oid = repo.write_blob(&big).unwrap().detach();
|
|
554 |
+ |
let small_oid = repo.write_blob(b"line one\nline two\n").unwrap().detach();
|
|
555 |
+ |
let tree = write_tree(
|
|
556 |
+ |
&repo,
|
|
557 |
+ |
&[
|
|
558 |
+ |
("big.txt", big_oid, EntryKind::Blob),
|
|
559 |
+ |
("small.txt", small_oid, EntryKind::Blob),
|
|
560 |
+ |
],
|
|
561 |
+ |
);
|
|
562 |
+ |
let commit_oid = commit_main(&repo, "c", tree, Vec::new());
|
| 568 |
563 |
|
|
| 569 |
564 |
|
assert!(matches!(
|
| 570 |
565 |
|
blame_file(&repo, commit_oid, "big.txt"),
|
| 684 |
679 |
|
/// Create a test repo with two commits: first adds README.md + src/main.rs,
|
| 685 |
680 |
|
/// second modifies only src/main.rs. Used for file_commit_log tests.
|
| 686 |
681 |
|
fn make_two_commit_repo() -> (tempfile::TempDir, std::path::PathBuf) {
|
| 687 |
|
- |
let tmp = tempfile::TempDir::new().unwrap();
|
| 688 |
|
- |
let bare_path = tmp.path().join("owner").join("testrepo.git");
|
| 689 |
|
- |
std::fs::create_dir_all(&bare_path).unwrap();
|
| 690 |
|
- |
let bare_repo = Repository::init_bare(&bare_path).unwrap();
|
| 691 |
|
- |
let sig = git2::Signature::now("Test", "test@example.com").unwrap();
|
|
682 |
+ |
use gix::objs::tree::EntryKind;
|
|
683 |
+ |
|
|
684 |
+ |
let (tmp, bare_path, repo) = init_test_repo("testrepo");
|
| 692 |
685 |
|
|
| 693 |
686 |
|
// Commit 1: README.md + src/main.rs
|
| 694 |
|
- |
let readme_oid = bare_repo.blob(b"# Test Repo\n").unwrap();
|
| 695 |
|
- |
let main_rs_oid = bare_repo.blob(b"fn main() {}\n").unwrap();
|
| 696 |
|
- |
let mut src_tb = bare_repo.treebuilder(None).unwrap();
|
| 697 |
|
- |
src_tb.insert("main.rs", main_rs_oid, 0o100_644).unwrap();
|
| 698 |
|
- |
let src_tree_oid = src_tb.write().unwrap();
|
| 699 |
|
- |
let mut root_tb = bare_repo.treebuilder(None).unwrap();
|
| 700 |
|
- |
root_tb.insert("README.md", readme_oid, 0o100_644).unwrap();
|
| 701 |
|
- |
root_tb.insert("src", src_tree_oid, 0o040_000).unwrap();
|
| 702 |
|
- |
let root_tree = bare_repo.find_tree(root_tb.write().unwrap()).unwrap();
|
| 703 |
|
- |
let c1 = bare_repo
|
| 704 |
|
- |
.commit(
|
| 705 |
|
- |
Some("refs/heads/main"),
|
| 706 |
|
- |
&sig,
|
| 707 |
|
- |
&sig,
|
| 708 |
|
- |
"Initial commit",
|
| 709 |
|
- |
&root_tree,
|
| 710 |
|
- |
&[],
|
| 711 |
|
- |
)
|
| 712 |
|
- |
.unwrap();
|
| 713 |
|
- |
bare_repo.set_head("refs/heads/main").unwrap();
|
|
687 |
+ |
let readme = repo.write_blob(b"# Test Repo\n").unwrap().detach();
|
|
688 |
+ |
let main_rs = repo.write_blob(b"fn main() {}\n").unwrap().detach();
|
|
689 |
+ |
let src_tree = write_tree(&repo, &[("main.rs", main_rs, EntryKind::Blob)]);
|
|
690 |
+ |
let root_tree = write_tree(
|
|
691 |
+ |
&repo,
|
|
692 |
+ |
&[
|
|
693 |
+ |
("README.md", readme, EntryKind::Blob),
|
|
694 |
+ |
("src", src_tree, EntryKind::Tree),
|
|
695 |
+ |
],
|
|
696 |
+ |
);
|
|
697 |
+ |
let first = commit_main(&repo, "Initial commit", root_tree, Vec::new());
|
| 714 |
698 |
|
|
| 715 |
699 |
|
// Commit 2: modify only src/main.rs
|
| 716 |
|
- |
let first = bare_repo.find_commit(c1).unwrap();
|
| 717 |
|
- |
let main_rs_v2 = bare_repo
|
| 718 |
|
- |
.blob(b"fn main() { println!(\"hello\"); }\n")
|
| 719 |
|
- |
.unwrap();
|
| 720 |
|
- |
let mut src_tb2 = bare_repo.treebuilder(None).unwrap();
|
| 721 |
|
- |
src_tb2.insert("main.rs", main_rs_v2, 0o100_644).unwrap();
|
| 722 |
|
- |
let src2 = src_tb2.write().unwrap();
|
| 723 |
|
- |
let mut root_tb2 = bare_repo.treebuilder(None).unwrap();
|
| 724 |
|
- |
root_tb2.insert("README.md", readme_oid, 0o100_644).unwrap();
|
| 725 |
|
- |
root_tb2.insert("src", src2, 0o040_000).unwrap();
|
| 726 |
|
- |
let root_tree2 = bare_repo.find_tree(root_tb2.write().unwrap()).unwrap();
|
| 727 |
|
- |
bare_repo
|
| 728 |
|
- |
.commit(
|
| 729 |
|
- |
Some("refs/heads/main"),
|
| 730 |
|
- |
&sig,
|
| 731 |
|
- |
&sig,
|
| 732 |
|
- |
"Update main.rs",
|
| 733 |
|
- |
&root_tree2,
|
| 734 |
|
- |
&[&first],
|
| 735 |
|
- |
)
|
| 736 |
|
- |
.unwrap();
|
|
700 |
+ |
let main_rs_v2 = repo
|
|
701 |
+ |
.write_blob(b"fn main() { println!(\"hello\"); }\n")
|
|
702 |
+ |
.unwrap()
|
|
703 |
+ |
.detach();
|
|
704 |
+ |
let src_tree2 = write_tree(&repo, &[("main.rs", main_rs_v2, EntryKind::Blob)]);
|
|
705 |
+ |
let root_tree2 = write_tree(
|
|
706 |
+ |
&repo,
|
|
707 |
+ |
&[
|
|
708 |
+ |
("README.md", readme, EntryKind::Blob),
|
|
709 |
+ |
("src", src_tree2, EntryKind::Tree),
|
|
710 |
+ |
],
|
|
711 |
+ |
);
|
|
712 |
+ |
commit_main(&repo, "Update main.rs", root_tree2, vec![first]);
|
| 737 |
713 |
|
|
| 738 |
714 |
|
(tmp, bare_path)
|
| 739 |
715 |
|
}
|
| 808 |
784 |
|
// Rename tracking comes from gitoxide's diff configuration. libgit2 was
|
| 809 |
785 |
|
// called without `find_similar`, so a rename used to render as an
|
| 810 |
786 |
|
// addition plus a deletion and the Renamed status never appeared.
|
| 811 |
|
- |
let tmp = tempfile::TempDir::new().unwrap();
|
| 812 |
|
- |
let bare_path = tmp.path().join("owner").join("rename.git");
|
| 813 |
|
- |
std::fs::create_dir_all(&bare_path).unwrap();
|
| 814 |
|
- |
let bare = Repository::init_bare(&bare_path).unwrap();
|
| 815 |
|
- |
let sig = git2::Signature::now("Test", "test@example.com").unwrap();
|
|
787 |
+ |
use gix::objs::tree::EntryKind;
|
| 816 |
788 |
|
|
| 817 |
|
- |
let body = b"one\ntwo\nthree\nfour\nfive\nsix\n";
|
| 818 |
|
- |
let blob = bare.blob(body).unwrap();
|
|
789 |
+ |
let (_tmp, _bare_path, repo) = init_test_repo("rename");
|
| 819 |
790 |
|
|
| 820 |
|
- |
let mut tb = bare.treebuilder(None).unwrap();
|
| 821 |
|
- |
tb.insert("before.txt", blob, 0o100_644).unwrap();
|
| 822 |
|
- |
let tree = bare.find_tree(tb.write().unwrap()).unwrap();
|
| 823 |
|
- |
let first = bare
|
| 824 |
|
- |
.commit(Some("refs/heads/main"), &sig, &sig, "add", &tree, &[])
|
| 825 |
|
- |
.unwrap();
|
| 826 |
|
- |
bare.set_head("refs/heads/main").unwrap();
|
|
791 |
+ |
let blob = repo
|
|
792 |
+ |
.write_blob(b"one\ntwo\nthree\nfour\nfive\nsix\n")
|
|
793 |
+ |
.unwrap()
|
|
794 |
+ |
.detach();
|
|
795 |
+ |
|
|
796 |
+ |
let tree = write_tree(&repo, &[("before.txt", blob, EntryKind::Blob)]);
|
|
797 |
+ |
let first = commit_main(&repo, "add", tree, Vec::new());
|
| 827 |
798 |
|
|
| 828 |
799 |
|
// Same blob, new name.
|
| 829 |
|
- |
let mut tb2 = bare.treebuilder(None).unwrap();
|
| 830 |
|
- |
tb2.insert("after.txt", blob, 0o100_644).unwrap();
|
| 831 |
|
- |
let tree2 = bare.find_tree(tb2.write().unwrap()).unwrap();
|
| 832 |
|
- |
let parent = bare.find_commit(first).unwrap();
|
| 833 |
|
- |
bare.commit(
|
| 834 |
|
- |
Some("refs/heads/main"),
|
| 835 |
|
- |
&sig,
|
| 836 |
|
- |
&sig,
|
| 837 |
|
- |
"rename",
|
| 838 |
|
- |
&tree2,
|
| 839 |
|
- |
&[&parent],
|
| 840 |
|
- |
)
|
| 841 |
|
- |
.unwrap();
|
| 842 |
|
- |
|
| 843 |
|
- |
let repo = open_gix(&bare_path);
|
|
800 |
+ |
let tree2 = write_tree(&repo, &[("after.txt", blob, EntryKind::Blob)]);
|
|
801 |
+ |
commit_main(&repo, "rename", tree2, vec![first]);
|
| 844 |
802 |
|
let head = resolve_ref(&repo, "main").unwrap();
|
| 845 |
803 |
|
let files = commit_diff(&repo, head, 10, 1000).unwrap();
|
| 846 |
804 |
|
|
| 858 |
816 |
|
fn commit_diff_escapes_html_in_content() {
|
| 859 |
817 |
|
// Diff lines are rendered into the commit page unescaped by the
|
| 860 |
818 |
|
// template, so the escaping has to happen here.
|
| 861 |
|
- |
let tmp = tempfile::TempDir::new().unwrap();
|
| 862 |
|
- |
let bare_path = tmp.path().join("owner").join("escape.git");
|
| 863 |
|
- |
std::fs::create_dir_all(&bare_path).unwrap();
|
| 864 |
|
- |
let bare = Repository::init_bare(&bare_path).unwrap();
|
| 865 |
|
- |
let sig = git2::Signature::now("Test", "test@example.com").unwrap();
|
|
819 |
+ |
use gix::objs::tree::EntryKind;
|
| 866 |
820 |
|
|
| 867 |
|
- |
let blob = bare.blob(b"<script>alert('x') && y</script>\n").unwrap();
|
| 868 |
|
- |
let mut tb = bare.treebuilder(None).unwrap();
|
| 869 |
|
- |
tb.insert("x.html", blob, 0o100_644).unwrap();
|
| 870 |
|
- |
let tree = bare.find_tree(tb.write().unwrap()).unwrap();
|
| 871 |
|
- |
bare.commit(Some("refs/heads/main"), &sig, &sig, "add", &tree, &[])
|
| 872 |
|
- |
.unwrap();
|
| 873 |
|
- |
bare.set_head("refs/heads/main").unwrap();
|
|
821 |
+ |
let (_tmp, _bare_path, repo) = init_test_repo("escape");
|
| 874 |
822 |
|
|
| 875 |
|
- |
let repo = open_gix(&bare_path);
|
|
823 |
+ |
let blob = repo
|
|
824 |
+ |
.write_blob(b"<script>alert('x') && y</script>\n")
|
|
825 |
+ |
.unwrap()
|
|
826 |
+ |
.detach();
|
|
827 |
+ |
let tree = write_tree(&repo, &[("x.html", blob, EntryKind::Blob)]);
|
|
828 |
+ |
commit_main(&repo, "add", tree, Vec::new());
|
| 876 |
829 |
|
let head = resolve_ref(&repo, "main").unwrap();
|
| 877 |
830 |
|
let files = commit_diff(&repo, head, 10, 1000).unwrap();
|
| 878 |
831 |
|
|