Search code examples
git

Is there a Git native way to non-interactively transform a commit's metadata to produce a new commit without touching the working tree and index?


This is a generalization of my previous question, aimed at improving my understanding of what can and cannot be done internally to Git.

Suppose I have a commit A, and I want to change some arbitrary metadata, whether it is the parents, the tree, the author/committer, the author/commit date, the commit message, or the gpg signature, to produce a new commit B that shares all metadata with A except the bits I changed. Further, suppose my changes do not require creating a new tree; I am either using the original tree or substituting another existing tree.

I am currently aware of at least two ways to do this:

  • I can git checkout A and then use git commit --amend with various options. This is native to git and moderately flexible in terms of what can be changed. However, it is interactive and touches the working tree.
  • I can use git cat-file | <insert_sed_script> | git hash-object -w -t commit --stdin as this answer to my previous question describes.

The second way is semantically what I am looking for, but requires an external script that performs text manipulation on the text representation of a commit.

Is text manipulation the only current option for transforming commit objects non-interactively, or is there a more structured and semantic way to manipulate the commit metadata?

  • Example 1: Rather than writing a sed script that semantically says "delete the lines that start with parent and then insert new lines that start with parent", can I write a git command that says "set the parents of this commit to X,Y,Z" and output the commit with that change? (git replace can handle this case, but not the general case.)
  • Example 2: Rather than writing a sed script that identifies the commit message based on regexes and replaces it, can I write a git command that says "set the commit message of this commit to 'hello world'" and output the commit with that change?

Part of the motivation for asking this is intellectual curiosity; the other part is that I am considering building such tooling externally (by wrapping the text manipulation) if it does not already exist.


Solution

  • git commit-tree creates commit objects from a tree. The commit message can be provided via stdin or -m option. -S/--gpg-sign allows to sign the new commit object. Set GIT_AUTHOR_DATE and GIT_COMMITTER_DATE to specify the author and committer time, respectively.

    Example:

    GIT_AUTHOR_NAME='Mer Lin' GIT_AUTHOR_EMAIL='[email protected]' GIT_AUTHOR_DATE='2024-12-26T13:37:42Z' git commit-tree -p first-parent-of-the-new-commit -p optional-second-parent-of-the-new-commit -m 'This commit was created manually
    
    Poviding multi-line commit messages
    is straightforward.' -S your-gpg-key-id the-tree-id
    

    To re-use/modify the message of an existing commit, feed it via stdin:

    git show --format=%B old-commit | sed '...' | GIT_AUTHOR_NAME='Mer Lin' GIT_AUTHOR_EMAIL='[email protected]' GIT_AUTHOR_DATE='2024-12-26T13:37:42Z' git commit-tree -p first-parent-of-the-new-commit -p optional-second-parent-of-the-new-commit -S your-gpg-key-id the-tree-id
    

    For instance, to append "Merry Christmas!" to your commit's message:

    {
      git show --format=%B old-commit;
      echo 'Merry Christmas!';
    } | git commit-tree -p old-commit^ old-commit^{tree}
    

    If you need to copy over committer/author date/name/email from the original commit, populate the environment variables from the info obtained from the original commit:

    GIT_AUTHOR_EMAIL="$(git show --format=%ae old-commit)" \
    GIT_AUTHOR_NAME="$(git show --format=%an old-commit)" \
    GIT_AUTHOR_DATE="$(git show --format=%aI old-commit)" \
    GIT_COMMITTER_EMAIL="$(git show --format=%ce old-commit)" \
    GIT_COMMITTER_NAME="$(git show --format=%cn old-commit)" \
    GIT_COMMITTER_DATE="$(git show --format=%cI old-commit)" \
    git commit-tree -p the-parent-id -m 'commit message' the-tree-id