blob: 1dcdfbef91af527288672f4a378b641508b04828 (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
126
127
128
129
130
131
132
133
134
135
136
/*
 * 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", "Not a commit: %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", "Not a commit: %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;

	// 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;
	}

	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));
	}

	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;
	// 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 = 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;
	// A failed setup leaves the walk holding freed commits, so it must
	// not be read from, and the headers wait until it has succeeded.
	if (prepare_revision_walk(&rev)) {
		cgit_print_error_page(500, "Internal Server Error", "Unable to read the history");
		return;
	}

	ctx.page.mimetype = "text/plain";
	ctx.page.filename = cgit_fmt("%s.patch", rev_range);
	cgit_print_http_headers();

	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.
		printf("-- \ncgit %s\n\n", cgit_version);
	}
}