ref-filter: add new "signature" atom

Duplicate the code for outputting the signature and its other parameters for commits and tags in ref-filter from pretty. In the future, this will help in getting rid of the current duplicate implementations of such logic everywhere, when ref-filter can do everything that pretty is doing. The new atom "signature" and its friends are equivalent to the existing pretty formats as follows: %(signature) = %GG %(signature:grade) = %G? %(siganture:signer) = %GS %(signature:key) = %GK %(signature:fingerprint) = %GF %(signature:primarykeyfingerprint) = %GP %(signature:trustlevel) = %GT Co-authored-by: Hariom Verma <hariom18599@gmail.com> Co-authored-by: Jaydeep Das <jaydeepjd.8914@gmail.com> Co-authored-by: Nsengiyumva Wilberforce <nsengiyumvawilberforce@gmail.com> Mentored-by: Christian Couder <christian.couder@gmail.com> Mentored-by: Hariom Verma <hariom18599@gmail.com> Signed-off-by: Kousik Sanagavarapu <five231003@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Kousik Sanagavarapu committed Jun 4, 2023 at 23:52 UTC 26c9c03f0a9c26f5e41af19f3cbd6f4dda7006b3
3 files changed +342 -2
Documentation/git-for-each-ref.txt
+27
@@ -221,6 +221,33 @@ symref::
221 `:lstrip` and `:rstrip` options in the same way as `refname`
222 above.
223
224 +signature::
225 + The GPG signature of a commit.
226 +
227 +signature:grade::
228 + Show "G" for a good (valid) signature, "B" for a bad
229 + signature, "U" for a good signature with unknown validity, "X"
230 + for a good signature that has expired, "Y" for a good
231 + signature made by an expired key, "R" for a good signature
232 + made by a revoked key, "E" if the signature cannot be
233 + checked (e.g. missing key) and "N" for no signature.
234 +
235 +signature:signer::
236 + The signer of the GPG signature of a commit.
237 +
238 +signature:key::
239 + The key of the GPG signature of a commit.
240 +
241 +signature:fingerprint::
242 + The fingerprint of the GPG signature of a commit.
243 +
244 +signature:primarykeyfingerprint::
245 + The primary key fingerprint of the GPG signature of a commit.
246 +
247 +signature:trustlevel::
248 + The trust level of the GPG signature of a commit. Possible
249 + outputs are `ultimate`, `fully`, `marginal`, `never` and `undefined`.
250 +
251 worktreepath::
252 The absolute path to the worktree in which the ref is checked
253 out, if it is checked out in any linked worktree. Empty string
ref-filter.c
+124 -2
@@ -150,6 +150,7 @@ enum atom_type {
150 ATOM_BODY,
151 ATOM_TRAILERS,
152 ATOM_CONTENTS,
153 + ATOM_SIGNATURE,
154 ATOM_RAW,
155 ATOM_UPSTREAM,
156 ATOM_PUSH,
@@ -215,6 +216,10 @@ static struct used_atom {
216 struct email_option {
217 enum { EO_RAW, EO_TRIM, EO_LOCALPART } option;
218 } email_option;
219 + struct {
220 + enum { S_BARE, S_GRADE, S_SIGNER, S_KEY,
221 + S_FINGERPRINT, S_PRI_KEY_FP, S_TRUST_LEVEL } option;
222 + } signature;
223 struct refname_atom refname;
224 char *head;
225 } u;
@@ -407,8 +412,37 @@ static int subject_atom_parser(struct ref_format *format UNUSED,
412 return 0;
413 }
414
410 -static int trailers_atom_parser(struct ref_format *format UNUSED,
411 - struct used_atom *atom,
415 +static int parse_signature_option(const char *arg)
416 +{
417 + if (!arg)
418 + return S_BARE;
419 + else if (!strcmp(arg, "signer"))
420 + return S_SIGNER;
421 + else if (!strcmp(arg, "grade"))
422 + return S_GRADE;
423 + else if (!strcmp(arg, "key"))
424 + return S_KEY;
425 + else if (!strcmp(arg, "fingerprint"))
426 + return S_FINGERPRINT;
427 + else if (!strcmp(arg, "primarykeyfingerprint"))
428 + return S_PRI_KEY_FP;
429 + else if (!strcmp(arg, "trustlevel"))
430 + return S_TRUST_LEVEL;
431 + return -1;
432 +}
433 +
434 +static int signature_atom_parser(struct ref_format *format UNUSED,
435 + struct used_atom *atom,
436 + const char *arg, struct strbuf *err)
437 +{
438 + int opt = parse_signature_option(arg);
439 + if (opt < 0)
440 + return err_bad_arg(err, "signature", arg);
441 + atom->u.signature.option = opt;
442 + return 0;
443 +}
444 +
445 +static int trailers_atom_parser(struct ref_format *format, struct used_atom *atom,
446 const char *arg, struct strbuf *err)
447 {
448 atom->u.contents.trailer_opts.no_divider = 1;
@@ -668,6 +702,7 @@ static struct {
702 [ATOM_BODY] = { "body", SOURCE_OBJ, FIELD_STR, body_atom_parser },
703 [ATOM_TRAILERS] = { "trailers", SOURCE_OBJ, FIELD_STR, trailers_atom_parser },
704 [ATOM_CONTENTS] = { "contents", SOURCE_OBJ, FIELD_STR, contents_atom_parser },
705 + [ATOM_SIGNATURE] = { "signature", SOURCE_OBJ, FIELD_STR, signature_atom_parser },
706 [ATOM_RAW] = { "raw", SOURCE_OBJ, FIELD_STR, raw_atom_parser },
707 [ATOM_UPSTREAM] = { "upstream", SOURCE_NONE, FIELD_STR, remote_ref_atom_parser },
708 [ATOM_PUSH] = { "push", SOURCE_NONE, FIELD_STR, remote_ref_atom_parser },
@@ -1405,6 +1440,92 @@ static void grab_person(const char *who, struct atom_value *val, int deref, void
1440 }
1441 }
1442
1443 +static void grab_signature(struct atom_value *val, int deref, struct object *obj)
1444 +{
1445 + int i;
1446 + struct commit *commit = (struct commit *) obj;
1447 + struct signature_check sigc = { 0 };
1448 + int signature_checked = 0;
1449 +
1450 + for (i = 0; i < used_atom_cnt; i++) {
1451 + struct used_atom *atom = &used_atom[i];
1452 + const char *name = atom->name;
1453 + struct atom_value *v = &val[i];
1454 + int opt;
1455 +
1456 + if (!!deref != (*name == '*'))
1457 + continue;
1458 + if (deref)
1459 + name++;
1460 +
1461 + if (!skip_prefix(name, "signature", &name) ||
1462 + (*name && *name != ':'))
1463 + continue;
1464 + if (!*name)
1465 + name = NULL;
1466 + else
1467 + name++;
1468 +
1469 + opt = parse_signature_option(name);
1470 + if (opt < 0)
1471 + continue;
1472 +
1473 + if (!signature_checked) {
1474 + check_commit_signature(commit, &sigc);
1475 + signature_checked = 1;
1476 + }
1477 +
1478 + switch (opt) {
1479 + case S_BARE:
1480 + v->s = xstrdup(sigc.output ? sigc.output: "");
1481 + break;
1482 + case S_SIGNER:
1483 + v->s = xstrdup(sigc.signer ? sigc.signer : "");
1484 + break;
1485 + case S_GRADE:
1486 + switch (sigc.result) {
1487 + case 'G':
1488 + switch (sigc.trust_level) {
1489 + case TRUST_UNDEFINED:
1490 + case TRUST_NEVER:
1491 + v->s = xstrfmt("%c", (char)'U');
1492 + break;
1493 + default:
1494 + v->s = xstrfmt("%c", (char)'G');
1495 + break;
1496 + }
1497 + break;
1498 + case 'B':
1499 + case 'E':
1500 + case 'N':
1501 + case 'X':
1502 + case 'Y':
1503 + case 'R':
1504 + v->s = xstrfmt("%c", (char)sigc.result);
1505 + break;
1506 + }
1507 + break;
1508 + case S_KEY:
1509 + v->s = xstrdup(sigc.key ? sigc.key : "");
1510 + break;
1511 + case S_FINGERPRINT:
1512 + v->s = xstrdup(sigc.fingerprint ?
1513 + sigc.fingerprint : "");
1514 + break;
1515 + case S_PRI_KEY_FP:
1516 + v->s = xstrdup(sigc.primary_key_fingerprint ?
1517 + sigc.primary_key_fingerprint : "");
1518 + break;
1519 + case S_TRUST_LEVEL:
1520 + v->s = xstrdup(gpg_trust_level_to_str(sigc.trust_level));
1521 + break;
1522 + }
1523 + }
1524 +
1525 + if (signature_checked)
1526 + signature_check_clear(&sigc);
1527 +}
1528 +
1529 static void find_subpos(const char *buf,
1530 const char **sub, size_t *sublen,
1531 const char **body, size_t *bodylen,
@@ -1598,6 +1719,7 @@ static void grab_values(struct atom_value *val, int deref, struct object *obj, s
1719 grab_sub_body_contents(val, deref, data);
1720 grab_person("author", val, deref, buf);
1721 grab_person("committer", val, deref, buf);
1722 + grab_signature(val, deref, obj);
1723 break;
1724 case OBJ_TREE:
1725 /* grab_tree_values(val, deref, obj, buf, sz); */
t/t6300-for-each-ref.sh
+191
@@ -6,6 +6,7 @@
6 test_description='for-each-ref test'
7
8 . ./test-lib.sh
9 +GNUPGHOME_NOT_USED=$GNUPGHOME
10 . "$TEST_DIRECTORY"/lib-gpg.sh
11 . "$TEST_DIRECTORY"/lib-terminal.sh
12
@@ -1522,4 +1523,194 @@ test_expect_success 'git for-each-ref with non-existing refs' '
1523 test_must_be_empty actual
1524 '
1525
1526 +GRADE_FORMAT="%(signature:grade)%0a%(signature:key)%0a%(signature:signer)%0a%(signature:fingerprint)%0a%(signature:primarykeyfingerprint)"
1527 +TRUSTLEVEL_FORMAT="%(signature:trustlevel)%0a%(signature:key)%0a%(signature:signer)%0a%(signature:fingerprint)%0a%(signature:primarykeyfingerprint)"
1528 +
1529 +test_expect_success GPG 'setup for signature atom using gpg' '
1530 + git checkout -b signed &&
1531 +
1532 + test_when_finished "test_unconfig commit.gpgSign" &&
1533 +
1534 + echo "1" >file &&
1535 + git add file &&
1536 + test_tick &&
1537 + git commit -S -m "file: 1" &&
1538 + git tag first-signed &&
1539 +
1540 + echo "2" >file &&
1541 + test_tick &&
1542 + git commit -a -m "file: 2" &&
1543 + git tag second-unsigned &&
1544 +
1545 + git config commit.gpgSign 1 &&
1546 + echo "3" >file &&
1547 + test_tick &&
1548 + git commit -a --no-gpg-sign -m "file: 3" &&
1549 + git tag third-unsigned &&
1550 +
1551 + test_tick &&
1552 + git rebase -f HEAD^^ && git tag second-signed HEAD^ &&
1553 + git tag third-signed &&
1554 +
1555 + echo "4" >file &&
1556 + test_tick &&
1557 + git commit -a -SB7227189 -m "file: 4" &&
1558 + git tag fourth-signed &&
1559 +
1560 + echo "5" >file &&
1561 + test_tick &&
1562 + git commit -a --no-gpg-sign -m "file: 5" &&
1563 + git tag fifth-unsigned &&
1564 +
1565 + echo "6" >file &&
1566 + test_tick &&
1567 + git commit -a --no-gpg-sign -m "file: 6" &&
1568 +
1569 + test_tick &&
1570 + git rebase -f HEAD^^ &&
1571 + git tag fifth-signed HEAD^ &&
1572 + git tag sixth-signed &&
1573 +
1574 + echo "7" >file &&
1575 + test_tick &&
1576 + git commit -a --no-gpg-sign -m "file: 7" &&
1577 + git tag seventh-unsigned
1578 +'
1579 +
1580 +test_expect_success GPGSSH 'setup for signature atom using ssh' '
1581 + test_when_finished "test_unconfig gpg.format user.signingkey" &&
1582 +
1583 + test_config gpg.format ssh &&
1584 + test_config user.signingkey "${GPGSSH_KEY_PRIMARY}" &&
1585 + echo "8" >file &&
1586 + test_tick &&
1587 + git commit -a -S -m "file: 8" &&
1588 + git tag eighth-signed-ssh
1589 +'
1590 +
1591 +test_expect_success GPG2 'bare signature atom' '
1592 + git verify-commit first-signed 2>out.raw &&
1593 + grep -Ev "checking the trustdb|PGP trust model" out.raw >out &&
1594 + head -3 out >expect &&
1595 + tail -1 out >>expect &&
1596 + echo >>expect &&
1597 + git for-each-ref refs/tags/first-signed \
1598 + --format="%(signature)" >actual &&
1599 + test_cmp expect actual
1600 +'
1601 +
1602 +test_expect_success GPG 'show good signature with custom format' '
1603 + git verify-commit first-signed &&
1604 + cat >expect <<-\EOF &&
1605 + G
1606 + 13B6F51ECDDE430D
1607 + C O Mitter <committer@example.com>
1608 + 73D758744BE721698EC54E8713B6F51ECDDE430D
1609 + 73D758744BE721698EC54E8713B6F51ECDDE430D
1610 + EOF
1611 + git for-each-ref refs/tags/first-signed \
1612 + --format="$GRADE_FORMAT" >actual &&
1613 + test_cmp expect actual
1614 +'
1615 +test_expect_success GPGSSH 'show good signature with custom format
1616 + with ssh' '
1617 + test_config gpg.ssh.allowedSignersFile "${GPGSSH_ALLOWED_SIGNERS}" &&
1618 + FINGERPRINT=$(ssh-keygen -lf "${GPGSSH_KEY_PRIMARY}" | awk "{print \$2;}") &&
1619 + cat >expect.tmpl <<-\EOF &&
1620 + G
1621 + FINGERPRINT
1622 + principal with number 1
1623 + FINGERPRINT
1624 +
1625 + EOF
1626 + sed "s|FINGERPRINT|$FINGERPRINT|g" expect.tmpl >expect &&
1627 + git for-each-ref refs/tags/eighth-signed-ssh \
1628 + --format="$GRADE_FORMAT" >actual &&
1629 + test_cmp expect actual
1630 +'
1631 +
1632 +test_expect_success GPG 'signature atom with grade option and bad signature' '
1633 + git cat-file commit third-signed >raw &&
1634 + sed -e "s/^file: 3/file: 3 forged/" raw >forged1 &&
1635 + FORGED1=$(git hash-object -w -t commit forged1) &&
1636 + git update-ref refs/tags/third-signed "$FORGED1" &&
1637 + test_must_fail git verify-commit "$FORGED1" &&
1638 +
1639 + cat >expect <<-\EOF &&
1640 + B
1641 + 13B6F51ECDDE430D
1642 + C O Mitter <committer@example.com>
1643 +
1644 +
1645 + EOF
1646 + git for-each-ref refs/tags/third-signed \
1647 + --format="$GRADE_FORMAT" >actual &&
1648 + test_cmp expect actual
1649 +'
1650 +
1651 +test_expect_success GPG 'show untrusted signature with custom format' '
1652 + cat >expect <<-\EOF &&
1653 + U
1654 + 65A0EEA02E30CAD7
1655 + Eris Discordia <discord@example.net>
1656 + F8364A59E07FFE9F4D63005A65A0EEA02E30CAD7
1657 + D4BE22311AD3131E5EDA29A461092E85B7227189
1658 + EOF
1659 + git for-each-ref refs/tags/fourth-signed \
1660 + --format="$GRADE_FORMAT" >actual &&
1661 + test_cmp expect actual
1662 +'
1663 +
1664 +test_expect_success GPG 'show untrusted signature with undefined trust level' '
1665 + cat >expect <<-\EOF &&
1666 + undefined
1667 + 65A0EEA02E30CAD7
1668 + Eris Discordia <discord@example.net>
1669 + F8364A59E07FFE9F4D63005A65A0EEA02E30CAD7
1670 + D4BE22311AD3131E5EDA29A461092E85B7227189
1671 + EOF
1672 + git for-each-ref refs/tags/fourth-signed \
1673 + --format="$TRUSTLEVEL_FORMAT" >actual &&
1674 + test_cmp expect actual
1675 +'
1676 +
1677 +test_expect_success GPG 'show untrusted signature with ultimate trust level' '
1678 + cat >expect <<-\EOF &&
1679 + ultimate
1680 + 13B6F51ECDDE430D
1681 + C O Mitter <committer@example.com>
1682 + 73D758744BE721698EC54E8713B6F51ECDDE430D
1683 + 73D758744BE721698EC54E8713B6F51ECDDE430D
1684 + EOF
1685 + git for-each-ref refs/tags/sixth-signed \
1686 + --format="$TRUSTLEVEL_FORMAT" >actual &&
1687 + test_cmp expect actual
1688 +'
1689 +
1690 +test_expect_success GPG 'show unknown signature with custom format' '
1691 + cat >expect <<-\EOF &&
1692 + E
1693 + 13B6F51ECDDE430D
1694 +
1695 +
1696 +
1697 + EOF
1698 + GNUPGHOME="$GNUPGHOME_NOT_USED" git for-each-ref \
1699 + refs/tags/sixth-signed --format="$GRADE_FORMAT" >actual &&
1700 + test_cmp expect actual
1701 +'
1702 +
1703 +test_expect_success GPG 'show lack of signature with custom format' '
1704 + cat >expect <<-\EOF &&
1705 + N
1706 +
1707 +
1708 +
1709 +
1710 + EOF
1711 + git for-each-ref refs/tags/seventh-unsigned \
1712 + --format="$GRADE_FORMAT" >actual &&
1713 + test_cmp expect actual
1714 +'
1715 +
1716 test_done