#!/usr/bin/env bash # # Commit into a GIT repository. # Copyright (c) Petr Baudis, 2005 # # Commits your changes to the GIT repository. Accepts the commit message # from `stdin`. If the commit message is not modified the commit will be # aborted. # # Note that you can undo a commit by the `cg-admin-uncommit` command, # but that is possible only under special circumstances. See the CAVEATS # section of its documentation. # # Commit author # ~~~~~~~~~~~~~ # Each commit has two user identification fields - commit author and committer. # By default, it is recorded that you authored the commit, but it is considered # a good practice to change this to the actual author of the change if you are # merely applying someone else's patch. It is always recorded that you were the # patch committer. # # The commit author is determined by examining various sources in this order: # # * '--author' (see OPTIONS) # # * 'GIT_AUTHOR_*' (see ENVIRONMENT) # # * '.git/author' (see FILES) # # * System information: The author name defaults to the GECOS field of your # '/etc/passwd' entry, which is taken almost verbatim. The author email # defaults to your 'username@hostname.domainname' (but you should change this # to the real email address you use if it is any different). # # OPTIONS # ------- # --author AUTHOR_STRING:: Set the author information according to the argument # Set the commit author information according to the argument instead # of your environment, .git/author, or user information. # # The 'AUTHOR_STRING' format is `Author Name Date`. The # author name and date is optional, only the email is required to be # always present (e.g. '--author ""' will use the current # date and the real name set for your system account (usually in # the gecos field), but a different email address). # # -c COMMIT_ID:: Copy author info and commit message from COMMIT_ID # Copy the commit from a given commit ID (that is the author information # and the commit message - NOT committer information). This option # is typically used when replaying commits from one lineage or # repository to another - see also `cg-patch -C`. # # -C:: Ignore cache # Make `cg-commit` ignore the cache and just commit the thing as-is. # Note, this is used internally by 'Cogito' when merging, and it is # also useful when you are performing the initial commit manually. This # option does not make sense when files are given on the command line. # # -m MESSAGE:: Specify commit message # Specify the commit message, which is used instead of starting # up an editor (if the input is not `stdin`, the input is appended # after all the '-m' messages). Multiple '-m' parameters are appended # to a single commit message, each as separate paragraph. # # -M FILE:: Read commit message from a file # Include commit message from a file (this has the same effect as if # you would cat it to stdin). # # -e:: Force message editing of messages given with -m # Force the editor to be brought up even when '-m' parameters were # passed to `cg-commit`. # # -E:: Force message editing and commit the result # Force the editor to be brought up and do the commit even if # the default commit message is not changed. # # -f:: Force commit when no changes has been made # Force the commit even when there's "nothing to commit", that is # the tree is the same as the last time you committed, no changes # happened. This also forces the commit even if committing is blocked # for some reason. # # -N:: Only update the cache # Don't add the files to the object database, just update the caches # and the commit information. This is for special purposes when you # might not actually _have_ any object database. This option is # normally not interesting. # # -p, --review:: Show and enable editing of changes being committed # Show changes being commited as a patch appended to the commit message # buffer. Changes made to the patch will be reapplied before completing # the commit. This only makes sense if you are going to edit the commit # message interactively. # # -q:: Be very very quiet # Be quiet in case there's "nothing to commit", and silently exit # returning success. In a sense, this is the opposite to '-f'. # # -s, --signoff[=STRING]:: Automatically append a sign off line # Add Signed-off-by line at the end of the commit message. # Optionally, specify the exact name and email to sign off with by # passing: `--signoff="Author Name "`. # # FILES # ----- # $GIT_DIR/author:: # If exists, it should be in the format # Person Name # (both parts are optional) and the GIT_AUTHOR_* environment variables # will be set accordingly - if they are not present in the environment # yet! # # $GIT_DIR/commit-template:: # If the file exists it will be used as a template when creating # the commit message. The template file makes it possible to # automatically add `Signed-off-by` line to the log message. # # $GIT_DIR/hooks/commit-post:: # If the file exists and is executable it will be executed upon # completion of the commit. The script is passed two arguments. # The first argument is the commit ID and the second is the # branchname. A sample `commit-post` script might look like: # # #!/bin/sh # id=$1 # branch=$2 # echo "Committed $id in $branch" | mail user@host # # ENVIRONMENT VARIABLES # --------------------- # See the 'Commit author' section above for details about the name/email/date # environment variables meaning and default values. # # GIT_AUTHOR_NAME:: # Author's name. # # GIT_AUTHOR_EMAIL:: # Author's e-mail address. # # GIT_AUTHOR_DATE:: # Date, useful when applying patches submitted over e-mail. # # GIT_COMMITTER_NAME:: # Committer's name. It defaults to the same as GIT_AUTHOR_NAME. # # GIT_COMMITTER_EMAIL:: # Committer's e-mail address. It defaults to the same as # GIT_AUTHOR_EMAIL. The recommended policy is not to change this, # though - it may not be necessarily a valid e-mail address, but # its purpose is more to identify the actual user and machine # where the commit was done. However, it is obviously ultimately # a policy decision of a particular project to determine whether # this should be a real e-mail or not. # # EDITOR:: # The editor used for entering revision log information. # # CONFIGURATION VARIABLES # ----------------------- # The following GIT configuration file variables are recognized: # # cogito.hooks.commit.post.allmerged:: # If set to "true" and you are committing a merge, the post-hook will # be called for all the merged commits in sequence (the earliest first). # Otherwise, the hook will be called only for the merge commit. USAGE="cg-commit [-m MESSAGE]... [-e] [-c COMMIT_ID] [OTHER_OPTIONS] [FILE]... [< MESSAGE]" . "${COGITO_LIB}"cg-Xlib || exit 1 ### XXX: The spaghetti code below got rather messy and convoluted over ### the time. Someone should clean it up. :/ --pasky load_author() { local astr="$1" force="$2" if [ "$force" -o -z "$GIT_AUTHOR_NAME" ] && echo "$astr" | grep -q '^[^< ]'; then export GIT_AUTHOR_NAME="$(echo "$astr" | sed 's/ *<.*//')" fi if [ "$force" -o -z "$GIT_AUTHOR_EMAIL" ] && echo "$astr" | grep -q '<.*>'; then export GIT_AUTHOR_EMAIL="$(echo "$astr" | sed 's/.*<\(.*\)>.*/\1/')" fi if [ "$force" -o -z "$GIT_AUTHOR_DATE" ] && echo "$astr" | grep -q '[^> ]$'; then export GIT_AUTHOR_DATE="$(echo "$astr" | sed 's/.*> *//')" fi } if [ -s "$_git/author" ]; then load_author "$(cat "$_git/author")" fi if [ -z "$GIT_AUTHOR_NAME" -o -z "$GIT_AUTHOR_EMAIL" ]; then # Always pre-fill those so that the user can modify them in the # commit template. idline="$(git-var GIT_AUTHOR_IDENT)" [ -z "$GIT_AUTHOR_NAME" ] && export GIT_AUTHOR_NAME="$(echo "$idline" | sed 's/ *<.*//')" [ -z "$GIT_AUTHOR_EMAIL" ] && export GIT_AUTHOR_EMAIL="$(echo "$idline" | sed 's/.*<\(.*\)>.*/\1/')" fi force= forceeditor= ignorecache= infoonly= commitalways= missingok= review= signoff= copy_commit= msgs=() msgfile= quiet= while optparse; do if optparse --author=; then load_author "$OPTARG" force elif optparse -C; then ignorecache=1 elif optparse -N; then missingok=--missing-ok infoonly=--info-only elif optparse -e; then forceeditor=1 elif optparse -E; then forceeditor=1 commitalways=1 elif optparse -f; then force=1 elif optparse -q; then quiet=1 elif optparse -p || optparse --review; then review=1 elif optparse -s || optparse --signoff; then [ "$signoff" ] || signoff="$(git-var GIT_AUTHOR_IDENT | sed 's/> .*/>/')" elif optparse --signoff=; then signoff="$OPTARG" elif optparse -m=; then msgs[${#msgs[@]}]="$OPTARG" elif optparse -M=; then msgfile="$OPTARG" elif optparse -c=; then copy_commit="$(cg-object-id -c "$OPTARG")" || exit 1 else optfail fi done if [ -s "$_git/blocked" ]; then if [ "$force" ]; then warn "committing to a blocked repository. Assuming you know what are you doing." else die "committing blocked: $(cat "$_git/blocked")" fi fi [ "$ignorecache" ] || cg-object-id HEAD >/dev/null 2>&1 || die "no previous commit; use -C for the initial commit" editor= [ "$forceeditor" ] && editor=1 [ ! "$msgs" ] && [ ! "$msgfile" ] && [ ! "$copy_commit" ] && editor=1 if [ "$review" ]; then PATCH="$(mktemp -t gitci.XXXXXX)" PATCH2="$(mktemp -t gitci.XXXXXX)" fi if [ "$ARGS" -o "$_git_relpath" ]; then [ "$ignorecache" ] && die "-C and listing files to commit does not make sense" [ -s "$_git/merging" ] && die "cannot commit individual files when merging" filter="$(mktemp -t gitci.XXXXXX)" [ "$_git_relpath" -a ! "$ARGS" ] && echo "$_git_relpath" >>"$filter" for file in "${ARGS[@]}"; do echo "${_git_relpath}$file" >>"$filter" done eval "commitfiles=($(cat "$filter" | path_xargs git-diff-index -r -m HEAD -- | \ sed -e 's/"\|\\/\\&/g' -e 's/^\([^ ]*\)\(.\) \(.*\)\( .*\)*$/"\2 \3"/'))" customfiles=1 [ "$review" ] && cat "$filter" | path_xargs git-diff-index -r -m -p HEAD -- > "$PATCH" rm "$filter" else # We bother with added/removed files here instead of updating # the cache at the time of cg-(add|rm), since we want to # have the cache in a consistent state representing the tree # as it was the last time we committed. Otherwise, e.g. partial # conflicts would be a PITA since added/removed files would # be committed along automagically as well. if [ ! "$ignorecache" ]; then # \t instead of the tab character itself works only with new # sed versions. eval "commitfiles=($(git-diff-index -r -m HEAD | \ sed -e 's/"\|\\/\\&/g' -e 's/^\([^ ]*\)\(.\) \(.*\)\( .*\)*$/"\2 \3"/'))" if [ -s "$_git/commit-ignore" ]; then newcommitfiles=() for file in "${commitfiles[@]}"; do fgrep -qx "${file:2}" "$_git/commit-ignore" && continue newcommitfiles[${#newcommitfiles[@]}]="$file" done commitfiles=("${newcommitfiles[@]}") fi fi [ "$review" ] && git-diff-index -r -m -p HEAD > "$PATCH" merging= [ -s "$_git/merging" ] && merging="$(cat "$_git/merging" | sed 's/^/-p /')" fi LOGMSG="$(mktemp -t gitci.XXXXXX)" LOGMSG2="$(mktemp -t gitci.XXXXXX)" written= if [ "$merging" ] && [ ! "$editor" ]; then warn "suppressing default merge log messages in favour of the custom -m passed to me." elif [ "$merging" ]; then echo -n 'Merge with ' >>"$LOGMSG" [ -s "$_git/merging-sym" ] || cp "$_git/merging" "$_git/merging-sym" for sym in $(cat "$_git/merging-sym"); do uri="$(cat "$_git/branches/$sym" 2>/dev/null)" [ "$uri" ] || uri="$sym" echo "$uri" >>"$LOGMSG" done echo >>"$LOGMSG" if [ -s "$_git/squashing" ]; then # We are squashing all the merged commits to a single one. # Therefore, helpfully pre-fill the commit message with # the messages of all the merged commits. git-rev-list --pretty "$(cat "$_git/merging")" ^HEAD >>"$LOGMSG" fi written=1 fi for msg in "${msgs[@]}"; do [ "$written" ] && echo >>"$LOGMSG" echo "$msg" | fmt -s >>"$LOGMSG" written=1 done if [ "$copy_commit" ]; then [ "$written" ] && echo >>"$LOGMSG" eval "$(git-cat-file commit "$copy_commit" | pick_author)" git-cat-file commit "$copy_commit" | sed -e '1,/^$/d' >>"$LOGMSG" written=1 fi if [ "$msgfile" ]; then [ "$written" ] && echo >>"$LOGMSG" cat "$msgfile" >>"$LOGMSG" || exit 1 written=1 fi # Always have at least one blank line, to ease the editing for # the poor people whose text editor has no 'O' command. [ "$written" ] || { tty -s && echo >>"$LOGMSG"; } if [ "$signoff" ] && ! grep -q -i "signed-off-by: $signoff" $LOGMSG; then grep -q -i sign-off-by $LOGMSG || echo echo "Signed-off-by: $signoff" fi >> $LOGMSG if [ -e "$_git/commit-template" ]; then cat "$_git/commit-template" >>"$LOGMSG" else cat >>"$LOGMSG" <>"$LOGMSG" [ "$GIT_AUTHOR_NAME" ] && echo "CG: Author: $GIT_AUTHOR_NAME" >>"$LOGMSG" [ "$GIT_AUTHOR_EMAIL" ] && echo "CG: Email: $GIT_AUTHOR_EMAIL" >>"$LOGMSG" [ "$GIT_AUTHOR_DATE" ] && echo "CG: Date: $GIT_AUTHOR_DATE" >>"$LOGMSG" echo "CG:" >>"$LOGMSG" fi if [ ! "$ignorecache" ] && [ ! "$review" ]; then if [ ! "$merging" ]; then if [ ! "$force" ] && [ ! "${commitfiles[*]}" ]; then rm "$LOGMSG" "$LOGMSG2" [ "$quiet" ] && exit 0 || die 'Nothing to commit' fi echo "CG: By deleting lines beginning with CG:F, the associated file" >>"$LOGMSG" echo "CG: will be removed from the commit list." >>"$LOGMSG" fi echo "CG:" >>"$LOGMSG" echo "CG: Modified files:" >>"$LOGMSG" for file in "${commitfiles[@]}"; do # TODO: Prepend a letter describing whether it's addition, # removal or update. Or call git status on those files. echo "CG:F $file" >>"$LOGMSG" [ ! "$editor" ] && echo "$file" done if [ -s "$_git/commit-ignore" ]; then echo "CG:" >>"$LOGMSG" echo "CG: I have kept back the $(wc -l "$_git/commit-ignore" | cut -d ' ' -f 1) file(s) containing your local changes." >>"$LOGMSG" echo "CG: You need not worry, the local changes will not interfere with the merge." >>"$LOGMSG" fi fi if [ "$review" ]; then echo "CG: Changes summary:" echo "CG:" git-apply --stat --summary < "$PATCH" | sed 's/^/CG: /' echo "CG:" fi >>"$LOGMSG" [ "$commitalways" ] || echo "CG: Do not save this file and just quit if you want to abort the commit." >>"$LOGMSG" echo "CG: -----------------------------------------------------------------------" >>"$LOGMSG" if [ "$review" ]; then { echo "CG:" echo "CG: The patch being committed:" echo "CG: (You can edit it; your tree will be modified accordingly and" echo "CG: the modified patch will be committed.)" echo "CG:" cat "$PATCH" } >>"$LOGMSG" ftdiff="filetype=diff" fi echo "CG: vim: textwidth=75 $ftdiff" >>"$LOGMSG" cp "$LOGMSG" "$LOGMSG2" if tty -s; then if [ "$editor" ]; then ${EDITOR:-vi} "$LOGMSG2" if ! [ "$commitalways" ] && ! [ "$LOGMSG2" -nt "$LOGMSG" ]; then echo "Log message unchanged or not specified" >&2 while true; do read -p 'Abort or commit? [ac] ' choice if [ "$choice" = "a" ] || [ "$choice" = "q" ]; then rm "$LOGMSG" "$LOGMSG2" [ "$review" ] && rm "$PATCH" "$PATCH2" echo "Commit message not modified, commit aborted" >&2 if [ "$merging" ]; then cat >&2 <<__END__ Note that the merge is NOT aborted - you can cg-commit again, cg-reset will abort it. __END__ [ -s "$_git/commit-ignore" ] && cat >&2 <<__END__ (But note that cg-reset will remove your pending local changes as well!) __END__ fi exit 1 elif [ "$choice" = "c" ]; then break fi done fi fi if [ ! "$ignorecache" ] && [ ! "$merging" ] && [ ! "$review" ]; then eval "newcommitfiles=($(grep ^CG:F "$LOGMSG2" | sed 's/^CG:F *\(.*\)$/"\1"/'))" if [ ! "$force" ] && [ ! "${newcommitfiles[*]}" ]; then rm "$LOGMSG" "$LOGMSG2" [ "$quiet" ] && exit 0 || die 'Nothing to commit' fi if [ "${commitfiles[*]}" != "${newcommitfiles[*]}" ]; then commitfiles=("${newcommitfiles[@]}") customfiles=1 fi fi setif () { if ! grep -q "^CG: $2:" "$LOGMSG2"; then unset $1 else export $1="$(grep "^CG: $2:" "$LOGMSG2" | cut -d ' ' -f 3-)" fi } setif GIT_AUTHOR_NAME Author setif GIT_AUTHOR_EMAIL Email setif GIT_AUTHOR_DATE Date else cat >>"$LOGMSG2" fi # Remove heading and trailing blank lines. if [ ! "$review" ]; then grep -v ^CG: "$LOGMSG2" | git-stripspace >"$LOGMSG" else sed '/^CG: Changes summary:/,$d' < "$LOGMSG2" | grep -v ^CG: | git-stripspace >"$LOGMSG" sed -n '/^CG: Changes summary:/,$p' < "$LOGMSG2" | grep -v ^CG: > "$PATCH2" fi rm "$LOGMSG2" if [ "$review" ]; then if ! cmp -s "$PATCH" "$PATCH2"; then echo "Reverting the original patch..." if ! cg-patch -R < "$PATCH"; then rm "$PATCH" "$LOGMSG" die "unable to revert the original patch; your edited patch is available in $PATCH2" fi echo "Applying the edited patch..." if ! cg-patch < "$PATCH2"; then rm "$PATCH" "$PATCH2" "$LOGMSG" die "unable to apply the edited patch" fi fi fi precommit_update() { queueN=(); queueD=(); queueM=(); for file in "$@"; do op="${file%% *}" fname="${file#* }" [ "$op" = "N" ] && op=A # N is to be renamed to A [ "$op" = "A" ] || [ "$op" = "D" ] || [ "$op" = "M" ] || op=M eval "queue$op[\${#queue$op[@]}]=\"\$fname\"" done oldIFS="$IFS" IFS=$'\n' # XXX: Do we even need to do the --add and --remove update-caches? [ "$queueA" ] && { ( echo "${queueA[*]}" | path_xargs git-update-index --add ${infoonly} -- ) || return 1; } [ "$queueD" ] && { ( echo "${queueD[*]}" | path_xargs git-update-index --force-remove -- ) || return 1; } [ "$queueM" ] && { ( echo "${queueM[*]}" | path_xargs git-update-index ${infoonly} -- ) || return 1; } IFS="$oldIFS" return 0 } if [ ! "$ignorecache" ]; then if [ "$customfiles" ]; then precommit_update "${commitfiles[@]}" || die "update-cache failed" export GIT_INDEX_FILE="$(mktemp -t gitci.XXXXXX)" git-read-tree HEAD fi precommit_update "${commitfiles[@]}" || die "update-cache failed" fi oldhead= oldheadname="$(git-symbolic-ref HEAD)" if [ -s "$_git/$oldheadname" ]; then oldhead="$(cat "$_git/$oldheadname")" oldheadstr="-p $oldhead" fi treeid="$(git-write-tree ${missingok})" [ "$treeid" ] || die "git-write-tree failed" if [ ! "$force" ] && [ ! "$merging" ] && [ "$oldhead" ] && [ "$treeid" = "$(cg-object-id -t)" ]; then echo "Refusing to make an empty commit - the tree was not modified" >&2 echo "since the previous commit. If you really want to make the" >&2 echo "commit, pass cg-commit the -f argument." >&2 exit 2; fi [ -s "$_git/squashing" ] && merging=" " # viciously prevent recording a proper merge newhead=$(git-commit-tree $treeid $oldheadstr $merging <"$LOGMSG") rm "$LOGMSG" if [ "$customfiles" ]; then rm "$GIT_INDEX_FILE" export GIT_INDEX_FILE= fi if [ "$newhead" ]; then git-update-ref HEAD $newhead $oldhead || die "unable to move to the new commit $newhead" echo "Committed as $newhead" [ "$merging" ] && rm -f "$_git/merging" "$_git/merging-sym" "$_git/merge-base" "$_git/squashing" rm -f "$_git/commit-ignore" # Trigger the postcommit hook branchname= if [ -s "$_git/branch-name" ]; then warn ".git/branch-name is deprecated and support for it will be removed soon." warn "So please stop relying on it, or complain at pasky@suse.cz. Thanks." branchname="$(cat "$_git/branch-name")" fi [ -z "$branchname" ] && [ "$_git_head" != "master" ] && branchname="$_git_head" if [ -x "$_git/hooks/commit-post" ]; then if [ "$(git-repo-config cogito.hooks.commit.post.allmerged)" = "true" ]; then # We just hope that for the initial commit, the user didn't # manage to install the hook yet. for merged in $(git-rev-list $newhead ^$oldhead | tac); do "$_git/hooks/commit-post" "$merged" "$branchname" done else "$_git/hooks/commit-post" "$newhead" "$branchname" fi fi exit 0 else die "error during commit (oldhead $oldhead, treeid $treeid)" fi