arxanas / git-branchless

High-velocity, monorepo-scale workflow for Git

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Crash when trying to reword or amend and `user.email` is empty

azaslavsky opened this issue · comments

Description of the bug

Running git reword or git amend produces the following error output:

The application panicked (crashed).
Message:  A fatal error occurred:
   0: could not create commit signature: failed to parse signature - Signature cannot have an empty name or email; class=Invalid (3)
   1: failed to parse signature - Signature cannot have an empty name or email; class=Invalid (3)

Location:
   git-branchless-lib/src/core/rewrite/execute.rs:728

Backtrace omitted. Run with RUST_BACKTRACE=1 environment variable to display it.
Run with RUST_BACKTRACE=full to include source snippets.
Location: git-branchless/src/commands/mod.rs:221

Backtrace omitted. Run with RUST_BACKTRACE=1 environment variable to display it.
Run with RUST_BACKTRACE=full to include source snippets.

For reference, my git config --global user.name is set, but my git config --global user.email is empty. However, even when I set the latter to test@example.com, the crash above persisted when calling git amend or git reword as before.

I'm using git-branchless v0.7.1 on top of git v2.30.1, on a mac running OSX 12.1 Monterey.

Expected behavior

I expect git amend|reword to succeed without crashing.

Actual behavior

The program crashed, with the error output provided above.

Version of rustc

This is not a build issue, but 1.69.0-nightly

Automated bug report

Software version

git-branchless 0.7.1

Operating system

macOS 12.1 (Darwin 21.2.0)

Command-line

/usr/local/bin/git-branchless bug-report 

Environment variables

SHELL=/bin/bash
EDITOR=vim

Git version

> git version 
git version 2.30.1 (Apple Git-130)

Hooks

Show 7 hooks
Hook post-applypatch
#!/bin/sh
## START BRANCHLESS CONFIG

git branchless hook post-applypatch "$@"

## END BRANCHLESS CONFIG
Hook post-checkout
#!/bin/sh
## START BRANCHLESS CONFIG

git branchless hook post-checkout "$@"

## END BRANCHLESS CONFIG
Hook post-commit
#!/bin/sh
## START BRANCHLESS CONFIG

git branchless hook post-commit "$@"

## END BRANCHLESS CONFIG
Hook post-merge
#!/bin/sh
## START BRANCHLESS CONFIG

git branchless hook post-merge "$@"

## END BRANCHLESS CONFIG
Hook post-rewrite
#!/bin/sh
## START BRANCHLESS CONFIG

git branchless hook post-rewrite "$@"

## END BRANCHLESS CONFIG
Hook pre-auto-gc
#!/bin/sh
## START BRANCHLESS CONFIG

git branchless hook pre-auto-gc "$@"

## END BRANCHLESS CONFIG
Hook reference-transaction
#!/bin/sh
## START BRANCHLESS CONFIG

# Avoid canceling the reference transaction in the case that `branchless` fails
# for whatever reason.
git branchless hook reference-transaction "$@" || (
echo 'branchless: Failed to process reference transaction!'
echo 'branchless: Some events (e.g. branch updates) may have been lost.'
echo 'branchless: This is a bug. Please report it.'
)

## END BRANCHLESS CONFIG

Events

Show 5 events
Event ID: 6, transaction ID: 46 (message: amendold)
  1. RefUpdateEvent { timestamp: 1681254261.486797, event_tx_id: EventTransactionId(46), ref_name: ReferenceName("HEAD"), old_oid: 1454e771901ff2a8437de85da63522577e73fcd6, new_oid: 9f2f932cb31680084f2e4fe8c659f246ded3f816, message: None }
  2. RefUpdateEvent { timestamp: 1681254261.486797, event_tx_id: EventTransactionId(46), ref_name: ReferenceName("refs/heads/redacted-ref-0"), old_oid: 1454e771901ff2a8437de85da63522577e73fcd6, new_oid: 9f2f932cb31680084f2e4fe8c659f246ded3f816, message: None }
  3. CommitEvent { timestamp: 1681254260.0, event_tx_id: EventTransactionId(46), commit_oid: NonZeroOid(9f2f932cb31680084f2e4fe8c659f246ded3f816) }
  4. RewriteEvent { timestamp: 1681254261.738377, event_tx_id: EventTransactionId(46), old_commit_oid: 1454e771901ff2a8437de85da63522577e73fcd6, new_commit_oid: 9f2f932cb31680084f2e4fe8c659f246ded3f816 }
O 08c4dfe 7m (main) xxxxxx xxxxxxxxxx
|
@ 9f2f932 6m (redacted-ref-0) xxxxxx xxx xxxx xxxxxx xxxxxx
Event ID: 3, transaction ID: 42 (message: record)
  1. RefUpdateEvent { timestamp: 1681254225.450446, event_tx_id: EventTransactionId(42), ref_name: ReferenceName("HEAD"), old_oid: 08c4dfeffdb1c4d43d531fbb85a665bf8b04ff2f, new_oid: 1454e771901ff2a8437de85da63522577e73fcd6, message: None }
  2. RefUpdateEvent { timestamp: 1681254225.450446, event_tx_id: EventTransactionId(42), ref_name: ReferenceName("refs/heads/redacted-ref-0"), old_oid: 08c4dfeffdb1c4d43d531fbb85a665bf8b04ff2f, new_oid: 1454e771901ff2a8437de85da63522577e73fcd6, message: None }
  3. CommitEvent { timestamp: 1681254225.0, event_tx_id: EventTransactionId(42), commit_oid: NonZeroOid(1454e771901ff2a8437de85da63522577e73fcd6) }
