A tool to help managing forked repos with their own history
8
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add lock file support to allow reproducible builds

webbeef 54c57290 d85ac98a

+74 -9
+1 -1
Cargo.toml
··· 1 1 [package] 2 2 name = "forkme" 3 - version = "0.1.0" 3 + version = "0.2.0" 4 4 edition = "2021" 5 5 description = "A tool for managing forks using a patch-based approach" 6 6 license = "AGPL-3.0-only"
+9 -2
src/commands/apply.rs
··· 3 3 use std::fs; 4 4 use std::path::Path; 5 5 6 - use crate::config::Config; 6 + use crate::config::{self, Config}; 7 7 use crate::git::{self, SOURCE_DIR}; 8 8 use crate::patch::{self, PatchEntry}; 9 9 ··· 19 19 } 20 20 21 21 git::ensure_on_forkme_branch(&repo)?; 22 - git::reset_to_upstream(&repo, &config.upstream.branch)?; 22 + 23 + // Reset to locked commit if available, otherwise upstream branch 24 + if let Some(locked_sha) = config::load_lock()? { 25 + let oid = git::resolve_commit(&repo, &locked_sha)?; 26 + git::reset_to_commit(&repo, oid)?; 27 + } else { 28 + git::reset_to_upstream(&repo, &config.upstream.branch)?; 29 + } 23 30 24 31 apply_patches(&repo)?; 25 32
+9 -3
src/commands/init.rs
··· 3 3 use std::io::Write; 4 4 use std::path::Path; 5 5 6 - use crate::config::{Config, Upstream}; 6 + use crate::config::{self, Config, Upstream}; 7 7 use crate::git::{self, SOURCE_DIR}; 8 8 use crate::patch; 9 9 ··· 44 44 // Clone the repository 45 45 let repo = git::clone_repo(&url, branch, depth)?; 46 46 47 - // Create the forkme branch 48 - git::create_forkme_branch(&repo, branch)?; 47 + // Create the forkme branch - from locked commit if available 48 + if let Some(locked_sha) = config::load_lock()? { 49 + let oid = git::resolve_commit(&repo, &locked_sha)?; 50 + git::create_forkme_branch_at(&repo, oid)?; 51 + println!("Using locked upstream revision: {}", &locked_sha[..12]); 52 + } else { 53 + git::create_forkme_branch(&repo, branch)?; 54 + } 49 55 50 56 // Create patches directory 51 57 patch::ensure_patches_dir()?;
+6 -1
src/commands/sync.rs
··· 1 1 use anyhow::Result; 2 2 use std::collections::HashSet; 3 3 4 - use crate::config::Config; 4 + use crate::config::{self, Config}; 5 5 use crate::git::{self, FileContent}; 6 6 use crate::patch::{self, PatchEntry}; 7 7 ··· 97 97 // Clean up empty directories 98 98 patch::cleanup_empty_dirs()?; 99 99 100 + // Update the lock file with current upstream commit 101 + let upstream_sha = git::get_upstream_commit_sha(&repo, &config.upstream.branch)?; 102 + config::save_lock(&upstream_sha)?; 103 + 100 104 println!("\nSynced {} files.", processed_files.len()); 105 + println!("Updated forkme.lock to {}", &upstream_sha[..12]); 101 106 102 107 if !skipped_files.is_empty() { 103 108 println!(
+21
src/config.rs
··· 4 4 use std::path::Path; 5 5 6 6 const CONFIG_FILE: &str = "forkme.toml"; 7 + const LOCK_FILE: &str = "forkme.lock"; 7 8 8 9 #[derive(Debug, Serialize, Deserialize)] 9 10 pub struct Config { ··· 41 42 pub fn exists() -> bool { 42 43 Path::new(CONFIG_FILE).exists() 43 44 } 45 + } 46 + 47 + pub fn load_lock() -> Result<Option<String>> { 48 + let path = Path::new(LOCK_FILE); 49 + if !path.exists() { 50 + return Ok(None); 51 + } 52 + let content = 53 + fs::read_to_string(path).with_context(|| format!("Failed to read {}", LOCK_FILE))?; 54 + let sha = content.trim().to_string(); 55 + if sha.is_empty() { 56 + return Ok(None); 57 + } 58 + Ok(Some(sha)) 59 + } 60 + 61 + pub fn save_lock(sha: &str) -> Result<()> { 62 + fs::write(LOCK_FILE, format!("{}\n", sha)) 63 + .with_context(|| format!("Failed to write {}", LOCK_FILE))?; 64 + Ok(()) 44 65 } 45 66 46 67 #[cfg(test)]
+28 -2
src/git.rs
··· 42 42 .with_context(|| format!("Failed to find remote branch {}", upstream_ref))?; 43 43 44 44 let commit = reference.peel_to_commit()?; 45 + create_forkme_branch_at(repo, commit.id()) 46 + } 47 + 48 + pub fn create_forkme_branch_at(repo: &Repository, oid: git2::Oid) -> Result<()> { 49 + let commit = repo.find_commit(oid)?; 45 50 46 51 // Create the forkme branch 47 52 repo.branch(FORKME_BRANCH, &commit, false) ··· 65 70 Ok(reference.peel_to_commit()?.id()) 66 71 } 67 72 73 + pub fn get_upstream_commit_sha(repo: &Repository, branch: &str) -> Result<String> { 74 + let oid = get_upstream_commit(repo, branch)?; 75 + Ok(oid.to_string()) 76 + } 77 + 78 + pub fn resolve_commit(repo: &Repository, sha: &str) -> Result<git2::Oid> { 79 + let oid = git2::Oid::from_str(sha).with_context(|| format!("Invalid commit SHA: {}", sha))?; 80 + // Verify the commit exists 81 + repo.find_commit(oid).with_context(|| { 82 + format!( 83 + "Commit {} not found. You may need a deeper clone (remove --depth option).", 84 + sha 85 + ) 86 + })?; 87 + Ok(oid) 88 + } 89 + 68 90 pub fn reset_to_upstream(repo: &Repository, branch: &str) -> Result<()> { 69 91 let upstream_oid = get_upstream_commit(repo, branch)?; 70 - let commit = repo.find_commit(upstream_oid)?; 92 + reset_to_commit(repo, upstream_oid) 93 + } 94 + 95 + pub fn reset_to_commit(repo: &Repository, oid: git2::Oid) -> Result<()> { 96 + let commit = repo.find_commit(oid)?; 71 97 let obj = commit.as_object(); 72 98 73 99 repo.reset(obj, ResetType::Hard, None) 74 100 .with_context(|| "Failed to reset to upstream")?; 75 101 76 - println!("Reset to upstream {}", branch); 102 + println!("Reset to {}", &oid.to_string()[..12]); 77 103 Ok(()) 78 104 } 79 105