1706 lines
66 KiB
C
1706 lines
66 KiB
C
/* git-merge-changelog - git "merge" driver for GNU style ChangeLog files.
|
|
Copyright (C) 2008-2024 Bruno Haible <bruno@clisp.org>
|
|
|
|
This file 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 file 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 <https://www.gnu.org/licenses/>. */
|
|
|
|
/* README:
|
|
The default merge driver of 'git' *always* produces conflicts when
|
|
pulling public modifications into a privately modified ChangeLog file.
|
|
This is because ChangeLog files are always modified at the top; the
|
|
default merge driver has no clue how to deal with this. Furthermore
|
|
the conflicts are presented with more <<<< ==== >>>> markers than
|
|
necessary; this is because the default merge driver makes pointless
|
|
efforts to look at the individual line changes inside a ChangeLog entry.
|
|
|
|
This program serves as a 'git' merge driver that avoids these problems.
|
|
1. It produces no conflict when ChangeLog entries have been inserted
|
|
at the top both in the public and in the private modification. It
|
|
puts the privately added entries above the publicly added entries.
|
|
2. It respects the structure of ChangeLog files: entries are not split
|
|
into lines but kept together.
|
|
3. It also handles the case of small modifications of past ChangeLog
|
|
entries, or of removed ChangeLog entries: they are merged as one
|
|
would expect it.
|
|
4. Conflicts are presented at the top of the file, rather than where
|
|
they occurred, so that the user will see them immediately. (Unlike
|
|
for source code written in some programming language, conflict markers
|
|
that are located several hundreds lines from the top will not cause
|
|
any syntax error and therefore would be likely to remain unnoticed.)
|
|
*/
|
|
|
|
/* Installation:
|
|
|
|
$ ./gnulib-tool --create-testdir --dir=/tmp/testdir123 git-merge-changelog
|
|
$ cd /tmp/testdir123
|
|
$ ./configure
|
|
$ make
|
|
$ make install
|
|
|
|
Additionally, for git users:
|
|
- Add to .git/config of the checkout (or to your $HOME/.gitconfig) the
|
|
lines
|
|
|
|
[merge "merge-changelog"]
|
|
name = GNU-style ChangeLog merge driver
|
|
driver = /usr/local/bin/git-merge-changelog %O %A %B
|
|
|
|
- Add to the top-level directory of the checkout a file '.gitattributes'
|
|
with this line:
|
|
|
|
ChangeLog merge=merge-changelog
|
|
|
|
(See "man 5 gitattributes" for more info.)
|
|
|
|
Additionally, for bzr users:
|
|
- Install the 'extmerge' bzr plug-in listed at
|
|
<http://doc.bazaar.canonical.com/plugins/en/index.html>
|
|
<http://wiki.bazaar.canonical.com/BzrPlugins>
|
|
- Add to your $HOME/.bazaar/bazaar.conf the line
|
|
|
|
external_merge = git-merge-changelog %b %T %o
|
|
|
|
- Then, to merge a conflict in a ChangeLog file, use
|
|
|
|
$ bzr extmerge ChangeLog
|
|
|
|
Additionally, for hg users:
|
|
- Add to your $HOME/.hgrc the lines
|
|
|
|
[merge-patterns]
|
|
ChangeLog = git-merge-changelog
|
|
|
|
[merge-tools]
|
|
git-merge-changelog.executable = /usr/local/bin/git-merge-changelog
|
|
git-merge-changelog.args = $base $local $other
|
|
|
|
See <https://www.selenic.com/mercurial/hgrc.5.html> section merge-tools
|
|
for reference.
|
|
*/
|
|
|
|
/* Use as an alternative to 'diff3':
|
|
git-merge-changelog performs the same role as "diff3 -m", just with
|
|
reordered arguments:
|
|
$ git-merge-changelog %O %A %B
|
|
is comparable to
|
|
$ diff3 -m %A %O %B
|
|
*/
|
|
|
|
/* Calling convention:
|
|
A merge driver is called with three filename arguments:
|
|
1. %O = The common ancestor of %A and %B.
|
|
2. %A = The file's contents from the "current branch".
|
|
3. %B = The file's contents from the "other branch"; this is the contents
|
|
being merged in.
|
|
|
|
In case of a "git stash apply" or of an upstream pull (e.g. from a subsystem
|
|
maintainer to a central maintainer) or of a downstream pull with --rebase:
|
|
2. %A = The file's newest pulled contents; modified by other committers.
|
|
3. %B = The user's newest copy of the file; modified by the user.
|
|
In case of a downstream pull (e.g. from a central repository to the user)
|
|
or of an upstream pull with --rebase:
|
|
2. %A = The user's newest copy of the file; modified by the user.
|
|
3. %B = The file's newest pulled contents; modified by other committers.
|
|
|
|
It should write its merged output into file %A. It can also echo some
|
|
remarks to stdout. It should exit with return code 0 if the merge could
|
|
be resolved cleanly, or with non-zero return code if there were conflicts.
|
|
*/
|
|
|
|
/* How it works:
|
|
The structure of a ChangeLog file: It consists of ChangeLog entries. A
|
|
ChangeLog entry starts at a line following a blank line and that starts with
|
|
a non-whitespace character, or at the beginning of a file.
|
|
The merge driver works as follows: It reads the three files into memory and
|
|
dissects them into ChangeLog entries. It then finds the differences between
|
|
%O and %B. They are classified as:
|
|
- removals (some consecutive entries removed),
|
|
- changes (some consecutive entries removed, some consecutive entries
|
|
added),
|
|
- additions (some consecutive entries added).
|
|
The driver then attempts to apply the changes to %A.
|
|
To this effect, it first computes a correspondence between the entries in %O
|
|
and the entries in %A, using fuzzy string matching to still identify changed
|
|
entries.
|
|
- Removals are applied one by one. If the entry is present in %A, at any
|
|
position, it is removed. If not, the removal is marked as a conflict.
|
|
- Additions at the top of %B are applied at the top of %A.
|
|
- Additions between entry x and entry y (y may be the file end) in %B are
|
|
applied between entry x and entry y in %A (if they still exist and are
|
|
still consecutive in %A), otherwise the additions are marked as a
|
|
conflict.
|
|
- Changes are categorized into "simple changes":
|
|
entry1 ... entryn
|
|
are mapped to
|
|
added_entry ... added_entry modified_entry1 ... modified_entryn,
|
|
where the correspondence between entry_i and modified_entry_i is still
|
|
clear; and "big changes": these are all the rest. Simple changes at the
|
|
top of %B are applied by putting the added entries at the top of %A. The
|
|
changes in simple changes are applied one by one; possibly leading to
|
|
single-entry conflicts. Big changes are applied en bloc, possibly
|
|
leading to conflicts spanning multiple entries.
|
|
- Conflicts are output at the top of the file and cause an exit status of
|
|
1.
|
|
*/
|
|
|
|
#include <config.h>
|
|
|
|
#include <getopt.h>
|
|
#include <limits.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/types.h>
|
|
#include <unistd.h>
|
|
|
|
#include <error.h>
|
|
#include "idx.h"
|
|
#include "read-file.h"
|
|
#include "gl_xlist.h"
|
|
#include "gl_array_list.h"
|
|
#include "gl_linkedhash_list.h"
|
|
#include "gl_rbtreehash_list.h"
|
|
#include "gl_linked_list.h"
|
|
#include "xalloc.h"
|
|
#include "xmalloca.h"
|
|
#include "fstrcmp.h"
|
|
#include "minmax.h"
|
|
#include "c-strstr.h"
|
|
#include "fwriteerror.h"
|
|
|
|
#define ASSERT(expr) \
|
|
do \
|
|
{ \
|
|
if (!(expr)) \
|
|
abort (); \
|
|
} \
|
|
while (0)
|
|
|
|
#define FSTRCMP_THRESHOLD 0.6
|
|
#define FSTRCMP_STRICTER_THRESHOLD 0.8
|
|
|
|
/* Representation of a ChangeLog entry.
|
|
The string may contain NUL bytes; therefore it is represented as a plain
|
|
opaque memory region. */
|
|
struct entry
|
|
{
|
|
char *string;
|
|
idx_t length;
|
|
/* Cache for the hash code. */
|
|
bool hashcode_cached;
|
|
size_t hashcode;
|
|
};
|
|
|
|
/* Create an entry.
|
|
The memory region passed by the caller must of indefinite extent. It is
|
|
*not* copied here. */
|
|
static struct entry *
|
|
entry_create (char *string, idx_t length)
|
|
{
|
|
struct entry *result = XMALLOC (struct entry);
|
|
result->string = string;
|
|
result->length = length;
|
|
result->hashcode_cached = false;
|
|
return result;
|
|
}
|
|
|
|
/* Compare two entries for equality. */
|
|
static bool
|
|
entry_equals (const void *elt1, const void *elt2)
|
|
{
|
|
const struct entry *entry1 = (const struct entry *) elt1;
|
|
const struct entry *entry2 = (const struct entry *) elt2;
|
|
return entry1->length == entry2->length
|
|
&& memcmp (entry1->string, entry2->string, entry1->length) == 0;
|
|
}
|
|
|
|
/* Return a hash code of the contents of a ChangeLog entry. */
|
|
static size_t
|
|
entry_hashcode (const void *elt)
|
|
{
|
|
struct entry *entry = (struct entry *) elt;
|
|
if (!entry->hashcode_cached)
|
|
{
|
|
/* See https://www.haible.de/bruno/hashfunc.html. */
|
|
const char *s;
|
|
idx_t n;
|
|
size_t h = 0;
|
|
|
|
for (s = entry->string, n = entry->length; n > 0; s++, n--)
|
|
h = (unsigned char) *s + ((h << 9) | (h >> (SIZE_WIDTH - 9)));
|
|
|
|
entry->hashcode = h;
|
|
entry->hashcode_cached = true;
|
|
}
|
|
return entry->hashcode;
|
|
}
|
|
|
|
/* Perform a fuzzy comparison of two ChangeLog entries.
|
|
Return a similarity measure of the two entries, a value between 0 and 1.
|
|
0 stands for very distinct, 1 for identical.
|
|
If the result is < LOWER_BOUND, an arbitrary other value < LOWER_BOUND can
|
|
be returned. */
|
|
static double
|
|
entry_fstrcmp (const struct entry *entry1, const struct entry *entry2,
|
|
double lower_bound)
|
|
{
|
|
/* fstrcmp works only on NUL terminated strings. */
|
|
char *memory;
|
|
double similarity;
|
|
|
|
if (memchr (entry1->string, '\0', entry1->length) != NULL)
|
|
return 0.0;
|
|
if (memchr (entry2->string, '\0', entry2->length) != NULL)
|
|
return 0.0;
|
|
memory = (char *) xmalloca (entry1->length + 1 + entry2->length + 1);
|
|
{
|
|
char *p = memory;
|
|
memcpy (p, entry1->string, entry1->length);
|
|
p += entry1->length;
|
|
*p++ = '\0';
|
|
memcpy (p, entry2->string, entry2->length);
|
|
p += entry2->length;
|
|
*p++ = '\0';
|
|
}
|
|
similarity =
|
|
fstrcmp_bounded (memory, memory + entry1->length + 1, lower_bound);
|
|
freea (memory);
|
|
return similarity;
|
|
}
|
|
|
|
/* This structure represents an entire ChangeLog file, after it was read
|
|
into memory. */
|
|
struct changelog_file
|
|
{
|
|
/* The entries, as a list. */
|
|
gl_list_t /* <struct entry *> */ entries_list;
|
|
/* The entries, as a list in opposite direction. */
|
|
gl_list_t /* <struct entry *> */ entries_reversed;
|
|
/* The entries, as an array. */
|
|
idx_t num_entries;
|
|
struct entry **entries;
|
|
};
|
|
|
|
/* Read a ChangeLog file into memory.
|
|
Return the contents in *RESULT. */
|
|
static void
|
|
read_changelog_file (const char *filename, struct changelog_file *result)
|
|
{
|
|
/* Read the file in text mode, otherwise it's hard to recognize empty
|
|
lines. */
|
|
size_t length;
|
|
char *contents = read_file (filename, 0, &length);
|
|
if (contents == NULL)
|
|
{
|
|
fprintf (stderr, "could not read file '%s'\n", filename);
|
|
exit (EXIT_FAILURE);
|
|
}
|
|
|
|
result->entries_list =
|
|
gl_list_create_empty (GL_LINKEDHASH_LIST, entry_equals, entry_hashcode,
|
|
NULL, true);
|
|
result->entries_reversed =
|
|
gl_list_create_empty (GL_RBTREEHASH_LIST, entry_equals, entry_hashcode,
|
|
NULL, true);
|
|
/* A ChangeLog file consists of ChangeLog entries. A ChangeLog entry starts
|
|
at a line following a blank line and that starts with a non-whitespace
|
|
character, or at the beginning of a file.
|
|
Split the file contents into entries. */
|
|
{
|
|
char *contents_end = contents + length;
|
|
char *start = contents;
|
|
while (start < contents_end)
|
|
{
|
|
/* Search the end of the current entry. */
|
|
char *ptr = start;
|
|
struct entry *curr;
|
|
|
|
while (ptr < contents_end)
|
|
{
|
|
ptr = memchr (ptr, '\n', contents_end - ptr);
|
|
if (ptr == NULL)
|
|
{
|
|
ptr = contents_end;
|
|
break;
|
|
}
|
|
ptr++;
|
|
if (contents_end - ptr >= 2
|
|
&& ptr[0] == '\n'
|
|
&& !(ptr[1] == '\n' || ptr[1] == '\t' || ptr[1] == ' '))
|
|
{
|
|
ptr++;
|
|
break;
|
|
}
|
|
}
|
|
|
|
curr = entry_create (start, ptr - start);
|
|
gl_list_add_last (result->entries_list, curr);
|
|
gl_list_add_first (result->entries_reversed, curr);
|
|
|
|
start = ptr;
|
|
}
|
|
}
|
|
|
|
result->num_entries = gl_list_size (result->entries_list);
|
|
result->entries = XNMALLOC (result->num_entries, struct entry *);
|
|
{
|
|
idx_t index = 0;
|
|
gl_list_iterator_t iter = gl_list_iterator (result->entries_list);
|
|
const void *elt;
|
|
gl_list_node_t node;
|
|
while (gl_list_iterator_next (&iter, &elt, &node))
|
|
result->entries[index++] = (struct entry *) elt;
|
|
gl_list_iterator_free (&iter);
|
|
ASSERT (index == result->num_entries);
|
|
}
|
|
}
|
|
|
|
/* A mapping (correspondence) between entries of FILE1 and of FILE2. */
|
|
struct entries_mapping
|
|
{
|
|
struct changelog_file *file1;
|
|
struct changelog_file *file2;
|
|
/* Mapping from indices in FILE1 to indices in FILE2.
|
|
A value -1 means that the entry from FILE1 is not found in FILE2.
|
|
A value -2 means that it has not yet been computed. */
|
|
ptrdiff_t *index_mapping;
|
|
/* Mapping from indices in FILE2 to indices in FILE1.
|
|
A value -1 means that the entry from FILE2 is not found in FILE1.
|
|
A value -2 means that it has not yet been computed. */
|
|
ptrdiff_t *index_mapping_reverse;
|
|
};
|
|
|
|
/* Look up (or lazily compute) the mapping of an entry in FILE1.
|
|
i is the index in FILE1.
|
|
Return the index in FILE2, or -1 when the entry is not found in FILE2. */
|
|
static ptrdiff_t
|
|
entries_mapping_get (struct entries_mapping *mapping, idx_t i)
|
|
{
|
|
if (mapping->index_mapping[i] < -1)
|
|
{
|
|
struct changelog_file *file1 = mapping->file1;
|
|
struct changelog_file *file2 = mapping->file2;
|
|
idx_t n1 = file1->num_entries;
|
|
idx_t n2 = file2->num_entries;
|
|
struct entry *entry_i = file1->entries[i];
|
|
idx_t j;
|
|
|
|
/* Search whether it approximately occurs in file2. */
|
|
ptrdiff_t best_j = -1;
|
|
double best_j_similarity = 0.0;
|
|
for (j = n2; j > 0; )
|
|
{
|
|
j--;
|
|
if (mapping->index_mapping_reverse[j] < 0)
|
|
{
|
|
double similarity =
|
|
entry_fstrcmp (entry_i, file2->entries[j], best_j_similarity);
|
|
if (similarity > best_j_similarity)
|
|
{
|
|
best_j = j;
|
|
best_j_similarity = similarity;
|
|
}
|
|
}
|
|
}
|
|
if (best_j_similarity >= FSTRCMP_THRESHOLD)
|
|
{
|
|
/* Found a similar entry in file2. */
|
|
struct entry *entry_j = file2->entries[best_j];
|
|
/* Search whether it approximately occurs in file1 at index i. */
|
|
ptrdiff_t best_i = -1;
|
|
double best_i_similarity = 0.0;
|
|
idx_t ii;
|
|
for (ii = n1; ii > 0; )
|
|
{
|
|
ii--;
|
|
if (mapping->index_mapping[ii] < 0)
|
|
{
|
|
double similarity =
|
|
entry_fstrcmp (file1->entries[ii], entry_j,
|
|
best_i_similarity);
|
|
if (similarity > best_i_similarity)
|
|
{
|
|
best_i = ii;
|
|
best_i_similarity = similarity;
|
|
}
|
|
}
|
|
}
|
|
if (best_i_similarity >= FSTRCMP_THRESHOLD && best_i == i)
|
|
{
|
|
mapping->index_mapping[i] = best_j;
|
|
mapping->index_mapping_reverse[best_j] = i;
|
|
}
|
|
}
|
|
if (mapping->index_mapping[i] < -1)
|
|
/* It does not approximately occur in FILE2.
|
|
Remember it, for next time. */
|
|
mapping->index_mapping[i] = -1;
|
|
}
|
|
return mapping->index_mapping[i];
|
|
}
|
|
|
|
/* Look up (or lazily compute) the mapping of an entry in FILE2.
|
|
j is the index in FILE2.
|
|
Return the index in FILE1, or -1 when the entry is not found in FILE1. */
|
|
static ptrdiff_t
|
|
entries_mapping_reverse_get (struct entries_mapping *mapping, idx_t j)
|
|
{
|
|
if (mapping->index_mapping_reverse[j] < -1)
|
|
{
|
|
struct changelog_file *file1 = mapping->file1;
|
|
struct changelog_file *file2 = mapping->file2;
|
|
idx_t n1 = file1->num_entries;
|
|
idx_t n2 = file2->num_entries;
|
|
struct entry *entry_j = file2->entries[j];
|
|
idx_t i;
|
|
|
|
/* Search whether it approximately occurs in file1. */
|
|
ptrdiff_t best_i = -1;
|
|
double best_i_similarity = 0.0;
|
|
for (i = n1; i > 0; )
|
|
{
|
|
i--;
|
|
if (mapping->index_mapping[i] < 0)
|
|
{
|
|
double similarity =
|
|
entry_fstrcmp (file1->entries[i], entry_j, best_i_similarity);
|
|
if (similarity > best_i_similarity)
|
|
{
|
|
best_i = i;
|
|
best_i_similarity = similarity;
|
|
}
|
|
}
|
|
}
|
|
if (best_i_similarity >= FSTRCMP_THRESHOLD)
|
|
{
|
|
/* Found a similar entry in file1. */
|
|
struct entry *entry_i = file1->entries[best_i];
|
|
/* Search whether it approximately occurs in file2 at index j. */
|
|
ptrdiff_t best_j = -1;
|
|
double best_j_similarity = 0.0;
|
|
idx_t jj;
|
|
for (jj = n2; jj > 0; )
|
|
{
|
|
jj--;
|
|
if (mapping->index_mapping_reverse[jj] < 0)
|
|
{
|
|
double similarity =
|
|
entry_fstrcmp (entry_i, file2->entries[jj],
|
|
best_j_similarity);
|
|
if (similarity > best_j_similarity)
|
|
{
|
|
best_j = jj;
|
|
best_j_similarity = similarity;
|
|
}
|
|
}
|
|
}
|
|
if (best_j_similarity >= FSTRCMP_THRESHOLD && best_j == j)
|
|
{
|
|
mapping->index_mapping_reverse[j] = best_i;
|
|
mapping->index_mapping[best_i] = j;
|
|
}
|
|
}
|
|
if (mapping->index_mapping_reverse[j] < -1)
|
|
/* It does not approximately occur in FILE1.
|
|
Remember it, for next time. */
|
|
mapping->index_mapping_reverse[j] = -1;
|
|
}
|
|
return mapping->index_mapping_reverse[j];
|
|
}
|
|
|
|
/* Compute a mapping (correspondence) between entries of FILE1 and of FILE2.
|
|
The correspondence also takes into account small modifications; i.e. the
|
|
indicated relation is not equality of entries but best-match similarity
|
|
of entries.
|
|
If FULL is true, the maximum of matching is done up-front. If it is false,
|
|
it is done in a lazy way through the functions entries_mapping_get and
|
|
entries_mapping_reverse_get.
|
|
Return the result in *RESULT. */
|
|
static void
|
|
compute_mapping (struct changelog_file *file1, struct changelog_file *file2,
|
|
bool full,
|
|
struct entries_mapping *result)
|
|
{
|
|
/* Mapping from indices in file1 to indices in file2. */
|
|
ptrdiff_t *index_mapping;
|
|
/* Mapping from indices in file2 to indices in file1. */
|
|
ptrdiff_t *index_mapping_reverse;
|
|
idx_t n1 = file1->num_entries;
|
|
idx_t n2 = file2->num_entries;
|
|
idx_t i, j;
|
|
|
|
index_mapping = XNMALLOC (n1, ptrdiff_t);
|
|
for (i = 0; i < n1; i++)
|
|
index_mapping[i] = -2;
|
|
|
|
index_mapping_reverse = XNMALLOC (n2, ptrdiff_t);
|
|
for (j = 0; j < n2; j++)
|
|
index_mapping_reverse[j] = -2;
|
|
|
|
for (i = n1; i > 0; )
|
|
{
|
|
i--;
|
|
/* Take an entry from file1. */
|
|
if (index_mapping[i] < -1)
|
|
{
|
|
struct entry *entry = file1->entries[i];
|
|
/* Search whether it occurs in file2. */
|
|
size_t jrev = gl_list_indexof (file2->entries_reversed, entry);
|
|
if (jrev != (size_t)(-1))
|
|
{
|
|
j = n2 - 1 - jrev;
|
|
/* Found an exact correspondence. */
|
|
/* If index_mapping_reverse[j] >= 0, we have already seen other
|
|
copies of this entry, and there were more occurrences of it in
|
|
file1 than in file2. In this case, do nothing. */
|
|
if (index_mapping_reverse[j] < 0)
|
|
{
|
|
index_mapping[i] = j;
|
|
index_mapping_reverse[j] = i;
|
|
/* Look for more occurrences of the same entry. Match them
|
|
as long as they pair up. Unpaired occurrences of the same
|
|
entry are left without mapping. */
|
|
{
|
|
idx_t curr_i = i;
|
|
idx_t curr_j = j;
|
|
|
|
for (;;)
|
|
{
|
|
size_t next_i =
|
|
gl_list_indexof_from (file1->entries_reversed,
|
|
n1 - curr_i, entry);
|
|
if (next_i == (size_t)(-1))
|
|
break;
|
|
size_t next_j =
|
|
gl_list_indexof_from (file2->entries_reversed,
|
|
n2 - curr_j, entry);
|
|
if (next_j == (size_t)(-1))
|
|
break;
|
|
curr_i = n1 - 1 - next_i;
|
|
curr_j = n2 - 1 - next_j;
|
|
ASSERT (index_mapping[curr_i] < 0);
|
|
ASSERT (index_mapping_reverse[curr_j] < 0);
|
|
index_mapping[curr_i] = curr_j;
|
|
index_mapping_reverse[curr_j] = curr_i;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
result->file1 = file1;
|
|
result->file2 = file2;
|
|
result->index_mapping = index_mapping;
|
|
result->index_mapping_reverse = index_mapping_reverse;
|
|
|
|
if (full)
|
|
for (i = n1; i > 0; )
|
|
{
|
|
i--;
|
|
entries_mapping_get (result, i);
|
|
}
|
|
}
|
|
|
|
/* An "edit" is a textual modification performed by the user, that needs to
|
|
be applied to the other file. */
|
|
enum edit_type
|
|
{
|
|
/* Some consecutive entries were added. */
|
|
ADDITION,
|
|
/* Some consecutive entries were removed; some other consecutive entries
|
|
were added at the same position. (Not necessarily the same number of
|
|
entries.) */
|
|
CHANGE,
|
|
/* Some consecutive entries were removed. */
|
|
REMOVAL
|
|
};
|
|
|
|
/* This structure represents an edit. */
|
|
struct edit
|
|
{
|
|
enum edit_type type;
|
|
/* Range of indices into the entries of FILE1. */
|
|
idx_t i1, i2; /* first, last index; only used for CHANGE, REMOVAL */
|
|
/* Range of indices into the entries of FILE2. */
|
|
idx_t j1, j2; /* first, last index; only used for ADDITION, CHANGE */
|
|
};
|
|
|
|
/* This structure represents the differences from one file, FILE1, to another
|
|
file, FILE2. */
|
|
struct differences
|
|
{
|
|
/* An array mapping FILE1 indices to FILE2 indices (or -1 when the entry
|
|
from FILE1 is not found in FILE2). */
|
|
ptrdiff_t *index_mapping;
|
|
/* An array mapping FILE2 indices to FILE1 indices (or -1 when the entry
|
|
from FILE2 is not found in FILE1). */
|
|
ptrdiff_t *index_mapping_reverse;
|
|
/* The edits that transform FILE1 into FILE2. */
|
|
idx_t num_edits;
|
|
struct edit **edits;
|
|
};
|
|
|
|
/* Import the difference detection algorithm from GNU diff. */
|
|
#define ELEMENT struct entry *
|
|
#define EQUAL entry_equals
|
|
#define OFFSET ptrdiff_t
|
|
#define OFFSET_MAX PTRDIFF_MAX
|
|
#define EXTRA_CONTEXT_FIELDS \
|
|
ptrdiff_t *index_mapping; \
|
|
ptrdiff_t *index_mapping_reverse;
|
|
#define NOTE_DELETE(ctxt, xoff) \
|
|
ctxt->index_mapping[xoff] = -1
|
|
#define NOTE_INSERT(ctxt, yoff) \
|
|
ctxt->index_mapping_reverse[yoff] = -1
|
|
#include "diffseq.h"
|
|
|
|
/* Compute the differences between the entries of FILE1 and the entries of
|
|
FILE2. */
|
|
static void
|
|
compute_differences (struct changelog_file *file1, struct changelog_file *file2,
|
|
struct differences *result)
|
|
{
|
|
/* Unlike compute_mapping, which mostly ignores the order of the entries and
|
|
therefore works well when some entries are permuted, here we use the order.
|
|
I think this is needed in order to distinguish changes from
|
|
additions+removals; I don't know how to say what is a "change" if the
|
|
files are considered as unordered sets of entries. */
|
|
struct context ctxt;
|
|
idx_t n1 = file1->num_entries;
|
|
idx_t n2 = file2->num_entries;
|
|
idx_t i;
|
|
idx_t j;
|
|
gl_list_t /* <struct edit *> */ edits;
|
|
|
|
ctxt.xvec = file1->entries;
|
|
ctxt.yvec = file2->entries;
|
|
ctxt.index_mapping = XNMALLOC (n1, ptrdiff_t);
|
|
for (i = 0; i < n1; i++)
|
|
ctxt.index_mapping[i] = 0;
|
|
ctxt.index_mapping_reverse = XNMALLOC (n2, ptrdiff_t);
|
|
for (j = 0; j < n2; j++)
|
|
ctxt.index_mapping_reverse[j] = 0;
|
|
ctxt.fdiag = XNMALLOC (2 * (n1 + n2 + 3), ptrdiff_t) + n2 + 1;
|
|
ctxt.bdiag = ctxt.fdiag + n1 + n2 + 3;
|
|
ctxt.too_expensive = n1 + n2;
|
|
|
|
/* Store in ctxt.index_mapping and ctxt.index_mapping_reverse a -1 for
|
|
each removed or added entry. */
|
|
compareseq (0, n1, 0, n2, 0, &ctxt);
|
|
|
|
/* Complete the index_mapping and index_mapping_reverse arrays. */
|
|
i = 0;
|
|
j = 0;
|
|
while (i < n1 || j < n2)
|
|
{
|
|
while (i < n1 && ctxt.index_mapping[i] < 0)
|
|
i++;
|
|
while (j < n2 && ctxt.index_mapping_reverse[j] < 0)
|
|
j++;
|
|
ASSERT ((i < n1) == (j < n2));
|
|
if (i == n1 && j == n2)
|
|
break;
|
|
ctxt.index_mapping[i] = j;
|
|
ctxt.index_mapping_reverse[j] = i;
|
|
i++;
|
|
j++;
|
|
}
|
|
|
|
/* Create the edits. */
|
|
edits = gl_list_create_empty (GL_ARRAY_LIST, NULL, NULL, NULL, true);
|
|
i = 0;
|
|
j = 0;
|
|
while (i < n1 || j < n2)
|
|
{
|
|
if (i == n1)
|
|
{
|
|
struct edit *e;
|
|
ASSERT (j < n2);
|
|
e = XMALLOC (struct edit);
|
|
e->type = ADDITION;
|
|
e->j1 = j;
|
|
e->j2 = n2 - 1;
|
|
gl_list_add_last (edits, e);
|
|
break;
|
|
}
|
|
if (j == n2)
|
|
{
|
|
struct edit *e;
|
|
ASSERT (i < n1);
|
|
e = XMALLOC (struct edit);
|
|
e->type = REMOVAL;
|
|
e->i1 = i;
|
|
e->i2 = n1 - 1;
|
|
gl_list_add_last (edits, e);
|
|
break;
|
|
}
|
|
if (ctxt.index_mapping[i] >= 0)
|
|
{
|
|
if (ctxt.index_mapping_reverse[j] >= 0)
|
|
{
|
|
ASSERT (ctxt.index_mapping[i] == j);
|
|
ASSERT (ctxt.index_mapping_reverse[j] == i);
|
|
i++;
|
|
j++;
|
|
}
|
|
else
|
|
{
|
|
struct edit *e;
|
|
ASSERT (ctxt.index_mapping_reverse[j] < 0);
|
|
e = XMALLOC (struct edit);
|
|
e->type = ADDITION;
|
|
e->j1 = j;
|
|
do
|
|
j++;
|
|
while (j < n2 && ctxt.index_mapping_reverse[j] < 0);
|
|
e->j2 = j - 1;
|
|
gl_list_add_last (edits, e);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (ctxt.index_mapping_reverse[j] >= 0)
|
|
{
|
|
struct edit *e;
|
|
ASSERT (ctxt.index_mapping[i] < 0);
|
|
e = XMALLOC (struct edit);
|
|
e->type = REMOVAL;
|
|
e->i1 = i;
|
|
do
|
|
i++;
|
|
while (i < n1 && ctxt.index_mapping[i] < 0);
|
|
e->i2 = i - 1;
|
|
gl_list_add_last (edits, e);
|
|
}
|
|
else
|
|
{
|
|
struct edit *e;
|
|
ASSERT (ctxt.index_mapping[i] < 0);
|
|
ASSERT (ctxt.index_mapping_reverse[j] < 0);
|
|
e = XMALLOC (struct edit);
|
|
e->type = CHANGE;
|
|
e->i1 = i;
|
|
do
|
|
i++;
|
|
while (i < n1 && ctxt.index_mapping[i] < 0);
|
|
e->i2 = i - 1;
|
|
e->j1 = j;
|
|
do
|
|
j++;
|
|
while (j < n2 && ctxt.index_mapping_reverse[j] < 0);
|
|
e->j2 = j - 1;
|
|
gl_list_add_last (edits, e);
|
|
}
|
|
}
|
|
}
|
|
|
|
result->index_mapping = ctxt.index_mapping;
|
|
result->index_mapping_reverse = ctxt.index_mapping_reverse;
|
|
result->num_edits = gl_list_size (edits);
|
|
result->edits = XNMALLOC (result->num_edits, struct edit *);
|
|
{
|
|
idx_t index = 0;
|
|
gl_list_iterator_t iter = gl_list_iterator (edits);
|
|
const void *elt;
|
|
gl_list_node_t node;
|
|
while (gl_list_iterator_next (&iter, &elt, &node))
|
|
result->edits[index++] = (struct edit *) elt;
|
|
gl_list_iterator_free (&iter);
|
|
ASSERT (index == result->num_edits);
|
|
}
|
|
}
|
|
|
|
/* An empty entry. */
|
|
static struct entry empty_entry = { NULL, 0 };
|
|
|
|
/* Return the end a paragraph.
|
|
ENTRY is an entry.
|
|
OFFSET is an offset into the entry, OFFSET <= ENTRY->length.
|
|
Return the offset of the end of paragraph, as an offset <= ENTRY->length;
|
|
it is the start of a blank line or the end of the entry. */
|
|
static idx_t
|
|
find_paragraph_end (const struct entry *entry, idx_t offset)
|
|
{
|
|
const char *string = entry->string;
|
|
idx_t length = entry->length;
|
|
|
|
for (;;)
|
|
{
|
|
const char *nl = memchr (string + offset, '\n', length - offset);
|
|
if (nl == NULL)
|
|
return length;
|
|
offset = (nl - string) + 1;
|
|
if (offset < length && string[offset] == '\n')
|
|
return offset;
|
|
}
|
|
}
|
|
|
|
/* Split a merged entry.
|
|
Given an old entry of the form
|
|
TITLE
|
|
BODY
|
|
and a new entry of the form
|
|
TITLE
|
|
BODY1
|
|
BODY'
|
|
where the two titles are the same and BODY and BODY' are very similar,
|
|
this computes two new entries
|
|
TITLE
|
|
BODY1
|
|
and
|
|
TITLE
|
|
BODY'
|
|
and returns true.
|
|
If the entries don't have this form, it returns false. */
|
|
static bool
|
|
try_split_merged_entry (const struct entry *old_entry,
|
|
const struct entry *new_entry,
|
|
struct entry *new_split[2])
|
|
{
|
|
idx_t old_title_len = find_paragraph_end (old_entry, 0);
|
|
idx_t new_title_len = find_paragraph_end (new_entry, 0);
|
|
struct entry old_body;
|
|
struct entry new_body;
|
|
idx_t best_split_offset;
|
|
double best_similarity;
|
|
idx_t split_offset;
|
|
|
|
/* Same title? */
|
|
if (!(old_title_len == new_title_len
|
|
&& memcmp (old_entry->string, new_entry->string, old_title_len) == 0))
|
|
return false;
|
|
|
|
old_body.string = old_entry->string + old_title_len;
|
|
old_body.length = old_entry->length - old_title_len;
|
|
|
|
/* Determine where to split the new entry.
|
|
This is done by maximizing the similarity between BODY and BODY'. */
|
|
best_split_offset = split_offset = new_title_len;
|
|
best_similarity = 0.0;
|
|
for (;;)
|
|
{
|
|
double similarity;
|
|
|
|
new_body.string = new_entry->string + split_offset;
|
|
new_body.length = new_entry->length - split_offset;
|
|
similarity =
|
|
entry_fstrcmp (&old_body, &new_body, best_similarity);
|
|
if (similarity > best_similarity)
|
|
{
|
|
best_split_offset = split_offset;
|
|
best_similarity = similarity;
|
|
}
|
|
if (best_similarity == 1.0)
|
|
/* It cannot get better. */
|
|
break;
|
|
|
|
if (split_offset < new_entry->length)
|
|
split_offset = find_paragraph_end (new_entry, split_offset + 1);
|
|
else
|
|
break;
|
|
}
|
|
|
|
/* BODY' should not be empty. */
|
|
if (best_split_offset == new_entry->length)
|
|
return false;
|
|
ASSERT (new_entry->string[best_split_offset] == '\n');
|
|
|
|
/* A certain similarity between BODY and BODY' is required. */
|
|
if (best_similarity < FSTRCMP_STRICTER_THRESHOLD)
|
|
return false;
|
|
|
|
new_split[0] = entry_create (new_entry->string, best_split_offset + 1);
|
|
|
|
{
|
|
idx_t len1 = new_title_len;
|
|
idx_t len2 = new_entry->length - best_split_offset;
|
|
char *combined = XNMALLOC (len1 + len2, char);
|
|
memcpy (combined, new_entry->string, len1);
|
|
memcpy (combined + len1, new_entry->string + best_split_offset, len2);
|
|
new_split[1] = entry_create (combined, len1 + len2);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/* Write the contents of an entry to the output stream FP. */
|
|
static void
|
|
entry_write (FILE *fp, struct entry *entry)
|
|
{
|
|
if (entry->length > 0)
|
|
fwrite (entry->string, 1, entry->length, fp);
|
|
}
|
|
|
|
/* This structure represents a conflict.
|
|
A conflict can occur for various reasons. */
|
|
struct conflict
|
|
{
|
|
/* Parts from the ancestor file. */
|
|
idx_t num_old_entries;
|
|
struct entry **old_entries;
|
|
/* Parts of the modified file. */
|
|
idx_t num_modified_entries;
|
|
struct entry **modified_entries;
|
|
};
|
|
|
|
/* Write a conflict to the output stream FP, including markers. */
|
|
static void
|
|
conflict_write (FILE *fp, struct conflict *c)
|
|
{
|
|
idx_t i;
|
|
|
|
/* Use the same syntax as git's default merge driver.
|
|
The spaces after <<<<<<< and >>>>>>> are for compatibility with
|
|
git/rerere.c, function 'is_cmarker'. Usually they would be followed by
|
|
branch or version names, but this info is not available to us here.
|
|
Don't indent the contents of the entries (with things like ">" or "-"),
|
|
otherwise the user needs more textual editing to resolve the conflict. */
|
|
fputs ("<<<<<<< \n", fp);
|
|
for (i = 0; i < c->num_old_entries; i++)
|
|
entry_write (fp, c->old_entries[i]);
|
|
fputs ("=======\n", fp);
|
|
for (i = 0; i < c->num_modified_entries; i++)
|
|
entry_write (fp, c->modified_entries[i]);
|
|
fputs (">>>>>>> \n", fp);
|
|
}
|
|
|
|
/* Long options. */
|
|
static const struct option long_options[] =
|
|
{
|
|
{ "help", no_argument, NULL, 'h' },
|
|
{ "split-merged-entry", no_argument, NULL, CHAR_MAX + 1 },
|
|
{ "version", no_argument, NULL, 'V' },
|
|
{ NULL, 0, NULL, 0 }
|
|
};
|
|
|
|
/* Print a usage message and exit. */
|
|
static void
|
|
usage (int status)
|
|
{
|
|
if (status != EXIT_SUCCESS)
|
|
fprintf (stderr, "Try '%s --help' for more information.\n",
|
|
getprogname ());
|
|
else
|
|
{
|
|
printf ("Usage: %s [OPTION] O-FILE-NAME A-FILE-NAME B-FILE-NAME\n",
|
|
getprogname ());
|
|
printf ("\n");
|
|
printf ("Merges independent modifications of a ChangeLog style file.\n");
|
|
printf ("O-FILE-NAME names the original file, the ancestor of the two others.\n");
|
|
printf ("A-FILE-NAME names the publicly modified file.\n");
|
|
printf ("B-FILE-NAME names the user-modified file.\n");
|
|
printf ("Writes the merged file into A-FILE-NAME.\n");
|
|
printf ("\n");
|
|
#if 0 /* --split-merged-entry is now on by default. */
|
|
printf ("Operation modifiers:\n");
|
|
printf ("\
|
|
--split-merged-entry Possibly split a merged entry between paragraphs.\n\
|
|
Use this if you have the habit to merge unrelated\n\
|
|
entries into a single one, separated only by a\n\
|
|
newline, just because they happened on the same\n\
|
|
date.\n");
|
|
printf ("\n");
|
|
#endif
|
|
printf ("Informative output:\n");
|
|
printf (" -h, --help display this help and exit\n");
|
|
printf (" -V, --version output version information and exit\n");
|
|
printf ("\n");
|
|
fputs ("Report bugs to <bug-gnulib@gnu.org>.\n",
|
|
stdout);
|
|
}
|
|
|
|
exit (status);
|
|
}
|
|
|
|
int
|
|
main (int argc, char *argv[])
|
|
{
|
|
int optchar;
|
|
bool do_help;
|
|
bool do_version;
|
|
bool split_merged_entry;
|
|
|
|
/* Set default values for variables. */
|
|
do_help = false;
|
|
do_version = false;
|
|
split_merged_entry = true;
|
|
|
|
/* Parse command line options. */
|
|
while ((optchar = getopt_long (argc, argv, "hV", long_options, NULL)) != EOF)
|
|
switch (optchar)
|
|
{
|
|
case '\0': /* Long option. */
|
|
break;
|
|
case 'h':
|
|
do_help = true;
|
|
break;
|
|
case 'V':
|
|
do_version = true;
|
|
break;
|
|
case CHAR_MAX + 1: /* --split-merged-entry */
|
|
break;
|
|
default:
|
|
usage (EXIT_FAILURE);
|
|
}
|
|
|
|
if (do_version)
|
|
{
|
|
/* Version information is requested. */
|
|
printf ("%s\n", getprogname ());
|
|
printf ("Copyright (C) %s Free Software Foundation, Inc.\n\
|
|
License GPLv2+: GNU GPL version 2 or later <https://gnu.org/licenses/gpl.html>\n\
|
|
This is free software: you are free to change and redistribute it.\n\
|
|
There is NO WARRANTY, to the extent permitted by law.\n\
|
|
",
|
|
"2020");
|
|
printf ("Written by %s.\n", "Bruno Haible");
|
|
exit (EXIT_SUCCESS);
|
|
}
|
|
|
|
if (do_help)
|
|
{
|
|
/* Help is requested. */
|
|
usage (EXIT_SUCCESS);
|
|
}
|
|
|
|
/* Test argument count. */
|
|
if (optind + 3 != argc)
|
|
error (EXIT_FAILURE, 0, "expected three arguments");
|
|
|
|
{
|
|
const char *ancestor_file_name; /* O-FILE-NAME */
|
|
const char *destination_file_name; /* A-FILE-NAME */
|
|
bool downstream;
|
|
const char *other_file_name; /* B-FILE-NAME */
|
|
const char *mainstream_file_name;
|
|
const char *modified_file_name;
|
|
struct changelog_file ancestor_file;
|
|
struct changelog_file mainstream_file;
|
|
struct changelog_file modified_file;
|
|
/* Mapping from indices in ancestor_file to indices in mainstream_file. */
|
|
struct entries_mapping mapping;
|
|
struct differences diffs;
|
|
gl_list_node_t *result_entries_pointers; /* array of pointers into result_entries */
|
|
gl_list_t /* <struct entry *> */ result_entries;
|
|
gl_list_t /* <struct conflict *> */ result_conflicts;
|
|
|
|
ancestor_file_name = argv[optind];
|
|
destination_file_name = argv[optind + 1];
|
|
other_file_name = argv[optind + 2];
|
|
|
|
/* Heuristic to determine whether it's a pull in downstream direction
|
|
(e.g. pull from a centralized server) or a pull in upstream direction
|
|
(e.g. "git stash apply").
|
|
|
|
For ChangeLog this distinction is important. The difference between
|
|
an "upstream" and a "downstream" repository is that more people are
|
|
looking at the "upstream" repository. They want to be informed about
|
|
changes and expect them to be shown at the top of the ChangeLog.
|
|
When a user pulls downstream, on the other hand, he has two options:
|
|
a) He gets the change entries from the central repository also at the
|
|
top of his ChangeLog, and his own changes come after them.
|
|
b) He gets the change entries from the central repository after those
|
|
he has collected for his branch. His own change entries stay at
|
|
the top of the ChangeLog file.
|
|
In the case a) he has to reorder the ChangeLog before he can commit.
|
|
No one does that. So most people want b).
|
|
In other words, the order of entries in a ChangeLog should represent
|
|
the order in which they have flown (or will flow) into the *central*
|
|
repository.
|
|
|
|
But in git this is fundamentally indistinguishable, because when Linus
|
|
pulls patches from akpm and akpm pulls patches from Linus, it's not
|
|
clear which of the two is more "upstream". Also, when you have many
|
|
branches in a repository and pull from one to another, "git" has no way
|
|
to know which branch is more "upstream" than the other. The git-tag(1)
|
|
manual page also says:
|
|
"One important aspect of git is it is distributed, and being
|
|
distributed largely means there is no inherent "upstream" or
|
|
"downstream" in the system."
|
|
Therefore anyone who attempts to produce a ChangeLog from the merge
|
|
history will fail.
|
|
|
|
Here we allow the user to specify the pull direction through an
|
|
environment variable (GIT_UPSTREAM or GIT_DOWNSTREAM). If these two
|
|
environment variables are not set, we assume a "simple single user"
|
|
usage pattern: He manages local changes through stashes and uses
|
|
"git pull" only to pull downstream.
|
|
|
|
How to distinguish these situation? There are several hints:
|
|
- During a "git stash apply", GIT_REFLOG_ACTION is not set. During
|
|
a "git pull", it is set to 'pull '. During a "git pull --rebase",
|
|
it is set to 'pull --rebase'. During a "git cherry-pick", it is
|
|
set to 'cherry-pick'.
|
|
- During a "git stash apply", there is an environment variable of
|
|
the form GITHEAD_<40_hex_digits>='Stashed changes'. */
|
|
{
|
|
const char *var;
|
|
|
|
var = getenv ("GIT_DOWNSTREAM");
|
|
if (var != NULL && var[0] != '\0')
|
|
downstream = true;
|
|
else
|
|
{
|
|
var = getenv ("GIT_UPSTREAM");
|
|
if (var != NULL && var[0] != '\0')
|
|
downstream = false;
|
|
else
|
|
{
|
|
var = getenv ("GIT_REFLOG_ACTION");
|
|
#if 0 /* Debugging code */
|
|
printf ("GIT_REFLOG_ACTION=|%s|\n", var);
|
|
#endif
|
|
if (var != NULL
|
|
&& ((strncmp (var, "pull", 4) == 0
|
|
&& c_strstr (var, " --rebase") == NULL)
|
|
|| strncmp (var, "merge origin", 12) == 0))
|
|
downstream = true;
|
|
else
|
|
{
|
|
/* "git stash apply", "git rebase", "git cherry-pick" and
|
|
similar. */
|
|
downstream = false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#if 0 /* Debugging code */
|
|
{
|
|
char buf[1000];
|
|
printf ("First line of %%A:\n");
|
|
sprintf (buf, "head -n 1 %s", destination_file_name); system (buf);
|
|
printf ("First line of %%B:\n");
|
|
sprintf (buf, "head -n 1 %s", other_file_name); system (buf);
|
|
printf ("Guessing calling convention: %s\n",
|
|
downstream
|
|
? "%A = modified by user, %B = upstream"
|
|
: "%A = upstream, %B = modified by user");
|
|
}
|
|
#endif
|
|
|
|
if (downstream)
|
|
{
|
|
mainstream_file_name = other_file_name;
|
|
modified_file_name = destination_file_name;
|
|
}
|
|
else
|
|
{
|
|
mainstream_file_name = destination_file_name;
|
|
modified_file_name = other_file_name;
|
|
}
|
|
|
|
/* Read the three files into memory. */
|
|
read_changelog_file (ancestor_file_name, &ancestor_file);
|
|
read_changelog_file (mainstream_file_name, &mainstream_file);
|
|
read_changelog_file (modified_file_name, &modified_file);
|
|
|
|
/* Compute correspondence between the entries of ancestor_file and of
|
|
mainstream_file. */
|
|
compute_mapping (&ancestor_file, &mainstream_file, false, &mapping);
|
|
(void) entries_mapping_reverse_get; /* avoid gcc "defined but not" warning */
|
|
|
|
/* Compute differences between the entries of ancestor_file and of
|
|
modified_file. */
|
|
compute_differences (&ancestor_file, &modified_file, &diffs);
|
|
|
|
/* Compute the result. */
|
|
result_entries_pointers =
|
|
XNMALLOC (mainstream_file.num_entries, gl_list_node_t);
|
|
result_entries =
|
|
gl_list_create_empty (GL_LINKED_LIST, entry_equals, entry_hashcode,
|
|
NULL, true);
|
|
{
|
|
idx_t k;
|
|
for (k = 0; k < mainstream_file.num_entries; k++)
|
|
result_entries_pointers[k] =
|
|
gl_list_add_last (result_entries, mainstream_file.entries[k]);
|
|
}
|
|
result_conflicts =
|
|
gl_list_create_empty (GL_ARRAY_LIST, NULL, NULL, NULL, true);
|
|
{
|
|
idx_t e;
|
|
for (e = 0; e < diffs.num_edits; e++)
|
|
{
|
|
struct edit *edit = diffs.edits[e];
|
|
switch (edit->type)
|
|
{
|
|
case ADDITION:
|
|
if (edit->j1 == 0)
|
|
{
|
|
/* An addition to the top of modified_file.
|
|
Apply it to the top of mainstream_file. */
|
|
idx_t j;
|
|
for (j = edit->j2; j > edit->j1; )
|
|
{
|
|
j--;
|
|
struct entry *added_entry = modified_file.entries[j];
|
|
gl_list_add_first (result_entries, added_entry);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
idx_t i_before;
|
|
idx_t i_after;
|
|
ptrdiff_t k_before;
|
|
ptrdiff_t k_after;
|
|
i_before = diffs.index_mapping_reverse[edit->j1 - 1];
|
|
ASSERT (i_before >= 0);
|
|
i_after = (edit->j2 + 1 == modified_file.num_entries
|
|
? ancestor_file.num_entries
|
|
: diffs.index_mapping_reverse[edit->j2 + 1]);
|
|
ASSERT (i_after >= 0);
|
|
ASSERT (i_after == i_before + 1);
|
|
/* An addition between ancestor_file.entries[i_before] and
|
|
ancestor_file.entries[i_after]. See whether these two
|
|
entries still exist in mainstream_file and are still
|
|
consecutive. */
|
|
k_before = entries_mapping_get (&mapping, i_before);
|
|
k_after = (i_after == ancestor_file.num_entries
|
|
? mainstream_file.num_entries
|
|
: entries_mapping_get (&mapping, i_after));
|
|
if (k_before >= 0 && k_after >= 0 && k_after == k_before + 1)
|
|
{
|
|
/* Yes, the entry before and after are still neighbours
|
|
in mainstream_file. Apply the addition between
|
|
them. */
|
|
if (k_after == mainstream_file.num_entries)
|
|
{
|
|
idx_t j;
|
|
for (j = edit->j1; j <= edit->j2; j++)
|
|
{
|
|
struct entry *added_entry = modified_file.entries[j];
|
|
gl_list_add_last (result_entries, added_entry);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
gl_list_node_t node_k_after = result_entries_pointers[k_after];
|
|
idx_t j;
|
|
for (j = edit->j1; j <= edit->j2; j++)
|
|
{
|
|
struct entry *added_entry = modified_file.entries[j];
|
|
gl_list_add_before (result_entries, node_k_after, added_entry);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
/* It's not clear where the additions should be applied.
|
|
Let the user decide. */
|
|
struct conflict *c = XMALLOC (struct conflict);
|
|
idx_t j;
|
|
c->num_old_entries = 0;
|
|
c->old_entries = NULL;
|
|
c->num_modified_entries = edit->j2 - edit->j1 + 1;
|
|
c->modified_entries =
|
|
XNMALLOC (c->num_modified_entries, struct entry *);
|
|
for (j = edit->j1; j <= edit->j2; j++)
|
|
c->modified_entries[j - edit->j1] = modified_file.entries[j];
|
|
gl_list_add_last (result_conflicts, c);
|
|
}
|
|
}
|
|
break;
|
|
case REMOVAL:
|
|
{
|
|
/* Apply the removals one by one. */
|
|
idx_t i;
|
|
for (i = edit->i1; i <= edit->i2; i++)
|
|
{
|
|
struct entry *removed_entry = ancestor_file.entries[i];
|
|
ptrdiff_t k = entries_mapping_get (&mapping, i);
|
|
if (k >= 0
|
|
&& entry_equals (removed_entry,
|
|
mainstream_file.entries[k]))
|
|
{
|
|
/* The entry to be removed still exists in
|
|
mainstream_file. Remove it. */
|
|
gl_list_node_set_value (result_entries,
|
|
result_entries_pointers[k],
|
|
&empty_entry);
|
|
}
|
|
else
|
|
{
|
|
/* The entry to be removed was already removed or was
|
|
modified. This is a conflict. */
|
|
struct conflict *c = XMALLOC (struct conflict);
|
|
c->num_old_entries = 1;
|
|
c->old_entries =
|
|
XNMALLOC (c->num_old_entries, struct entry *);
|
|
c->old_entries[0] = removed_entry;
|
|
c->num_modified_entries = 0;
|
|
c->modified_entries = NULL;
|
|
gl_list_add_last (result_conflicts, c);
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
case CHANGE:
|
|
{
|
|
bool done = false;
|
|
/* When the user usually merges entries from the same day,
|
|
and this edit is at the top of the file: */
|
|
if (split_merged_entry && edit->j1 == 0)
|
|
{
|
|
/* Test whether the change is "simple merged", i.e. whether
|
|
it consists of additions, followed by an augmentation of
|
|
the first changed entry, followed by small changes of the
|
|
remaining entries:
|
|
entry_1
|
|
entry_2
|
|
...
|
|
entry_n
|
|
are mapped to
|
|
added_entry
|
|
...
|
|
added_entry
|
|
augmented_entry_1
|
|
modified_entry_2
|
|
...
|
|
modified_entry_n. */
|
|
if (edit->i2 - edit->i1 <= edit->j2 - edit->j1)
|
|
{
|
|
struct entry *split[2];
|
|
bool simple_merged =
|
|
try_split_merged_entry (ancestor_file.entries[edit->i1],
|
|
modified_file.entries[edit->i1 + edit->j2 - edit->i2],
|
|
split);
|
|
if (simple_merged)
|
|
{
|
|
idx_t i;
|
|
for (i = edit->i1 + 1; i <= edit->i2; i++)
|
|
if (entry_fstrcmp (ancestor_file.entries[i],
|
|
modified_file.entries[i + edit->j2 - edit->i2],
|
|
FSTRCMP_THRESHOLD)
|
|
< FSTRCMP_THRESHOLD)
|
|
{
|
|
simple_merged = false;
|
|
break;
|
|
}
|
|
}
|
|
if (simple_merged)
|
|
{
|
|
/* Apply the additions at the top of modified_file.
|
|
Apply each of the single-entry changes
|
|
separately. */
|
|
idx_t num_changed = edit->i2 - edit->i1 + 1; /* > 0 */
|
|
idx_t num_added = (edit->j2 - edit->j1 + 1) - num_changed;
|
|
idx_t j;
|
|
/* First part of the split modified_file.entries[edit->j2 - edit->i2 + edit->i1]: */
|
|
gl_list_add_first (result_entries, split[0]);
|
|
/* The additions. */
|
|
for (j = edit->j1 + num_added; j > edit->j1; )
|
|
{
|
|
j--;
|
|
struct entry *added_entry = modified_file.entries[j];
|
|
gl_list_add_first (result_entries, added_entry);
|
|
}
|
|
/* Now the single-entry changes. */
|
|
for (j = edit->j1 + num_added; j <= edit->j2; j++)
|
|
{
|
|
struct entry *changed_entry =
|
|
(j == edit->j1 + num_added
|
|
? split[1]
|
|
: modified_file.entries[j]);
|
|
idx_t i = j + edit->i2 - edit->j2;
|
|
ptrdiff_t k = entries_mapping_get (&mapping, i);
|
|
if (k >= 0
|
|
&& entry_equals (ancestor_file.entries[i],
|
|
mainstream_file.entries[k]))
|
|
{
|
|
gl_list_node_set_value (result_entries,
|
|
result_entries_pointers[k],
|
|
changed_entry);
|
|
}
|
|
else if (!entry_equals (ancestor_file.entries[i],
|
|
changed_entry))
|
|
{
|
|
struct conflict *c = XMALLOC (struct conflict);
|
|
c->num_old_entries = 1;
|
|
c->old_entries =
|
|
XNMALLOC (c->num_old_entries, struct entry *);
|
|
c->old_entries[0] = ancestor_file.entries[i];
|
|
c->num_modified_entries = 1;
|
|
c->modified_entries =
|
|
XNMALLOC (c->num_modified_entries, struct entry *);
|
|
c->modified_entries[0] = changed_entry;
|
|
gl_list_add_last (result_conflicts, c);
|
|
}
|
|
}
|
|
done = true;
|
|
}
|
|
}
|
|
}
|
|
if (!done)
|
|
{
|
|
bool simple;
|
|
/* Test whether the change is "simple", i.e. whether it
|
|
consists of small changes to the old ChangeLog entries
|
|
and additions before them:
|
|
entry_1
|
|
...
|
|
entry_n
|
|
are mapped to
|
|
added_entry
|
|
...
|
|
added_entry
|
|
modified_entry_1
|
|
...
|
|
modified_entry_n. */
|
|
if (edit->i2 - edit->i1 <= edit->j2 - edit->j1)
|
|
{
|
|
idx_t i;
|
|
simple = true;
|
|
for (i = edit->i1; i <= edit->i2; i++)
|
|
if (entry_fstrcmp (ancestor_file.entries[i],
|
|
modified_file.entries[i + edit->j2 - edit->i2],
|
|
FSTRCMP_THRESHOLD)
|
|
< FSTRCMP_THRESHOLD)
|
|
{
|
|
simple = false;
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
simple = false;
|
|
if (simple)
|
|
{
|
|
/* Apply the additions and each of the single-entry
|
|
changes separately. */
|
|
idx_t num_changed = edit->i2 - edit->i1 + 1; /* > 0 */
|
|
idx_t num_added = (edit->j2 - edit->j1 + 1) - num_changed;
|
|
if (edit->j1 == 0)
|
|
{
|
|
/* A simple change at the top of modified_file.
|
|
Apply it to the top of mainstream_file. */
|
|
idx_t j;
|
|
for (j = edit->j1 + num_added; j > edit->j1; )
|
|
{
|
|
j--;
|
|
struct entry *added_entry = modified_file.entries[j];
|
|
gl_list_add_first (result_entries, added_entry);
|
|
}
|
|
for (j = edit->j1 + num_added; j <= edit->j2; j++)
|
|
{
|
|
struct entry *changed_entry = modified_file.entries[j];
|
|
idx_t i = j + edit->i2 - edit->j2;
|
|
ptrdiff_t k = entries_mapping_get (&mapping, i);
|
|
if (k >= 0
|
|
&& entry_equals (ancestor_file.entries[i],
|
|
mainstream_file.entries[k]))
|
|
{
|
|
gl_list_node_set_value (result_entries,
|
|
result_entries_pointers[k],
|
|
changed_entry);
|
|
}
|
|
else
|
|
{
|
|
struct conflict *c;
|
|
ASSERT (!entry_equals (ancestor_file.entries[i],
|
|
changed_entry));
|
|
c = XMALLOC (struct conflict);
|
|
c->num_old_entries = 1;
|
|
c->old_entries =
|
|
XNMALLOC (c->num_old_entries, struct entry *);
|
|
c->old_entries[0] = ancestor_file.entries[i];
|
|
c->num_modified_entries = 1;
|
|
c->modified_entries =
|
|
XNMALLOC (c->num_modified_entries, struct entry *);
|
|
c->modified_entries[0] = changed_entry;
|
|
gl_list_add_last (result_conflicts, c);
|
|
}
|
|
}
|
|
done = true;
|
|
}
|
|
else
|
|
{
|
|
idx_t i_before;
|
|
ptrdiff_t k_before;
|
|
bool linear;
|
|
i_before = diffs.index_mapping_reverse[edit->j1 - 1];
|
|
ASSERT (i_before >= 0);
|
|
/* A simple change after ancestor_file.entries[i_before].
|
|
See whether this entry and the following num_changed
|
|
entries still exist in mainstream_file and are still
|
|
consecutive. */
|
|
k_before = entries_mapping_get (&mapping, i_before);
|
|
linear = (k_before >= 0);
|
|
if (linear)
|
|
{
|
|
idx_t i;
|
|
for (i = i_before + 1; i <= i_before + num_changed; i++)
|
|
if (entries_mapping_get (&mapping, i) != k_before + (i - i_before))
|
|
{
|
|
linear = false;
|
|
break;
|
|
}
|
|
}
|
|
if (linear)
|
|
{
|
|
gl_list_node_t node_for_insert =
|
|
result_entries_pointers[k_before + 1];
|
|
idx_t j;
|
|
for (j = edit->j1 + num_added; j > edit->j1; )
|
|
{
|
|
j--;
|
|
struct entry *added_entry = modified_file.entries[j];
|
|
gl_list_add_before (result_entries, node_for_insert, added_entry);
|
|
}
|
|
for (j = edit->j1 + num_added; j <= edit->j2; j++)
|
|
{
|
|
struct entry *changed_entry = modified_file.entries[j];
|
|
idx_t i = j + edit->i2 - edit->j2;
|
|
idx_t k = entries_mapping_get (&mapping, i);
|
|
ASSERT (k >= 0);
|
|
if (entry_equals (ancestor_file.entries[i],
|
|
mainstream_file.entries[k]))
|
|
{
|
|
gl_list_node_set_value (result_entries,
|
|
result_entries_pointers[k],
|
|
changed_entry);
|
|
}
|
|
else
|
|
{
|
|
struct conflict *c;
|
|
ASSERT (!entry_equals (ancestor_file.entries[i],
|
|
changed_entry));
|
|
c = XMALLOC (struct conflict);
|
|
c->num_old_entries = 1;
|
|
c->old_entries =
|
|
XNMALLOC (c->num_old_entries, struct entry *);
|
|
c->old_entries[0] = ancestor_file.entries[i];
|
|
c->num_modified_entries = 1;
|
|
c->modified_entries =
|
|
XNMALLOC (c->num_modified_entries, struct entry *);
|
|
c->modified_entries[0] = changed_entry;
|
|
gl_list_add_last (result_conflicts, c);
|
|
}
|
|
}
|
|
done = true;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
/* A big change.
|
|
See whether the num_changed entries still exist
|
|
unchanged in mainstream_file and are still
|
|
consecutive. */
|
|
idx_t i_first;
|
|
ptrdiff_t k_first;
|
|
bool linear_unchanged;
|
|
i_first = edit->i1;
|
|
k_first = entries_mapping_get (&mapping, i_first);
|
|
linear_unchanged =
|
|
(k_first >= 0
|
|
&& entry_equals (ancestor_file.entries[i_first],
|
|
mainstream_file.entries[k_first]));
|
|
if (linear_unchanged)
|
|
{
|
|
idx_t i;
|
|
for (i = i_first + 1; i <= edit->i2; i++)
|
|
if (!(entries_mapping_get (&mapping, i) == k_first + (i - i_first)
|
|
&& entry_equals (ancestor_file.entries[i],
|
|
mainstream_file.entries[entries_mapping_get (&mapping, i)])))
|
|
{
|
|
linear_unchanged = false;
|
|
break;
|
|
}
|
|
}
|
|
if (linear_unchanged)
|
|
{
|
|
gl_list_node_t node_for_insert =
|
|
result_entries_pointers[k_first];
|
|
idx_t j;
|
|
idx_t i;
|
|
for (j = edit->j2; j > edit->j1; )
|
|
{
|
|
j--;
|
|
struct entry *new_entry = modified_file.entries[j];
|
|
gl_list_add_before (result_entries, node_for_insert, new_entry);
|
|
}
|
|
for (i = edit->i1; i <= edit->i2; i++)
|
|
{
|
|
idx_t k = entries_mapping_get (&mapping, i);
|
|
ASSERT (k >= 0);
|
|
ASSERT (entry_equals (ancestor_file.entries[i],
|
|
mainstream_file.entries[k]));
|
|
gl_list_node_set_value (result_entries,
|
|
result_entries_pointers[k],
|
|
&empty_entry);
|
|
}
|
|
done = true;
|
|
}
|
|
}
|
|
}
|
|
if (!done)
|
|
{
|
|
struct conflict *c = XMALLOC (struct conflict);
|
|
idx_t i, j;
|
|
c->num_old_entries = edit->i2 - edit->i1 + 1;
|
|
c->old_entries =
|
|
XNMALLOC (c->num_old_entries, struct entry *);
|
|
for (i = edit->i1; i <= edit->i2; i++)
|
|
c->old_entries[i - edit->i1] = ancestor_file.entries[i];
|
|
c->num_modified_entries = edit->j2 - edit->j1 + 1;
|
|
c->modified_entries =
|
|
XNMALLOC (c->num_modified_entries, struct entry *);
|
|
for (j = edit->j1; j <= edit->j2; j++)
|
|
c->modified_entries[j - edit->j1] = modified_file.entries[j];
|
|
gl_list_add_last (result_conflicts, c);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Output the result. */
|
|
{
|
|
FILE *fp = fopen (destination_file_name, "w");
|
|
if (fp == NULL)
|
|
{
|
|
fprintf (stderr, "could not write file '%s'\n", destination_file_name);
|
|
exit (EXIT_FAILURE);
|
|
}
|
|
|
|
/* Output the conflicts at the top. */
|
|
{
|
|
idx_t n = gl_list_size (result_conflicts);
|
|
idx_t i;
|
|
for (i = 0; i < n; i++)
|
|
conflict_write (fp, (struct conflict *) gl_list_get_at (result_conflicts, i));
|
|
}
|
|
/* Output the modified and unmodified entries, in order. */
|
|
{
|
|
gl_list_iterator_t iter = gl_list_iterator (result_entries);
|
|
const void *elt;
|
|
gl_list_node_t node;
|
|
while (gl_list_iterator_next (&iter, &elt, &node))
|
|
entry_write (fp, (struct entry *) elt);
|
|
gl_list_iterator_free (&iter);
|
|
}
|
|
|
|
if (fwriteerror (fp))
|
|
{
|
|
fprintf (stderr, "error writing to file '%s'\n", destination_file_name);
|
|
exit (EXIT_FAILURE);
|
|
}
|
|
}
|
|
|
|
exit (gl_list_size (result_conflicts) > 0 ? EXIT_FAILURE : EXIT_SUCCESS);
|
|
}
|
|
}
|