O 08c4dfe 7m (main) xxxxxx xxxxxxxxxx
|
@ 9f2f932 6m (redacted-ref-0) xxxxxx xxx xxxx xxxxxx xxxxxx
Event ID: 1, transaction ID: 38 (message: co)
  1. RefUpdateEvent { timestamp: 1681254203.575399, event_tx_id: EventTransactionId(38), ref_name: ReferenceName("refs/heads/redacted-ref-0"), old_oid: 0000000000000000000000000000000000000000, new_oid: 08c4dfeffdb1c4d43d531fbb85a665bf8b04ff2f, message: None }
  2. RefUpdateEvent { timestamp: 1681254203.739983, event_tx_id: EventTransactionId(38), ref_name: ReferenceName("HEAD"), old_oid: 08c4dfeffdb1c4d43d531fbb85a665bf8b04ff2f, new_oid: 08c4dfeffdb1c4d43d531fbb85a665bf8b04ff2f, message: None }
O 08c4dfe 7m (main) xxxxxx xxxxxxxxxx
|
@ 9f2f932 6m (redacted-ref-0) xxxxxx xxx xxxx xxxxxx xxxxxx

There are no previous available events.

O 08c4dfe 7m (main) xxxxxx xxxxxxxxxx
|
@ 9f2f932 6m (redacted-ref-0) xxxxxx xxx xxxx xxxxxx xxxxxx

There are no previous available events.

O 08c4dfe 7m (main) xxxxxx xxxxxxxxxx
|
@ 9f2f932 6m (redacted-ref-0) xxxxxx xxx xxxx xxxxxx xxxxxx

Version of git-branchless

0.7.1

Version of git

2.30.1

Hi @azaslavsky, thanks for reporting. I think this is a limitation in the git2 crate, and arguably in libgit2 itself. In libgit2, it claims that an empty email is invalid:

https://github.com/libgit2/libgit2/blob/abb0b313172d1b4477fe0c6e88102ce4bb8db90c/src/libgit2/signature.c#L93-L96

However, it's obviously possible to create signatures with empty emails. If we were using libgit2 directly, it would probably be possible to bypass the constructor and set the email string ourselves, or perhaps use the function to parse a signature from a buffer. However, the git2 Rust wrappers don't seem to have any workaround (without unsafely interfacing with libgit2 itself).

As a workaround, I recommend that you use git commit --amend to assign a non-empty signature to the commit in question, after which you should be able to use git amend or git reword as desired. I would also recommend that you check out Jujutsu, which has first-class support for empty names and emails and even commit messages, and can be used in a Git repository.

For my own future reference, while I was able to reproduce the issue with manual testing, the following patch did not expose the issue (it somehow successfully amended the commit?):

Patch
From 53affaae2d670f82364c01d4477a0f9ba64f1d0c Mon Sep 17 00:00:00 2001
From: Waleed Khan <me@waleedkhan.name>
Date: Sat, 22 Apr 2023 14:25:29 -0700
Subject: [PATCH] temp: attempt to create test exposing empty email issue

---
 git-branchless-testing/src/lib.rs  | 23 ++++++++--
 git-branchless/tests/test_amend.rs | 71 +++++++++++++++++++++++++++++-
 2 files changed, 89 insertions(+), 5 deletions(-)

diff --git a/git-branchless-testing/src/lib.rs b/git-branchless-testing/src/lib.rs
index c011cb2f..5f135a48 100644
--- a/git-branchless-testing/src/lib.rs
+++ b/git-branchless-testing/src/lib.rs
@@ -55,6 +55,8 @@ pub struct GitInitOptions {
 
     /// If `true`, run `git branchless init` as part of initialization process.
     pub run_branchless_init: bool,
+
+    pub configure_signature: bool,
 }
 
 impl Default for GitInitOptions {
@@ -62,6 +64,7 @@ impl Default for GitInitOptions {
         GitInitOptions {
             make_initial_commit: true,
             run_branchless_init: true,
+            configure_signature: true,
         }
     }
 }
