blob: e8ec3138f6ea83b9f7a425bd238fb935350fb3b7 (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
137
/*
 * 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. It is one of the repository commands in
 * cmd.c, taking the newer revision from id, the older one from id2, and an
 * optional path that narrows the diff. A merge carries no single patch and so
 * 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.noplainemail) {
		rev_argv[FORMAT_ARG] =
			"--format=format:From %H Mon Sep 17 00:00:00 "
			"2001%nFrom: %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);
	}
}