Skip to content

Commit e2240a9

Browse files
To1negitster
authored andcommitted
last-modified: new subcommand to show when files were last modified
Similar to git-blame(1), introduce a new subcommand git-last-modified(1). This command shows the most recent modification to paths in a tree. It does so by expanding the tree at a given commit, taking note of the current state of each path, and then walking backwards through history looking for commits where each path changed into its final commit ID. Based-on-patch-by: Jeff King <[email protected]> Improved-by: Ævar Arnfjörð Bjarmason <[email protected]> Signed-off-by: Toon Claes <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent e813a02 commit e2240a9

File tree

11 files changed

+553
-0
lines changed

11 files changed

+553
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@
8787
/git-init-db
8888
/git-interpret-trailers
8989
/git-instaweb
90+
/git-last-modified
9091
/git-log
9192
/git-ls-files
9293
/git-ls-remote

Documentation/git-last-modified.adoc

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
git-last-modified(1)
2+
====================
3+
4+
NAME
5+
----
6+
git-last-modified - EXPERIMENTAL: Show when files were last modified
7+
8+
9+
SYNOPSIS
10+
--------
11+
[synopsis]
12+
git last-modified [--recursive] [--show-trees] [<revision-range>] [[--] <path>...]
13+
14+
DESCRIPTION
15+
-----------
16+
17+
Shows which commit last modified each of the relevant files and subdirectories.
18+
A commit renaming a path, or changing it's mode is also taken into account.
19+
20+
THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.
21+
22+
OPTIONS
23+
-------
24+
25+
-r::
26+
--recursive::
27+
Instead of showing tree entries, step into subtrees and show all entries
28+
inside them recursively.
29+
30+
-t::
31+
--show-trees::
32+
Show tree entries even when recursing into them. It has no effect
33+
without `--recursive`.
34+
35+
<revision-range>::
36+
Only traverse commits in the specified revision range. When no
37+
`<revision-range>` is specified, it defaults to `HEAD` (i.e. the whole
38+
history leading to the current commit). For a complete list of ways to
39+
spell `<revision-range>`, see the 'Specifying Ranges' section of
40+
linkgit:gitrevisions[7].
41+
42+
[--] <path>...::
43+
For each _<path>_ given, the commit which last modified it is returned.
44+
Without an optional path parameter, all files and subdirectories
45+
in path traversal the are included in the output.
46+
47+
SEE ALSO
48+
--------
49+
linkgit:git-blame[1],
50+
linkgit:git-log[1].
51+
52+
GIT
53+
---
54+
Part of the linkgit:git[1] suite

Documentation/meson.build

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ manpages = {
7474
'git-init.adoc' : 1,
7575
'git-instaweb.adoc' : 1,
7676
'git-interpret-trailers.adoc' : 1,
77+
'git-last-modified.adoc' : 1,
7778
'git-log.adoc' : 1,
7879
'git-ls-files.adoc' : 1,
7980
'git-ls-remote.adoc' : 1,

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1265,6 +1265,7 @@ BUILTIN_OBJS += builtin/hook.o
12651265
BUILTIN_OBJS += builtin/index-pack.o
12661266
BUILTIN_OBJS += builtin/init-db.o
12671267
BUILTIN_OBJS += builtin/interpret-trailers.o
1268+
BUILTIN_OBJS += builtin/last-modified.o
12681269
BUILTIN_OBJS += builtin/log.o
12691270
BUILTIN_OBJS += builtin/ls-files.o
12701271
BUILTIN_OBJS += builtin/ls-remote.o

builtin.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@ int cmd_hook(int argc, const char **argv, const char *prefix, struct repository
176176
int cmd_index_pack(int argc, const char **argv, const char *prefix, struct repository *repo);
177177
int cmd_init_db(int argc, const char **argv, const char *prefix, struct repository *repo);
178178
int cmd_interpret_trailers(int argc, const char **argv, const char *prefix, struct repository *repo);
179+
int cmd_last_modified(int argc, const char **argv, const char *prefix, struct repository *repo);
179180
int cmd_log_reflog(int argc, const char **argv, const char *prefix, struct repository *repo);
180181
int cmd_log(int argc, const char **argv, const char *prefix, struct repository *repo);
181182
int cmd_ls_files(int argc, const char **argv, const char *prefix, struct repository *repo);

builtin/last-modified.c

Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
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+
}

command-list.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ git-index-pack plumbingmanipulators
124124
git-init mainporcelain init
125125
git-instaweb ancillaryinterrogators complete
126126
git-interpret-trailers purehelpers
127+
git-last-modified plumbinginterrogators
127128
git-log mainporcelain info
128129
git-ls-files plumbinginterrogators
129130
git-ls-remote plumbinginterrogators

git.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -565,6 +565,7 @@ static struct cmd_struct commands[] = {
565565
{ "init", cmd_init_db },
566566
{ "init-db", cmd_init_db },
567567
{ "interpret-trailers", cmd_interpret_trailers, RUN_SETUP_GENTLY },
568+
{ "last-modified", cmd_last_modified, RUN_SETUP },
568569
{ "log", cmd_log, RUN_SETUP },
569570
{ "ls-files", cmd_ls_files, RUN_SETUP },
570571
{ "ls-remote", cmd_ls_remote, RUN_SETUP_GENTLY },

meson.build

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -607,6 +607,7 @@ builtin_sources = [
607607
'builtin/index-pack.c',
608608
'builtin/init-db.c',
609609
'builtin/interpret-trailers.c',
610+
'builtin/last-modified.c',
610611
'builtin/log.c',
611612
'builtin/ls-files.c',
612613
'builtin/ls-remote.c',

0 commit comments

Comments
 (0)