ref-filter: implement '--contains' option

'tag -l' and 'branch -l' have two different ways of finding out if a certain ref contains a commit. Implement both these methods in ref-filter and give the caller of ref-filter API the option to pick which implementation to be used. 'branch -l' uses 'is_descendant_of()' from commit.c which is left as the default implementation to be used. 'tag -l' uses a more specific algorithm since ffc4b80. This implementation is used whenever the 'with_commit_tag_algo' bit is set in 'struct ref_filter'. Mentored-by: Christian Couder <christian.couder@gmail.com> Mentored-by: Matthieu Moy <matthieu.moy@grenoble-inp.fr> Signed-off-by: Karthik Nayak <karthik.188@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Karthik Nayak committed Jul 7, 2015 at 21:36 UTC ee2bd06b0f735a00ce0216ca1d3391b13722d987
3 files changed +121 -1
builtin/tag.c
+5
@@ -86,6 +86,11 @@ static int in_commit_list(const struct commit_list *want, struct commit *c)
86 return 0;
87 }
88
89 +/*
90 + * The entire code segment for supporting the --contains option has been
91 + * copied over to ref-filter.{c,h}. This will be deleted evetually when
92 + * we port tag.c to use ref-filter APIs.
93 + */
94 enum contains_result {
95 CONTAINS_UNKNOWN = -1,
96 CONTAINS_NO = 0,
ref-filter.c
+113 -1
@@ -818,6 +818,114 @@ static void get_ref_atom_value(struct ref_array_item *ref, int atom, struct atom
818 *v = &ref->value[atom];
819 }
820
821 +enum contains_result {
822 + CONTAINS_UNKNOWN = -1,
823 + CONTAINS_NO = 0,
824 + CONTAINS_YES = 1
825 +};
826 +
827 +/*
828 + * Mimicking the real stack, this stack lives on the heap, avoiding stack
829 + * overflows.
830 + *
831 + * At each recursion step, the stack items points to the commits whose
832 + * ancestors are to be inspected.
833 + */
834 +struct contains_stack {
835 + int nr, alloc;
836 + struct contains_stack_entry {
837 + struct commit *commit;
838 + struct commit_list *parents;
839 + } *contains_stack;
840 +};
841 +
842 +static int in_commit_list(const struct commit_list *want, struct commit *c)
843 +{
844 + for (; want; want = want->next)
845 + if (!hashcmp(want->item->object.sha1, c->object.sha1))
846 + return 1;
847 + return 0;
848 +}
849 +
850 +/*
851 + * Test whether the candidate or one of its parents is contained in the list.
852 + * Do not recurse to find out, though, but return -1 if inconclusive.
853 + */
854 +static enum contains_result contains_test(struct commit *candidate,
855 + const struct commit_list *want)
856 +{
857 + /* was it previously marked as containing a want commit? */
858 + if (candidate->object.flags & TMP_MARK)
859 + return 1;
860 + /* or marked as not possibly containing a want commit? */
861 + if (candidate->object.flags & UNINTERESTING)
862 + return 0;
863 + /* or are we it? */
864 + if (in_commit_list(want, candidate)) {
865 + candidate->object.flags |= TMP_MARK;
866 + return 1;
867 + }
868 +
869 + if (parse_commit(candidate) < 0)
870 + return 0;
871 +
872 + return -1;
873 +}
874 +
875 +static void push_to_contains_stack(struct commit *candidate, struct contains_stack *contains_stack)
876 +{
877 + ALLOC_GROW(contains_stack->contains_stack, contains_stack->nr + 1, contains_stack->alloc);
878 + contains_stack->contains_stack[contains_stack->nr].commit = candidate;
879 + contains_stack->contains_stack[contains_stack->nr++].parents = candidate->parents;
880 +}
881 +
882 +static enum contains_result contains_tag_algo(struct commit *candidate,
883 + const struct commit_list *want)
884 +{
885 + struct contains_stack contains_stack = { 0, 0, NULL };
886 + int result = contains_test(candidate, want);
887 +
888 + if (result != CONTAINS_UNKNOWN)
889 + return result;
890 +
891 + push_to_contains_stack(candidate, &contains_stack);
892 + while (contains_stack.nr) {
893 + struct contains_stack_entry *entry = &contains_stack.contains_stack[contains_stack.nr - 1];
894 + struct commit *commit = entry->commit;
895 + struct commit_list *parents = entry->parents;
896 +
897 + if (!parents) {
898 + commit->object.flags |= UNINTERESTING;
899 + contains_stack.nr--;
900 + }
901 + /*
902 + * If we just popped the stack, parents->item has been marked,
903 + * therefore contains_test will return a meaningful 0 or 1.
904 + */
905 + else switch (contains_test(parents->item, want)) {
906 + case CONTAINS_YES:
907 + commit->object.flags |= TMP_MARK;
908 + contains_stack.nr--;
909 + break;
910 + case CONTAINS_NO:
911 + entry->parents = parents->next;
912 + break;
913 + case CONTAINS_UNKNOWN:
914 + push_to_contains_stack(parents->item, &contains_stack);
915 + break;
916 + }
917 + }
918 + free(contains_stack.contains_stack);
919 + return contains_test(candidate, want);
920 +}
921 +
922 +static int commit_contains(struct ref_filter *filter, struct commit *commit)
923 +{
924 + if (filter->with_commit_tag_algo)
925 + return contains_tag_algo(commit, filter->with_commit);
926 + return is_descendant_of(commit, filter->with_commit);
927 +}
928 +
929 /*
930 * Return 1 if the refname matches one of the patterns, otherwise 0.
931 * A pattern can be path prefix (e.g. a refname "refs/heads/master"
@@ -917,10 +1025,14 @@ static int ref_filter_handler(const char *refname, const struct object_id *oid,
1025 * obtain the commit using the 'oid' available and discard all
1026 * non-commits early. The actual filtering is done later.
1027 */
920 - if (filter->merge_commit) {
1028 + if (filter->merge_commit || filter->with_commit) {
1029 commit = lookup_commit_reference_gently(oid->hash, 1);
1030 if (!commit)
1031 return 0;
1032 + /* We perform the filtering for the '--contains' option */
1033 + if (filter->with_commit &&
1034 + !commit_contains(filter, commit))
1035 + return 0;
1036 }
1037
1038 /*
ref-filter.h
+3
@@ -44,6 +44,7 @@ struct ref_array {
44 struct ref_filter {
45 const char **name_patterns;
46 struct sha1_array points_at;
47 + struct commit_list *with_commit;
48
49 enum {
50 REF_FILTER_MERGED_NONE = 0,
@@ -51,6 +52,8 @@ struct ref_filter {
52 REF_FILTER_MERGED_OMIT
53 } merge;
54 struct commit *merge_commit;
55 +
56 + unsigned int with_commit_tag_algo : 1;
57 };
58
59 struct ref_filter_cbdata {