git-hot 0.6__py3-none-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,333 @@
1
+ .\" Copyright 1996-2026 Diomidis Spinellis
2
+ .\"
3
+ .\" Licensed under the Apache License, Version 2.0 (the "License");
4
+ .\" you may not use this file except in compliance with the License.
5
+ .\" You may obtain a copy of the License at
6
+ .\"
7
+ .\" http://www.apache.org/licenses/LICENSE-2.0
8
+ .\"
9
+ .\" Unless required by applicable law or agreed to in writing, software
10
+ .\" distributed under the License is distributed on an "AS IS" BASIS,
11
+ .\" WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ .\" See the License for the specific language governing permissions and
13
+ .\" limitations under the License.
14
+ .\"
15
+ .TH GIT-HOT 1 "May 2026" "git-hot 0.1" "Git Manual"
16
+ .SH NAME
17
+ git-hot \- report Git file and line lifetime churn
18
+ .SH SYNOPSIS
19
+ .SY git
20
+ .B hot
21
+ .RB [ \-h ]
22
+ .RB [ \-d
23
+ .IR dir ]
24
+ .RB [ \-q ]
25
+ .RB [ \-D
26
+ .IR opts ]
27
+ .RB [ \-\-color
28
+ .BR always | never ]
29
+ .RB [ \-\-color-domain
30
+ .BR churn | age | lifetime ]
31
+ .RB [ \-\-format
32
+ .IR format ]
33
+ .RI [ ref ]
34
+ .RI [[ \-\- ]
35
+ .IR path ]
36
+ .YS
37
+ .SY git-hot
38
+ .RB [ \-h ]
39
+ .RB [ \-d
40
+ .IR dir ]
41
+ .RB [ \-q ]
42
+ .RB [ \-D
43
+ .IR opts ]
44
+ .RB [ \-\-color
45
+ .BR always | never ]
46
+ .RB [ \-\-color-domain
47
+ .BR churn | age | lifetime ]
48
+ .RB [ \-\-format
49
+ .IR format ]
50
+ .RI [ ref ]
51
+ .RI [[ \-\- ]
52
+ .IR path ]
53
+ .YS
54
+ .SH DESCRIPTION
55
+ .B git hot
56
+ is a Git extension that reports how "hot" the current files or lines in a
57
+ repository are, using line lifetime and churn information derived from Git
58
+ history.
59
+ It walks the repository's topologically ordered longest commit path, streams
60
+ zero-context diffs through the code lifetime processor, and reports metrics
61
+ for the files or lines that are alive at the selected revision.
62
+ .PP
63
+ When invoked without
64
+ .IR path ,
65
+ .B git hot
66
+ prints one line per current source file, sorted by path.
67
+ The default columns are the maximum line churn in the file, the median changed
68
+ line lifetime in days, the median live line age in days, and the path.
69
+ .PP
70
+ When
71
+ .I path
72
+ is specified,
73
+ .B git hot
74
+ prints the reconstructed contents of that file, with each line preceded by its
75
+ churn count by default.
76
+ The path form follows Git's usual revision/path convention; use
77
+ .B \-\-
78
+ to disambiguate a path from a revision.
79
+ .PP
80
+ Unless quiet mode is selected,
81
+ .B git hot
82
+ reports progress on standard error.
83
+ When standard output is a terminal, output is sent through the configured Git
84
+ pager.
85
+ .SH OPTIONS
86
+ .TP
87
+ .B \-h, \-\-help
88
+ Show usage information and exit.
89
+ .TP
90
+ .BI "\-d " dir ", \-\-dir " dir
91
+ Reconstruct source files below
92
+ .I dir
93
+ with each line preceded by its churn count.
94
+ This option also leaves the normal selected output behavior in effect.
95
+ .TP
96
+ .BI "\-\-format " format
97
+ Format file or line output using a restricted Python f-string expression.
98
+ The available fields depend on whether
99
+ .B git hot
100
+ is producing file metrics or reconstructed line output; see
101
+ .BR "FORMAT STRINGS" .
102
+ .TP
103
+ .B \-q, \-\-quiet
104
+ Suppress progress output.
105
+ .TP
106
+ .BI "\-D " opts ", \-\-debug " opts
107
+ Enable debugging output selected by letters in
108
+ .IR opts .
109
+ Useful flags include
110
+ .B g
111
+ to show Git invocations,
112
+ .B H
113
+ to show commit headers,
114
+ .B D
115
+ to show diff headers,
116
+ .B E
117
+ to show diff extended headers,
118
+ .B L
119
+ to show line-of-code processing,
120
+ .B P
121
+ to show change-set pushes,
122
+ .B C
123
+ to show commit-set changes,
124
+ .B S
125
+ to show splicing operations,
126
+ .B R
127
+ to reconstruct repository contents, and
128
+ .B @
129
+ to show range headers.
130
+ .TP
131
+ .BI "\-\-color " when
132
+ Control ANSI color output.
133
+ .I when
134
+ must be
135
+ .B always
136
+ or
137
+ .BR never .
138
+ When this option is omitted, color is enabled only when standard output is a
139
+ terminal.
140
+ .TP
141
+ .BI "\-\-color-domain " domain
142
+ Choose the metric used for automatic color ranking.
143
+ .I domain
144
+ must be
145
+ .BR churn ,
146
+ .BR age ,
147
+ or
148
+ .BR lifetime .
149
+ The default is
150
+ .BR churn .
151
+ .SH ARGUMENTS
152
+ .TP
153
+ .I ref
154
+ Git revision or reference to analyze.
155
+ If omitted,
156
+ .B git hot
157
+ uses the repository's default revision selection as provided by
158
+ .BR "git log" .
159
+ .TP
160
+ .I path
161
+ Limit analysis to one path and print reconstructed line details for that path.
162
+ At most one path may be specified.
163
+ Use
164
+ .B \-\-
165
+ before the path when the path could be parsed as a revision, or when no
166
+ .I ref
167
+ is supplied.
168
+ .SH OUTPUT
169
+ .SS File Metrics
170
+ The default repository-wide output is equivalent to the format
171
+ .PP
172
+ .EX
173
+ {max(churn):5d} {days(median(changed_lifetime)):5d} {days(median(line_age)):5d} {path}
174
+ .EE
175
+ .PP
176
+ The columns are:
177
+ .TP
178
+ .B max churn
179
+ The maximum number of changes recorded for any live line in the file.
180
+ .TP
181
+ .B median changed lifetime
182
+ The median time, in rounded integer days, between changes of lines in the file.
183
+ .TP
184
+ .B median line age
185
+ The median age, in rounded integer days, of the file's live lines at the
186
+ analyzed revision.
187
+ .TP
188
+ .B path
189
+ The repository path.
190
+ .SS Path Output
191
+ For a selected
192
+ .IR path ,
193
+ the default output is equivalent to:
194
+ .PP
195
+ .EX
196
+ {churn:>{5}d} {line}
197
+ .EE
198
+ .PP
199
+ Each output line represents one live reconstructed line from the selected file.
200
+ .SH FORMAT STRINGS
201
+ .B git hot
202
+ evaluates
203
+ .B \-\-format
204
+ as a Python f-string with a restricted set of names.
205
+ The same option formats file-metric output when no path is selected, and line
206
+ output when a path is selected.
207
+ .PP
208
+ Common helper names are:
209
+ .BR days ,
210
+ .BR max ,
211
+ .BR min ,
212
+ .BR median ,
213
+ .BR mean ,
214
+ .BR quartile_rank ,
215
+ .BR color ,
216
+ .BR color_reset ,
217
+ .BR list ,
218
+ and
219
+ .BR map .
220
+ .PP
221
+ File-metric format strings may use:
222
+ .BR path ,
223
+ .BR churn ,
224
+ .BR changed_lifetime ,
225
+ .BR change_lifetime ,
226
+ .BR line_age ,
227
+ .BR line_churns ,
228
+ .BR line_change_lifetimes ,
229
+ .BR line_ages ,
230
+ .BR file_line_churns ,
231
+ .BR file_line_change_lifetimes ,
232
+ .BR file_line_ages ,
233
+ .BR repo_line_churns ,
234
+ .BR repo_line_change_lifetimes ,
235
+ and
236
+ .BR repo_line_ages .
237
+ .PP
238
+ Line format strings may use:
239
+ .BR churn ,
240
+ .BR age ,
241
+ .BR hash ,
242
+ .BR change_lifetimes ,
243
+ .BR lifetime_median ,
244
+ .BR lifetime_mean ,
245
+ .BR birthtime ,
246
+ .BR line ,
247
+ .BR file_line_churns ,
248
+ .BR file_line_ages ,
249
+ .BR file_line_change_lifetimes ,
250
+ .BR repo_line_churns ,
251
+ .BR repo_line_ages ,
252
+ and
253
+ .BR repo_line_change_lifetimes .
254
+ .PP
255
+ If the format string calls
256
+ .BR color() ,
257
+ .B git hot
258
+ assumes color is handled explicitly and appends a reset sequence when color is
259
+ enabled.
260
+ Otherwise, enabled color is applied automatically using the selected
261
+ .BR \-\-color-domain .
262
+ .SH EXAMPLES
263
+ .PP
264
+ Show hot files in the current repository:
265
+ .EX
266
+ $ git hot
267
+ .EE
268
+ .PP
269
+ Show hot files at
270
+ .BR HEAD :
271
+ .EX
272
+ $ git hot HEAD
273
+ .EE
274
+ .PP
275
+ Show line churn for one file:
276
+ .EX
277
+ $ git hot -- src/main.py
278
+ .EE
279
+ .PP
280
+ Show line ages and birth commits for one file:
281
+ .EX
282
+ $ git hot -q --format '{days(age)} {hash[:7]} {line}' -- src/main.py
283
+ .EE
284
+ .PP
285
+ Reconstruct all source files with churn prefixes below
286
+ .BR hot-tree :
287
+ .EX
288
+ $ git hot --dir hot-tree HEAD
289
+ .EE
290
+ .SH FILES
291
+ .TP
292
+ .I git-hot
293
+ The executable Git extension.
294
+ When installed on
295
+ .BR PATH ,
296
+ Git can invoke it as
297
+ .BR "git hot" .
298
+ .TP
299
+ .I daglp
300
+ Helper program used to compute the longest path through the repository commit
301
+ DAG.
302
+ .SH NOTES
303
+ .B git hot
304
+ expects to run inside a Git repository and requires
305
+ .B daglp
306
+ to be available on
307
+ .BR PATH .
308
+ It uses Git commands including
309
+ .BR "git log" ,
310
+ .BR "git show" ,
311
+ and
312
+ .BR "git diff" ,
313
+ with rename and copy detection enabled.
314
+ .PP
315
+ Only source-code files are reported in file-metric output.
316
+ Binary files and deleted files are skipped.
317
+ .SH EXIT STATUS
318
+ .TP
319
+ .B 0
320
+ Successful completion.
321
+ .TP
322
+ .B 1
323
+ A processing error occurred, such as an invalid output format or a failed
324
+ helper command.
325
+ .TP
326
+ .B 2
327
+ Command-line usage error reported by the argument parser.
328
+ .SH SEE ALSO
329
+ .BR git (1),
330
+ .BR git-log (1),
331
+ .BR git-diff (1),
332
+ .BR git-show (1),
333
+ .BR less (1)
@@ -0,0 +1,5 @@
1
+ """Git extension for reporting source code line lifetime and churn."""
2
+
3
+ from .lifetime import VERSION
4
+
5
+ __all__ = ["VERSION"]
@@ -0,0 +1,8 @@
1
+ """Run the lifetime command as a module."""
2
+
3
+ import sys
4
+
5
+ from .lifetime import main
6
+
7
+ if __name__ == "__main__":
8
+ sys.exit(main())
@@ -0,0 +1,226 @@
1
+ /*
2
+ * Copyright 1996-2026 Diomidis Spinellis
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ *
16
+ *
17
+ * Given as input a topologically sorted list of each commit's parents,
18
+ * output the longest path of the DAG from the beginning (the oldest commit)
19
+ * to the end (the newest one).
20
+ * See https://en.wikipedia.org/wiki/Longest_path_problem#Acyclic_graphs_and_critical_paths
21
+ * The input should come from
22
+ * git log --topo-order --pretty=format:'%H %at %P'
23
+ * The output is "SHA identifier" lines.
24
+ *
25
+ */
26
+
27
+ use std::collections::HashMap;
28
+ use std::env;
29
+ use std::fs::File;
30
+ use std::io::{self, BufRead, BufReader};
31
+ use std::process;
32
+
33
+ const DEBUG: bool = false;
34
+
35
+ #[derive(Default)]
36
+ struct Vertex {
37
+ name: String,
38
+ // Author/commit time, SHA, bug ids, or any other tracked commit element.
39
+ identifier: String,
40
+ // Longest path ending here; None means it has not yet been calculated.
41
+ max_length: Option<i32>,
42
+ // Parent commits of this commit.
43
+ edges: Vec<String>,
44
+ // The next vertex in the recorded longest path.
45
+ lp_from: Option<String>,
46
+ }
47
+
48
+ impl Vertex {
49
+ fn new(name: &str, identifier: &str) -> Self {
50
+ Self {
51
+ name: name.to_string(),
52
+ identifier: identifier.to_string(),
53
+ max_length: None,
54
+ edges: Vec::new(),
55
+ lp_from: None,
56
+ }
57
+ }
58
+ }
59
+
60
+ fn get_vertex<'a>(
61
+ vertices: &'a mut HashMap<String, Vertex>,
62
+ name: &str,
63
+ identifier: &str,
64
+ ) -> &'a mut Vertex {
65
+ // Return a node by name, adding it to the map if needed.
66
+ let vertex = vertices
67
+ .entry(name.to_string())
68
+ .or_insert_with(|| Vertex::new(name, identifier));
69
+ if !identifier.is_empty() {
70
+ vertex.identifier = identifier.to_string();
71
+ }
72
+ vertex
73
+ }
74
+
75
+ fn reader_from_args() -> Box<dyn BufRead> {
76
+ let args: Vec<String> = env::args().collect();
77
+ if args.len() == 2 {
78
+ match File::open(&args[1]) {
79
+ Ok(file) => Box::new(BufReader::new(file)),
80
+ Err(error) => {
81
+ eprintln!("{}: {error}", args[1]);
82
+ process::exit(1);
83
+ }
84
+ }
85
+ } else {
86
+ Box::new(BufReader::new(io::stdin()))
87
+ }
88
+ }
89
+
90
+ fn read_graph(
91
+ reader: Box<dyn BufRead>,
92
+ ) -> io::Result<(HashMap<String, Vertex>, Vec<String>, Option<String>)> {
93
+ let mut vertices = HashMap::new();
94
+ let mut order = Vec::new();
95
+ let mut end = None;
96
+
97
+ for line in reader.lines() {
98
+ let line = line?;
99
+ let mut parts = line.split_whitespace();
100
+ let Some(node_name) = parts.next() else {
101
+ continue;
102
+ };
103
+ let identifier = parts.next().unwrap_or("");
104
+
105
+ // Read node, adding it to the map if needed.
106
+ get_vertex(&mut vertices, node_name, identifier);
107
+ order.push(node_name.to_string());
108
+ if end.is_none() {
109
+ end = Some(node_name.to_string());
110
+ }
111
+
112
+ // Create edges from this commit to its parents.
113
+ for parent_name in parts {
114
+ if DEBUG {
115
+ eprintln!("{parent_name} parent of {node_name}");
116
+ }
117
+ get_vertex(&mut vertices, parent_name, "");
118
+ vertices
119
+ .get_mut(node_name)
120
+ .expect("current vertex exists")
121
+ .edges
122
+ .push(parent_name.to_string());
123
+ }
124
+ }
125
+
126
+ Ok((vertices, order, end))
127
+ }
128
+
129
+ fn calculate_max_lengths(vertices: &mut HashMap<String, Vertex>, order: &[String]) {
130
+ // Parent vertices that do not appear as full input lines are roots.
131
+ for vertex in vertices.values_mut() {
132
+ vertex.max_length = Some(0);
133
+ }
134
+
135
+ // Record the maximum path associated with each vertex. The input is
136
+ // topologically sorted newest to oldest, so reverse order gives parents
137
+ // before children and avoids recursion over long commit histories.
138
+ for name in order.iter().rev() {
139
+ let edges = vertices
140
+ .get(name)
141
+ .map(|vertex| vertex.edges.clone())
142
+ .unwrap_or_default();
143
+ let max_path = edges
144
+ .iter()
145
+ .filter_map(|edge| vertices.get(edge).and_then(|vertex| vertex.max_length))
146
+ .max()
147
+ .unwrap_or(-1);
148
+ let length = max_path + 1;
149
+ if DEBUG {
150
+ eprintln!("max_length({name}) = {length}");
151
+ }
152
+ if let Some(vertex) = vertices.get_mut(name) {
153
+ vertex.max_length = Some(length);
154
+ }
155
+ }
156
+ }
157
+
158
+ fn mark_longest_path(
159
+ vertices: &mut HashMap<String, Vertex>,
160
+ order: &[String],
161
+ end: &str,
162
+ ) -> Option<String> {
163
+ // Calculate the maximum paths of all vertices.
164
+ calculate_max_lengths(vertices, order);
165
+
166
+ // Obtain and record the longest path. The strict comparison preserves
167
+ // the original first-parent tie behavior.
168
+ let mut current = Some(end.to_string());
169
+ let mut start = Some(end.to_string());
170
+ while let Some(name) = current {
171
+ let edges = vertices
172
+ .get(&name)
173
+ .map(|vertex| vertex.edges.clone())
174
+ .unwrap_or_default();
175
+ let mut longest = None;
176
+ let mut longest_length = None;
177
+ for edge in edges {
178
+ let length = vertices.get(&edge).and_then(|vertex| vertex.max_length);
179
+ if longest.is_none() || length > longest_length {
180
+ longest = Some(edge);
181
+ longest_length = length;
182
+ }
183
+ }
184
+
185
+ if let Some(parent) = longest {
186
+ if let Some(vertex) = vertices.get_mut(&parent) {
187
+ vertex.lp_from = Some(name);
188
+ }
189
+ start = Some(parent.clone());
190
+ current = Some(parent);
191
+ } else {
192
+ current = None;
193
+ }
194
+ }
195
+
196
+ start
197
+ }
198
+
199
+ fn print_longest_path(vertices: &HashMap<String, Vertex>, start: Option<String>) {
200
+ // Display the longest path.
201
+ let mut current = start;
202
+ while let Some(name) = current {
203
+ let vertex = vertices.get(&name).expect("path vertex exists");
204
+ println!("{} {}", vertex.name, vertex.identifier);
205
+ current = vertex.lp_from.clone();
206
+ }
207
+ }
208
+
209
+ fn run() -> io::Result<()> {
210
+ let reader = reader_from_args();
211
+ let (mut vertices, order, end) = read_graph(reader)?;
212
+ let Some(end) = end else {
213
+ return Ok(());
214
+ };
215
+
216
+ let start = mark_longest_path(&mut vertices, &order, &end);
217
+ print_longest_path(&vertices, start);
218
+ Ok(())
219
+ }
220
+
221
+ fn main() {
222
+ if let Err(error) = run() {
223
+ eprintln!("{error}");
224
+ process::exit(1);
225
+ }
226
+ }