//
// 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"));
}