From 31cb850c04873e29ad1ff5a5d5255283827907b2 Mon Sep 17 00:00:00 2001
From: Boris Kolpackov <boris@codesynthesis.com>
Date: Tue, 27 Sep 2016 06:42:36 +0200
Subject: Implement update script

---
 README     |  14 ++++--
 README.cli |  19 ++++---
 update     | 164 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 185 insertions(+), 12 deletions(-)
 create mode 100755 update

diff --git a/README b/README
index b084a49..5ee7acf 100644
--- a/README
+++ b/README
@@ -85,9 +85,9 @@ infrastructure).
 Further, labels can be used to group notes based on certain criteria. For
 example, doc (documentation issue), windows (Windows-specific), 2.0.0
 (scheduled for the 2.0.0 release), john (assigned to John). The names of
-subdirectories in which the issue is located are also considered its labels.
-So, for example, if the above "Detect empty name" bug was filed in
-lihello/format/, then its labels would be bug, format, and libhello.
+subdirectories in which the note is located are also considered its labels. So,
+for example, if the above "Detect empty name" bug was filed in lihello/format/,
+then its labels would be bug, format, and libhello.
 
 The body of a note is free-form. However, for clarity, it makes sense to avoid
 using '-' for lists in the body ('*' for the first level and '~' for the second
@@ -158,8 +158,8 @@ For example:
 
 Add bug: Detect empty name
 
-If you only have a single issue added in the database then you can use the add
-script to automate it. This script will commit the new issues with the correct
+If you only have a single note added in the database then you can use the add
+script to automate this. This script will commit the new note with the correct
 message and, unless the -c option is specified, push the result to origin. This
 should make filing new notes a fairly burdenless process: write a note using
 your favorite text editor and run the add script. Note that the add script
@@ -174,6 +174,10 @@ For example:
 
 Update idea: Implement pluggable formatter
 
+Similar to adding, if you only have a single note updated in the database then
+you can use the update script to automate this. Note that the update script
+currently cannot handle updates with extra files.
+
 Once a note is acted upon (implemented or you have decided not to do anything
 about it), you can either delete it or move it to the reference. Simply
 deleting a note is appropriate for simple bugs and features where all the
diff --git a/README.cli b/README.cli
index 2ca9e55..d61e868 100644
--- a/README.cli
+++ b/README.cli
@@ -107,7 +107,7 @@ a new feature), \c{quality} (improve quality of implementation), \c{infra}
 Further, labels can be used to group notes based on certain criteria. For
 example, \c{doc} (documentation issue), \c{windows} (Windows-specific),
 \c{2.0.0} (scheduled for the 2.0.0 release), \c{john} (assigned to John). The
-names of subdirectories in which the issue is located are also considered its
+names of subdirectories in which the note is located are also considered its
 labels. So, for example, if the above \"Detect empty name\" bug was filed in
 \c{lihello/format/}, then its labels would be \c{bug}, \c{format}, and
 \c{libhello}.
@@ -189,12 +189,13 @@ For example:
 Add bug: Detect empty name
 \
 
-If you only have a single issue added in the database then you can use the
-\c{add} script to automate it. This script will commit the new issues with the
-correct message and, unless the \c{-c} option is specified, push the result to
-\c{origin}. This should make filing new notes a fairly burdenless process:
-write a note using your favorite text editor and run the \c{add} script. Note
-that the \c{add} script currently cannot handle notes with extra files.
+If you only have a single note added in the database then you can use the
+\c{add} script to automate this. This script will commit the new note with
+the correct message and, unless the \c{-c} option is specified, push the
+result to \c{origin}. This should make filing new notes a fairly burdenless
+process: write a note using your favorite text editor and run the \c{add}
+script. Note that the \c{add} script currently cannot handle notes with extra
+files.
 
 If you change an existing note (for example, add additional information), then
 the commit message should have the following form:
@@ -209,6 +210,10 @@ For example:
 Update idea: Implement pluggable formatter
 \
 
+Similar to adding, if you only have a single note updated in the database then
+you can use the \c{update} script to automate this. Note that the \c{update}
+script currently cannot handle updates with extra files.
+
 Once a note is acted upon (implemented or you have decided not to do anything
 about it), you can either delete it or move it to the reference. Simply
 deleting a note is appropriate for simple bugs and features where all the
