Skip to content

support for openpgp, ssh, x509 signatures with tests #1538

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 6 commits into
base: master
Choose a base branch
from

Conversation

mhammerly
Copy link

@mhammerly mhammerly commented Apr 19, 2025

yet another PR enters the mix for #465

mostly based on:

my additions:

  • updating the MSRV to 1.80, in line with the git2-ext MSRV
  • fixed existing snapshot tests
  • adds integration tests for creating commit signatures using gpg, ssh-keygen, and gpgsm.

there are three integration tests:

  • one for openpgp signatures, which is gated by a test_gpg feature flag and requires a TEST_GPG env var to point at a gpg binary
  • one for ssh signatures, which is gated by a test_ssh feature flag and requires a TEST_SSH_KEYGEN env var to point at an ssh-keygen binary
  • one for x509 signatures, which is gated by a test_x509 feature flag and requires a TEST_GPGSM env var to point at a gpgsm binary

the openpgp and x509 tests actually expect GnuPG tools because their keyring format or whatever is checked in (meaning, no smimesign for mac)

run them with:

$ cargo test -p git-branchless-lib --test test_sign --all-features

@mhammerly mhammerly force-pushed the gpg-support branch 2 times, most recently from 36d6600 to 97c5af9 Compare April 19, 2025 17:47
@claytonrcarter
Copy link
Collaborator

this PR is not ready to merge. git-branchless depends on git2 version 0.20.0 whereas the new dependency git2-ext caps its dependency at 0.18.0. we need to resolve that before merging

If I understand the deps situation correctly, we are waiting on:

  • git-fixture is updated but needs a release
  • git2-ext (which depends on git-fixture) needs an update and release

The good news is that I believe @epage maintains both of those and has contributed to this project in the past. So, if you can open a PR to update git2-ext, we may be able to beg a favor to get the deps straightened out.

I don't have any knowledge or experience w/ gpg or commit signing, but I may be able to do some code review to shepherd this along. It's a much requested feature!

@mhammerly
Copy link
Author

mhammerly commented Apr 19, 2025

i pinged ed on #465 (comment) to ask whether there was a reason the dependency version was capped at 0.18 and request these updates. i suppose opening a PR to do the git2 update is easy enough, though we'll need another PR to update git-fixture when that gets released

EDIT i forgot i tried to do that earlier. can't update the git2 dep without also updating git-fixture because the current latest release locks 0.18 as well. whatever, i can just update the PR when a new release is pushed gitext-rs/git2-ext#92

@epage
Copy link
Collaborator

epage commented Apr 21, 2025

Sorry, no one had told me they were moving forward with this and needed a release with support for new git2 versions.

I've released v0.6.3.

@mhammerly
Copy link
Author

no apology needed! thank you for implementing the signing functionality in the first place, and for doing this little chore to unblock integrating it! i'm using it at work every day and it works great!

@mhammerly mhammerly force-pushed the gpg-support branch 3 times, most recently from ef87113 to 2cd3c0f Compare April 21, 2025 17:59
@mhammerly
Copy link
Author

alright, updated git2-ext, quoted the rustc version strings because yaml was parsing 1.80 to 1.8, set the paths for gpg, gpgsm, ssh-keygen in test invocations

remaining issue is apparently gpg.format ssh support was only added in git >= 2.34 so we can't use --all-features on older versions. i'll deal with that later. but the other two tests pass, and if the 2.37.3 test job goes first then all three should pass

Copy link
Collaborator

@claytonrcarter claytonrcarter left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for shepherding this work through, and to the previous authors for getting it started. Most of this looked pretty straightforward and made sense to me, but there were a few places I requested some changes, most of which were to refactor this code into a similar style as the rest of the code base.

I'll take another look when that's done. Thanks again!

Comment on lines +826 to +842
// Run smartlog without --show-signature flag
{
let stdout = git.smartlog()?;
// Without the flag, the signature indicators shouldn't be visible
// However, we can't reliably check for absence of brackets since they might be used elsewhere
// So we'll just ensure the command runs successfully
assert!(!stdout.is_empty(), "Smartlog output should not be empty");
}

