Hi, on IRC, "cehteh" requested that I send an updated version of the script. It was submitted when a short-lived option of git-diff was used, so the first publicised version no longer works. This has seen a tremendous amount of work by Jason Chu, which made it work more like darcs when called as "bash git-hunk-commit.bash --darcs". It still relies heavily on bash arrays. Ciao, Dscho
#!/bin/bash # Copyright (C) 2006 Johannes E. Schindelin # Distributed under the same license as git. # Use this command to commit just a few hunks of the current output # of "git diff". For your security, it only works when the index matches # HEAD. # ensure that this is a git repository . git-sh-setup TMP_FILE=$GIT_DIR/tmp.$$.txt trap "rm -f $TMP_FILE; exit" 0 1 2 3 15 # the index must match the HEAD index_check="$(git diff --index --name-only HEAD 2>&1)" if [ $? -ne 0 ]; then index_check="$(git diff --cached --name-only HEAD 2>&1)" fi if [ -n "$index_check" ]; then echo "The staging area (AKA index) is already dirty." exit 1 fi # read the names of all modified files into the array "modified" declare -a modified filenr=1 git ls-files --modified -z > $TMP_FILE while read -d $'\0' file; do modified[$filenr]="$file" filenr=$(($filenr+1)) done < $TMP_FILE if [ ${#modified[*]} = 0 ]; then echo "No modified files." exit 1 fi declare -a hunks declare -a darcs_hunk_state # interactively show the hunks of a file and ask if they should be committed. # 1st parameter is the index into the modified file list. # Darcs mode means that all hunks are presented one after another. # Normal mode means user can specify hunks interactively. select_hunks () { local index=$1 local filename=${modified[$index]} local -a diff local -a hunk_start local current_hunks=${hunks[$index]} local lineno local hunkno local action local i local active lineno=1 hunkno=0 git diff "$filename" > $TMP_FILE while read -r line; do diff[$lineno]="$line" case "$line" in @@*) hunk_start[$hunkno]=$lineno hunkno=$(($hunkno+1)) ;; esac lineno=$(($lineno+1)) done < $TMP_FILE hunk_start[$hunkno]=$lineno action="" while [ "$action" != commit -a "$action" != abort ]; do echo echo "Current hunks: ($current_hunks) of $hunkno hunks" echo "To show (and decide on) a hunk type in the number." echo "To commit the current hunks, say 'commit', else 'abort'." echo echo -n "Your choice? " read action case "$action" in c) action=commit;; q|a) action=abort;; commit|abort) ;; [1-9]*) echo for ((i=${hunk_start[$(($action-1))]}; i<${hunk_start[$action]}; i++)); do echo ${diff[$i]} done | less -FS active=$(echo $current_hunks,$action | tr , '\n' | sort | uniq -u | tr '\n' , | sed -e "s/^,//" -e "s/,$//") if [ ${#active} -lt ${#current_hunks} ]; then i=yes else i=no fi echo while [ -n "$action" -a "$action" != yes -a "$action" != no -a -n "$action" ]; do echo -n "Commit this hunk (default is $i)? " read action case "$action" in y) action=yes;; n) action=no;; esac done if [ -n "$action" -a $i != "$action" ]; then current_hunks=$active fi ;; *) echo "Unknown command: $action";; esac done if [ "$action" = commit ]; then hunks[$index]=$current_hunks fi } darcs_select_hunks () { local index=1 local filename=${modified[$index]} local -a diff local -a hunk_start local lineno local hunkno local i local current_hunkno=1 local current_hunk_state local input='' local done=0 # Hunk number over all files local absolute_hunkno=1 # Was the input we just had something that would finish recording # if we hit the end of the patches? local quiter lineno=1 hunkno=0 git diff "$filename" > $TMP_FILE while read -r line; do diff[$lineno]="$line" case "$line" in @@*) hunk_start[$hunkno]=$lineno hunkno=$(($hunkno+1)) ;; esac lineno=$(($lineno+1)) done < $TMP_FILE hunk_start[$hunkno]=$lineno while [ $done -eq 0 ]; do quiter=0 # Darcs hunk preamble echo "hunk $filename" # Display hunk for ((i=${hunk_start[$(($current_hunkno-1))]}; i<${hunk_start[$current_hunkno]}; i++)); do echo ${diff[$i]} done current_hunk_state=$(get_hunk_state $index $current_hunkno) echo -n "Shall I record this change? ($absolute_hunkno/?) [" case "$current_hunk_state" in y) echo -n Ynwsfqadjkc ;; n) echo -n yNwsfqadjkc ;; w) echo -n ynWsfqadjkc ;; esac echo -n "], or ? for help: " read -n 1 input echo case "$input" in n|y|w) set_hunk_state $index $current_hunkno $input current_hunkno=$((current_hunkno+1)) absolute_hunkno=$((absolute_hunkno+1)) quiter=1 ;; j) current_hunkno=$((current_hunkno+1)) absolute_hunkno=$((absolute_hunkno+1)) ;; k) current_hunkno=$((current_hunkno-1)) absolute_hunkno=$((absolute_hunkno-1)) ;; q) done=2 continue ;; ' ') current_hunkno=$((current_hunkno+1)) absolute_hunkno=$((absolute_hunkno+1)) quiter=1 ;; d) done=1 continue ;; ?|h) print_darcs_help continue ;; esac if [ $((current_hunkno+1)) -gt ${#hunk_start[@]} ]; then index=$(($index+1)) if [ $index -gt ${#modified[@]} ]; then if [ $quiter -eq 1 ]; then done=1 else current_hunkno=$((current_hunkno-1)) absolute_hunkno=$((absolute_hunkno-1)) index=$(($index-1)) fi continue fi filename=${modified[$index]} unset hunk_start lineno=1 hunkno=0 git diff "$filename" > $TMP_FILE while read -r line; do diff[$lineno]="$line" case "$line" in @@*) hunk_start[$hunkno]=$lineno hunkno=$(($hunkno+1)) ;; esac lineno=$(($lineno+1)) done < $TMP_FILE hunk_start[$hunkno]=$lineno current_hunkno=1 elif [ $current_hunkno -lt 1 ]; then index=$(($index-1)) if [ $index -lt 1 ]; then index=1 current_hunkno=1 absolute_hunkno=1 continue fi filename=${modified[$index]} unset hunk_start lineno=1 hunkno=0 git diff "$filename" > $TMP_FILE while read -r line; do diff[$lineno]="$line" case "$line" in @@*) hunk_start[$hunkno]=$lineno hunkno=$(($hunkno+1)) ;; esac lineno=$(($lineno+1)) done < $TMP_FILE hunk_start[$hunkno]=$lineno current_hunkno=$((hunkno)) fi done case $done in # q -- don't do anything 2) echo "Commit cancelled." exit 0 ;; # d -- commit everything that was a y 1) set_darcs_hunks return ;; esac } set_darcs_hunks () { local i local active for ((i=1; i<$filenr; i++)); do active=$(echo ${darcs_hunk_state[$i]} | tr , '\n' | grep :y$ | sed -e 's/:y$//' | sort | tr '\n' , | sed -e "s/^,//" -e "s/,$//") hunks[$i]=$active done } print_darcs_help () { cat << EOF How to use git-hunk-commit... y: record this patch n: don't record it w: wait and decide later, defaulting to no s: don't record the rest of the changes to this file f: record the rest of the changes to this file d: record selected patches, skipping all the remaining patches a: record all the remaining patches q: cancel record j: skip to next patch k: back up to previous patch c: calculate number of patches h or ?: show this help <Space>: accept the current default (which is capitalized) EOF } # Given the index ($1) and hunkno ($2) return the current darcs choice get_hunk_state () { local index=$1 local hunkno=$2 local current_darcs_state=${darcs_hunk_state[$index]} local value=$(echo $current_darcs_state | sed -e "s/.*\(^\|,\)$hunkno:\([^,]*\)\(,\|$\).*/\2/") if [ "$value" = "$current_darcs_state" ]; then echo w else echo $value fi } # Given the index ($1), hunkno ($2), and new state ($3) set the entry in darcs_hunk_state set_hunk_state () { local index=$1 local hunkno=$2 local state=$3 local current_darcs_state=${darcs_hunk_state[$index]} local newstates='' newstates=$(echo $current_darcs_state | sed -e "s/\(^\|,\)$hunkno:[^,]*\(,\|$\)//") newstates="$newstates,$hunkno:$state" darcs_hunk_state[$index]=$(echo $newstates | sed -e "s/^,//" -e "s/,$//") } # Apply the hunks saved in the array hunks for the specified file. # This means that the diff is rewritten to skip the unwanted hunks. apply_hunks () { local index=$1 local filename=${modified[$index]} local -a current_hunks local lineno local lineno2 local linediff local hunkno local i local active i=0 echo ${hunks[$index]} | tr , '\n' > $TMP_FILE while read hunkno; do current_hunks[$i]=$hunkno i=$(($i+1)) done < $TMP_FILE linediff=0 hunkno=0 i=0 active=true git diff "$filename" > $TMP_FILE while read -r line do case "$line" in @@*) hunkno=$(($hunkno+1)) if [ $hunkno = "${current_hunks[$i]}" ]; then active=true i=$(($i+1)) if [ $linediff -ne 0 ]; then lineno=$(echo "$line" | sed "s/^.*+\([0-9]*\)[, ].*$/\1/") lineno2=$(($lineno+$linediff)) line="$(echo "$line" | sed "s/+$lineno/+$lineno2/")" fi else active= lineno=$(echo "$line" | sed -n "s/^.*-[0-9]*,\([0-9]*\) .*$/\1/p") if [ -z "$lineno" ]; then lineno=1 fi lineno2=$(echo "$line" | sed -n "s/^.*+[0-9]*,\([0-9]*\) .*$/\1/p") if [ -z "$lineno2" ]; then lineno2=1 fi linediff=$(($linediff+$lineno-$lineno2)) fi ;; esac if [ -n "$active" ]; then echo "$line" fi done < $TMP_FILE } darcs_mode= case "$1" in --darcs) darcs_mode=true;; esac IFS='' action= i= if [ ! -n "$darcs_mode" ]; then while [ "$action" != commit -a "$action" != abort ]; do echo for ((i=1; i<$filenr; i++)); do echo -n "$i ${modified[$i]}" if [ -n "${hunks[$i]}" ]; then echo " (${hunks[$i]})" else echo fi done | less -FS echo echo "To put one or more hunks of a file into the staging area (AKA" echo "index), type in the number of the file." echo "To commit, say 'commit', to abort, say 'abort'." echo echo -n "Your choice? " read action case "$action" in c) action=commit;; q|a) action=abort;; commit|abort) ;; [0-9]*) select_hunks "$action";; *) echo "Unknown command." ;; esac done else darcs_select_hunks action="commit" fi if [ "$action" = commit ]; then for ((i=1; i<$filenr; i++)); do if [ -n "${hunks[$i]}" ]; then apply_hunks $i fi done | tee a123 | git apply --cached || exit echo git commit fi