PageRenderTime 27ms CodeModel.GetById 26ms RepoModel.GetById 0ms app.codeStats 0ms

/builtin/fmt-merge-msg.c

https://gitlab.com/nmusco/git
C | 711 lines | 607 code | 85 blank | 19 comment | 155 complexity | 72f448f5ef1498d12d04a41be6f7c552 MD5 | raw file
  1. #include "builtin.h"
  2. #include "cache.h"
  3. #include "commit.h"
  4. #include "diff.h"
  5. #include "revision.h"
  6. #include "tag.h"
  7. #include "string-list.h"
  8. #include "branch.h"
  9. #include "fmt-merge-msg.h"
  10. #include "gpg-interface.h"
  11. static const char * const fmt_merge_msg_usage[] = {
  12. N_("git fmt-merge-msg [-m <message>] [--log[=<n>]|--no-log] [--file <file>]"),
  13. NULL
  14. };
  15. static int use_branch_desc;
  16. int fmt_merge_msg_config(const char *key, const char *value, void *cb)
  17. {
  18. if (!strcmp(key, "merge.log") || !strcmp(key, "merge.summary")) {
  19. int is_bool;
  20. merge_log_config = git_config_bool_or_int(key, value, &is_bool);
  21. if (!is_bool && merge_log_config < 0)
  22. return error("%s: negative length %s", key, value);
  23. if (is_bool && merge_log_config)
  24. merge_log_config = DEFAULT_MERGE_LOG_LEN;
  25. } else if (!strcmp(key, "merge.branchdesc")) {
  26. use_branch_desc = git_config_bool(key, value);
  27. } else {
  28. return git_default_config(key, value, cb);
  29. }
  30. return 0;
  31. }
  32. /* merge data per repository where the merged tips came from */
  33. struct src_data {
  34. struct string_list branch, tag, r_branch, generic;
  35. int head_status;
  36. };
  37. struct origin_data {
  38. unsigned char sha1[20];
  39. unsigned is_local_branch:1;
  40. };
  41. static void init_src_data(struct src_data *data)
  42. {
  43. data->branch.strdup_strings = 1;
  44. data->tag.strdup_strings = 1;
  45. data->r_branch.strdup_strings = 1;
  46. data->generic.strdup_strings = 1;
  47. }
  48. static struct string_list srcs = STRING_LIST_INIT_DUP;
  49. static struct string_list origins = STRING_LIST_INIT_DUP;
  50. struct merge_parents {
  51. int alloc, nr;
  52. struct merge_parent {
  53. unsigned char given[20];
  54. unsigned char commit[20];
  55. unsigned char used;
  56. } *item;
  57. };
  58. /*
  59. * I know, I know, this is inefficient, but you won't be pulling and merging
  60. * hundreds of heads at a time anyway.
  61. */
  62. static struct merge_parent *find_merge_parent(struct merge_parents *table,
  63. unsigned char *given,
  64. unsigned char *commit)
  65. {
  66. int i;
  67. for (i = 0; i < table->nr; i++) {
  68. if (given && hashcmp(table->item[i].given, given))
  69. continue;
  70. if (commit && hashcmp(table->item[i].commit, commit))
  71. continue;
  72. return &table->item[i];
  73. }
  74. return NULL;
  75. }
  76. static void add_merge_parent(struct merge_parents *table,
  77. unsigned char *given,
  78. unsigned char *commit)
  79. {
  80. if (table->nr && find_merge_parent(table, given, commit))
  81. return;
  82. ALLOC_GROW(table->item, table->nr + 1, table->alloc);
  83. hashcpy(table->item[table->nr].given, given);
  84. hashcpy(table->item[table->nr].commit, commit);
  85. table->item[table->nr].used = 0;
  86. table->nr++;
  87. }
  88. static int handle_line(char *line, struct merge_parents *merge_parents)
  89. {
  90. int i, len = strlen(line);
  91. struct origin_data *origin_data;
  92. char *src, *origin;
  93. struct src_data *src_data;
  94. struct string_list_item *item;
  95. int pulling_head = 0;
  96. unsigned char sha1[20];
  97. if (len < 43 || line[40] != '\t')
  98. return 1;
  99. if (starts_with(line + 41, "not-for-merge"))
  100. return 0;
  101. if (line[41] != '\t')
  102. return 2;
  103. i = get_sha1_hex(line, sha1);
  104. if (i)
  105. return 3;
  106. if (!find_merge_parent(merge_parents, sha1, NULL))
  107. return 0; /* subsumed by other parents */
  108. origin_data = xcalloc(1, sizeof(struct origin_data));
  109. hashcpy(origin_data->sha1, sha1);
  110. if (line[len - 1] == '\n')
  111. line[len - 1] = 0;
  112. line += 42;
  113. /*
  114. * At this point, line points at the beginning of comment e.g.
  115. * "branch 'frotz' of git://that/repository.git".
  116. * Find the repository name and point it with src.
  117. */
  118. src = strstr(line, " of ");
  119. if (src) {
  120. *src = 0;
  121. src += 4;
  122. pulling_head = 0;
  123. } else {
  124. src = line;
  125. pulling_head = 1;
  126. }
  127. item = unsorted_string_list_lookup(&srcs, src);
  128. if (!item) {
  129. item = string_list_append(&srcs, src);
  130. item->util = xcalloc(1, sizeof(struct src_data));
  131. init_src_data(item->util);
  132. }
  133. src_data = item->util;
  134. if (pulling_head) {
  135. origin = src;
  136. src_data->head_status |= 1;
  137. } else if (starts_with(line, "branch ")) {
  138. origin_data->is_local_branch = 1;
  139. origin = line + 7;
  140. string_list_append(&src_data->branch, origin);
  141. src_data->head_status |= 2;
  142. } else if (starts_with(line, "tag ")) {
  143. origin = line;
  144. string_list_append(&src_data->tag, origin + 4);
  145. src_data->head_status |= 2;
  146. } else if (starts_with(line, "remote-tracking branch ")) {
  147. origin = line + strlen("remote-tracking branch ");
  148. string_list_append(&src_data->r_branch, origin);
  149. src_data->head_status |= 2;
  150. } else {
  151. origin = src;
  152. string_list_append(&src_data->generic, line);
  153. src_data->head_status |= 2;
  154. }
  155. if (!strcmp(".", src) || !strcmp(src, origin)) {
  156. int len = strlen(origin);
  157. if (origin[0] == '\'' && origin[len - 1] == '\'')
  158. origin = xmemdupz(origin + 1, len - 2);
  159. } else {
  160. char *new_origin = xmalloc(strlen(origin) + strlen(src) + 5);
  161. sprintf(new_origin, "%s of %s", origin, src);
  162. origin = new_origin;
  163. }
  164. if (strcmp(".", src))
  165. origin_data->is_local_branch = 0;
  166. string_list_append(&origins, origin)->util = origin_data;
  167. return 0;
  168. }
  169. static void print_joined(const char *singular, const char *plural,
  170. struct string_list *list, struct strbuf *out)
  171. {
  172. if (list->nr == 0)
  173. return;
  174. if (list->nr == 1) {
  175. strbuf_addf(out, "%s%s", singular, list->items[0].string);
  176. } else {
  177. int i;
  178. strbuf_addstr(out, plural);
  179. for (i = 0; i < list->nr - 1; i++)
  180. strbuf_addf(out, "%s%s", i > 0 ? ", " : "",
  181. list->items[i].string);
  182. strbuf_addf(out, " and %s", list->items[list->nr - 1].string);
  183. }
  184. }
  185. static void add_branch_desc(struct strbuf *out, const char *name)
  186. {
  187. struct strbuf desc = STRBUF_INIT;
  188. if (!read_branch_desc(&desc, name)) {
  189. const char *bp = desc.buf;
  190. while (*bp) {
  191. const char *ep = strchrnul(bp, '\n');
  192. if (*ep)
  193. ep++;
  194. strbuf_addf(out, " : %.*s", (int)(ep - bp), bp);
  195. bp = ep;
  196. }
  197. if (out->buf[out->len - 1] != '\n')
  198. strbuf_addch(out, '\n');
  199. }
  200. strbuf_release(&desc);
  201. }
  202. #define util_as_integral(elem) ((intptr_t)((elem)->util))
  203. static void record_person(int which, struct string_list *people,
  204. struct commit *commit)
  205. {
  206. char *name_buf, *name, *name_end;
  207. struct string_list_item *elem;
  208. const char *field;
  209. field = (which == 'a') ? "\nauthor " : "\ncommitter ";
  210. name = strstr(commit->buffer, field);
  211. if (!name)
  212. return;
  213. name += strlen(field);
  214. name_end = strchrnul(name, '<');
  215. if (*name_end)
  216. name_end--;
  217. while (isspace(*name_end) && name <= name_end)
  218. name_end--;
  219. if (name_end < name)
  220. return;
  221. name_buf = xmemdupz(name, name_end - name + 1);
  222. elem = string_list_lookup(people, name_buf);
  223. if (!elem) {
  224. elem = string_list_insert(people, name_buf);
  225. elem->util = (void *)0;
  226. }
  227. elem->util = (void*)(util_as_integral(elem) + 1);
  228. free(name_buf);
  229. }
  230. static int cmp_string_list_util_as_integral(const void *a_, const void *b_)
  231. {
  232. const struct string_list_item *a = a_, *b = b_;
  233. return util_as_integral(b) - util_as_integral(a);
  234. }
  235. static void add_people_count(struct strbuf *out, struct string_list *people)
  236. {
  237. if (people->nr == 1)
  238. strbuf_addf(out, "%s", people->items[0].string);
  239. else if (people->nr == 2)
  240. strbuf_addf(out, "%s (%d) and %s (%d)",
  241. people->items[0].string,
  242. (int)util_as_integral(&people->items[0]),
  243. people->items[1].string,
  244. (int)util_as_integral(&people->items[1]));
  245. else if (people->nr)
  246. strbuf_addf(out, "%s (%d) and others",
  247. people->items[0].string,
  248. (int)util_as_integral(&people->items[0]));
  249. }
  250. static void credit_people(struct strbuf *out,
  251. struct string_list *them,
  252. int kind)
  253. {
  254. const char *label;
  255. const char *me;
  256. if (kind == 'a') {
  257. label = "By";
  258. me = git_author_info(IDENT_NO_DATE);
  259. } else {
  260. label = "Via";
  261. me = git_committer_info(IDENT_NO_DATE);
  262. }
  263. if (!them->nr ||
  264. (them->nr == 1 &&
  265. me &&
  266. (me = skip_prefix(me, them->items->string)) != NULL &&
  267. skip_prefix(me, " <")))
  268. return;
  269. strbuf_addf(out, "\n%c %s ", comment_line_char, label);
  270. add_people_count(out, them);
  271. }
  272. static void add_people_info(struct strbuf *out,
  273. struct string_list *authors,
  274. struct string_list *committers)
  275. {
  276. if (authors->nr)
  277. qsort(authors->items,
  278. authors->nr, sizeof(authors->items[0]),
  279. cmp_string_list_util_as_integral);
  280. if (committers->nr)
  281. qsort(committers->items,
  282. committers->nr, sizeof(committers->items[0]),
  283. cmp_string_list_util_as_integral);
  284. credit_people(out, authors, 'a');
  285. credit_people(out, committers, 'c');
  286. }
  287. static void shortlog(const char *name,
  288. struct origin_data *origin_data,
  289. struct commit *head,
  290. struct rev_info *rev,
  291. struct fmt_merge_msg_opts *opts,
  292. struct strbuf *out)
  293. {
  294. int i, count = 0;
  295. struct commit *commit;
  296. struct object *branch;
  297. struct string_list subjects = STRING_LIST_INIT_DUP;
  298. struct string_list authors = STRING_LIST_INIT_DUP;
  299. struct string_list committers = STRING_LIST_INIT_DUP;
  300. int flags = UNINTERESTING | TREESAME | SEEN | SHOWN | ADDED;
  301. struct strbuf sb = STRBUF_INIT;
  302. const unsigned char *sha1 = origin_data->sha1;
  303. int limit = opts->shortlog_len;
  304. branch = deref_tag(parse_object(sha1), sha1_to_hex(sha1), 40);
  305. if (!branch || branch->type != OBJ_COMMIT)
  306. return;
  307. setup_revisions(0, NULL, rev, NULL);
  308. add_pending_object(rev, branch, name);
  309. add_pending_object(rev, &head->object, "^HEAD");
  310. head->object.flags |= UNINTERESTING;
  311. if (prepare_revision_walk(rev))
  312. die("revision walk setup failed");
  313. while ((commit = get_revision(rev)) != NULL) {
  314. struct pretty_print_context ctx = {0};
  315. if (commit->parents && commit->parents->next) {
  316. /* do not list a merge but count committer */
  317. if (opts->credit_people)
  318. record_person('c', &committers, commit);
  319. continue;
  320. }
  321. if (!count && opts->credit_people)
  322. /* the 'tip' committer */
  323. record_person('c', &committers, commit);
  324. if (opts->credit_people)
  325. record_person('a', &authors, commit);
  326. count++;
  327. if (subjects.nr > limit)
  328. continue;
  329. format_commit_message(commit, "%s", &sb, &ctx);
  330. strbuf_ltrim(&sb);
  331. if (!sb.len)
  332. string_list_append(&subjects,
  333. sha1_to_hex(commit->object.sha1));
  334. else
  335. string_list_append(&subjects, strbuf_detach(&sb, NULL));
  336. }
  337. if (opts->credit_people)
  338. add_people_info(out, &authors, &committers);
  339. if (count > limit)
  340. strbuf_addf(out, "\n* %s: (%d commits)\n", name, count);
  341. else
  342. strbuf_addf(out, "\n* %s:\n", name);
  343. if (origin_data->is_local_branch && use_branch_desc)
  344. add_branch_desc(out, name);
  345. for (i = 0; i < subjects.nr; i++)
  346. if (i >= limit)
  347. strbuf_addf(out, " ...\n");
  348. else
  349. strbuf_addf(out, " %s\n", subjects.items[i].string);
  350. clear_commit_marks((struct commit *)branch, flags);
  351. clear_commit_marks(head, flags);
  352. free_commit_list(rev->commits);
  353. rev->commits = NULL;
  354. rev->pending.nr = 0;
  355. string_list_clear(&authors, 0);
  356. string_list_clear(&committers, 0);
  357. string_list_clear(&subjects, 0);
  358. }
  359. static void fmt_merge_msg_title(struct strbuf *out,
  360. const char *current_branch) {
  361. int i = 0;
  362. char *sep = "";
  363. strbuf_addstr(out, "Merge ");
  364. for (i = 0; i < srcs.nr; i++) {
  365. struct src_data *src_data = srcs.items[i].util;
  366. const char *subsep = "";
  367. strbuf_addstr(out, sep);
  368. sep = "; ";
  369. if (src_data->head_status == 1) {
  370. strbuf_addstr(out, srcs.items[i].string);
  371. continue;
  372. }
  373. if (src_data->head_status == 3) {
  374. subsep = ", ";
  375. strbuf_addstr(out, "HEAD");
  376. }
  377. if (src_data->branch.nr) {
  378. strbuf_addstr(out, subsep);
  379. subsep = ", ";
  380. print_joined("branch ", "branches ", &src_data->branch,
  381. out);
  382. }
  383. if (src_data->r_branch.nr) {
  384. strbuf_addstr(out, subsep);
  385. subsep = ", ";
  386. print_joined("remote-tracking branch ", "remote-tracking branches ",
  387. &src_data->r_branch, out);
  388. }
  389. if (src_data->tag.nr) {
  390. strbuf_addstr(out, subsep);
  391. subsep = ", ";
  392. print_joined("tag ", "tags ", &src_data->tag, out);
  393. }
  394. if (src_data->generic.nr) {
  395. strbuf_addstr(out, subsep);
  396. print_joined("commit ", "commits ", &src_data->generic,
  397. out);
  398. }
  399. if (strcmp(".", srcs.items[i].string))
  400. strbuf_addf(out, " of %s", srcs.items[i].string);
  401. }
  402. if (!strcmp("master", current_branch))
  403. strbuf_addch(out, '\n');
  404. else
  405. strbuf_addf(out, " into %s\n", current_branch);
  406. }
  407. static void fmt_tag_signature(struct strbuf *tagbuf,
  408. struct strbuf *sig,
  409. const char *buf,
  410. unsigned long len)
  411. {
  412. const char *tag_body = strstr(buf, "\n\n");
  413. if (tag_body) {
  414. tag_body += 2;
  415. strbuf_add(tagbuf, tag_body, buf + len - tag_body);
  416. }
  417. strbuf_complete_line(tagbuf);
  418. if (sig->len) {
  419. strbuf_addch(tagbuf, '\n');
  420. strbuf_add_commented_lines(tagbuf, sig->buf, sig->len);
  421. }
  422. }
  423. static void fmt_merge_msg_sigs(struct strbuf *out)
  424. {
  425. int i, tag_number = 0, first_tag = 0;
  426. struct strbuf tagbuf = STRBUF_INIT;
  427. for (i = 0; i < origins.nr; i++) {
  428. unsigned char *sha1 = origins.items[i].util;
  429. enum object_type type;
  430. unsigned long size, len;
  431. char *buf = read_sha1_file(sha1, &type, &size);
  432. struct strbuf sig = STRBUF_INIT;
  433. if (!buf || type != OBJ_TAG)
  434. goto next;
  435. len = parse_signature(buf, size);
  436. if (size == len)
  437. ; /* merely annotated */
  438. else if (verify_signed_buffer(buf, len, buf + len, size - len, &sig, NULL)) {
  439. if (!sig.len)
  440. strbuf_addstr(&sig, "gpg verification failed.\n");
  441. }
  442. if (!tag_number++) {
  443. fmt_tag_signature(&tagbuf, &sig, buf, len);
  444. first_tag = i;
  445. } else {
  446. if (tag_number == 2) {
  447. struct strbuf tagline = STRBUF_INIT;
  448. strbuf_addch(&tagline, '\n');
  449. strbuf_add_commented_lines(&tagline,
  450. origins.items[first_tag].string,
  451. strlen(origins.items[first_tag].string));
  452. strbuf_insert(&tagbuf, 0, tagline.buf,
  453. tagline.len);
  454. strbuf_release(&tagline);
  455. }
  456. strbuf_addch(&tagbuf, '\n');
  457. strbuf_add_commented_lines(&tagbuf,
  458. origins.items[i].string,
  459. strlen(origins.items[i].string));
  460. fmt_tag_signature(&tagbuf, &sig, buf, len);
  461. }
  462. strbuf_release(&sig);
  463. next:
  464. free(buf);
  465. }
  466. if (tagbuf.len) {
  467. strbuf_addch(out, '\n');
  468. strbuf_addbuf(out, &tagbuf);
  469. }
  470. strbuf_release(&tagbuf);
  471. }
  472. static void find_merge_parents(struct merge_parents *result,
  473. struct strbuf *in, unsigned char *head)
  474. {
  475. struct commit_list *parents, *next;
  476. struct commit *head_commit;
  477. int pos = 0, i, j;
  478. parents = NULL;
  479. while (pos < in->len) {
  480. int len;
  481. char *p = in->buf + pos;
  482. char *newline = strchr(p, '\n');
  483. unsigned char sha1[20];
  484. struct commit *parent;
  485. struct object *obj;
  486. len = newline ? newline - p : strlen(p);
  487. pos += len + !!newline;
  488. if (len < 43 ||
  489. get_sha1_hex(p, sha1) ||
  490. p[40] != '\t' ||
  491. p[41] != '\t')
  492. continue; /* skip not-for-merge */
  493. /*
  494. * Do not use get_merge_parent() here; we do not have
  495. * "name" here and we do not want to contaminate its
  496. * util field yet.
  497. */
  498. obj = parse_object(sha1);
  499. parent = (struct commit *)peel_to_type(NULL, 0, obj, OBJ_COMMIT);
  500. if (!parent)
  501. continue;
  502. commit_list_insert(parent, &parents);
  503. add_merge_parent(result, obj->sha1, parent->object.sha1);
  504. }
  505. head_commit = lookup_commit(head);
  506. if (head_commit)
  507. commit_list_insert(head_commit, &parents);
  508. parents = reduce_heads(parents);
  509. while (parents) {
  510. for (i = 0; i < result->nr; i++)
  511. if (!hashcmp(result->item[i].commit,
  512. parents->item->object.sha1))
  513. result->item[i].used = 1;
  514. next = parents->next;
  515. free(parents);
  516. parents = next;
  517. }
  518. for (i = j = 0; i < result->nr; i++) {
  519. if (result->item[i].used) {
  520. if (i != j)
  521. result->item[j] = result->item[i];
  522. j++;
  523. }
  524. }
  525. result->nr = j;
  526. }
  527. int fmt_merge_msg(struct strbuf *in, struct strbuf *out,
  528. struct fmt_merge_msg_opts *opts)
  529. {
  530. int i = 0, pos = 0;
  531. unsigned char head_sha1[20];
  532. const char *current_branch;
  533. void *current_branch_to_free;
  534. struct merge_parents merge_parents;
  535. memset(&merge_parents, 0, sizeof(merge_parents));
  536. /* get current branch */
  537. current_branch = current_branch_to_free =
  538. resolve_refdup("HEAD", head_sha1, 1, NULL);
  539. if (!current_branch)
  540. die("No current branch");
  541. if (starts_with(current_branch, "refs/heads/"))
  542. current_branch += 11;
  543. find_merge_parents(&merge_parents, in, head_sha1);
  544. /* get a line */
  545. while (pos < in->len) {
  546. int len;
  547. char *newline, *p = in->buf + pos;
  548. newline = strchr(p, '\n');
  549. len = newline ? newline - p : strlen(p);
  550. pos += len + !!newline;
  551. i++;
  552. p[len] = 0;
  553. if (handle_line(p, &merge_parents))
  554. die ("Error in line %d: %.*s", i, len, p);
  555. }
  556. if (opts->add_title && srcs.nr)
  557. fmt_merge_msg_title(out, current_branch);
  558. if (origins.nr)
  559. fmt_merge_msg_sigs(out);
  560. if (opts->shortlog_len) {
  561. struct commit *head;
  562. struct rev_info rev;
  563. head = lookup_commit_or_die(head_sha1, "HEAD");
  564. init_revisions(&rev, NULL);
  565. rev.commit_format = CMIT_FMT_ONELINE;
  566. rev.ignore_merges = 1;
  567. rev.limited = 1;
  568. strbuf_complete_line(out);
  569. for (i = 0; i < origins.nr; i++)
  570. shortlog(origins.items[i].string,
  571. origins.items[i].util,
  572. head, &rev, opts, out);
  573. }
  574. strbuf_complete_line(out);
  575. free(current_branch_to_free);
  576. free(merge_parents.item);
  577. return 0;
  578. }
  579. int cmd_fmt_merge_msg(int argc, const char **argv, const char *prefix)
  580. {
  581. const char *inpath = NULL;
  582. const char *message = NULL;
  583. int shortlog_len = -1;
  584. struct option options[] = {
  585. { OPTION_INTEGER, 0, "log", &shortlog_len, N_("n"),
  586. N_("populate log with at most <n> entries from shortlog"),
  587. PARSE_OPT_OPTARG, NULL, DEFAULT_MERGE_LOG_LEN },
  588. { OPTION_INTEGER, 0, "summary", &shortlog_len, N_("n"),
  589. N_("alias for --log (deprecated)"),
  590. PARSE_OPT_OPTARG | PARSE_OPT_HIDDEN, NULL,
  591. DEFAULT_MERGE_LOG_LEN },
  592. OPT_STRING('m', "message", &message, N_("text"),
  593. N_("use <text> as start of message")),
  594. OPT_FILENAME('F', "file", &inpath, N_("file to read from")),
  595. OPT_END()
  596. };
  597. FILE *in = stdin;
  598. struct strbuf input = STRBUF_INIT, output = STRBUF_INIT;
  599. int ret;
  600. struct fmt_merge_msg_opts opts;
  601. git_config(fmt_merge_msg_config, NULL);
  602. argc = parse_options(argc, argv, prefix, options, fmt_merge_msg_usage,
  603. 0);
  604. if (argc > 0)
  605. usage_with_options(fmt_merge_msg_usage, options);
  606. if (shortlog_len < 0)
  607. shortlog_len = (merge_log_config > 0) ? merge_log_config : 0;
  608. if (inpath && strcmp(inpath, "-")) {
  609. in = fopen(inpath, "r");
  610. if (!in)
  611. die_errno("cannot open '%s'", inpath);
  612. }
  613. if (strbuf_read(&input, fileno(in), 0) < 0)
  614. die_errno("could not read input file");
  615. if (message)
  616. strbuf_addstr(&output, message);
  617. memset(&opts, 0, sizeof(opts));
  618. opts.add_title = !message;
  619. opts.credit_people = 1;
  620. opts.shortlog_len = shortlog_len;
  621. ret = fmt_merge_msg(&input, &output, &opts);
  622. if (ret)
  623. return ret;
  624. write_in_full(STDOUT_FILENO, output.buf, output.len);
  625. return 0;
  626. }