Merge lp:~renatofilho/qtorganizer5-eds/tz-support into lp:qtorganizer5-eds

Proposed by Renato Araujo Oliveira Filho
Status: Merged
Approved by: Michael Sheldon
Approved revision: 48
Merged at revision: 45
Proposed branch: lp:~renatofilho/qtorganizer5-eds/tz-support
Merge into: lp:qtorganizer5-eds
Diff against target: 305 lines (+109/-35)
4 files modified
qorganizer/qorganizer-eds-engine.cpp (+55/-24)
qorganizer/qorganizer-eds-engine.h (+2/-1)
tests/unittest/event-test.cpp (+40/-0)
tests/unittest/recurrence-test.cpp (+12/-10)
To merge this branch: bzr merge lp:~renatofilho/qtorganizer5-eds/tz-support
Reviewer Review Type Date Requested Status
Michael Sheldon (community) Approve
PS Jenkins bot continuous-integration Approve
Review via email: mp+213714@code.launchpad.net

Commit message

Initial implementation of time zone support.

To post a comment you must log in.
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
46. By Renato Araujo Oliveira Filho

Fixed tzid set when time zone is null.

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Approve (continuous-integration)
47. By Renato Araujo Oliveira Filho

Fixed datetime conversion when using timezones.

Revision history for this message
Alan Pope 🍺🐧🐱 πŸ¦„ (popey) wrote :

Tested this on my desktop which has multiple calendars synced (personal and work) and all events (created by people in different timezones) appeared in the right place.

Revision history for this message
Renato Araujo Oliveira Filho (renatofilho) wrote :

Are there any related MPs required for this MP to build/function as expected? NO

Is your branch in sync with latest trunk (e.g. bzr pull lp:trunk -> no changes): YES

Did you perform an exploratory manual test run of your code change and any related functionality on device or emulator? YES

Did you successfully run all tests found in your component's Test Plan (https://wiki.ubuntu.com/Process/Merges/TestPlan/qtorganizer5-eds) on device or emulator? YES

If you changed the UI, was the change specified/approved by design? NO UI CHANGE

If you changed the packaging (debian), did you subscribe a core-dev to this MP? NO PACKAGE CHANGE

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Approve (continuous-integration)
48. By Renato Araujo Oliveira Filho

Fixed failing test when running on devices with different time zone.

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Approve (continuous-integration)
Revision history for this message
Michael Sheldon (michael-sheldon) wrote :

Did you perform an exploratory manual test run of the code change and any related functionality on device or emulator?

 * Yes, synced events now display with correct timezone

Did CI run pass? If not, please explain why.

 * Yes

Have you checked that submitter has accurately filled out the submitter checklist and has taken no shortcut?

 * Yes

review: Approve
49. By Renato Araujo Oliveira Filho

