/* * 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-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) 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; struct strvec rev_argv = STRVEC_INIT; char rev_range[REV_RANGE_LEN]; const char *version; if (!new_rev) new_rev = ctx.qry.head; // A leading colon makes git read the path as a pathspec with magic, // which can end the request inside git after the headers are out. if (prefix && prefix[0] == ':') { cgit_print_error_page(400, "Bad Request", "Invalid path"); return; } commit = cgit_get_commit(new_rev, &new_oid); if (!commit || cgit_get_old_oid(old_rev, commit, &old_oid)) return; if (is_null_oid(&old_oid)) { oid_to_hex_r(rev_range, &new_oid); } else { xsnprintf(rev_range, sizeof(rev_range), "%s..%s", oid_to_hex(&old_oid), oid_to_hex(&new_oid)); } strvec_pushl(&rev_argv, "patch", "--reverse", NULL); if (ctx.cfg.enable_plain_email) strvec_push(&rev_argv, "--format=email"); else strvec_push(&rev_argv, "--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"); strvec_pushl(&rev_argv, rev_range, "--", NULL); if (prefix) strvec_push(&rev_argv, prefix); // Git inlines a blob up to its own big-file threshold, far past what // any other view inflates, so max-blob-size is applied through it. if (ctx.cfg.max_blob_size) repo_settings_set_big_file_threshold(the_repository, (unsigned long)ctx.cfg.max_blob_size * 1024); repo_init_revisions(the_repository, &rev, NULL); 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; // 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. if (prefix) rev.diffopt.stat_sep = xstrfmt("(limited to '%s')\n\n", prefix); setup_revisions(rev_argv.nr, rev_argv.v, &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; // The headers wait until the walk has started. if (cgit_prepare_walk(&rev)) { strvec_clear(&rev_argv); return; } ctx.page.mimetype = "text/plain"; ctx.page.filename = cgit_fmt("%s.patch", rev_range); cgit_print_http_headers(); // git format-patch signs with its bare version number, so the v that // cgit's tags carry is left off to read the same way. if (!skip_prefix(cgit_version, "v", &version)) version = cgit_version; while ((commit = get_revision(&rev))) { 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. if (ctx.cfg.enable_version_info) printf("-- \ncgit %s\n\n", version); else printf("-- \ncgit\n\n"); } strvec_clear(&rev_argv); }