/* * The patch page, which serves a commit or a range of commits as plain text * in the mail format git format-patch writes, so that a change read in cgit * can be fed straight to git am. A merge carries no single patch and drops * out of a range, and max-patch-count bounds how many commits one request * may emit. */ #define USE_THE_REPOSITORY_VARIABLE #include "cgit.h" #include "html.h" #include "ui-diff.h" #include "ui-patch.h" #include "ui-shared.h" // Room for two hex object ids, the two dots between them, and the null byte. #define REV_RANGE_LEN (2 * GIT_MAX_HEXSZ + 3) // Where the format argument sits in the walk arguments below. #define FORMAT_ARG 2 /* * A root commit has no parent, so the old end is left null and the caller asks * for that one commit instead. */ static int resolve_range(const char *new_rev, const char *old_rev, struct object_id *new_oid, struct object_id *old_oid) { struct commit *commit; if (repo_get_oid(the_repository, new_rev, new_oid)) { cgit_print_error_page(404, "Not Found", "Bad object id: %s", new_rev); return -1; } commit = lookup_commit_reference(the_repository, new_oid); if (!commit) { cgit_print_error_page(404, "Not Found", "Bad commit reference: %s", new_rev); return -1; } if (old_rev) { if (repo_get_oid(the_repository, old_rev, old_oid)) { cgit_print_error_page(404, "Not Found", "Bad object id: %s", old_rev); return -1; } if (!lookup_commit_reference(the_repository, old_oid)) { cgit_print_error_page(404, "Not Found", "Bad commit reference: %s", old_rev); return -1; } } else if (commit->parents && commit->parents->item) { oidcpy(old_oid, &commit->parents->item->object.oid); } else { oidclr(old_oid, the_repository->hash_algo); } return 0; } void cgit_print_patch(const char *new_rev, const char *old_rev, const char *prefix) { struct rev_info rev; struct commit *commit; struct object_id new_oid, old_oid; char rev_range[REV_RANGE_LEN]; // setup_revisions reads these the way git reads a command line, so the // first entry stands in for the program name and is skipped, and the // array has to stay null terminated because the path after the double // dash is picked up past the count. const char *rev_argv[] = { NULL, "--reverse", "--format=email", rev_range, "--", prefix, NULL }; int rev_argc = ARRAY_SIZE(rev_argv) - 1; if (!prefix) rev_argc--; if (!new_rev) new_rev = ctx.qry.head; if (resolve_range(new_rev, old_rev, &new_oid, &old_oid)) return; if (is_null_oid(&old_oid)) { memcpy(rev_range, oid_to_hex(&new_oid), the_hash_algo->hexsz + 1); } else { xsnprintf(rev_range, REV_RANGE_LEN, "%s..%s", oid_to_hex(&old_oid), oid_to_hex(&new_oid)); } ctx.page.mimetype = "text/plain"; ctx.page.filename = cgit_fmt("%s.patch", rev_range); cgit_print_http_headers(); if (!ctx.cfg.enable_plain_email) { rev_argv[FORMAT_ARG] = "--format=format:From %H Mon Sep 17 00:00:00 2001%n" "From: %an%nDate: %aD%n%w(78,0,1)Subject: %s%n%n%w(0)%b"; } repo_init_revisions(the_repository, &rev, NULL); rev.abbrev = DEFAULT_ABBREV; rev.verbose_header = 1; rev.diff = 1; rev.show_root_diff = 1; rev.max_parents = 1; rev.diffopt.output_format |= DIFF_FORMAT_DIFFSTAT | DIFF_FORMAT_PATCH | DIFF_FORMAT_SUMMARY; if (prefix) // Allocated rather than formatted into cgit_fmt's fixed // buffer, because the path comes from the request and a long // one would abort the process here, with the headers for a // successful response already on the wire. rev.diffopt.stat_sep = cgit_fmtalloc("(limited to '%s')\n\n", prefix); setup_revisions(rev_argc, rev_argv, &rev, NULL); // A single commit resolves to a range starting at its parent, so this // only ever cuts an explicit range short and keeps one request from // emitting a patch for the whole history. if (ctx.cfg.max_patch_count > 0) rev.max_count = ctx.cfg.max_patch_count; prepare_revision_walk(&rev); while ((commit = get_revision(&rev)) != NULL) { log_tree_commit(&rev, commit); // Two dashes and a space is the mail signature separator, so // git am and mail readers drop the version note below rather // than carrying it into the commit message. printf("-- \ncgit %s\n\n", cgit_version); } }