Fixed timezone name parse.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'qorganizer/qorganizer-eds-engine.cpp'
--- qorganizer/qorganizer-eds-engine.cpp 2014-03-06 21:31:03 +0000
+++ qorganizer/qorganizer-eds-engine.cpp 2014-04-03 21:15:35 +0000
@@ -34,9 +34,8 @@
3434
35#include <QtCore/qdebug.h>35#include <QtCore/qdebug.h>
36#include <QtCore/QPointer>36#include <QtCore/QPointer>
37#include <QtCore/qstringbuilder.h>
38#include <QtCore/quuid.h>
39#include <QtCore/QCoreApplication>37#include <QtCore/QCoreApplication>
38#include <QtCore/QTimeZone>
4039
41#include <QtOrganizer/QOrganizerEventAttendee>40#include <QtOrganizer/QOrganizerEventAttendee>
42#include <QtOrganizer/QOrganizerItemLocation>41#include <QtOrganizer/QOrganizerItemLocation>
@@ -1094,10 +1093,37 @@
1094 change->emitSignals(this);1093 change->emitSignals(this);
1095}1094}
10961095
1097QDateTime QOrganizerEDSEngine::fromIcalTime(struct icaltimetype value)1096QDateTime QOrganizerEDSEngine::fromIcalTime(struct icaltimetype value, const char *tzId)
1098{1097{
1099 uint tmTime = icaltime_as_timet(value);1098 uint tmTime;
1100 return QDateTime::fromTime_t(tmTime);1099
1100 if (tzId) {
1101 QByteArray tzName(tzId);
1102 // keep only the timezone name.
1103 tzName = tzName.replace("/freeassociation.sourceforge.net/Tzfile/", "");
1104 const icaltimezone *timezone = const_cast<const icaltimezone *>(icaltimezone_get_builtin_timezone(tzName.constData()));
1105 tmTime = icaltime_as_timet_with_zone(value, timezone);
1106 return QDateTime::fromTime_t(tmTime, QTimeZone(tzId));
1107 } else {
1108 tmTime = icaltime_as_timet(value);
1109 return QDateTime::fromTime_t(tmTime);
1110 }
1111}
1112
1113icaltimetype QOrganizerEDSEngine::fromQDateTime(const QDateTime &dateTime,
1114 bool allDay,
1115 QByteArray *tzId)
1116{
1117
1118 if (dateTime.timeSpec() == Qt::TimeZone) {
1119 const icaltimezone *timezone = 0;
1120 *tzId = dateTime.timeZone().id();
1121 timezone = const_cast<const icaltimezone *>(icaltimezone_get_builtin_timezone(tzId->constData()));
1122 return icaltime_from_timet_with_zone(dateTime.toTime_t(), allDay, timezone);
1123 } else {
1124 return icaltime_from_timet(dateTime.toTime_t(), allDay);
1125 }
1126
1101}1127}
11021128
1103void QOrganizerEDSEngine::parseStartTime(ECalComponent *comp, QOrganizerItem *item)1129void QOrganizerEDSEngine::parseStartTime(ECalComponent *comp, QOrganizerItem *item)
@@ -1106,7 +1132,7 @@
1106 e_cal_component_get_dtstart(comp, dt);1132 e_cal_component_get_dtstart(comp, dt);
1107 if (dt->value) {1133 if (dt->value) {
1108 QOrganizerEventTime etr = item->detail(QOrganizerItemDetail::TypeEventTime);1134 QOrganizerEventTime etr = item->detail(QOrganizerItemDetail::TypeEventTime);
1109 etr.setStartDateTime(fromIcalTime(*dt->value));1135 etr.setStartDateTime(fromIcalTime(*dt->value, dt->tzid));
1110 if (icaltime_is_date(*dt->value) != etr.isAllDay()) {1136 if (icaltime_is_date(*dt->value) != etr.isAllDay()) {
1111 etr.setAllDay(icaltime_is_date(*dt->value));1137 etr.setAllDay(icaltime_is_date(*dt->value));
1112 }1138 }
@@ -1121,7 +1147,7 @@
1121 e_cal_component_get_dtstart(comp, dt);1147 e_cal_component_get_dtstart(comp, dt);
1122 if (dt->value) {1148 if (dt->value) {
1123 QOrganizerTodoTime etr = item->detail(QOrganizerItemDetail::TypeTodoTime);1149 QOrganizerTodoTime etr = item->detail(QOrganizerItemDetail::TypeTodoTime);
1124 etr.setStartDateTime(fromIcalTime(*dt->value));1150 etr.setStartDateTime(fromIcalTime(*dt->value, dt->tzid));
1125 if (icaltime_is_date(*dt->value) != etr.isAllDay()) {1151 if (icaltime_is_date(*dt->value) != etr.isAllDay()) {
1126 etr.setAllDay(icaltime_is_date(*dt->value));1152 etr.setAllDay(icaltime_is_date(*dt->value));
1127 }1153 }
@@ -1136,7 +1162,7 @@
1136 e_cal_component_get_dtend(comp, dt);1162 e_cal_component_get_dtend(comp, dt);
1137 if (dt->value) {1163 if (dt->value) {
1138 QOrganizerEventTime etr = item->detail(QOrganizerItemDetail::TypeEventTime);1164 QOrganizerEventTime etr = item->detail(QOrganizerItemDetail::TypeEventTime);
1139 etr.setEndDateTime(fromIcalTime(*dt->value));1165 etr.setEndDateTime(fromIcalTime(*dt->value, dt->tzid));
1140 if (icaltime_is_date(*dt->value) != etr.isAllDay()) {1166 if (icaltime_is_date(*dt->value) != etr.isAllDay()) {
1141 etr.setAllDay(icaltime_is_date(*dt->value));1167 etr.setAllDay(icaltime_is_date(*dt->value));
1142 }1168 }
@@ -1217,7 +1243,7 @@
1217 e_cal_component_get_rdate_list(comp, &periodList);1243 e_cal_component_get_rdate_list(comp, &periodList);
1218 for(GSList *i = periodList; i != 0; i = i->next) {1244 for(GSList *i = periodList; i != 0; i = i->next) {
1219 ECalComponentPeriod *period = (ECalComponentPeriod*) i->data;1245 ECalComponentPeriod *period = (ECalComponentPeriod*) i->data;
1220 QDateTime dt = fromIcalTime(period->start);1246 QDateTime dt = fromIcalTime(period->start, 0);
1221 dates.insert(dt.date());1247 dates.insert(dt.date());
1222 //TODO: period.end, period.duration1248 //TODO: period.end, period.duration
1223 }1249 }
@@ -1235,7 +1261,7 @@
1235 e_cal_component_get_exdate_list(comp, &exdateList);1261 e_cal_component_get_exdate_list(comp, &exdateList);
1236 for(GSList *i = exdateList; i != 0; i = i->next) {1262 for(GSList *i = exdateList; i != 0; i = i->next) {
1237 ECalComponentDateTime* dateTime = (ECalComponentDateTime*) i->data;1263 ECalComponentDateTime* dateTime = (ECalComponentDateTime*) i->data;
1238 QDateTime dt = fromIcalTime(*dateTime->value);1264 QDateTime dt = fromIcalTime(*dateTime->value, dateTime->tzid);
1239 dates.insert(dt.date());1265 dates.insert(dt.date());
1240 }1266 }
1241 e_cal_component_free_exdate_list(exdateList);1267 e_cal_component_free_exdate_list(exdateList);
@@ -1280,7 +1306,7 @@
1280 if (rule->count > 0) {1306 if (rule->count > 0) {
1281 qRule.setLimit(rule->count);1307 qRule.setLimit(rule->count);
1282 } else {1308 } else {
1283 QDateTime dt = fromIcalTime(rule->until);1309 QDateTime dt = fromIcalTime(rule->until, 0);
1284 if (dt.isValid()) {1310 if (dt.isValid()) {
1285 qRule.setLimit(dt.date());1311 qRule.setLimit(dt.date());
1286 } else {1312 } else {
@@ -1346,7 +1372,7 @@
1346 e_cal_component_get_due(comp, &due);1372 e_cal_component_get_due(comp, &due);
1347 if (due.value) {1373 if (due.value) {
1348 QOrganizerTodoTime ttr = item->detail(QOrganizerItemDetail::TypeTodoTime);1374 QOrganizerTodoTime ttr = item->detail(QOrganizerItemDetail::TypeTodoTime);
1349 ttr.setDueDateTime(fromIcalTime(*due.value));1375 ttr.setDueDateTime(fromIcalTime(*due.value, due.tzid));
1350 if (icaltime_is_date(*due.value) != ttr.isAllDay()) {1376 if (icaltime_is_date(*due.value) != ttr.isAllDay()) {
1351 ttr.setAllDay(icaltime_is_date(*due.value));1377 ttr.setAllDay(icaltime_is_date(*due.value));
1352 }1378 }
@@ -1488,7 +1514,7 @@
1488 e_cal_component_get_dtstart(comp, &dt);1514 e_cal_component_get_dtstart(comp, &dt);
1489 if (dt.value) {1515 if (dt.value) {
1490 QOrganizerJournalTime jtime;1516 QOrganizerJournalTime jtime;
1491 jtime.setEntryDateTime(fromIcalTime(*dt.value));1517 jtime.setEntryDateTime(fromIcalTime(*dt.value, dt.tzid));
1492 journal->saveDetail(&jtime);1518 journal->saveDetail(&jtime);
1493 }1519 }
1494 e_cal_component_free_datetime(&dt);1520 e_cal_component_free_datetime(&dt);
@@ -1692,9 +1718,10 @@
1692{1718{
1693 QOrganizerEventTime etr = item.detail(QOrganizerItemDetail::TypeEventTime);1719 QOrganizerEventTime etr = item.detail(QOrganizerItemDetail::TypeEventTime);
1694 if (!etr.isEmpty()) {1720 if (!etr.isEmpty()) {
1695 struct icaltimetype ict = icaltime_from_timet(etr.startDateTime().toTime_t(), etr.isAllDay());1721 QByteArray tzId;
1722 struct icaltimetype ict = fromQDateTime(etr.startDateTime(), etr.isAllDay(), &tzId);
1696 ECalComponentDateTime dt;1723 ECalComponentDateTime dt;
1697 dt.tzid = NULL;1724 dt.tzid = tzId.isEmpty() ? NULL : tzId.constData();
1698 dt.value = &ict;1725 dt.value = &ict;
1699 e_cal_component_set_dtstart(comp, &dt);1726 e_cal_component_set_dtstart(comp, &dt);
1700 }1727 }
@@ -1704,9 +1731,10 @@
1704{1731{
1705 QOrganizerEventTime etr = item.detail(QOrganizerItemDetail::TypeEventTime);1732 QOrganizerEventTime etr = item.detail(QOrganizerItemDetail::TypeEventTime);
1706 if (!etr.isEmpty()) {1733 if (!etr.isEmpty()) {
1707 struct icaltimetype ict = icaltime_from_timet(etr.endDateTime().toTime_t(), etr.isAllDay());1734 QByteArray tzId;
1735 struct icaltimetype ict = fromQDateTime(etr.endDateTime(), etr.isAllDay(), &tzId);
1708 ECalComponentDateTime dt;1736 ECalComponentDateTime dt;
1709 dt.tzid = NULL;1737 dt.tzid = tzId.isEmpty() ? NULL : tzId.constData();
1710 dt.value = &ict;1738 dt.value = &ict;
1711 e_cal_component_set_dtend(comp, &dt);1739 e_cal_component_set_dtend(comp, &dt);
1712 }1740 }
@@ -1716,9 +1744,10 @@
1716{1744{
1717 QOrganizerTodoTime etr = item.detail(QOrganizerItemDetail::TypeTodoTime);1745 QOrganizerTodoTime etr = item.detail(QOrganizerItemDetail::TypeTodoTime);
1718 if (!etr.isEmpty()) {1746 if (!etr.isEmpty()) {
1719 struct icaltimetype ict = icaltime_from_timet(etr.startDateTime().toTime_t(), etr.isAllDay());1747 QByteArray tzId;
1748 struct icaltimetype ict = fromQDateTime(etr.startDateTime(), etr.isAllDay(), &tzId);
1720 ECalComponentDateTime dt;1749 ECalComponentDateTime dt;
1721 dt.tzid = NULL;1750 dt.tzid = tzId.isEmpty() ? NULL : tzId.constData();
1722 dt.value = &ict;1751 dt.value = &ict;
1723 e_cal_component_set_dtstart(comp, &dt);;1752 e_cal_component_set_dtstart(comp, &dt);;
1724 }1753 }
@@ -1888,9 +1917,10 @@
1888{1917{
1889 QOrganizerTodoTime ttr = item.detail(QOrganizerItemDetail::TypeTodoTime);1918 QOrganizerTodoTime ttr = item.detail(QOrganizerItemDetail::TypeTodoTime);
1890 if (!ttr.isEmpty()) {1919 if (!ttr.isEmpty()) {
1891 struct icaltimetype ict = icaltime_from_timet(ttr.dueDateTime().toTime_t(), ttr.isAllDay());1920 QByteArray tzId;
1921 struct icaltimetype ict = fromQDateTime(ttr.dueDateTime(), ttr.isAllDay(), &tzId);
1892 ECalComponentDateTime dt;1922 ECalComponentDateTime dt;
1893 dt.tzid = NULL;1923 dt.tzid = tzId.isEmpty() ? NULL : tzId.constData();
1894 dt.value = &ict;1924 dt.value = &ict;
1895 e_cal_component_set_due(comp, &dt);;1925 e_cal_component_set_due(comp, &dt);;
1896 }1926 }
@@ -2065,9 +2095,10 @@
20652095
2066 QOrganizerJournalTime jtime = item.detail(QOrganizerItemDetail::TypeJournalTime);2096 QOrganizerJournalTime jtime = item.detail(QOrganizerItemDetail::TypeJournalTime);
2067 if (!jtime.isEmpty()) {2097 if (!jtime.isEmpty()) {
2068 struct icaltimetype ict = icaltime_from_timet(jtime.entryDateTime().toTime_t(), FALSE);2098 QByteArray tzId;
2099 struct icaltimetype ict = fromQDateTime(jtime.entryDateTime(), false, &tzId);
2069 ECalComponentDateTime dt;2100 ECalComponentDateTime dt;
2070 dt.tzid = NULL;2101 dt.tzid = tzId.isEmpty() ? NULL : tzId.constData();
2071 dt.value = &ict;2102 dt.value = &ict;
2072 e_cal_component_set_dtstart(comp, &dt);2103 e_cal_component_set_dtstart(comp, &dt);
2073 }2104 }
20742105
=== modified file 'qorganizer/qorganizer-eds-engine.h'
--- qorganizer/qorganizer-eds-engine.h 2014-02-25 17:12:32 +0000
+++ qorganizer/qorganizer-eds-engine.h 2014-04-03 21:15:35 +0000
@@ -190,7 +190,8 @@
190 static void parseStatus(ECalComponent *comp, QtOrganizer::QOrganizerItem *item);190 static void parseStatus(ECalComponent *comp, QtOrganizer::QOrganizerItem *item);
191 static void parseAttendeeList(ECalComponent *comp, QtOrganizer::QOrganizerItem *item);191 static void parseAttendeeList(ECalComponent *comp, QtOrganizer::QOrganizerItem *item);
192192
193 static QDateTime fromIcalTime(struct icaltimetype value);193 static QDateTime fromIcalTime(struct icaltimetype value, const char *tzId);
194 static icaltimetype fromQDateTime(const QDateTime &dateTime, bool allDay, QByteArray *tzId);
194195
195 static QtOrganizer::QOrganizerItem *parseEvent(ECalComponent *comp);196 static QtOrganizer::QOrganizerItem *parseEvent(ECalComponent *comp);
196 static QtOrganizer::QOrganizerItem *parseToDo(ECalComponent *comp);197 static QtOrganizer::QOrganizerItem *parseToDo(ECalComponent *comp);
197198
=== modified file 'tests/unittest/event-test.cpp'
--- tests/unittest/event-test.cpp 2014-02-28 16:05:35 +0000
+++ tests/unittest/event-test.cpp 2014-04-03 21:15:35 +0000
@@ -512,6 +512,46 @@
512 QOrganizerTodo newTodo = static_cast<QOrganizerTodo>(items[0]);512 QOrganizerTodo newTodo = static_cast<QOrganizerTodo>(items[0]);
513 QCOMPARE(newTodo.startDateTime(), startDateTime);513 QCOMPARE(newTodo.startDateTime(), startDateTime);
514 }514 }
515
516 void testCreateWithDiffTimeZone()
517 {
518 static QString displayLabelValue = QStringLiteral("Event with diff timezone");
519 static QString descriptionValue = QStringLiteral("Event with diff timezone description");
520 static QDateTime startDateTime = QDateTime(QDate(2013, 9, 3), QTime(0, 30, 0), QTimeZone("Asia/Bangkok"));
521
522 // create a new item
523 QOrganizerTodo todo;
524 todo.setCollectionId(m_collection.id());
525 todo.setStartDateTime(startDateTime);
526 todo.setDisplayLabel(displayLabelValue);
527 todo.setDescription(descriptionValue);
528
529 QtOrganizer::QOrganizerManager::Error error;
530 QMap<int, QtOrganizer::QOrganizerManager::Error> errorMap;
531 QList<QOrganizerItem> items;
532 items << todo;
533 bool saveResult = m_engine->saveItems(&items,
534 QList<QtOrganizer::QOrganizerItemDetail::DetailType>(),
535 &errorMap,
536 &error);
537 QVERIFY(saveResult);
538 QCOMPARE(error, QOrganizerManager::NoError);
539 QVERIFY(errorMap.isEmpty());
540
541 // query by the new item
542 QOrganizerItemFetchHint hint;
543 QList<QOrganizerItemId> ids;
544 ids << items[0].id();
545 items = m_engine->items(ids, hint, &errorMap, &error);
546 QCOMPARE(items.count(), 1);
547
548 // compare start datetime
549 QOrganizerTodo newTodo = static_cast<QOrganizerTodo>(items[0]);
550 QDateTime newStartDateTime = newTodo.startDateTime();
551 QCOMPARE(newStartDateTime.timeSpec(), Qt::TimeZone);
552 QCOMPARE(newStartDateTime.timeZone(), QTimeZone("Asia/Bangkok"));
553 QCOMPARE(newTodo.startDateTime(), startDateTime);
554 }
515};555};
516556
517const QString EventTest::defaultCollectionName = QStringLiteral("TEST_EVENT_COLLECTION");557const QString EventTest::defaultCollectionName = QStringLiteral("TEST_EVENT_COLLECTION");
518558
=== modified file 'tests/unittest/recurrence-test.cpp'
--- tests/unittest/recurrence-test.cpp 2014-03-06 21:31:03 +0000
+++ tests/unittest/recurrence-test.cpp 2014-04-03 21:15:35 +0000
@@ -160,11 +160,13 @@
160 {160 {
161 static QString displayLabelValue = QStringLiteral("Monthly test");161 static QString displayLabelValue = QStringLiteral("Monthly test");
162 static QString descriptionValue = QStringLiteral("Monthly description");162 static QString descriptionValue = QStringLiteral("Monthly description");
163 static QDateTime eventStartDate = QDateTime(QDate(2013, 1, 1), QTime(0, 0, 0), Qt::UTC);
164 static QDateTime eventEndDate = QDateTime(QDate(2013, 1, 1), QTime(0, 30, 0), Qt::UTC);
163165
164 QOrganizerEvent ev;166 QOrganizerEvent ev;
165 ev.setCollectionId(m_collection.id());167 ev.setCollectionId(m_collection.id());
166 ev.setStartDateTime(QDateTime(QDate(2013, 1, 1), QTime(0,0,0)));168 ev.setStartDateTime(eventStartDate);
167 ev.setEndDateTime(QDateTime(QDate(2013, 1, 1), QTime(0,30,0)));169 ev.setEndDateTime(eventEndDate);
168 ev.setDisplayLabel(displayLabelValue);170 ev.setDisplayLabel(displayLabelValue);
169 ev.setDescription(descriptionValue);171 ev.setDescription(descriptionValue);
170172
@@ -192,19 +194,19 @@
192 QOrganizerItemFetchHint hint;194 QOrganizerItemFetchHint hint;
193 QOrganizerItemFilter filter;195 QOrganizerItemFilter filter;
194 items = m_engine->items(filter,196 items = m_engine->items(filter,
195 QDateTime(QDate(2013, 1, 1), QTime(0,0,0)),197 eventStartDate,
196 QDateTime(QDate(2014, 1, 1), QTime(0,0,0)),198 eventStartDate.addYears(1),
197 100,199 100,
198 sort,200 sort,
199 hint,201 hint,
200 &error);202 &error);
201 QCOMPARE(items.count(), 24);203 QCOMPARE(items.count(), 24);
202 for(int i=0; i < 12; i++) {204 for(int i=0; i < 12; i++) {
203 QOrganizerEventTime time = items[i*2].detail(QOrganizerItemDetail::TypeEventTime);205 QOrganizerEventTime time = items[i*2].detail(QOrganizerItemDetail::TypeEventTime);
204 QCOMPARE(time.startDateTime(), QDateTime(QDate(2013, i+1, 1), QTime(0,0,0)));206 QCOMPARE(time.startDateTime(), eventStartDate.addMonths(i));
205207
206 time = items[(i*2)+1].detail(QOrganizerItemDetail::TypeEventTime);208 time = items[(i*2)+1].detail(QOrganizerItemDetail::TypeEventTime);
207 QCOMPARE(time.startDateTime(), QDateTime(QDate(2013, i+1, 5), QTime(0,0,0)));209 QCOMPARE(time.startDateTime(), QDateTime(QDate(2013, i+1, 5), QTime(0,0,0), Qt::UTC));
208 }210 }
209 }211 }
210212

Subscribers

People subscribed via source and target branches