Compare commits
15 Commits
Author | SHA1 | Date |
---|---|---|
Anna | fae0e06123 | |
Anna | fae0dae758 | |
Anna | fae0b36248 | |
Anna | fae00948bc | |
Anna | fae08c0d5d | |
Anna | fae00a0a34 | |
Anna | fae07fc045 | |
Anna | fae0b13a6d | |
Anna | fae0a19576 | |
Anna | fae067bd5d | |
Anna | fae0bbfc58 | |
Anna | fae0ddf9d8 | |
Anna | fae095df18 | |
Anna | fae0cda583 | |
Anna | fae0ce0b2b |
|
@ -480,7 +480,6 @@ dependencies = [
|
|||
"iana-time-zone",
|
||||
"js-sys",
|
||||
"num-traits",
|
||||
"time",
|
||||
"wasm-bindgen",
|
||||
"windows-targets 0.48.5",
|
||||
]
|
||||
|
@ -1243,7 +1242,6 @@ name = "git-vain"
|
|||
version = "1.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"clap",
|
||||
"data-encoding",
|
||||
"directories",
|
||||
|
@ -2826,17 +2824,6 @@ dependencies = [
|
|||
"syn 2.0.29",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.1.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"wasi 0.10.0+wasi-snapshot-preview1",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tiny-keccak"
|
||||
version = "2.0.2"
|
||||
|
@ -3048,12 +3035,6 @@ version = "0.9.0+wasi-snapshot-preview1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.10.0+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.0+wasi-snapshot-preview1"
|
||||
|
|
|
@ -16,7 +16,6 @@ crypto-rust = [
|
|||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
chrono = "0.4"
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
data-encoding = "2"
|
||||
directories = "5"
|
||||
|
|
38
README.md
38
README.md
|
@ -10,21 +10,39 @@ Supports:
|
|||
- multithreading
|
||||
- config file for saving common options
|
||||
|
||||
## Install
|
||||
|
||||
Download the [latest release][] or build from source using `cargo`.
|
||||
|
||||
Make sure `git-vain` is in your `$PATH` or running `git vain` won't work.
|
||||
|
||||
To build from source, run `cargo install --path .` or `cargo build --release`
|
||||
in your local checkout.
|
||||
|
||||
[latest release]: https://git.anna.lgbt/anna/git-vain/releases/latest
|
||||
|
||||
## Usage
|
||||
|
||||
Replace the hash of the commit at `HEAD` with one that begins with `c0ffee`:
|
||||
`git vain c0ffee`
|
||||
|
||||
Try using `--help` for more information.
|
||||
|
||||
## Performance
|
||||
|
||||
Performance depends on hardware, method, and number of threads. The tests below
|
||||
were performed on Framework Laptop (A6) with an 11th Gen Intel® Core™ i7-1165G7
|
||||
@ 2.80GHz. Each method was tested attempting to generate the prefix `deadbeef`
|
||||
for 30 seconds using one thread.
|
||||
were performed on an AMD Ryzen 7 3800X 8-Core Processor. Each result is the
|
||||
average of three trials running for 30 seconds using a single thread.
|
||||
|
||||
```
|
||||
counter: 163,504,123 hashes (5,461,682.8102/s)
|
||||
header: 124,617,875 hashes (4,150,993.2260/s)
|
||||
random: 110,886,440 hashes (3,715,452.2177/s)
|
||||
increment: 107,137,532 hashes (3,573,438.8338/s)
|
||||
sequoia*: 864,486 hashes ( 28,923.1870/s)
|
||||
sequoia: 339,407 hashes ( 11,403.6102/s)
|
||||
gpg-agent: 353 hashes ( 11.7519/s)
|
||||
counter: 154,293,195.67 hashes (5,143,106.52/s)
|
||||
random: 134,886,025.00 hashes (4,496,200.83/s)
|
||||
header: 131,035,925.67 hashes (4,367,864.19/s)
|
||||
increment: 95,501,212.00 hashes (3,183,373.73/s)
|
||||
decrement: 95,358,885.33 hashes (3,178,629.51/s)
|
||||
sequoia*: 839,872.67 hashes ( 27,995.76/s)
|
||||
sequoia: 330,972.67 hashes ( 11,032.42/s)
|
||||
gpg-agent: 113.67 hashes ( 3.79/s)
|
||||
```
|
||||
<small>The asterisked sequoia is using the `crypto-rust` feature.</small>
|
||||
|
||||
|
|
164
src/main.rs
164
src/main.rs
|
@ -14,32 +14,19 @@ use git2::Repository;
|
|||
use gpgme::{Context, Protocol, SignMode};
|
||||
use indicatif::ProgressStyle;
|
||||
use keyring::Entry;
|
||||
use openpgp::{
|
||||
packet::prelude::*,
|
||||
parse::Parse,
|
||||
serialize::{
|
||||
Serialize,
|
||||
stream::{Armorer, Message},
|
||||
},
|
||||
types::*,
|
||||
};
|
||||
use rand::Rng;
|
||||
use sequoia_openpgp as openpgp;
|
||||
use sequoia_openpgp::crypto::KeyPair;
|
||||
use sequoia_openpgp::policy::StandardPolicy;
|
||||
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).
|
||||
///
|
||||
/// This works by decrementing the commit time one second and recalculating the
|
||||
/// hash until a hash matching your selected prefix is found.
|
||||
///
|
||||
/// If using PGP signing, the timestamp remains unchanged. Every PGP signature is different, even on
|
||||
/// the same data, so decrementing the timestamp is no longer required.
|
||||
/// Providing a key file is orders of magnitude faster than providing a key ID,
|
||||
/// since using a key ID goes through gpg-agent, which is very slow.
|
||||
#[derive(Parser)]
|
||||
#[command(author, version)]
|
||||
struct Cli {
|
||||
|
@ -386,6 +373,10 @@ fn main() -> Result<()> {
|
|||
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")?;
|
||||
|
@ -465,7 +456,7 @@ fn main() -> Result<()> {
|
|||
|
||||
for _ in 0..threads {
|
||||
let bar = bar.clone();
|
||||
let mut seq_key = seq_key.clone();
|
||||
let seq_key = seq_key.clone();
|
||||
let key = key_id.cloned();
|
||||
let counter = Arc::clone(&counter);
|
||||
let found = Arc::clone(&found);
|
||||
|
@ -499,107 +490,114 @@ fn main() -> Result<()> {
|
|||
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::SeqCst) {
|
||||
let append = if gpg.is_some() || seq_key.is_some() {
|
||||
None
|
||||
} else {
|
||||
match method {
|
||||
while !found.load(Ordering::Relaxed) {
|
||||
let (mut header, append) = if !signing {
|
||||
let append = match method {
|
||||
Method::Random => {
|
||||
let mut bytes = [0; 16];
|
||||
rand::thread_rng().fill(&mut bytes);
|
||||
Some(Cow::from(data_encoding::HEXLOWER.encode(&bytes)))
|
||||
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::SeqCst) + 1;
|
||||
Some(Cow::from(count_buffer.format(count)))
|
||||
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::SeqCst) - 1,
|
||||
Method::Increment => timestamp.fetch_add(1, Ordering::SeqCst) + 1,
|
||||
_ => timestamp.load(Ordering::SeqCst),
|
||||
};
|
||||
|
||||
author_parts[author_len - 2] = buffer.format(timestamp).to_owned();
|
||||
committer_parts[committer_len - 2] = buffer.format(timestamp).to_owned();
|
||||
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!(),
|
||||
};
|
||||
|
||||
header_lines[author_idx] = author_parts.join(" ");
|
||||
header_lines[committer_idx] = committer_parts.join(" ");
|
||||
}
|
||||
author_parts[author_len - 2] = buffer.format(timestamp).to_owned();
|
||||
committer_parts[committer_len - 2] = buffer.format(timestamp).to_owned();
|
||||
|
||||
if method == Method::Header {
|
||||
let count = counter.fetch_add(1, Ordering::SeqCst) + 1;
|
||||
if first {
|
||||
header_lines.insert(committer_idx + 1, String::with_capacity(16));
|
||||
first = false;
|
||||
header_lines[author_idx] = author_parts.join(" ");
|
||||
header_lines[committer_idx] = committer_parts.join(" ");
|
||||
}
|
||||
|
||||
let line = &mut header_lines[committer_idx + 1];
|
||||
line.clear();
|
||||
line.push_str("xvain ");
|
||||
line.push_str(buffer.format(count));
|
||||
}
|
||||
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 mut header = match method {
|
||||
// counter and random don't mutate the header, so don't
|
||||
// allocate for it
|
||||
Method::Counter | Method::Random => Cow::from(&stripped_header),
|
||||
_ => Cow::from(header_lines.join("\n")),
|
||||
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 {
|
||||
let header = header.to_mut();
|
||||
signature_bytes.clear();
|
||||
let to_sign = format!("{header}\n{message}");
|
||||
let mut output = Vec::new();
|
||||
ctx.sign(SignMode::Detached, to_sign, &mut output)
|
||||
ctx.sign(SignMode::Detached, to_sign, &mut signature_bytes)
|
||||
.context("could not sign commit")
|
||||
.unwrap(); // FIXME
|
||||
|
||||
let sig = String::from_utf8(output)
|
||||
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) = &mut seq_key {
|
||||
let header = header.to_mut();
|
||||
let to_sign = format!("{header}\n{message}");
|
||||
let sig = SignatureBuilder::new(SignatureType::Binary)
|
||||
.sign_message(key, to_sign)
|
||||
.context("failed to sign message")
|
||||
.unwrap(); // FIXME
|
||||
|
||||
let mut output = Vec::new();
|
||||
let message = Message::new(&mut output);
|
||||
let mut message = Armorer::new(message)
|
||||
} 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 pgp message")
|
||||
.context("failed to build armorer")
|
||||
.unwrap(); // FIXME
|
||||
Packet::from(sig)
|
||||
.serialize(&mut message)
|
||||
.context("failed to serialise packet")
|
||||
.unwrap(); // FIXME
|
||||
message.finalize()
|
||||
.context("could not finalise message")
|
||||
let mut msg = Signer::new(msg, key.clone())
|
||||
.detached()
|
||||
.build()
|
||||
.context("failed to build signer")
|
||||
.unwrap(); // FIXME
|
||||
|
||||
let sig = String::from_utf8(output)
|
||||
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(' ');
|
||||
|
@ -639,7 +637,7 @@ fn main() -> Result<()> {
|
|||
}
|
||||
}
|
||||
|
||||
found.store(true, Ordering::SeqCst);
|
||||
found.store(true, Ordering::Relaxed);
|
||||
let message = match append {
|
||||
Some(append) => Cow::from(format!("{message}\n{append}\n")),
|
||||
None => Cow::from(&message),
|
||||
|
|
Loading…
Reference in New Issue