]> git.ipfire.org Git - thirdparty/git.git/blobdiff - add-patch.c
Sync with maint
[thirdparty/git.git] / add-patch.c
index 79eefa950501c845add3d98b017075f2713bdd3e..2c46fe5b3332bf844007ea0d17dee250f0ba5756 100644 (file)
@@ -5,23 +5,62 @@
 #include "argv-array.h"
 #include "pathspec.h"
 #include "color.h"
+#include "diff.h"
+
+enum prompt_mode_type {
+       PROMPT_MODE_CHANGE = 0, PROMPT_DELETION, PROMPT_HUNK
+};
+
+static const char *prompt_mode[] = {
+       N_("Stage mode change [y,n,a,q,d%s,?]? "),
+       N_("Stage deletion [y,n,a,q,d%s,?]? "),
+       N_("Stage this hunk [y,n,a,q,d%s,?]? ")
+};
+
+struct hunk_header {
+       unsigned long old_offset, old_count, new_offset, new_count;
+       /*
+        * Start/end offsets to the extra text after the second `@@` in the
+        * hunk header, e.g. the function signature. This is expected to
+        * include the newline.
+        */
+       size_t extra_start, extra_end, colored_extra_start, colored_extra_end;
+};
 
 struct hunk {
-       size_t start, end, colored_start, colored_end;
+       size_t start, end, colored_start, colored_end, splittable_into;
+       ssize_t delta;
        enum { UNDECIDED_HUNK = 0, SKIP_HUNK, USE_HUNK } use;
+       struct hunk_header header;
 };
 
 struct add_p_state {
-       struct repository *r;
+       struct add_i_state s;
        struct strbuf answer, buf;
 
        /* parsed diff */
        struct strbuf plain, colored;
-       struct hunk head;
-       struct hunk *hunk;
-       size_t hunk_nr, hunk_alloc;
+       struct file_diff {
+               struct hunk head;
+               struct hunk *hunk;
+               size_t hunk_nr, hunk_alloc;
+               unsigned deleted:1, mode_change:1,binary:1;
+       } *file_diff;
+       size_t file_diff_nr;
 };
 
+static void err(struct add_p_state *s, const char *fmt, ...)
+{
+       va_list args;
+
+       va_start(args, fmt);
+       fputs(s->s.error_color, stderr);
+       vfprintf(stderr, fmt, args);
+       fputs(s->s.reset_color, stderr);
+       fputc('\n', stderr);
+       va_end(args);
+}
+
 static void setup_child_process(struct add_p_state *s,
                                struct child_process *cp, ...)
 {
@@ -35,7 +74,81 @@ static void setup_child_process(struct add_p_state *s,
 
        cp->git_cmd = 1;
        argv_array_pushf(&cp->env_array,
-                        INDEX_ENVIRONMENT "=%s", s->r->index_file);
+                        INDEX_ENVIRONMENT "=%s", s->s.r->index_file);
+}
+
+static int parse_range(const char **p,
+                      unsigned long *offset, unsigned long *count)
+{
+       char *pend;
+
+       *offset = strtoul(*p, &pend, 10);
+       if (pend == *p)
+               return -1;
+       if (*pend != ',') {
+               *count = 1;
+               *p = pend;
+               return 0;
+       }
+       *count = strtoul(pend + 1, (char **)p, 10);
+       return *p == pend + 1 ? -1 : 0;
+}
+
+static int parse_hunk_header(struct add_p_state *s, struct hunk *hunk)
+{
+       struct hunk_header *header = &hunk->header;
+       const char *line = s->plain.buf + hunk->start, *p = line;
+       char *eol = memchr(p, '\n', s->plain.len - hunk->start);
+
+       if (!eol)
+               eol = s->plain.buf + s->plain.len;
+
+       if (!skip_prefix(p, "@@ -", &p) ||
+           parse_range(&p, &header->old_offset, &header->old_count) < 0 ||
+           !skip_prefix(p, " +", &p) ||
+           parse_range(&p, &header->new_offset, &header->new_count) < 0 ||
+           !skip_prefix(p, " @@", &p))
+               return error(_("could not parse hunk header '%.*s'"),
+                            (int)(eol - line), line);
+
+       hunk->start = eol - s->plain.buf + (*eol == '\n');
+       header->extra_start = p - s->plain.buf;
+       header->extra_end = hunk->start;
+
+       if (!s->colored.len) {
+               header->colored_extra_start = header->colored_extra_end = 0;
+               return 0;
+       }
+
+       /* Now find the extra text in the colored diff */
+       line = s->colored.buf + hunk->colored_start;
+       eol = memchr(line, '\n', s->colored.len - hunk->colored_start);
+       if (!eol)
+               eol = s->colored.buf + s->colored.len;
+       p = memmem(line, eol - line, "@@ -", 4);
+       if (!p)
+               return error(_("could not parse colored hunk header '%.*s'"),
+                            (int)(eol - line), line);
+       p = memmem(p + 4, eol - p - 4, " @@", 3);
+       if (!p)
+               return error(_("could not parse colored hunk header '%.*s'"),
+                            (int)(eol - line), line);
+       hunk->colored_start = eol - s->colored.buf + (*eol == '\n');
+       header->colored_extra_start = p + 3 - s->colored.buf;
+       header->colored_extra_end = hunk->colored_start;
+
+       return 0;
+}
+
+static int is_octal(const char *p, size_t len)
+{
+       if (!len)
+               return 0;
+
+       while (len--)
+               if (*p < '0' || *(p++) > '7')
+                       return 0;
+       return 1;
 }
 
 static int parse_diff(struct add_p_state *s, const struct pathspec *ps)
@@ -43,8 +156,9 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps)
        struct argv_array args = ARGV_ARRAY_INIT;
        struct strbuf *plain = &s->plain, *colored = NULL;
        struct child_process cp = CHILD_PROCESS_INIT;
-       char *p, *pend, *colored_p = NULL, *colored_pend = NULL;
-       size_t i, color_arg_index;
+       char *p, *pend, *colored_p = NULL, *colored_pend = NULL, marker = '\0';
+       size_t file_diff_alloc = 0, i, color_arg_index;
+       struct file_diff *file_diff = NULL;
        struct hunk *hunk = NULL;
        int res;
 
@@ -84,32 +198,115 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps)
        }
        argv_array_clear(&args);
 