// Run smartlog with --show-signature flag
{
let (stdout, _) = git.branchless("smartlog", &["--show-signature"])?;
// With the flag, the command should run successfully
assert!(!stdout.is_empty(), "Smartlog output should not be empty");
// The signature status indicators should be present in some form,
// but we don't validate exactly which status since that depends on the environment
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[please change] Many tests in this project use insta snapshots to capture the output of commands, and I think that it would be best for these tests to follow suit. Snapshots can be tedious to keep updated, but they tend to be much more pleasant to debug. You can see the test above (lines 800-810) for an example.

The first assertion here, that the smartlog works without the flag, may not be needed, since the rest of this file already tests that. It can be handy to have when running a single test though; in that case, it should also use a snapshot.

@@ -23,6 +23,7 @@ use chrono::{DateTime, Utc};
use cursive::theme::BaseColor;
use cursive::utils::markup::StyledString;
use git2::DiffOptions;
use git2_ext::ops::Sign;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion (maybe/probably change)] I think that it would be best to wrap our own type around this (for example crate::git::sign::Signer) to reduce the number of places we may have to rely on git2_ext and make future refactoring easier. (eg if we ever switch to signing support directly in git2, we can just update our implementation of Signer (hopefully) without having to update all of the args, params, etc that depend on it.)

Here's an example of how this is done for Commit:

/// Represents a commit object in the Git object database.
#[derive(Clone, Debug)]
pub struct Commit<'repo> {
pub(super) inner: git2::Commit<'repo>,
}

Comment on lines +164 to +176
let mut args = vec!["commit"];

args.extend(messages.iter().flat_map(|message| ["--message", message]));

if working_copy_changes_type == WorkingCopyChangesType::Unstaged {
args.push("--all");
}

let sign_flag = sign_option.as_git_flag();
if let Some(flag) = &sign_flag {
args.push(flag);
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Can you restore the block expression syntax (in other words let args = { ... }) while including the new signing lines? That way args can remain immutable in the else.

Comment on lines +419 to +429
let mut args = vec!["commit"];

if !message.is_empty() {
args.extend(["--message", &message]);
}

let sign_flag = sign_option.as_git_flag();
if let Some(flag) = &sign_flag {
args.push(flag);
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[please change] Same here, please: let mut args = ...; => let args = { ... }

Comment on lines +419 to +429
let mut args = vec!["commit"];

if !message.is_empty() {
args.extend(["--message", &message]);
}

let sign_flag = sign_option.as_git_flag();
if let Some(flag) = &sign_flag {
args.push(flag);
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[please change] Same here, please: let mut args = ...; => let args = { ... }

Comment on lines +560 to +562
let mut result = StyledString::new();
result.append_styled("[G]", BaseColor::Green.light());
Ok(Some(result))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick (maybe change)] It looks like other uses of StyledString in this file seem to use let result = StyledString::styled("[G]", BaseColor::Green.light());. To be consistent, can you update these uses of ::new() to match. Or is there a reason that won't work in this context?

Comment on lines +822 to +824
// Just create a regular commit without trying to sign it
git.commit_file("signed_file", 1)?;
git.commit_file("unsigned_file", 2)?;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[question] Are these filenames/comments correct? I don't know much about signing commits, but it seems like something is missing to actually do the signing.

Comment on lines +847 to +867
#[test]
fn test_show_signature_snapshot() -> eyre::Result<()> {
let git = make_git()?;

git.init_repo()?;

// Create regular commits without trying to sign them
git.commit_file("signed_file", 1)?;

// Create a branch and make another commit
git.run(&["checkout", "-b", "branch1", "master"])?;
git.commit_file("another_file", 2)?;

// Run smartlog with --show-signature flag
let (stdout, _) = git.branchless("smartlog", &["--show-signature"])?;

// Simply verify the command runs without error
assert!(!stdout.is_empty(), "Smartlog output should not be empty");

Ok(())
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[please change] This seems to be very similar to the previous test. Can you double to check and confirm that they're both really needed?

Comment on lines +869 to +906
#[test]
fn test_show_signature_argument_parsing() -> eyre::Result<()> {
let git = make_git()?;
git.init_repo()?;

// Test that invalid flag combination reports error
// For example, combine with a flag that doesn't make sense (if any)
// If there are no incompatible flags, we can at least test that the flag is recognized

// Test help output includes the flag
let (help_stdout, _) = git.branchless("smartlog", &["--help"])?;
assert!(
help_stdout.contains("--show-signature"),
"Help output should include --show-signature option"
);

// Test that the flag is recognized (should execute without error)
let result = git.branchless("smartlog", &["--show-signature"]);
assert!(
result.is_ok(),
"The --show-signature flag should be recognized"
);

// Test that the flag works when combined with other flags
let result = git.branchless("smartlog", &["--show-signature", "--exact"]);
assert!(
result.is_ok(),
"The --show-signature flag should work with --exact"
);

let result = git.branchless("smartlog", &["--show-signature", "--reverse"]);
assert!(
result.is_ok(),
"The --show-signature flag should work with --reverse"
);

Ok(())
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[please change] I also think that this test can be removed; mostly it seems to be testing that clap is working as expected. If I'm misunderstanding, let's update it to be more clear about which flags may or may not work with the new --show-signature flag.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants