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