blob: baa87c94a8901cf8df2a6efa02e0e5e90c485a45 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
/*
 * 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);
	}
}