get_merge_bases: clean up even when there is no common commit.
[gitweb.git] / commit.c
index 0327df12324ac77c1d5d82a22ce4b30f1b5c7a18..12882fd496e5c18dacae76a6c68784700ce63721 100644 (file)
--- a/commit.c
+++ b/commit.c
@@ -236,6 +236,7 @@ static struct commit_graft *lookup_commit_graft(const unsigned char *sha1)
 
 int parse_commit_buffer(struct commit *item, void *buffer, unsigned long size)
 {
+       char *tail = buffer;
        char *bufptr = buffer;
        unsigned char parent[20];
        struct commit_list **pptr;
@@ -245,9 +246,10 @@ int parse_commit_buffer(struct commit *item, void *buffer, unsigned long size)
        if (item->object.parsed)
                return 0;
        item->object.parsed = 1;
-       if (memcmp(bufptr, "tree ", 5))
+       tail += size;
+       if (tail <= bufptr + 5 || memcmp(bufptr, "tree ", 5))
                return error("bogus commit object %s", sha1_to_hex(item->object.sha1));
-       if (get_sha1_hex(bufptr + 5, parent) < 0)
+       if (tail <= bufptr + 45 || get_sha1_hex(bufptr + 5, parent) < 0)
                return error("bad tree pointer in commit %s",
                             sha1_to_hex(item->object.sha1));
        item->tree = lookup_tree(parent);
@@ -257,10 +259,12 @@ int parse_commit_buffer(struct commit *item, void *buffer, unsigned long size)
        pptr = &item->parents;
 
        graft = lookup_commit_graft(item->object.sha1);
-       while (!memcmp(bufptr, "parent ", 7)) {
+       while (bufptr + 48 < tail && !memcmp(bufptr, "parent ", 7)) {
                struct commit *new_parent;
 
-               if (get_sha1_hex(bufptr + 7, parent) || bufptr[47] != '\n')
+               if (tail <= bufptr + 48 ||
+                   get_sha1_hex(bufptr + 7, parent) ||
+                   bufptr[47] != '\n')
                        return error("bad parents in commit %s", sha1_to_hex(item->object.sha1));
                bufptr += 48;
                if (graft)
@@ -393,13 +397,12 @@ void clear_commit_marks(struct commit *commit, unsigned int mark)
 {
        struct commit_list *parents;
 
+       if (!commit)
+               return;
        parents = commit->parents;
        commit->object.flags &= ~mark;
        while (parents) {
-               struct commit *parent = parents->item;
-               if (parent && parent->object.parsed &&
-                   (parent->object.flags & mark))
-                       clear_commit_marks(parent, mark);
+               clear_commit_marks(parents->item, mark);
                parents = parents->next;
        }
 }
@@ -842,3 +845,247 @@ void sort_in_topological_order_fn(struct commit_list ** list, int lifo,
        }
        free(nodes);
 }
+
+/* merge-rebase stuff */
+
+/* bits #0..7 in revision.h */
+#define PARENT1                (1u<< 8)
+#define PARENT2                (1u<< 9)
+#define STALE          (1u<<10)
+
+static struct commit *interesting(struct commit_list *list)
+{
+       while (list) {
+               struct commit *commit = list->item;
+               list = list->next;
+               if (commit->object.flags & STALE)
+                       continue;
+               return commit;
+       }
+       return NULL;
+}
+
+/*
+ * A pathological example of how this thing works.
+ *
+ * Suppose we had this commit graph, where chronologically
+ * the timestamp on the commit are A <= B <= C <= D <= E <= F
+ * and we are trying to figure out the merge base for E and F
+ * commits.
+ *
+ *                  F
+ *                 / \
+ *            E   A   D
+ *             \ /   /
+ *              B   /
+ *               \ /
+ *                C
+ *
+ * First we push E and F to list to be processed.  E gets bit 1
+ * and F gets bit 2.  The list becomes:
+ *
+ *     list=F(2) E(1), result=empty
+ *
+ * Then we pop F, the newest commit, from the list.  Its flag is 2.
+ * We scan its parents, mark them reachable from the side that F is
+ * reachable from, and push them to the list:
+ *
+ *     list=E(1) D(2) A(2), result=empty
+ *
+ * Next pop E and do the same.
+ *
+ *     list=D(2) B(1) A(2), result=empty
+ *
+ * Next pop D and do the same.
+ *
+ *     list=C(2) B(1) A(2), result=empty
+ *
+ * Next pop C and do the same.
+ *
+ *     list=B(1) A(2), result=empty
+ *
+ * Now it is B's turn.  We mark its parent, C, reachable from B's side,
+ * and push it to the list:
+ *
+ *     list=C(3) A(2), result=empty
+ *
+ * Now pop C and notice it has flags==3.  It is placed on the result list,
+ * and the list now contains:
+ *
+ *     list=A(2), result=C(3)
+ *
+ * We pop A and do the same.
+ *
+ *     list=B(3), result=C(3)
+ *
+ * Next, we pop B and something very interesting happens.  It has flags==3
+ * so it is also placed on the result list, and its parents are marked
+ * stale, retroactively, and placed back on the list:
+ *
+ *    list=C(7), result=C(7) B(3)
+ *
+ * Now, list does not have any interesting commit.  So we find the newest
+ * commit from the result list that is not marked stale.  Which is
+ * commit B.
+ *
+ *
+ * Another pathological example how this thing used to fail to mark an
+ * ancestor of a merge base as STALE before we introduced the
+ * postprocessing phase (mark_reachable_commits).
+ *
+ *               2
+ *               H
+ *         1    / \
+ *         G   A   \
+ *         |\ /     \
+ *         | B       \
+ *         |  \       \
+ *          \  C       F
+ *           \  \     /
+ *            \  D   /
+ *             \ |  /
+ *              \| /
+ *               E
+ *
+ *      list                   A B C D E F G H
+ *      G1 H2                  - - - - - - 1 2
+ *      H2 E1 B1               - 1 - - 1 - 1 2
+ *      F2 E1 B1 A2            2 1 - - 1 2 1 2
+ *      E3 B1 A2               2 1 - - 3 2 1 2
+ *      B1 A2                  2 1 - - 3 2 1 2
+ *      C1 A2                  2 1 1 - 3 2 1 2
+ *      D1 A2                  2 1 1 1 3 2 1 2
+ *      A2                     2 1 1 1 3 2 1 2
+ *      B3                     2 3 1 1 3 2 1 2
+ *      C7                     2 3 7 1 3 2 1 2
+ *
+ * At this point, unfortunately, everybody in the list is
+ * stale, so we fail to complete the following two
+ * steps to fully marking stale commits.
+ *
+ *      D7                     2 3 7 7 3 2 1 2
+ *      E7                     2 3 7 7 7 2 1 2
+ *
+ * and we ended up showing E as an interesting merge base.
+ * The postprocessing phase re-injects C and continues traversal
+ * to contaminate D and E.
+ */
+
+static void mark_reachable_commits(struct commit_list *result,
+                                  struct commit_list *list)
+{
+       struct commit_list *tmp;
+
+       /*
+        * Postprocess to fully contaminate the well.
+        */
+       for (tmp = result; tmp; tmp = tmp->next) {
+               struct commit *c = tmp->item;
+               /* Reinject stale ones to list,
+                * so we can scan their parents.
+                */
+               if (c->object.flags & STALE)
+                       commit_list_insert(c, &list);
+       }
+       while (list) {
+               struct commit *c = list->item;
+               struct commit_list *parents;
+
+               tmp = list;
+               list = list->next;
+               free(tmp);
+
+               /* Anything taken out of the list is stale, so
+                * mark all its parents stale.  We do not
+                * parse new ones (we already parsed all the relevant
+                * ones).
+                */
+               parents = c->parents;
+               while (parents) {
+                       struct commit *p = parents->item;
+                       parents = parents->next;
+                       if (!(p->object.flags & STALE)) {
+                               p->object.flags |= STALE;
+                               commit_list_insert(p, &list);
+                       }
+               }
+       }
+}
+
+struct commit_list *get_merge_bases(struct commit *rev1, struct commit *rev2,
+                                    int cleanup)
+{
+       struct commit_list *list = NULL;
+       struct commit_list *result = NULL;
+       struct commit_list *tmp = NULL;
+
+       if (rev1 == rev2)
+               return commit_list_insert(rev1, &result);
+
+       parse_commit(rev1);
+       parse_commit(rev2);
+
+       rev1->object.flags |= PARENT1;
+       rev2->object.flags |= PARENT2;
+       insert_by_date(rev1, &list);
+       insert_by_date(rev2, &list);
+
+       while (interesting(list)) {
+               struct commit *commit = list->item;
+               struct commit_list *parents;
+               int flags = commit->object.flags
+                       & (PARENT1 | PARENT2 | STALE);
+
+               tmp = list;
+               list = list->next;
+               free(tmp);
+               if (flags == (PARENT1 | PARENT2)) {
+                       insert_by_date(commit, &result);
+
+                       /* Mark parents of a found merge stale */
+                       flags |= STALE;
+               }
+               parents = commit->parents;
+               while (parents) {
+                       struct commit *p = parents->item;
+                       parents = parents->next;
+                       if ((p->object.flags & flags) == flags)
+                               continue;
+                       parse_commit(p);
+                       p->object.flags |= flags;
+                       insert_by_date(p, &list);
+               }
+       }
+
+       if (!result)
+               goto finish;
+
+       if (result->next && list)
+               mark_reachable_commits(result, list);
+
+       /* cull duplicates */
+       for (tmp = result, list = NULL; tmp; ) {
+               struct commit *commit = tmp->item;
+               struct commit_list *next = tmp->next;
+               if (commit->object.flags & STALE) {
+                       if (list != NULL)
+                               list->next = next;
+                       free(tmp);
+               } else {
+                       if (list == NULL)
+                               result = tmp;
+                       list = tmp;
+                       commit->object.flags |= STALE;
+               }
+
+               tmp = next;
+       }
+
+ finish:
+       if (cleanup) {
+               clear_commit_marks(rev1, PARENT1 | PARENT2 | STALE);
+               clear_commit_marks(rev2, PARENT1 | PARENT2 | STALE);
+       }
+
+       return result;
+}