diff --git a/update b/update
new file mode 100755
index 0000000..fc2ba54
--- /dev/null
+++ b/update
@@ -0,0 +1,164 @@
+#! /usr/bin/env bash
+
+# Commit and push an update to an existing note.
+#
+# -c
+#   Commit only, don't push.
+#
+usage="usage: $0 [-c]"
+
+owd=`pwd`
+trap "{ cd $owd; exit 1; }" ERR
+set -o errtrace # Trap in functions.
+
+function info () { echo "$*" 1>&2; }
+function error () { info "$*"; exit 1; }
+
+push="y"
+
+while [ $# -gt 0 ]; do
+  case $1 in
+    -c)
+      push="n"
+      shift
+      ;;
+    *)
+      error "$usage"
+      ;;
+  esac
+done
+
+if git status --porcelain | grep -q '^M '; then
+  error "error: repository already has staged changes"
+fi
+
+# @@ We could probably handle the case where extra files are added to the
+# note. We just need to make sure they are not changes to other notes.
+#
+if git status --porcelain | grep -q '^?? '; then
+  error "error: repository has untracked files"
+fi
+
+modified="$(git status --porcelain  | sed -n -e 's/^ M \(.*\)/\1/p')"
+
+mc="$(echo "$modified" | wc -w)"
+
+if [ "$mc" -eq 0 ]; then
+  error "error: nothing modified"
+fi
+
+if [ "$mc" -gt 1 ]; then
+  error "error: multiple modified files"
+fi
+
+# Extract the start line and count from the changeset header.
+#
+# The format is:
+#
+# @@ [+-]FROM-START,FROM-COUNT [+-]TO-START,TO-COUNT @@
+#
+# Note that if the changeset has only one line then the count is omitted.
+#
+function changeset_start ()
+{
+  echo "$1" | sed -e 's/^@@ [^ ]* [+-]\([^, ]*\).*$/\1/'
+}
+
+function changeset_count ()
+{
+  local r
+  r="$(echo "$1" | sed -n -e 's/^@@ [^ ]* [+-][^,]*,\([^ ]*\) @@$/\1/p')"
+  if [ -z "$r" ]; then
+    r="1"
+  fi
+  echo "$r"
+}
+
+#changeset_start "@@ -12,3 +13,11 @@"
+#changeset_count "@@ -12,3 +13,11 @@"
+#changeset_start "@@ -12 +13 @@"
+#changeset_count "@@ -12 +13 @@"
+
+h=
+if [ "$(basename "$modified")" = "list" ]; then
+  #
+  # This is a list of notes. The plan is to get the start line numbers for the
+  # first and last change set. We then scan the list line by line looking for
+  # headers until we hit the start of the first change set: the last header
+  # that we see should be our header. We also continue scanning until we hit
+  # the end of the last changeset: if we see any headers then we are updating
+  # multiple notes.
+  #
+
+  # Get the diff output as a list of change set headers.
+  #
+  diff="$(git diff -U0 | sed -n -e '/^@@ /p')"
+
+  fc="$(echo "$diff" | head -n 1)"
+  lc="$(echo "$diff" | tail -n 1)"
+
+  sl="$(changeset_start "$fc")"
+  el="$(($(changeset_start "$lc") + $(changeset_count "$lc") - 1))"
+
+  # Scan the list looking for headers.
+  #
+  l=
+  c="0"
+  while read l || [ -n "$l" ]; do
+
+    # Stop if we are past the end line.
+    #
+    c="$(($c + 1))"
+    if [ "$c" -gt "$el" ]; then
+      break
+    fi
+
+    # See if this is a header.
+    #
+    if echo "$l" | grep -q '^[-?!] '; then
+
+      # If we are past the start line then we are modifying multiple notes.
+      #
+      if [ "$c" -gt "$sl" ]; then
+	error "error: multiple modified notes in $modified:${sl}-${el}"
+      fi
+
+      # This is the last header.
+      #
+      h="$l"
+    fi
+  done < <(cat "$modified")
+
+  if [ -z "$h" ]; then
+    error "error: unable to find note header for $modified:${sl}-${el}"
+  fi
+else
+  # This is a file note. All we need to do is get its header (the first line).
+  #
+  h="$(head -n 1 "$modified")"
+fi
+
+# Header should start with one of [-?!].
+#
+sum="$(echo "$h" | sed -n -re 's/^[-?!] ([^[]*)( \[.*\])?$/\1/p')"
+
+if [ -z "$sum" ]; then
+  error "error: unable to parse note header '$h'"
+fi
+
+# By convention the first label is a note type (bug, feature, etc).
+#
+type="$(echo "$h" | sed -n -re 's/^[^[]*\[([^ ]*).*\]$/\1/p')"
+
+if [ -z "$type" ]; then
+  error "error: note header does not include note type as first label"
+fi
+
+# Ok, all looks good so add, commit and push the change.
+#
+git add "$modified"
+git ci -m "Update $type: $sum"
+
+if [ "$push" = "y" ]; then
+  git push
+fi
-- 
cgit v1.1