Blob


1 /*
2 * Copyright (c) 2018, 2019 Stefan Sperling <stsp@openbsd.org>
3 *
4 * Permission to use, copy, modify, and distribute this software for any
5 * purpose with or without fee is hereby granted, provided that the above
6 * copyright notice and this permission notice appear in all copies.
7 *
8 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
9 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
10 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
11 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
12 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
13 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
14 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
15 */
17 #include <sys/queue.h>
18 #include <sys/stat.h>
20 #include <sha1.h>
21 #include <string.h>
22 #include <stdio.h>
23 #include <stdlib.h>
24 #include <time.h>
25 #include <util.h>
26 #include <zlib.h>
28 #include "got_error.h"
29 #include "got_object.h"
30 #include "got_cancel.h"
31 #include "got_blame.h"
32 #include "got_commit_graph.h"
33 #include "got_opentemp.h"
35 #include "got_lib_inflate.h"
36 #include "got_lib_delta.h"
37 #include "got_lib_object.h"
38 #include "got_lib_diff.h"
40 struct got_blame_line {
41 int annotated;
42 struct got_object_id id;
43 };
45 struct got_blame {
46 FILE *f;
47 size_t filesize;
48 int nlines;
49 int nannotated;
50 struct got_blame_line *lines; /* one per line */
51 off_t *line_offsets; /* one per line */
52 int ncommits;
53 };
55 static const struct got_error *
56 annotate_line(struct got_blame *blame, int lineno, struct got_object_id *id,
57 const struct got_error *(*cb)(void *, int, int, struct got_object_id *),
58 void *arg)
59 {
60 const struct got_error *err = NULL;
61 struct got_blame_line *line;
63 if (lineno < 1 || lineno > blame->nlines)
64 return NULL;
66 line = &blame->lines[lineno - 1];
67 if (line->annotated)
68 return NULL;
70 memcpy(&line->id, id, sizeof(line->id));
71 line->annotated = 1;
72 blame->nannotated++;
73 if (cb)
74 err = cb(arg, blame->nlines, lineno, id);
75 return err;
76 }
78 static const struct got_error *
79 blame_changes(struct got_blame *blame, struct got_diff_changes *changes,
80 struct got_object_id *commit_id,
81 const struct got_error *(*cb)(void *, int, int, struct got_object_id *),
82 void *arg)
83 {
84 const struct got_error *err = NULL;
85 struct got_diff_change *change;
87 SIMPLEQ_FOREACH(change, &changes->entries, entry) {
88 int c = change->cv.c;
89 int d = change->cv.d;
90 int new_lineno = (c < d ? c : d);
91 int new_length = (c < d ? d - c + 1 : (c == d ? 1 : 0));
92 int ln;
94 for (ln = new_lineno; ln < new_lineno + new_length; ln++) {
95 err = annotate_line(blame, ln, commit_id, cb, arg);
96 if (err)
97 return err;
98 if (blame->nlines == blame->nannotated)
99 break;
103 return NULL;
106 static const struct got_error *
107 blame_commit(struct got_blame *blame, struct got_object_id *parent_id,
108 struct got_object_id *id, const char *path, struct got_repository *repo,
109 const struct got_error *(*cb)(void *, int, int, struct got_object_id *),
110 void *arg)
112 const struct got_error *err = NULL;
113 struct got_object *obj = NULL;
114 struct got_object_id *obj_id = NULL;
115 struct got_commit_object *commit = NULL;
116 struct got_blob_object *blob = NULL;
117 struct got_diff_changes *changes = NULL;
119 err = got_object_open_as_commit(&commit, repo, parent_id);
120 if (err)
121 return err;
123 err = got_object_id_by_path(&obj_id, repo, parent_id, path);
124 if (err) {
125 if (err->code == GOT_ERR_NO_TREE_ENTRY)
126 err = NULL;
127 goto done;
130 err = got_object_open(&obj, repo, obj_id);
131 if (err)
132 goto done;
134 if (obj->type != GOT_OBJ_TYPE_BLOB) {
135 err = got_error(GOT_ERR_OBJ_TYPE);
136 goto done;
139 err = got_object_blob_open(&blob, repo, obj, 8192);
140 if (err)
141 goto done;
143 if (fseek(blame->f, 0L, SEEK_SET) == -1) {
144 err = got_ferror(blame->f, GOT_ERR_IO);
145 goto done;
148 err = got_diff_blob_file_lines_changed(&changes, blob, blame->f,
149 blame->filesize);
150 if (err)
151 goto done;
153 if (changes) {
154 err = blame_changes(blame, changes, id, cb, arg);
155 got_diff_free_changes(changes);
156 } else if (cb)
157 err = cb(arg, blame->nlines, -1, id);
158 done:
159 if (commit)
160 got_object_commit_close(commit);
161 free(obj_id);
162 if (obj)
163 got_object_close(obj);
164 if (blob)
165 got_object_blob_close(blob);
166 return err;
169 static const struct got_error *
170 blame_close(struct got_blame *blame)
172 const struct got_error *err = NULL;
174 if (blame->f && fclose(blame->f) != 0)
175 err = got_error_from_errno("fclose");
176 free(blame->lines);
177 free(blame);
178 return err;
181 static const struct got_error *
182 blame_open(struct got_blame **blamep, const char *path,
183 struct got_object_id *start_commit_id, struct got_repository *repo,
184 const struct got_error *(*cb)(void *, int, int, struct got_object_id *),
185 void *arg, got_cancel_cb cancel_cb, void *cancel_arg)
187 const struct got_error *err = NULL;
188 struct got_object *obj = NULL;
189 struct got_object_id *obj_id = NULL;
190 struct got_blob_object *blob = NULL;
191 struct got_blame *blame = NULL;
192 struct got_object_id *id = NULL, *pid = NULL;
193 int lineno;
194 struct got_commit_graph *graph = NULL;
196 *blamep = NULL;
198 err = got_object_id_by_path(&obj_id, repo, start_commit_id, path);
199 if (err)
200 return err;
202 err = got_object_open(&obj, repo, obj_id);
203 if (err)
204 goto done;
206 if (obj->type != GOT_OBJ_TYPE_BLOB) {
207 err = got_error(GOT_ERR_OBJ_TYPE);
208 goto done;
211 err = got_object_blob_open(&blob, repo, obj, 8192);
212 if (err)
213 goto done;
215 blame = calloc(1, sizeof(*blame));
216 if (blame == NULL)
217 return got_error_from_errno("calloc");
219 blame->f = got_opentemp();
220 if (blame->f == NULL) {
221 err = got_error_from_errno("got_opentemp");
222 goto done;
224 err = got_object_blob_dump_to_file(&blame->filesize, &blame->nlines,
225 &blame->line_offsets, blame->f, blob);
226 if (err || blame->nlines == 0)
227 goto done;
229 /* Don't include \n at EOF in the blame line count. */
230 if (blame->line_offsets[blame->nlines - 1] == blame->filesize)
231 blame->nlines--;
233 blame->lines = calloc(blame->nlines, sizeof(*blame->lines));
234 if (blame->lines == NULL) {
235 err = got_error_from_errno("calloc");
236 goto done;
239 err = got_commit_graph_open(&graph, start_commit_id, path, 1, repo);
240 if (err)
241 return err;
242 err = got_commit_graph_iter_start(graph, start_commit_id, repo,
243 cancel_cb, cancel_arg);
244 if (err)
245 goto done;
246 id = start_commit_id;
247 for (;;) {
248 err = got_commit_graph_iter_next(&pid, graph);
249 if (err) {
250 if (err->code == GOT_ERR_ITER_COMPLETED) {
251 err = NULL;
252 break;
254 if (err->code != GOT_ERR_ITER_NEED_MORE)
255 break;
256 err = got_commit_graph_fetch_commits(graph, 1, repo,
257 cancel_cb, cancel_arg);
258 if (err)
259 break;
260 continue;
262 if (pid) {
263 err = blame_commit(blame, pid, id, path, repo, cb, arg);
264 if (err) {
265 if (err->code == GOT_ERR_ITER_COMPLETED)
266 err = NULL;
267 break;
269 if (blame->nannotated == blame->nlines)
270 break;
272 id = pid;
275 if (id && blame->nannotated < blame->nlines) {
276 /* Annotate remaining non-annotated lines with last commit. */
277 for (lineno = 1; lineno <= blame->nlines; lineno++) {
278 err = annotate_line(blame, lineno, id, cb, arg);
279 if (err)
280 goto done;
284 done:
285 if (graph)
286 got_commit_graph_close(graph);
287 free(obj_id);
288 if (obj)
289 got_object_close(obj);
290 if (blob)
291 got_object_blob_close(blob);
292 if (err) {
293 if (blame)
294 blame_close(blame);
295 } else
296 *blamep = blame;
298 return err;
301 const struct got_error *
302 got_blame(const char *path, struct got_object_id *commit_id,
303 struct got_repository *repo,
304 const struct got_error *(*cb)(void *, int, int, struct got_object_id *),
305 void *arg, got_cancel_cb cancel_cb, void* cancel_arg)
307 const struct got_error *err = NULL, *close_err = NULL;
308 struct got_blame *blame;
309 char *abspath;
311 if (asprintf(&abspath, "%s%s", path[0] == '/' ? "" : "/", path) == -1)
312 return got_error_from_errno2("asprintf", path);
314 err = blame_open(&blame, abspath, commit_id, repo, cb, arg,
315 cancel_cb, cancel_arg);
316 free(abspath);
317 if (blame)
318 close_err = blame_close(blame);
319 return err ? err : close_err;