-       /* parse hunks */
+       /* parse files and hunks */
        p = plain->buf;
        pend = p + plain->len;
        while (p != pend) {
                char *eol = memchr(p, '\n', pend - p);
+               const char *deleted = NULL, *mode_change = NULL;
+
                if (!eol)
                        eol = pend;
 
                if (starts_with(p, "diff ")) {
-                       if (p != plain->buf)
-                               BUG("multi-file diff not yet handled");
-                       hunk = &s->head;
+                       s->file_diff_nr++;
+                       ALLOC_GROW(s->file_diff, s->file_diff_nr,
+                                  file_diff_alloc);
+                       file_diff = s->file_diff + s->file_diff_nr - 1;
+                       memset(file_diff, 0, sizeof(*file_diff));
+                       hunk = &file_diff->head;
+                       hunk->start = p - plain->buf;
+                       if (colored_p)
+                               hunk->colored_start = colored_p - colored->buf;
+                       marker = '\0';
                } else if (p == plain->buf)
                        BUG("diff starts with unexpected line:\n"
                            "%.*s\n", (int)(eol - p), p);
-               else if (starts_with(p, "@@ ")) {
-                       s->hunk_nr++;
-                       ALLOC_GROW(s->hunk, s->hunk_nr,
-                                  s->hunk_alloc);
-                       hunk = s->hunk + s->hunk_nr - 1;
+               else if (file_diff->deleted)
+                       ; /* keep the rest of the file in a single "hunk" */
+               else if (starts_with(p, "@@ ") ||
+                        (hunk == &file_diff->head &&
+                         skip_prefix(p, "deleted file", &deleted))) {
+                       if (marker == '-' || marker == '+')
+                               /*
+                                * Should not happen; previous hunk did not end
+                                * in a context line? Handle it anyway.
+                                */
+                               hunk->splittable_into++;
+
+                       file_diff->hunk_nr++;
+                       ALLOC_GROW(file_diff->hunk, file_diff->hunk_nr,
+                                  file_diff->hunk_alloc);
+                       hunk = file_diff->hunk + file_diff->hunk_nr - 1;
                        memset(hunk, 0, sizeof(*hunk));
 
                        hunk->start = p - plain->buf;
                        if (colored)
                                hunk->colored_start = colored_p - colored->buf;
-               }
+
+                       if (deleted)
+                               file_diff->deleted = 1;
+                       else if (parse_hunk_header(s, hunk) < 0)
+                               return -1;
+
+                       /*
+                        * Start counting into how many hunks this one can be
+                        * split
+                        */
+                       marker = *p;
+               } else if (hunk == &file_diff->head &&
+                          skip_prefix(p, "old mode ", &mode_change) &&
+                          is_octal(mode_change, eol - mode_change)) {
+                       if (file_diff->mode_change)
+                               BUG("double mode change?\n\n%.*s",
+                                   (int)(eol - plain->buf), plain->buf);
+                       if (file_diff->hunk_nr++)
+                               BUG("mode change in the middle?\n\n%.*s",
+                                   (int)(eol - plain->buf), plain->buf);
+
+                       /*
+                        * Do *not* change `hunk`: the mode change pseudo-hunk
+                        * is _part of_ the header "hunk".
+                        */
+                       file_diff->mode_change = 1;
+                       ALLOC_GROW(file_diff->hunk, file_diff->hunk_nr,
+                                  file_diff->hunk_alloc);
+                       memset(file_diff->hunk, 0, sizeof(struct hunk));
+                       file_diff->hunk->start = p - plain->buf;
+                       if (colored_p)
+                               file_diff->hunk->colored_start =
+                                       colored_p - colored->buf;
+               } else if (hunk == &file_diff->head &&
+                          skip_prefix(p, "new mode ", &mode_change) &&
+                          is_octal(mode_change, eol - mode_change)) {
+
+                       /*
+                        * Extend the "mode change" pseudo-hunk to include also
+                        * the "new mode" line.
+                        */
+                       if (!file_diff->mode_change)
+                               BUG("'new mode' without 'old mode'?\n\n%.*s",
+                                   (int)(eol - plain->buf), plain->buf);
+                       if (file_diff->hunk_nr != 1)
+                               BUG("mode change in the middle?\n\n%.*s",
+                                   (int)(eol - plain->buf), plain->buf);
+                       if (p - plain->buf != file_diff->hunk->end)
+                               BUG("'new mode' does not immediately follow "
+                                   "'old mode'?\n\n%.*s",
+                                   (int)(eol - plain->buf), plain->buf);
+               } else if (hunk == &file_diff->head &&
+                          starts_with(p, "Binary files "))
+                       file_diff->binary = 1;
+
+               if (file_diff->deleted && file_diff->mode_change)
+                       BUG("diff contains delete *and* a mode change?!?\n%.*s",
+                           (int)(eol - (plain->buf + file_diff->head.start)),
+                           plain->buf + file_diff->head.start);
+
+               if ((marker == '-' || marker == '+') && *p == ' ')
+                       hunk->splittable_into++;
+               if (marker && *p != '\\')
+                       marker = *p;
 
                p = eol == pend ? pend : eol + 1;
                hunk->end = p - plain->buf;
@@ -124,14 +321,80 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps)
 
                        hunk->colored_end = colored_p - colored->buf;
                }
+
+               if (mode_change) {
+                       if (file_diff->hunk_nr != 1)
+                               BUG("mode change in hunk #%d???",
+                                   (int)file_diff->hunk_nr);
+                       /* Adjust the end of the "mode change" pseudo-hunk */
+                       file_diff->hunk->end = hunk->end;
+                       if (colored)
+                               file_diff->hunk->colored_end = hunk->colored_end;
+               }
        }
 
+       if (marker == '-' || marker == '+')
+               /*
+                * Last hunk ended in non-context line (i.e. it appended lines
+                * to the file, so there are no trailing context lines).
+                */
+               hunk->splittable_into++;
+
        return 0;
 }
 
