Merge lp:~bregma/geis/lp-937021 into lp:geis

Proposed by Stephen M. Webb
Status: Merged
Merged at revision: 213
Proposed branch: lp:~bregma/geis/lp-937021
Merge into: lp:geis
Diff against target: 499 lines (+261/-56)
10 files modified
.bzrignore (+3/-3)
configure.ac (+3/-1)
libutouch-geis/backend/grail/geis_grail_backend.c (+15/-1)
libutouch-geis/backend/grail/geis_grail_window_grab.c (+15/-4)
libutouch-geis/backend/grail/geis_grail_window_grab.h (+4/-2)
testsuite/geis2/Makefile.am (+3/-1)
testsuite/geis2/gtest_attrs.cpp (+9/-44)
testsuite/geis2/gtest_geis_fixture.cpp (+64/-0)
testsuite/geis2/gtest_geis_fixture.h (+50/-0)
testsuite/geis2/gtest_subscriptions.cpp (+95/-0)
To merge this branch: bzr merge lp:~bregma/geis/lp-937021
Reviewer Review Type Date Requested Status
Chase Douglas (community) Approve
Review via email: mp+94837@code.launchpad.net

Description of the change

Check he success of grabbing multi-touch input on a window and propagates the result up the stack.

Fixes lp:937021.

Fix can be verified by running the geistest tool without any command-line arguments so that it attempts to grab input from the root window. This will now fail, additional error messages are show if the environment variable GEIS_DEBUG is set to an integer value greater than or equal to 1.

To post a comment you must log in.
Revision history for this message
Jussi Pakkanen (jpakkane) wrote :

Having final_exit and goto would make more sense if some resource were freed there. Personally I would replace them with an explicit "return GEIS_STATUS_UNKNOWN_ERROR;". This is not a blocker for merging, though.

Revision history for this message
Chase Douglas (chasedouglas) wrote :

This needs an automated test that runs at 'make check'.

review: Needs Fixing
lp:~bregma/geis/lp-937021 updated
210. By Stephen M. Webb

synched with trunk

211. By Stephen M. Webb

Refactored GEIS test fixture.

212. By Stephen M. Webb

Removed warnings from configure.

213. By Stephen M. Webb

Added test case for lp:937021

214. By Stephen M. Webb

Got lp:937021 test case passing (when evemu works).

215. By Stephen M. Webb

Eliminated transient evemy device creation failures.

Revision history for this message
Stephen M. Webb (bregma) wrote :

OK, test cases seem to pass every time. Ready for full review.

Revision history for this message
Chase Douglas (chasedouglas) wrote :

Looks good to me :). The GeisSubscriptionTests class doesn't need the SetUp or TearDown methods since they just call the superclass methods. After deleting them, feel free to merge.

review: Approve
lp:~bregma/geis/lp-937021 updated
216. By Stephen M. Webb

Removed unnecessary fixture SetUp() and TearDown() in gtest-based tests.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file '.bzrignore'
--- .bzrignore 2012-02-21 20:31:39 +0000
+++ .bzrignore 2012-03-07 18:15:24 +0000
@@ -10,9 +10,9 @@
10*Makefile.in10*Makefile.in
11aclocal.m411aclocal.m4
12autom4te.cache12autom4te.cache
13check_geis1_api13check-*-compile
14check_geis2_api14check_*_api
15check_geis2_internals15check_*_internals
16check_geis_util16check_geis_util
17config.*17config.*
18configure18configure
1919
=== modified file 'configure.ac'
--- configure.ac 2012-02-23 17:07:26 +0000
+++ configure.ac 2012-03-07 18:15:24 +0000
@@ -32,6 +32,7 @@
3232
33# Checks for programs.33# Checks for programs.
34AM_PROG_CC_C_O34AM_PROG_CC_C_O
35AC_PROG_CXX
35AM_PATH_PYTHON([2.7])36AM_PATH_PYTHON([2.7])
3637
37LT_PREREQ([2.2.6b])38LT_PREREQ([2.2.6b])
@@ -99,7 +100,7 @@
99 [with_integration_tests=check])100 [with_integration_tests=check])
100AC_MSG_RESULT([$with_integration_tests])101AC_MSG_RESULT([$with_integration_tests])
101AS_IF([test "x$with_integration_tests" != xno],102AS_IF([test "x$with_integration_tests" != xno],
102 [AC_PROG_CXX103 [AC_LANG_PUSH(C++)
103 AC_CHECK_LIB([gtest], [main], [have_gtest=yes])104 AC_CHECK_LIB([gtest], [main], [have_gtest=yes])
104 AS_IF([test "x$have_gtest" = xyes],105 AS_IF([test "x$have_gtest" = xyes],
105 [PKG_CHECK_MODULES([XORG_GTEST],106 [PKG_CHECK_MODULES([XORG_GTEST],
@@ -109,6 +110,7 @@
109 unit testing disabled])])110 unit testing disabled])])
110 PKG_CHECK_MODULES([EVEMU],[utouch-evemu >= 1.0.5])111 PKG_CHECK_MODULES([EVEMU],[utouch-evemu >= 1.0.5])
111 ])112 ])
113 AC_LANG_POP
112 XORG_GTEST_LIBS="$XORG_GTEST_LIBS -lgtest -lpthread"])114 XORG_GTEST_LIBS="$XORG_GTEST_LIBS -lgtest -lpthread"])
113AM_CONDITIONAL([HAVE_XORG_GTEST],[test "x$have_xorg_gtest" = xyes])115AM_CONDITIONAL([HAVE_XORG_GTEST],[test "x$have_xorg_gtest" = xyes])
114116
115117
=== modified file 'libutouch-geis/backend/grail/geis_grail_backend.c'
--- libutouch-geis/backend/grail/geis_grail_backend.c 2012-02-24 17:12:40 +0000
+++ libutouch-geis/backend/grail/geis_grail_backend.c 2012-03-07 18:15:24 +0000
@@ -1437,7 +1437,13 @@
1437 (void *)ugsub,1437 (void *)ugsub,
1438 grail_use_atomic_gestures);1438 grail_use_atomic_gestures);
14391439
1440 geis_grail_window_grab_store_grab(gbe->window_grabs, window_id);1440 status = geis_grail_window_grab_store_grab(gbe->window_grabs, window_id);
1441 if (status != GEIS_STATUS_SUCCESS)
1442 {
1443 geis_error("failed to grab input on window 0x%08x", window_id);
1444 goto final_exit;
1445 }
1446
1441 ugstatus = grail_subscription_activate(gbe->grail, ugsub);1447 ugstatus = grail_subscription_activate(gbe->grail, ugsub);
1442 if (ugstatus != UGStatusSuccess)1448 if (ugstatus != UGStatusSuccess)
1443 {1449 {
@@ -1486,6 +1492,10 @@
1486 device,1492 device,
1487 window_id,1493 window_id,
1488 subscription);1494 subscription);
1495 if (status != GEIS_STATUS_SUCCESS)
1496 {
1497 goto final_exit;
1498 }
1489 }1499 }
1490 else1500 else
1491 {1501 {
@@ -1506,6 +1516,8 @@
1506 subscription);1516 subscription);
1507 }1517 }
1508 }1518 }
1519
1520final_exit:
1509 return status;1521 return status;
1510}1522}
15111523
@@ -1558,6 +1570,8 @@
1558 status = GEIS_STATUS_SUCCESS;1570 status = GEIS_STATUS_SUCCESS;
1559 }1571 }
1560 geis_device_bag_delete(selected_devices);1572 geis_device_bag_delete(selected_devices);
1573 if (status != GEIS_STATUS_SUCCESS)
1574 break;
1561 }1575 }
15621576
1563 return status;1577 return status;
15641578
=== modified file 'libutouch-geis/backend/grail/geis_grail_window_grab.c'
--- libutouch-geis/backend/grail/geis_grail_window_grab.c 2012-03-02 17:54:50 +0000
+++ libutouch-geis/backend/grail/geis_grail_window_grab.c 2012-03-07 18:15:24 +0000
@@ -20,7 +20,6 @@
20#include "geis_config.h"20#include "geis_config.h"
21#include "geis_grail_window_grab.h"21#include "geis_grail_window_grab.h"
2222
23#include "geis/geis.h"
24#include "geis_bag.h"23#include "geis_bag.h"
25#include "geis_logging.h"24#include "geis_logging.h"
26#include <X11/extensions/XInput2.h>25#include <X11/extensions/XInput2.h>
@@ -124,9 +123,10 @@
124/*123/*
125 * Grabs a window through a window grab store.124 * Grabs a window through a window grab store.
126 */125 */
127void126GeisStatus
128geis_grail_window_grab_store_grab(GeisGrailWindowGrabStore wgs, Window window_id)127geis_grail_window_grab_store_grab(GeisGrailWindowGrabStore wgs, Window window_id)
129{128{
129 GeisStatus status = GEIS_STATUS_UNKNOWN_ERROR;
130 GeisGrailWindowGrab grab = _window_grab_store_find(wgs, window_id);130 GeisGrailWindowGrab grab = _window_grab_store_find(wgs, window_id);
131 if (!grab)131 if (!grab)
132 {132 {
@@ -147,16 +147,27 @@
147 int xstat = XIGrabTouchBegin(wgs->display, XIAllMasterDevices,147 int xstat = XIGrabTouchBegin(wgs->display, XIAllMasterDevices,
148 window_id,148 window_id,
149 0, &mask, 1, &mods);149 0, &mask, 1, &mods);
150 free(mask.mask);
150 if (xstat)151 if (xstat)
151 {152 {
152 geis_error("error %d returned from XIGrabTouchBegin()", xstat);153 geis_error("error %d returned from XIGrabTouchBegin()", xstat);
153 }154 goto final_exit;
154 free(mask.mask);155 }
156 else if (mods.status != XIGrabSuccess)
157 {
158 geis_error("status %d returned from XIGrabTouchBegin()", mods.status);
159 goto final_exit;
160 }
161 status = GEIS_STATUS_SUCCESS;
155 }162 }
156 else163 else
157 {164 {
158 ++grab->grab_count;165 ++grab->grab_count;
166 status = GEIS_STATUS_SUCCESS;
159 }167 }
168
169final_exit:
170 return status;
160}171}
161172
162173
163174
=== modified file 'libutouch-geis/backend/grail/geis_grail_window_grab.h'
--- libutouch-geis/backend/grail/geis_grail_window_grab.h 2012-01-31 20:46:42 +0000
+++ libutouch-geis/backend/grail/geis_grail_window_grab.h 2012-03-07 18:15:24 +0000
@@ -20,6 +20,7 @@
20#ifndef GEIS_BACKEND_GRAIL_WINDOW_GRAB_H_20#ifndef GEIS_BACKEND_GRAIL_WINDOW_GRAB_H_
21#define GEIS_BACKEND_GRAIL_WINDOW_GRAB_H_21#define GEIS_BACKEND_GRAIL_WINDOW_GRAB_H_
2222
23#include "geis/geis.h"
23#include <X11/Xlib.h>24#include <X11/Xlib.h>
2425
2526
@@ -56,9 +57,10 @@
56 * @param store A window grab store.57 * @param store A window grab store.
57 * @param window The window to grab.58 * @param window The window to grab.
58 *59 *
59 * @returns a reference to the grab.60 * @returns GEIS_STATUS_SUCCESS if the multi-touch for the window was grabbed
61 * successfully, GEIS_STATUS_UNKNOWN_ERROR otherwise.
60 */62 */
61void63GeisStatus
62geis_grail_window_grab_store_grab(GeisGrailWindowGrabStore store,64geis_grail_window_grab_store_grab(GeisGrailWindowGrabStore store,
63 Window window);65 Window window);
6466
6567
=== modified file 'testsuite/geis2/Makefile.am'
--- testsuite/geis2/Makefile.am 2012-03-05 14:22:20 +0000
+++ testsuite/geis2/Makefile.am 2012-03-07 18:15:24 +0000
@@ -44,7 +44,9 @@
4444
45gtest_geis2_api_SOURCES = \45gtest_geis2_api_SOURCES = \
46 gtest_attrs.cpp \46 gtest_attrs.cpp \
47 gtest_evemu_device.h gtest_evemu_device.cpp47 gtest_subscriptions.cpp \
48 gtest_evemu_device.h gtest_evemu_device.cpp \
49 gtest_geis_fixture.h gtest_geis_fixture.cpp
4850
49gtest_geis2_api_CPPFLAGS = \51gtest_geis2_api_CPPFLAGS = \
50 --std=c++0x \52 --std=c++0x \
5153
=== modified file 'testsuite/geis2/gtest_attrs.cpp'
--- testsuite/geis2/gtest_attrs.cpp 2012-03-05 17:22:19 +0000
+++ testsuite/geis2/gtest_attrs.cpp 2012-03-07 18:15:24 +0000
@@ -7,13 +7,11 @@
7#include <functional>7#include <functional>
8#include "geis/geis.h"8#include "geis/geis.h"
9#include "gtest_evemu_device.h"9#include "gtest_evemu_device.h"
10#include "gtest_geis_fixture.h"
10#include <gtest/gtest.h>11#include <gtest/gtest.h>
11#include <memory>
12#include <mutex>12#include <mutex>
13#include <sys/select.h>13#include <sys/select.h>
14#include <sys/time.h>14#include <sys/time.h>
15#include <X11/extensions/XInput2.h>
16#include <xorg/gtest/test.h>
1715
1816
19namespace17namespace
@@ -23,52 +21,19 @@
23static const std::string TEST_DEVICE_EVENTS_FILE("touchscreen_a_rotate90.events");21static const std::string TEST_DEVICE_EVENTS_FILE("touchscreen_a_rotate90.events");
2422
25/**23/**
26 * Fixture for testing expected attributes.24 * Fixture for testing expected attributes. This has to be a separate class
25 * because of the way Java reflection is used in jUnit.
27 */26 */
28class GeisAttributeTests27class GeisAttributeTests
29: public xorg::testing::Test28: public GTestGeisFixture
30{29{
31public:30public:
32 void31 GeisAttributeTests()
33 SetUp()32 : evemu_device_(TEST_DEVICE_PROP_FILE)
34 {33 { }
35 // Chain up the static class heirarchy, as if the test framework was
36 // designed by a Java trainee with some exposure to glib but has never used
37 // C++.
38 ASSERT_NO_FATAL_FAILURE(xorg::testing::Test::SetUp());
39
40 // Verify that whatever X server is in use supports the required XInput
41 // version.
42 int xi_major = 2;
43 int xi_minor = 2;
44 ASSERT_EQ(Success, XIQueryVersion(Display(), &xi_major, &xi_minor));
45 ASSERT_GE(xi_major, 2);
46 ASSERT_GE(xi_minor, 2);
47
48 evemu_device_.reset(new Testsuite::EvemuDevice(TEST_DEVICE_PROP_FILE));
49
50 geis_ = geis_new(GEIS_INIT_SYNCHRONOUS_START, GEIS_INIT_NO_ATOMIC_GESTURES, NULL);
51 }
52
53 void
54 TearDown()
55 {
56 geis_delete(geis_);
57 xorg::testing::Test::TearDown(); // NVI ftw.
58 evemu_device_.reset();
59 }
60
61 int
62 geis_fd()
63 {
64 GeisInteger fd;
65 geis_get_configuration(geis_, GEIS_CONFIGURATION_FD, &fd);
66 return fd;
67 }
6834
69protected:35protected:
70 std::unique_ptr<Testsuite::EvemuDevice> evemu_device_;36 Testsuite::EvemuDevice evemu_device_;
71 Geis geis_;
72};37};
7338
7439
@@ -114,7 +79,7 @@
114 {79 {
115 if (geis_event_type(event) == GEIS_EVENT_INIT_COMPLETE)80 if (geis_event_type(event) == GEIS_EVENT_INIT_COMPLETE)
116 {81 {
117 evemu_device_->play(TEST_DEVICE_EVENTS_FILE);82 evemu_device_.play(TEST_DEVICE_EVENTS_FILE);
118 }83 }
119 else if (geis_event_type(event) == GEIS_EVENT_GESTURE_END)84 else if (geis_event_type(event) == GEIS_EVENT_GESTURE_END)
120 {85 {
12186
=== added file 'testsuite/geis2/gtest_geis_fixture.cpp'
--- testsuite/geis2/gtest_geis_fixture.cpp 1970-01-01 00:00:00 +0000
+++ testsuite/geis2/gtest_geis_fixture.cpp 2012-03-07 18:15:24 +0000
@@ -0,0 +1,64 @@
1/**
2 * @file gtest_geis_fixture.cpp
3 * @brief A GTest fixture for testing the full uTouch stack through GEIS.
4 */
5/*
6 * Copyright 2012 Canonical Ltd.
7 *
8 * This program is free software: you can redistribute it and/or modify it
9 * under the terms of the GNU General Public License version 3, as published
10 * by the Free Software Foundation.
11 *
12 * This program is distributed in the hope that it will be useful, but
13 * WITHOUT ANY WARRANTY; without even the implied warranties of
14 * MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
15 * PURPOSE. See the GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License along
18 * with this program. If not, see <http://www.gnu.org/licenses/>.
19 */
20#include "gtest_geis_fixture.h"
21
22#include <unistd.h>
23#include <X11/extensions/XInput2.h>
24
25
26void GTestGeisFixture::
27SetUp()
28{
29 // wait for things to settle
30 usleep(1000000);
31
32 // Chain up the static class heirarchy, as if the test framework was
33 // designed by a Java trainee with some exposure to glib but has never used
34 // C++.
35 ASSERT_NO_FATAL_FAILURE(xorg::testing::Test::SetUp());
36
37 // Verify that whatever X server is in use supports the required XInput
38 // version.
39 int xi_major = 2;
40 int xi_minor = 2;
41 ASSERT_EQ(Success, XIQueryVersion(Display(), &xi_major, &xi_minor));
42 ASSERT_GE(xi_major, 2);
43 ASSERT_GE(xi_minor, 2);
44
45 geis_ = geis_new(GEIS_INIT_SYNCHRONOUS_START, GEIS_INIT_NO_ATOMIC_GESTURES, NULL);
46}
47
48void GTestGeisFixture::
49TearDown()
50{
51 geis_delete(geis_);
52 xorg::testing::Test::TearDown(); // NVI ftw.
53 usleep(1000000);
54}
55
56int GTestGeisFixture::
57geis_fd()
58{
59 GeisInteger fd;
60 geis_get_configuration(geis_, GEIS_CONFIGURATION_FD, &fd);
61 return fd;
62}
63
64
065
=== added file 'testsuite/geis2/gtest_geis_fixture.h'
--- testsuite/geis2/gtest_geis_fixture.h 1970-01-01 00:00:00 +0000
+++ testsuite/geis2/gtest_geis_fixture.h 2012-03-07 18:15:24 +0000
@@ -0,0 +1,50 @@
1/**
2 * @file gtest_geis_fixture.h
3 * @brief A GTest fixture for testing the full uTouch stack through GEIS.
4 */
5/*
6 * Copyright 2012 Canonical Ltd.
7 *
8 * This program is free software: you can redistribute it and/or modify it
9 * under the terms of the GNU General Public License version 3, as published
10 * by the Free Software Foundation.
11 *
12 * This program is distributed in the hope that it will be useful, but
13 * WITHOUT ANY WARRANTY; without even the implied warranties of
14 * MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
15 * PURPOSE. See the GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License along
18 * with this program. If not, see <http://www.gnu.org/licenses/>.
19 */
20#ifndef GTEST_GEIS_FIXTURE_H_
21#define GTEST_GEIS_FIXTURE_H_
22
23#include "geis/geis.h"
24#include <gtest/gtest.h>
25#include <xorg/gtest/test.h>
26
27
28/**
29 * Fixture for testing expected attributes.
30 */
31class GTestGeisFixture
32: public xorg::testing::Test
33{
34public:
35 void
36 SetUp();
37
38 void
39 TearDown();
40
41 int
42 geis_fd();
43
44protected:
45 Geis geis_;
46};
47
48
49#endif /* GTEST_GEIS_FIXTURE_H_ */
50
051
=== added file 'testsuite/geis2/gtest_subscriptions.cpp'
--- testsuite/geis2/gtest_subscriptions.cpp 1970-01-01 00:00:00 +0000
+++ testsuite/geis2/gtest_subscriptions.cpp 2012-03-07 18:15:24 +0000
@@ -0,0 +1,95 @@
1/**
2 * GTest-based test suite for GEIS subscription actions.
3 *
4 * Copyright 2012 Canonical Ltd.
5 */
6
7#include "gtest_evemu_device.h"
8#include "gtest_geis_fixture.h"
9
10
11static const std::string TEST_DEVICE_PROP_FILE("touchscreen_a.prop");
12
13
14/**
15 * Tests need to make sure at least one multi-touch device is available.
16 */
17class GeisSubscriptionTests
18: public GTestGeisFixture
19{
20public:
21 GeisSubscriptionTests()
22 : evemu_device_(TEST_DEVICE_PROP_FILE)
23 { }
24
25private:
26 Testsuite::EvemuDevice evemu_device_;
27};
28
29
30/**
31 * Regression test for lp:937021: Geis subscription touch grabs need to check
32 * for failure.
33 *
34 * This test creates two subscriptions, both attached to the root window but
35 * each on a different X client. Since multiple subscriptions for the same
36 * window but a different client are expected to fail using the XI2.2-based
37 * grail back end, activating the second subscription is extected to resut in
38 * a failure.
39 *
40 * This behaviour is very dependent on the particular back end used and internal
41 * implementattion details of that back end. It is not a unit test or a
42 * reliable group or system test.
43 */
44TEST_F(GeisSubscriptionTests, duplicate_window_subscription)
45{
46 Geis geis2 = geis_new(GEIS_INIT_SYNCHRONOUS_START,
47 GEIS_INIT_NO_ATOMIC_GESTURES,
48 NULL);
49 EXPECT_TRUE(geis2 != NULL) << "can not create second geis instance";
50
51 GeisSubscription sub1 = geis_subscription_new(geis_,
52 "subscription 1",
53 GEIS_SUBSCRIPTION_NONE);
54 EXPECT_TRUE(sub1 != NULL) << "can not create first subscription";
55
56 GeisSubscription sub2 = geis_subscription_new(geis2,
57 "subscription 2",
58 GEIS_SUBSCRIPTION_NONE);
59 EXPECT_TRUE(sub1 != NULL) << "can not create second subscription";
60
61 GeisFilter filter1 = geis_filter_new(geis_, "root window 1");
62 EXPECT_TRUE(filter1 != NULL) << "can not create filter 1";
63
64 GeisStatus fs = geis_filter_add_term(filter1,
65 GEIS_FILTER_CLASS,
66 GEIS_CLASS_ATTRIBUTE_NAME, GEIS_FILTER_OP_EQ, GEIS_GESTURE_ROTATE,
67 GEIS_GESTURE_ATTRIBUTE_TOUCHES, GEIS_FILTER_OP_GT, 1,
68 NULL);
69 EXPECT_EQ(fs, GEIS_STATUS_SUCCESS) << "can not add class to filter 1";
70
71 GeisFilter filter2 = geis_filter_new(geis_, "root window 2");
72 EXPECT_TRUE(filter2 != NULL) << "can not create filter 2";
73
74 fs = geis_filter_add_term(filter2,
75 GEIS_FILTER_CLASS,
76 GEIS_CLASS_ATTRIBUTE_NAME, GEIS_FILTER_OP_EQ, GEIS_GESTURE_ROTATE,
77 GEIS_GESTURE_ATTRIBUTE_TOUCHES, GEIS_FILTER_OP_GT, 1,
78 NULL);
79 EXPECT_EQ(fs, GEIS_STATUS_SUCCESS) << "can not add class to filter 2";
80
81 fs = geis_subscription_add_filter(sub1, filter1);
82 EXPECT_EQ(fs, GEIS_STATUS_SUCCESS) << "can not subscribe filter 1";
83 fs = geis_subscription_add_filter(sub2, filter2);
84 EXPECT_EQ(fs, GEIS_STATUS_SUCCESS) << "can not subscribe filter 2";
85
86 EXPECT_EQ(GEIS_STATUS_SUCCESS, geis_subscription_activate(sub1))
87 << "can not activate subscription 1";
88 EXPECT_NE(GEIS_STATUS_SUCCESS, geis_subscription_activate(sub2))
89 << "mistakenly activated subscription 2";
90
91 geis_subscription_delete(sub2);
92 geis_subscription_delete(sub1);
93 geis_delete(geis2);
94}
95

Subscribers

People subscribed via source and target branches