]> git.ipfire.org Git - thirdparty/git.git/blobdiff - add-patch.c
t7063: more thorough status checking
[thirdparty/git.git] / add-patch.c
index 2d34ddd7f41b16b60e193607e72a117ceedbc20e..2c46fe5b3332bf844007ea0d17dee250f0ba5756 100644 (file)
@@ -12,9 +12,9 @@ enum prompt_mode_type {
 };
 
 static const char *prompt_mode[] = {
-       N_("Stage mode change [y,n,a,d%s,?]? "),
-       N_("Stage deletion [y,n,a,d%s,?]? "),
-       N_("Stage this hunk [y,n,a,d%s,?]? ")
+       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 {
@@ -29,6 +29,7 @@ struct hunk_header {
 
 struct hunk {
        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;
 };
@@ -43,7 +44,7 @@ struct add_p_state {
                struct hunk head;
                struct hunk *hunk;
                size_t hunk_nr, hunk_alloc;
-               unsigned deleted:1, mode_change:1;
+               unsigned deleted:1, mode_change:1,binary:1;
        } *file_diff;
        size_t file_diff_nr;
 };
@@ -293,7 +294,9 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps)
                                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",
@@ -433,22 +436,167 @@ static void render_diff_header(struct add_p_state *s,
        }
 }
 
+/* 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, struct strbuf *out)
+                            struct file_diff *file_diff, int use_all,
+                            struct strbuf *out)
 {
        struct hunk *hunk;
-       size_t i;
+       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 (hunk->use != USE_HUNK)
+               if (!use_all && hunk->use != USE_HUNK)
                        delta += hunk->header.old_count
                                - hunk->header.new_count;
-               else
+               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;
+               }
        }
 }
 
@@ -611,16 +759,268 @@ next_hunk_line:
        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;
+
+       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;
+
+       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,
@@ -631,7 +1031,7 @@ static int patch_update_file(struct add_p_state *s,
        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 (!file_diff->hunk_nr)
@@ -677,8 +1077,13 @@ static int patch_update_file(struct add_p_state *s,
                        strbuf_addstr(&s->buf, ",j");
                if (hunk_index + 1 < file_diff->hunk_nr)
                        strbuf_addstr(&s->buf, ",J");
+               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;
@@ -715,12 +1120,16 @@ soft_increment:
                                if (hunk->use == UNDECIDED_HUNK)
                                        hunk->use = USE_HUNK;
                        }
-               } else if (ch == 'd') {
+               } 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;
                        }
+                       if (ch == 'q') {
+                               quit = 1;
+                               break;
+                       }
                } else if (s->answer.buf[0] == 'K') {
                        if (hunk_index)
                                hunk_index--;
@@ -741,6 +1150,90 @@ soft_increment:
                                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)
@@ -750,9 +1243,38 @@ soft_increment:
                                color_fprintf_ln(stdout, s->s.header_color,
                                                 _("Split into %d hunks."),
                                                 (int)splittable_into);
-               } else
-                       color_fprintf(stdout, s->s.help_color,
+               } 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? */
@@ -763,7 +1285,7 @@ soft_increment:
        if (i < file_diff->hunk_nr) {
                /* At least one hunk selected: apply */
                strbuf_reset(&s->buf);
-               reassemble_patch(s, file_diff, &s->buf);
+               reassemble_patch(s, file_diff, 0, &s->buf);
 
                discard_index(s->s.r->index);
                setup_child_process(s, &cp, "apply", "--cached", NULL);
@@ -776,7 +1298,7 @@ soft_increment:
        }
 
        putchar('\n');
-       return 0;
+       return quit;
 }
 
 int run_add_p(struct repository *r, const struct pathspec *ps)
@@ -784,7 +1306,7 @@ int run_add_p(struct repository *r, const struct pathspec *ps)
        struct add_p_state s = {
                { r }, STRBUF_INIT, STRBUF_INIT, STRBUF_INIT, STRBUF_INIT
        };
-       size_t i;
+       size_t i, binary_count = 0;
 
        init_add_i_state(&s.s, r);
 
@@ -798,9 +1320,16 @@ int run_add_p(struct repository *r, const struct pathspec *ps)
        }
 
        for (i = 0; i < s.file_diff_nr; i++)
-               if (patch_update_file(&s, s.file_diff + 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);
        strbuf_release(&s.plain);