+static size_t find_next_line(struct strbuf *sb, size_t offset)
+{
+       char *eol;
+
+       if (offset >= sb->len)
+               BUG("looking for next line beyond buffer (%d >= %d)\n%s",
+                   (int)offset, (int)sb->len, sb->buf);
+
+       eol = memchr(sb->buf + offset, '\n', sb->len - offset);
+       if (!eol)
+               return sb->len;
+       return eol - sb->buf + 1;
+}
+
 static void render_hunk(struct add_p_state *s, struct hunk *hunk,
-                       int colored, struct strbuf *out)
+                       ssize_t delta, int colored, struct strbuf *out)
 {
+       struct hunk_header *header = &hunk->header;
+
+       if (hunk->header.old_offset != 0 || hunk->header.new_offset != 0) {
+               /*
+                * Generate the hunk header dynamically, except for special
+                * hunks (such as the diff header).
+                */
+               const char *p;
+               size_t len;
+               unsigned long old_offset = header->old_offset;
+               unsigned long new_offset = header->new_offset;
+
+               if (!colored) {
+                       p = s->plain.buf + header->extra_start;
+                       len = header->extra_end - header->extra_start;
+               } else {
+                       strbuf_addstr(out, s->s.fraginfo_color);
+                       p = s->colored.buf + header->colored_extra_start;
+                       len = header->colored_extra_end
+                               - header->colored_extra_start;
+               }
+
+               new_offset += delta;
+
+               strbuf_addf(out, "@@ -%lu,%lu +%lu,%lu @@",
+                           old_offset, header->old_count,
+                           new_offset, header->new_count);
+               if (len)
+                       strbuf_add(out, p, len);
+               else if (colored)
+                       strbuf_addf(out, "%s\n", GIT_COLOR_RESET);
+               else
+                       strbuf_addch(out, '\n');
+       }
+
        if (colored)
                strbuf_add(out, s->colored.buf + hunk->colored_start,
                           hunk->colored_end - hunk->colored_start);
@@ -140,61 +403,658 @@ static void render_hunk(struct add_p_state *s, struct hunk *hunk,
                           hunk->end - hunk->start);
 }
 
