Line data Source code
1 : /*
2 : * Copyright (C) 2004-2025 Savoir-faire Linux Inc.
3 : *
4 : * This program is free software: you can redistribute it and/or modify
5 : * it under the terms of the GNU General Public License as published by
6 : * the Free Software Foundation, either version 3 of the License, or
7 : * (at your option) any later version.
8 : *
9 : * This program is distributed in the hope that it will be useful,
10 : * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 : * GNU General Public License for more details.
13 : *
14 : * You should have received a copy of the GNU General Public License
15 : * along with this program. If not, see <https://www.gnu.org/licenses/>.
16 : */
17 : #include "gitserver.h"
18 :
19 : #include "fileutils.h"
20 : #include "logger.h"
21 : #include "gittransport.h"
22 : #include "manager.h"
23 : #include <opendht/thread_pool.h>
24 : #include <dhtnet/multiplexed_socket.h>
25 : #include <fmt/compile.h>
26 :
27 : #include <charconv>
28 : #include <ctime>
29 : #include <fstream>
30 : #include <git2.h>
31 : #include <iomanip>
32 :
33 : using namespace std::string_view_literals;
34 : constexpr auto FLUSH_PKT = "0000"sv;
35 : constexpr auto NAK_PKT = "0008NAK\n"sv;
36 : constexpr auto DONE_CMD = "done\n"sv;
37 : constexpr auto WANT_CMD = "want"sv;
38 : constexpr auto HAVE_CMD = "have"sv;
39 : constexpr auto SERVER_CAPABILITIES = " HEAD\0side-band side-band-64k shallow no-progress include-tag"sv;
40 :
41 : namespace jami {
42 :
43 : class GitServer::Impl
44 : {
45 : public:
46 848 : Impl(const std::string& accountId,
47 : const std::string& repositoryId,
48 : const std::string& repository,
49 : const std::shared_ptr<dhtnet::ChannelSocket>& socket)
50 848 : : accountId_(accountId)
51 848 : , repositoryId_(repositoryId)
52 848 : , repository_(repository)
53 848 : , socket_(socket)
54 : {
55 3392 : JAMI_DEBUG("[Account {}] [Conversation {}] [GitServer {}] created", accountId_, repositoryId_, fmt::ptr(this));
56 : // Check at least if repository is correct
57 : git_repository* repo;
58 848 : if (git_repository_open(&repo, repository_.c_str()) != 0) {
59 0 : dht::ThreadPool::io().run([socket = socket_] { socket->shutdown(); });
60 0 : return;
61 : }
62 848 : git_repository_free(repo);
63 :
64 848 : socket_->setOnRecv([this](const uint8_t* buf, std::size_t len) {
65 5593 : std::lock_guard lk(destroyMtx_);
66 5591 : if (isDestroying_)
67 0 : return len;
68 5590 : if (parseOrder(std::string_view((const char*) buf, len)))
69 15492 : while (parseOrder())
70 : ;
71 5593 : return len;
72 5593 : });
73 0 : }
74 848 : ~Impl()
75 : {
76 848 : stop();
77 3392 : JAMI_DEBUG("[Account {}] [Conversation {}] [GitServer {}] destroyed", accountId_, repositoryId_, fmt::ptr(this));
78 848 : }
79 2123 : void stop()
80 : {
81 2123 : std::lock_guard lk(destroyMtx_);
82 2122 : if (isDestroying_.exchange(true)) {
83 1275 : socket_->setOnRecv({});
84 2548 : dht::ThreadPool::io().run([socket = socket_] { socket->shutdown(); });
85 : }
86 2123 : }
87 : bool parseOrder(std::string_view buf = {});
88 :
89 : void sendReferenceCapabilities(bool sendVersion = false);
90 : bool NAK();
91 : void ACKCommon();
92 : bool ACKFirst();
93 : void sendPackData();
94 : std::map<std::string, std::string> getParameters(std::string_view pkt_line);
95 :
96 : std::string accountId_ {};
97 : std::string repositoryId_ {};
98 : std::string repository_ {};
99 : std::shared_ptr<dhtnet::ChannelSocket> socket_ {};
100 : std::string wantedReference_ {};
101 : std::string common_ {};
102 : std::vector<std::string> haveRefs_ {};
103 : std::string cachedPkt_ {};
104 : std::mutex destroyMtx_ {};
105 : std::atomic_bool isDestroying_ {false};
106 : onFetchedCb onFetchedCb_ {};
107 : };
108 :
109 : bool
110 21081 : GitServer::Impl::parseOrder(std::string_view buf)
111 : {
112 21081 : std::string pkt = std::move(cachedPkt_);
113 21085 : if (!buf.empty())
114 5592 : pkt += buf;
115 :
116 : // Parse pkt len
117 : // Reference: https://github.com/git/git/blob/master/Documentation/technical/protocol-common.txt#L51
118 : // The first four bytes define the length of the packet and 0000 is a FLUSH pkt
119 :
120 21086 : unsigned int pkt_len = 0;
121 21086 : auto [p, ec] = std::from_chars(pkt.data(), pkt.data() + 4, pkt_len, 16);
122 21081 : if (ec != std::errc()) {
123 0 : JAMI_ERROR("[Account {}] [Conversation {}] [GitServer {}] Unable to parse packet size",
124 : accountId_,
125 : repositoryId_,
126 : fmt::ptr(this));
127 : }
128 21081 : if (pkt_len != pkt.size()) {
129 : // Store next packet part
130 18006 : if (pkt_len == 0) {
131 : // FLUSH_PKT
132 3632 : pkt_len = 4;
133 : }
134 18006 : cachedPkt_ = pkt.substr(pkt_len);
135 : }
136 :
137 21090 : auto pack = std::string_view(pkt).substr(4, pkt_len - 4);
138 21088 : if (pack == DONE_CMD) {
139 : // Reference:
140 : // https://github.com/git/git/blob/master/Documentation/technical/pack-protocol.txt#L390 Do
141 : // not do multi-ack, just send ACK + pack file
142 : // In case of no common base, send NAK
143 4456 : JAMI_LOG("[Account {}] [Conversation {}] [GitServer {}] Peer negotiation is done. Answering to want order",
144 : accountId_,
145 : repositoryId_,
146 : fmt::ptr(this));
147 : bool sendData;
148 1114 : if (common_.empty())
149 190 : sendData = NAK();
150 : else
151 924 : sendData = ACKFirst();
152 1114 : if (sendData)
153 1114 : sendPackData();
154 1114 : return !cachedPkt_.empty();
155 19971 : } else if (pack.empty()) {
156 3633 : if (!haveRefs_.empty()) {
157 : // Reference:
158 : // https://github.com/git/git/blob/master/Documentation/technical/pack-protocol.txt#L390
159 : // Do not do multi-ack, just send ACK + pack file In case of no common base ACK
160 560 : ACKCommon();
161 560 : NAK();
162 : }
163 3632 : return !cachedPkt_.empty();
164 : }
165 :
166 16337 : auto lim = pack.find(' ');
167 16339 : auto cmd = pack.substr(0, lim);
168 16338 : auto dat = (lim < pack.size()) ? pack.substr(lim + 1) : std::string_view {};
169 16336 : if (cmd == UPLOAD_PACK_CMD) {
170 : // Cf: https://github.com/git/git/blob/master/Documentation/technical/pack-protocol.txt#L166
171 : // References discovery
172 7844 : JAMI_LOG("[Account {}] [Conversation {}] [GitServer {}] Upload pack command detected.",
173 : accountId_,
174 : repositoryId_,
175 : fmt::ptr(this));
176 1961 : auto version = 1;
177 1961 : auto parameters = getParameters(dat);
178 1961 : auto versionIt = parameters.find("version");
179 1961 : bool sendVersion = false;
180 1961 : if (versionIt != parameters.end()) {
181 0 : auto [p, ec] = std::from_chars(versionIt->second.data(),
182 0 : versionIt->second.data() + versionIt->second.size(),
183 0 : version);
184 0 : if (ec == std::errc()) {
185 0 : sendVersion = true;
186 : } else {
187 0 : JAMI_WARNING("[Account {}] [Conversation {}] [GitServer {}] Invalid version detected: {}",
188 : accountId_,
189 : repositoryId_,
190 : fmt::ptr(this),
191 : versionIt->second);
192 : }
193 : }
194 1961 : if (version == 1) {
195 1961 : sendReferenceCapabilities(sendVersion);
196 : } else {
197 0 : JAMI_ERROR("[Account {}] [Conversation {}] [GitServer {}] That protocol version is not yet supported "
198 : "(version: {:d})",
199 : accountId_,
200 : repositoryId_,
201 : fmt::ptr(this),
202 : version);
203 : }
204 16337 : } else if (cmd == WANT_CMD) {
205 : // Reference:
206 : // https://github.com/git/git/blob/master/Documentation/technical/pack-protocol.txt#L229
207 : // TODO can have more want
208 1114 : wantedReference_ = dat.substr(0, 40);
209 4456 : JAMI_LOG("[Account {}] [Conversation {}] [GitServer {}] Peer want ref: {}",
210 : accountId_,
211 : repositoryId_,
212 : fmt::ptr(this),
213 : wantedReference_);
214 13264 : } else if (cmd == HAVE_CMD) {
215 13267 : const auto& commit = haveRefs_.emplace_back(dat.substr(0, 40));
216 13262 : if (common_.empty()) {
217 : // Detect first common commit
218 : // Reference:
219 : // https://github.com/git/git/blob/master/Documentation/technical/pack-protocol.txt#L390
220 : // TODO do not open repository every time
221 : git_repository* repo;
222 924 : if (git_repository_open(&repo, repository_.c_str()) != 0) {
223 0 : JAMI_WARNING("[Account {}] [Conversation {}] [GitServer {}] Unable to open {}",
224 : accountId_,
225 : repositoryId_,
226 : fmt::ptr(this),
227 : repository_);
228 0 : return !cachedPkt_.empty();
229 : }
230 924 : GitRepository rep {repo, git_repository_free};
231 : git_oid commit_id;
232 924 : if (git_oid_fromstr(&commit_id, commit.c_str()) == 0) {
233 : // Reference found
234 924 : common_ = commit;
235 : }
236 924 : }
237 : } else {
238 0 : JAMI_WARNING("[Account {}] [Conversation {}] [GitServer {}] Unwanted packet received: {}",
239 : accountId_,
240 : repositoryId_,
241 : fmt::ptr(this),
242 : pkt);
243 : }
244 16337 : return !cachedPkt_.empty();
245 21083 : }
246 :
247 : std::string
248 27453 : toGitHex(size_t value)
249 : {
250 109812 : return fmt::format(FMT_COMPILE("{:04x}"), value & 0x0FFFF);
251 : }
252 :
253 : void
254 1961 : GitServer::Impl::sendReferenceCapabilities(bool sendVersion)
255 : {
256 : // Get references
257 : // First, get the HEAD reference
258 : // https://github.com/git/git/blob/master/Documentation/technical/pack-protocol.txt#L166
259 : git_repository* repo;
260 1961 : if (git_repository_open(&repo, repository_.c_str()) != 0) {
261 0 : JAMI_WARNING("[Account {}] [Conversation {}] [GitServer {}] Unable to open {}",
262 : accountId_,
263 : repositoryId_,
264 : fmt::ptr(this),
265 : repository_);
266 0 : dht::ThreadPool::io().run([socket = socket_] { socket->shutdown(); });
267 0 : return;
268 : }
269 1961 : GitRepository rep {repo, git_repository_free};
270 :
271 : // Answer with the version number
272 : // **** When the client initially connects the server will immediately respond
273 : // **** with a version number (if "version=1" is sent as an Extra Parameter),
274 1961 : std::error_code ec;
275 1961 : if (sendVersion) {
276 0 : constexpr auto toSend = "000eversion 1\0"sv;
277 0 : socket_->write(reinterpret_cast<const unsigned char*>(toSend.data()), toSend.size(), ec);
278 0 : if (ec) {
279 0 : JAMI_WARNING("[Account {}] [Conversation {}] [GitServer {}] Unable to send data for {}: {}",
280 : accountId_,
281 : repositoryId_,
282 : fmt::ptr(this),
283 : repository_,
284 : ec.message());
285 0 : dht::ThreadPool::io().run([socket = socket_] { socket->shutdown(); });
286 0 : return;
287 : }
288 : }
289 :
290 : git_oid commit_id;
291 1961 : if (git_reference_name_to_id(&commit_id, rep.get(), "HEAD") < 0) {
292 0 : JAMI_ERROR("[Account {}] [Conversation {}] [GitServer {}] Unable to get reference for HEAD",
293 : accountId_,
294 : repositoryId_,
295 : fmt::ptr(this));
296 0 : dht::ThreadPool::io().run([socket = socket_] { socket->shutdown(); });
297 0 : return;
298 : }
299 1961 : std::string_view currentHead = git_oid_tostr_s(&commit_id);
300 :
301 : // Send references
302 1961 : std::ostringstream packet;
303 1961 : packet << toGitHex(5 + currentHead.size() + SERVER_CAPABILITIES.size());
304 1961 : packet << currentHead << SERVER_CAPABILITIES << "\n";
305 :
306 : // Now, add other references
307 : git_strarray refs;
308 1961 : if (git_reference_list(&refs, rep.get()) == 0) {
309 25884 : for (std::size_t i = 0; i < refs.count; ++i) {
310 23925 : std::string_view ref = refs.strings[i];
311 23923 : if (git_reference_name_to_id(&commit_id, rep.get(), ref.data()) < 0) {
312 0 : JAMI_WARNING("[Account {}] [Conversation {}] [GitServer {}] Unable to get reference for {}",
313 : accountId_,
314 : repositoryId_,
315 : fmt::ptr(this),
316 : ref);
317 0 : continue;
318 0 : }
319 23924 : currentHead = git_oid_tostr_s(&commit_id);
320 :
321 23917 : packet << toGitHex(6 /* size + space + \n */ + currentHead.size() + ref.size());
322 23921 : packet << currentHead << " " << ref << "\n";
323 : }
324 : }
325 1959 : git_strarray_dispose(&refs);
326 :
327 : // And add FLUSH
328 1961 : packet << FLUSH_PKT;
329 1961 : auto toSend = packet.str();
330 1961 : socket_->write(reinterpret_cast<const unsigned char*>(toSend.data()), toSend.size(), ec);
331 1961 : if (ec) {
332 0 : JAMI_WARNING("[Account {}] [Conversation {}] [GitServer {}] Unable to send data for {}: {}",
333 : accountId_,
334 : repositoryId_,
335 : fmt::ptr(this),
336 : repository_,
337 : ec.message());
338 0 : dht::ThreadPool::io().run([socket = socket_] { socket->shutdown(); });
339 : }
340 1961 : }
341 :
342 : void
343 560 : GitServer::Impl::ACKCommon()
344 : {
345 560 : std::error_code ec;
346 : // Ack common base
347 560 : if (!common_.empty()) {
348 1120 : auto toSend = fmt::format(FMT_COMPILE("{:04x}ACK {} continue\n"),
349 560 : 18 + common_.size() /* size + ACK + space * 2 + continue + \n */,
350 560 : common_);
351 560 : socket_->write(reinterpret_cast<const unsigned char*>(toSend.c_str()), toSend.size(), ec);
352 560 : if (ec) {
353 0 : JAMI_WARNING("[Account {}] [Conversation {}] [GitServer {}] Unable to send data for {}: {}",
354 : accountId_,
355 : repositoryId_,
356 : fmt::ptr(this),
357 : repository_,
358 : ec.message());
359 0 : dht::ThreadPool::io().run([socket = socket_] { socket->shutdown(); });
360 : }
361 560 : }
362 560 : }
363 :
364 : bool
365 924 : GitServer::Impl::ACKFirst()
366 : {
367 924 : std::error_code ec;
368 : // Ack common base
369 924 : if (!common_.empty()) {
370 1848 : auto toSend = fmt::format(FMT_COMPILE("{:04x}ACK {}\n"),
371 924 : 9 + common_.size() /* size + ACK + space + \n */,
372 924 : common_);
373 924 : socket_->write(reinterpret_cast<const unsigned char*>(toSend.c_str()), toSend.size(), ec);
374 924 : if (ec) {
375 0 : JAMI_WARNING("[Account {}] [Conversation {}] [GitServer {}] Unable to send data for {}: {}",
376 : accountId_,
377 : repositoryId_,
378 : fmt::ptr(this),
379 : repository_,
380 : ec.message());
381 0 : dht::ThreadPool::io().run([socket = socket_] { socket->shutdown(); });
382 0 : return false;
383 : }
384 924 : }
385 924 : return true;
386 : }
387 :
388 : bool
389 750 : GitServer::Impl::NAK()
390 : {
391 750 : std::error_code ec;
392 : // NAK
393 750 : socket_->write(reinterpret_cast<const unsigned char*>(NAK_PKT.data()), NAK_PKT.size(), ec);
394 750 : if (ec) {
395 0 : JAMI_WARNING("[Account {}] [Conversation {}] [GitServer {}] Unable to send data for {}: {}",
396 : accountId_,
397 : repositoryId_,
398 : fmt::ptr(this),
399 : repository_,
400 : ec.message());
401 0 : dht::ThreadPool::io().run([socket = socket_] { socket->shutdown(); });
402 0 : return false;
403 : }
404 750 : return true;
405 : }
406 :
407 : void
408 1114 : GitServer::Impl::sendPackData()
409 : {
410 : git_repository* repo_ptr;
411 1114 : if (git_repository_open(&repo_ptr, repository_.c_str()) != 0) {
412 0 : JAMI_WARNING("[Account {}] [Conversation {}] [GitServer {}] Unable to open {}",
413 : accountId_,
414 : repositoryId_,
415 : fmt::ptr(this),
416 : repository_);
417 0 : return;
418 : }
419 1114 : GitRepository repo {repo_ptr, git_repository_free};
420 :
421 : git_packbuilder* pb_ptr;
422 1114 : if (git_packbuilder_new(&pb_ptr, repo.get()) != 0) {
423 0 : JAMI_WARNING("[Account {}] [Conversation {}] [GitServer {}] Unable to open packbuilder for {}",
424 : accountId_,
425 : repositoryId_,
426 : fmt::ptr(this),
427 : repository_);
428 0 : return;
429 : }
430 1114 : GitPackBuilder pb {pb_ptr, git_packbuilder_free};
431 :
432 1114 : std::string fetched = wantedReference_;
433 : git_oid oid;
434 1114 : if (git_oid_fromstr(&oid, fetched.c_str()) < 0) {
435 0 : JAMI_ERROR("[Account {}] [Conversation {}] [GitServer {}] Unable to get reference for commit {}",
436 : accountId_,
437 : repositoryId_,
438 : fmt::ptr(this),
439 : fetched);
440 0 : return;
441 : }
442 :
443 1114 : git_revwalk* walker_ptr = nullptr;
444 1114 : if (git_revwalk_new(&walker_ptr, repo.get()) < 0 || git_revwalk_push(walker_ptr, &oid) < 0) {
445 0 : if (walker_ptr)
446 0 : git_revwalk_free(walker_ptr);
447 0 : return;
448 : }
449 1114 : GitRevWalker walker {walker_ptr, git_revwalk_free};
450 1114 : git_revwalk_sorting(walker.get(), GIT_SORT_TOPOLOGICAL);
451 : // Add first commit
452 1114 : std::set<std::string> parents;
453 1114 : auto haveCommit = false;
454 :
455 3062 : while (!git_revwalk_next(&oid, walker.get())) {
456 : // log until have refs
457 2871 : std::string id = git_oid_tostr_s(&oid);
458 2872 : haveCommit |= std::find(haveRefs_.begin(), haveRefs_.end(), id) != haveRefs_.end();
459 2872 : auto itParents = std::find(parents.begin(), parents.end(), id);
460 2871 : if (itParents != parents.end())
461 1758 : parents.erase(itParents);
462 2870 : if (haveCommit && parents.size() == 0 /* We are sure that all commits are there */)
463 924 : break;
464 1947 : if (git_packbuilder_insert_commit(pb.get(), &oid) != 0) {
465 0 : JAMI_WARNING("[Account {}] [Conversation {}] [GitServer {}] Unable to open insert commit {} for {}",
466 : accountId_,
467 : repositoryId_,
468 : fmt::ptr(this),
469 : git_oid_tostr_s(&oid),
470 : repository_);
471 0 : return;
472 : }
473 :
474 : // Get next commit to pack
475 : git_commit* commit_ptr;
476 1948 : if (git_commit_lookup(&commit_ptr, repo.get(), &oid) < 0) {
477 0 : JAMI_ERROR("[Account {}] [Conversation {}] [GitServer {}] Unable to look up current commit",
478 : accountId_,
479 : repositoryId_,
480 : fmt::ptr(this));
481 0 : return;
482 : }
483 1948 : GitCommit commit {commit_ptr, git_commit_free};
484 1947 : auto parentsCount = git_commit_parentcount(commit.get());
485 3712 : for (unsigned int p = 0; p < parentsCount; ++p) {
486 : // make sure to explore all branches
487 1764 : const git_oid* pid = git_commit_parent_id(commit.get(), p);
488 1764 : if (pid)
489 1764 : parents.emplace(git_oid_tostr_s(pid));
490 : }
491 2872 : }
492 :
493 1114 : git_buf data = {};
494 1114 : if (git_packbuilder_write_buf(&data, pb.get()) != 0) {
495 0 : JAMI_WARNING("[Account {}] [Conversation {}] [GitServer {}] Unable to write pack data for {}",
496 : accountId_,
497 : repositoryId_,
498 : fmt::ptr(this),
499 : repository_);
500 0 : return;
501 : }
502 :
503 1114 : std::size_t sent = 0;
504 1114 : std::size_t len = data.size;
505 1114 : std::error_code ec;
506 1114 : std::vector<uint8_t> toSendData;
507 : do {
508 : // cf https://github.com/git/git/blob/master/Documentation/technical/pack-protocol.txt#L166
509 : // In 'side-band-64k' mode it will send up to 65519 data bytes plus 1 control code, for a
510 : // total of up to 65520 bytes in a pkt-line.
511 1580 : std::size_t pkt_size = std::min(static_cast<std::size_t>(65515), len - sent);
512 1580 : std::string toSendHeader = toGitHex(pkt_size + 5);
513 1581 : toSendData.clear();
514 1579 : toSendData.reserve(pkt_size + 5);
515 1581 : toSendData.insert(toSendData.end(), toSendHeader.begin(), toSendHeader.end());
516 1581 : toSendData.push_back(0x1);
517 1581 : toSendData.insert(toSendData.end(), data.ptr + sent, data.ptr + sent + pkt_size);
518 :
519 1581 : socket_->write(reinterpret_cast<const unsigned char*>(toSendData.data()), toSendData.size(), ec);
520 1581 : if (ec) {
521 0 : JAMI_WARNING("[Account {}] [Conversation {}] [GitServer {}] Unable to send data for {}: {}",
522 : accountId_,
523 : repositoryId_,
524 : fmt::ptr(this),
525 : repository_,
526 : ec.message());
527 0 : git_buf_dispose(&data);
528 0 : return;
529 : }
530 1581 : sent += pkt_size;
531 3162 : } while (sent < len);
532 1114 : git_buf_dispose(&data);
533 1114 : toSendData = {};
534 :
535 : // And finish by a little FLUSH
536 1114 : socket_->write(reinterpret_cast<const uint8_t*>(FLUSH_PKT.data()), FLUSH_PKT.size(), ec);
537 1114 : if (ec) {
538 0 : JAMI_WARNING("[Account {}] [Conversation {}] [GitServer {}] Unable to send data for {}: {}",
539 : accountId_,
540 : repositoryId_,
541 : fmt::ptr(this),
542 : repository_,
543 : ec.message());
544 : }
545 :
546 : // Clear sent data
547 1114 : haveRefs_.clear();
548 1114 : wantedReference_.clear();
549 1114 : common_.clear();
550 1114 : if (onFetchedCb_)
551 1114 : onFetchedCb_(fetched);
552 1114 : }
553 :
554 : std::map<std::string, std::string>
555 1961 : GitServer::Impl::getParameters(std::string_view pkt_line)
556 : {
557 1961 : std::map<std::string, std::string> parameters;
558 1961 : std::string key, value;
559 1961 : auto isKey = true;
560 1961 : auto nullChar = 0;
561 221593 : for (auto letter : pkt_line) {
562 219632 : if (letter == '\0') {
563 : // parameters such as host or version are after the first \0
564 3922 : if (nullChar != 0 && !key.empty()) {
565 1961 : parameters.try_emplace(std::move(key), std::move(value));
566 : }
567 3922 : nullChar += 1;
568 3922 : isKey = true;
569 3922 : key.clear();
570 3922 : value.clear();
571 215710 : } else if (letter == '=') {
572 1961 : isKey = false;
573 213749 : } else if (nullChar != 0) {
574 133348 : if (isKey) {
575 7844 : key += letter;
576 : } else {
577 125504 : value += letter;
578 : }
579 : }
580 : }
581 3922 : return parameters;
582 1961 : }
583 :
584 848 : GitServer::GitServer(const std::string& accountId,
585 : const std::string& conversationId,
586 848 : const std::shared_ptr<dhtnet::ChannelSocket>& client)
587 : {
588 1696 : auto path = (fileutils::get_data_dir() / accountId / "conversations" / conversationId).string();
589 848 : pimpl_ = std::make_unique<GitServer::Impl>(accountId, conversationId, path, client);
590 848 : }
591 :
592 848 : GitServer::~GitServer()
593 : {
594 848 : stop();
595 848 : pimpl_.reset();
596 848 : }
597 :
598 : void
599 848 : GitServer::setOnFetched(const onFetchedCb& cb)
600 : {
601 848 : if (!pimpl_)
602 0 : return;
603 848 : pimpl_->onFetchedCb_ = cb;
604 : }
605 :
606 : void
607 1275 : GitServer::stop()
608 : {
609 1275 : pimpl_->stop();
610 1275 : }
611 :
612 : } // namespace jami
|