|
| 1 | +#include "git-compat-util.h" |
| 2 | +#include "builtin.h" |
| 3 | +#include "commit.h" |
| 4 | +#include "config.h" |
| 5 | +#include "diff.h" |
| 6 | +#include "diffcore.h" |
| 7 | +#include "hashmap.h" |
| 8 | +#include "hex.h" |
| 9 | +#include "log-tree.h" |
| 10 | +#include "object-name.h" |
| 11 | +#include "object.h" |
| 12 | +#include "parse-options.h" |
| 13 | +#include "quote.h" |
| 14 | +#include "repository.h" |
| 15 | +#include "revision.h" |
| 16 | + |
| 17 | +struct last_modified_entry { |
| 18 | + struct hashmap_entry hashent; |
| 19 | + struct object_id oid; |
| 20 | + const char path[FLEX_ARRAY]; |
| 21 | +}; |
| 22 | + |
| 23 | +static int last_modified_entry_hashcmp(const void *unused UNUSED, |
| 24 | + const struct hashmap_entry *hent1, |
| 25 | + const struct hashmap_entry *hent2, |
| 26 | + const void *path) |
| 27 | +{ |
| 28 | + const struct last_modified_entry *ent1 = |
| 29 | + container_of(hent1, const struct last_modified_entry, hashent); |
| 30 | + const struct last_modified_entry *ent2 = |
| 31 | + container_of(hent2, const struct last_modified_entry, hashent); |
| 32 | + return strcmp(ent1->path, path ? path : ent2->path); |
| 33 | +} |
| 34 | + |
| 35 | +struct last_modified { |
| 36 | + struct hashmap paths; |
| 37 | + struct rev_info rev; |
| 38 | + int recursive; |
| 39 | + int tree_in_recursive; |
| 40 | +}; |
| 41 | + |
| 42 | +static void last_modified_release(struct last_modified *lm) |
| 43 | +{ |
| 44 | + hashmap_clear_and_free(&lm->paths, struct last_modified_entry, hashent); |
| 45 | + release_revisions(&lm->rev); |
| 46 | +} |
| 47 | + |
| 48 | +struct last_modified_callback_data { |
| 49 | + struct last_modified *lm; |
| 50 | + struct commit *commit; |
| 51 | +}; |
| 52 | + |
| 53 | +static void add_path_from_diff(struct diff_queue_struct *q, |
| 54 | + struct diff_options *opt UNUSED, void *data) |
| 55 | +{ |
| 56 | + struct last_modified *lm = data; |
| 57 | + |
| 58 | + for (int i = 0; i < q->nr; i++) { |
| 59 | + struct diff_filepair *p = q->queue[i]; |
| 60 | + struct last_modified_entry *ent; |
| 61 | + const char *path = p->two->path; |
| 62 | + |
| 63 | + FLEX_ALLOC_STR(ent, path, path); |
| 64 | + oidcpy(&ent->oid, &p->two->oid); |
| 65 | + hashmap_entry_init(&ent->hashent, strhash(ent->path)); |
| 66 | + hashmap_add(&lm->paths, &ent->hashent); |
| 67 | + } |
| 68 | +} |
| 69 | + |
| 70 | +static int populate_paths_from_revs(struct last_modified *lm) |
| 71 | +{ |
| 72 | + int num_interesting = 0; |
| 73 | + struct diff_options diffopt; |
| 74 | + |
| 75 | + memcpy(&diffopt, &lm->rev.diffopt, sizeof(diffopt)); |
| 76 | + copy_pathspec(&diffopt.pathspec, &lm->rev.diffopt.pathspec); |
| 77 | + /* |
| 78 | + * Use a callback to populate the paths from revs |
| 79 | + */ |
| 80 | + diffopt.output_format = DIFF_FORMAT_CALLBACK; |
| 81 | + diffopt.format_callback = add_path_from_diff; |
| 82 | + diffopt.format_callback_data = lm; |
| 83 | + |
| 84 | + for (size_t i = 0; i < lm->rev.pending.nr; i++) { |
| 85 | + struct object_array_entry *obj = lm->rev.pending.objects + i; |
| 86 | + |
| 87 | + if (obj->item->flags & UNINTERESTING) |
| 88 | + continue; |
| 89 | + |
| 90 | + if (num_interesting++) |
| 91 | + return error(_("last-modified can only operate on one tree at a time")); |
| 92 | + |
| 93 | + diff_tree_oid(lm->rev.repo->hash_algo->empty_tree, |
| 94 | + &obj->item->oid, "", &diffopt); |
| 95 | + diff_flush(&diffopt); |
| 96 | + } |
| 97 | + diff_free(&diffopt); |
| 98 | + |
| 99 | + return 0; |
| 100 | +} |
| 101 | + |
| 102 | +static void last_modified_emit(struct last_modified *lm, |
| 103 | + const char *path, const struct commit *commit) |
| 104 | + |
| 105 | +{ |
| 106 | + if (commit->object.flags & BOUNDARY) |
| 107 | + putchar('^'); |
| 108 | + printf("%s\t", oid_to_hex(&commit->object.oid)); |
| 109 | + |
| 110 | + if (lm->rev.diffopt.line_termination) |
| 111 | + write_name_quoted(path, stdout, '\n'); |
| 112 | + else |
| 113 | + printf("%s%c", path, '\0'); |
| 114 | + |
| 115 | + fflush(stdout); |
| 116 | +} |
| 117 | + |
| 118 | +static void mark_path(const char *path, const struct object_id *oid, |
| 119 | + struct last_modified_callback_data *data) |
| 120 | +{ |
| 121 | + struct last_modified_entry *ent; |
| 122 | + |
| 123 | + /* Is it even a path that we are interested in? */ |
| 124 | + ent = hashmap_get_entry_from_hash(&data->lm->paths, strhash(path), path, |
| 125 | + struct last_modified_entry, hashent); |
| 126 | + if (!ent) |
| 127 | + return; |
| 128 | + |
| 129 | + /* |
| 130 | + * Is it arriving at a version of interest, or is it from a side branch |
| 131 | + * which did not contribute to the final state? |
| 132 | + */ |
| 133 | + if (!oideq(oid, &ent->oid)) |
| 134 | + return; |
| 135 | + |
| 136 | + last_modified_emit(data->lm, path, data->commit); |
| 137 | + |
| 138 | + hashmap_remove(&data->lm->paths, &ent->hashent, path); |
| 139 | + free(ent); |
| 140 | +} |
| 141 | + |
| 142 | +static void last_modified_diff(struct diff_queue_struct *q, |
| 143 | + struct diff_options *opt UNUSED, void *cbdata) |
| 144 | +{ |
| 145 | + struct last_modified_callback_data *data = cbdata; |
| 146 | + |
| 147 | + for (int i = 0; i < q->nr; i++) { |
| 148 | + struct diff_filepair *p = q->queue[i]; |
| 149 | + switch (p->status) { |
| 150 | + case DIFF_STATUS_DELETED: |
| 151 | + /* |
| 152 | + * There's no point in feeding a deletion, as it could |
| 153 | + * not have resulted in our current state, which |
| 154 | + * actually has the file. |
| 155 | + */ |
| 156 | + break; |
| 157 | + |
| 158 | + default: |
| 159 | + /* |
| 160 | + * Otherwise, we care only that we somehow arrived at |
| 161 | + * a final oid state. Note that this covers some |
| 162 | + * potentially controversial areas, including: |
| 163 | + * |
| 164 | + * 1. A rename or copy will be found, as it is the |
| 165 | + * first time the content has arrived at the given |
| 166 | + * path. |
| 167 | + * |
| 168 | + * 2. Even a non-content modification like a mode or |
| 169 | + * type change will trigger it. |
| 170 | + * |
| 171 | + * We take the inclusive approach for now, and find |
| 172 | + * anything which impacts the path. Options to tweak |
| 173 | + * the behavior (e.g., to "--follow" the content across |
| 174 | + * renames) can come later. |
| 175 | + */ |
| 176 | + mark_path(p->two->path, &p->two->oid, data); |
| 177 | + break; |
| 178 | + } |
| 179 | + } |
| 180 | +} |
| 181 | + |
| 182 | +static int last_modified_run(struct last_modified *lm) |
| 183 | +{ |
| 184 | + struct last_modified_callback_data data = { .lm = lm }; |
| 185 | + |
| 186 | + lm->rev.diffopt.output_format = DIFF_FORMAT_CALLBACK; |
| 187 | + lm->rev.diffopt.format_callback = last_modified_diff; |
| 188 | + lm->rev.diffopt.format_callback_data = &data; |
| 189 | + |
| 190 | + prepare_revision_walk(&lm->rev); |
| 191 | + |
| 192 | + while (hashmap_get_size(&lm->paths)) { |
| 193 | + data.commit = get_revision(&lm->rev); |
| 194 | + if (!data.commit) |
| 195 | + break; |
| 196 | + |
| 197 | + if (data.commit->object.flags & BOUNDARY) { |
| 198 | + diff_tree_oid(lm->rev.repo->hash_algo->empty_tree, |
| 199 | + &data.commit->object.oid, "", |
| 200 | + &lm->rev.diffopt); |
| 201 | + diff_flush(&lm->rev.diffopt); |
| 202 | + } else { |
| 203 | + log_tree_commit(&lm->rev, data.commit); |
| 204 | + } |
| 205 | + } |
| 206 | + |
| 207 | + return 0; |
| 208 | +} |
| 209 | + |
| 210 | +static int last_modified_init(struct last_modified *lm, struct repository *r, |
| 211 | + const char *prefix, int argc, const char **argv) |
| 212 | +{ |
| 213 | + hashmap_init(&lm->paths, last_modified_entry_hashcmp, NULL, 0); |
| 214 | + |
| 215 | + repo_init_revisions(r, &lm->rev, prefix); |
| 216 | + lm->rev.def = "HEAD"; |
| 217 | + lm->rev.combine_merges = 1; |
| 218 | + lm->rev.show_root_diff = 1; |
| 219 | + lm->rev.boundary = 1; |
| 220 | + lm->rev.no_commit_id = 1; |
| 221 | + lm->rev.diff = 1; |
| 222 | + lm->rev.diffopt.flags.recursive = lm->recursive || lm->tree_in_recursive; |
| 223 | + lm->rev.diffopt.flags.tree_in_recursive = lm->tree_in_recursive; |
| 224 | + |
| 225 | + if ((argc = setup_revisions(argc, argv, &lm->rev, NULL)) > 1) { |
| 226 | + error(_("unknown last-modified argument: %s"), argv[1]); |
| 227 | + return argc; |
| 228 | + } |
| 229 | + |
| 230 | + if (populate_paths_from_revs(lm) < 0) |
| 231 | + return error(_("unable to setup last-modified")); |
| 232 | + |
| 233 | + return 0; |
| 234 | +} |
| 235 | + |
| 236 | +int cmd_last_modified(int argc, const char **argv, const char *prefix, |
| 237 | + struct repository *repo) |
| 238 | +{ |
| 239 | + int ret; |
| 240 | + struct last_modified lm; |
| 241 | + |
| 242 | + const char * const last_modified_usage[] = { |
| 243 | + N_("git last-modified [-r] [-t] " |
| 244 | + "[<revision-range>] [[--] <path>...]"), |
| 245 | + NULL |
| 246 | + }; |
| 247 | + |
| 248 | + struct option last_modified_options[] = { |
| 249 | + OPT_BOOL('r', "recursive", &lm.recursive, |
| 250 | + N_("recurse into subtrees")), |
| 251 | + OPT_BOOL('t', "tree-in-recursive", &lm.tree_in_recursive, |
| 252 | + N_("recurse into subtrees and include the tree entries too")), |
| 253 | + OPT_END() |
| 254 | + }; |
| 255 | + |
| 256 | + memset(&lm, 0, sizeof(lm)); |
| 257 | + |
| 258 | + argc = parse_options(argc, argv, prefix, last_modified_options, |
| 259 | + last_modified_usage, |
| 260 | + PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT); |
| 261 | + |
| 262 | + repo_config(repo, git_default_config, NULL); |
| 263 | + |
| 264 | + if ((ret = last_modified_init(&lm, repo, prefix, argc, argv))) { |
| 265 | + if (ret > 0) |
| 266 | + usage_with_options(last_modified_usage, |
| 267 | + last_modified_options); |
| 268 | + goto out; |
| 269 | + } |
| 270 | + |
| 271 | + if ((ret = last_modified_run(&lm))) |
| 272 | + goto out; |
| 273 | + |
| 274 | +out: |
| 275 | + last_modified_release(&lm); |
| 276 | + |
| 277 | + return ret; |
| 278 | +} |
0 commit comments