709 lines
26 KiB
Rust
709 lines
26 KiB
Rust
use std::borrow::Cow;
|
|
use std::fs::File;
|
|
use std::io::{ErrorKind, Write};
|
|
use std::process::Command;
|
|
use std::sync::Arc;
|
|
use std::sync::atomic::{AtomicBool, AtomicI64, AtomicUsize, Ordering};
|
|
|
|
use anyhow::{Context as _, Result};
|
|
use clap::{Parser, ValueEnum};
|
|
use directories::ProjectDirs;
|
|
use flate2::Compression;
|
|
use flate2::write::ZlibEncoder;
|
|
use git2::Repository;
|
|
use gpgme::{Context, Protocol, SignMode};
|
|
use indicatif::ProgressStyle;
|
|
use keyring::Entry;
|
|
use rand::Rng;
|
|
use sequoia_openpgp::{
|
|
self as openpgp,
|
|
crypto::KeyPair,
|
|
parse::Parse,
|
|
policy::StandardPolicy,
|
|
serialize::stream::{Armorer, Message, Signer},
|
|
};
|
|
use serde::Deserialize;
|
|
use sha1::{Digest, Sha1};
|
|
|
|
/// A vanity commit hash generator that's fast, works with PGP signing, and can
|
|
/// be used on arbitrary commits (not just HEAD).
|
|
#[derive(Parser)]
|
|
#[command(author, version)]
|
|
struct Cli {
|
|
/// The commit to replace with a vanity hash.
|
|
#[arg(short, long, default_value_t = String::from("HEAD"))]
|
|
commit: String,
|
|
|
|
/// The key ID to use for signing.
|
|
///
|
|
/// This is very slow compared to providing a key file, but this will use
|
|
/// gpg-agent.
|
|
#[arg(short = 'k', long, value_name = "KEY-ID")]
|
|
signing_key: Option<String>,
|
|
|
|
/// Path to an export secret key to use for signing. This is orders of
|
|
/// magnitude faster than providing a key ID, but it will not use gpg-agent
|
|
/// and requires you to provide your password to git-vain.
|
|
#[arg(short = 'K', long, value_name = "KEY-FILE")]
|
|
signing_key_file: Option<String>,
|
|
|
|
/// Prevent signing of commits. This is useful to override signing keys
|
|
/// configured in the config file.
|
|
#[arg(long)]
|
|
no_sign: bool,
|
|
|
|
/// The number of threads to use when searching for a matching hash.
|
|
/// Defaults to 0, which will use the number of logical cores on your
|
|
/// system.
|
|
#[arg(short, long)]
|
|
threads: Option<usize>,
|
|
|
|
/// The method of changing the commit to generate a new hash. Defaults to
|
|
/// counter.
|
|
///
|
|
/// Note that this has no effect if a signing key is provided.
|
|
///
|
|
/// - decrement will decrease the commit date by one second for each attempt
|
|
///
|
|
/// - increment will increase the commit date by one second for each attempt
|
|
///
|
|
/// - random will append a random string to the commit message for each
|
|
/// attempt
|
|
///
|
|
/// - counter will append an increasing number to the commit message for
|
|
/// each attempt
|
|
///
|
|
/// - header will add an invalid git header line to the commit with an
|
|
/// incrementing counter (git ignores additional headers, but this may not
|
|
/// be compatible with future header changes)
|
|
#[arg(short, long, value_enum)]
|
|
method: Option<Method>,
|
|
|
|
/// Force generating a new vanity hash for commits that already start with
|
|
/// the requested prefix.
|
|
#[arg(short, long)]
|
|
force: bool,
|
|
|
|
/// If this flag is specified, the program will print where it expects an
|
|
/// optional config file to be placed, then it will exit.
|
|
#[arg(long, exclusive = true)]
|
|
print_config_path: bool,
|
|
|
|
/// A key file to save a password for. This prograam will prompt for a
|
|
/// password, then save the given password to your system's key store and
|
|
/// use it to decrypt your secret key for future operations. The program
|
|
/// will exit after saving the password.
|
|
///
|
|
/// This can be used across multiple invocations to save passwords for
|
|
/// multiple keys.
|
|
#[arg(long, exclusive = true, value_name = "KEY-FILE")]
|
|
save_password: Option<String>,
|
|
|
|
/// Instruct git-vain to forget the password for the key ID specified.
|
|
///
|
|
/// Key IDs are the last 16 characters of the key's ID in `gpg -K`.
|
|
#[arg(long, exclusive = true, value_name = "KEY-ID")]
|
|
forget_password: Option<String>,
|
|
|
|
/// The vanity commit hash prefix you desire.
|
|
#[arg(required_unless_present_any(["print_config_path", "save_password", "forget_password"]))]
|
|
prefix: Option<String>,
|
|
}
|
|
|
|
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Deserialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
enum Method {
|
|
Decrement,
|
|
Increment,
|
|
Random,
|
|
Counter,
|
|
Header,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
struct Config {
|
|
signing_key_id: Option<String>,
|
|
signing_key_file: Option<String>,
|
|
threads: Option<usize>,
|
|
method: Option<Method>,
|
|
}
|
|
|
|
fn get_config(args: &Cli) -> Result<Option<Config>> {
|
|
let dirs = match ProjectDirs::from("lgbt.anna", "", "git-vain") {
|
|
Some(d) => d,
|
|
None => {
|
|
if args.print_config_path {
|
|
eprintln!("no home directory could be found");
|
|
}
|
|
|
|
return Ok(None);
|
|
}
|
|
};
|
|
|
|
let config_path = dirs.config_dir().join("config.toml");
|
|
if args.print_config_path {
|
|
println!("{}", config_path.to_string_lossy());
|
|
return Ok(None);
|
|
}
|
|
|
|
let raw_config = match std::fs::read_to_string(&config_path) {
|
|
Ok(c) => c,
|
|
Err(e) if e.kind() == ErrorKind::NotFound => {
|
|
return Ok(None);
|
|
}
|
|
Err(e) => return Err(e).context("could not read config file"),
|
|
};
|
|
|
|
toml::from_str::<Config>(&raw_config)
|
|
.map(Some)
|
|
.context("could not parse config file")
|
|
}
|
|
|
|
fn unlock_key_file(path: &str, check_secrets: bool, return_password: bool) -> Result<(KeyPair, Option<String>)> {
|
|
let key = openpgp::Cert::from_file(path)
|
|
.context("failed to read key from file")?;
|
|
|
|
let mut pw = None;
|
|
let mut keys = Vec::new();
|
|
let p = &StandardPolicy::new();
|
|
for key in key.keys()
|
|
.with_policy(p, None)
|
|
.alive()
|
|
.revoked(false)
|
|
.for_signing()
|
|
.secret()
|
|
.map(|ka| ka.key())
|
|
{
|
|
let key = if key.has_unencrypted_secret() {
|
|
key.clone().into_keypair()
|
|
.context("failed to convert unencrypted key into keypair")?
|
|
} else {
|
|
if check_secrets {
|
|
let key_id = key.keyid().to_hex();
|
|
let entry = Entry::new(PW_SERVICE, &key_id)
|
|
.context("could not get secret provider entry")?;
|
|
match entry.get_password() {
|
|
Ok(p) => match key.clone().decrypt_secret(&p.into()) {
|
|
Ok(decrypted) => {
|
|
let pair = decrypted.into_keypair()
|
|
.context("could not convert key into keypair")?;
|
|
keys.push(pair);
|
|
continue;
|
|
}
|
|
Err(_) => {
|
|
eprintln!("warn: saved password for key {} was incorrect", key_id);
|
|
}
|
|
},
|
|
Err(keyring::Error::NoEntry) => {}
|
|
Err(e) => {
|
|
eprintln!("warn: could not get saved password for key {}: {:#}", key_id, e);
|
|
}
|
|
}
|
|
}
|
|
|
|
let mut first = true;
|
|
loop {
|
|
let mut prompt = inquire::Password::new("Key password: ");
|
|
if !first {
|
|
prompt = prompt.with_help_message("Failed to decrypt the secret key with that password");
|
|
}
|
|
|
|
let password = prompt.without_confirmation()
|
|
.prompt()?;
|
|
let decrypted = key.clone()
|
|
.decrypt_secret(&password.trim().into())
|
|
.context("failed to decrypt secret key");
|
|
if return_password {
|
|
pw = Some(password);
|
|
}
|
|
|
|
first = false;
|
|
|
|
match decrypted {
|
|
Ok(d) => break d.into_keypair()
|
|
.context("failed to convert decrypted key into keypair")?,
|
|
Err(_) => continue,
|
|
}
|
|
}
|
|
};
|
|
|
|
keys.push(key);
|
|
}
|
|
|
|
if keys.is_empty() {
|
|
anyhow::bail!("no suitable signing keys in key file");
|
|
}
|
|
|
|
// we know keys isn't empty
|
|
Ok((
|
|
keys.pop().unwrap(),
|
|
pw,
|
|
))
|
|
}
|
|
|
|
const PW_SERVICE: &str = "lgbt.anna.git-vain";
|
|
const NIBBLES: [[u8; 16]; 16] = [
|
|
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
|
|
[16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31],
|
|
[32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47],
|
|
[48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63],
|
|
[64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79],
|
|
[80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95],
|
|
[96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111],
|
|
[112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127],
|
|
[128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143],
|
|
[144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159],
|
|
[160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175],
|
|
[176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191],
|
|
[192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207],
|
|
[208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223],
|
|
[224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239],
|
|
[240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255],
|
|
];
|
|
|
|
fn main() -> Result<()> {
|
|
let args = Cli::parse();
|
|
let config = get_config(&args)
|
|
.context("could not get configuration")?;
|
|
if args.print_config_path {
|
|
return Ok(());
|
|
}
|
|
|
|
if args.save_password.is_some() && args.forget_password.is_some() {
|
|
anyhow::bail!("cannot save and forget passwords at the same time");
|
|
}
|
|
|
|
if let Some(key_path) = args.save_password {
|
|
let (key, password) = unlock_key_file(&key_path, false, true)
|
|
.context("could not get keypair")?;
|
|
let password = password.context("failed to get password")?;
|
|
let id = key.public().keyid().to_hex();
|
|
let entry = Entry::new(PW_SERVICE, &id)
|
|
.context("could not create entry in secrets provider")?;
|
|
|
|
entry.set_password(&password)
|
|
.context("failed to set password in secrets provider")?;
|
|
|
|
println!("password for key {id} saved");
|
|
return Ok(());
|
|
}
|
|
|
|
if let Some(id) = args.forget_password {
|
|
let entry = Entry::new(PW_SERVICE, &id)
|
|
.context("could not look up entry in secrets provider")?;
|
|
return match entry.delete_password() {
|
|
Ok(()) => {
|
|
println!("password for key {id} forgotten");
|
|
Ok(())
|
|
}
|
|
Err(keyring::Error::NoEntry) => {
|
|
eprintln!("no password saved for key {id}");
|
|
Ok(())
|
|
}
|
|
Err(e) => Err(e).context("could not delete password"),
|
|
};
|
|
}
|
|
|
|
if args.signing_key.is_some() && args.signing_key_file.is_some() {
|
|
anyhow::bail!("specify a key id or key file, not both");
|
|
}
|
|
|
|
if let Some(config) = &config {
|
|
if config.signing_key_id.is_some() && config.signing_key_file.is_some() {
|
|
anyhow::bail!("specify a key id or key file in the config file, not both");
|
|
}
|
|
}
|
|
|
|
let prefix = args.prefix
|
|
.expect("clap should handle this")
|
|
.to_lowercase();
|
|
|
|
if !prefix.chars().all(|c| c.is_ascii_hexdigit()) {
|
|
anyhow::bail!("prefix must be hexadecimal");
|
|
}
|
|
|
|
let (byte_prefix, nibble_values) = if prefix.len() % 2 == 0 {
|
|
// even
|
|
let bytes = data_encoding::HEXLOWER.decode(prefix.as_bytes())
|
|
.context("could not decode prefix as hex")?;
|
|
(bytes, None)
|
|
} else {
|
|
// odd
|
|
let bytes = data_encoding::HEXLOWER.decode(prefix[..prefix.len() - 1].as_bytes())
|
|
.context("could not decode prefix as hex")?;
|
|
let last = prefix.as_bytes()[prefix.len() - 1];
|
|
let idx = match last {
|
|
48..=57 => last - 48,
|
|
97..=102 => last - 97 + 10,
|
|
_ => unreachable!(),
|
|
};
|
|
let nibble = NIBBLES[idx as usize];
|
|
(bytes, Some(nibble))
|
|
};
|
|
|
|
let threads = match args.threads.or_else(|| config.as_ref().and_then(|c| c.threads)) {
|
|
None | Some(0) => num_cpus::get(),
|
|
Some(x) => x,
|
|
};
|
|
|
|
let key_file = if args.no_sign {
|
|
None
|
|
} else {
|
|
match &args.signing_key_file {
|
|
Some(f) => Some(f),
|
|
None if args.signing_key.is_none() => config.as_ref()
|
|
.and_then(|c| c.signing_key_file.as_ref()),
|
|
None => None,
|
|
}
|
|
};
|
|
|
|
let key_id = if args.no_sign {
|
|
None
|
|
} else {
|
|
match &args.signing_key {
|
|
Some(i) => Some(i),
|
|
None if args.signing_key_file.is_none() => config.as_ref()
|
|
.and_then(|c| c.signing_key_id.as_ref()),
|
|
None => None,
|
|
}
|
|
};
|
|
|
|
let mut seq_key = None;
|
|
if let Some(path) = key_file {
|
|
seq_key = Some(unlock_key_file(path, true, false).context("could not get keypair")?.0);
|
|
}
|
|
|
|
if (key_id.is_some() || seq_key.is_some()) && args.method.is_some() {
|
|
eprintln!("warn: method specified will be ignored: commit signing is enabled (try with --no-sign)");
|
|
}
|
|
|
|
// first open the repo and find the commit pointed to by HEAD
|
|
let repo = Repository::open(".")
|
|
.context("no git repository in this directory")?;
|
|
|
|
let obj = repo.revparse_single(&args.commit)
|
|
.context("could not find reference")?;
|
|
let commit = repo.find_commit(obj.id())
|
|
.context("could not resolve reference to a commit")?;
|
|
|
|
if !args.force && commit.id().to_string().starts_with(&prefix) {
|
|
anyhow::bail!("commit already starts with prefix");
|
|
}
|
|
|
|
// grab the header of the commit and the message
|
|
let raw_header = commit.raw_header()
|
|
.context("commit had an invalid header")?;
|
|
|
|
let mut header_lines = Vec::default();
|
|
// strip out the signature lines
|
|
{
|
|
let mut in_sig = false;
|
|
for line in raw_header.split('\n') {
|
|
if line.starts_with("gpgsig ") {
|
|
in_sig = true;
|
|
continue;
|
|
}
|
|
|
|
if in_sig {
|
|
if line.starts_with(' ') {
|
|
continue;
|
|
} else {
|
|
in_sig = false;
|
|
}
|
|
}
|
|
|
|
header_lines.push(line.to_string());
|
|
}
|
|
}
|
|
|
|
let stripped_header = header_lines.join("\n");
|
|
|
|
let message = commit.message_raw()
|
|
.context("commit had an invalid message")?
|
|
.to_string();
|
|
|
|
// find the author and committer
|
|
let author_idx = header_lines.iter()
|
|
.position(|line| line.starts_with("author "))
|
|
.context("missing author")?;
|
|
let committer_idx = header_lines.iter()
|
|
.position(|line| line.starts_with("committer "))
|
|
.context("missing committer")?;
|
|
|
|
// split the lines by spaces
|
|
let author_parts: Vec<_> = header_lines[author_idx].split(' ')
|
|
.map(ToOwned::to_owned)
|
|
.collect();
|
|
let committer_parts: Vec<_> = header_lines[committer_idx].split(' ')
|
|
.map(ToOwned::to_owned)
|
|
.collect();
|
|
// get the timestamp
|
|
let timestamp = author_parts.get(author_parts.len() - 2)
|
|
.context("missing timestamp")?
|
|
.parse::<i64>()
|
|
.context("invalid timestamp")?;
|
|
|
|
let author_len = author_parts.len();
|
|
let committer_len = committer_parts.len();
|
|
|
|
let counter = Arc::new(AtomicUsize::default());
|
|
let found = Arc::new(AtomicBool::new(false));
|
|
let timestamp = Arc::new(AtomicI64::new(timestamp));
|
|
let (commit_tx, commit_rx) = std::sync::mpsc::channel();
|
|
|
|
let bar = indicatif::ProgressBar::new_spinner()
|
|
.with_style(ProgressStyle::with_template("{spinner} {elapsed} - {human_len} hashes ({per_sec})")?);
|
|
|
|
for _ in 0..threads {
|
|
let bar = bar.clone();
|
|
let seq_key = seq_key.clone();
|
|
let key = key_id.cloned();
|
|
let counter = Arc::clone(&counter);
|
|
let found = Arc::clone(&found);
|
|
let timestamp = Arc::clone(×tamp);
|
|
let message = message.clone();
|
|
let byte_prefix = byte_prefix.clone();
|
|
let mut author_parts = author_parts.clone();
|
|
let mut committer_parts = committer_parts.clone();
|
|
let mut header_lines = header_lines.clone();
|
|
let stripped_header = stripped_header.clone();
|
|
let method = args.method
|
|
.or_else(|| config.as_ref().and_then(|c| c.method))
|
|
.unwrap_or(Method::Counter);
|
|
let commit_tx = commit_tx.clone();
|
|
|
|
std::thread::spawn(move || {
|
|
let mut gpg = match key {
|
|
Some(key) => {
|
|
let mut ctx = Context::from_protocol(Protocol::OpenPgp)
|
|
.context("could not create openpgp context")
|
|
.unwrap(); // FIXME
|
|
ctx.set_armor(true);
|
|
let key = ctx.get_secret_key(key)
|
|
.context("could not find secret key")
|
|
.unwrap(); // FIXME
|
|
ctx.add_signer(&key)
|
|
.context("could not add signer")
|
|
.unwrap(); // FIXME
|
|
Some(ctx)
|
|
}
|
|
None => None,
|
|
};
|
|
|
|
let signing = gpg.is_some() || seq_key.is_some();
|
|
let mut signature_bytes = Vec::with_capacity(1024);
|
|
let mut sha1 = Sha1::default();
|
|
let mut buffer = itoa::Buffer::new();
|
|
let mut count_buffer = itoa::Buffer::new();
|
|
let mut random_bytes = [0; 16];
|
|
let mut random_hex = [0; 32];
|
|
let mut first = true;
|
|
while !found.load(Ordering::Relaxed) {
|
|
let (mut header, append) = if !signing {
|
|
let append = match method {
|
|
Method::Random => {
|
|
rand::thread_rng().fill(&mut random_bytes);
|
|
data_encoding::HEXLOWER.encode_mut(&random_bytes, &mut random_hex);
|
|
let random = unsafe {
|
|
std::str::from_utf8_unchecked(&random_hex)
|
|
};
|
|
|
|
Some(random)
|
|
}
|
|
Method::Counter => {
|
|
let count = counter.fetch_add(1, Ordering::Relaxed) + 1;
|
|
Some(count_buffer.format(count))
|
|
}
|
|
_ => None,
|
|
};
|
|
|
|
if matches!(method, Method::Increment | Method::Decrement) {
|
|
let timestamp = match method {
|
|
Method::Decrement => timestamp.fetch_sub(1, Ordering::Relaxed) - 1,
|
|
Method::Increment => timestamp.fetch_add(1, Ordering::Relaxed) + 1,
|
|
_ => unreachable!(),
|
|
};
|
|
|
|
author_parts[author_len - 2] = buffer.format(timestamp).to_owned();
|
|
committer_parts[committer_len - 2] = buffer.format(timestamp).to_owned();
|
|
|
|
header_lines[author_idx] = author_parts.join(" ");
|
|
header_lines[committer_idx] = committer_parts.join(" ");
|
|
}
|
|
|
|
if method == Method::Header {
|
|
let count = counter.fetch_add(1, Ordering::Relaxed) + 1;
|
|
if first {
|
|
header_lines.insert(committer_idx + 1, String::with_capacity(16));
|
|
first = false;
|
|
}
|
|
|
|
let line = &mut header_lines[committer_idx + 1];
|
|
line.clear();
|
|
line.push_str("xvain ");
|
|
line.push_str(buffer.format(count));
|
|
}
|
|
|
|
let header = match method {
|
|
Method::Counter | Method::Random => Cow::from(&stripped_header),
|
|
_ => Cow::from(header_lines.join("\n")),
|
|
};
|
|
|
|
(header, append)
|
|
} else {
|
|
(Cow::from(&stripped_header), None)
|
|
};
|
|
|
|
// NOTE: don't need to handle append here, since we'll never be
|
|
// both appending *and* signing
|
|
if let Some(ctx) = &mut gpg {
|
|
signature_bytes.clear();
|
|
let to_sign = format!("{header}\n{message}");
|
|
ctx.sign(SignMode::Detached, to_sign, &mut signature_bytes)
|
|
.context("could not sign commit")
|
|
.unwrap(); // FIXME
|
|
|
|
let sig = std::str::from_utf8(&signature_bytes)
|
|
.context("signature was not utf-8")
|
|
.unwrap(); // FIXME
|
|
|
|
let header = header.to_mut();
|
|
header.push_str("gpgsig");
|
|
for line in sig.trim().split('\n') {
|
|
header.push(' ');
|
|
header.push_str(line);
|
|
header.push('\n');
|
|
}
|
|
} else if let Some(key) = &seq_key {
|
|
signature_bytes.clear();
|
|
let msg = Message::new(&mut signature_bytes);
|
|
let msg = Armorer::new(msg)
|
|
.kind(openpgp::armor::Kind::Signature)
|
|
.build()
|
|
.context("failed to build armorer")
|
|
.unwrap(); // FIXME
|
|
let mut msg = Signer::new(msg, key.clone())
|
|
.detached()
|
|
.build()
|
|
.context("failed to build signer")
|
|
.unwrap(); // FIXME
|
|
|
|
msg.write_all(header.as_bytes()).unwrap(); // FIXME
|
|
msg.write_all(&[b'\n']).unwrap(); // FIXME
|
|
msg.write_all(message.as_bytes()).unwrap(); // FIXME
|
|
msg.finalize().unwrap(); // FIXME
|
|
|
|
let sig = std::str::from_utf8(&signature_bytes)
|
|
.context("signature was not utf-8")
|
|
.unwrap(); // FIXME
|
|
|
|
let header = header.to_mut();
|
|
header.push_str("gpgsig");
|
|
for line in sig.trim().split('\n') {
|
|
header.push(' ');
|
|
header.push_str(line);
|
|
header.push('\n');
|
|
}
|
|
}
|
|
|
|
let message_len = match &append {
|
|
Some(a) => message.len() + a.len() + 2,
|
|
None => message.len(),
|
|
};
|
|
|
|
sha1.update("commit ");
|
|
sha1.update(buffer.format(header.len() + message_len + 1));
|
|
sha1.update([0]);
|
|
sha1.update(header.as_bytes());
|
|
sha1.update("\n");
|
|
match &append {
|
|
Some(a) => {
|
|
sha1.update(&message);
|
|
sha1.update("\n");
|
|
sha1.update(a.as_bytes());
|
|
sha1.update("\n");
|
|
}
|
|
None => sha1.update(&message),
|
|
}
|
|
|
|
let hash = sha1.finalize_reset();
|
|
bar.inc(1);
|
|
|
|
if hash[0..byte_prefix.len()] == byte_prefix {
|
|
// check the nibble
|
|
if let Some(values) = nibble_values {
|
|
if !values.contains(&hash[byte_prefix.len()]) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
found.store(true, Ordering::Relaxed);
|
|
let message = match append {
|
|
Some(append) => Cow::from(format!("{message}\n{append}\n")),
|
|
None => Cow::from(&message),
|
|
};
|
|
let total = format!("{header}\n{message}");
|
|
let raw_commit = format!("commit {}\0", total.len());
|
|
let hash = data_encoding::HEXLOWER.encode(&hash);
|
|
commit_tx.send((hash, format!("{raw_commit}{total}"))).unwrap();
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
let (hash, new_commit_data) = commit_rx.recv().unwrap();
|
|
bar.finish_and_clear();
|
|
let path = repo.path();
|
|
// let old_commit_id = commit.id().to_string();
|
|
|
|
// let old_path = path.join("objects")
|
|
// .join(&old_commit_id[..2])
|
|
// .join(&old_commit_id[2..]);
|
|
let new_path = path.join("objects")
|
|
.join(&hash[..2])
|
|
.join(&hash[2..]);
|
|
|
|
{
|
|
std::fs::create_dir_all(new_path.parent().unwrap())
|
|
.context("could not create directory")?;
|
|
let file = File::create(new_path)
|
|
.context("could not create commit file")?;
|
|
let mut compressor = ZlibEncoder::new(file, Compression::fast());
|
|
compressor.write_all(new_commit_data.as_bytes())
|
|
.context("could not write commit")?;
|
|
}
|
|
|
|
// let new_commit = repo.find_annotated_commit(Oid::from_str(&hash)?)
|
|
// .context("cannot get new commit")?;
|
|
// let new_new_commit = repo.find_commit(Oid::from_str(&hash)?)
|
|
// .context("cannot get new commit (2)")?;
|
|
// let old_commit = repo.revparse_single(&format!("{}~1", commit.id()))
|
|
// .context("cannot find old commit")?;
|
|
// let old_commit = repo.find_annotated_commit(old_commit.id())
|
|
// .context("cannot get new commit")?;
|
|
// let mut rebase = repo.rebase(None, Some(&old_commit), Some(&new_commit), None)?;
|
|
// while let Some(op) = rebase.next() {
|
|
// op?;
|
|
// rebase.commit(None, &new_new_commit.committer(), None)?;
|
|
// }
|
|
// rebase.finish(None)?;
|
|
let previous = format!("{}~1", commit.id());
|
|
let is_root = repo.revparse_single(&previous).is_err();
|
|
let final_arg = if is_root {
|
|
"--root"
|
|
} else {
|
|
&*previous
|
|
};
|
|
|
|
Command::new("git")
|
|
.args(["rebase", "--onto", &hash, final_arg])
|
|
.spawn()
|
|
.context("could not spawn rebase command")?
|
|
.wait()
|
|
.context("rebase command failed")?;
|
|
eprintln!("{hash}");
|
|
|
|
Ok(())
|
|
}
|