-static void reassemble_patch(struct add_p_state *s, struct strbuf *out)
+static void render_diff_header(struct add_p_state *s,
+                              struct file_diff *file_diff, int colored,
+                              struct strbuf *out)
+{
+       /*
+        * If there was a mode change, the first hunk is a pseudo hunk that
+        * corresponds to the mode line in the header. If the user did not want
+        * to stage that "hunk", we actually have to cut it out from the header.
+        */
+       int skip_mode_change =
+               file_diff->mode_change && file_diff->hunk->use != USE_HUNK;
+       struct hunk *head = &file_diff->head, *first = file_diff->hunk;
+
+       if (!skip_mode_change) {
+               render_hunk(s, head, 0, colored, out);
+               return;
+       }
+
+       if (colored) {
+               const char *p = s->colored.buf;
+
+               strbuf_add(out, p + head->colored_start,
+                           first->colored_start - head->colored_start);
+               strbuf_add(out, p + first->colored_end,
+                           head->colored_end - first->colored_end);
+       } else {
+               const char *p = s->plain.buf;
+
+               strbuf_add(out, p + head->start, first->start - head->start);
+               strbuf_add(out, p + first->end, head->end - first->end);
+       }
+}
+
+/* Coalesce hunks again that were split */
+static int merge_hunks(struct add_p_state *s, struct file_diff *file_diff,
+                      size_t *hunk_index, int use_all, struct hunk *merged)
+{
+       size_t i = *hunk_index, delta;
+       struct hunk *hunk = file_diff->hunk + i;
+       /* `header` corresponds to the merged hunk */
+       struct hunk_header *header = &merged->header, *next;
+
+       if (!use_all && hunk->use != USE_HUNK)
+               return 0;
+
+       *merged = *hunk;
+       /* We simply skip the colored part (if any) when merging hunks */
+       merged->colored_start = merged->colored_end = 0;
+
+       for (; i + 1 < file_diff->hunk_nr; i++) {
+               hunk++;
+               next = &hunk->header;
+
+               /*
+                * Stop merging hunks when:
+                *
+                * - the hunk is not selected for use, or
+                * - the hunk does not overlap with the already-merged hunk(s)
+                */
+               if ((!use_all && hunk->use != USE_HUNK) ||
+                   header->new_offset >= next->new_offset + merged->delta ||
+                   header->new_offset + header->new_count
+                   < next->new_offset + merged->delta)
+                       break;
+
+               /*
+                * If the hunks were not edited, and overlap, we can simply
+                * extend the line range.
+                */
+               if (merged->start < hunk->start && merged->end > hunk->start) {
+                       merged->end = hunk->end;
+                       merged->colored_end = hunk->colored_end;
+                       delta = 0;
+               } else {
+                       const char *plain = s->plain.buf;
+                       size_t  overlapping_line_count = header->new_offset
+                               + header->new_count - merged->delta
+                               - next->new_offset;
+                       size_t overlap_end = hunk->start;
+                       size_t overlap_start = overlap_end;
+                       size_t overlap_next, len, j;
+
+                       /*
+                        * One of the hunks was edited: the modified hunk was
+                        * appended to the strbuf `s->plain`.
+                        *
+                        * Let's ensure that at least the last context line of
+                        * the first hunk overlaps with the corresponding line
+                        * of the second hunk, and then merge.
+                        */
+                       for (j = 0; j < overlapping_line_count; j++) {
+                               overlap_next = find_next_line(&s->plain,
+                                                             overlap_end);
+
+                               if (overlap_next > hunk->end)
+                                       BUG("failed to find %d context lines "
+                                           "in:\n%.*s",
+                                           (int)overlapping_line_count,
+                                           (int)(hunk->end - hunk->start),
+                                           plain + hunk->start);
+
+                               if (plain[overlap_end] != ' ')
+                                       return error(_("expected context line "
+                                                      "#%d in\n%.*s"),
+                                                    (int)(j + 1),
+                                                    (int)(hunk->end
+                                                          - hunk->start),
+                                                    plain + hunk->start);
+
+                               overlap_start = overlap_end;
+                               overlap_end = overlap_next;
+                       }
+                       len = overlap_end - overlap_start;
+
+                       if (len > merged->end - merged->start ||
+                           memcmp(plain + merged->end - len,
+                                  plain + overlap_start, len))
+                               return error(_("hunks do not overlap:\n%.*s\n"
+                                              "\tdoes not end with:\n%.*s"),
+                                            (int)(merged->end - merged->start),
+                                            plain + merged->start,
+                                            (int)len, plain + overlap_start);
+
+                       /*
+                        * Since the start-end ranges are not adjacent, we
+                        * cannot simply take the union of the ranges. To
+                        * address that, we temporarily append the union of the
+                        * lines to the `plain` strbuf.
+                        */
+                       if (merged->end != s->plain.len) {
+                               size_t start = s->plain.len;
+
+                               strbuf_add(&s->plain, plain + merged->start,
+                                          merged->end - merged->start);
+                               plain = s->plain.buf;
+                               merged->start = start;
+                               merged->end = s->plain.len;
+                       }
+
+                       strbuf_add(&s->plain,
+                                  plain + overlap_end,
+                                  hunk->end - overlap_end);
+                       merged->end = s->plain.len;
+                       merged->splittable_into += hunk->splittable_into;
+                       delta = merged->delta;
+                       merged->delta += hunk->delta;
+               }
+
+               header->old_count = next->old_offset + next->old_count
+                       - header->old_offset;
+               header->new_count = next->new_offset + delta
+                       + next->new_count - header->new_offset;
+       }
+
+       if (i == *hunk_index)
+               return 0;
+
+       *hunk_index = i;
+       return 1;
+}
+
+static void reassemble_patch(struct add_p_state *s,
+                            struct file_diff *file_diff, int use_all,
+                            struct strbuf *out)
 {
        struct hunk *hunk;
+       size_t save_len = s->plain.len, i;
+       ssize_t delta = 0;
+
+       render_diff_header(s, file_diff, 0, out);
+
+       for (i = file_diff->mode_change; i < file_diff->hunk_nr; i++) {
+               struct hunk merged = { 0 };
+
+               hunk = file_diff->hunk + i;
+               if (!use_all && hunk->use != USE_HUNK)
+                       delta += hunk->header.old_count
+                               - hunk->header.new_count;
+               else {
+                       /* merge overlapping hunks into a temporary hunk */
+                       if (merge_hunks(s, file_diff, &i, use_all, &merged))
+                               hunk = &merged;
+
+                       render_hunk(s, hunk, delta, 0, out);
+
+                       /*
+                        * In case `merge_hunks()` used `plain` as a scratch
+                        * pad (this happens when an edited hunk had to be
+                        * coalesced with another hunk).
+                        */
+                       strbuf_setlen(&s->plain, save_len);
+
+                       delta += hunk->delta;
+               }
+       }
+}
+
+static int split_hunk(struct add_p_state *s, struct file_diff *file_diff,
+                      size_t hunk_index)
+{
+       int colored = !!s->colored.len, first = 1;
+       struct hunk *hunk = file_diff->hunk + hunk_index;
+       size_t splittable_into;
+       size_t end, colored_end, current, colored_current = 0, context_line_count;
+       struct hunk_header remaining, *header;
+       char marker, ch;
+
+       if (hunk_index >= file_diff->hunk_nr)
+               BUG("invalid hunk index: %d (must be >= 0 and < %d)",
+                   (int)hunk_index, (int)file_diff->hunk_nr);
+
+       if (hunk->splittable_into < 2)
+               return 0;
+       splittable_into = hunk->splittable_into;
+
+       end = hunk->end;
+       colored_end = hunk->colored_end;
+
+       remaining = hunk->header;
+
+       file_diff->hunk_nr += splittable_into - 1;
+       ALLOC_GROW(file_diff->hunk, file_diff->hunk_nr, file_diff->hunk_alloc);
+       if (hunk_index + splittable_into < file_diff->hunk_nr)
+               memmove(file_diff->hunk + hunk_index + splittable_into,
+                       file_diff->hunk + hunk_index + 1,
+                       (file_diff->hunk_nr - hunk_index - splittable_into)
+                       * sizeof(*hunk));
+       hunk = file_diff->hunk + hunk_index;
+       hunk->splittable_into = 1;
+       memset(hunk + 1, 0, (splittable_into - 1) * sizeof(*hunk));
+
+       header = &hunk->header;
+       header->old_count = header->new_count = 0;
+
+       current = hunk->start;
+       if (colored)
+               colored_current = hunk->colored_start;
+       marker = '\0';
+       context_line_count = 0;
+
+       while (splittable_into > 1) {
+               ch = s->plain.buf[current];
+
+               if (!ch)
+                       BUG("buffer overrun while splitting hunks");
+
+               /*
+                * Is this the first context line after a chain of +/- lines?
+                * Then record the start of the next split hunk.
+                */
+               if ((marker == '-' || marker == '+') && ch == ' ') {
+                       first = 0;
+                       hunk[1].start = current;
+                       if (colored)
+                               hunk[1].colored_start = colored_current;
+                       context_line_count = 0;
+               }
+
+               /*
+                * Was the previous line a +/- one? Alternatively, is this the
+                * first line (and not a +/- one)?
+                *
+                * Then just increment the appropriate counter and continue
+                * with the next line.
+                */
+               if (marker != ' ' || (ch != '-' && ch != '+')) {
+next_hunk_line:
+                       /* Comment lines are attached to the previous line */
+                       if (ch == '\\')
+                               ch = marker ? marker : ' ';
+
+                       /* current hunk not done yet */
+                       if (ch == ' ')
+                               context_line_count++;
+                       else if (ch == '-')
+                               header->old_count++;
+                       else if (ch == '+')
+                               header->new_count++;
+                       else
+                               BUG("unhandled diff marker: '%c'", ch);
+                       marker = ch;
+                       current = find_next_line(&s->plain, current);
+                       if (colored)
+                               colored_current =
+                                       find_next_line(&s->colored,
+                                                      colored_current);
+                       continue;
+               }
+
+               /*
+                * We got us the start of a new hunk!
+                *
+                * This is a context line, so it is shared with the previous
+                * hunk, if any.
+                */
+
+               if (first) {
+                       if (header->old_count || header->new_count)
+                               BUG("counts are off: %d/%d",
+                                   (int)header->old_count,
+                                   (int)header->new_count);
+
+                       header->old_count = context_line_count;
+                       header->new_count = context_line_count;
+                       context_line_count = 0;
+                       first = 0;
+                       goto next_hunk_line;
+               }
+
+               remaining.old_offset += header->old_count;
+               remaining.old_count -= header->old_count;
+               remaining.new_offset += header->new_count;
+               remaining.new_count -= header->new_count;
+
+               /* initialize next hunk header's offsets */
+               hunk[1].header.old_offset =
+                       header->old_offset + header->old_count;
+               hunk[1].header.new_offset =
+                       header->new_offset + header->new_count;
+
+               /* add one split hunk */
+               header->old_count += context_line_count;
+               header->new_count += context_line_count;
+
+               hunk->end = current;
+               if (colored)
+                       hunk->colored_end = colored_current;
+
+               hunk++;
+               hunk->splittable_into = 1;
+               hunk->use = hunk[-1].use;
+               header = &hunk->header;
+
+               header->old_count = header->new_count = context_line_count;
+               context_line_count = 0;
+
+               splittable_into--;
+               marker = ch;
+       }
+
+       /* last hunk simply gets the rest */
+       if (header->old_offset != remaining.old_offset)
+               BUG("miscounted old_offset: %lu != %lu",
+                   header->old_offset, remaining.old_offset);
+       if (header->new_offset != remaining.new_offset)
+               BUG("miscounted new_offset: %lu != %lu",
+                   header->new_offset, remaining.new_offset);
+       header->old_count = remaining.old_count;
+       header->new_count = remaining.new_count;
+       hunk->end = end;
+       if (colored)
+               hunk->colored_end = colored_end;
+
+       return 0;
+}
+
+static void recolor_hunk(struct add_p_state *s, struct hunk *hunk)
+{
+       const char *plain = s->plain.buf;
+       size_t current, eol, next;
+
+       if (!s->colored.len)
+               return;
+
+       hunk->colored_start = s->colored.len;
+       for (current = hunk->start; current < hunk->end; ) {
+               for (eol = current; eol < hunk->end; eol++)
+                       if (plain[eol] == '\n')
+                               break;
+               next = eol + (eol < hunk->end);
+               if (eol > current && plain[eol - 1] == '\r')
+                       eol--;
+
+               strbuf_addstr(&s->colored,
+                             plain[current] == '-' ?
+                             s->s.file_old_color :
+                             plain[current] == '+' ?
+                             s->s.file_new_color :
+                             s->s.context_color);
+               strbuf_add(&s->colored, plain + current, eol - current);
+               strbuf_addstr(&s->colored, GIT_COLOR_RESET);
+               if (next > eol)
+                       strbuf_add(&s->colored, plain + eol, next - eol);
+               current = next;
+       }
+       hunk->colored_end = s->colored.len;
+}
+
+static int edit_hunk_manually(struct add_p_state *s, struct hunk *hunk)
+{
+       size_t i;
+
+       strbuf_reset(&s->buf);
+       strbuf_commented_addf(&s->buf, _("Manual hunk edit mode -- see bottom for "
+                                     "a quick guide.\n"));
+       render_hunk(s, hunk, 0, 0, &s->buf);
+       strbuf_commented_addf(&s->buf,
+                             _("---\n"
+                               "To remove '%c' lines, make them ' ' lines "
+                               "(context).\n"
+                               "To remove '%c' lines, delete them.\n"
+                               "Lines starting with %c will be removed.\n"),
+                             '-', '+', comment_line_char);
+       strbuf_commented_addf(&s->buf,
+                             _("If the patch applies cleanly, the edited hunk "
+                               "will immediately be\n"
+                               "marked for staging.\n"));
+       /*
+        * TRANSLATORS: 'it' refers to the patch mentioned in the previous
+        * messages.
+        */
+       strbuf_commented_addf(&s->buf,
+                             _("If it does not apply cleanly, you will be "
+                               "given an opportunity to\n"
+                               "edit again.  If all lines of the hunk are "
+                               "removed, then the edit is\n"
+                               "aborted and the hunk is left unchanged.\n"));
+
+       if (strbuf_edit_interactively(&s->buf, "addp-hunk-edit.diff", NULL) < 0)
+               return -1;
+
+       /* strip out commented lines */
+       hunk->start = s->plain.len;
+       for (i = 0; i < s->buf.len; ) {
+               size_t next = find_next_line(&s->buf, i);
+
+               if (s->buf.buf[i] != comment_line_char)
+                       strbuf_add(&s->plain, s->buf.buf + i, next - i);
+               i = next;
+       }
+
+       hunk->end = s->plain.len;
+       if (hunk->end == hunk->start)
+               /* The user aborted editing by deleting everything */
+               return 0;
+
+       recolor_hunk(s, hunk);
+
+       /*
+        * If the hunk header is intact, parse it, otherwise simply use the
+        * hunk header prior to editing (which will adjust `hunk->start` to
+        * skip the hunk header).
+        */
+       if (s->plain.buf[hunk->start] == '@' &&
+           parse_hunk_header(s, hunk) < 0)
+               return error(_("could not parse hunk header"));
+
+       return 1;
+}
+
+static ssize_t recount_edited_hunk(struct add_p_state *s, struct hunk *hunk,
+                                  size_t orig_old_count, size_t orig_new_count)
+{
+       struct hunk_header *header = &hunk->header;
        size_t i;
 
-       render_hunk(s, &s->head, 0, out);
+       header->old_count = header->new_count = 0;
+       for (i = hunk->start; i < hunk->end; ) {
+               switch (s->plain.buf[i]) {
+               case '-':
+                       header->old_count++;
+                       break;
+               case '+':
+                       header->new_count++;
+                       break;
+               case ' ': case '\r': case '\n':
+                       header->old_count++;
+                       header->new_count++;
+                       break;
+               }
+
+               i = find_next_line(&s->plain, i);
+       }
+
+       return orig_old_count - orig_new_count
+               - header->old_count + header->new_count;
+}
+
+static int run_apply_check(struct add_p_state *s,
+                          struct file_diff *file_diff)
+{
+       struct child_process cp = CHILD_PROCESS_INIT;
+
+       strbuf_reset(&s->buf);
+       reassemble_patch(s, file_diff, 1, &s->buf);
+
+       setup_child_process(s, &cp,
+                           "apply", "--cached", "--check", NULL);
+       if (pipe_command(&cp, s->buf.buf, s->buf.len, NULL, 0, NULL, 0))
+               return error(_("'git apply --cached' failed"));
+
+       return 0;
+}
+
+static int prompt_yesno(struct add_p_state *s, const char *prompt)
+{
+       for (;;) {
+               color_fprintf(stdout, s->s.prompt_color, "%s", _(prompt));
+               fflush(stdout);
+               if (strbuf_getline(&s->answer, stdin) == EOF)
+                       return -1;
+               strbuf_trim_trailing_newline(&s->answer);
+               switch (tolower(s->answer.buf[0])) {
+               case 'n': return 0;
+               case 'y': return 1;
+               }
+       }
+}
+
+static int edit_hunk_loop(struct add_p_state *s,
+                         struct file_diff *file_diff, struct hunk *hunk)
+{
+       size_t plain_len = s->plain.len, colored_len = s->colored.len;
+       struct hunk backup;
+
+       backup = *hunk;
+
+       for (;;) {
+               int res = edit_hunk_manually(s, hunk);
+               if (res == 0) {
+                       /* abandonded */
+                       *hunk = backup;
+                       return -1;
+               }
+
+               if (res > 0) {
+                       hunk->delta +=
+                               recount_edited_hunk(s, hunk,
+                                                   backup.header.old_count,
+                                                   backup.header.new_count);
+                       if (!run_apply_check(s, file_diff))
+                               return 0;
+               }
+
+               /* Drop edits (they were appended to s->plain) */
+               strbuf_setlen(&s->plain, plain_len);
+               strbuf_setlen(&s->colored, colored_len);
+               *hunk = backup;
+
+               /*
+                * TRANSLATORS: do not translate [y/n]
+                * The program will only accept that input at this point.
+                * Consider translating (saying "no" discards!) as
+                * (saying "n" for "no" discards!) if the translation
+                * of the word "no" does not start with n.
+                */
+               res = prompt_yesno(s, _("Your edited hunk does not apply. "
+                                       "Edit again (saying \"no\" discards!) "
+                                       "[y/n]? "));
+               if (res < 1)
+                       return -1;
+       }
+}
+
+#define SUMMARY_HEADER_WIDTH 20
+#define SUMMARY_LINE_WIDTH 80
+static void summarize_hunk(struct add_p_state *s, struct hunk *hunk,
+                          struct strbuf *out)
+{
+       struct hunk_header *header = &hunk->header;
+       struct strbuf *plain = &s->plain;
+       size_t len = out->len, i;
+
+       strbuf_addf(out, " -%lu,%lu +%lu,%lu ",
+                   header->old_offset, header->old_count,
+                   header->new_offset, header->new_count);
+       if (out->len - len < SUMMARY_HEADER_WIDTH)
+               strbuf_addchars(out, ' ',
+                               SUMMARY_HEADER_WIDTH + len - out->len);
+       for (i = hunk->start; i < hunk->end; i = find_next_line(plain, i))
+               if (plain->buf[i] != ' ')
+                       break;
+       if (i < hunk->end)
+               strbuf_add(out, plain->buf + i, find_next_line(plain, i) - i);
+       if (out->len - len > SUMMARY_LINE_WIDTH)
+               strbuf_setlen(out, len + SUMMARY_LINE_WIDTH);
+       strbuf_complete_line(out);
+}
+
+#define DISPLAY_HUNKS_LINES 20
+static size_t display_hunks(struct add_p_state *s,
+                           struct file_diff *file_diff, size_t start_index)
+{
+       size_t end_index = start_index + DISPLAY_HUNKS_LINES;
+
+       if (end_index > file_diff->hunk_nr)
+               end_index = file_diff->hunk_nr;
 
-       for (i = 0; i < s->hunk_nr; i++) {
-               hunk = s->hunk + i;
-               if (hunk->use == USE_HUNK)
-                       render_hunk(s, hunk, 0, out);
+       while (start_index < end_index) {
+               struct hunk *hunk = file_diff->hunk + start_index++;
+
+               strbuf_reset(&s->buf);
+               strbuf_addf(&s->buf, "%c%2d: ", hunk->use == USE_HUNK ? '+'
+                           : hunk->use == SKIP_HUNK ? '-' : ' ',
+                           (int)start_index);
+               summarize_hunk(s, hunk, &s->buf);
+               fputs(s->buf.buf, stdout);
        }
+
+       return end_index;
 }
 
 static const char help_patch_text[] =
 N_("y - stage this hunk\n"
    "n - do not stage this hunk\n"
+   "q - quit; do not stage this hunk or any of the remaining ones\n"
    "a - stage this and all the remaining hunks\n"
-   "d - do not stage this hunk nor any of the remaining hunks\n"
-   "j - leave this hunk undecided, see next undecided hunk\n"
+   "d - do not stage this hunk nor any of the remaining hunks\n");
+
+static const char help_patch_remainder[] =
+N_("j - leave this hunk undecided, see next undecided hunk\n"
    "J - leave this hunk undecided, see next hunk\n"
    "k - leave this hunk undecided, see previous undecided hunk\n"
    "K - leave this hunk undecided, see previous hunk\n"
+   "g - select a hunk to go to\n"
+   "/ - search for a hunk matching the given regex\n"
+   "s - split the current hunk into smaller hunks\n"
+   "e - manually edit the current hunk\n"
    "? - print help\n");
 
-static int patch_update_file(struct add_p_state *s)
+static int patch_update_file(struct add_p_state *s,
+                            struct file_diff *file_diff)
 {
        size_t hunk_index = 0;
        ssize_t i, undecided_previous, undecided_next;
        struct hunk *hunk;
        char ch;
        struct child_process cp = CHILD_PROCESS_INIT;
-       int colored = !!s->colored.len;
+       int colored = !!s->colored.len, quit = 0;
+       enum prompt_mode_type prompt_mode_type;
 
-       if (!s->hunk_nr)
+       if (!file_diff->hunk_nr)
                return 0;
 
        strbuf_reset(&s->buf);
-       render_hunk(s, &s->head, colored, &s->buf);
+       render_diff_header(s, file_diff, colored, &s->buf);
        fputs(s->buf.buf, stdout);
        for (;;) {
-               if (hunk_index >= s->hunk_nr)
+               if (hunk_index >= file_diff->hunk_nr)
                        hunk_index = 0;
-               hunk = s->hunk + hunk_index;
+               hunk = file_diff->hunk + hunk_index;
 
                undecided_previous = -1;
                for (i = hunk_index - 1; i >= 0; i--)
-                       if (s->hunk[i].use == UNDECIDED_HUNK) {
+                       if (file_diff->hunk[i].use == UNDECIDED_HUNK) {
                                undecided_previous = i;
                                break;
                        }
 
                undecided_next = -1;
-               for (i = hunk_index + 1; i < s->hunk_nr; i++)
-                       if (s->hunk[i].use == UNDECIDED_HUNK) {
+               for (i = hunk_index + 1; i < file_diff->hunk_nr; i++)
+                       if (file_diff->hunk[i].use == UNDECIDED_HUNK) {
                                undecided_next = i;
                                break;
                        }
@@ -205,7 +1065,7 @@ static int patch_update_file(struct add_p_state *s)
                        break;
 
                strbuf_reset(&s->buf);
-               render_hunk(s, hunk, colored, &s->buf);
+               render_hunk(s, hunk, 0, colored, &s->buf);
                fputs(s->buf.buf, stdout);
 
                strbuf_reset(&s->buf);
@@ -215,11 +1075,29 @@ static int patch_update_file(struct add_p_state *s)
                        strbuf_addstr(&s->buf, ",K");
                if (undecided_next >= 0)
                        strbuf_addstr(&s->buf, ",j");
-               if (hunk_index + 1 < s->hunk_nr)
+               if (hunk_index + 1 < file_diff->hunk_nr)
                        strbuf_addstr(&s->buf, ",J");
-               printf("(%"PRIuMAX"/%"PRIuMAX") ",
-                      (uintmax_t)hunk_index + 1, (uintmax_t)s->hunk_nr);
-               printf(_("Stage this hunk [y,n,a,d%s,?]? "), s->buf.buf);
+               if (file_diff->hunk_nr > 1)
+                       strbuf_addstr(&s->buf, ",g,/");
+               if (hunk->splittable_into > 1)
+                       strbuf_addstr(&s->buf, ",s");
+               if (hunk_index + 1 > file_diff->mode_change &&
+                   !file_diff->deleted)
+                       strbuf_addstr(&s->buf, ",e");
+
+               if (file_diff->deleted)
+                       prompt_mode_type = PROMPT_DELETION;
+               else if (file_diff->mode_change && !hunk_index)
+                       prompt_mode_type = PROMPT_MODE_CHANGE;
+               else
+                       prompt_mode_type = PROMPT_HUNK;
+
+               color_fprintf(stdout, s->s.prompt_color,
+                             "(%"PRIuMAX"/%"PRIuMAX") ",
+                             (uintmax_t)hunk_index + 1,
+                             (uintmax_t)file_diff->hunk_nr);
+               color_fprintf(stdout, s->s.prompt_color,
+                             _(prompt_mode[prompt_mode_type]), s->buf.buf);
                fflush(stdout);
                if (strbuf_getline(&s->answer, stdin) == EOF)
                        break;
@@ -232,63 +1110,205 @@ static int patch_update_file(struct add_p_state *s)
                        hunk->use = USE_HUNK;
 soft_increment:
                        hunk_index = undecided_next < 0 ?
-                               s->hunk_nr : undecided_next;
+                               file_diff->hunk_nr : undecided_next;
                } else if (ch == 'n') {
                        hunk->use = SKIP_HUNK;
                        goto soft_increment;
                } else if (ch == 'a') {
-                       for (; hunk_index < s->hunk_nr; hunk_index++) {
-                               hunk = s->hunk + hunk_index;
+                       for (; hunk_index < file_diff->hunk_nr; hunk_index++) {
+                               hunk = file_diff->hunk + hunk_index;
                                if (hunk->use == UNDECIDED_HUNK)
                                        hunk->use = USE_HUNK;
                        }
-               } else if (ch == 'd') {
-                       for (; hunk_index < s->hunk_nr; hunk_index++) {
-                               hunk = s->hunk + hunk_index;
+               } else if (ch == 'd' || ch == 'q') {
+                       for (; hunk_index < file_diff->hunk_nr; hunk_index++) {
+                               hunk = file_diff->hunk + hunk_index;
                                if (hunk->use == UNDECIDED_HUNK)
                                        hunk->use = SKIP_HUNK;
                        }
-               } else if (hunk_index && s->answer.buf[0] == 'K')
-                       hunk_index--;
-               else if (hunk_index + 1 < s->hunk_nr &&
-                        s->answer.buf[0] == 'J')
-                       hunk_index++;
-               else if (undecided_previous >= 0 &&
-                        s->answer.buf[0] == 'k')
-                       hunk_index = undecided_previous;
-               else if (undecided_next >= 0 && s->answer.buf[0] == 'j')
-                       hunk_index = undecided_next;
-               else
-                       puts(_(help_patch_text));
+                       if (ch == 'q') {
+                               quit = 1;
+                               break;
+                       }
+               } else if (s->answer.buf[0] == 'K') {
+                       if (hunk_index)
+                               hunk_index--;
+                       else
+                               err(s, _("No previous hunk"));
+               } else if (s->answer.buf[0] == 'J') {
+                       if (hunk_index + 1 < file_diff->hunk_nr)
+                               hunk_index++;
+                       else
+                               err(s, _("No next hunk"));
+               } else if (s->answer.buf[0] == 'k') {
+                       if (undecided_previous >= 0)
+                               hunk_index = undecided_previous;
+                       else
+                               err(s, _("No previous hunk"));
+               } else if (s->answer.buf[0] == 'j') {
+                       if (undecided_next >= 0)
+                               hunk_index = undecided_next;
+                       else
+                               err(s, _("No next hunk"));
+               } else if (s->answer.buf[0] == 'g') {
+                       char *pend;
+                       unsigned long response;
+
+                       if (file_diff->hunk_nr < 2) {
+                               err(s, _("No other hunks to goto"));
+                               continue;
+                       }
+                       strbuf_remove(&s->answer, 0, 1);
+                       strbuf_trim(&s->answer);
+                       i = hunk_index - DISPLAY_HUNKS_LINES / 2;
+                       if (i < file_diff->mode_change)
+                               i = file_diff->mode_change;
+                       while (s->answer.len == 0) {
+                               i = display_hunks(s, file_diff, i);
+                               printf("%s", i < file_diff->hunk_nr ?
+                                      _("go to which hunk (<ret> to see "
+                                        "more)? ") : _("go to which hunk? "));
+                               fflush(stdout);
+                               if (strbuf_getline(&s->answer,
+                                                  stdin) == EOF)
+                                       break;
+                               strbuf_trim_trailing_newline(&s->answer);
+                       }
+
+                       strbuf_trim(&s->answer);
+                       response = strtoul(s->answer.buf, &pend, 10);
+                       if (*pend || pend == s->answer.buf)
+                               err(s, _("Invalid number: '%s'"),
+                                   s->answer.buf);
+                       else if (0 < response && response <= file_diff->hunk_nr)
+                               hunk_index = response - 1;
+                       else
+                               err(s, Q_("Sorry, only %d hunk available.",
+                                         "Sorry, only %d hunks available.",
+                                         file_diff->hunk_nr),
+                                   (int)file_diff->hunk_nr);
+               } else if (s->answer.buf[0] == '/') {
+                       regex_t regex;
+                       int ret;
+
+                       if (file_diff->hunk_nr < 2) {
+                               err(s, _("No other hunks to search"));
+                               continue;
+                       }
+                       strbuf_remove(&s->answer, 0, 1);
+                       strbuf_trim_trailing_newline(&s->answer);
+                       if (s->answer.len == 0) {
+                               printf("%s", _("search for regex? "));
+                               fflush(stdout);
+                               if (strbuf_getline(&s->answer,
+                                                  stdin) == EOF)
+                                       break;
+                               strbuf_trim_trailing_newline(&s->answer);
+                               if (s->answer.len == 0)
+                                       continue;
+                       }
+                       ret = regcomp(&regex, s->answer.buf,
+                                     REG_EXTENDED | REG_NOSUB | REG_NEWLINE);
+                       if (ret) {
+                               char errbuf[1024];
+
+                               regerror(ret, &regex, errbuf, sizeof(errbuf));
+                               err(s, _("Malformed search regexp %s: %s"),
+                                   s->answer.buf, errbuf);
+                               continue;
+                       }
+                       i = hunk_index;
+                       for (;;) {
+                               /* render the hunk into a scratch buffer */
+                               render_hunk(s, file_diff->hunk + i, 0, 0,
+                                           &s->buf);
+                               if (regexec(&regex, s->buf.buf, 0, NULL, 0)
+                                   != REG_NOMATCH)
+                                       break;
+                               i++;
+                               if (i == file_diff->hunk_nr)
+                                       i = 0;
+                               if (i != hunk_index)
+                                       continue;
+                               err(s, _("No hunk matches the given pattern"));
+                               break;
+                       }
+                       hunk_index = i;
+               } else if (s->answer.buf[0] == 's') {
+                       size_t splittable_into = hunk->splittable_into;
+                       if (splittable_into < 2)
+                               err(s, _("Sorry, cannot split this hunk"));
+                       else if (!split_hunk(s, file_diff,
+                                            hunk - file_diff->hunk))
+                               color_fprintf_ln(stdout, s->s.header_color,
+                                                _("Split into %d hunks."),
+                                                (int)splittable_into);
+               } else if (s->answer.buf[0] == 'e') {
+                       if (hunk_index + 1 == file_diff->mode_change)
+                               err(s, _("Sorry, cannot edit this hunk"));
+                       else if (edit_hunk_loop(s, file_diff, hunk) >= 0) {
+                               hunk->use = USE_HUNK;
+                               goto soft_increment;
+                       }
+               } else {
+                       const char *p = _(help_patch_remainder), *eol = p;
+
+                       color_fprintf(stdout, s->s.help_color, "%s",
+                                     _(help_patch_text));
+
+                       /*
+                        * Show only those lines of the remainder that are
+                        * actually applicable with the current hunk.
+                        */
+                       for (; *p; p = eol + (*eol == '\n')) {
+                               eol = strchrnul(p, '\n');
+
+                               /*
+                                * `s->buf` still contains the part of the
+                                * commands shown in the prompt that are not
+                                * always available.
+                                */
+                               if (*p != '?' && !strchr(s->buf.buf, *p))
+                                       continue;
+
+                               color_fprintf_ln(stdout, s->s.help_color,
+                                                "%.*s", (int)(eol - p), p);
+                       }
+               }
        }
 
        /* Any hunk to be used? */
-       for (i = 0; i < s->hunk_nr; i++)
-               if (s->hunk[i].use == USE_HUNK)
+       for (i = 0; i < file_diff->hunk_nr; i++)
+               if (file_diff->hunk[i].use == USE_HUNK)
                        break;
 
-       if (i < s->hunk_nr) {
+       if (i < file_diff->hunk_nr) {
                /* At least one hunk selected: apply */
                strbuf_reset(&s->buf);
-               reassemble_patch(s, &s->buf);
+               reassemble_patch(s, file_diff, 0, &s->buf);
 
-               discard_index(s->r->index);
+               discard_index(s->s.r->index);
                setup_child_process(s, &cp, "apply", "--cached", NULL);
                if (pipe_command(&cp, s->buf.buf, s->buf.len,
                                 NULL, 0, NULL, 0))
                        error(_("'git apply --cached' failed"));
-               if (!repo_read_index(s->r))
-                       repo_refresh_and_write_index(s->r, REFRESH_QUIET, 0,
+               if (!repo_read_index(s->s.r))
+                       repo_refresh_and_write_index(s->s.r, REFRESH_QUIET, 0,
                                                     1, NULL, NULL, NULL);
        }
 
        putchar('\n');
-       return 0;
+       return quit;
 }
 
 int run_add_p(struct repository *r, const struct pathspec *ps)
 {
-       struct add_p_state s = { r, STRBUF_INIT, STRBUF_INIT, STRBUF_INIT };
+       struct add_p_state s = {
+               { r }, STRBUF_INIT, STRBUF_INIT, STRBUF_INIT, STRBUF_INIT
+       };
+       size_t i, binary_count = 0;
+
+       init_add_i_state(&s.s, r);
 
        if (discard_index(r->index) < 0 || repo_read_index(r) < 0 ||
            repo_refresh_and_write_index(r, REFRESH_QUIET, 0, 1,
@@ -299,8 +1319,16 @@ int run_add_p(struct repository *r, const struct pathspec *ps)
                return -1;
        }
 
-       if (s.hunk_nr)
-               patch_update_file(&s);
+       for (i = 0; i < s.file_diff_nr; i++)
+               if (s.file_diff[i].binary && !s.file_diff[i].hunk_nr)
+                       binary_count++;
+               else if (patch_update_file(&s, s.file_diff + i))
+                       break;
+
+       if (s.file_diff_nr == 0)
+               fprintf(stderr, _("No changes.\n"));
+       else if (binary_count == s.file_diff_nr)
+               fprintf(stderr, _("Only binary files changed.\n"));
 
        strbuf_release(&s.answer);
        strbuf_release(&s.buf);