@@ -171,6 +174,9 @@ impl Git {
         let new_path = self.get_path_for_env();
         let envs = vec![
             ("GIT_CONFIG_NOSYSTEM", OsString::from("1")),
+            ("GIT_CONFIG_SYSTEM", OsString::new()), // since Git v2.32.0 https://stackoverflow.com/a/67512433
+            ("GIT_CONFIG_GLOBAL", OsString::new()), // since Git v2.32.0 https://stackoverflow.com/a/67512433
+            ("GIT_ATTR_NOSYSTEM", OsString::from("1")), // mysterious undocumented config option?  https://stackoverflow.com/questions/43881807/how-to-tell-git-to-ignore-global-config/67512433#comment74799506_43882168
             ("GIT_AUTHOR_DATE", date.clone()),
             ("GIT_COMMITTER_DATE", date),
             ("GIT_EDITOR", git_editor),
@@ -390,12 +396,21 @@ then you can only run tests in the main `git-branchless` and \
     /// with it.
     #[instrument]
     pub fn init_repo_with_options(&self, options: &GitInitOptions) -> eyre::Result<()> {
+        let GitInitOptions {
+            make_initial_commit,
+            run_branchless_init,
+            configure_signature,
+        } = options;
+
         self.run(&["init"])?;
-        self.run(&["config", "user.name", DUMMY_NAME])?;
-        self.run(&["config", "user.email", DUMMY_EMAIL])?;
         self.run(&["config", "core.abbrev", "7"])?;
 
-        if options.make_initial_commit {
+        if *configure_signature {
+            self.run(&["config", "user.name", DUMMY_NAME])?;
+            self.run(&["config", "user.email", DUMMY_EMAIL])?;
+        }
+
+        if *make_initial_commit {
             self.commit_file("initial", 0)?;
         }
 
@@ -415,7 +430,7 @@ then you can only run tests in the main `git-branchless` and \
         // ```
         self.run(&["config", "core.autocrlf", "false"])?;
 
-        if options.run_branchless_init {
+        if *run_branchless_init {
             self.branchless("init", &[])?;
         }
 
diff --git a/git-branchless/tests/test_amend.rs b/git-branchless/tests/test_amend.rs
index 8c8b6fc6..608f0a24 100644
--- a/git-branchless/tests/test_amend.rs
+++ b/git-branchless/tests/test_amend.rs
@@ -1,4 +1,6 @@
-use git_branchless_testing::{make_git, remove_rebase_lines, trim_lines, GitRunOptions};
+use git_branchless_testing::{
+    make_git, remove_rebase_lines, trim_lines, GitInitOptions, GitRunOptions,
+};
 
 #[test]
 fn test_amend_with_children() -> eyre::Result<()> {
@@ -1135,3 +1137,70 @@ fn test_amend_move_detached_branch() -> eyre::Result<()> {
 
     Ok(())
 }
+
+#[test]
+fn test_amend_empty_email() -> eyre::Result<()> {
+    let git = make_git()?;
+    git.init_repo_with_options(&GitInitOptions {
+        configure_signature: false,
+        ..Default::default()
+    })?;
+
+    git.run(&["config", "user.name", "Foo Bar"])?;
+    git.run(&["config", "user.email", ""])?;
+    git.detach_head()?;
+    git.commit_file("test1", 1)?;
+
+    {
+        let (stdout, _stderr) = git.run(&["show"])?;
+        insta::assert_snapshot!(stdout, @r###"
+        commit 2823d3098a992b872a6fd551eeb10fc743e8e39d
+        Author: Foo Bar <>
+        Date:   Thu Oct 29 12:34:56 2020 -0100
+
+            create test1.txt
+
+        diff --git a/test1.txt b/test1.txt
+        new file mode 100644
+        index 0000000..7432a8f
+        --- /dev/null
+        +++ b/test1.txt
+        @@ -0,0 +1 @@
+        +test1 contents
+        "###);
+    }
+
+    git.write_file_txt("test1", "updated contents\n")?;
+    {
+        let (stdout, stderr) = git.branchless("amend", &[])?;
+        insta::assert_snapshot!(stderr, @r###"
+        branchless: creating working copy snapshot
+        branchless: processing 1 update: ref HEAD
+        "###);
+        insta::assert_snapshot!(stdout, @r###"
+        branchless: running command: <git-executable> reset 937a25b6ca80c7dc92a179befb36e39a8ecfa8bf
+        Amended with 1 uncommitted change.
+        "###);
+    }
+
+    {
+        let (stdout, _stderr) = git.run(&["show"])?;
+        insta::assert_snapshot!(stdout, @r###"
+        commit 937a25b6ca80c7dc92a179befb36e39a8ecfa8bf
+        Author: Foo Bar <>
+        Date:   Thu Oct 29 12:34:56 2020 -0100
+
+            create test1.txt
+
+        diff --git a/test1.txt b/test1.txt
+        new file mode 100644
+        index 0000000..27e2fc9
+        --- /dev/null
+        +++ b/test1.txt
+        @@ -0,0 +1 @@
+        +updated contents
+        "###);
+    }
+
+    Ok(())
+}
-- 
2.40.0

Hmm, I tried your suggestion (first commit using the old-school git commit --amend, then try git amend/reword, but the latter commands till fail in the same way they did before? It seems like the issue is with trying to generate a new signature when the commit message or contents change, though this is just me speculating. I'll try to build git-branchless from source with some print debug statements to see what could be the issue on my end.

You may also need to pass --reset-author or similar to fully reset the signature. Make sure both the author and committer signatures have been updated.