/* * The side by side rendering of a diff, the layout a reader gets instead of * the unified listing. ui-diff.c drives it, handing over each line xdiff * produces and calling in around the header and the footer of every file. * Removed and added lines are held back until their run ends, so that the two * sides can be paired into a row apiece. When a run has as many removals as * additions the paired lines are compared character by character, and what * differs within them is marked. */ #include "cgit.h" #include "html.h" #include "ui-diff.h" #include "ui-shared.h" #include "ui-ssdiff.h" // The stylesheet sets no tab-size and tabs are expanded here rather than left // to the browser, so this has to be the width a browser would pick on its own. #define TAB_WIDTH 8 // One line held back until its run ends, owning the copy taken of it. struct deferred_line { int line_no; char *line; struct deferred_line *next; }; static int current_old_line, current_new_line; static int **lcs_table; static struct deferred_line *deferred_old, *deferred_old_last; static struct deferred_line *deferred_new, *deferred_new_last; /* * The table is reused by every comparison and nothing clears it in between, * because the fill in longest_common_subsequence works back from the far * corner and writes every cell it goes on to read. */ static void create_lcs_table(void) { int i; if (lcs_table) return; lcs_table = xcalloc(MAX_SSDIFF_M, sizeof(int *)); lcs_table[0] = xcalloc(MAX_SSDIFF_SIZE, sizeof(int)); for (i = 1; i < MAX_SSDIFF_M; i++) lcs_table[i] = lcs_table[0] + i * MAX_SSDIFF_N; } /* * The characters the two lines have in common, in order, which the caller * owns. A line too long for the table gets NULL back and is shown whole * instead. */ static char *longest_common_subsequence(const char *old_line, const char *new_line) { int old_len = strlen(old_line); int new_len = strlen(new_line); int i, j, pos, lcs_len; char *lcs; if (old_len >= MAX_SSDIFF_M || new_len >= MAX_SSDIFF_N) return NULL; create_lcs_table(); for (i = old_len; i >= 0; i--) { for (j = new_len; j >= 0; j--) { if (old_line[i] == '\0' || new_line[j] == '\0') { lcs_table[i][j] = 0; } else if (old_line[i] == new_line[j]) { lcs_table[i][j] = 1 + lcs_table[i + 1][j + 1]; } else { int drop_old = lcs_table[i + 1][j]; int drop_new = lcs_table[i][j + 1]; lcs_table[i][j] = (drop_old > drop_new ? drop_old : drop_new); } } } lcs_len = lcs_table[0][0]; lcs = xmalloc(lcs_len + 2); memset(lcs, 0, sizeof(*lcs) * (lcs_len + 2)); pos = 0; i = 0; j = 0; while (i < old_len && j < new_len) { if (old_line[i] == new_line[j]) { lcs[pos] = old_line[i]; pos += 1; i += 1; j += 1; } else if (lcs_table[i + 1][j] >= lcs_table[i][j + 1]) { i += 1; } else { j += 1; } } return lcs; } /* * The line with its tabs expanded, which the caller owns. Built in one * forward pass so a tab heavy line stays linear in its own length. */ static char *expand_tabs(const char *line) { struct strbuf out = STRBUF_INIT; const char *p; for (p = line; *p; p++) { if (*p == '\t') strbuf_addchars(&out, ' ', TAB_WIDTH - (out.len % TAB_WIDTH)); else strbuf_addch(&out, *p); } return strbuf_detach(&out, NULL); } static void flush_run(struct strbuf *run) { if (!run->len) return; html_txt(run->buf); strbuf_reset(run); } /* * A stretch the other side does not share is escaped in one call, so a * changed line does not go through the output path a byte at a time. */ static void print_line_with_lcs(const char *class, const char *line, const char *lcs) { int len = strlen(line); int in_common = 1; int matched = 0; int i; struct strbuf run = STRBUF_INIT; for (i = 0; i < len; i++) { if (in_common) { if (line[i] == lcs[matched]) matched += 1; else { in_common = 0; flush_run(&run); htmlf("", class); } } else if (line[i] == lcs[matched]) { in_common = 1; flush_run(&run); html(""); matched += 1; } strbuf_addch(&run, line[i]); } flush_run(&run); if (!in_common) html(""); strbuf_release(&run); } static void print_lineno_cell(struct diff_filespec *file, const struct object_id *rev, int line_no) { struct strbuf path = STRBUF_INIT; char *anchor, *query, *fileurl; const char *rev_hex; anchor = cgit_fmt("n%d", line_no); rev_hex = is_null_oid(&file->oid) ? "HEAD" : oid_to_hex(rev); query = cgit_fmt("id=%s#%s", rev_hex, anchor); // The path is repository content, so percent-encode it so that the url // stays a url whatever bytes the path holds. if (file->path) strbuf_add_percentencode(&path, file->path, 0); fileurl = cgit_fileurl(ctx.repo->url, "tree", path.buf, query); html("%s", anchor + 1); html(""); free(fileurl); strbuf_release(&path); } static void print_row(const char *class, int old_line_no, char *old_line, int new_line_no, char *new_line, int highlight_chars) { char *lcs = NULL; // The first byte of a line is the marker xdiff put on it, not text. if (old_line) old_line = expand_tabs(old_line + 1); if (new_line) new_line = expand_tabs(new_line + 1); if (highlight_chars && old_line && new_line) lcs = longest_common_subsequence(old_line, new_line); html("\n"); if (old_line_no > 0) { print_lineno_cell(cgit_get_current_old_file(), old_rev_oid, old_line_no); htmlf("", class); } else if (old_line) htmlf("", class); else htmlf("", class); if (old_line) { if (lcs) print_line_with_lcs("del", old_line, lcs); else html_txt(old_line); } html("\n"); if (new_line_no > 0) { print_lineno_cell(cgit_get_current_new_file(), new_rev_oid, new_line_no); htmlf("", class); } else if (new_line) htmlf("", class); else htmlf("", class); if (new_line) { if (lcs) print_line_with_lcs("add", new_line, lcs); else html_txt(new_line); } html("\n"); free(lcs); free(new_line); free(old_line); } static void defer_line(struct deferred_line **head, struct deferred_line **last, const char *line, int line_no) { struct deferred_line *item = xmalloc(sizeof(*item)); item->line = xstrdup(line); item->line_no = line_no; item->next = NULL; if (*head) (*last)->next = item; else *head = item; *last = item; } /* * print_row frees only what it derives from the line it is handed and never * keeps the pointer itself, so the item can go as soon as its row is written. */ static void free_deferred(struct deferred_line *item) { free(item->line); free(item); } static int count_deferred(struct deferred_line *item) { int count = 0; while (item) { count += 1; item = item->next; } return count; } static void print_deferred_old_lines(void) { struct deferred_line *item = deferred_old; struct deferred_line *next; while (item) { print_row("del", item->line_no, item->line, -1, NULL, 0); next = item->next; free_deferred(item); item = next; } } static void print_deferred_new_lines(void) { struct deferred_line *item = deferred_new; struct deferred_line *next; while (item) { print_row("add", -1, NULL, item->line_no, item->line, 0); next = item->next; free_deferred(item); item = next; } } /* * Pairing a removal with an addition only stands for anything when the two * runs are the same length, which is why the character marking is offered only * then. */ static void print_deferred_changed_lines(void) { struct deferred_line *old_item = deferred_old; struct deferred_line *new_item = deferred_new; struct deferred_line *next; int highlight_chars; highlight_chars = count_deferred(old_item) == count_deferred(new_item); while (old_item || new_item) { if (old_item && new_item) print_row("changed", old_item->line_no, old_item->line, new_item->line_no, new_item->line, highlight_chars); else if (old_item) print_row("changed", old_item->line_no, old_item->line, -1, NULL, 0); else if (new_item) print_row("changed", -1, NULL, new_item->line_no, new_item->line, 0); if (old_item) { next = old_item->next; free_deferred(old_item); old_item = next; } if (new_item) { next = new_item->next; free_deferred(new_item); new_item = next; } } } /* * git leaves the length out when a hunk covers a single line, as in * "@@ -1 +1 @@", so the number runs to whatever follows it rather than to a * comma that may belong to the other side or be missing altogether. */ static int hunk_start_line(const char *hunk, char marker) { const char *p; long line_no; p = strchr(hunk, marker); if (p == NULL) return 0; p += 1; line_no = strtol(p, NULL, 10); if (line_no < 0 || line_no > INT_MAX) return 0; return (int)line_no; } static void print_deferred_lines(void) { if (!deferred_old && !deferred_new) return; if (deferred_old && !deferred_new) print_deferred_old_lines(); else if (!deferred_old && deferred_new) print_deferred_new_lines(); else print_deferred_changed_lines(); deferred_old = deferred_old_last = NULL; deferred_new = deferred_new_last = NULL; } /* * The length counts the byte that ends the line, and the buffer belongs to the * caller, so that byte is only swapped for a NUL while the row is written and * is put back before returning. */ void cgit_ssdiff_line_cb(char *line, int len) { char terminator = line[len - 1]; line[len - 1] = '\0'; if (line[0] == '@') { current_old_line = hunk_start_line(line, '-'); current_new_line = hunk_start_line(line, '+'); } if (line[0] == ' ') { print_deferred_lines(); print_row("ctx", current_old_line, line, current_new_line, line, 0); current_old_line += 1; current_new_line += 1; } else if (line[0] == '+') { defer_line(&deferred_new, &deferred_new_last, line, current_new_line); current_new_line += 1; } else if (line[0] == '-') { defer_line(&deferred_old, &deferred_old_last, line, current_old_line); current_old_line += 1; } else if (line[0] == '@') { html(""); html_txt(line); html("\n"); } else { html(""); html_txt(line); html("\n"); } line[len - 1] = terminator; } void cgit_ssdiff_header_begin(void) { current_old_line = -1; current_new_line = -1; html("\n"); html(""); } void cgit_ssdiff_header_end(void) { html("\n"); } void cgit_ssdiff_footer(void) { print_deferred_lines(); html("\n"); }