// // aegis - project change supervisor // Copyright (C) 2002-2008 Peter Miller // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation; either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see // . // #include #include // for assert #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include struct column_t { ~column_t() { } column_t() : formula(0), heading(0), width(0), stp(0), maximum(0), previous(0), newval(0) { } string_ty *formula; string_ty *heading; int width; output::pointer fp; symtab_ty *stp; long maximum; string_ty *previous; string_ty *newval; }; struct column_list_t { ~column_list_t() { delete [] item; } column_list_t() : length(0), maximum(0), item(0) { } size_t length; size_t maximum; column_t *item; }; static column_list_t columns; static symtab_ty *file_stp; static int filestat = -1; static const char *diff_option; static void column_list_append(column_list_t *clp, string_ty *formula, string_ty *heading, int width) { if (clp->length >= clp->maximum) { clp->maximum = clp->maximum * 2 + 4; column_t *new_item = new column_t [clp->maximum]; for (size_t j = 0; j < clp->length; ++j) new_item[j] = clp->item[j]; delete [] clp->item; clp->item = new_item; } column_t *cp = clp->item + clp->length++; cp->formula = formula; cp->heading = heading; cp->width = width; cp->fp.reset(); cp->stp = new symtab_ty(5); cp->maximum = 1; cp->previous = 0; cp->newval = 0; } static string_ty * change_number_get(change::pointer cp) { return str_format("%ld", magic_zero_decode(cp->number)); } static void process(change_identifier &cid, string_ty *filename, line_list_t *buffer) { size_t j; file_event_list::pointer felp; string_ty *prev_ifn; int prev_ifn_unlink; size_t linum; string_ty *output_file_name; patch_list_ty *plp; patch_ty *pap; // // We can't cope with change history that isn't in history yet. // change::pointer cp = cid.get_cp(); project_ty *pp = cid.get_pp(); if (!change_was_a_branch(cp) && !cp->is_completed()) { sub_context_ty sc; sc.var_set_string("Number", change_number_get(cp)); project_fatal(pp, &sc, i18n("change $number not completed")); // NOTREACHED } // // Get the time to extract the files at. // time_t when = change_completion_timestamp(cp); // // Reconstruct the file history. // project_file_roll_forward historian(pp, when, 1); felp = historian.get(filename); if (!felp) { // FIXME: add fuzzy file name matching // project_fatal(pp, &sc, i18n("no $filename, closest is $guess")); sub_context_ty sc; sc.var_set_string("File_Name", filename); project_fatal(pp, &sc, i18n("no $filename")); // NOTREACHED } // // We need a temporary file to park patches in. // output_file_name = os_edit_filename(0); os_become_orig(); undo_unlink_errok(output_file_name); os_become_undo(); // // Process each event in the file's history. // line_list_constructor(buffer); prev_ifn = 0; prev_ifn_unlink = 0; for (j = 0; j < felp->size(); ++j) { trace(("j = %d of %d\n", (int)j, (int)felp->size())); file_event *fep; string_ty *ifn; int ifn_unlink; input ifp; size_t m; // // find the file within the change // fep = felp->get(j); assert(fep->get_src()); // // What we do next depends on what the change did to the file. // ifn = 0; ifn_unlink = 0; switch (fep->get_src()->action) { case file_action_create: // // read whole file into buffer // trace(("create %s\n", change_version_get(fep->get_change())->str_text)); treat_as_create: line_list_clear(buffer); ifn = project_file_version_path(pp, fep->get_src(), &ifn_unlink); os_become_orig(); ifp = input_file_text_open(ifn); for (linum = 0;; ++linum) { nstring s; if (!ifp->one_line(s)) break; line_list_insert(buffer, linum, fep->get_change(), s.get_ref()); assert(buffer->item[linum].cp == fep->get_change()); assert(str_equal(buffer->item[linum].text, s.get_ref())); } ifp.close(); os_become_undo(); break; case file_action_insulate: case file_action_transparent: assert(0); // fall through... case file_action_modify: trace(("modify %s\n", change_version_get(fep->get_change())->str_text)); if (!prev_ifn) { trace(("treating this modify as a create\n")); goto treat_as_create; } // // generate the difference between the last edit and this edit. // ifn = project_file_version_path(pp, fep->get_src(), &ifn_unlink); trace(("prev_ifn = \"%s\"\n", prev_ifn ? prev_ifn->str_text : "NULL")); trace(("ifn = \"%s\"\n", ifn->str_text)); trace(("output_file_name = \"%s\"\n", output_file_name->str_text)); trace(("filename = \"%s\"\n", filename->str_text)); trace(("diff_option = \"%s\"\n", diff_option)); change_run_annotate_diff_command ( cid.get_cp(), user_ty::create(), prev_ifn, ifn, output_file_name, filename, (diff_option ? diff_option : "") ); // // read the diff in as a patch // os_become_orig(); ifp = input_file_text_open(output_file_name); plp = patch_read(ifp, 0); ifp.close(); os_become_undo(); assert(plp); // // There should be either 0 or 1 files in the patch list we // just read. // assert(plp->length < 2); if (plp->length == 0) { patch_list_delete(plp); break; } pap = plp->item[0]; // // Work through the hunks, applying them one at a time. // // By working backwards, the "before" line numbers are always // valid, even as the buffer grows and strinks as we apply // the patch hunks. // for (m = 0; m < pap->actions.length; ++m) { patch_hunk_ty *php; size_t first_line; size_t k; php = pap->actions.item[pap->actions.length - 1 - m]; first_line = php->before.start_line_number; if (first_line > 0) --first_line; line_list_delete ( buffer, first_line, php->before.length ); for (k = 0; k < php->after.length; ++k) { patch_line_ty *plip; plip = php->after.item + k; assert(plip->type != patch_line_type_deleted); if (plip->type != patch_line_type_deleted) { line_list_insert ( buffer, first_line++, fep->get_change(), plip->value ); } } } patch_list_delete(plp); #ifdef DEBUG // // Check that our reconstruction matches the file contents. // // Note that this assumes they didn't use any of // the more interesting white space options in the // annotate_diff_command. // os_become_orig(); ifp = input_file_text_open(ifn); linum = 1; for (m = 0 ; m < buffer->length1; ++m, ++linum) { line_t *lp = buffer->item + buffer->start1 + m; nstring s; if (!ifp->one_line(s)) { trace(("line %d: file too short\n", linum)); assert(0); break; } if (nstring(lp->text) != s) { trace(("line %d: lp->text %08lX != s %08lX\n", linum, (long)lp->text, (long)s.c_str())); assert(0); } } for (m = 0 ; m < buffer->length2; ++m, ++linum) { line_t *lp = buffer->item + buffer->start2 + m; nstring s; if (!ifp->one_line(s)) { trace(("line %d: file too short\n", linum)); assert(0); break; } if (nstring(lp->text) != s) { trace(("line %d: lp->text %08lX != s %08lX\n", linum, (long)lp->text, (long)s)); assert(0); } } ifp.close(); os_become_undo(); #endif break; case file_action_remove: trace(("remove\n")); line_list_clear(buffer); break; } if (prev_ifn) { if (prev_ifn_unlink) { os_become_orig(); os_unlink(prev_ifn); os_become_undo(); } str_free(prev_ifn); } prev_ifn = ifn; prev_ifn_unlink = ifn_unlink; trace(("buf line = %ld\n", (long)(buffer->length1 + buffer->length2))); } } static void incr(symtab_ty *stp, string_ty *key, long *maximum_p) { long *data; data = (long int *)stp->query(key); if (!data) { static size_t templen; static long *temp; if (templen == 0) { templen = 100; temp = new long [templen]; } data = temp++; --templen; *data = 0; stp->assign(key, data); } ++*data; if (maximum_p && *data > *maximum_p) *maximum_p = *data; } static void emit_range(output::pointer line_col, output::pointer source_col, line_t *line_array, size_t line_len, long *linum_p, col::pointer ofp) { size_t j; for (j = 0; j < line_len; ++j, ++*linum_p) { size_t k; line_t *lp; int changed; lp = line_array + j; changed = 0; for (k = 0; k < columns.length; ++k) { column_t *cp; cp = columns.item + k; cp->newval = substitute(0, lp->cp, cp->formula); incr(cp->stp, cp->newval, &cp->maximum); if (!cp->previous || !str_equal(cp->previous, cp->newval)) ++changed; } for (k = 0; k < columns.length; ++k) { column_t *cp; cp = columns.item + k; if (changed) { // // The choices are to print only the columns changed, or // all the columns when one column changes. CVS annotate // prints all of the columns. // cp->fp->fputs(cp->newval); } if (cp->previous) str_free(cp->previous); cp->previous = cp->newval; cp->newval = 0; } line_col->fprintf("%5ld", *linum_p); source_col->fputs(lp->text); ofp->eoln(); // // Collect the file statistics. // for (k = 0; ; ++k) { fstate_src_ty *src; src = change_file_nth(lp->cp, k, view_path_first); if (!src) break; incr(file_stp, src->file_name, 0); } } } static void emit(line_list_t *buffer, string_ty *outfilename, string_ty *filename, project_ty *pp) { col::pointer ofp; output::pointer line_col; output::pointer source_col; size_t j; long linum; int left; trace(("buf line = %ld\n", (long)(buffer->length1 + buffer->length2))); ofp = col::open(outfilename); ofp->title("Annotated File Listing", filename->str_text); // // Create the columns. // left = 0; for (j = 0; j < columns.length; ++j) { column_t *cp; cp = columns.item + j; cp->fp = ofp->create(left, left + cp->width, cp->heading->str_text); left += cp->width + 1; } line_col = ofp->create(left, left + 6, "Line\n------"); source_col = ofp->create(left + 7, 0, "Source\n---------"); file_stp = new symtab_ty(5); // // Emit the lines. // linum = 1; trace(("buffer->length1 = %ld\n", (long)buffer->length1)); emit_range ( line_col, source_col, buffer->item + buffer->start1, buffer->length1, &linum, ofp ); trace(("buffer->length2 = %ld\n", (long)buffer->length2)); emit_range ( line_col, source_col, buffer->item + buffer->start2, buffer->length2, &linum, ofp ); if (linum > 1) { ofp->eject(); ofp->title("Statistics", filename->str_text); --linum; } for (j = 0; j < columns.length; ++j) { column_t *cp; size_t k; cp = columns.item + j; string_list_ty keys; cp->stp->keys(&keys); ofp->need(keys.nstrings > 10 ? 10 : (int)keys.nstrings); keys.sort_version(); for (k = 0; k < keys.nstrings; ++k) { string_ty *key; long *data; key = keys.string[k]; data = (long int *)cp->stp->query(key); assert(key); if (!data) continue; cp->fp->fputs(key); line_col->fprintf("%5ld", *data); source_col->fprintf("%6.2f%%", 100. * *data / linum); // // Histogram in trhe rest of the line. // left = source_col->page_width() - 8; if (left > 0) { if (left > 50) left = 50; left = (left * *data + cp->maximum / 2) / cp->maximum; if (left > 0) { source_col->fputc(' '); for (;;) { source_col->fputc('*'); --left; if (left <= 0) break; } } } ofp->eoln(); } } if (filestat > 0) { // // Emit the file statistics. // string_list_ty keys; file_stp->keys(&keys); keys.sort(); ofp->need(keys.nstrings > 10 ? 10 : (int)keys.nstrings); for (j = 0; j < keys.nstrings; ++j) { string_ty *key; long *data; fstate_src_ty *src; key = keys.string[j]; if (str_equal(key, filename)) continue; data = (long int *)file_stp->query(key); assert(key); if (!data) continue; src = project_file_find(pp, key, view_path_extreme); if (!src) continue; line_col->fprintf("%5ld", *data); source_col->fputs(key); ofp->eoln(); } } } void annotate(void) { string_ty *filename; string_ty *outfile; line_list_t buffer; trace(("annotate()\n{\n")); filename = 0; outfile = 0; change_identifier cid; arglex(); while (arglex_token != arglex_token_eoln) { switch (arglex_token) { default: generic_argument(usage); continue; case arglex_token_branch: case arglex_token_change: case arglex_token_delta: case arglex_token_delta_date: case arglex_token_delta_from_change: case arglex_token_grandparent: case arglex_token_number: case arglex_token_project: case arglex_token_trunk: cid.command_line_parse(usage); continue; case arglex_token_string: if (filename) fatal_intl(0, i18n("too many files")); filename = str_from_c(arglex_value.alv_string); break; case arglex_token_output: if (outfile) duplicate_option(usage); if (arglex() != arglex_token_string) option_needs_file(arglex_token_output, usage); outfile = str_from_c(arglex_value.alv_string); break; case arglex_token_column: { string_ty *formula; string_ty *heading; int width; string_ty *s; if (arglex() != arglex_token_string) option_needs_string(arglex_token_column, usage); formula = str_from_c(arglex_value.alv_string); if (arglex() == arglex_token_string) { heading = str_from_c(arglex_value.alv_string); arglex(); } else heading = str_copy(formula); if (arglex_token == arglex_token_number) { width = arglex_value.alv_number; if (width < 1) width = 7; arglex(); } else width = 7; char *minus_ch = new char [width + 1]; memset(minus_ch, '-', width); minus_ch[width] = 0; s = str_format("%.*s\n%s", width, heading->str_text, minus_ch); delete [] minus_ch; str_free(heading); heading = s; column_list_append(&columns, formula, heading, width); } continue; case arglex_token_filestat: filestat = 1; break; case arglex_token_filestat_not: filestat = 0; break; case arglex_token_diff_option: if (diff_option) duplicate_option(usage); if (arglex_get_string() != arglex_token_string) option_needs_file(arglex_token_diff_option, usage); diff_option = arglex_value.alv_string; break; } arglex(); } if (!cid.set()) cid.set_baseline(); cid.command_line_check(usage); if (!filename) fatal_intl(0, i18n("no file names")); // // Insert the default columns if the user does not specify any. // if (columns.length == 0) { string_ty *formula; string_ty *heading; int width; formula = str_from_c("${ch date %Y-%m}"); heading = str_from_c("Date\n-------"); width = 7; column_list_append(&columns, formula, heading, width); formula = str_from_c("$version"); heading = str_from_c("Version\n---------"); width = 9; column_list_append(&columns, formula, heading, width); formula = str_from_c("${ch developer}"); heading = str_from_c("Who\n--------"); width = 8; column_list_append(&columns, formula, heading, width); } process(cid, filename, &buffer); trace(("buf lines = %ld\n", (long)(buffer.length1 + buffer.length2))); emit(&buffer, outfile, filename, cid.get_pp()); trace(("}\n")); }